mirror of
https://github.com/mihonapp/mihon.git
synced 2025-07-30 03:15:54 +02:00
Compare commits
192 Commits
Author | SHA1 | Date | |
---|---|---|---|
85791a9336 | |||
a4eba50cfd | |||
f48b2681e3 | |||
ab46bd56b0 | |||
c23506e887 | |||
9ad67a7b7d | |||
7a1b6142df | |||
478256d766 | |||
4d92caacef | |||
fd45de5c58 | |||
bcaa9674fe | |||
40aa3b7e18 | |||
5aea21a194 | |||
b5e118e2b4 | |||
dfec0e45ed | |||
ff2a4e6952 | |||
7660751f7f | |||
78b9ac4766 | |||
d5c75571dc | |||
16b9c459ab | |||
41c060e28b | |||
a3090e62f5 | |||
39b7024be0 | |||
d019c5999b | |||
20264eecb9 | |||
cc55453076 | |||
6cab2427f5 | |||
511bcc9197 | |||
00ac632d8f | |||
649209890d | |||
f2fca0f13d | |||
4084d5e69a | |||
e8beb7103c | |||
0e4ce0f1ae | |||
c42d517f6b | |||
356cd4ef52 | |||
88619145d8 | |||
6ba779fb7a | |||
8bd965267c | |||
7f76ffa5cb | |||
4acc7cee3d | |||
be28e0b559 | |||
116fec208b | |||
fece92e15a | |||
dce3049446 | |||
fcd6fe5d8a | |||
a69a833716 | |||
697b082591 | |||
b2d58e04d2 | |||
8bfc5f0450 | |||
a252a8acee | |||
447ee4bd09 | |||
3cd6382795 | |||
5d1134dfa8 | |||
05e7b0dc22 | |||
c0647c3110 | |||
ef84ed4982 | |||
a1e83b9f19 | |||
4ce4ee3c00 | |||
0d62aedfbb | |||
b7c2890250 | |||
ae97bb0445 | |||
117fd4bd0f | |||
bd424ce460 | |||
1dddba7f25 | |||
7fd75b7501 | |||
423f07033e | |||
ef9c457681 | |||
a6d4a3b785 | |||
2e487f8a3f | |||
2423a70abd | |||
13d39fc942 | |||
b7547a8458 | |||
8931dbb657 | |||
52416ff3a8 | |||
3dbfee91f6 | |||
09d4901781 | |||
62955e7385 | |||
1ef7722504 | |||
24bb2f02dc | |||
627698d81f | |||
d4c8480dee | |||
015e8deb79 | |||
714aa4b4ba | |||
8d5f798591 | |||
e65f59b3df | |||
341c3d179e | |||
67128937ca | |||
d9ea621e54 | |||
fb35d7af59 | |||
c254aa6fcc | |||
37d30eb887 | |||
49cdcc644c | |||
07e5525c74 | |||
776194f5b2 | |||
ed80ee98a7 | |||
040bac3da2 | |||
9df721d158 | |||
c50ede8b2c | |||
ba0907ae59 | |||
e9dce32a98 | |||
535cc0d81e | |||
5801297d78 | |||
51a33a47cd | |||
01a1a9ebab | |||
438bad9649 | |||
fe3b36caeb | |||
83588e14d9 | |||
64b1c9636b | |||
db0c1b2634 | |||
568c4d8c8e | |||
d645507eeb | |||
3548112ab2 | |||
0cb042cd93 | |||
0eadc028b6 | |||
82f3677168 | |||
70ed49e478 | |||
3c67a36b60 | |||
e5621246ec | |||
cb71d44024 | |||
7e3ea9074c | |||
e2cf157857 | |||
60890147c3 | |||
64c95305b9 | |||
feddd9285d | |||
d1b393965f | |||
e31a39b9d5 | |||
98fc028d39 | |||
88fd799a30 | |||
ef937f277e | |||
c3fb5af3fc | |||
859e8deb02 | |||
932c92412c | |||
05771ddf6d | |||
848d387ec4 | |||
ac6b4235b9 | |||
ab73e98075 | |||
aecdd04e04 | |||
e5cdf74587 | |||
8d25ce7323 | |||
8deca3b63a | |||
9b967177c5 | |||
4dfb3cc972 | |||
73e5e9ecd9 | |||
653b7ffcd0 | |||
8791b72cb1 | |||
d961492380 | |||
07de367476 | |||
31d96c2bf0 | |||
fb8aafb69f | |||
3d58b78062 | |||
ec5e6958ef | |||
71bd5fe367 | |||
6385c71c72 | |||
d43255e688 | |||
3527dedc99 | |||
de50f53be4 | |||
f2e4b2fc99 | |||
e6f3cd03bb | |||
a1e31549a2 | |||
71d225c562 | |||
7c23212850 | |||
fdf178d4df | |||
04ebca8413 | |||
edeee54fb2 | |||
a906e9b302 | |||
fff72b61df | |||
74381ef59e | |||
64f95af3e5 | |||
85a1eb75c9 | |||
597cec3064 | |||
b03ebc1fa4 | |||
6c53bb4d51 | |||
fb7a458747 | |||
db25a9ae4f | |||
c69420373a | |||
2b8347f899 | |||
281a3911f6 | |||
9b77dd9a2b | |||
cb8cff3179 | |||
3db85c7274 | |||
b41ac355a0 | |||
88d9ffe92e | |||
5113c78ab6 | |||
3854995ef2 | |||
36e14b951a | |||
9299a4beff | |||
d681bea395 | |||
0f3f1e9226 | |||
79ab492a5b | |||
62db4bb09d | |||
7be2cbb75b |
.github
CONTRIBUTING.mdREADME.mdapp
build.gradle.ktsproguard-rules.pro
build.gradle.ktssrc
main
AndroidManifest.xml
java
eu
kanade
tachiyomi
App.ktAppModule.ktMigrations.kt
data
backup
BackupRestoreService.kt
legacy
coil
database
download
library
notification
preference
track
updater
extension
network
interceptor
source
ui
base
activity
controller
presenter
browse
BrowseController.kt
extension
migration
search
sources
source
category
download
library
ChangeMangaCategoriesDialog.ktChangeMangaCoverDialog.ktDeleteLibraryMangasDialog.ktLibraryCategoryView.ktLibraryController.ktLibraryItem.ktLibraryPresenter.ktLibrarySettingsSheet.ktLibrarySort.kt
setting
main
manga
MangaController.ktMangaPresenter.kt
chapter
ChapterDownloadView.ktDeleteChaptersDialog.ktDownloadCustomChaptersDialog.ktSetChapterSettingsDialog.kt
base
info
track
more
reader
ReaderActivity.ktReaderNavigationOverlayView.ktReaderPageSheet.ktReaderPresenter.ktReaderSeekBar.kt
loader
viewer
recent
history
updates
security
setting
SettingsAdvancedController.ktSettingsBackupController.ktSettingsController.ktSettingsDownloadController.ktSettingsGeneralController.ktSettingsLibraryController.ktSettingsTrackingController.kt
track
webview
util
chapter
lang
preference
storage
system
view
widget
res
anim
color-night-v31
color-v31
color
background_color_chip_state.xmlbutton_action_selector.xmldraggable_card_foreground.xmllibrary_item_background.xmllibrary_item_foreground.xmlnav_selector.xmlripple_toolbar_fainter.xmlsource_comfortable_item_title.xmltabs_selector.xmltabs_selector_background.xmltext_input_stroke.xml
drawable-v26
drawable
ic_book_24dp.xmlic_close_24dp.xmlic_delete_24dp.xmlic_delete_sweep_24dp.xmlic_folder_24dp.xmlic_glasses_24dp.xmlic_more_vert_24.xmlic_pause_24dp.xmlic_photo_24dp.xmlic_play_arrow_24dp.xmlic_refresh_24dp.xmlic_share_24dp.xmlic_tachi.xmlic_tachi_splash.xmllibrary_item_selector.xmllibrary_item_selector_overlay.xmllist_item_selector.xmllist_item_selector_background.xmlreader_seekbar_background.xmlreader_seekbar_ripple.xmlripple_background.xmlripple_regular.xmlsc_collections_bookmark_48dp.xmlsc_explore_48dp.xmlsc_history_48dp.xmlsc_new_releases_48dp.xmlselectable_item_background.xmltab_indicator.xml
layout-sw720dp
layout
action_toolbar.xmlcategories_item.xmlchapter_download_view.xmlcommon_dialog_with_checkbox.xmlcommon_spinner_item.xmlcommon_tabbed_sheet.xmldialog_quadstatemultichoice_item.xmldialog_stub_quadstatemultichoice.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.xmllibrary_controller.xmllibrary_grid_recycler.xmllibrary_list_recycler.xmlmain_activity.xmlmain_activity_fab.xmlmain_activity_toolbar.xmlmanga_chapters_header.xmlmanga_info_header.xmlmd_listitem_quadstatemultichoice.xmlnavigation_view_checkbox.xmlnavigation_view_checkedtext.xmlnavigation_view_group.xmlnavigation_view_radio.xmlnavigation_view_spinner.xmlnavigation_view_text.xmlpref_account_login.xmlpref_more_header.xmlpref_settings.xmlreader_activity.xmlreader_page_sheet.xmlreader_transition_view.xmlsettings_search_controller_card.xmlsource_comfortable_grid_item.xmlsource_compact_grid_item.xmlsource_filter_sheet.xmlsource_list_item.xmlsource_main_controller_card_item.xmlsource_recycler_autofit.xmlspinner_preference.xmltrack_controller.xmltrack_item.xmltrack_search_dialog.xmltrack_search_item.xmlwebview_activity.xml
menu
browse_extensions.xmlbrowse_migrate.xmlbrowse_sources.xmlcategory_selection.xmlchapter_download.xmlchapter_selection.xmlextension_details.xmlgeneric_selection.xmlglobal_search.xmlhistory.xmllibrary.xmllibrary_selection.xmlmanga.xmlmigration.xmlreader.xmlsettings_main.xmlsettings_tracking.xmlsource_browse.xmltrack_item.xmltrack_item_date.xmltrack_search.xmlupdates.xmlupdates_chapter_selection.xmlwebview.xml
values-am
values-ar
values-b+es+419
values-bg
values-bn
values-ca
values-cs
values-cv
values-de
values-el
values-eo
values-es
values-fa
values-fi
values-fil
values-fr
values-gl
values-he
values-hi
values-hr
values-hu
values-in
values-it
values-ja
values-jv
values-ka-rGE
values-kn
values-ko
values-lv
values-mr
values-ms
values-nb-rNO
values-ne
values-night
values-nl
values-pl
values-pt-rBR
values-pt
values-ro
values-ru
values-sah
values-sc
values-sr
values-sv
values-sw600dp-port
values-th
values-ti
values-tr
values-uk
values-uz
values-v26
values-v27
values-v31
values-vi
values-zh-rCN
values-zh-rTW
values
xml
test
java
eu
kanade
tachiyomi
data
buildSrc/src/main/kotlin
gradle/wrapper
settings.gradle.kts
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.0)
|
- To the latest version of the app (stable is v0.12.1)
|
||||||
- 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
|
||||||
|
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
name: "🐞 Bug report"
|
|
||||||
about: Report a bug
|
|
||||||
title: "[Bug] <Write short description here>"
|
|
||||||
labels: "bug"
|
|
||||||
---
|
|
||||||
|
|
||||||
**PLEASE READ THIS**
|
|
||||||
|
|
||||||
I acknowledge that:
|
|
||||||
|
|
||||||
- I have updated:
|
|
||||||
- To the latest version of the app (stable is v0.11.0)
|
|
||||||
- All extensions
|
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
|
||||||
- I will fill out the title and the information in this template
|
|
||||||
|
|
||||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Device information
|
|
||||||
* Tachiyomi version: ?
|
|
||||||
* Android version: ?
|
|
||||||
* Device: ?
|
|
||||||
|
|
||||||
## Steps to reproduce
|
|
||||||
1. First step
|
|
||||||
2. Second step
|
|
||||||
|
|
||||||
### Expected behavior
|
|
||||||
This should happen.
|
|
||||||
|
|
||||||
### Actual behavior
|
|
||||||
This happened instead.
|
|
||||||
|
|
||||||
## Other details
|
|
||||||
Additional details and attachments.
|
|
||||||
|
|
||||||
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
|
13
.github/ISSUE_TEMPLATE/config.yml
vendored
13
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Tachiyomi help website
|
- name: ⚠️ Extension/source issue
|
||||||
|
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||||
|
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
||||||
|
- name: 📦 Tachiyomi extensions
|
||||||
|
url: https://tachiyomi.org/extensions
|
||||||
|
about: List of all available extensions with download links
|
||||||
|
- name: 🖥️ Tachiyomi website
|
||||||
url: https://tachiyomi.org/help/
|
url: https://tachiyomi.org/help/
|
||||||
about: Common questions are answered here.
|
about: Guides, troubleshooting, and answers to common questions
|
||||||
- name: Tachiyomi extensions GitHub repository
|
|
||||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
about: Issues about an extension/source/catalogue should be opened here instead.
|
|
||||||
|
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,29 +0,0 @@
|
|||||||
---
|
|
||||||
name: "🌟 Feature request"
|
|
||||||
about: Suggest a feature to improve Tachiyomi
|
|
||||||
title: "[Feature Request] <Write short description here>"
|
|
||||||
labels: "feature"
|
|
||||||
---
|
|
||||||
|
|
||||||
**PLEASE READ THIS**
|
|
||||||
|
|
||||||
I acknowledge that:
|
|
||||||
|
|
||||||
- I have updated:
|
|
||||||
- To the latest version of the app (stable is v0.11.0)
|
|
||||||
- All extensions
|
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
|
||||||
- I will fill out the title and the information in this template
|
|
||||||
|
|
||||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Why/User Benefit/User Problem
|
|
||||||
(explain why this feature should be added)
|
|
||||||
|
|
||||||
## What/Requirements
|
|
||||||
(explain how this feature would behave)
|
|
106
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
Normal file
106
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
name: 🐞 Issue report
|
||||||
|
description: Report an issue in Tachiyomi
|
||||||
|
labels: [Bug]
|
||||||
|
body:
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||||
|
required: true
|
||||||
|
- label: I have updated the app to version **[0.12.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
|
required: true
|
||||||
|
- label: I have updated all installed extensions.
|
||||||
|
required: true
|
||||||
|
- label: I will fill out all of the requested information in this form.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: tachiyomi-version
|
||||||
|
attributes:
|
||||||
|
label: Tachiyomi version
|
||||||
|
description: You can find your Tachiyomi version in **More → About**.
|
||||||
|
placeholder: |
|
||||||
|
Example: "0.11.1"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: android-version
|
||||||
|
attributes:
|
||||||
|
label: Android version
|
||||||
|
description: You can find this somewhere in your Android settings.
|
||||||
|
placeholder: |
|
||||||
|
Example: "Android 11"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: device
|
||||||
|
attributes:
|
||||||
|
label: Device
|
||||||
|
description: List your device and model.
|
||||||
|
placeholder: |
|
||||||
|
Example: "Google Pixel 5"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce-steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Provide an example of the issue.
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
1. First step
|
||||||
|
2. Second step
|
||||||
|
3. Issue here
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: Explain what you should expect to happen.
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
"This should happen..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behavior
|
||||||
|
attributes:
|
||||||
|
label: Actual behavior
|
||||||
|
description: Explain what actually happens.
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
"This happened instead..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: crash-logs
|
||||||
|
attributes:
|
||||||
|
label: Crash logs
|
||||||
|
description: |
|
||||||
|
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
|
||||||
|
placeholder: |
|
||||||
|
You can paste the crash logs in pure text or upload it as an attachment.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: other-details
|
||||||
|
attributes:
|
||||||
|
label: Other details
|
||||||
|
placeholder: |
|
||||||
|
Additional details and attachments.
|
39
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
Normal file
39
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
name: ⭐ Feature request
|
||||||
|
description: Suggest a feature to improve Tachiyomi
|
||||||
|
labels: [Feature request]
|
||||||
|
body:
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
- label: I have updated the app to version **[0.12.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
|
required: true
|
||||||
|
- label: I will fill out all of the requested information in this form.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: feature-description
|
||||||
|
attributes:
|
||||||
|
label: Describe your suggested feature
|
||||||
|
description: How can Tachiyomi be improved?
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
"It should work like this..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: other-details
|
||||||
|
attributes:
|
||||||
|
label: Other details
|
||||||
|
placeholder: |
|
||||||
|
Additional details and attachments.
|
8
.github/ISSUE_TEMPLATE/source_issue.md
vendored
8
.github/ISSUE_TEMPLATE/source_issue.md
vendored
@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
name: "Extension/source/catalogue issue"
|
|
||||||
about: "Do not open an issue here. See https://github.com/tachiyomiorg/tachiyomi-extensions"
|
|
||||||
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/tachiyomiorg/tachiyomi-extensions"
|
|
||||||
labels: "catalog, invalid"
|
|
||||||
---
|
|
||||||
|
|
||||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@ -22,7 +22,6 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build app
|
name: Build app
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -61,6 +60,8 @@ jobs:
|
|||||||
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
|
||||||
|
16
.github/workflows/issue_closer.yml
vendored
16
.github/workflows/issue_closer.yml
vendored
@ -13,16 +13,6 @@ jobs:
|
|||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
rules: |
|
rules: |
|
||||||
[
|
[
|
||||||
{
|
|
||||||
"type": "title",
|
|
||||||
"regex": ".*THIS ISSUE IS IN THE WRONG REPO.*",
|
|
||||||
"message": "It was not opened in the correct repo, as the template mentioned."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "title",
|
|
||||||
"regex": ".*<Write short description here>*",
|
|
||||||
"message": "The description in the title was not filled out."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "body",
|
"type": "body",
|
||||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||||
@ -32,5 +22,11 @@ jobs:
|
|||||||
"type": "body",
|
"type": "body",
|
||||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||||
"message": "Requested information in the template was not filled out."
|
"message": "Requested information in the template was not filled out."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "both",
|
||||||
|
"regex": ".*(aniyomi|anime).*",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
2
.github/workflows/issue_moderator.yml
vendored
2
.github/workflows/issue_moderator.yml
vendored
@ -9,6 +9,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Moderate issues
|
- name: Moderate issues
|
||||||
uses: tachiyomiorg/issue-moderator-action@v1.0
|
uses: tachiyomiorg/issue-moderator-action@v1.1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
@ -26,7 +26,7 @@ When creating a fork, remember to:
|
|||||||
- To avoid confusion with the main app:
|
- To avoid confusion with the main app:
|
||||||
- Change the app name
|
- Change the app name
|
||||||
- Change the app icon
|
- Change the app icon
|
||||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt)
|
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt)
|
||||||
- To avoid installation conflicts:
|
- To avoid installation conflicts:
|
||||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||||
|
11
README.md
11
README.md
@ -1,6 +1,6 @@
|
|||||||
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
||||||
|-------|----------|---------|------------|---------|
|
|-------|----------|---------|------------|---------|
|
||||||
|  | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
|  | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
|
|
||||||
# Tachiyomi
|
# Tachiyomi
|
||||||
@ -11,10 +11,10 @@ Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
Features include:
|
Features include:
|
||||||
* Online reading from [a variety of sources](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
* Online reading from a variety of sources
|
||||||
* Local reading of downloaded manga
|
* Local reading of downloaded content
|
||||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
|
||||||
* Categories to organize your library
|
* Categories to organize your library
|
||||||
* Light and dark themes
|
* Light and dark themes
|
||||||
* Schedule updating your library for new chapters
|
* Schedule updating your library for new chapters
|
||||||
@ -23,7 +23,7 @@ Features include:
|
|||||||
## Download
|
## Download
|
||||||
Get the app from our [releases page](https://github.com/tachiyomiorg/tachiyomi/releases).
|
Get the app from our [releases page](https://github.com/tachiyomiorg/tachiyomi/releases).
|
||||||
|
|
||||||
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/tachiyomiorg/android-app-preview/releases).
|
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/tachiyomiorg/tachiyomi-preview/releases).
|
||||||
|
|
||||||
## Issues, Feature Requests and Contributing
|
## Issues, Feature Requests and Contributing
|
||||||
|
|
||||||
@ -44,7 +44,6 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
* For large logs use http://pastebin.com/ (or similar)
|
|
||||||
* Don't group unrelated requests into one issue
|
* Don't group unrelated requests into one issue
|
||||||
|
|
||||||
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
||||||
|
@ -18,18 +18,19 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
|||||||
|
|
||||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||||
|
|
||||||
|
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion(AndroidConfig.compileSdk)
|
compileSdk = AndroidConfig.compileSdk
|
||||||
buildToolsVersion(AndroidConfig.buildTools)
|
|
||||||
ndkVersion = AndroidConfig.ndk
|
ndkVersion = AndroidConfig.ndk
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
minSdkVersion(AndroidConfig.minSdk)
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdkVersion(AndroidConfig.targetSdk)
|
targetSdk = AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode = 62
|
versionCode = 67
|
||||||
versionName = "0.11.0"
|
versionName = "0.12.1"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@ -39,15 +40,18 @@ android {
|
|||||||
// Please disable ACRA or use your own instance in forked versions of the project
|
// Please disable ACRA or use your own instance in forked versions of the project
|
||||||
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
||||||
|
|
||||||
multiDexEnabled = true
|
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
|
abiFilters += SUPPORTED_ABIS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
splits {
|
||||||
viewBinding = true
|
abi {
|
||||||
|
isEnable = false
|
||||||
|
reset()
|
||||||
|
include(*SUPPORTED_ABIS.toTypedArray())
|
||||||
|
isUniversalApk = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@ -101,7 +105,11 @@ android {
|
|||||||
includeInApk = false
|
includeInApk = false
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
disable("MissingTranslation", "ExtraTranslation")
|
disable("MissingTranslation", "ExtraTranslation")
|
||||||
isAbortOnError = false
|
isAbortOnError = false
|
||||||
isCheckReleaseBuilds = false
|
isCheckReleaseBuilds = false
|
||||||
@ -119,21 +127,27 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||||
|
|
||||||
|
val coroutinesVersion = "1.5.1"
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||||
|
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
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-alpha01")
|
||||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha01")
|
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
||||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
||||||
implementation("androidx.browser:browser:1.3.0")
|
implementation("androidx.browser:browser:1.3.0")
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta02")
|
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.6.0-beta01")
|
implementation("androidx.core:core-ktx:1.7.0-alpha01")
|
||||||
implementation("androidx.multidex:multidex:2.0.1")
|
implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.0")
|
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||||
|
|
||||||
val lifecycleVersion = "2.4.0-alpha01"
|
val lifecycleVersion = "2.4.0-alpha01"
|
||||||
@ -142,10 +156,10 @@ dependencies {
|
|||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation("androidx.work:work-runtime-ktx:2.7.0-alpha03")
|
implementation("androidx.work:work-runtime-ktx:2.6.0-beta01")
|
||||||
|
|
||||||
// UI library
|
// UI library
|
||||||
implementation("com.google.android.material:material:1.4.0-beta01")
|
implementation("com.google.android.material:material:1.5.0-alpha01")
|
||||||
|
|
||||||
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
||||||
|
|
||||||
@ -163,13 +177,13 @@ dependencies {
|
|||||||
implementation("com.squareup.okio:okio:2.10.0")
|
implementation("com.squareup.okio:okio:2.10.0")
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
implementation("org.conscrypt:conscrypt-android:2.5.1")
|
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
val kotlinSerializationVersion = "1.2.0"
|
val kotlinSerializationVersion = "1.2.2"
|
||||||
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")
|
||||||
implementation("com.google.code.gson:gson:2.8.6")
|
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
|
||||||
@ -181,13 +195,13 @@ dependencies {
|
|||||||
implementation("com.github.junrar:junrar:7.4.0")
|
implementation("com.github.junrar:junrar:7.4.0")
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation("org.jsoup:jsoup:1.13.1")
|
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")
|
||||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||||
implementation("com.github.requery:sqlite-android:3.35.5")
|
implementation("com.github.requery:sqlite-android:3.36.0")
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
||||||
@ -201,14 +215,14 @@ dependencies {
|
|||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
|
||||||
// Image library
|
// Image library
|
||||||
val coilVersion = "1.2.1"
|
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")
|
||||||
|
|
||||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
||||||
exclude(module = "image-decoder")
|
exclude(module = "image-decoder")
|
||||||
}
|
}
|
||||||
implementation("com.github.tachiyomiorg:image-decoder:7a44c9b")
|
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||||
@ -228,21 +242,14 @@ dependencies {
|
|||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
||||||
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
||||||
|
|
||||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
|
||||||
val materialDialogsVersion = "3.1.1"
|
|
||||||
implementation("com.afollestad.material-dialogs:core:$materialDialogsVersion")
|
|
||||||
implementation("com.afollestad.material-dialogs:input:$materialDialogsVersion")
|
|
||||||
implementation("com.afollestad.material-dialogs:datetime:$materialDialogsVersion")
|
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
implementation("com.bluelinelabs:conductor:2.1.5")
|
val conductorVersion = "3.0.0"
|
||||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
||||||
exclude(group = "com.android.support")
|
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
||||||
}
|
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
||||||
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
|
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
val flowbindingVersion = "1.0.0"
|
val flowbindingVersion = "1.2.0"
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||||
@ -259,15 +266,8 @@ dependencies {
|
|||||||
|
|
||||||
val robolectricVersion = "3.1.4"
|
val robolectricVersion = "3.1.4"
|
||||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
||||||
testImplementation("org.robolectric:shadows-multidex:$robolectricVersion")
|
|
||||||
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
||||||
|
|
||||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
|
||||||
|
|
||||||
val coroutinesVersion = "1.4.3"
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
||||||
}
|
}
|
||||||
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@ -4,6 +4,7 @@
|
|||||||
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
||||||
-keep,allowoptimization class androidx.preference.** { *; }
|
-keep,allowoptimization class androidx.preference.** { *; }
|
||||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||||
|
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
||||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||||
-keep,allowoptimization class okio.** { public protected *; }
|
-keep,allowoptimization class okio.** { public protected *; }
|
||||||
-keep,allowoptimization class rx.** { public protected *; }
|
-keep,allowoptimization class rx.** { public protected *; }
|
||||||
@ -11,6 +12,7 @@
|
|||||||
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||||
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
||||||
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
||||||
|
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||||
|
|
||||||
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
|
@ -23,8 +23,7 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="false"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@ -32,12 +31,15 @@
|
|||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.Base"
|
android:theme="@style/Theme.Tachiyomi"
|
||||||
|
android:supportsRtl="true"
|
||||||
android:networkSecurityConfig="@xml/network_security_config">
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/Theme.Splash">
|
android:theme="@style/Theme.Tachiyomi.SplashScreen"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@ -51,7 +53,8 @@
|
|||||||
android:name=".ui.main.DeepLinkActivity"
|
android:name=".ui.main.DeepLinkActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
android:label="@string/action_global_search">
|
android:label="@string/action_global_search"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||||
@ -72,9 +75,11 @@
|
|||||||
android:name="android.app.searchable"
|
android:name="android.app.searchable"
|
||||||
android:resource="@xml/searchable" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@ -82,15 +87,26 @@
|
|||||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
android:resource="@xml/s_pen_actions"/>
|
android:resource="@xml/s_pen_actions"/>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.security.UnlockActivity"
|
android:name=".ui.security.UnlockActivity"
|
||||||
android:theme="@style/Theme.Base" />
|
android:theme="@style/Theme.Tachiyomi"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.webview.WebViewActivity"
|
android:name=".ui.webview.WebViewActivity"
|
||||||
android:configChanges="uiMode|orientation|screenSize" />
|
android:configChanges="uiMode|orientation|screenSize"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||||
android:label="Anilist">
|
android:label="Anilist"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -104,7 +120,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||||
android:label="MyAnimeList">
|
android:label="MyAnimeList"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -118,7 +135,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||||
android:label="Shikimori">
|
android:label="Shikimori"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -132,7 +150,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.BangumiLoginActivity"
|
android:name=".ui.setting.track.BangumiLoginActivity"
|
||||||
android:label="Bangumi">
|
android:label="Bangumi"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -145,20 +164,6 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.provider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/provider_paths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.notification.NotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@ -183,6 +188,16 @@
|
|||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.backup.BackupRestoreService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -7,9 +7,9 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
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.Lifecycle
|
||||||
@ -17,18 +17,19 @@ import androidx.lifecycle.LifecycleObserver
|
|||||||
import androidx.lifecycle.OnLifecycleEvent
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.multidex.MultiDex
|
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
import coil.decode.GifDecoder
|
import coil.decode.GifDecoder
|
||||||
import coil.decode.ImageDecoderDecoder
|
import coil.decode.ImageDecoderDecoder
|
||||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
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.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.LocaleHelper
|
|
||||||
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
|
||||||
@ -68,8 +69,6 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
setupAcra()
|
setupAcra()
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
|
|
||||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
|
||||||
// Show notification to disable Incognito Mode when it's enabled
|
// Show notification to disable Incognito Mode when it's enabled
|
||||||
@ -81,7 +80,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
|
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
|
||||||
setContentTitle(getString(R.string.pref_incognito_mode))
|
setContentTitle(getString(R.string.pref_incognito_mode))
|
||||||
setContentText(getString(R.string.notification_incognito_text))
|
setContentText(getString(R.string.notification_incognito_text))
|
||||||
setSmallIcon(R.drawable.ic_glasses_black_24dp)
|
setSmallIcon(R.drawable.ic_glasses_24dp)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
|
|
||||||
val pendingIntent = PendingIntent.getBroadcast(
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
@ -99,21 +98,23 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||||
}
|
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
preferences.themeMode()
|
||||||
super.attachBaseContext(base)
|
.asImmediateFlow {
|
||||||
MultiDex.install(this)
|
AppCompatDelegate.setDefaultNightMode(
|
||||||
}
|
when (it) {
|
||||||
|
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
super.onConfigurationChanged(newConfig)
|
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
LocaleHelper.updateConfiguration(this, newConfig, true)
|
}
|
||||||
|
)
|
||||||
|
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Handler
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@ -44,7 +44,7 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
Handler().post {
|
ContextCompat.getMainExecutor(app).execute {
|
||||||
get<PreferencesHelper>()
|
get<PreferencesHelper>()
|
||||||
|
|
||||||
get<NetworkHelper>()
|
get<NetworkHelper>()
|
||||||
|
@ -12,6 +12,8 @@ import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
|||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
@ -96,9 +98,15 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 44) {
|
if (oldVersion < 44) {
|
||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
if (oldSortingMode == LibrarySort.SOURCE) {
|
||||||
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
prefs.edit {
|
||||||
|
putInt(PreferenceKeys.librarySortingMode, LibrarySort.ALPHA)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 52) {
|
if (oldVersion < 52) {
|
||||||
@ -190,6 +198,45 @@ object Migrations {
|
|||||||
LibraryUpdateJob.setupTask(context, 3)
|
LibraryUpdateJob.setupTask(context, 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 64) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
|
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val newSortingMode = when (oldSortingMode) {
|
||||||
|
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
||||||
|
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
|
||||||
|
LibrarySort.LAST_CHECKED -> SortModeSetting.LAST_CHECKED
|
||||||
|
LibrarySort.UNREAD -> SortModeSetting.UNREAD
|
||||||
|
LibrarySort.TOTAL -> SortModeSetting.TOTAL_CHAPTERS
|
||||||
|
LibrarySort.LATEST_CHAPTER -> SortModeSetting.LATEST_CHAPTER
|
||||||
|
LibrarySort.CHAPTER_FETCH_DATE -> SortModeSetting.DATE_FETCHED
|
||||||
|
LibrarySort.DATE_ADDED -> SortModeSetting.DATE_ADDED
|
||||||
|
else -> SortModeSetting.ALPHABETICAL
|
||||||
|
}
|
||||||
|
|
||||||
|
val newSortingDirection = when (oldSortingDirection) {
|
||||||
|
true -> SortDirectionSetting.ASCENDING
|
||||||
|
else -> SortDirectionSetting.DESCENDING
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit(commit = true) {
|
||||||
|
remove(PreferenceKeys.librarySortingMode)
|
||||||
|
remove(PreferenceKeys.librarySortingDirection)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit {
|
||||||
|
putString(PreferenceKeys.librarySortingMode, newSortingMode.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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ class BackupRestoreService : Service() {
|
|||||||
|
|
||||||
private fun destroyJob() {
|
private fun destroyJob() {
|
||||||
backupRestore?.job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
ioScope?.cancel()
|
ioScope.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
|
@ -2,44 +2,52 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.github.salomonbrys.kotson.registerTypeAdapter
|
|
||||||
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.Companion.CURRENT_VERSION
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryImplTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterImplTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaImplTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackImplTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
import kotlinx.serialization.modules.contextual
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||||
|
|
||||||
val parser: Gson = when (version) {
|
val parser: Json = when (version) {
|
||||||
2 -> GsonBuilder()
|
2 -> Json {
|
||||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
// Forks may have added items to backup
|
||||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
ignoreUnknownKeys = true
|
||||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
|
||||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
// Register custom serializers
|
||||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
serializersModule = SerializersModule {
|
||||||
.create()
|
contextual(MangaTypeSerializer)
|
||||||
|
contextual(MangaImplTypeSerializer)
|
||||||
|
contextual(ChapterTypeSerializer)
|
||||||
|
contextual(ChapterImplTypeSerializer)
|
||||||
|
contextual(CategoryTypeSerializer)
|
||||||
|
contextual(CategoryImplTypeSerializer)
|
||||||
|
contextual(TrackTypeSerializer)
|
||||||
|
contextual(TrackImplTypeSerializer)
|
||||||
|
contextual(HistoryTypeSerializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> throw Exception("Unknown backup version")
|
else -> throw Exception("Unknown backup version")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,12 +87,11 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
/**
|
/**
|
||||||
* Restore the categories from Json
|
* Restore the categories from Json
|
||||||
*
|
*
|
||||||
* @param jsonCategories array containing categories
|
* @param backupCategories array containing categories
|
||||||
*/
|
*/
|
||||||
internal fun restoreCategories(jsonCategories: JsonArray) {
|
internal fun restoreCategories(backupCategories: List<Category>) {
|
||||||
// Get categories from file and from db
|
// Get categories from file and from db
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them
|
||||||
backupCategories.forEach { category ->
|
backupCategories.forEach { category ->
|
||||||
|
@ -2,88 +2,80 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGAS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.MangaObject
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
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.JsonObject
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import kotlinx.serialization.json.intOrNull
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
||||||
|
|
||||||
override suspend fun performRestore(uri: Uri): Boolean {
|
override suspend fun performRestore(uri: Uri): Boolean {
|
||||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
// Read the json and create a Json Object,
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
// cannot use the backupManager json deserializer one because its not initialized yet
|
||||||
|
val backupObject = Json.decodeFromString<JsonObject>(
|
||||||
|
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
||||||
|
)
|
||||||
|
|
||||||
val version = json.get(Backup.VERSION)?.asInt ?: 1
|
// Get parser version
|
||||||
|
val version = backupObject["version"]?.jsonPrimitive?.intOrNull ?: 1
|
||||||
|
|
||||||
|
// Initialize manager
|
||||||
backupManager = LegacyBackupManager(context, version)
|
backupManager = LegacyBackupManager(context, version)
|
||||||
|
|
||||||
val mangasJson = json.get(MANGAS).asJsonArray
|
// Decode the json object to a Backup object
|
||||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
val backup = backupManager.parser.decodeFromJsonElement<Backup>(backupObject)
|
||||||
|
|
||||||
|
restoreAmount = backup.mangas.size + 1 // +1 for categories
|
||||||
|
|
||||||
// Restore categories
|
// Restore categories
|
||||||
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
backup.categories?.let { restoreCategories(it) }
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
|
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(backup.extensions ?: emptyList())
|
||||||
|
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
mangasJson.forEach {
|
backup.mangas.forEach {
|
||||||
if (job?.isActive != true) {
|
if (job?.isActive != true) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreManga(it.asJsonObject)
|
restoreManga(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreCategories(categoriesJson: JsonElement) {
|
private fun restoreCategories(categoriesJson: List<Category>) {
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
backupManager.restoreCategories(categoriesJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
private suspend fun restoreManga(mangaJson: MangaObject) {
|
||||||
val manga = backupManager.parser.fromJson<MangaImpl>(
|
val manga = mangaJson.manga
|
||||||
mangaJson.get(
|
val chapters = mangaJson.chapters ?: emptyList()
|
||||||
Backup.MANGA
|
val categories = mangaJson.categories ?: emptyList()
|
||||||
)
|
val history = mangaJson.history ?: emptyList()
|
||||||
)
|
val tracks = mangaJson.track ?: emptyList()
|
||||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
|
||||||
mangaJson.get(Backup.CHAPTERS)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val categories = backupManager.parser.fromJson<List<String>>(
|
|
||||||
mangaJson.get(Backup.CATEGORIES)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val history = backupManager.parser.fromJson<List<DHistory>>(
|
|
||||||
mangaJson.get(Backup.HISTORY)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
|
||||||
mangaJson.get(Backup.TRACK)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
|
|
||||||
val source = backupManager.sourceManager.get(manga.source)
|
val source = backupManager.sourceManager.get(manga.source)
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
@ -2,12 +2,12 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
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 okio.buffer
|
||||||
|
import okio.source
|
||||||
|
|
||||||
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
/**
|
/**
|
||||||
@ -17,30 +17,30 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
* @return List of missing sources or missing trackers.
|
* @return List of missing sources or missing trackers.
|
||||||
*/
|
*/
|
||||||
override fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
val backupManager = LegacyBackupManager(context)
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
|
||||||
|
|
||||||
val version = json.get(Backup.VERSION)
|
val backup = backupManager.parser.decodeFromString<Backup>(
|
||||||
val mangasJson = json.get(Backup.MANGAS)
|
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
||||||
if (version == null || mangasJson == null) {
|
)
|
||||||
|
|
||||||
|
if (backup.version == null) {
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
val mangas = mangasJson.asJsonArray
|
if (backup.mangas.isEmpty()) {
|
||||||
if (mangas.size() == 0) {
|
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources = getSourceMapping(json)
|
val sources = getSourceMapping(backup.extensions ?: emptyList())
|
||||||
val missingSources = sources
|
val missingSources = sources
|
||||||
.filter { sourceManager.get(it.key) == null }
|
.filter { sourceManager.get(it.key) == null }
|
||||||
.values
|
.values
|
||||||
.sorted()
|
.sorted()
|
||||||
|
|
||||||
val trackers = mangas
|
val trackers = backup.mangas
|
||||||
.filter { it.asJsonObject.has("track") }
|
.filterNot { it.track.isNullOrEmpty() }
|
||||||
.flatMap { it.asJsonObject["track"].asJsonArray }
|
.flatMap { it.track ?: emptyList() }
|
||||||
.map { it.asJsonObject["s"].asInt }
|
.map { it.sync_id }
|
||||||
.distinct()
|
.distinct()
|
||||||
val missingTrackers = trackers
|
val missingTrackers = trackers
|
||||||
.mapNotNull { trackManager.getService(it) }
|
.mapNotNull { trackManager.getService(it) }
|
||||||
@ -52,12 +52,10 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
|
||||||
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
return extensionsMapping
|
||||||
|
|
||||||
return extensionsMapping.asJsonArray
|
|
||||||
.map {
|
.map {
|
||||||
val items = it.asString.split(":")
|
val items = it.split(":")
|
||||||
items[0].toLong() to items[1]
|
items[0].toLong() to items[1]
|
||||||
}
|
}
|
||||||
.toMap()
|
.toMap()
|
||||||
|
@ -1,25 +1,37 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.models
|
package eu.kanade.tachiyomi.data.backup.legacy.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
@Serializable
|
||||||
* Json values
|
data class Backup(
|
||||||
*/
|
val version: Int? = null,
|
||||||
object Backup {
|
var mangas: MutableList<MangaObject> = mutableListOf(),
|
||||||
const val CURRENT_VERSION = 2
|
var categories: List<@Contextual Category>? = null,
|
||||||
const val MANGA = "manga"
|
var extensions: List<String>? = null
|
||||||
const val MANGAS = "mangas"
|
) {
|
||||||
const val TRACK = "track"
|
companion object {
|
||||||
const val CHAPTERS = "chapters"
|
const val CURRENT_VERSION = 2
|
||||||
const val CATEGORIES = "categories"
|
|
||||||
const val EXTENSIONS = "extensions"
|
|
||||||
const val HISTORY = "history"
|
|
||||||
const val VERSION = "version"
|
|
||||||
|
|
||||||
fun getDefaultFilename(): String {
|
fun getDefaultFilename(): String {
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
return "tachiyomi_$date.json"
|
return "tachiyomi_$date.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MangaObject(
|
||||||
|
var manga: @Contextual Manga,
|
||||||
|
var chapters: List<@Contextual Chapter>? = null,
|
||||||
|
var categories: List<String>? = null,
|
||||||
|
var track: List<@Contextual Track>? = null,
|
||||||
|
var history: List<@Contextual DHistory>? = null
|
||||||
|
)
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
|
||||||
*/
|
|
||||||
object CategoryTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<CategoryImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginArray()
|
|
||||||
value(it.name)
|
|
||||||
value(it.order)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val category = CategoryImpl()
|
|
||||||
category.name = nextString()
|
|
||||||
category.order = nextInt()
|
|
||||||
endArray()
|
|
||||||
category
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
49
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeSerializer.kt
Normal file
49
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeSerializer.kt
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class CategoryBaseSerializer<T : Category> : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Category")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.name)
|
||||||
|
add(value.order)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a category impl and cast as T so that the serializer accepts it
|
||||||
|
return CategoryImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
name = array[0].jsonPrimitive.content
|
||||||
|
order = array[1].jsonPrimitive.int
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a category and category impl
|
||||||
|
object CategoryTypeSerializer : CategoryBaseSerializer<Category>()
|
||||||
|
|
||||||
|
object CategoryImplTypeSerializer : CategoryBaseSerializer<CategoryImpl>()
|
@ -1,59 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
|
||||||
*/
|
|
||||||
object ChapterTypeAdapter {
|
|
||||||
|
|
||||||
private const val URL = "u"
|
|
||||||
private const val READ = "r"
|
|
||||||
private const val BOOKMARK = "b"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<ChapterImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
if (it.read || it.bookmark || it.last_page_read != 0) {
|
|
||||||
beginObject()
|
|
||||||
name(URL)
|
|
||||||
value(it.url)
|
|
||||||
if (it.read) {
|
|
||||||
name(READ)
|
|
||||||
value(1)
|
|
||||||
}
|
|
||||||
if (it.bookmark) {
|
|
||||||
name(BOOKMARK)
|
|
||||||
value(1)
|
|
||||||
}
|
|
||||||
if (it.last_page_read != 0) {
|
|
||||||
name(LAST_READ)
|
|
||||||
value(it.last_page_read)
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
val chapter = ChapterImpl()
|
|
||||||
beginObject()
|
|
||||||
while (hasNext()) {
|
|
||||||
if (peek() == JsonToken.NAME) {
|
|
||||||
when (nextName()) {
|
|
||||||
URL -> chapter.url = nextString()
|
|
||||||
READ -> chapter.read = nextInt() == 1
|
|
||||||
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
|
||||||
LAST_READ -> chapter.last_page_read = nextInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
chapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
66
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeSerializer.kt
Normal file
66
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeSerializer.kt
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.intOrNull
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class ChapterBaseSerializer<T : Chapter> : KSerializer<T> {
|
||||||
|
|
||||||
|
override val descriptor = buildClassSerialDescriptor("Chapter")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonObject {
|
||||||
|
put(URL, value.url)
|
||||||
|
if (value.read) {
|
||||||
|
put(READ, 1)
|
||||||
|
}
|
||||||
|
if (value.bookmark) {
|
||||||
|
put(BOOKMARK, 1)
|
||||||
|
}
|
||||||
|
if (value.last_page_read != 0) {
|
||||||
|
put(LAST_READ, value.last_page_read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a chapter impl and cast as T so that the serializer accepts it
|
||||||
|
return ChapterImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val jsonObject = decoder.decodeJsonElement().jsonObject
|
||||||
|
url = jsonObject[URL]!!.jsonPrimitive.content
|
||||||
|
read = jsonObject[READ]?.jsonPrimitive?.intOrNull == 1
|
||||||
|
bookmark = jsonObject[BOOKMARK]?.jsonPrimitive?.intOrNull == 1
|
||||||
|
last_page_read = jsonObject[LAST_READ]?.jsonPrimitive?.intOrNull ?: last_page_read
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val URL = "u"
|
||||||
|
private const val READ = "r"
|
||||||
|
private const val BOOKMARK = "b"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a chapter and chapter impl
|
||||||
|
object ChapterTypeSerializer : ChapterBaseSerializer<Chapter>()
|
||||||
|
|
||||||
|
object ChapterImplTypeSerializer : ChapterBaseSerializer<ChapterImpl>()
|
@ -1,32 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [DHistory] to / from json
|
|
||||||
*/
|
|
||||||
object HistoryTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<DHistory> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
if (it.lastRead != 0L) {
|
|
||||||
beginArray()
|
|
||||||
value(it.url)
|
|
||||||
value(it.lastRead)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val url = nextString()
|
|
||||||
val lastRead = nextLong()
|
|
||||||
endArray()
|
|
||||||
DHistory(url, lastRead)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
41
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeSerializer.kt
Normal file
41
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeSerializer.kt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [DHistory] to / from json
|
||||||
|
*/
|
||||||
|
object HistoryTypeSerializer : KSerializer<DHistory> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("History")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: DHistory) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.url)
|
||||||
|
add(value.lastRead)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): DHistory {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
return DHistory(
|
||||||
|
url = array[0].jsonPrimitive.content,
|
||||||
|
lastRead = array[1].jsonPrimitive.long
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [MangaImpl] to / from json
|
|
||||||
*/
|
|
||||||
object MangaTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<MangaImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginArray()
|
|
||||||
value(it.url)
|
|
||||||
value(it.title)
|
|
||||||
value(it.source)
|
|
||||||
value(it.viewer_flags)
|
|
||||||
value(it.chapter_flags)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val manga = MangaImpl()
|
|
||||||
manga.url = nextString()
|
|
||||||
manga.title = nextString()
|
|
||||||
manga.source = nextLong()
|
|
||||||
manga.viewer_flags = nextInt()
|
|
||||||
manga.chapter_flags = nextInt()
|
|
||||||
endArray()
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
56
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeSerializer.kt
Normal file
56
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeSerializer.kt
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [MangaImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class MangaBaseSerializer<T : Manga> : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Manga")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.url)
|
||||||
|
add(value.title)
|
||||||
|
add(value.source)
|
||||||
|
add(value.viewer_flags)
|
||||||
|
add(value.chapter_flags)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a manga impl and cast as T so that the serializer accepts it
|
||||||
|
return MangaImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
url = array[0].jsonPrimitive.content
|
||||||
|
title = array[1].jsonPrimitive.content
|
||||||
|
source = array[2].jsonPrimitive.long
|
||||||
|
viewer_flags = array[3].jsonPrimitive.int
|
||||||
|
chapter_flags = array[4].jsonPrimitive.int
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a manga and manga impl
|
||||||
|
object MangaTypeSerializer : MangaBaseSerializer<Manga>()
|
||||||
|
|
||||||
|
object MangaImplTypeSerializer : MangaBaseSerializer<MangaImpl>()
|
@ -1,59 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [TrackImpl] to / from json
|
|
||||||
*/
|
|
||||||
object TrackTypeAdapter {
|
|
||||||
|
|
||||||
private const val SYNC = "s"
|
|
||||||
private const val MEDIA = "r"
|
|
||||||
private const val LIBRARY = "ml"
|
|
||||||
private const val TITLE = "t"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
private const val TRACKING_URL = "u"
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<TrackImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginObject()
|
|
||||||
name(TITLE)
|
|
||||||
value(it.title)
|
|
||||||
name(SYNC)
|
|
||||||
value(it.sync_id)
|
|
||||||
name(MEDIA)
|
|
||||||
value(it.media_id)
|
|
||||||
name(LIBRARY)
|
|
||||||
value(it.library_id)
|
|
||||||
name(LAST_READ)
|
|
||||||
value(it.last_chapter_read)
|
|
||||||
name(TRACKING_URL)
|
|
||||||
value(it.tracking_url)
|
|
||||||
endObject()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
val track = TrackImpl()
|
|
||||||
beginObject()
|
|
||||||
while (hasNext()) {
|
|
||||||
if (peek() == JsonToken.NAME) {
|
|
||||||
when (nextName()) {
|
|
||||||
TITLE -> track.title = nextString()
|
|
||||||
SYNC -> track.sync_id = nextInt()
|
|
||||||
MEDIA -> track.media_id = nextInt()
|
|
||||||
LIBRARY -> track.library_id = nextLong()
|
|
||||||
LAST_READ -> track.last_chapter_read = nextInt()
|
|
||||||
TRACKING_URL -> track.tracking_url = nextString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
67
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeSerializer.kt
Normal file
67
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeSerializer.kt
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [TrackImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class TrackBaseSerializer<T : Track> : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Track")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonObject {
|
||||||
|
put(TITLE, value.title)
|
||||||
|
put(SYNC, value.sync_id)
|
||||||
|
put(MEDIA, value.media_id)
|
||||||
|
put(LIBRARY, value.library_id)
|
||||||
|
put(LAST_READ, value.last_chapter_read)
|
||||||
|
put(TRACKING_URL, value.tracking_url)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a track impl and cast as T so that the serializer accepts it
|
||||||
|
return TrackImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val jsonObject = decoder.decodeJsonElement().jsonObject
|
||||||
|
title = jsonObject[TITLE]!!.jsonPrimitive.content
|
||||||
|
sync_id = jsonObject[SYNC]!!.jsonPrimitive.int
|
||||||
|
media_id = jsonObject[MEDIA]!!.jsonPrimitive.int
|
||||||
|
library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long
|
||||||
|
last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.int
|
||||||
|
tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SYNC = "s"
|
||||||
|
private const val MEDIA = "r"
|
||||||
|
private const val LIBRARY = "ml"
|
||||||
|
private const val TITLE = "t"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
private const val TRACKING_URL = "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a track and track impl
|
||||||
|
object TrackTypeSerializer : TrackBaseSerializer<Track>()
|
||||||
|
|
||||||
|
object TrackImplTypeSerializer : TrackBaseSerializer<TrackImpl>()
|
@ -27,7 +27,6 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
||||||
@ -62,14 +61,15 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
||||||
val coverFile = coverCache.getCoverFile(manga) ?: error("No cover specified")
|
// Only cache separately if it's a library item
|
||||||
|
val coverCacheFile = if (manga.favorite) {
|
||||||
|
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
// Use previously cached cover if exist
|
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
||||||
if (coverFile.exists() && options.diskCachePolicy.readEnabled) {
|
return fileLoader(coverCacheFile)
|
||||||
if (!manga.favorite) {
|
|
||||||
coverFile.setLastModified(Date().time)
|
|
||||||
}
|
|
||||||
return fileLoader(coverFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val (response, body) = awaitGetCall(manga, options)
|
val (response, body) = awaitGetCall(manga, options)
|
||||||
@ -78,18 +78,16 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
throw HttpException(response)
|
throw HttpException(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to disk for future use
|
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
|
||||||
if (options.diskCachePolicy.writeEnabled) {
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||||
val tmpFile = File(coverFile.absolutePath + "_tmp")
|
coverCacheFile.parentFile?.mkdirs()
|
||||||
tmpFile.parentFile?.mkdirs()
|
if (coverCacheFile.exists()) {
|
||||||
tmpFile.sink().buffer().use { output ->
|
coverCacheFile.delete()
|
||||||
|
}
|
||||||
|
coverCacheFile.sink().buffer().use { output ->
|
||||||
output.writeAll(input)
|
output.writeAll(input)
|
||||||
}
|
}
|
||||||
if (coverFile.exists()) {
|
|
||||||
coverFile.delete()
|
|
||||||
}
|
|
||||||
tmpFile.renameTo(coverFile)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,10 +106,6 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
|
|
||||||
private fun getCall(manga: Manga, options: Options): Call {
|
private fun getCall(manga: Manga, options: Options): Call {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
val client = source?.client ?: defaultClient
|
|
||||||
|
|
||||||
val newClient = client.newBuilder().build()
|
|
||||||
|
|
||||||
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
it.headers(source.headers)
|
it.headers(source.headers)
|
||||||
@ -135,7 +129,8 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
return newClient.newCall(request)
|
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
|
||||||
|
return client.newCall(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileLoader(manga: Manga): FetchResult {
|
private fun fileLoader(manga: Manga): FetchResult {
|
||||||
@ -153,7 +148,7 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
private fun getResourceType(cover: String?): Type? {
|
private fun getResourceType(cover: String?): Type? {
|
||||||
return when {
|
return when {
|
||||||
cover.isNullOrEmpty() -> null
|
cover.isNullOrEmpty() -> null
|
||||||
cover.startsWith("http") || cover.startsWith("Custom-", true) -> Type.URL
|
cover.startsWith("http", true) || cover.startsWith("Custom-", true) -> Type.URL
|
||||||
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import coil.bitmap.BitmapPool
|
||||||
|
import coil.decode.DecodeResult
|
||||||
|
import coil.decode.Decoder
|
||||||
|
import coil.decode.Options
|
||||||
|
import coil.size.Size
|
||||||
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import okio.BufferedSource
|
||||||
|
import tachiyomi.decoder.ImageDecoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
||||||
|
*/
|
||||||
|
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
||||||
|
|
||||||
|
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
|
||||||
|
val type = source.peek().inputStream().use {
|
||||||
|
ImageUtil.findImageType(it)
|
||||||
|
}
|
||||||
|
return when (type) {
|
||||||
|
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
||||||
|
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun decode(
|
||||||
|
pool: BitmapPool,
|
||||||
|
source: BufferedSource,
|
||||||
|
size: Size,
|
||||||
|
options: Options
|
||||||
|
): DecodeResult {
|
||||||
|
val decoder = source.use {
|
||||||
|
ImageDecoder.newInstance(it.inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
|
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder." }
|
||||||
|
|
||||||
|
val bitmap = decoder.decode(rgb565 = options.allowRgb565)
|
||||||
|
decoder.recycle()
|
||||||
|
|
||||||
|
check(bitmap != null) { "Failed to decode image." }
|
||||||
|
|
||||||
|
return DecodeResult(
|
||||||
|
drawable = bitmap.toDrawable(resources),
|
||||||
|
isSampled = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 11
|
const val DATABASE_VERSION = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@ -82,6 +82,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
db.execSQL(MangaTable.addDateAdded)
|
db.execSQL(MangaTable.addDateAdded)
|
||||||
db.execSQL(MangaTable.backfillDateAdded)
|
db.execSQL(MangaTable.backfillDateAdded)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 12) {
|
||||||
|
db.execSQL(MangaTable.addNextUpdateCol)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_NEXT_UPDATE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
||||||
@ -62,6 +63,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
||||||
COL_FAVORITE to obj.favorite,
|
COL_FAVORITE to obj.favorite,
|
||||||
COL_LAST_UPDATE to obj.last_update,
|
COL_LAST_UPDATE to obj.last_update,
|
||||||
|
COL_NEXT_UPDATE to obj.next_update,
|
||||||
COL_INITIALIZED to obj.initialized,
|
COL_INITIALIZED to obj.initialized,
|
||||||
COL_VIEWER to obj.viewer_flags,
|
COL_VIEWER to obj.viewer_flags,
|
||||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||||
@ -84,6 +86,7 @@ interface BaseMangaGetResolver {
|
|||||||
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
||||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
||||||
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
||||||
|
next_update = cursor.getLong(cursor.getColumnIndex(COL_NEXT_UPDATE))
|
||||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||||
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface Category : Serializable {
|
interface Category : Serializable {
|
||||||
@ -12,6 +15,22 @@ interface Category : Serializable {
|
|||||||
|
|
||||||
var flags: Int
|
var flags: Int
|
||||||
|
|
||||||
|
private fun setFlags(flag: Int, mask: Int) {
|
||||||
|
flags = flags and mask.inv() or (flag and mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayMode: Int
|
||||||
|
get() = flags and DisplayModeSetting.MASK
|
||||||
|
set(mode) = setFlags(mode, DisplayModeSetting.MASK)
|
||||||
|
|
||||||
|
var sortMode: Int
|
||||||
|
get() = flags and SortModeSetting.MASK
|
||||||
|
set(mode) = setFlags(mode, SortModeSetting.MASK)
|
||||||
|
|
||||||
|
var sortDirection: Int
|
||||||
|
get() = flags and SortDirectionSetting.MASK
|
||||||
|
set(mode) = setFlags(mode, SortDirectionSetting.MASK)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun create(name: String): Category = CategoryImpl().apply {
|
fun create(name: String): Category = CategoryImpl().apply {
|
||||||
|
@ -13,8 +13,12 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
var favorite: Boolean
|
var favorite: Boolean
|
||||||
|
|
||||||
|
// last time the chapter list changed in any way
|
||||||
var last_update: Long
|
var last_update: Long
|
||||||
|
|
||||||
|
// predicted next update time based on latest (by date) 4 chapters' deltas
|
||||||
|
var next_update: Long
|
||||||
|
|
||||||
var date_added: Long
|
var date_added: Long
|
||||||
|
|
||||||
var viewer_flags: Int
|
var viewer_flags: Int
|
||||||
|
@ -26,6 +26,8 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var last_update: Long = 0
|
override var last_update: Long = 0
|
||||||
|
|
||||||
|
override var next_update: Long = 0
|
||||||
|
|
||||||
override var date_added: Long = 0
|
override var date_added: Long = 0
|
||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
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.LibraryMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.*
|
||||||
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.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
|
||||||
@ -19,15 +15,6 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
|||||||
|
|
||||||
interface MangaQueries : DbProvider {
|
interface MangaQueries : DbProvider {
|
||||||
|
|
||||||
fun getMangas() = db.get()
|
|
||||||
.listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(
|
|
||||||
Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getLibraryMangas() = db.get()
|
fun getLibraryMangas() = db.get()
|
||||||
.listOfObjects(LibraryManga::class.java)
|
.listOfObjects(LibraryManga::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
@ -39,17 +26,21 @@ interface MangaQueries : DbProvider {
|
|||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getFavoriteMangas() = db.get()
|
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
|
||||||
.listOfObjects(Manga::class.java)
|
var queryBuilder = Query.builder()
|
||||||
.withQuery(
|
.table(MangaTable.TABLE)
|
||||||
Query.builder()
|
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||||
.table(MangaTable.TABLE)
|
.whereArgs(1)
|
||||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
|
||||||
.whereArgs(1)
|
if (sortByTitle) {
|
||||||
.orderBy(MangaTable.COL_TITLE)
|
queryBuilder = queryBuilder.orderBy(MangaTable.COL_TITLE)
|
||||||
.build()
|
}
|
||||||
)
|
|
||||||
.prepare()
|
return db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(queryBuilder.build())
|
||||||
|
.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
fun getManga(url: String, sourceId: Long) = db.get()
|
fun getManga(url: String, sourceId: Long) = db.get()
|
||||||
.`object`(Manga::class.java)
|
.`object`(Manga::class.java)
|
||||||
@ -97,6 +88,11 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateNextUpdated(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaNextUpdatedPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun updateLastUpdated(manga: Manga) = db.put()
|
fun updateLastUpdated(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaLastUpdatedPutResolver())
|
.withPutResolver(MangaLastUpdatedPutResolver())
|
||||||
|
31
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaNextUpdatedPutResolver.kt
Normal file
31
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaNextUpdatedPutResolver.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
class MangaNextUpdatedPutResolver : PutResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
|
val contentValues = mapToContentValues(manga)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
|
put(MangaTable.COL_NEXT_UPDATE, manga.next_update)
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,8 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_LAST_UPDATE = "last_update"
|
const val COL_LAST_UPDATE = "last_update"
|
||||||
|
|
||||||
|
const val COL_NEXT_UPDATE = "next_update"
|
||||||
|
|
||||||
const val COL_DATE_ADDED = "date_added"
|
const val COL_DATE_ADDED = "date_added"
|
||||||
|
|
||||||
const val COL_INITIALIZED = "initialized"
|
const val COL_INITIALIZED = "initialized"
|
||||||
@ -57,6 +59,7 @@ object MangaTable {
|
|||||||
$COL_THUMBNAIL_URL TEXT,
|
$COL_THUMBNAIL_URL TEXT,
|
||||||
$COL_FAVORITE INTEGER NOT NULL,
|
$COL_FAVORITE INTEGER NOT NULL,
|
||||||
$COL_LAST_UPDATE LONG,
|
$COL_LAST_UPDATE LONG,
|
||||||
|
$COL_NEXT_UPDATE LONG,
|
||||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||||
$COL_VIEWER INTEGER NOT NULL,
|
$COL_VIEWER INTEGER NOT NULL,
|
||||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||||
@ -86,4 +89,7 @@ object MangaTable {
|
|||||||
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
||||||
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
||||||
"GROUP BY $TABLE.$COL_ID)"
|
"GROUP BY $TABLE.$COL_ID)"
|
||||||
|
|
||||||
|
val addNextUpdateCol: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_NEXT_UPDATE LONG DEFAULT 0"
|
||||||
}
|
}
|
||||||
|
@ -95,6 +95,23 @@ class DownloadManager(private val context: Context) {
|
|||||||
downloader.clearQueue(isNotification)
|
downloader.clearQueue(isNotification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDownloadNow(chapter: Chapter) {
|
||||||
|
val download = downloader.queue.find { it.chapter.id == chapter.id } ?: return
|
||||||
|
val queue = downloader.queue.toMutableList()
|
||||||
|
queue.remove(download)
|
||||||
|
queue.add(0, download)
|
||||||
|
reorderQueue(queue)
|
||||||
|
if (isPaused()) {
|
||||||
|
if (DownloadService.isRunning(context)) {
|
||||||
|
downloader.start()
|
||||||
|
} else {
|
||||||
|
DownloadService.start(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPaused() = downloader.isPaused()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorders the download queue.
|
* Reorders the download queue.
|
||||||
*
|
*
|
||||||
|
@ -19,6 +19,7 @@ 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.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.connectivityManager
|
import eu.kanade.tachiyomi.util.system.connectivityManager
|
||||||
|
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 rx.android.schedulers.AndroidSchedulers
|
||||||
@ -58,6 +59,16 @@ class DownloadService : Service() {
|
|||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
context.stopService(Intent(context, DownloadService::class.java))
|
context.stopService(Intent(context, DownloadService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status of the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @return true if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isRunning(context: Context): Boolean {
|
||||||
|
return context.isServiceRunning(DownloadService::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val downloadManager: DownloadManager by injectLazy()
|
private val downloadManager: DownloadManager by injectLazy()
|
||||||
|
@ -157,6 +157,11 @@ class Downloader(
|
|||||||
notifier.paused = true
|
notifier.paused = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if downloader is paused
|
||||||
|
*/
|
||||||
|
fun isPaused() = !isRunning
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes everything from the queue.
|
* Removes everything from the queue.
|
||||||
*
|
*
|
||||||
|
@ -64,22 +64,25 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Shows the notification containing the currently updating manga and the progress.
|
* Shows the notification containing the currently updating manga and the progress.
|
||||||
*
|
*
|
||||||
* @param manga the manga that's being updated.
|
* @param manga the manga that are being updated.
|
||||||
* @param current the current progress.
|
* @param current the current progress.
|
||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
fun showProgressNotification(manga: List<Manga>, current: Int, total: Int) {
|
||||||
val title = if (preferences.hideNotificationContent()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
context.getString(R.string.notification_check_updates)
|
progressNotificationBuilder
|
||||||
|
.setContentTitle(context.getString(R.string.notification_check_updates))
|
||||||
|
.setContentText("($current/$total)")
|
||||||
} else {
|
} else {
|
||||||
manga.title
|
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
|
||||||
|
progressNotificationBuilder
|
||||||
|
.setContentTitle(context.getString(R.string.notification_updating, current, total))
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||||
}
|
}
|
||||||
|
|
||||||
context.notificationManager.notify(
|
context.notificationManager.notify(
|
||||||
Notifications.ID_LIBRARY_PROGRESS,
|
Notifications.ID_LIBRARY_PROGRESS,
|
||||||
progressNotificationBuilder
|
progressNotificationBuilder
|
||||||
.setContentTitle(title.chop(40))
|
|
||||||
.setContentText("($current/$total)")
|
|
||||||
.setProgress(total, current, false)
|
.setProgress(total, current, false)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
@ -203,7 +206,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
// Mark chapters as read action
|
// Mark chapters as read action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_glasses_black_24dp,
|
R.drawable.ic_glasses_24dp,
|
||||||
context.getString(R.string.action_mark_as_read),
|
context.getString(R.string.action_mark_as_read),
|
||||||
NotificationReceiver.markAsReadPendingBroadcast(
|
NotificationReceiver.markAsReadPendingBroadcast(
|
||||||
context,
|
context,
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
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 kotlin.Comparator
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class will provide various functions to rank manga to efficiently schedule manga to update.
|
* This class will provide various functions to rank manga to efficiently schedule manga to update.
|
||||||
@ -9,9 +12,26 @@ object LibraryUpdateRanker {
|
|||||||
|
|
||||||
val rankingScheme = listOf(
|
val rankingScheme = listOf(
|
||||||
(this::lexicographicRanking)(),
|
(this::lexicographicRanking)(),
|
||||||
(this::latestFirstRanking)()
|
(this::latestFirstRanking)(),
|
||||||
|
(this::nextFirstRanking)()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a total ordering over all the Mangas.
|
||||||
|
*
|
||||||
|
* Orders the manga based on the distance between the next expected update and now.
|
||||||
|
* The comparator is reversed, placing the smallest (and thus closest to updating now) first.
|
||||||
|
*/
|
||||||
|
fun nextFirstRanking(): Comparator<Manga> {
|
||||||
|
val time = System.currentTimeMillis()
|
||||||
|
return Collections.reverseOrder(
|
||||||
|
Comparator { mangaFirst: Manga,
|
||||||
|
mangaSecond: Manga ->
|
||||||
|
compareValues(abs(mangaSecond.next_update - time), abs(mangaFirst.next_update - time))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a total ordering over all the [Manga]s.
|
* Provides a total ordering over all the [Manga]s.
|
||||||
*
|
*
|
||||||
|
@ -20,9 +20,9 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
|||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
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.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||||
@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.source.model.toSManga
|
|||||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
@ -47,10 +48,14 @@ import kotlinx.coroutines.awaitAll
|
|||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -270,50 +275,79 @@ class LibraryUpdateService(
|
|||||||
* @return an observable delivering the progress of each update.
|
* @return an observable delivering the progress of each update.
|
||||||
*/
|
*/
|
||||||
suspend fun updateChapterList() {
|
suspend fun updateChapterList() {
|
||||||
|
val semaphore = Semaphore(5)
|
||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInteger(0)
|
||||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||||
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
val newUpdates = CopyOnWriteArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
||||||
var hasDownloads = false
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||||
|
|
||||||
mangaToUpdate.forEach { manga ->
|
withIOContext {
|
||||||
if (updateJob?.isActive != true) {
|
mangaToUpdate.groupBy { it.source }
|
||||||
return
|
.values
|
||||||
}
|
.map { mangaInSource ->
|
||||||
|
async {
|
||||||
|
semaphore.withPermit {
|
||||||
|
mangaInSource.forEach { manga ->
|
||||||
|
if (updateJob?.isActive != true) {
|
||||||
|
return@async
|
||||||
|
}
|
||||||
|
|
||||||
notifier.showProgressNotification(manga, progressCount.andIncrement, mangaToUpdate.size)
|
currentlyUpdatingManga.add(manga)
|
||||||
|
notifier.showProgressNotification(
|
||||||
|
currentlyUpdatingManga,
|
||||||
|
progressCount.get(),
|
||||||
|
mangaToUpdate.size
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val (newChapters, _) = updateManga(manga)
|
val (newChapters, _) = updateManga(manga)
|
||||||
|
|
||||||
if (newChapters.isNotEmpty()) {
|
if (newChapters.isNotEmpty()) {
|
||||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||||
downloadChapters(manga, newChapters)
|
downloadChapters(manga, newChapters)
|
||||||
hasDownloads = true
|
hasDownloads.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to the manga that contains new chapters
|
||||||
|
newUpdates.add(manga to newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray())
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
val errorMessage = if (e is NoChaptersException) {
|
||||||
|
getString(R.string.no_chapters_error)
|
||||||
|
} else if (e is SourceManager.SourceNotInstalledException) {
|
||||||
|
// failedUpdates will already have the source, don't need to copy it into the message
|
||||||
|
getString(R.string.loader_not_implemented_error)
|
||||||
|
} else {
|
||||||
|
e.message
|
||||||
|
}
|
||||||
|
failedUpdates.add(manga to errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferences.autoUpdateTrackers()) {
|
||||||
|
updateTrackings(manga, loggedServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentlyUpdatingManga.remove(manga)
|
||||||
|
progressCount.andIncrement
|
||||||
|
notifier.showProgressNotification(
|
||||||
|
currentlyUpdatingManga,
|
||||||
|
progressCount.get(),
|
||||||
|
mangaToUpdate.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to the manga that contains new chapters
|
|
||||||
newUpdates.add(manga to newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray())
|
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
.awaitAll()
|
||||||
val errorMessage = if (e is NoChaptersException) {
|
|
||||||
getString(R.string.no_chapters_error)
|
|
||||||
} else {
|
|
||||||
e.message
|
|
||||||
}
|
|
||||||
failedUpdates.add(manga to errorMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferences.autoUpdateTrackers()) {
|
|
||||||
updateTrackings(manga, loggedServices)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notifier.cancelProgressNotification()
|
notifier.cancelProgressNotification()
|
||||||
|
|
||||||
if (newUpdates.isNotEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
notifier.showUpdateNotifications(newUpdates)
|
notifier.showUpdateNotifications(newUpdates)
|
||||||
if (hasDownloads) {
|
if (hasDownloads.get()) {
|
||||||
DownloadService.start(this)
|
DownloadService.start(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -369,29 +403,56 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers() {
|
private suspend fun updateCovers() {
|
||||||
var progressCount = 0
|
val semaphore = Semaphore(5)
|
||||||
|
val progressCount = AtomicInteger(0)
|
||||||
|
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||||
|
|
||||||
mangaToUpdate.forEach { manga ->
|
withIOContext {
|
||||||
if (updateJob?.isActive != true) {
|
mangaToUpdate.groupBy { it.source }
|
||||||
return
|
.values
|
||||||
}
|
.map { mangaInSource ->
|
||||||
|
async {
|
||||||
|
semaphore.withPermit {
|
||||||
|
mangaInSource.forEach { manga ->
|
||||||
|
if (updateJob?.isActive != true) {
|
||||||
|
return@async
|
||||||
|
}
|
||||||
|
|
||||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
currentlyUpdatingManga.add(manga)
|
||||||
|
notifier.showProgressNotification(
|
||||||
|
currentlyUpdatingManga,
|
||||||
|
progressCount.get(),
|
||||||
|
mangaToUpdate.size
|
||||||
|
)
|
||||||
|
|
||||||
sourceManager.get(manga.source)?.let { source ->
|
sourceManager.get(manga.source)?.let { source ->
|
||||||
try {
|
try {
|
||||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
val networkManga =
|
||||||
val sManga = networkManga.toSManga()
|
source.getMangaDetails(manga.toMangaInfo())
|
||||||
manga.prepUpdateCover(coverCache, sManga, true)
|
val sManga = networkManga.toSManga()
|
||||||
sManga.thumbnail_url?.let {
|
manga.prepUpdateCover(coverCache, sManga, true)
|
||||||
manga.thumbnail_url = it
|
sManga.thumbnail_url?.let {
|
||||||
db.insertManga(manga).executeAsBlocking()
|
manga.thumbnail_url = it
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Ignore errors and continue
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentlyUpdatingManga.remove(manga)
|
||||||
|
progressCount.andIncrement
|
||||||
|
notifier.showProgressNotification(
|
||||||
|
currentlyUpdatingManga,
|
||||||
|
progressCount.get(),
|
||||||
|
mangaToUpdate.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
|
||||||
// Ignore errors and continue
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
}
|
||||||
}
|
.awaitAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
coverCache.clearMemoryCache()
|
coverCache.clearMemoryCache()
|
||||||
@ -411,8 +472,7 @@ class LibraryUpdateService(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify manga that will update.
|
notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size)
|
||||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
|
||||||
|
|
||||||
// Update the tracking details.
|
// Update the tracking details.
|
||||||
updateTrackings(manga, loggedServices)
|
updateTrackings(manga, loggedServices)
|
||||||
@ -432,7 +492,7 @@ class LibraryUpdateService(
|
|||||||
val updatedTrack = service.refresh(track)
|
val updatedTrack = service.refresh(track)
|
||||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||||
|
|
||||||
if (service is UnattendedTrackService) {
|
if (service is EnhancedTrackService) {
|
||||||
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
|
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@ -454,9 +514,19 @@ class LibraryUpdateService(
|
|||||||
if (errors.isNotEmpty()) {
|
if (errors.isNotEmpty()) {
|
||||||
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
|
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
|
||||||
file.bufferedWriter().use { out ->
|
file.bufferedWriter().use { out ->
|
||||||
errors.forEach { (manga, error) ->
|
// Error file format:
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
// ! Error
|
||||||
out.write("${manga.title} ($source): $error\n")
|
// # Source
|
||||||
|
// - Manga
|
||||||
|
errors.groupBy({ it.second }, { it.first }).forEach { (error, mangas) ->
|
||||||
|
out.write("! ${error}\n")
|
||||||
|
mangas.groupBy { it.source }.forEach { (srcId, mangas) ->
|
||||||
|
val source = sourceManager.getOrStub(srcId)
|
||||||
|
out.write(" # $source\n")
|
||||||
|
mangas.forEach {
|
||||||
|
out.write(" - ${it.title}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return file
|
return file
|
||||||
|
@ -2,12 +2,11 @@ package eu.kanade.tachiyomi.data.notification
|
|||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@ -25,6 +24,7 @@ import eu.kanade.tachiyomi.util.lang.launchIO
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
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
|
||||||
@ -130,16 +130,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
*/
|
*/
|
||||||
private fun shareImage(context: Context, path: String, notificationId: Int) {
|
private fun shareImage(context: Context, path: String, notificationId: Int) {
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
val uri = File(path).getUriCompat(context)
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
clipData = ClipData.newRawUri(null, uri)
|
|
||||||
type = "image/*"
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
}
|
|
||||||
dismissNotification(context, notificationId)
|
dismissNotification(context, notificationId)
|
||||||
// Launch share activity
|
context.startActivity(File(path).getUriCompat(context).toShareIntent(context))
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -150,16 +142,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
*/
|
*/
|
||||||
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
|
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
|
||||||
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
clipData = ClipData.newRawUri(null, uri)
|
|
||||||
type = fileMimeType
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
}
|
|
||||||
// Dismiss notification
|
|
||||||
dismissNotification(context, notificationId)
|
dismissNotification(context, notificationId)
|
||||||
// Launch share activity
|
context.startActivity(uri.toShareIntent(context, fileMimeType))
|
||||||
context.startActivity(sendIntent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -208,7 +192,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
*/
|
*/
|
||||||
private fun cancelRestore(context: Context, notificationId: Int) {
|
private fun cancelRestore(context: Context, notificationId: Int) {
|
||||||
BackupRestoreService.stop(context)
|
BackupRestoreService.stop(context)
|
||||||
Handler().post { dismissNotification(context, notificationId) }
|
ContextCompat.getMainExecutor(context).execute { dismissNotification(context, notificationId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -219,7 +203,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
*/
|
*/
|
||||||
private fun cancelLibraryUpdate(context: Context, notificationId: Int) {
|
private fun cancelLibraryUpdate(context: Context, notificationId: Int) {
|
||||||
LibraryUpdateService.stop(context)
|
LibraryUpdateService.stop(context)
|
||||||
Handler().post { dismissNotification(context, notificationId) }
|
ContextCompat.getMainExecutor(context).execute { dismissNotification(context, notificationId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,15 +7,15 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val themeMode = "pref_theme_mode_key"
|
const val themeMode = "pref_theme_mode_key"
|
||||||
|
|
||||||
const val themeLight = "pref_theme_light_key"
|
const val appTheme = "pref_app_theme"
|
||||||
|
|
||||||
const val themeDark = "pref_theme_dark_key"
|
const val themeDarkAmoled = "pref_theme_dark_amoled_key"
|
||||||
|
|
||||||
const val confirmExit = "pref_confirm_exit"
|
const val confirmExit = "pref_confirm_exit"
|
||||||
|
|
||||||
const val hideBottomBarOnScroll = "pref_hide_bottom_bar_on_scroll"
|
const val hideBottomBarOnScroll = "pref_hide_bottom_bar_on_scroll"
|
||||||
|
|
||||||
const val showSideNavOnBottom = "pref_show_side_nav_on_bottom"
|
const val sideNavIconAlignment = "pref_side_nav_icon_alignment"
|
||||||
|
|
||||||
const val enableTransitions = "pref_enable_transitions_key"
|
const val enableTransitions = "pref_enable_transitions_key"
|
||||||
|
|
||||||
@ -99,8 +99,6 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||||
|
|
||||||
const val autoAddTrack = "pref_auto_add_track_key"
|
|
||||||
|
|
||||||
const val lastUsedSource = "last_catalogue_source"
|
const val lastUsedSource = "last_catalogue_source"
|
||||||
|
|
||||||
const val lastUsedCategory = "last_used_category"
|
const val lastUsedCategory = "last_used_category"
|
||||||
@ -147,6 +145,7 @@ object PreferenceKeys {
|
|||||||
const val filterTracked = "pref_filter_library_tracked"
|
const val filterTracked = "pref_filter_library_tracked"
|
||||||
|
|
||||||
const val librarySortingMode = "library_sorting_mode"
|
const val librarySortingMode = "library_sorting_mode"
|
||||||
|
const val librarySortingDirection = "library_sorting_ascending"
|
||||||
|
|
||||||
const val automaticExtUpdates = "automatic_ext_updates"
|
const val automaticExtUpdates = "automatic_ext_updates"
|
||||||
|
|
||||||
@ -185,6 +184,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val defaultCategory = "default_category"
|
const val defaultCategory = "default_category"
|
||||||
|
|
||||||
|
const val categorizedDisplay = "categorized_display"
|
||||||
|
|
||||||
const val skipRead = "skip_read"
|
const val skipRead = "skip_read"
|
||||||
|
|
||||||
const val skipFiltered = "skip_filtered"
|
const val skipFiltered = "skip_filtered"
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
const val UNMETERED_NETWORK = "wifi"
|
const val UNMETERED_NETWORK = "wifi"
|
||||||
const val CHARGING = "ac"
|
const val CHARGING = "ac"
|
||||||
|
|
||||||
@ -17,35 +19,28 @@ object PreferenceValues {
|
|||||||
system,
|
system,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keys are lowercase to match legacy string values
|
|
||||||
enum class LightThemeVariant {
|
|
||||||
default,
|
|
||||||
blue,
|
|
||||||
strawberrydaiquiri,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys are lowercase to match legacy string values
|
|
||||||
enum class DarkThemeVariant {
|
|
||||||
default,
|
|
||||||
blue,
|
|
||||||
greenapple,
|
|
||||||
midnightdusk,
|
|
||||||
amoled,
|
|
||||||
hotpink,
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ktlint-enable experimental:enum-entry-name-case */
|
/* ktlint-enable experimental:enum-entry-name-case */
|
||||||
|
|
||||||
enum class DisplayMode {
|
enum class AppTheme(val titleResId: Int?) {
|
||||||
COMPACT_GRID,
|
DEFAULT(R.string.theme_default),
|
||||||
COMFORTABLE_GRID,
|
MONET(R.string.theme_monet),
|
||||||
LIST,
|
BLUE(R.string.theme_blue),
|
||||||
|
GREEN_APPLE(R.string.theme_greenapple),
|
||||||
|
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||||
|
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||||
|
TAKO(R.string.theme_tako),
|
||||||
|
YINYANG(R.string.theme_yinyang),
|
||||||
|
YOTSUBA(R.string.theme_yotsuba),
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
|
DARK_BLUE(null),
|
||||||
|
HOT_PINK(null),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
|
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
|
||||||
NONE,
|
NONE,
|
||||||
HORIZONTAL(shouldInvertHorizontal = true),
|
HORIZONTAL(shouldInvertHorizontal = true),
|
||||||
VERTICAL(shouldInvertVertical = true),
|
VERTICAL(shouldInvertVertical = true),
|
||||||
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true)
|
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,12 @@ 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.DisplayMode
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode.*
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
|
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.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
@ -66,7 +69,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun hideBottomBarOnScroll() = flowPrefs.getBoolean(Keys.hideBottomBarOnScroll, true)
|
fun hideBottomBarOnScroll() = flowPrefs.getBoolean(Keys.hideBottomBarOnScroll, true)
|
||||||
|
|
||||||
fun showSideNavOnBottom() = flowPrefs.getBoolean(Keys.showSideNavOnBottom, false)
|
fun sideNavIconAlignment() = flowPrefs.getInt(Keys.sideNavIconAlignment, 0)
|
||||||
|
|
||||||
fun useAuthenticator() = flowPrefs.getBoolean(Keys.useAuthenticator, false)
|
fun useAuthenticator() = flowPrefs.getBoolean(Keys.useAuthenticator, false)
|
||||||
|
|
||||||
@ -82,13 +85,13 @@ 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, false)
|
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, true)
|
||||||
|
|
||||||
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system)
|
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, system)
|
||||||
|
|
||||||
fun themeLight() = flowPrefs.getEnum(Keys.themeLight, Values.LightThemeVariant.default)
|
fun appTheme() = flowPrefs.getEnum(Keys.appTheme, Values.AppTheme.DEFAULT)
|
||||||
|
|
||||||
fun themeDark() = flowPrefs.getEnum(Keys.themeDark, Values.DarkThemeVariant.default)
|
fun themeDarkAmoled() = flowPrefs.getBoolean(Keys.themeDarkAmoled, false)
|
||||||
|
|
||||||
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
||||||
|
|
||||||
@ -174,15 +177,13 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||||
|
|
||||||
fun autoAddTrack() = prefs.getBoolean(Keys.autoAddTrack, true)
|
|
||||||
|
|
||||||
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
|
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
|
||||||
|
|
||||||
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
|
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
|
||||||
|
|
||||||
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
|
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
|
||||||
|
|
||||||
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayMode.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("en", Locale.getDefault().language))
|
||||||
|
|
||||||
@ -233,7 +234,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
||||||
|
|
||||||
fun libraryDisplayMode() = flowPrefs.getEnum(Keys.libraryDisplayMode, DisplayMode.COMPACT_GRID)
|
fun libraryDisplayMode() = flowPrefs.getEnum(Keys.libraryDisplayMode, DisplayModeSetting.COMPACT_GRID)
|
||||||
|
|
||||||
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
||||||
|
|
||||||
@ -255,9 +256,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun filterTracking(name: Int) = flowPrefs.getInt("${Keys.filterTracked}_$name", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
fun filterTracking(name: Int) = flowPrefs.getInt("${Keys.filterTracked}_$name", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||||
|
|
||||||
fun librarySortingMode() = flowPrefs.getInt(Keys.librarySortingMode, 0)
|
fun librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL)
|
||||||
|
fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING)
|
||||||
fun librarySortingAscending() = flowPrefs.getBoolean("library_sorting_ascending", true)
|
|
||||||
|
|
||||||
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
||||||
|
|
||||||
@ -280,10 +280,12 @@ 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() = prefs.getString(Keys.lang, "")
|
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 skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
||||||
|
|
||||||
fun skipFiltered() = prefs.getBoolean(Keys.skipFiltered, true)
|
fun skipFiltered() = prefs.getBoolean(Keys.skipFiltered, true)
|
||||||
|
@ -5,14 +5,21 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An Unattended Track Service will never prompt the user to match a manga with the remote.
|
* An Enhanced Track Service will never prompt the user to match a manga with the remote.
|
||||||
* It is expected that such Track Sercice can only work with specific sources and unique IDs.
|
* It is expected that such Track Service can only work with specific sources and unique IDs.
|
||||||
*/
|
*/
|
||||||
interface UnattendedTrackService {
|
interface EnhancedTrackService {
|
||||||
/**
|
/**
|
||||||
* This TrackService will only work with the sources that are accepted by this filter function.
|
* This TrackService will only work with the sources that are accepted by this filter function.
|
||||||
*/
|
*/
|
||||||
fun accept(source: Source): Boolean
|
fun accept(source: Source): Boolean {
|
||||||
|
return source::class.qualifiedName in getAcceptedSources()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully qualified source classes that this track service is compatible with.
|
||||||
|
*/
|
||||||
|
fun getAcceptedSources(): List<String>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* match is similar to TrackService.search, but only return zero or one match.
|
* match is similar to TrackService.search, but only return zero or one match.
|
@ -36,6 +36,10 @@ abstract class TrackService(val id: Int) {
|
|||||||
|
|
||||||
abstract fun getStatus(status: Int): String
|
abstract fun getStatus(status: Int): String
|
||||||
|
|
||||||
|
abstract fun getReadingStatus(): Int
|
||||||
|
|
||||||
|
abstract fun getRereadingStatus(): Int
|
||||||
|
|
||||||
abstract fun getCompletionStatus(): Int
|
abstract fun getCompletionStatus(): Int
|
||||||
|
|
||||||
abstract fun getScoreList(): List<String>
|
abstract fun getScoreList(): List<String>
|
||||||
@ -46,9 +50,9 @@ abstract class TrackService(val id: Int) {
|
|||||||
|
|
||||||
abstract fun displayScore(track: Track): String
|
abstract fun displayScore(track: Track): String
|
||||||
|
|
||||||
abstract suspend fun update(track: Track): Track
|
abstract suspend fun update(track: Track, didReadChapter: Boolean = false): Track
|
||||||
|
|
||||||
abstract suspend fun bind(track: Track): Track
|
abstract suspend fun bind(track: Track, hasReadChapters: Boolean = false): Track
|
||||||
|
|
||||||
abstract suspend fun search(query: String): List<TrackSearch>
|
abstract suspend fun search(query: String): List<TrackSearch>
|
||||||
|
|
||||||
|
@ -72,6 +72,10 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = REPEATING
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
override fun getScoreList(): List<String> {
|
||||||
@ -134,7 +138,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.addLibManga(track)
|
return api.addLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
// If user was using API v1 fetch library_id
|
// If user was using API v1 fetch library_id
|
||||||
if (track.library_id == null || track.library_id!! == 0L) {
|
if (track.library_id == null || track.library_id!! == 0L) {
|
||||||
val libManga = api.findLibManga(track, getUsername().toInt())
|
val libManga = api.findLibManga(track, getUsername().toInt())
|
||||||
@ -142,18 +146,30 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
track.library_id = libManga.library_id
|
track.library_id = libManga.library_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (track.status != REPEATING && didReadChapter) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val remoteTrack = api.findLibManga(track, getUsername().toInt())
|
val remoteTrack = api.findLibManga(track, getUsername().toInt())
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
val isRereading = track.status == REPEATING
|
||||||
|
track.status = if (isRereading.not() && hasReadChapters) READING else track.status
|
||||||
|
}
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = READING
|
track.status = if (hasReadChapters) READING else PLANNING
|
||||||
track.score = 0F
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,6 @@ package eu.kanade.tachiyomi.data.track.anilist
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.afollestad.date.dayOfMonth
|
|
||||||
import com.afollestad.date.month
|
|
||||||
import com.afollestad.date.year
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
@ -315,9 +312,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
val calendar = Calendar.getInstance()
|
val calendar = Calendar.getInstance()
|
||||||
calendar.timeInMillis = dateValue
|
calendar.timeInMillis = dateValue
|
||||||
return buildJsonObject {
|
return buildJsonObject {
|
||||||
put("year", calendar.year)
|
put("year", calendar.get(Calendar.YEAR))
|
||||||
put("month", calendar.month + 1)
|
put("month", calendar.get(Calendar.MONTH) + 1)
|
||||||
put("day", calendar.dayOfMonth)
|
put("day", calendar.get(Calendar.DAY_OF_MONTH))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,24 +35,34 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.addLibManga(track)
|
return api.addLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (didReadChapter) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val statusTrack = api.statusLibManga(track)
|
val statusTrack = api.statusLibManga(track)
|
||||||
val remoteTrack = api.findLibManga(track)
|
val remoteTrack = api.findLibManga(track)
|
||||||
return if (remoteTrack != null && statusTrack != null) {
|
return if (remoteTrack != null && statusTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
track.status = statusTrack.status
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
track.status = if (hasReadChapters) READING else statusTrack.status
|
||||||
|
}
|
||||||
|
|
||||||
track.score = statusTrack.score
|
track.score = statusTrack.score
|
||||||
track.last_chapter_read = statusTrack.last_chapter_read
|
track.last_chapter_read = statusTrack.last_chapter_read
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
refresh(track)
|
refresh(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = READING
|
track.status = if (hasReadChapters) READING else PLANNING
|
||||||
track.score = 0F
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
update(track)
|
update(track)
|
||||||
@ -91,6 +101,10 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = -1
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
override suspend fun login(username: String, password: String) = login(password)
|
override suspend fun login(username: String, password: String) = login(password)
|
||||||
|
@ -26,6 +26,8 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
@StringRes
|
@StringRes
|
||||||
override fun nameRes() = R.string.tracker_kitsu
|
override fun nameRes() = R.string.tracker_kitsu
|
||||||
|
|
||||||
|
override val supportsReadingDates: Boolean = true
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val interceptor by lazy { KitsuInterceptor(this) }
|
private val interceptor by lazy { KitsuInterceptor(this) }
|
||||||
@ -51,6 +53,10 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = -1
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
override fun getScoreList(): List<String> {
|
||||||
@ -71,18 +77,29 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.addLibManga(track, getUserId())
|
return api.addLibManga(track, getUserId())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (didReadChapter) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val remoteTrack = api.findLibManga(track, getUserId())
|
val remoteTrack = api.findLibManga(track, getUserId())
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.media_id = remoteTrack.media_id
|
track.media_id = remoteTrack.media_id
|
||||||
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
track.status = if (hasReadChapters) READING else track.status
|
||||||
|
}
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
track.status = READING
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
track.score = 0F
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,8 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
put("status", track.toKitsuStatus())
|
put("status", track.toKitsuStatus())
|
||||||
put("progress", track.last_chapter_read)
|
put("progress", track.last_chapter_read)
|
||||||
put("ratingTwenty", track.toKitsuScore())
|
put("ratingTwenty", track.toKitsuScore())
|
||||||
|
put("startedAt", KitsuDateHelper.convert(track.started_reading_date))
|
||||||
|
put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object KitsuDateHelper {
|
||||||
|
|
||||||
|
private const val pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||||
|
private val formatter = SimpleDateFormat(pattern, Locale.ENGLISH)
|
||||||
|
|
||||||
|
fun convert(dateValue: Long): String? {
|
||||||
|
if (dateValue == 0L) return null
|
||||||
|
|
||||||
|
return formatter.format(Date(dateValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parse(dateString: String?): Long {
|
||||||
|
if (dateString == null) return 0L
|
||||||
|
|
||||||
|
val dateValue = formatter.parse(dateString)
|
||||||
|
|
||||||
|
return dateValue?.time ?: return 0
|
||||||
|
}
|
||||||
|
}
|
@ -58,6 +58,8 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
|
|||||||
val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content
|
val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content
|
||||||
private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content
|
private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content
|
||||||
private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty()
|
private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty()
|
||||||
|
private val startedAt = obj["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull
|
||||||
|
private val finishedAt = obj["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull
|
||||||
private val libraryId = obj["id"]!!.jsonPrimitive.int
|
private val libraryId = obj["id"]!!.jsonPrimitive.int
|
||||||
val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content
|
val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content
|
||||||
private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull
|
private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull
|
||||||
@ -73,6 +75,8 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
|
|||||||
publishing_status = this@KitsuLibManga.status
|
publishing_status = this@KitsuLibManga.status
|
||||||
publishing_type = type
|
publishing_type = type
|
||||||
start_date = startDate
|
start_date = startDate
|
||||||
|
started_reading_date = KitsuDateHelper.parse(startedAt)
|
||||||
|
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
|
||||||
|
@ -6,22 +6,19 @@ import androidx.annotation.StringRes
|
|||||||
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.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.NoLoginTrackService
|
import eu.kanade.tachiyomi.data.track.NoLoginTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import okhttp3.Dns
|
import okhttp3.Dns
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class Komga(private val context: Context, id: Int) : TrackService(id), UnattendedTrackService, NoLoginTrackService {
|
class Komga(private val context: Context, id: Int) : TrackService(id), EnhancedTrackService, NoLoginTrackService {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val UNREAD = 1
|
const val UNREAD = 1
|
||||||
const val READING = 2
|
const val READING = 2
|
||||||
const val COMPLETED = 3
|
const val COMPLETED = 3
|
||||||
|
|
||||||
const val ACCEPTED_SOURCE = "eu.kanade.tachiyomi.extension.all.komga.Komga"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val client: OkHttpClient =
|
override val client: OkHttpClient =
|
||||||
@ -49,17 +46,27 @@ class Komga(private val context: Context, id: Int) : TrackService(id), Unattende
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = -1
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
override fun getScoreList(): List<String> = emptyList()
|
override fun getScoreList(): List<String> = emptyList()
|
||||||
|
|
||||||
override fun displayScore(track: Track): String = ""
|
override fun displayScore(track: Track): String = ""
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (didReadChapter) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateProgress(track)
|
return api.updateProgress(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +75,7 @@ class Komga(private val context: Context, id: Int) : TrackService(id), Unattende
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun refresh(track: Track): Track {
|
override suspend fun refresh(track: Track): Track {
|
||||||
val remoteTrack = api.getTrackSearch(track.tracking_url)!!
|
val remoteTrack = api.getTrackSearch(track.tracking_url)
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
return track
|
return track
|
||||||
@ -84,7 +91,7 @@ class Komga(private val context: Context, id: Int) : TrackService(id), Unattende
|
|||||||
saveCredentials("user", "pass")
|
saveCredentials("user", "pass")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun accept(source: Source): Boolean = source::class.qualifiedName == ACCEPTED_SOURCE
|
override fun getAcceptedSources() = listOf("eu.kanade.tachiyomi.extension.all.komga.Komga")
|
||||||
|
|
||||||
override suspend fun match(manga: Manga): TrackSearch? =
|
override suspend fun match(manga: Manga): TrackSearch? =
|
||||||
try {
|
try {
|
||||||
|
@ -56,6 +56,10 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = REREADING
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
override fun getScoreList(): List<String> {
|
||||||
@ -67,22 +71,35 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun add(track: Track): Track {
|
private suspend fun add(track: Track): Track {
|
||||||
track.status = READING
|
|
||||||
track.score = 0F
|
|
||||||
return api.updateItem(track)
|
return api.updateItem(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (track.status != REREADING && didReadChapter) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateItem(track)
|
return api.updateItem(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val remoteTrack = api.findListItem(track)
|
val remoteTrack = api.findListItem(track)
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.media_id = remoteTrack.media_id
|
track.media_id = remoteTrack.media_id
|
||||||
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
val isRereading = track.status == REREADING
|
||||||
|
track.status = if (isRereading.not() && hasReadChapters) READING else track.status
|
||||||
|
}
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,19 +44,31 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.addLibManga(track, getUsername())
|
return api.addLibManga(track, getUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (track.status != REPEATING && didReadChapter) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track, getUsername())
|
return api.updateLibManga(track, getUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val remoteTrack = api.findLibManga(track, getUsername())
|
val remoteTrack = api.findLibManga(track, getUsername())
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
val isRereading = track.status == REPEATING
|
||||||
|
track.status = if (isRereading.not() && hasReadChapters) READING else track.status
|
||||||
|
}
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = READING
|
track.status = if (hasReadChapters) READING else PLANNING
|
||||||
track.score = 0F
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
@ -94,6 +106,10 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = REPEATING
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
override suspend fun login(username: String, password: String) = login(password)
|
override suspend fun login(username: String, password: String) = login(password)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.github
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.updater.Release
|
import android.os.Build
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@ -15,16 +15,25 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
class GithubRelease(
|
class GithubRelease(
|
||||||
@SerialName("tag_name") val version: String,
|
@SerialName("tag_name") val version: String,
|
||||||
@SerialName("body") override val info: String,
|
@SerialName("body") val info: String,
|
||||||
@SerialName("assets") private val assets: List<Assets>
|
@SerialName("assets") private val assets: List<Assets>
|
||||||
) : Release {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get download link of latest release from the assets.
|
* Get download link of latest release from the assets.
|
||||||
* @return download link of latest release.
|
* @return download link of latest release.
|
||||||
*/
|
*/
|
||||||
override val downloadLink: String
|
fun getDownloadLink(): String {
|
||||||
get() = assets[0].downloadLink
|
val apkVariant = when (Build.SUPPORTED_ABIS[0]) {
|
||||||
|
"arm64-v8a" -> "-arm64-v8a"
|
||||||
|
"armeabi-v7a" -> "-armeabi-v7a"
|
||||||
|
"x86", "x86_64" -> "-x86"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return assets.find { it.downloadLink.contains("tachiyomi$apkVariant-") }?.downloadLink
|
||||||
|
?: assets[0].downloadLink
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assets class containing download url.
|
* Assets class containing download url.
|
@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.github
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
|
||||||
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
|
||||||
@ -21,7 +20,7 @@ class GithubUpdateChecker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkForUpdate(): UpdateResult {
|
suspend fun checkForUpdate(): GithubUpdateResult {
|
||||||
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"))
|
||||||
@ -32,7 +31,7 @@ class GithubUpdateChecker {
|
|||||||
if (isNewVersion(it.version)) {
|
if (isNewVersion(it.version)) {
|
||||||
GithubUpdateResult.NewUpdate(it)
|
GithubUpdateResult.NewUpdate(it)
|
||||||
} else {
|
} else {
|
||||||
GithubUpdateResult.NoNewUpdate()
|
GithubUpdateResult.NoNewUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
sealed class GithubUpdateResult {
|
||||||
|
class NewUpdate(val release: GithubRelease) : GithubUpdateResult()
|
||||||
|
object NoNewUpdate : GithubUpdateResult()
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
|
||||||
|
|
||||||
interface Release {
|
|
||||||
|
|
||||||
val info: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get download link of latest release.
|
|
||||||
* @return download link of latest release.
|
|
||||||
*/
|
|
||||||
val downloadLink: String
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
|
||||||
|
|
||||||
abstract class UpdateResult {
|
|
||||||
|
|
||||||
open class NewUpdate<T : Release>(val release: T) : UpdateResult()
|
|
||||||
open class NoNewUpdate : UpdateResult()
|
|
||||||
}
|
|
@ -8,7 +8,6 @@ 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.data.updater.github.GithubUpdateChecker
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@ -19,8 +18,8 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
|||||||
try {
|
try {
|
||||||
val result = GithubUpdateChecker().checkForUpdate()
|
val result = GithubUpdateChecker().checkForUpdate()
|
||||||
|
|
||||||
if (result is UpdateResult.NewUpdate<*>) {
|
if (result is GithubUpdateResult.NewUpdate) {
|
||||||
UpdaterNotifier(context).promptUpdate(result.release.downloadLink)
|
UpdaterNotifier(context).promptUpdate(result.release.getDownloadLink())
|
||||||
}
|
}
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.github
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
|
||||||
|
|
||||||
sealed class GithubUpdateResult : UpdateResult() {
|
|
||||||
|
|
||||||
class NewUpdate(release: GithubRelease) : UpdateResult.NewUpdate<GithubRelease>(release)
|
|
||||||
class NoNewUpdate : UpdateResult.NoNewUpdate()
|
|
||||||
}
|
|
@ -79,9 +79,6 @@ internal class ExtensionGithubApi {
|
|||||||
fun getApkUrl(extension: Extension.Available): String {
|
fun getApkUrl(extension: Extension.Available): String {
|
||||||
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
|
||||||
const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
|
||||||
|
@ -2,11 +2,10 @@ package eu.kanade.tachiyomi.network.interceptor
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@ -28,7 +27,7 @@ import java.util.concurrent.TimeUnit
|
|||||||
|
|
||||||
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val executor = ContextCompat.getMainExecutor(context)
|
||||||
|
|
||||||
private val networkHelper: NetworkHelper by injectLazy()
|
private val networkHelper: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
@ -92,7 +91,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||||
headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
|
headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
|
||||||
|
|
||||||
handler.post {
|
executor.execute {
|
||||||
val webview = WebView(context)
|
val webview = WebView(context)
|
||||||
webView = webview
|
webView = webview
|
||||||
webview.setDefaultSettings()
|
webview.setDefaultSettings()
|
||||||
@ -146,7 +145,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
// around 4 seconds but it can take more due to slow networks or server issues.
|
// around 4 seconds but it can take more due to slow networks or server issues.
|
||||||
latch.await(12, TimeUnit.SECONDS)
|
latch.await(12, TimeUnit.SECONDS)
|
||||||
|
|
||||||
handler.post {
|
executor.execute {
|
||||||
if (!cloudflareBypassed) {
|
if (!cloudflareBypassed) {
|
||||||
isWebViewOutdated = webView?.isOutdated() == true
|
isWebViewOutdated = webView?.isOutdated() == true
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,6 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
const val ID = 0L
|
const val ID = 0L
|
||||||
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
||||||
|
|
||||||
private const val COVER_NAME = "cover.jpg"
|
|
||||||
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
||||||
|
|
||||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||||
@ -40,18 +39,29 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
input.close()
|
input.close()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
val cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
|
||||||
|
|
||||||
// It might not exist if using the external SD card
|
if (cover != null && cover.exists()) {
|
||||||
cover.parentFile?.mkdirs()
|
// It might not exist if using the external SD card
|
||||||
input.use {
|
cover.parentFile?.mkdirs()
|
||||||
cover.outputStream().use {
|
input.use {
|
||||||
input.copyTo(it)
|
cover.outputStream().use {
|
||||||
|
input.copyTo(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cover
|
return cover
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns valid cover file inside [parent] directory.
|
||||||
|
*/
|
||||||
|
private fun getCoverFile(parent: File): File? {
|
||||||
|
return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
|
||||||
|
it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getBaseDirectories(context: Context): List<File> {
|
private fun getBaseDirectories(context: Context): List<File> {
|
||||||
val c = context.getString(R.string.app_name) + File.separator + "local"
|
val c = context.getString(R.string.app_name) + File.separator + "local"
|
||||||
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
||||||
@ -84,9 +94,9 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
when (state?.index) {
|
when (state?.index) {
|
||||||
0 -> {
|
0 -> {
|
||||||
mangaDirs = if (state.ascending) {
|
mangaDirs = if (state.ascending) {
|
||||||
mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
|
mangaDirs.sortedBy { it.name.lowercase(Locale.ENGLISH) }
|
||||||
} else {
|
} else {
|
||||||
mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
|
mangaDirs.sortedByDescending { it.name.lowercase(Locale.ENGLISH) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
@ -105,8 +115,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
// Try to find the cover
|
// Try to find the cover
|
||||||
for (dir in baseDirs) {
|
for (dir in baseDirs) {
|
||||||
val cover = File("${dir.absolutePath}/$url", COVER_NAME)
|
val cover = getCoverFile(File("${dir.absolutePath}/$url"))
|
||||||
if (cover.exists()) {
|
if (cover != null && cover.exists()) {
|
||||||
thumbnail_url = cover.absolutePath
|
thumbnail_url = cover.absolutePath
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -238,7 +248,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isSupportedFile(extension: String): Boolean {
|
private fun isSupportedFile(extension: String): Boolean {
|
||||||
return extension.toLowerCase(Locale.ROOT) in SUPPORTED_ARCHIVE_TYPES
|
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFormat(chapter: SChapter): Format {
|
fun getFormat(chapter: SChapter): Format {
|
||||||
|
@ -70,8 +70,11 @@ open class SourceManager(private val context: Context) {
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSourceNotInstalledException(): Exception {
|
private fun getSourceNotInstalledException(): SourceNotInstalledException {
|
||||||
return Exception(context.getString(R.string.source_not_installed, id.toString()))
|
return SourceNotInstalledException(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class SourceNotInstalledException(val id: Long) :
|
||||||
|
Exception(context.getString(R.string.source_not_installed, id.toString()))
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* Note the generated id sets the sign bit to 0.
|
* Note the generated id sets the sign bit to 0.
|
||||||
*/
|
*/
|
||||||
override val id by lazy {
|
override val id by lazy {
|
||||||
val key = "${name.toLowerCase()}/$lang/$versionId"
|
val key = "${name.lowercase()}/$lang/$versionId"
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Visible name of the source.
|
* Visible name of the source.
|
||||||
*/
|
*/
|
||||||
override fun toString() = "$name (${lang.toUpperCase()})"
|
override fun toString() = "$name (${lang.uppercase()})"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||||
@ -341,7 +341,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*/
|
*/
|
||||||
private fun getUrlWithoutDomain(orig: String): String {
|
private fun getUrlWithoutDomain(orig: String): String {
|
||||||
return try {
|
return try {
|
||||||
val uri = URI(orig)
|
val uri = URI(orig.replace(" ", "%20"))
|
||||||
var out = uri.path
|
var out = uri.path
|
||||||
if (uri.query != null) {
|
if (uri.query != null) {
|
||||||
out += "?" + uri.query
|
out += "?" + uri.query
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.activity
|
package eu.kanade.tachiyomi.ui.base.activity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
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
|
||||||
@ -14,9 +15,8 @@ abstract class BaseRxActivity<VB : ViewBinding, P : BasePresenter<*>> : NucleusA
|
|||||||
|
|
||||||
lateinit var binding: VB
|
lateinit var binding: VB
|
||||||
|
|
||||||
init {
|
override fun attachBaseContext(newBase: Context) {
|
||||||
@Suppress("LeakingThis")
|
super.attachBaseContext(LocaleHelper.createLocaleWrapper(newBase))
|
||||||
LocaleHelper.updateConfiguration(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -1,43 +1,68 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.activity
|
package eu.kanade.tachiyomi.ui.base.activity
|
||||||
|
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
import android.content.Context
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DarkThemeVariant
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.LightThemeVariant
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
abstract class BaseThemedActivity : AppCompatActivity() {
|
abstract class BaseThemedActivity : AppCompatActivity() {
|
||||||
|
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context) {
|
||||||
|
super.attachBaseContext(LocaleHelper.createLocaleWrapper(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val isDarkMode = when (preferences.themeMode().get()) {
|
applyAppTheme(preferences)
|
||||||
ThemeMode.light -> false
|
|
||||||
ThemeMode.dark -> true
|
|
||||||
ThemeMode.system -> resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
|
|
||||||
}
|
|
||||||
val themeId = if (isDarkMode) {
|
|
||||||
when (preferences.themeDark().get()) {
|
|
||||||
DarkThemeVariant.default -> R.style.Theme_Tachiyomi_Dark
|
|
||||||
DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_Dark_Blue
|
|
||||||
DarkThemeVariant.greenapple -> R.style.Theme_Tachiyomi_Dark_GreenApple
|
|
||||||
DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_Dark_MidnightDusk
|
|
||||||
DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
|
|
||||||
DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_Amoled_HotPink
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (preferences.themeLight().get()) {
|
|
||||||
LightThemeVariant.default -> R.style.Theme_Tachiyomi_Light
|
|
||||||
LightThemeVariant.blue -> R.style.Theme_Tachiyomi_Light_Blue
|
|
||||||
LightThemeVariant.strawberrydaiquiri -> R.style.Theme_Tachiyomi_Light_StrawberryDaiquiri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTheme(themeId)
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun AppCompatActivity.applyAppTheme(preferences: PreferencesHelper) {
|
||||||
|
val resIds = mutableListOf<Int>()
|
||||||
|
when (preferences.appTheme().get()) {
|
||||||
|
PreferenceValues.AppTheme.MONET -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_Monet
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.BLUE -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_Blue
|
||||||
|
resIds += R.style.ThemeOverlay_Tachiyomi_ColoredBars
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.GREEN_APPLE -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_GreenApple
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.MIDNIGHT_DUSK -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_MidnightDusk
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.STRAWBERRY_DAIQUIRI -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_StrawberryDaiquiri
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.TAKO -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_Tako
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.YINYANG -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_YinYang
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.YOTSUBA -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_Yotsuba
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferences.themeDarkAmoled().get()) {
|
||||||
|
resIds += R.style.ThemeOverlay_Tachiyomi_Amoled
|
||||||
|
}
|
||||||
|
|
||||||
|
resIds.forEach {
|
||||||
|
setTheme(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.base.activity
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
|
|
||||||
abstract class BaseViewBindingActivity<VB : ViewBinding> : BaseThemedActivity() {
|
abstract class BaseViewBindingActivity<VB : ViewBinding> : BaseThemedActivity() {
|
||||||
|
|
||||||
@ -12,11 +11,6 @@ abstract class BaseViewBindingActivity<VB : ViewBinding> : BaseThemedActivity()
|
|||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
private val secureActivityDelegate = SecureActivityDelegate(this)
|
private val secureActivityDelegate = SecureActivityDelegate(this)
|
||||||
|
|
||||||
init {
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
LocaleHelper.updateConfiguration(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -10,14 +10,12 @@ import androidx.viewbinding.ViewBinding
|
|||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
import com.bluelinelabs.conductor.RestoreViewOnCreateController
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) : Controller(bundle) {
|
||||||
RestoreViewOnCreateController(bundle) {
|
|
||||||
|
|
||||||
protected lateinit var binding: VB
|
protected lateinit var binding: VB
|
||||||
private set
|
private set
|
||||||
|
@ -5,7 +5,7 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.bluelinelabs.conductor.RestoreViewOnCreateController
|
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 com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
|
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
|
||||||
@ -16,7 +16,7 @@ import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
|
|||||||
*
|
*
|
||||||
* Implementations should override this class and implement [.onCreateDialog] to create a custom dialog, such as an [android.app.AlertDialog]
|
* Implementations should override this class and implement [.onCreateDialog] to create a custom dialog, such as an [android.app.AlertDialog]
|
||||||
*/
|
*/
|
||||||
abstract class DialogController : RestoreViewOnCreateController {
|
abstract class DialogController : Controller {
|
||||||
|
|
||||||
protected var dialog: Dialog? = null
|
protected var dialog: Dialog? = null
|
||||||
private set
|
private set
|
||||||
|
@ -32,6 +32,12 @@ open class BasePresenter<V> : RxPresenter<V>() {
|
|||||||
presenterScope.cancel()
|
presenterScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We're trying to avoid using Rx, so we "undeprecate" this
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun getView(): V? {
|
||||||
|
return super.getView()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
|
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
|
||||||
* subscription list.
|
* subscription list.
|
||||||
|
@ -9,7 +9,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
|
|||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
import com.bluelinelabs.conductor.Router
|
import com.bluelinelabs.conductor.Router
|
||||||
import com.bluelinelabs.conductor.RouterTransaction
|
import com.bluelinelabs.conductor.RouterTransaction
|
||||||
import com.bluelinelabs.conductor.support.RouterPagerAdapter
|
import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter
|
||||||
import com.google.android.material.badge.BadgeDrawable
|
import com.google.android.material.badge.BadgeDrawable
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
|
@ -136,7 +136,7 @@ open class ExtensionController :
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchView.queryTextChanges()
|
searchView.queryTextChanges()
|
||||||
.filter { router.backstack.lastOrNull()?.controller() == this }
|
.filter { router.backstack.lastOrNull()?.controller == this }
|
||||||
.onEach {
|
.onEach {
|
||||||
query = it.toString()
|
query = it.toString()
|
||||||
drawExtensions()
|
drawExtensions()
|
||||||
|
@ -40,7 +40,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||||||
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 && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
||||||
else -> ""
|
else -> ""
|
||||||
}.toUpperCase()
|
}.uppercase()
|
||||||
|
|
||||||
binding.image.clear()
|
binding.image.clear()
|
||||||
if (extension is Extension.Available) {
|
if (extension is Extension.Available) {
|
||||||
|
@ -61,9 +61,9 @@ open class ExtensionPresenter(
|
|||||||
|
|
||||||
val items = mutableListOf<ExtensionItem>()
|
val items = mutableListOf<ExtensionItem>()
|
||||||
|
|
||||||
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.pkgName }
|
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.name }
|
||||||
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.pkgName }))
|
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.name }))
|
||||||
val untrustedSorted = untrusted.sortedBy { it.pkgName }
|
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
|
||||||
.filter { avail ->
|
.filter { avail ->
|
||||||
@ -82,9 +82,11 @@ open class ExtensionPresenter(
|
|||||||
}
|
}
|
||||||
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])
|
||||||
}
|
}
|
||||||
|
|
||||||
items += untrustedSorted.map { extension ->
|
items += untrustedSorted.map { extension ->
|
||||||
ExtensionItem(extension, header)
|
ExtensionItem(extension, header)
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.browse.extension
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
|
||||||
@ -21,15 +21,16 @@ class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
return MaterialDialog(activity!!)
|
return MaterialAlertDialogBuilder(activity!!)
|
||||||
.title(R.string.untrusted_extension)
|
.setTitle(R.string.untrusted_extension)
|
||||||
.message(R.string.untrusted_extension_message)
|
.setMessage(R.string.untrusted_extension_message)
|
||||||
.positiveButton(R.string.ext_trust) {
|
.setPositiveButton(R.string.ext_trust) { _, _ ->
|
||||||
(targetController as? Listener)?.trustSignature(args.getString(SIGNATURE_KEY)!!)
|
(targetController as? Listener)?.trustSignature(args.getString(SIGNATURE_KEY)!!)
|
||||||
}
|
}
|
||||||
.negativeButton(R.string.ext_uninstall) {
|
.setNegativeButton(R.string.ext_uninstall) { _, _ ->
|
||||||
(targetController as? Listener)?.uninstallExtension(args.getString(PKGNAME_KEY)!!)
|
(targetController as? Listener)?.uninstallExtension(args.getString(PKGNAME_KEY)!!)
|
||||||
}
|
}
|
||||||
|
.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
@ -114,7 +114,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
.forEach {
|
.forEach {
|
||||||
val preferenceBlock = {
|
val preferenceBlock = {
|
||||||
it.value
|
it.value
|
||||||
.sortedWith(compareBy({ !it.isEnabled() }, { it.name.toLowerCase() }))
|
.sortedWith(compareBy({ !it.isEnabled() }, { it.name.lowercase() }))
|
||||||
.forEach { source ->
|
.forEach { source ->
|
||||||
val sourcePrefs = mutableListOf<Preference>()
|
val sourcePrefs = mutableListOf<Preference>()
|
||||||
|
|
||||||
|
@ -3,10 +3,9 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import com.afollestad.materialdialogs.list.listItemsMultiChoice
|
|
||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.bluelinelabs.conductor.RouterTransaction
|
import com.bluelinelabs.conductor.RouterTransaction
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@ -86,28 +85,29 @@ class SearchController(
|
|||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
val prefValue = preferences.migrateFlags().get()
|
val prefValue = preferences.migrateFlags().get()
|
||||||
|
val enabledFlagsPositions = MigrationFlags.getEnabledFlagsPositions(prefValue)
|
||||||
|
val items = MigrationFlags.titles
|
||||||
|
.map { resources?.getString(it) }
|
||||||
|
.toTypedArray()
|
||||||
|
val selected = items
|
||||||
|
.mapIndexed { i, _ -> enabledFlagsPositions.contains(i) }
|
||||||
|
.toBooleanArray()
|
||||||
|
|
||||||
val preselected =
|
return MaterialAlertDialogBuilder(activity!!)
|
||||||
MigrationFlags.getEnabledFlagsPositions(
|
.setTitle(R.string.migration_dialog_what_to_include)
|
||||||
prefValue
|
.setMultiChoiceItems(items, selected) { _, which, checked ->
|
||||||
)
|
selected[which] = checked
|
||||||
|
|
||||||
return MaterialDialog(activity!!)
|
|
||||||
.title(R.string.migration_dialog_what_to_include)
|
|
||||||
.listItemsMultiChoice(
|
|
||||||
items = MigrationFlags.titles.map { resources?.getString(it) as CharSequence },
|
|
||||||
initialSelection = preselected.toIntArray()
|
|
||||||
) { _, positions, _ ->
|
|
||||||
// Save current settings for the next time
|
|
||||||
val newValue =
|
|
||||||
MigrationFlags.getFlagsFromPositions(
|
|
||||||
positions.toTypedArray()
|
|
||||||
)
|
|
||||||
preferences.migrateFlags().set(newValue)
|
|
||||||
}
|
}
|
||||||
.positiveButton(R.string.migrate) {
|
.setPositiveButton(R.string.migrate) { _, _ ->
|
||||||
|
// Save current settings for the next time
|
||||||
|
val selectedIndices = mutableListOf<Int>()
|
||||||
|
selected.forEachIndexed { i, b -> if (b) selectedIndices.add(i) }
|
||||||
|
val newValue = MigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray())
|
||||||
|
preferences.migrateFlags().set(newValue)
|
||||||
|
|
||||||
if (callingController != null) {
|
if (callingController != null) {
|
||||||
if (callingController.javaClass == SourceSearchController::class.java) {
|
if (callingController.javaClass == SourceSearchController::class.java) {
|
||||||
router.popController(callingController)
|
router.popController(callingController)
|
||||||
@ -115,7 +115,7 @@ class SearchController(
|
|||||||
}
|
}
|
||||||
(targetController as? SearchController)?.migrateManga(manga, newManga)
|
(targetController as? SearchController)?.migrateManga(manga, newManga)
|
||||||
}
|
}
|
||||||
.negativeButton(R.string.copy) {
|
.setNegativeButton(R.string.copy) { _, _, ->
|
||||||
if (callingController != null) {
|
if (callingController != null) {
|
||||||
if (callingController.javaClass == SourceSearchController::class.java) {
|
if (callingController.javaClass == SourceSearchController::class.java) {
|
||||||
router.popController(callingController)
|
router.popController(callingController)
|
||||||
@ -123,7 +123,8 @@ class SearchController(
|
|||||||
}
|
}
|
||||||
(targetController as? SearchController)?.copyManga(manga, newManga)
|
(targetController as? SearchController)?.copyManga(manga, newManga)
|
||||||
}
|
}
|
||||||
.neutralButton(android.R.string.cancel)
|
.setNeutralButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,13 +26,18 @@ class SourceSearchController(
|
|||||||
override fun onItemClick(view: View, position: Int): Boolean {
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
||||||
newManga = item.manga
|
newManga = item.manga
|
||||||
val searchController = router.backstack.findLast { it.controller().javaClass == SearchController::class.java }?.controller() as SearchController?
|
val searchController = router.backstack.findLast { it.controller.javaClass == SearchController::class.java }?.controller as SearchController?
|
||||||
val dialog =
|
val dialog =
|
||||||
SearchController.MigrationDialog(oldManga, newManga, this)
|
SearchController.MigrationDialog(oldManga, newManga, this)
|
||||||
dialog.targetController = searchController
|
dialog.targetController = searchController
|
||||||
dialog.showDialog(router)
|
dialog.showDialog(router)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onItemLongClick(position: Int) {
|
||||||
|
view?.let { super.onItemClick(it, position) }
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val MANGA_KEY = "oldManga"
|
const val MANGA_KEY = "oldManga"
|
||||||
}
|
}
|
||||||
|
@ -72,8 +72,6 @@ class MigrationSourcesController :
|
|||||||
parentController!!.router.pushController(controller.withFadeTransaction())
|
parentController!!.router.pushController(controller.withFadeTransaction())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val HELP_URL = "https://tachiyomi.org/help/guides/source-migration/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val HELP_URL = "https://tachiyomi.org/help/guides/source-migration/"
|
||||||
|
@ -34,7 +34,7 @@ class MigrationSourcesPresenter(
|
|||||||
val source = sourceManager.getOrStub(it.key)
|
val source = sourceManager.getOrStub(it.key)
|
||||||
SourceItem(source, it.value.size, header)
|
SourceItem(source, it.value.size, header)
|
||||||
}
|
}
|
||||||
.sortedBy { it.source.name.toLowerCase() }
|
.sortedBy { it.source.name.lowercase() }
|
||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,9 @@ import android.view.MenuInflater
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import com.afollestad.materialdialogs.list.listItems
|
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
@ -32,6 +31,8 @@ import eu.kanade.tachiyomi.ui.browse.BrowseController
|
|||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||||
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@ -82,6 +83,9 @@ class SourceController :
|
|||||||
// Create recycler and set adapter.
|
// Create recycler and set adapter.
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
binding.recycler.adapter = adapter
|
binding.recycler.adapter = adapter
|
||||||
|
binding.recycler.onAnimationsFinished {
|
||||||
|
(activity as? MainActivity)?.ready = true
|
||||||
|
}
|
||||||
adapter?.fastScroller = binding.fastScroller
|
adapter?.fastScroller = binding.fastScroller
|
||||||
|
|
||||||
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
||||||
@ -238,15 +242,13 @@ class SourceController :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
return MaterialDialog(activity!!)
|
return MaterialAlertDialogBuilder(activity!!)
|
||||||
.title(text = source)
|
.setTitle(source)
|
||||||
.listItems(
|
.setItems(items.map { it.first }.toTypedArray()) { dialog, which ->
|
||||||
items = items.map { it.first },
|
|
||||||
waitForPositiveButton = false
|
|
||||||
) { dialog, which, _ ->
|
|
||||||
items[which].second()
|
items[which].second()
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
|
.create()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class SourceFilterController : SettingsController() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
orderedLangs.forEach { lang ->
|
orderedLangs.forEach { lang ->
|
||||||
val sources = sourcesByLang[lang].orEmpty().sortedBy { it.name.toLowerCase() }
|
val sources = sourcesByLang[lang].orEmpty().sortedBy { it.name.lowercase() }
|
||||||
|
|
||||||
// Create a preference group and set initial state and change listener
|
// Create a preference group and set initial state and change listener
|
||||||
switchPreferenceCategory {
|
switchPreferenceCategory {
|
||||||
|
@ -122,7 +122,7 @@ class SourcePresenter(
|
|||||||
return sourceManager.getCatalogueSources()
|
return sourceManager.getCatalogueSources()
|
||||||
.filter { it.lang in languages }
|
.filter { it.lang in languages }
|
||||||
.filterNot { it.id.toString() in disabledSourceIds }
|
.filterNot { it.id.toString() in disabledSourceIds }
|
||||||
.sortedBy { "(${it.lang}) ${it.name.toLowerCase()}" } +
|
.sortedBy { "(${it.lang}) ${it.name.lowercase()}" } +
|
||||||
sourceManager.get(LocalSource.ID) as LocalSource
|
sourceManager.get(LocalSource.ID) as LocalSource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,8 +13,7 @@ import androidx.core.view.updatePadding
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.afollestad.materialdialogs.list.listItems
|
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.tfcporciuncula.flow.Preference
|
import com.tfcporciuncula.flow.Preference
|
||||||
@ -24,12 +23,12 @@ import eu.davidea.flexibleadapter.items.IFlexible
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
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.databinding.SourceControllerBinding
|
import eu.kanade.tachiyomi.databinding.SourceControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||||
@ -37,6 +36,7 @@ import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.more.MoreController
|
import eu.kanade.tachiyomi.ui.more.MoreController
|
||||||
@ -205,7 +205,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
binding.catalogueView.removeView(oldRecycler)
|
binding.catalogueView.removeView(oldRecycler)
|
||||||
}
|
}
|
||||||
|
|
||||||
val recycler = if (preferences.sourceDisplayMode().get() == DisplayMode.LIST) {
|
val recycler = if (preferences.sourceDisplayMode().get() == DisplayModeSetting.LIST) {
|
||||||
RecyclerView(view.context).apply {
|
RecyclerView(view.context).apply {
|
||||||
id = R.id.recycler
|
id = R.id.recycler
|
||||||
layoutManager = LinearLayoutManager(context)
|
layoutManager = LinearLayoutManager(context)
|
||||||
@ -261,7 +261,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
searchItem.fixExpand(
|
searchItem.fixExpand(
|
||||||
onExpand = { invalidateMenuOnExpand() },
|
onExpand = { invalidateMenuOnExpand() },
|
||||||
onCollapse = {
|
onCollapse = {
|
||||||
if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller() is GlobalSearchController) {
|
if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller is GlobalSearchController) {
|
||||||
router.popController(this)
|
router.popController(this)
|
||||||
} else {
|
} else {
|
||||||
nonSubmittedQuery = ""
|
nonSubmittedQuery = ""
|
||||||
@ -273,9 +273,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
)
|
)
|
||||||
|
|
||||||
val displayItem = when (preferences.sourceDisplayMode().get()) {
|
val displayItem = when (preferences.sourceDisplayMode().get()) {
|
||||||
DisplayMode.COMPACT_GRID -> R.id.action_compact_grid
|
DisplayModeSetting.COMPACT_GRID -> R.id.action_compact_grid
|
||||||
DisplayMode.COMFORTABLE_GRID -> R.id.action_comfortable_grid
|
DisplayModeSetting.COMFORTABLE_GRID -> R.id.action_comfortable_grid
|
||||||
DisplayMode.LIST -> R.id.action_list
|
DisplayModeSetting.LIST -> R.id.action_list
|
||||||
}
|
}
|
||||||
menu.findItem(displayItem).isChecked = true
|
menu.findItem(displayItem).isChecked = true
|
||||||
}
|
}
|
||||||
@ -297,9 +297,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_search -> expandActionViewFromInteraction = true
|
R.id.action_search -> expandActionViewFromInteraction = true
|
||||||
R.id.action_compact_grid -> setDisplayMode(DisplayMode.COMPACT_GRID)
|
R.id.action_compact_grid -> setDisplayMode(DisplayModeSetting.COMPACT_GRID)
|
||||||
R.id.action_comfortable_grid -> setDisplayMode(DisplayMode.COMFORTABLE_GRID)
|
R.id.action_comfortable_grid -> setDisplayMode(DisplayModeSetting.COMFORTABLE_GRID)
|
||||||
R.id.action_list -> setDisplayMode(DisplayMode.LIST)
|
R.id.action_list -> setDisplayMode(DisplayModeSetting.LIST)
|
||||||
R.id.action_open_in_web_view -> openInWebView()
|
R.id.action_open_in_web_view -> openInWebView()
|
||||||
R.id.action_local_source_help -> openLocalSourceHelpGuide()
|
R.id.action_local_source_help -> openLocalSourceHelpGuide()
|
||||||
}
|
}
|
||||||
@ -335,6 +335,54 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
presenter.restartPager(newQuery)
|
presenter.restartPager(newQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to restart the request with a new genre-filtered query.
|
||||||
|
* If the genre name can't be found the filters,
|
||||||
|
* the standard searchWithQuery search method is used instead.
|
||||||
|
*
|
||||||
|
* @param genreName the name of the genre
|
||||||
|
*/
|
||||||
|
fun searchWithGenre(genreName: String) {
|
||||||
|
presenter.sourceFilters = presenter.source.getFilterList()
|
||||||
|
|
||||||
|
var filterList: FilterList? = null
|
||||||
|
|
||||||
|
filter@ for (sourceFilter in presenter.sourceFilters) {
|
||||||
|
if (sourceFilter is Filter.Group<*>) {
|
||||||
|
for (filter in sourceFilter.state) {
|
||||||
|
if (filter is Filter<*> && filter.name.equals(genreName, true)) {
|
||||||
|
when (filter) {
|
||||||
|
is Filter.TriState -> filter.state = 1
|
||||||
|
is Filter.CheckBox -> filter.state = true
|
||||||
|
}
|
||||||
|
filterList = presenter.sourceFilters
|
||||||
|
break@filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (sourceFilter is Filter.Select<*>) {
|
||||||
|
val index = sourceFilter.values.filterIsInstance<String>()
|
||||||
|
.indexOfFirst { it.equals(genreName, true) }
|
||||||
|
|
||||||
|
if (index != -1) {
|
||||||
|
sourceFilter.state = index
|
||||||
|
filterList = presenter.sourceFilters
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterList != null) {
|
||||||
|
filterSheet?.setFilters(presenter.filterItems)
|
||||||
|
|
||||||
|
showProgressBar()
|
||||||
|
|
||||||
|
adapter?.clear()
|
||||||
|
presenter.restartPager("", filterList)
|
||||||
|
} else {
|
||||||
|
searchWithQuery(genreName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from the presenter when the network request is received.
|
* Called from the presenter when the network request is received.
|
||||||
*
|
*
|
||||||
@ -446,7 +494,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
*
|
*
|
||||||
* @param mode the mode to change to
|
* @param mode the mode to change to
|
||||||
*/
|
*/
|
||||||
private fun setDisplayMode(mode: DisplayMode) {
|
private fun setDisplayMode(mode: DisplayModeSetting) {
|
||||||
val view = view ?: return
|
val view = view ?: return
|
||||||
val adapter = adapter ?: return
|
val adapter = adapter ?: return
|
||||||
|
|
||||||
@ -540,11 +588,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
|
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
|
||||||
|
|
||||||
if (manga.favorite) {
|
if (manga.favorite) {
|
||||||
MaterialDialog(activity)
|
MaterialAlertDialogBuilder(activity)
|
||||||
.listItems(
|
.setTitle(manga.title)
|
||||||
items = listOf(activity.getString(R.string.remove_from_library)),
|
.setItems(arrayOf(activity.getString(R.string.remove_from_library))) { _, which ->
|
||||||
waitForPositiveButton = false
|
|
||||||
) { _, which, _ ->
|
|
||||||
when (which) {
|
when (which) {
|
||||||
0 -> {
|
0 -> {
|
||||||
presenter.changeMangaFavorite(manga)
|
presenter.changeMangaFavorite(manga)
|
||||||
|
@ -9,9 +9,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
@ -37,6 +37,7 @@ import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
|
|||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.removeCovers
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
@ -44,7 +45,6 @@ import kotlinx.coroutines.flow.collect
|
|||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
@ -104,7 +104,7 @@ open class BrowseSourcePresenter(
|
|||||||
/**
|
/**
|
||||||
* Subscription for one request from the pager.
|
* Subscription for one request from the pager.
|
||||||
*/
|
*/
|
||||||
private var pageSubscription: Subscription? = null
|
private var nextPageJob: Job? = null
|
||||||
|
|
||||||
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
|
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
|
||||||
|
|
||||||
@ -175,14 +175,14 @@ open class BrowseSourcePresenter(
|
|||||||
fun requestNext() {
|
fun requestNext() {
|
||||||
if (!hasNextPage()) return
|
if (!hasNextPage()) return
|
||||||
|
|
||||||
pageSubscription?.let { remove(it) }
|
nextPageJob?.cancel()
|
||||||
pageSubscription = Observable.defer { pager.requestNext() }
|
nextPageJob = launchIO {
|
||||||
.subscribeFirst(
|
try {
|
||||||
{ _, _ ->
|
pager.requestNextPage()
|
||||||
// Nothing to do when onNext is emitted.
|
} catch (e: Throwable) {
|
||||||
},
|
withUIContext { view?.onAddPageError(e) }
|
||||||
BrowseSourceController::onAddPageError
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -267,9 +267,7 @@ open class BrowseSourcePresenter(
|
|||||||
} else {
|
} else {
|
||||||
ChapterSettingsHelper.applySettingDefaults(manga)
|
ChapterSettingsHelper.applySettingDefaults(manga)
|
||||||
|
|
||||||
if (prefs.autoAddTrack()) {
|
autoAddTrack(manga)
|
||||||
autoAddTrack(manga)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
@ -277,7 +275,7 @@ open class BrowseSourcePresenter(
|
|||||||
|
|
||||||
private fun autoAddTrack(manga: Manga) {
|
private fun autoAddTrack(manga: Manga) {
|
||||||
loggedServices
|
loggedServices
|
||||||
.filterIsInstance<UnattendedTrackService>()
|
.filterIsInstance<EnhancedTrackService>()
|
||||||
.filter { it.accept(source) }
|
.filter { it.accept(source) }
|
||||||
.forEach { service ->
|
.forEach { service ->
|
||||||
launchIO {
|
launchIO {
|
||||||
|
@ -19,7 +19,7 @@ abstract class Pager(var currentPage: Int = 1) {
|
|||||||
return results.asObservable()
|
return results.asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun requestNext(): Observable<MangasPage>
|
abstract suspend fun requestNextPage()
|
||||||
|
|
||||||
fun onPageReceived(mangasPage: MangasPage) {
|
fun onPageReceived(mangasPage: MangasPage) {
|
||||||
val page = currentPage
|
val page = currentPage
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import coil.clear
|
import coil.clear
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.CachePolicy
|
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.transition.CrossfadeTransition
|
import coil.transition.CrossfadeTransition
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
@ -38,6 +38,12 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F
|
|||||||
// Set alpha of thumbnail.
|
// Set alpha of thumbnail.
|
||||||
binding.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
|
binding.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
|
||||||
|
|
||||||
|
// For rounded corners
|
||||||
|
binding.badges.clipToOutline = true
|
||||||
|
|
||||||
|
// Set favorite badge
|
||||||
|
binding.favoriteText.isVisible = manga.favorite
|
||||||
|
|
||||||
setImage(manga)
|
setImage(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +59,6 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F
|
|||||||
val request = ImageRequest.Builder(view.context)
|
val request = ImageRequest.Builder(view.context)
|
||||||
.data(manga)
|
.data(manga)
|
||||||
.setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
.setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
||||||
.diskCachePolicy(CachePolicy.DISABLED)
|
|
||||||
.target(StateImageViewTarget(binding.thumbnail, binding.progress, crossfadeDuration))
|
.target(StateImageViewTarget(binding.thumbnail, binding.progress, crossfadeDuration))
|
||||||
.build()
|
.build()
|
||||||
itemView.context.imageLoader.enqueue(request)
|
itemView.context.imageLoader.enqueue(request)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user