mirror of
https://github.com/mihonapp/mihon.git
synced 2025-07-28 10:25:53 +02:00
Compare commits
190 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 have updated:
|
||||
- To the latest version of the app (stable is v0.11.0)
|
||||
- To the latest version of the app (stable is v0.11.1)
|
||||
- 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
|
||||
|
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
|
||||
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/
|
||||
about: Common questions are answered here.
|
||||
- name: Tachiyomi extensions GitHub repository
|
||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
about: Issues about an extension/source/catalogue should be opened here instead.
|
||||
about: Guides, troubleshooting, and answers to common questions
|
||||
|
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.11.1](https://github.com/tachiyomiorg/tachiyomi/releases/tag/v0.11.1)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: tachiyomi-version
|
||||
attributes:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.11.1"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: android-version
|
||||
attributes:
|
||||
label: Android version
|
||||
description: You can find this somewhere in your Android settings.
|
||||
placeholder: |
|
||||
Example: "Android 11"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: Device
|
||||
description: List your device and model.
|
||||
placeholder: |
|
||||
Example: "Google Pixel 5"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
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.11.1](https://github.com/tachiyomiorg/tachiyomi/releases/tag/v0.11.1)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
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:
|
||||
name: Build app
|
||||
needs: check_wrapper
|
||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@ -61,6 +60,8 @@ jobs:
|
||||
set -x
|
||||
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||
|
||||
# TODO: need to support multiple APKs
|
||||
|
||||
- name: Sign APK
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
|
16
.github/workflows/issue_closer.yml
vendored
16
.github/workflows/issue_closer.yml
vendored
@ -13,16 +13,6 @@ jobs:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
@ -32,5 +22,11 @@ jobs:
|
||||
"type": "body",
|
||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||
"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
|
||||
steps:
|
||||
- name: Moderate issues
|
||||
uses: tachiyomiorg/issue-moderator-action@v1.0
|
||||
uses: tachiyomiorg/issue-moderator-action@v1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
@ -26,7 +26,7 @@ When creating a fork, remember to:
|
||||
- To avoid confusion with the main app:
|
||||
- Change the app name
|
||||
- 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:
|
||||
- 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:
|
||||
|
11
README.md
11
README.md
@ -1,6 +1,6 @@
|
||||
| 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
|
||||
@ -11,10 +11,10 @@ Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
||||
## Features
|
||||
|
||||
Features include:
|
||||
* Online reading from [a variety of sources](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
||||
* Local reading of downloaded manga
|
||||
* Online reading from a variety of sources
|
||||
* Local reading of downloaded content
|
||||
* 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
|
||||
* Light and dark themes
|
||||
* Schedule updating your library for new chapters
|
||||
@ -23,7 +23,7 @@ Features include:
|
||||
## Download
|
||||
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
|
||||
|
||||
@ -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 screenshot (if needed)
|
||||
* 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
|
||||
|
||||
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")
|
||||
|
||||
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||
|
||||
android {
|
||||
compileSdkVersion(AndroidConfig.compileSdk)
|
||||
buildToolsVersion(AndroidConfig.buildTools)
|
||||
compileSdk = AndroidConfig.compileSdk
|
||||
ndkVersion = AndroidConfig.ndk
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
minSdkVersion(AndroidConfig.minSdk)
|
||||
targetSdkVersion(AndroidConfig.targetSdk)
|
||||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode = 62
|
||||
versionName = "0.11.0"
|
||||
versionCode = 66
|
||||
versionName = "0.12.0"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@ -39,15 +40,18 @@ android {
|
||||
// Please disable ACRA or use your own instance in forked versions of the project
|
||||
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
||||
|
||||
multiDexEnabled = true
|
||||
|
||||
ndk {
|
||||
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||
abiFilters += SUPPORTED_ABIS
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
splits {
|
||||
abi {
|
||||
isEnable = false
|
||||
reset()
|
||||
include(*SUPPORTED_ABIS.toTypedArray())
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -101,7 +105,11 @@ android {
|
||||
includeInApk = false
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
lint {
|
||||
disable("MissingTranslation", "ExtraTranslation")
|
||||
isAbortOnError = false
|
||||
isCheckReleaseBuilds = false
|
||||
@ -119,21 +127,27 @@ android {
|
||||
|
||||
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
|
||||
implementation("org.tachiyomi:source-api:1.1")
|
||||
|
||||
// AndroidX libraries
|
||||
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.browser:browser:1.3.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.core:core-ktx:1.6.0-beta01")
|
||||
implementation("androidx.multidex:multidex:2.0.1")
|
||||
implementation("androidx.core:core-ktx:1.7.0-alpha01")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
|
||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
|
||||
val lifecycleVersion = "2.4.0-alpha01"
|
||||
@ -142,10 +156,10 @@ dependencies {
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||
|
||||
// Job scheduling
|
||||
implementation("androidx.work:work-runtime-ktx:2.7.0-alpha03")
|
||||
implementation("androidx.work:work-runtime-ktx:2.6.0-beta01")
|
||||
|
||||
// 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")
|
||||
|
||||
@ -163,13 +177,13 @@ dependencies {
|
||||
implementation("com.squareup.okio:okio:2.10.0")
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.1")
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||
|
||||
// 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-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")
|
||||
|
||||
// JavaScript engine
|
||||
@ -181,13 +195,13 @@ dependencies {
|
||||
implementation("com.github.junrar:junrar:7.4.0")
|
||||
|
||||
// HTML parser
|
||||
implementation("org.jsoup:jsoup:1.13.1")
|
||||
implementation("org.jsoup:jsoup:1.14.1")
|
||||
|
||||
// Database
|
||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||
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
|
||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
||||
@ -201,14 +215,14 @@ dependencies {
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
|
||||
// Image library
|
||||
val coilVersion = "1.2.1"
|
||||
val coilVersion = "1.3.2"
|
||||
implementation("io.coil-kt:coil:$coilVersion")
|
||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
||||
|
||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
||||
exclude(module = "image-decoder")
|
||||
}
|
||||
implementation("com.github.tachiyomiorg:image-decoder:7a44c9b")
|
||||
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
|
||||
|
||||
// Logging
|
||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||
@ -228,21 +242,14 @@ dependencies {
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.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
|
||||
implementation("com.bluelinelabs:conductor:2.1.5")
|
||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
||||
exclude(group = "com.android.support")
|
||||
}
|
||||
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
|
||||
val conductorVersion = "3.0.0"
|
||||
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
||||
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
||||
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
||||
|
||||
// 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-appcompat:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||
@ -259,15 +266,8 @@ dependencies {
|
||||
|
||||
val robolectricVersion = "3.1.4"
|
||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
||||
testImplementation("org.robolectric:shadows-multidex:$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/
|
||||
// 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 androidx.preference.** { *; }
|
||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||
-keep,allowoptimization class okio.** { public protected *; }
|
||||
-keep,allowoptimization class rx.** { public protected *; }
|
||||
@ -11,6 +12,7 @@
|
||||
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||
-keep,allowoptimization class com.github.salomonbrys.kotson.** { 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 ----------
|
||||
-dontwarn sun.misc.**
|
||||
|
@ -23,8 +23,7 @@
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:allowBackup="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@ -32,12 +31,15 @@
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
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">
|
||||
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.Splash">
|
||||
android:theme="@style/Theme.Tachiyomi.SplashScreen"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@ -51,7 +53,8 @@
|
||||
android:name=".ui.main.DeepLinkActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:label="@string/action_global_search">
|
||||
android:label="@string/action_global_search"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||
@ -72,9 +75,11 @@
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.reader.ReaderActivity"
|
||||
android:launchMode="singleTask">
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||
</intent-filter>
|
||||
@ -82,15 +87,26 @@
|
||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
android:resource="@xml/s_pen_actions"/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.security.UnlockActivity"
|
||||
android:theme="@style/Theme.Base" />
|
||||
android:theme="@style/Theme.Tachiyomi"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
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
|
||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||
android:label="Anilist">
|
||||
android:label="Anilist"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@ -104,7 +120,8 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||
android:label="MyAnimeList">
|
||||
android:label="MyAnimeList"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@ -118,7 +135,8 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||
android:label="Shikimori">
|
||||
android:label="Shikimori"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@ -132,7 +150,8 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.track.BangumiLoginActivity"
|
||||
android:label="Bangumi">
|
||||
android:label="Bangumi"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@ -145,20 +164,6 @@
|
||||
</intent-filter>
|
||||
</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
|
||||
android:name=".data.notification.NotificationReceiver"
|
||||
android:exported="false" />
|
||||
@ -183,6 +188,16 @@
|
||||
android:name=".data.backup.BackupRestoreService"
|
||||
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>
|
||||
|
||||
</manifest>
|
||||
|
@ -7,9 +7,9 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.webkit.WebView
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@ -17,18 +17,19 @@ import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.multidex.MultiDex
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
||||
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.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@ -68,8 +69,6 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
||||
setupAcra()
|
||||
setupNotificationChannels()
|
||||
|
||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
|
||||
// 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) {
|
||||
setContentTitle(getString(R.string.pref_incognito_mode))
|
||||
setContentText(getString(R.string.notification_incognito_text))
|
||||
setSmallIcon(R.drawable.ic_glasses_black_24dp)
|
||||
setSmallIcon(R.drawable.ic_glasses_24dp)
|
||||
setOngoing(true)
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
@ -99,21 +98,23 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
||||
}
|
||||
}
|
||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
MultiDex.install(this)
|
||||
preferences.themeMode()
|
||||
.asImmediateFlow {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (it) {
|
||||
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
LocaleHelper.updateConfiguration(this, newConfig, true)
|
||||
)
|
||||
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this).apply {
|
||||
componentRegistry {
|
||||
add(TachiyomiImageDecoder(this@App.resources))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
add(ImageDecoderDecoder(this@App))
|
||||
} else {
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
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.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
@ -44,7 +44,7 @@ class AppModule(val app: Application) : InjektModule {
|
||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||
|
||||
// Asynchronously init expensive components for a faster cold start
|
||||
Handler().post {
|
||||
ContextCompat.getMainExecutor(app).execute {
|
||||
get<PreferencesHelper>()
|
||||
|
||||
get<NetworkHelper>()
|
||||
|
@ -12,6 +12,8 @@ import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||
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.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
@ -96,9 +98,15 @@ object Migrations {
|
||||
}
|
||||
if (oldVersion < 44) {
|
||||
// Reset sorting preference if using removed sort by source
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
||||
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
||||
if (oldSortingMode == LibrarySort.SOURCE) {
|
||||
prefs.edit {
|
||||
putInt(PreferenceKeys.librarySortingMode, LibrarySort.ALPHA)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 52) {
|
||||
@ -190,6 +198,45 @@ object Migrations {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ class BackupRestoreService : Service() {
|
||||
|
||||
private fun destroyJob() {
|
||||
backupRestore?.job?.cancel()
|
||||
ioScope?.cancel()
|
||||
ioScope.cancel()
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
|
@ -2,44 +2,52 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
||||
|
||||
import android.content.Context
|
||||
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.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.serializer.CategoryTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryImplTypeSerializer
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeSerializer
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterImplTypeSerializer
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeSerializer
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeSerializer
|
||||
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.ChapterImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
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.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
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
|
||||
|
||||
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||
|
||||
val parser: Gson = when (version) {
|
||||
2 -> GsonBuilder()
|
||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||
.create()
|
||||
val parser: Json = when (version) {
|
||||
2 -> Json {
|
||||
// Forks may have added items to backup
|
||||
ignoreUnknownKeys = true
|
||||
|
||||
// Register custom serializers
|
||||
serializersModule = SerializersModule {
|
||||
contextual(MangaTypeSerializer)
|
||||
contextual(MangaImplTypeSerializer)
|
||||
contextual(ChapterTypeSerializer)
|
||||
contextual(ChapterImplTypeSerializer)
|
||||
contextual(CategoryTypeSerializer)
|
||||
contextual(CategoryImplTypeSerializer)
|
||||
contextual(TrackTypeSerializer)
|
||||
contextual(TrackImplTypeSerializer)
|
||||
contextual(HistoryTypeSerializer)
|
||||
}
|
||||
}
|
||||
else -> throw Exception("Unknown backup version")
|
||||
}
|
||||
|
||||
@ -79,12 +87,11 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
/**
|
||||
* 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
|
||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||
|
||||
// Iterate over them
|
||||
backupCategories.forEach { category ->
|
||||
|
@ -2,88 +2,80 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
||||
|
||||
import android.content.Context
|
||||
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.data.backup.AbstractBackupRestore
|
||||
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.MANGAS
|
||||
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.ChapterImpl
|
||||
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.TrackImpl
|
||||
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
|
||||
|
||||
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
||||
|
||||
override suspend fun performRestore(uri: Uri): Boolean {
|
||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||
val json = JsonParser.parseReader(reader).asJsonObject
|
||||
// Read the json and create a Json Object,
|
||||
// cannot use the backupManager json deserializer one because its not initialized yet
|
||||
val backupObject = Json.decodeFromString<JsonObject>(
|
||||
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
||||
)
|
||||
|
||||
val version = json.get(Backup.VERSION)?.asInt ?: 1
|
||||
// Get parser version
|
||||
val version = backupObject["version"]?.jsonPrimitive?.intOrNull ?: 1
|
||||
|
||||
// Initialize manager
|
||||
backupManager = LegacyBackupManager(context, version)
|
||||
|
||||
val mangasJson = json.get(MANGAS).asJsonArray
|
||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||
// Decode the json object to a Backup object
|
||||
val backup = backupManager.parser.decodeFromJsonElement<Backup>(backupObject)
|
||||
|
||||
restoreAmount = backup.mangas.size + 1 // +1 for categories
|
||||
|
||||
// Restore categories
|
||||
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
||||
backup.categories?.let { restoreCategories(it) }
|
||||
|
||||
// Store source mapping for error messages
|
||||
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
|
||||
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(backup.extensions ?: emptyList())
|
||||
|
||||
// Restore individual manga
|
||||
mangasJson.forEach {
|
||||
backup.mangas.forEach {
|
||||
if (job?.isActive != true) {
|
||||
return false
|
||||
}
|
||||
|
||||
restoreManga(it.asJsonObject)
|
||||
restoreManga(it)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun restoreCategories(categoriesJson: JsonElement) {
|
||||
private fun restoreCategories(categoriesJson: List<Category>) {
|
||||
db.inTransaction {
|
||||
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
||||
backupManager.restoreCategories(categoriesJson)
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||
}
|
||||
|
||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||
val manga = backupManager.parser.fromJson<MangaImpl>(
|
||||
mangaJson.get(
|
||||
Backup.MANGA
|
||||
)
|
||||
)
|
||||
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()
|
||||
)
|
||||
private suspend fun restoreManga(mangaJson: MangaObject) {
|
||||
val manga = mangaJson.manga
|
||||
val chapters = mangaJson.chapters ?: emptyList()
|
||||
val categories = mangaJson.categories ?: emptyList()
|
||||
val history = mangaJson.history ?: emptyList()
|
||||
val tracks = mangaJson.track ?: emptyList()
|
||||
|
||||
val source = backupManager.sourceManager.get(manga.source)
|
||||
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.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.data.backup.AbstractBackupRestoreValidator
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
|
||||
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||
/**
|
||||
@ -17,30 +17,30 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||
* @return List of missing sources or missing trackers.
|
||||
*/
|
||||
override fun validate(context: Context, uri: Uri): Results {
|
||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||
val json = JsonParser.parseReader(reader).asJsonObject
|
||||
val backupManager = LegacyBackupManager(context)
|
||||
|
||||
val version = json.get(Backup.VERSION)
|
||||
val mangasJson = json.get(Backup.MANGAS)
|
||||
if (version == null || mangasJson == null) {
|
||||
val backup = backupManager.parser.decodeFromString<Backup>(
|
||||
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
||||
)
|
||||
|
||||
if (backup.version == null) {
|
||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||
}
|
||||
|
||||
val mangas = mangasJson.asJsonArray
|
||||
if (mangas.size() == 0) {
|
||||
if (backup.mangas.isEmpty()) {
|
||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||
}
|
||||
|
||||
val sources = getSourceMapping(json)
|
||||
val sources = getSourceMapping(backup.extensions ?: emptyList())
|
||||
val missingSources = sources
|
||||
.filter { sourceManager.get(it.key) == null }
|
||||
.values
|
||||
.sorted()
|
||||
|
||||
val trackers = mangas
|
||||
.filter { it.asJsonObject.has("track") }
|
||||
.flatMap { it.asJsonObject["track"].asJsonArray }
|
||||
.map { it.asJsonObject["s"].asInt }
|
||||
val trackers = backup.mangas
|
||||
.filterNot { it.track.isNullOrEmpty() }
|
||||
.flatMap { it.track ?: emptyList() }
|
||||
.map { it.sync_id }
|
||||
.distinct()
|
||||
val missingTrackers = trackers
|
||||
.mapNotNull { trackManager.getService(it) }
|
||||
@ -52,12 +52,10 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
||||
|
||||
return extensionsMapping.asJsonArray
|
||||
fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
|
||||
return extensionsMapping
|
||||
.map {
|
||||
val items = it.asString.split(":")
|
||||
val items = it.split(":")
|
||||
items[0].toLong() to items[1]
|
||||
}
|
||||
.toMap()
|
||||
|
@ -1,25 +1,37 @@
|
||||
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.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Json values
|
||||
*/
|
||||
object Backup {
|
||||
@Serializable
|
||||
data class Backup(
|
||||
val version: Int? = null,
|
||||
var mangas: MutableList<MangaObject> = mutableListOf(),
|
||||
var categories: List<@Contextual Category>? = null,
|
||||
var extensions: List<String>? = null
|
||||
) {
|
||||
companion object {
|
||||
const val CURRENT_VERSION = 2
|
||||
const val MANGA = "manga"
|
||||
const val MANGAS = "mangas"
|
||||
const val TRACK = "track"
|
||||
const val CHAPTERS = "chapters"
|
||||
const val CATEGORIES = "categories"
|
||||
const val EXTENSIONS = "extensions"
|
||||
const val HISTORY = "history"
|
||||
const val VERSION = "version"
|
||||
|
||||
fun getDefaultFilename(): String {
|
||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||
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.injectLazy
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
val coverFile = coverCache.getCoverFile(manga) ?: error("No cover specified")
|
||||
|
||||
// Use previously cached cover if exist
|
||||
if (coverFile.exists() && options.diskCachePolicy.readEnabled) {
|
||||
if (!manga.favorite) {
|
||||
coverFile.setLastModified(Date().time)
|
||||
// Only cache separately if it's a library item
|
||||
val coverCacheFile = if (manga.favorite) {
|
||||
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return fileLoader(coverFile)
|
||||
|
||||
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
||||
return fileLoader(coverCacheFile)
|
||||
}
|
||||
|
||||
val (response, body) = awaitGetCall(manga, options)
|
||||
@ -78,18 +78,16 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
||||
throw HttpException(response)
|
||||
}
|
||||
|
||||
// Write to disk for future use
|
||||
if (options.diskCachePolicy.writeEnabled) {
|
||||
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||
val tmpFile = File(coverFile.absolutePath + "_tmp")
|
||||
tmpFile.parentFile?.mkdirs()
|
||||
tmpFile.sink().buffer().use { output ->
|
||||
coverCacheFile.parentFile?.mkdirs()
|
||||
if (coverCacheFile.exists()) {
|
||||
coverCacheFile.delete()
|
||||
}
|
||||
coverCacheFile.sink().buffer().use { output ->
|
||||
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 {
|
||||
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 {
|
||||
if (source != null) {
|
||||
it.headers(source.headers)
|
||||
@ -135,7 +129,8 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
||||
}
|
||||
}.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 {
|
||||
@ -153,7 +148,7 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
||||
private fun getResourceType(cover: String?): Type? {
|
||||
return when {
|
||||
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
|
||||
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.
|
||||
*/
|
||||
const val DATABASE_VERSION = 11
|
||||
const val DATABASE_VERSION = 12
|
||||
}
|
||||
|
||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||
@ -82,6 +82,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
db.execSQL(MangaTable.addDateAdded)
|
||||
db.execSQL(MangaTable.backfillDateAdded)
|
||||
}
|
||||
if (oldVersion < 12) {
|
||||
db.execSQL(MangaTable.addNextUpdateCol)
|
||||
}
|
||||
}
|
||||
|
||||
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_INITIALIZED
|
||||
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_STATUS
|
||||
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_FAVORITE to obj.favorite,
|
||||
COL_LAST_UPDATE to obj.last_update,
|
||||
COL_NEXT_UPDATE to obj.next_update,
|
||||
COL_INITIALIZED to obj.initialized,
|
||||
COL_VIEWER to obj.viewer_flags,
|
||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||
@ -84,6 +86,7 @@ interface BaseMangaGetResolver {
|
||||
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
||||
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
|
||||
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||
|
@ -1,5 +1,8 @@
|
||||
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
|
||||
|
||||
interface Category : Serializable {
|
||||
@ -12,6 +15,22 @@ interface Category : Serializable {
|
||||
|
||||
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 {
|
||||
|
||||
fun create(name: String): Category = CategoryImpl().apply {
|
||||
|
@ -13,8 +13,12 @@ interface Manga : SManga {
|
||||
|
||||
var favorite: Boolean
|
||||
|
||||
// last time the chapter list changed in any way
|
||||
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 viewer_flags: Int
|
||||
|
@ -26,6 +26,8 @@ open class MangaImpl : Manga {
|
||||
|
||||
override var last_update: Long = 0
|
||||
|
||||
override var next_update: Long = 0
|
||||
|
||||
override var date_added: Long = 0
|
||||
|
||||
override var initialized: Boolean = false
|
||||
|
@ -1,17 +1,13 @@
|
||||
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.Query
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.*
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
@ -19,15 +15,6 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
interface MangaQueries : DbProvider {
|
||||
|
||||
fun getMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getLibraryMangas() = db.get()
|
||||
.listOfObjects(LibraryManga::class.java)
|
||||
.withQuery(
|
||||
@ -39,17 +26,21 @@ interface MangaQueries : DbProvider {
|
||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun getFavoriteMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
|
||||
var queryBuilder = Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||
.whereArgs(1)
|
||||
.orderBy(MangaTable.COL_TITLE)
|
||||
.build()
|
||||
)
|
||||
|
||||
if (sortByTitle) {
|
||||
queryBuilder = queryBuilder.orderBy(MangaTable.COL_TITLE)
|
||||
}
|
||||
|
||||
return db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(queryBuilder.build())
|
||||
.prepare()
|
||||
}
|
||||
|
||||
fun getManga(url: String, sourceId: Long) = db.get()
|
||||
.`object`(Manga::class.java)
|
||||
@ -97,6 +88,11 @@ interface MangaQueries : DbProvider {
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
|
||||
.prepare()
|
||||
|
||||
fun updateNextUpdated(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaNextUpdatedPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateLastUpdated(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.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_NEXT_UPDATE = "next_update"
|
||||
|
||||
const val COL_DATE_ADDED = "date_added"
|
||||
|
||||
const val COL_INITIALIZED = "initialized"
|
||||
@ -57,6 +59,7 @@ object MangaTable {
|
||||
$COL_THUMBNAIL_URL TEXT,
|
||||
$COL_FAVORITE INTEGER NOT NULL,
|
||||
$COL_LAST_UPDATE LONG,
|
||||
$COL_NEXT_UPDATE LONG,
|
||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||
$COL_VIEWER INTEGER NOT NULL,
|
||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||
@ -86,4 +89,7 @@ object MangaTable {
|
||||
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
||||
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_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)
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
|
@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
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.toast
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
@ -58,6 +59,16 @@ class DownloadService : Service() {
|
||||
fun stop(context: Context) {
|
||||
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()
|
||||
|
@ -157,6 +157,11 @@ class Downloader(
|
||||
notifier.paused = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if downloader is paused
|
||||
*/
|
||||
fun isPaused() = !isRunning
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param manga the manga that's being updated.
|
||||
* @param manga the manga that are being updated.
|
||||
* @param current the current progress.
|
||||
* @param total the total progress.
|
||||
*/
|
||||
fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
||||
val title = if (preferences.hideNotificationContent()) {
|
||||
context.getString(R.string.notification_check_updates)
|
||||
fun showProgressNotification(manga: List<Manga>, current: Int, total: Int) {
|
||||
if (preferences.hideNotificationContent()) {
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(context.getString(R.string.notification_check_updates))
|
||||
.setContentText("($current/$total)")
|
||||
} 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(
|
||||
Notifications.ID_LIBRARY_PROGRESS,
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(title.chop(40))
|
||||
.setContentText("($current/$total)")
|
||||
.setProgress(total, current, false)
|
||||
.build()
|
||||
)
|
||||
@ -203,7 +206,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
|
||||
// Mark chapters as read action
|
||||
addAction(
|
||||
R.drawable.ic_glasses_black_24dp,
|
||||
R.drawable.ic_glasses_24dp,
|
||||
context.getString(R.string.action_mark_as_read),
|
||||
NotificationReceiver.markAsReadPendingBroadcast(
|
||||
context,
|
||||
|
@ -1,6 +1,9 @@
|
||||
package eu.kanade.tachiyomi.data.library
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.util.Collections
|
||||
import kotlin.Comparator
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* This class will provide various functions to rank manga to efficiently schedule manga to update.
|
||||
@ -9,9 +12,26 @@ object LibraryUpdateRanker {
|
||||
|
||||
val rankingScheme = listOf(
|
||||
(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.
|
||||
*
|
||||
|
@ -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.notification.Notifications
|
||||
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.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
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.syncChaptersWithSource
|
||||
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.shouldDownloadNewChapters
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
@ -47,10 +48,14 @@ import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
@ -270,18 +275,31 @@ class LibraryUpdateService(
|
||||
* @return an observable delivering the progress of each update.
|
||||
*/
|
||||
suspend fun updateChapterList() {
|
||||
val semaphore = Semaphore(5)
|
||||
val progressCount = AtomicInteger(0)
|
||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
||||
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
||||
var hasDownloads = false
|
||||
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||
val newUpdates = CopyOnWriteArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||
val hasDownloads = AtomicBoolean(false)
|
||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||
|
||||
mangaToUpdate.forEach { manga ->
|
||||
withIOContext {
|
||||
mangaToUpdate.groupBy { it.source }
|
||||
.values
|
||||
.map { mangaInSource ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
mangaInSource.forEach { manga ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return
|
||||
return@async
|
||||
}
|
||||
|
||||
notifier.showProgressNotification(manga, progressCount.andIncrement, mangaToUpdate.size)
|
||||
currentlyUpdatingManga.add(manga)
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
|
||||
try {
|
||||
val (newChapters, _) = updateManga(manga)
|
||||
@ -289,7 +307,7 @@ class LibraryUpdateService(
|
||||
if (newChapters.isNotEmpty()) {
|
||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(manga, newChapters)
|
||||
hasDownloads = true
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
@ -298,6 +316,9 @@ class LibraryUpdateService(
|
||||
} 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
|
||||
}
|
||||
@ -307,13 +328,26 @@ class LibraryUpdateService(
|
||||
if (preferences.autoUpdateTrackers()) {
|
||||
updateTrackings(manga, loggedServices)
|
||||
}
|
||||
|
||||
currentlyUpdatingManga.remove(manga)
|
||||
progressCount.andIncrement
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
|
||||
if (newUpdates.isNotEmpty()) {
|
||||
notifier.showUpdateNotifications(newUpdates)
|
||||
if (hasDownloads) {
|
||||
if (hasDownloads.get()) {
|
||||
DownloadService.start(this)
|
||||
}
|
||||
}
|
||||
@ -369,18 +403,32 @@ class LibraryUpdateService(
|
||||
}
|
||||
|
||||
private suspend fun updateCovers() {
|
||||
var progressCount = 0
|
||||
val semaphore = Semaphore(5)
|
||||
val progressCount = AtomicInteger(0)
|
||||
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||
|
||||
mangaToUpdate.forEach { manga ->
|
||||
withIOContext {
|
||||
mangaToUpdate.groupBy { it.source }
|
||||
.values
|
||||
.map { mangaInSource ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
mangaInSource.forEach { manga ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return
|
||||
return@async
|
||||
}
|
||||
|
||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
||||
currentlyUpdatingManga.add(manga)
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
try {
|
||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
val networkManga =
|
||||
source.getMangaDetails(manga.toMangaInfo())
|
||||
val sManga = networkManga.toSManga()
|
||||
manga.prepUpdateCover(coverCache, sManga, true)
|
||||
sManga.thumbnail_url?.let {
|
||||
@ -392,6 +440,19 @@ class LibraryUpdateService(
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
currentlyUpdatingManga.remove(manga)
|
||||
progressCount.andIncrement
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
coverCache.clearMemoryCache()
|
||||
@ -411,8 +472,7 @@ class LibraryUpdateService(
|
||||
return
|
||||
}
|
||||
|
||||
// Notify manga that will update.
|
||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
||||
notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size)
|
||||
|
||||
// Update the tracking details.
|
||||
updateTrackings(manga, loggedServices)
|
||||
@ -432,7 +492,7 @@ class LibraryUpdateService(
|
||||
val updatedTrack = service.refresh(track)
|
||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||
|
||||
if (service is UnattendedTrackService) {
|
||||
if (service is EnhancedTrackService) {
|
||||
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
@ -454,9 +514,19 @@ class LibraryUpdateService(
|
||||
if (errors.isNotEmpty()) {
|
||||
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
|
||||
file.bufferedWriter().use { out ->
|
||||
errors.forEach { (manga, error) ->
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
out.write("${manga.title} ($source): $error\n")
|
||||
// Error file format:
|
||||
// ! Error
|
||||
// # 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
|
||||
|
@ -2,12 +2,11 @@ package eu.kanade.tachiyomi.data.notification
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||
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.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -130,16 +130,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* @param notificationId id of notification
|
||||
*/
|
||||
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)
|
||||
// Launch share activity
|
||||
context.startActivity(intent)
|
||||
context.startActivity(File(path).getUriCompat(context).toShareIntent(context))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -150,16 +142,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* @param notificationId id of notification
|
||||
*/
|
||||
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)
|
||||
// Launch share activity
|
||||
context.startActivity(sendIntent)
|
||||
context.startActivity(uri.toShareIntent(context, fileMimeType))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -208,7 +192,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
*/
|
||||
private fun cancelRestore(context: Context, notificationId: Int) {
|
||||
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) {
|
||||
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 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 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"
|
||||
|
||||
@ -99,8 +99,6 @@ object PreferenceKeys {
|
||||
|
||||
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 lastUsedCategory = "last_used_category"
|
||||
@ -147,6 +145,7 @@ object PreferenceKeys {
|
||||
const val filterTracked = "pref_filter_library_tracked"
|
||||
|
||||
const val librarySortingMode = "library_sorting_mode"
|
||||
const val librarySortingDirection = "library_sorting_ascending"
|
||||
|
||||
const val automaticExtUpdates = "automatic_ext_updates"
|
||||
|
||||
@ -185,6 +184,8 @@ object PreferenceKeys {
|
||||
|
||||
const val defaultCategory = "default_category"
|
||||
|
||||
const val categorizedDisplay = "categorized_display"
|
||||
|
||||
const val skipRead = "skip_read"
|
||||
|
||||
const val skipFiltered = "skip_filtered"
|
||||
|
@ -1,5 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.preference
|
||||
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
const val UNMETERED_NETWORK = "wifi"
|
||||
const val CHARGING = "ac"
|
||||
|
||||
@ -17,35 +19,28 @@ object PreferenceValues {
|
||||
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 */
|
||||
|
||||
enum class DisplayMode {
|
||||
COMPACT_GRID,
|
||||
COMFORTABLE_GRID,
|
||||
LIST,
|
||||
enum class AppTheme(val titleResId: Int?) {
|
||||
DEFAULT(R.string.theme_default),
|
||||
MONET(R.string.theme_monet),
|
||||
BLUE(R.string.theme_blue),
|
||||
GREEN_APPLE(R.string.theme_greenapple),
|
||||
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||
TAKO(R.string.theme_tako),
|
||||
YINYANG(R.string.theme_yinyang),
|
||||
YOTSUBA(R.string.theme_yotsuba),
|
||||
|
||||
// Deprecated
|
||||
DARK_BLUE(null),
|
||||
HOT_PINK(null),
|
||||
}
|
||||
|
||||
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
|
||||
NONE,
|
||||
HORIZONTAL(shouldInvertHorizontal = 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 eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode.*
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
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.ReadingModeType
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
@ -66,7 +69,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
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)
|
||||
|
||||
@ -82,13 +85,13 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
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)
|
||||
|
||||
@ -174,15 +177,13 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||
|
||||
fun autoAddTrack() = prefs.getBoolean(Keys.autoAddTrack, true)
|
||||
|
||||
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
|
||||
|
||||
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 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))
|
||||
|
||||
@ -233,7 +234,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
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)
|
||||
|
||||
@ -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 librarySortingMode() = flowPrefs.getInt(Keys.librarySortingMode, 0)
|
||||
|
||||
fun librarySortingAscending() = flowPrefs.getBoolean("library_sorting_ascending", true)
|
||||
fun librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL)
|
||||
fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING)
|
||||
|
||||
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
||||
|
||||
@ -280,10 +280,12 @@ class PreferencesHelper(val context: Context) {
|
||||
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, 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 categorisedDisplaySettings() = flowPrefs.getBoolean(Keys.categorizedDisplay, false)
|
||||
|
||||
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* An Unattended 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.
|
||||
* An Enhanced Track Service will never prompt the user to match a manga with the remote.
|
||||
* 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.
|
||||
*/
|
||||
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.
|
@ -36,6 +36,10 @@ abstract class TrackService(val id: Int) {
|
||||
|
||||
abstract fun getStatus(status: Int): String
|
||||
|
||||
abstract fun getReadingStatus(): Int
|
||||
|
||||
abstract fun getRereadingStatus(): Int
|
||||
|
||||
abstract fun getCompletionStatus(): Int
|
||||
|
||||
abstract fun getScoreList(): List<String>
|
||||
@ -46,9 +50,9 @@ abstract class TrackService(val id: Int) {
|
||||
|
||||
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>
|
||||
|
||||
|
@ -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 getScoreList(): List<String> {
|
||||
@ -134,7 +138,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
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 (track.library_id == null || track.library_id!! == 0L) {
|
||||
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
|
||||
}
|
||||
|
||||
if (track.status != COMPLETED) {
|
||||
if (track.status != REPEATING && didReadChapter) {
|
||||
track.status = READING
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
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)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.status = READING
|
||||
track.status = if (hasReadChapters) READING else PLANNING
|
||||
track.score = 0F
|
||||
add(track)
|
||||
}
|
||||
|
@ -2,9 +2,6 @@ package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import android.net.Uri
|
||||
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.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
@ -315,9 +312,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = dateValue
|
||||
return buildJsonObject {
|
||||
put("year", calendar.year)
|
||||
put("month", calendar.month + 1)
|
||||
put("day", calendar.dayOfMonth)
|
||||
put("year", calendar.get(Calendar.YEAR))
|
||||
put("month", calendar.get(Calendar.MONTH) + 1)
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override suspend fun bind(track: Track): Track {
|
||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||
val statusTrack = api.statusLibManga(track)
|
||||
val remoteTrack = api.findLibManga(track)
|
||||
return if (remoteTrack != null && statusTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
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.last_chapter_read = statusTrack.last_chapter_read
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
refresh(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.status = READING
|
||||
track.status = if (hasReadChapters) READING else PLANNING
|
||||
track.score = 0F
|
||||
add(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 suspend fun login(username: String, password: String) = login(password)
|
||||
|
@ -26,6 +26,8 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
@StringRes
|
||||
override fun nameRes() = R.string.tracker_kitsu
|
||||
|
||||
override val supportsReadingDates: Boolean = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
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 getScoreList(): List<String> {
|
||||
@ -71,18 +77,29 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
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)
|
||||
}
|
||||
|
||||
override suspend fun bind(track: Track): Track {
|
||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||
val remoteTrack = api.findLibManga(track, getUserId())
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.media_id = remoteTrack.media_id
|
||||
|
||||
if (track.status != COMPLETED) {
|
||||
track.status = if (hasReadChapters) READING else track.status
|
||||
}
|
||||
|
||||
update(track)
|
||||
} else {
|
||||
track.status = READING
|
||||
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||
track.score = 0F
|
||||
add(track)
|
||||
}
|
||||
|
@ -84,6 +84,8 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
put("status", track.toKitsuStatus())
|
||||
put("progress", track.last_chapter_read)
|
||||
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
|
||||
private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content
|
||||
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
|
||||
val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content
|
||||
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_type = type
|
||||
start_date = startDate
|
||||
started_reading_date = KitsuDateHelper.parse(startedAt)
|
||||
finished_reading_date = KitsuDateHelper.parse(finishedAt)
|
||||
status = toTrackStatus()
|
||||
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
|
||||
last_chapter_read = progress
|
||||
|
@ -6,22 +6,19 @@ import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
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.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import okhttp3.Dns
|
||||
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 {
|
||||
const val UNREAD = 1
|
||||
const val READING = 2
|
||||
const val COMPLETED = 3
|
||||
|
||||
const val ACCEPTED_SOURCE = "eu.kanade.tachiyomi.extension.all.komga.Komga"
|
||||
}
|
||||
|
||||
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 getScoreList(): List<String> = emptyList()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override suspend fun bind(track: Track): Track {
|
||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): 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 {
|
||||
val remoteTrack = api.getTrackSearch(track.tracking_url)!!
|
||||
val remoteTrack = api.getTrackSearch(track.tracking_url)
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
return track
|
||||
@ -84,7 +91,7 @@ class Komga(private val context: Context, id: Int) : TrackService(id), Unattende
|
||||
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? =
|
||||
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 getScoreList(): List<String> {
|
||||
@ -67,22 +71,35 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||
}
|
||||
|
||||
private suspend fun add(track: Track): Track {
|
||||
return api.updateItem(track)
|
||||
}
|
||||
|
||||
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||
if (track.status != COMPLETED) {
|
||||
if (track.status != REREADING && didReadChapter) {
|
||||
track.status = READING
|
||||
track.score = 0F
|
||||
}
|
||||
}
|
||||
|
||||
return api.updateItem(track)
|
||||
}
|
||||
|
||||
override suspend fun update(track: Track): 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)
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
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)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
@ -44,19 +44,31 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||
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())
|
||||
}
|
||||
|
||||
override suspend fun bind(track: Track): Track {
|
||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||
val remoteTrack = api.findLibManga(track, getUsername())
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
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)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.status = READING
|
||||
track.status = if (hasReadChapters) READING else PLANNING
|
||||
track.score = 0F
|
||||
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 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.Serializable
|
||||
|
||||
@ -15,16 +15,25 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
class GithubRelease(
|
||||
@SerialName("tag_name") val version: String,
|
||||
@SerialName("body") override val info: String,
|
||||
@SerialName("body") val info: String,
|
||||
@SerialName("assets") private val assets: List<Assets>
|
||||
) : Release {
|
||||
) {
|
||||
|
||||
/**
|
||||
* Get download link of latest release from the assets.
|
||||
* @return download link of latest release.
|
||||
*/
|
||||
override val downloadLink: String
|
||||
get() = assets[0].downloadLink
|
||||
fun getDownloadLink(): String {
|
||||
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.
|
@ -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.data.updater.UpdateResult
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
@ -21,7 +20,7 @@ class GithubUpdateChecker {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdate(): UpdateResult {
|
||||
suspend fun checkForUpdate(): GithubUpdateResult {
|
||||
return withIOContext {
|
||||
networkService.client
|
||||
.newCall(GET("https://api.github.com/repos/$repo/releases/latest"))
|
||||
@ -32,7 +31,7 @@ class GithubUpdateChecker {
|
||||
if (isNewVersion(it.version)) {
|
||||
GithubUpdateResult.NewUpdate(it)
|
||||
} 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.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@ -19,8 +18,8 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
try {
|
||||
val result = GithubUpdateChecker().checkForUpdate()
|
||||
|
||||
if (result is UpdateResult.NewUpdate<*>) {
|
||||
UpdaterNotifier(context).promptUpdate(result.release.downloadLink)
|
||||
if (result is GithubUpdateResult.NewUpdate) {
|
||||
UpdaterNotifier(context).promptUpdate(result.release.getDownloadLink())
|
||||
}
|
||||
Result.success()
|
||||
} 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 {
|
||||
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.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
@ -28,7 +27,7 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
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()
|
||||
|
||||
@ -92,7 +91,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||
headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
|
||||
|
||||
handler.post {
|
||||
executor.execute {
|
||||
val webview = WebView(context)
|
||||
webView = webview
|
||||
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.
|
||||
latch.await(12, TimeUnit.SECONDS)
|
||||
|
||||
handler.post {
|
||||
executor.execute {
|
||||
if (!cloudflareBypassed) {
|
||||
isWebViewOutdated = webView?.isOutdated() == true
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
const val ID = 0L
|
||||
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 LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
@ -40,8 +39,9 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
input.close()
|
||||
return null
|
||||
}
|
||||
val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
||||
val cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
|
||||
|
||||
if (cover != null && cover.exists()) {
|
||||
// It might not exist if using the external SD card
|
||||
cover.parentFile?.mkdirs()
|
||||
input.use {
|
||||
@ -49,9 +49,19 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
input.copyTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
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> {
|
||||
val c = context.getString(R.string.app_name) + File.separator + "local"
|
||||
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
||||
@ -84,9 +94,9 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
when (state?.index) {
|
||||
0 -> {
|
||||
mangaDirs = if (state.ascending) {
|
||||
mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
|
||||
mangaDirs.sortedBy { it.name.lowercase(Locale.ENGLISH) }
|
||||
} else {
|
||||
mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
|
||||
mangaDirs.sortedByDescending { it.name.lowercase(Locale.ENGLISH) }
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
@ -105,8 +115,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
|
||||
// Try to find the cover
|
||||
for (dir in baseDirs) {
|
||||
val cover = File("${dir.absolutePath}/$url", COVER_NAME)
|
||||
if (cover.exists()) {
|
||||
val cover = getCoverFile(File("${dir.absolutePath}/$url"))
|
||||
if (cover != null && cover.exists()) {
|
||||
thumbnail_url = cover.absolutePath
|
||||
break
|
||||
}
|
||||
@ -238,7 +248,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -70,8 +70,11 @@ open class SourceManager(private val context: Context) {
|
||||
return name
|
||||
}
|
||||
|
||||
private fun getSourceNotInstalledException(): Exception {
|
||||
return Exception(context.getString(R.string.source_not_installed, id.toString()))
|
||||
private fun getSourceNotInstalledException(): SourceNotInstalledException {
|
||||
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.
|
||||
*/
|
||||
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())
|
||||
(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.
|
||||
*/
|
||||
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
|
||||
@ -341,7 +341,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
*/
|
||||
private fun getUrlWithoutDomain(orig: String): String {
|
||||
return try {
|
||||
val uri = URI(orig)
|
||||
val uri = URI(orig.replace(" ", "%20"))
|
||||
var out = uri.path
|
||||
if (uri.query != null) {
|
||||
out += "?" + uri.query
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.base.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
@ -14,9 +15,8 @@ abstract class BaseRxActivity<VB : ViewBinding, P : BasePresenter<*>> : NucleusA
|
||||
|
||||
lateinit var binding: VB
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
LocaleHelper.updateConfiguration(this)
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
super.attachBaseContext(LocaleHelper.createLocaleWrapper(newBase))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -1,43 +1,68 @@
|
||||
package eu.kanade.tachiyomi.ui.base.activity
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DarkThemeVariant
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.LightThemeVariant
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class BaseThemedActivity : AppCompatActivity() {
|
||||
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
super.attachBaseContext(LocaleHelper.createLocaleWrapper(newBase))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val isDarkMode = when (preferences.themeMode().get()) {
|
||||
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)
|
||||
applyAppTheme(preferences)
|
||||
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 androidx.viewbinding.ViewBinding
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
abstract class BaseViewBindingActivity<VB : ViewBinding> : BaseThemedActivity() {
|
||||
|
||||
@ -12,11 +11,6 @@ abstract class BaseViewBindingActivity<VB : ViewBinding> : BaseThemedActivity()
|
||||
@Suppress("LeakingThis")
|
||||
private val secureActivityDelegate = SecureActivityDelegate(this)
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
LocaleHelper.updateConfiguration(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -10,14 +10,12 @@ import androidx.viewbinding.ViewBinding
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.RestoreViewOnCreateController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import timber.log.Timber
|
||||
|
||||
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
RestoreViewOnCreateController(bundle) {
|
||||
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) : Controller(bundle) {
|
||||
|
||||
protected lateinit var binding: VB
|
||||
private set
|
||||
|
@ -5,7 +5,7 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.RestoreViewOnCreateController
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
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]
|
||||
*/
|
||||
abstract class DialogController : RestoreViewOnCreateController {
|
||||
abstract class DialogController : Controller {
|
||||
|
||||
protected var dialog: Dialog? = null
|
||||
private set
|
||||
|
@ -32,6 +32,12 @@ open class BasePresenter<V> : RxPresenter<V>() {
|
||||
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
|
||||
* subscription list.
|
||||
|
@ -9,7 +9,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.Router
|
||||
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.tabs.TabLayout
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
|
@ -136,7 +136,7 @@ open class ExtensionController :
|
||||
}
|
||||
|
||||
searchView.queryTextChanges()
|
||||
.filter { router.backstack.lastOrNull()?.controller() == this }
|
||||
.filter { router.backstack.lastOrNull()?.controller == this }
|
||||
.onEach {
|
||||
query = it.toString()
|
||||
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.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
||||
else -> ""
|
||||
}.toUpperCase()
|
||||
}.uppercase()
|
||||
|
||||
binding.image.clear()
|
||||
if (extension is Extension.Available) {
|
||||
|
@ -61,9 +61,9 @@ open class ExtensionPresenter(
|
||||
|
||||
val items = mutableListOf<ExtensionItem>()
|
||||
|
||||
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.pkgName }
|
||||
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.pkgName }))
|
||||
val untrustedSorted = untrusted.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.name }))
|
||||
val untrustedSorted = untrusted.sortedBy { it.name }
|
||||
val availableSorted = available
|
||||
// Filter out already installed extensions and disabled languages
|
||||
.filter { avail ->
|
||||
@ -82,9 +82,11 @@ open class ExtensionPresenter(
|
||||
}
|
||||
if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
|
||||
val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size)
|
||||
|
||||
items += installedSorted.map { extension ->
|
||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
||||
}
|
||||
|
||||
items += untrustedSorted.map { extension ->
|
||||
ExtensionItem(extension, header)
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.browse.extension
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
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 {
|
||||
return MaterialDialog(activity!!)
|
||||
.title(R.string.untrusted_extension)
|
||||
.message(R.string.untrusted_extension_message)
|
||||
.positiveButton(R.string.ext_trust) {
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle(R.string.untrusted_extension)
|
||||
.setMessage(R.string.untrusted_extension_message)
|
||||
.setPositiveButton(R.string.ext_trust) { _, _ ->
|
||||
(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)!!)
|
||||
}
|
||||
.create()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
@ -114,7 +114,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
.forEach {
|
||||
val preferenceBlock = {
|
||||
it.value
|
||||
.sortedWith(compareBy({ !it.isEnabled() }, { it.name.toLowerCase() }))
|
||||
.sortedWith(compareBy({ !it.isEnabled() }, { it.name.lowercase() }))
|
||||
.forEach { source ->
|
||||
val sourcePrefs = mutableListOf<Preference>()
|
||||
|
||||
|
@ -3,10 +3,9 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
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.RouterTransaction
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@ -86,28 +85,29 @@ class SearchController(
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
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 =
|
||||
MigrationFlags.getEnabledFlagsPositions(
|
||||
prefValue
|
||||
)
|
||||
|
||||
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)
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle(R.string.migration_dialog_what_to_include)
|
||||
.setMultiChoiceItems(items, selected) { _, which, checked ->
|
||||
selected[which] = checked
|
||||
}
|
||||
.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.javaClass == SourceSearchController::class.java) {
|
||||
router.popController(callingController)
|
||||
@ -115,7 +115,7 @@ class SearchController(
|
||||
}
|
||||
(targetController as? SearchController)?.migrateManga(manga, newManga)
|
||||
}
|
||||
.negativeButton(R.string.copy) {
|
||||
.setNegativeButton(R.string.copy) { _, _, ->
|
||||
if (callingController != null) {
|
||||
if (callingController.javaClass == SourceSearchController::class.java) {
|
||||
router.popController(callingController)
|
||||
@ -123,7 +123,8 @@ class SearchController(
|
||||
}
|
||||
(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 {
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
||||
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 =
|
||||
SearchController.MigrationDialog(oldManga, newManga, this)
|
||||
dialog.targetController = searchController
|
||||
dialog.showDialog(router)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int) {
|
||||
view?.let { super.onItemClick(it, position) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MANGA_KEY = "oldManga"
|
||||
}
|
||||
|
@ -72,8 +72,6 @@ class MigrationSourcesController :
|
||||
parentController!!.router.pushController(controller.withFadeTransaction())
|
||||
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)
|
||||
SourceItem(source, it.value.size, header)
|
||||
}
|
||||
.sortedBy { it.source.name.toLowerCase() }
|
||||
.sortedBy { it.source.name.lowercase() }
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,9 @@ import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
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.ControllerChangeType
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
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.globalsearch.GlobalSearchController
|
||||
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.api.get
|
||||
|
||||
@ -82,6 +83,9 @@ class SourceController :
|
||||
// Create recycler and set adapter.
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.adapter = adapter
|
||||
binding.recycler.onAnimationsFinished {
|
||||
(activity as? MainActivity)?.ready = true
|
||||
}
|
||||
adapter?.fastScroller = binding.fastScroller
|
||||
|
||||
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
||||
@ -238,15 +242,13 @@ class SourceController :
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog(activity!!)
|
||||
.title(text = source)
|
||||
.listItems(
|
||||
items = items.map { it.first },
|
||||
waitForPositiveButton = false
|
||||
) { dialog, which, _ ->
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle(source)
|
||||
.setItems(items.map { it.first }.toTypedArray()) { dialog, which ->
|
||||
items[which].second()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,7 @@ class SourceFilterController : SettingsController() {
|
||||
)
|
||||
|
||||
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
|
||||
switchPreferenceCategory {
|
||||
|
@ -122,7 +122,7 @@ class SourcePresenter(
|
||||
return sourceManager.getCatalogueSources()
|
||||
.filter { it.lang in languages }
|
||||
.filterNot { it.id.toString() in disabledSourceIds }
|
||||
.sortedBy { "(${it.lang}) ${it.name.toLowerCase()}" } +
|
||||
.sortedBy { "(${it.lang}) ${it.name.lowercase()}" } +
|
||||
sourceManager.get(LocalSource.ID) as LocalSource
|
||||
}
|
||||
|
||||
|
@ -13,8 +13,7 @@ import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tfcporciuncula.flow.Preference
|
||||
@ -24,12 +23,12 @@ import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
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.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.databinding.SourceControllerBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
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.online.HttpSource
|
||||
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.browse.source.globalsearch.GlobalSearchController
|
||||
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.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.more.MoreController
|
||||
@ -205,7 +205,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
binding.catalogueView.removeView(oldRecycler)
|
||||
}
|
||||
|
||||
val recycler = if (preferences.sourceDisplayMode().get() == DisplayMode.LIST) {
|
||||
val recycler = if (preferences.sourceDisplayMode().get() == DisplayModeSetting.LIST) {
|
||||
RecyclerView(view.context).apply {
|
||||
id = R.id.recycler
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
@ -261,7 +261,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
searchItem.fixExpand(
|
||||
onExpand = { invalidateMenuOnExpand() },
|
||||
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)
|
||||
} else {
|
||||
nonSubmittedQuery = ""
|
||||
@ -273,9 +273,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
)
|
||||
|
||||
val displayItem = when (preferences.sourceDisplayMode().get()) {
|
||||
DisplayMode.COMPACT_GRID -> R.id.action_compact_grid
|
||||
DisplayMode.COMFORTABLE_GRID -> R.id.action_comfortable_grid
|
||||
DisplayMode.LIST -> R.id.action_list
|
||||
DisplayModeSetting.COMPACT_GRID -> R.id.action_compact_grid
|
||||
DisplayModeSetting.COMFORTABLE_GRID -> R.id.action_comfortable_grid
|
||||
DisplayModeSetting.LIST -> R.id.action_list
|
||||
}
|
||||
menu.findItem(displayItem).isChecked = true
|
||||
}
|
||||
@ -297,9 +297,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_search -> expandActionViewFromInteraction = true
|
||||
R.id.action_compact_grid -> setDisplayMode(DisplayMode.COMPACT_GRID)
|
||||
R.id.action_comfortable_grid -> setDisplayMode(DisplayMode.COMFORTABLE_GRID)
|
||||
R.id.action_list -> setDisplayMode(DisplayMode.LIST)
|
||||
R.id.action_compact_grid -> setDisplayMode(DisplayModeSetting.COMPACT_GRID)
|
||||
R.id.action_comfortable_grid -> setDisplayMode(DisplayModeSetting.COMFORTABLE_GRID)
|
||||
R.id.action_list -> setDisplayMode(DisplayModeSetting.LIST)
|
||||
R.id.action_open_in_web_view -> openInWebView()
|
||||
R.id.action_local_source_help -> openLocalSourceHelpGuide()
|
||||
}
|
||||
@ -335,6 +335,54 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
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.
|
||||
*
|
||||
@ -446,7 +494,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
*
|
||||
* @param mode the mode to change to
|
||||
*/
|
||||
private fun setDisplayMode(mode: DisplayMode) {
|
||||
private fun setDisplayMode(mode: DisplayModeSetting) {
|
||||
val view = view ?: return
|
||||
val adapter = adapter ?: return
|
||||
|
||||
@ -540,11 +588,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
|
||||
|
||||
if (manga.favorite) {
|
||||
MaterialDialog(activity)
|
||||
.listItems(
|
||||
items = listOf(activity.getString(R.string.remove_from_library)),
|
||||
waitForPositiveButton = false
|
||||
) { _, which, _ ->
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(manga.title)
|
||||
.setItems(arrayOf(activity.getString(R.string.remove_from_library))) { _, which ->
|
||||
when (which) {
|
||||
0 -> {
|
||||
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.toMangaInfo
|
||||
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.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
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.withUIContext
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
@ -44,7 +45,6 @@ import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
@ -104,7 +104,7 @@ open class BrowseSourcePresenter(
|
||||
/**
|
||||
* 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 } }
|
||||
|
||||
@ -175,14 +175,14 @@ open class BrowseSourcePresenter(
|
||||
fun requestNext() {
|
||||
if (!hasNextPage()) return
|
||||
|
||||
pageSubscription?.let { remove(it) }
|
||||
pageSubscription = Observable.defer { pager.requestNext() }
|
||||
.subscribeFirst(
|
||||
{ _, _ ->
|
||||
// Nothing to do when onNext is emitted.
|
||||
},
|
||||
BrowseSourceController::onAddPageError
|
||||
)
|
||||
nextPageJob?.cancel()
|
||||
nextPageJob = launchIO {
|
||||
try {
|
||||
pager.requestNextPage()
|
||||
} catch (e: Throwable) {
|
||||
withUIContext { view?.onAddPageError(e) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -267,17 +267,15 @@ open class BrowseSourcePresenter(
|
||||
} else {
|
||||
ChapterSettingsHelper.applySettingDefaults(manga)
|
||||
|
||||
if (prefs.autoAddTrack()) {
|
||||
autoAddTrack(manga)
|
||||
}
|
||||
}
|
||||
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
private fun autoAddTrack(manga: Manga) {
|
||||
loggedServices
|
||||
.filterIsInstance<UnattendedTrackService>()
|
||||
.filterIsInstance<EnhancedTrackService>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
launchIO {
|
||||
|
@ -19,7 +19,7 @@ abstract class Pager(var currentPage: Int = 1) {
|
||||
return results.asObservable()
|
||||
}
|
||||
|
||||
abstract fun requestNext(): Observable<MangasPage>
|
||||
abstract suspend fun requestNextPage()
|
||||
|
||||
fun onPageReceived(mangasPage: MangasPage) {
|
||||
val page = currentPage
|
||||
|
@ -1,9 +1,9 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.imageLoader
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import coil.transition.CrossfadeTransition
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
@ -38,6 +38,12 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F
|
||||
// Set alpha of thumbnail.
|
||||
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)
|
||||
}
|
||||
|
||||
@ -53,7 +59,6 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F
|
||||
val request = ImageRequest.Builder(view.context)
|
||||
.data(manga)
|
||||
.setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
||||
.diskCachePolicy(CachePolicy.DISABLED)
|
||||
.target(StateImageViewTarget(binding.thumbnail, binding.progress, crossfadeDuration))
|
||||
.build()
|
||||
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