mirror of
https://github.com/mihonapp/mihon.git
synced 2025-07-27 09:55:53 +02:00
Compare commits
585 Commits
Author | SHA1 | Date | |
---|---|---|---|
2b38b4e022 | |||
4ecde9fc39 | |||
445ee274c5 | |||
f2bdc514e8 | |||
5afff31f72 | |||
2dfafa387b | |||
7318f4f5dd | |||
175b77fe6f | |||
346652e508 | |||
f0eb42e72d | |||
37100f0937 | |||
ac980a4dbf | |||
a8b53499af | |||
a8aeae329e | |||
52911539b8 | |||
3026ff241b | |||
2466a079d5 | |||
ed9fdf49e2 | |||
668d962233 | |||
996f770935 | |||
041a6dd919 | |||
dbad60d03b | |||
27a60423dc | |||
5a37d38a84 | |||
6f566e67d5 | |||
dd490f2ac9 | |||
5409af0a6c | |||
0ed0d903cc | |||
85be4c492d | |||
c06ad8b87e | |||
b89acb5853 | |||
7890511a53 | |||
3aa4e6eb93 | |||
f8eb9f94f4 | |||
c581b9eeb9 | |||
ffd9c6995a | |||
ef600c0956 | |||
5c0a43e8d6 | |||
8e332dba30 | |||
cd07027192 | |||
da2b30268a | |||
1163aa4e4e | |||
ddb856edc7 | |||
9c426bc216 | |||
382852d0bd | |||
87ae86e1be | |||
9547311d7d | |||
1613d561c1 | |||
538478cac8 | |||
267ecce958 | |||
fae43fedfa | |||
c447022092 | |||
56042ad0b6 | |||
45da036789 | |||
b47b702a52 | |||
869424cd16 | |||
b9fd01315b | |||
a72098b862 | |||
86016de6cb | |||
592b9fedb9 | |||
d06984e3a3 | |||
6b55ee250d | |||
10eef282fa | |||
f312936629 | |||
d53bb4c337 | |||
1a605e27bc | |||
08ee858f64 | |||
af70fe3e7e | |||
29c5c0af50 | |||
9420b750d2 | |||
6f5328f663 | |||
90214d02d7 | |||
2f07f226b8 | |||
a8ad19a89d | |||
57c07250fd | |||
4a3e4a7c5c | |||
c284a23afb | |||
fad1449de3 | |||
f18d161eaf | |||
88054b453a | |||
c560373596 | |||
d698d03521 | |||
d8c8d7c588 | |||
9120e82517 | |||
e214746536 | |||
142396400c | |||
51d48bdde6 | |||
44b055c019 | |||
790d7b9170 | |||
d8719ceee9 | |||
71ddb16574 | |||
2932ed670f | |||
ae2a6a3d4f | |||
30061ada58 | |||
a131e28b60 | |||
8c1662cfdb | |||
299e52e877 | |||
95b253db09 | |||
067cb2452e | |||
45e4092335 | |||
7659a997cf | |||
aa5e428222 | |||
319e4360c8 | |||
f5c6e80dbb | |||
7108993936 | |||
b6553bdc34 | |||
19fe689969 | |||
d6386cef41 | |||
b88f8ae9d2 | |||
408c7b2ca6 | |||
271253fd0b | |||
5348154c42 | |||
e1b1f4f3fc | |||
75a2110626 | |||
9857d3d6ea | |||
836a2649d3 | |||
59cba2533c | |||
a6ac2fbc9a | |||
3da8677e32 | |||
4d0d7d5ad6 | |||
8c4ece4b2d | |||
bf3bb8a378 | |||
cf5e60f8eb | |||
7de707c60a | |||
5cd11ad8c3 | |||
6bba52a2b6 | |||
54b476df4e | |||
a68f123594 | |||
08ad4f96b9 | |||
77a3acf5cc | |||
dea585e69b | |||
879dacfba6 | |||
b459234ddc | |||
76d2c676fd | |||
d5015d37e1 | |||
1b71e4cee7 | |||
18ef5c6ff9 | |||
35e0561950 | |||
adab8e3ed8 | |||
89dbb4d300 | |||
e3f3686b8a | |||
9984e983b4 | |||
4ebe67ef53 | |||
1a11d4153e | |||
cd7cf3583e | |||
66a180bc36 | |||
eb06667455 | |||
0ff8966a27 | |||
2cc6794db5 | |||
0cb4094dd9 | |||
edd213343b | |||
46ec655db5 | |||
769efd9d06 | |||
49cb3b6aa7 | |||
8ad98b67d2 | |||
8a8f1d3205 | |||
4a27f0546c | |||
727a7e4b2d | |||
2b5e8241ab | |||
3dc4fd8dd1 | |||
375a27a93d | |||
544387d1a0 | |||
cb8120d38f | |||
78a261f5d3 | |||
b8f7653fb2 | |||
e0d2a01bc8 | |||
560be9f553 | |||
47723042c5 | |||
d04d676d2f | |||
3435636ca0 | |||
2e1572d7cc | |||
938339690e | |||
dbb2c523c1 | |||
0b9d436753 | |||
2d03f3ce1e | |||
c4a476d0d2 | |||
5122aed332 | |||
5336c5b46e | |||
22615f5981 | |||
bdf4b4b679 | |||
548e300c4b | |||
8a5d8c96ef | |||
78c2631b6f | |||
7c246ffc71 | |||
8bb85753cc | |||
abfdde28ef | |||
9801f1edfa | |||
fc3a200a63 | |||
6a00658119 | |||
353485054e | |||
800583b5e2 | |||
2db2b7348d | |||
f3718257f5 | |||
5500762acd | |||
4c8f5e1f7a | |||
733cf99bb4 | |||
58c2f22120 | |||
42accebeca | |||
1c5c370c12 | |||
448645d83a | |||
09b6a3b41e | |||
74206d60ce | |||
c3a0de7fab | |||
7edf7a434f | |||
b701821550 | |||
d022bf2673 | |||
7eed8c440c | |||
1ab12e380a | |||
728e14e8e4 | |||
8aa402526a | |||
4793ee4786 | |||
a09d6c0470 | |||
9e83130bd8 | |||
2ed01af723 | |||
afc80d6a7c | |||
532a1b1aba | |||
65062b4bcb | |||
c16206d816 | |||
185283f864 | |||
7d1f5c7383 | |||
945afc71ef | |||
818fe50f77 | |||
6fddad7a77 | |||
38d131be37 | |||
aeff846e1f | |||
6b52fc1e2d | |||
0671b530ba | |||
207f9c26ae | |||
6367ce5e5e | |||
ba1a2e9942 | |||
7f998ecdbd | |||
ecd5414287 | |||
6107f5f3d2 | |||
13afa9f476 | |||
cd87c7e88e | |||
ed4dea8686 | |||
808177f8c9 | |||
aed51251b3 | |||
1c2730163d | |||
0de86dfe6f | |||
7a1b99be46 | |||
9b64b0139c | |||
0a6160d7cf | |||
e51a6d332e | |||
a9d2741e6a | |||
12bd7268d2 | |||
be0a23d9ad | |||
458a0e608a | |||
32f3a50def | |||
7de4226d80 | |||
6a39c8fc13 | |||
dc39669321 | |||
be4f27028c | |||
60e73e2d1f | |||
e8f284d377 | |||
3ea3b0bf2e | |||
e1a43d2e7d | |||
2e918fe1d6 | |||
601309c7cc | |||
10ddeeb799 | |||
3463d6c752 | |||
8acce011b5 | |||
fe9ea50356 | |||
e6f29ae57f | |||
6cfd2c510b | |||
430ff80198 | |||
230fa76d57 | |||
46a4b0e0b6 | |||
5b3cadb7a8 | |||
3153071a8a | |||
bba7372556 | |||
9fe1a7e2ae | |||
98822a39d9 | |||
a2c830b908 | |||
bdef2cfdfb | |||
f229a5e2ec | |||
845e061382 | |||
e7d4eb1ae3 | |||
b4ba56bfb4 | |||
25784d1fe5 | |||
619eca7a51 | |||
f3d85655a0 | |||
9600675677 | |||
ce8a759192 | |||
88bc0bf613 | |||
b508e4208a | |||
c74d8cf499 | |||
a34c2b082f | |||
ad49a02879 | |||
e985ffc690 | |||
6cbb02f02d | |||
c0d0ff66b6 | |||
1e4d7f8c6e | |||
a8a761aa5f | |||
41952f0215 | |||
bfcc883f01 | |||
39722055f5 | |||
f85dfa90b8 | |||
0a4163d236 | |||
78de11a9e3 | |||
d2fc6d9f44 | |||
abf31f4a79 | |||
f28dd4f4de | |||
55b64899f5 | |||
d4aeeadb26 | |||
7ce0110158 | |||
7c1e55eb7f | |||
27542bc81d | |||
9ebbfb2d90 | |||
701b1ee744 | |||
0edc981cd2 | |||
da5942b398 | |||
709de81814 | |||
90b312a56e | |||
459759bfe5 | |||
00817aacfe | |||
e306eb0874 | |||
33a02b47d5 | |||
f0a5557e60 | |||
58a871c8cc | |||
4f56071786 | |||
f8b2c79aef | |||
8f00d34b0b | |||
6129519e5a | |||
593091a5e3 | |||
22ed163c8f | |||
93e2b88d41 | |||
7cd54dc8f0 | |||
ccd7c8df53 | |||
5b3bd3f470 | |||
bf1b7f44b6 | |||
538dd60580 | |||
f453236840 | |||
bfe7aa1ed2 | |||
9e2ef82902 | |||
9352e249ee | |||
3800065230 | |||
ebc2c4f73a | |||
f057440cc1 | |||
506f9cfca8 | |||
8a70c3353f | |||
3d8f123e05 | |||
a8c8f15e07 | |||
21e647017b | |||
2a1bb3dc27 | |||
55a3094a65 | |||
b4490e209b | |||
9aa676333c | |||
71b23e57ff | |||
2c76bc99fc | |||
bb06895145 | |||
684965f3e5 | |||
e621f4e2fa | |||
028ea57232 | |||
718fa25c10 | |||
90c9f28818 | |||
cb9c5a35cb | |||
fadaefeaef | |||
b17b882a3b | |||
f0f3afd5f1 | |||
42026b49bf | |||
151193c4c3 | |||
3448751e0e | |||
aae011ed83 | |||
c95a269460 | |||
98c0e5271f | |||
f343131802 | |||
ea34ba53b9 | |||
b8d8cf19d9 | |||
c9be4093e7 | |||
082eef708f | |||
9106fc5b94 | |||
918502742d | |||
f32f1eeaa5 | |||
2d1404d155 | |||
a56997e98c | |||
ef918078d1 | |||
7e61900cf5 | |||
e98f90b099 | |||
2e127dff1f | |||
828db19e02 | |||
99aa3f5713 | |||
1a568e2961 | |||
e863e8c64b | |||
f5b591430c | |||
8cfaf8eb51 | |||
675c0cefc3 | |||
1a52385b78 | |||
372e500590 | |||
cc1a317439 | |||
6d650518a1 | |||
7940117577 | |||
b0f87fdd21 | |||
dc92ffed87 | |||
4af578e310 | |||
e22825d818 | |||
e2da6259e7 | |||
d149017c60 | |||
afc400121b | |||
ef993515c6 | |||
edb1d21ddc | |||
ba8abd94a8 | |||
c6d4e4c15f | |||
09f0ac866f | |||
7ed25704d6 | |||
2196dac63e | |||
c8f70efded | |||
ea97488670 | |||
c2255b0a0f | |||
f754b081ce | |||
07771cb5e4 | |||
690d8e43ae | |||
82f14a7d59 | |||
b284384f0a | |||
1ae0d1b5d0 | |||
9de08c8166 | |||
a2d007f2a9 | |||
774f818bbb | |||
0ec7121b8f | |||
d7d46f4447 | |||
45fad147bf | |||
3664195c71 | |||
fce3cd00a1 | |||
33b3be0d0e | |||
cfd1b4a6c6 | |||
d45fefd6f0 | |||
f125ab01ee | |||
be001d090c | |||
971d8a7e40 | |||
a2cf210a52 | |||
3eec207166 | |||
b5d83bdb56 | |||
2c495c4119 | |||
7c72d6cb7c | |||
8362bf0886 | |||
1a8155c45b | |||
3f2f946019 | |||
2c14a8dee1 | |||
917a283bd1 | |||
3e403d5ab3 | |||
746d35b52b | |||
9a7a03e327 | |||
a051079c6a | |||
7b3c18bb97 | |||
52daf3d58c | |||
f41bde5ee1 | |||
6151318ac1 | |||
b45c322729 | |||
b00e8768dc | |||
156feb6e8e | |||
e942b8a402 | |||
abdb67a123 | |||
ee20787c5e | |||
ec4e631760 | |||
02b430a5bf | |||
7878053df2 | |||
12a593c3c6 | |||
6b1f130750 | |||
bde4c0a648 | |||
5ae4621da1 | |||
5ea8d0546e | |||
8a064c118f | |||
2f91c27df2 | |||
763bd54707 | |||
0ea3cc7ce4 | |||
0de3558ab3 | |||
069f4e12d8 | |||
ae4dfc9956 | |||
ee711dc0fb | |||
c316e7faab | |||
7083b3d912 | |||
2d3a1b6a9e | |||
0df23ab878 | |||
7ed8de2ef4 | |||
d935e22f0d | |||
0e26abf7a6 | |||
59aef13200 | |||
9d1f6c4416 | |||
b9f7660a91 | |||
18b5250ed1 | |||
f683f21ee2 | |||
bd033db84c | |||
ab036312a4 | |||
634da15191 | |||
cea1720ea0 | |||
3f2f542265 | |||
b77edb2b5b | |||
1b699bb814 | |||
333c035fed | |||
ce29914c56 | |||
70e5361146 | |||
e7d6dfff53 | |||
eebfad5a95 | |||
77c0a93ac6 | |||
63a3e126b3 | |||
3ea84cf0ce | |||
7fa80ae556 | |||
925f71af15 | |||
c666dd623d | |||
2cd8733212 | |||
4b2a9bc621 | |||
12a9d0575d | |||
edcfa28b0b | |||
3155829994 | |||
d25707554e | |||
38df44ef4b | |||
df683375b1 | |||
cc3cbbc4bb | |||
6922394b8e | |||
24fd82d773 | |||
57aefcd917 | |||
b3854ad382 | |||
5f5fc77877 | |||
0493e77cff | |||
6240fe1dfc | |||
beb7f90908 | |||
a3917972b4 | |||
7094fef37f | |||
0f41e56a24 | |||
52b283283f | |||
ebb15bf96c | |||
6c527d52fb | |||
b8ea57e097 | |||
909aed4262 | |||
4d2fff9538 | |||
9a45983f17 | |||
11926014da | |||
72002c13d6 | |||
6ed767ae84 | |||
3826b307f7 | |||
887b157056 | |||
d36dd39743 | |||
dd008bc13a | |||
50b282f58b | |||
f8a7efbce7 | |||
7d2caeb270 | |||
708e71a35a | |||
4eaccc966e | |||
3670d649b8 | |||
90ab04e81d | |||
26b8df5354 | |||
11a8046c5f | |||
da16110e1c | |||
914b686c8e | |||
27133520fc | |||
24b967ad5c | |||
ca4b4a3f1e | |||
faef35ec47 | |||
326d4c2641 | |||
83436c9550 | |||
2084822731 | |||
071bad1232 | |||
ae1a76da2b | |||
fbc6965c4e | |||
57a5862840 | |||
91fbccdbaa | |||
0ab0dd95ae | |||
bc41040fd3 | |||
4c8dfd0c0c | |||
2b9dbfb390 | |||
84d546b724 | |||
63053b9940 | |||
2256030a2a | |||
79da33b597 | |||
7d67450e58 | |||
8aa11951bf | |||
f23f22ab01 | |||
96a64c7bd2 | |||
d1bb0fdf1d | |||
feca30d7ed | |||
b650151693 | |||
bb3afd0dc9 | |||
5e77ae208d | |||
24e5a4d7ec | |||
1d10d29fa9 | |||
9b00e91773 | |||
cd73c30d6f | |||
7bbba0c7d9 | |||
7907a4fc24 | |||
2f94f62a56 | |||
85791a9336 | |||
a4eba50cfd | |||
03980b2f27 | |||
664e5cfb59 | |||
b9736df7e0 |
.editorconfig
.github
CODE_OF_CONDUCT.mdCONTRIBUTING.mdREADME.mdapp
build.gradle.ktsproguard-rules.pro
build.gradle.ktssrc
debug
res
mipmap-anydpi-v26
main
AndroidManifest.xml
java
eu
kanade
tachiyomi
App.ktAppInfo.ktAppModule.ktMigrations.kt
annotations
data
backup
AbstractBackupManager.ktAbstractBackupRestore.ktAbstractBackupRestoreValidator.ktBackupConst.ktBackupCreateService.ktBackupCreatorJob.ktBackupNotifier.ktBackupRestoreService.kt
full
legacy
cache
coil
database
DbOpenCallback.kt
mappers
CategoryTypeMapping.ktChapterTypeMapping.ktHistoryTypeMapping.ktMangaCategoryTypeMapping.ktMangaTypeMapping.ktTrackTypeMapping.kt
models
queries
CategoryQueries.ktChapterQueries.ktHistoryQueries.ktMangaCategoryQueries.ktMangaQueries.ktRawQueries.ktTrackQueries.kt
resolvers
ChapterBackupPutResolver.ktChapterKnownBackupPutResolver.ktChapterProgressPutResolver.ktChapterSourceOrderPutResolver.ktHistoryLastReadPutResolver.ktLibraryMangaGetResolver.ktMangaChapterGetResolver.ktMangaChapterHistoryGetResolver.ktMangaCoverLastModifiedPutResolver.ktMangaFavoritePutResolver.ktMangaFlagsPutResolver.ktMangaLastUpdatedPutResolver.ktMangaNextUpdatedPutResolver.ktMangaTitlePutResolver.ktSourceIdMangaCountGetResolver.kt
tables
download
DownloadCache.ktDownloadManager.ktDownloadNotifier.ktDownloadPendingDeleter.ktDownloadProvider.ktDownloadService.ktDownloadStore.ktDownloader.kt
model
library
notification
preference
saver
track
EnhancedTrackService.kt
anilist
bangumi
Avatar.ktBangumi.ktBangumiApi.ktBangumiInterceptor.ktBangumiModels.ktCollection.ktOAuth.ktStatus.ktUser.kt
job
kitsu
komga
model
myanimelist
shikimori
updater
extension
network
source
ui
base
activity
controller
BaseController.ktConductorExtensions.ktDialogController.ktNoAppBarElevationController.ktOneWayFadeChangeHandler.ktSearchableNucleusController.ktTabbedController.ktToolbarLiftOnScrollController.kt
delegate
presenter
browse
BrowseController.kt
extension
ExtensionAdapter.ktExtensionController.ktExtensionFilterController.ktExtensionGroupHolder.ktExtensionGroupItem.ktExtensionHolder.ktExtensionItem.ktExtensionPresenter.ktExtensionTrustDialog.kt
details
migration
manga
search
sources
source
LangItem.ktSourceController.ktSourceFilterController.ktSourceHolder.ktSourceItem.ktSourcePresenter.kt
browse
BrowseSourceController.ktBrowseSourcePresenter.ktSourceComfortableGridHolder.ktSourceCompactGridHolder.ktSourceFilterSheet.ktSourceGridHolder.ktSourceItem.ktSourceListHolder.kt
filter
globalsearch
GlobalSearchAdapter.ktGlobalSearchCardHolder.ktGlobalSearchCardItem.ktGlobalSearchController.ktGlobalSearchItem.ktGlobalSearchPresenter.kt
latest
category
download
DownloadAdapter.ktDownloadController.ktDownloadHeaderHolder.ktDownloadHeaderItem.ktDownloadHolder.ktDownloadItem.ktDownloadPresenter.kt
library
ChangeMangaCategoriesDialog.ktDeleteLibraryMangasDialog.ktLibraryAdapter.ktLibraryCategoryView.ktLibraryComfortableGridHolder.ktLibraryCompactGridHolder.ktLibraryController.ktLibraryHolder.ktLibraryItem.ktLibraryListHolder.ktLibraryPresenter.ktLibrarySettingsSheet.kt
setting
main
manga
MangaController.ktMangaPresenter.kt
chapter
ChapterDownloadView.ktChapterHolder.ktChapterItem.ktChaptersAdapter.ktChaptersSettingsSheet.ktDownloadCustomChaptersDialog.ktMangaChaptersHeaderAdapter.ktSetChapterSettingsDialog.kt
base
info
track
more
reader
PageIndicatorTextView.ktReaderActivity.ktReaderColorFilterView.ktReaderNavigationOverlayView.ktReaderPageSheet.ktReaderPresenter.ktReaderSeekBar.ktReaderSlider.ktSaveImageNotifier.kt
loader
ChapterLoader.ktDownloadPageLoader.ktEpubPageLoader.ktHttpPageLoader.ktRarPageLoader.ktZipPageLoader.kt
model
setting
OrientationType.ktReaderColorFilterSettings.ktReaderGeneralSettings.ktReaderReadingModeSettings.ktReaderSettingsSheet.ktReadingModeType.kt
viewer
recent
security
setting
SettingsAdvancedController.ktSettingsAppearanceController.ktSettingsBackupController.ktSettingsBrowseController.ktSettingsController.ktSettingsDownloadController.ktSettingsGeneralController.ktSettingsLibraryController.ktSettingsMainController.ktSettingsReaderController.ktSettingsSecurityController.ktSettingsTrackingController.kt
database
search
track
webview
util
CrashLogUtil.ktMangaExtensions.kt
chapter
lang
CloseableExtensions.ktDateExtensions.ktHash.ktRetryWithDelay.ktRxCoroutineBridge.ktStringExtensions.kt
preference
storage
system
AnimationExtensions.ktAuthenticatorUtil.ktContextExtensions.ktDeviceUtil.ktGLUtil.ktImageUtil.ktIntentExtensions.ktLocaleHelper.ktLogcatExtensions.ktNotificationExtensions.ktWebViewClientCompat.ktWebViewUtil.kt
view
widget
ActionModeWithToolbar.ktActionToolbar.ktDialogCustomDownloadView.ktElevationAppBarLayout.ktEmptyView.ktExtendedNavigationView.ktHideBottomNavigationOnScrollBehavior.ktMangaSummaryView.ktMaterialFastScroll.ktMaterialSpinnerView.ktMinMaxNumberPicker.ktNegativeSeekBar.ktOutlineSpan.ktRecyclerViewPagerAdapter.ktRevealAnimationView.ktSimpleNavigationView.ktStateImageViewTarget.ktTachiyomiAppBarLayout.ktTachiyomiBottomNavigationView.ktTachiyomiChangeHandlerFrameLayout.ktTachiyomiCoordinatorLayout.ktTachiyomiFullscreenDialog.ktTachiyomiScrollingViewBehavior.ktTachiyomiSearchView.ktTachiyomiTextInputEditText.ktThemedSwipeRefreshLayout.kt
listener
materialdialogs
MaterialAlertDialogBuilderExtensions.ktQuadStateMultiChoiceDialogAdapter.ktQuadStateMultiChoiceViewHolder.ktQuadStateTextView.kt
preference
LoginDialogPreference.ktLoginPreference.ktSwitchPreferenceCategory.ktThemesPreference.ktThemesPreferenceAdapter.ktTrackerPreference.kt
sheet
res
anim
color-v31
color
drawable
anim_caret_down.xmlanim_caret_up.xmlanim_library_enter.xmlanim_library_leave.xmlcard_gradient_shape.xmlcover_error.xmlic_broken_image_grey_24dp.xmlic_library_outline_24dp.xmlic_palette_24dp.xmlic_save_24dp.xmlic_screen_lock_rotation_24dp.xmlic_status_completed_24dp.xmlic_status_licensed_24dp.xmlic_status_ongoing_24dp.xmlic_status_unknown_24dp.xmlic_tachi_monochrome_launcher.xmlic_travel_explore_24dp.xmlmanga_backdrop_gradient.xmlmanga_info_gradient.xmlmanga_info_more_gradient.xmlmaterial_bubble_drawable.xmloval.xmlreader_seekbar_background.xmlreader_seekbar_button.xmlreader_seekbar_ripple.xmltab_indicator.xmltransparent_tabs_background.xml
font
layout-sw720dp
layout
action_toolbar.xmlcategories_item.xmlchapters_item.xmlclear_database_controller.xmlclear_database_source_item.xmlcommon_dialog_with_checkbox.xmlcommon_tabbed_sheet.xmlcommon_view_empty.xmldialog_stub_textinput.xmldownload_custom_amount.xmldownload_header.xmldownload_item.xmlextension_detail_header.xmlextension_item.xmlglobal_search_controller_card.xmlglobal_search_controller_card_item.xmlhistory_item.xmllibrary_controller.xmllicenses_controller.xmllicenses_item.xmlmain_activity.xmlmain_activity_toolbar.xmlmanga_chapters_header.xmlmanga_controller.xmlmanga_full_cover_dialog.xmlmanga_info_header.xmlmanga_summary.xmlmigration_sources_controller.xmlnavigation_view_checkbox.xmlnavigation_view_checkedtext.xmlnavigation_view_group.xmlnavigation_view_radio.xmlnavigation_view_spinner.xmlnavigation_view_text.xmlpref_account_login.xmlpref_library_columns.xmlpref_more_header.xmlpref_spinner.xmlpref_theme_item.xmlpref_themes_list.xmlpref_tracker_item.xmlpref_widget_imageview.xmlpref_widget_switch_material.xmlreader_activity.xmlreader_color_filter_settings.xmlreader_error.xmlreader_page_sheet.xmlreader_pager_settings.xmlreader_reading_mode_settings.xmlreader_transition_view.xmlreader_webtoon_settings.xmlsection_header_item.xmlsettings_search_controller_card.xmlsource_comfortable_grid_item.xmlsource_compact_grid_item.xmlsource_filter_sheet.xmlsource_grid_item_badges.xmlsource_list_item.xmlsource_main_controller_item.xmltrack_item.xmltrack_search_dialog.xmltrack_search_item.xmlupdates_controller.xmlupdates_item.xml
menu
browse_extensions.xmlbrowse_migrate.xmlbrowse_sources.xmlcategory_selection.xmlchapter_selection.xmldownload_single.xmlextension_details.xmlfull_cover.xmlgeneric_selection.xmlglobal_search.xmlhistory.xmllibrary.xmllibrary_selection.xmlmanga.xmlmigration.xmlreader.xmlsettings_backup.xmlsettings_main.xmlsettings_tracking.xmlsource_browse.xmltrack_search.xmlupdates.xmlupdates_chapter_selection.xmlwebview.xml
mipmap-anydpi-v26
values-aii
values-am
values-ar
values-b+es+419
values-be
values-bg
values-bn
values-ca
values-cs
values-cv
values-de
values-el
values-eo
values-es
values-eu
values-fa
values-fi
values-fil
values-fr
values-gl
values-he
values-hi
values-hr
values-hu
values-in
values-it
values-ja
values-jv
values-ka-rGE
values-kk
values-km
values-kn
values-ko
values-lt
values-lv
values-ml
values-mr
values-ms
values-my
values-nb-rNO
values-ne
values-night-v31
values-night
bools.xmlcolors.xmlcolors_greenapple.xmlcolors_midnightdusk.xmlcolors_strawberry.xmlcolors_tachiyomi.xmlcolors_tako.xmlcolors_tealturqoise.xmlcolors_yinyang.xmlcolors_yotsuba.xmlthemes.xml
values-nl
values-or
values-pl
values-pt-rBR
values-pt
values-ro
values-ru
values-sa
values-sah
values-sc
values-sdh
values-sk
values-sr
values-sv
values-sw600dp
values-sw720dp
values-te
values-th
values-tl
values-tr
values-uk
values-v26
values-v28
values-v31
values-vi
values-zh-rCN
values-zh-rTW
values
arrays.xmlattrs.xmlbools.xmlcolors.xmlcolors_greenapple.xmlcolors_midnightdusk.xmlcolors_strawberry.xmlcolors_tachiyomi.xmlcolors_tako.xmlcolors_tealturqoise.xmlcolors_yinyang.xmlcolors_yotsuba.xmldimens.xmlstrings.xmlstyles.xmlthemes.xml
xml
standard
test
java
eu
kanade
tachiyomi
data
buildSrc/src/main/kotlin
gradle
ktlintCodeStyle.xmlsettings.gradle.kts
5
.editorconfig
Normal file
5
.editorconfig
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[*.{kt,kts}]
|
||||||
|
indent_size=4
|
||||||
|
insert_final_newline=true
|
||||||
|
ij_kotlin_allow_trailing_comma=true
|
||||||
|
ij_kotlin_allow_trailing_comma_on_call_site=true
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -3,7 +3,7 @@
|
|||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v0.11.1)
|
- To the latest version of the app (stable is v0.13.2)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
104
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
104
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -3,57 +3,6 @@ description: Report an issue in Tachiyomi
|
|||||||
labels: [Bug]
|
labels: [Bug]
|
||||||
body:
|
body:
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
|
||||||
options:
|
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
|
||||||
required: true
|
|
||||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
|
||||||
required: true
|
|
||||||
- label: I have updated the app to version **[0.11.1](https://github.com/tachiyomiorg/tachiyomi/releases/tag/v0.11.1)**.
|
|
||||||
required: true
|
|
||||||
- label: I have updated all installed extensions.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: tachiyomi-version
|
|
||||||
attributes:
|
|
||||||
label: Tachiyomi version
|
|
||||||
description: You can find your Tachiyomi version in **More → About**.
|
|
||||||
placeholder: |
|
|
||||||
Example: "0.11.1"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: android-version
|
|
||||||
attributes:
|
|
||||||
label: Android version
|
|
||||||
description: You can find this somewhere in your Android settings.
|
|
||||||
placeholder: |
|
|
||||||
Example: "Android 11"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: device
|
|
||||||
attributes:
|
|
||||||
label: Device
|
|
||||||
description: List your device and model.
|
|
||||||
placeholder: |
|
|
||||||
Example: "Google Pixel 5"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: reproduce-steps
|
id: reproduce-steps
|
||||||
attributes:
|
attributes:
|
||||||
@ -84,7 +33,7 @@ body:
|
|||||||
label: Actual behavior
|
label: Actual behavior
|
||||||
description: Explain what actually happens.
|
description: Explain what actually happens.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example:
|
Example:
|
||||||
"This happened instead..."
|
"This happened instead..."
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@ -98,9 +47,60 @@ body:
|
|||||||
placeholder: |
|
placeholder: |
|
||||||
You can paste the crash logs in pure text or upload it as an attachment.
|
You can paste the crash logs in pure text or upload it as an attachment.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: tachiyomi-version
|
||||||
|
attributes:
|
||||||
|
label: Tachiyomi version
|
||||||
|
description: You can find your Tachiyomi version in **More → About**.
|
||||||
|
placeholder: |
|
||||||
|
Example: "0.13.2"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: android-version
|
||||||
|
attributes:
|
||||||
|
label: Android version
|
||||||
|
description: You can find this somewhere in your Android settings.
|
||||||
|
placeholder: |
|
||||||
|
Example: "Android 11"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: device
|
||||||
|
attributes:
|
||||||
|
label: Device
|
||||||
|
description: List your device and model.
|
||||||
|
placeholder: |
|
||||||
|
Example: "Google Pixel 5"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: other-details
|
id: other-details
|
||||||
attributes:
|
attributes:
|
||||||
label: Other details
|
label: Other details
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Additional details and attachments.
|
Additional details and attachments.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||||
|
required: true
|
||||||
|
- label: I have updated the app to version **[0.13.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
|
required: true
|
||||||
|
- label: I have updated all installed extensions.
|
||||||
|
required: true
|
||||||
|
- label: I will fill out all of the requested information in this form.
|
||||||
|
required: true
|
||||||
|
34
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
34
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -3,23 +3,6 @@ description: Suggest a feature to improve Tachiyomi
|
|||||||
labels: [Feature request]
|
labels: [Feature request]
|
||||||
body:
|
body:
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
|
||||||
options:
|
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
|
||||||
required: true
|
|
||||||
- label: I have updated the app to version **[0.11.1](https://github.com/tachiyomiorg/tachiyomi/releases/tag/v0.11.1)**.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: feature-description
|
id: feature-description
|
||||||
attributes:
|
attributes:
|
||||||
@ -37,3 +20,20 @@ body:
|
|||||||
label: Other details
|
label: Other details
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Additional details and attachments.
|
Additional details and attachments.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
- label: I have updated the app to version **[0.13.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
|
required: true
|
||||||
|
- label: I will fill out all of the requested information in this form.
|
||||||
|
required: true
|
||||||
|
12
.github/pull_request_template.md
vendored
Normal file
12
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!--
|
||||||
|
Please include a summary of the change and which issue is fixed.
|
||||||
|
Also make sure you've tested your code and also done a self-review of it.
|
||||||
|
Don't forget to check all base themes and tablet mode for relevant changes.
|
||||||
|
|
||||||
|
If your changes are visual, please provide images below:
|
||||||
|
|
||||||
|
### Images
|
||||||
|
| Image 1 | Image 2 |
|
||||||
|
| ------- | ------- |
|
||||||
|
|  |  |
|
||||||
|
-->
|
BIN
.github/readme-images/screens.png
vendored
BIN
.github/readme-images/screens.png
vendored
Binary file not shown.
Before ![]() (image error) Size: 454 KiB |
3
.github/runner-files/ci-gradle.properties
vendored
3
.github/runner-files/ci-gradle.properties
vendored
@ -2,5 +2,4 @@ org.gradle.daemon=false
|
|||||||
org.gradle.jvmargs=-Xmx5120m
|
org.gradle.jvmargs=-Xmx5120m
|
||||||
org.gradle.workers.max=2
|
org.gradle.workers.max=2
|
||||||
|
|
||||||
kotlin.incremental=false
|
kotlin.incremental=false
|
||||||
kotlin.compiler.execution.strategy=in-process
|
|
33
.github/workflows/build_pull_request.yml
vendored
Normal file
33
.github/workflows/build_pull_request.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: PR build check
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
- 'app/src/main/res/**/strings.xml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build app
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Clone repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Validate Gradle Wrapper
|
||||||
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
|
- name: Set up JDK 11
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 11
|
||||||
|
|
||||||
|
- name: Copy CI gradle.properties
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.gradle
|
||||||
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
|
- name: Build app
|
||||||
|
uses: gradle/gradle-command-action@v2
|
||||||
|
with:
|
||||||
|
arguments: assembleStandardRelease
|
@ -5,34 +5,25 @@ on:
|
|||||||
- master
|
- master
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check_wrapper:
|
build:
|
||||||
name: Validate Gradle Wrapper
|
name: Build app
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Cancel previous runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.9.1
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
|
all_but_latest: true
|
||||||
|
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build app
|
|
||||||
needs: check_wrapper
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Cancel previous runs
|
|
||||||
uses: styfle/cancel-workflow-action@0.5.0
|
|
||||||
with:
|
|
||||||
access_token: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Clone repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
@ -44,24 +35,18 @@ jobs:
|
|||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: assembleStandardRelease
|
arguments: assembleStandardRelease
|
||||||
wrapper-cache-enabled: true
|
|
||||||
dependencies-cache-enabled: true
|
|
||||||
configuration-cache-enabled: true
|
|
||||||
|
|
||||||
# Sign APK and create release for tags
|
# Sign APK and create release for tags
|
||||||
|
|
||||||
- name: Get tag name
|
- name: Get tag name
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
id: get_tag_name
|
|
||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||||
|
|
||||||
# TODO: need to support multiple APKs
|
|
||||||
|
|
||||||
- name: Sign APK
|
- name: Sign APK
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
uses: r0adkll/sign-android-release@v1
|
uses: r0adkll/sign-android-release@v1
|
||||||
@ -75,9 +60,23 @@ jobs:
|
|||||||
- name: Clean up build artifacts
|
- name: Clean up build artifacts
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
run: |
|
run: |
|
||||||
cp ${{ env.SIGNED_RELEASE_FILE }} tachiyomi-${{ env.VERSION_TAG }}.apk
|
set -e
|
||||||
md5=`md5sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_MD5=$md5" >> $GITHUB_ENV
|
mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||||
|
sha=`sha256sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||||
|
echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk tachiyomi-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
||||||
|
sha=`sha256sum tachiyomi-arm64-v8a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||||
|
echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk tachiyomi-armeabi-v7a-${{ env.VERSION_TAG }}.apk
|
||||||
|
sha=`sha256sum tachiyomi-armeabi-v7a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||||
|
echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk tachiyomi-x86-${{ env.VERSION_TAG }}.apk
|
||||||
|
sha=`sha256sum tachiyomi-x86-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||||
|
echo "APK_X86_SHA=$sha" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
@ -86,9 +85,21 @@ jobs:
|
|||||||
tag_name: ${{ env.VERSION_TAG }}
|
tag_name: ${{ env.VERSION_TAG }}
|
||||||
name: Tachiyomi ${{ env.VERSION_TAG }}
|
name: Tachiyomi ${{ env.VERSION_TAG }}
|
||||||
body: |
|
body: |
|
||||||
MD5: ${{ env.APK_MD5 }}
|
---
|
||||||
|
|
||||||
|
### Checksums
|
||||||
|
|
||||||
|
| Variant | SHA-256 |
|
||||||
|
| ------- | ------- |
|
||||||
|
| Universal | ${{ env.APK_UNIVERSAL_SHA }}
|
||||||
|
| arm64-v8a | ${{ env.APK_ARM64_V8A_SHA }}
|
||||||
|
| armeabi-v7a | ${{ env.APK_ARMEABI_V7A_SHA }}
|
||||||
|
| x86 | ${{ env.APK_X86_SHA }} |
|
||||||
files: |
|
files: |
|
||||||
tachiyomi-${{ env.VERSION_TAG }}.apk
|
tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||||
|
tachiyomi-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
||||||
|
tachiyomi-armeabi-v7a-${{ env.VERSION_TAG }}.apk
|
||||||
|
tachiyomi-x86-${{ env.VERSION_TAG }}.apk
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
env:
|
env:
|
16
.github/workflows/cancel_pull_request.yml
vendored
Normal file
16
.github/workflows/cancel_pull_request.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
name: Cancel old pull request workflows
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["PR build check"]
|
||||||
|
types:
|
||||||
|
- requested
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cancel:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: styfle/cancel-workflow-action@0.9.1
|
||||||
|
with:
|
||||||
|
all_but_latest: true
|
||||||
|
workflow_id: ${{ github.event.workflow.id }}
|
32
.github/workflows/issue_closer.yml
vendored
32
.github/workflows/issue_closer.yml
vendored
@ -1,32 +0,0 @@
|
|||||||
name: Issue closer
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened, edited, reopened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
autoclose:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Autoclose issues
|
|
||||||
uses: arkon/issue-closer-action@v3.4
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
rules: |
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"type": "body",
|
|
||||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
|
||||||
"message": "The acknowledgment section was not removed."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
23
.github/workflows/issue_moderator.yml
vendored
23
.github/workflows/issue_moderator.yml
vendored
@ -1,6 +1,8 @@
|
|||||||
name: Issue moderator
|
name: Issue moderator
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, edited, reopened]
|
||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
@ -9,6 +11,25 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Moderate issues
|
- name: Moderate issues
|
||||||
uses: tachiyomiorg/issue-moderator-action@v1.1
|
uses: tachiyomiorg/issue-moderator-action@v1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
auto-close-rules: |
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "body",
|
||||||
|
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||||
|
"message": "The acknowledgment section was not removed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "body",
|
||||||
|
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||||
|
"message": "Requested information in the template was not filled out."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "both",
|
||||||
|
"regex": "^(?!.*myanimelist.*).*(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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
8
.github/workflows/lock.yml
vendored
8
.github/workflows/lock.yml
vendored
@ -3,7 +3,7 @@ name: Lock threads
|
|||||||
on:
|
on:
|
||||||
# Daily
|
# Daily
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 * * * *'
|
- cron: '0 0 * * *'
|
||||||
# Manual trigger
|
# Manual trigger
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
@ -12,8 +12,8 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v2
|
- uses: dessant/lock-threads@v3
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-lock-inactive-days: '2'
|
issue-inactive-days: '2'
|
||||||
pr-lock-inactive-days: '2'
|
pr-inactive-days: '2'
|
||||||
|
@ -1,76 +1,126 @@
|
|||||||
# Code of Conduct
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
contributors and maintainers pledge to making participation in our project and
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
level of experience, education, socio-economic status, nationality, personal
|
nationality, personal appearance, race, caste, color, religion, or sexual identity
|
||||||
appearance, race, religion, or sexual identity and orientation.
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
Examples of behavior that contributes to a positive environment for our
|
||||||
include:
|
community include:
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
* Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing viewpoints and experiences
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Gracefully accepting constructive criticism
|
* Giving and gracefully accepting constructive feedback
|
||||||
* Focusing on what is best for the community
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
* Showing empathy towards other community members
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
advances
|
advances of any kind
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
* Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic
|
* Publishing others' private information, such as a physical or email
|
||||||
address, without explicit permission
|
address, without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
## Our Responsibilities
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
Community moderators are responsible for clarifying and enforcing our standards of
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
response to any instances of unacceptable behavior.
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
Community moderators have the right and responsibility to remove, edit, or reject
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
permanently any contributor for other behaviors that they deem inappropriate,
|
decisions when appropriate.
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
when an individual is representing the project or its community. Examples of
|
an individual is officially representing the community in public spaces.
|
||||||
representing a project or community include using an official project e-mail
|
Examples of representing our community include using an official e-mail address,
|
||||||
address, posting via an official social media account, or acting as an appointed
|
posting via an official social media account, or acting as an appointed
|
||||||
representative at an online or offline event. Representation of a project may be
|
representative at an online or offline event.
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
|
reported to the community moderators responsible for enforcement at
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
the [Tachiyomi Discord server](https://discord.gg/tachiyomi).
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
||||||
Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
All community moderators are obligated to respect the privacy and security of the
|
||||||
faith may face temporary or permanent repercussions as determined by other
|
reporter of any incident.
|
||||||
members of the project's leadership.
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community moderators will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community moderators, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
|
||||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
version 2.1, available at
|
||||||
|
[v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
https://www.contributor-covenant.org/faq
|
[FAQ](https://www.contributor-covenant.org/faq). Translations are available
|
||||||
|
at [translations](https://www.contributor-covenant.org/translations).
|
||||||
|
@ -10,7 +10,23 @@ Thanks for your interest in contributing to Tachiyomi!
|
|||||||
Pull requests are welcome!
|
Pull requests are welcome!
|
||||||
|
|
||||||
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
||||||
|
You do not need to ask for permission nor an assignment.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before you start, please note that the ability to use following technologies is **required** and that existing contributors will not actively teach them to you.
|
||||||
|
|
||||||
|
- Basic [Android development](https://developer.android.com/)
|
||||||
|
- [Kotlin](https://kotlinlang.org/)
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
- [Android Studio](https://developer.android.com/studio)
|
||||||
|
- Emulator or phone with developer options enabled to test changes.
|
||||||
|
|
||||||
|
## Getting help
|
||||||
|
|
||||||
|
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
|
|
||||||
@ -26,7 +42,7 @@ When creating a fork, remember to:
|
|||||||
- To avoid confusion with the main app:
|
- To avoid confusion with the main app:
|
||||||
- Change the app name
|
- Change the app name
|
||||||
- Change the app icon
|
- Change the app icon
|
||||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/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/AppUpdateChecker.kt)
|
||||||
- To avoid installation conflicts:
|
- To avoid installation conflicts:
|
||||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||||
|
@ -6,8 +6,6 @@
|
|||||||
# Tachiyomi
|
# Tachiyomi
|
||||||
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Features include:
|
Features include:
|
||||||
@ -38,7 +36,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
|
|
||||||
<details><summary>Bugs</summary>
|
<details><summary>Bugs</summary>
|
||||||
|
|
||||||
* Include version (More > About > Version)
|
* Include version (More → About → Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* Preview version is equal to the number of commits as seen in the main page
|
* Preview version is equal to the number of commits as seen in the main page
|
||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
@ -13,7 +9,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||||
apply(plugin = "com.google.gms.google-services")
|
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||||
@ -28,14 +24,14 @@ android {
|
|||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
minSdk = AndroidConfig.minSdk
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdk = AndroidConfig.targetSdk
|
targetSdk = AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
versionCode = 78
|
||||||
versionCode = 66
|
versionName = "0.13.2"
|
||||||
versionName = "0.12.0"
|
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
||||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||||
|
buildConfigField("boolean", "PREVIEW", "false")
|
||||||
|
|
||||||
// Please disable ACRA or use your own instance in forked versions of the project
|
// Please disable ACRA or use your own instance in forked versions of the project
|
||||||
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
||||||
@ -43,11 +39,13 @@ android {
|
|||||||
ndk {
|
ndk {
|
||||||
abiFilters += SUPPORTED_ABIS
|
abiFilters += SUPPORTED_ABIS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
isEnable = false
|
isEnable = true
|
||||||
reset()
|
reset()
|
||||||
include(*SUPPORTED_ABIS.toTypedArray())
|
include(*SUPPORTED_ABIS.toTypedArray())
|
||||||
isUniversalApk = true
|
isUniversalApk = true
|
||||||
@ -58,28 +56,28 @@ android {
|
|||||||
named("debug") {
|
named("debug") {
|
||||||
versionNameSuffix = "-${getCommitCount()}"
|
versionNameSuffix = "-${getCommitCount()}"
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
|
|
||||||
isShrinkResources = true
|
|
||||||
isMinifyEnabled = true
|
|
||||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
|
||||||
}
|
|
||||||
create("debugFull") { // Debug without R8
|
|
||||||
initWith(getByName("debug"))
|
|
||||||
isShrinkResources = false
|
|
||||||
isMinifyEnabled = false
|
|
||||||
}
|
}
|
||||||
named("release") {
|
named("release") {
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
||||||
}
|
}
|
||||||
|
create("preview") {
|
||||||
|
initWith(getByName("release"))
|
||||||
|
buildConfigField("boolean", "PREVIEW", "true")
|
||||||
|
|
||||||
|
val debugType = getByName("debug")
|
||||||
|
signingConfig = debugType.signingConfig
|
||||||
|
versionNameSuffix = debugType.versionNameSuffix
|
||||||
|
applicationIdSuffix = debugType.applicationIdSuffix
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("debugFull").res.srcDirs("src/debug/res")
|
getByName("preview").res.srcDirs("src/debug/res")
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions("default")
|
flavorDimensions.add("default")
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
create("standard") {
|
create("standard") {
|
||||||
@ -87,18 +85,22 @@ android {
|
|||||||
dimension = "default"
|
dimension = "default"
|
||||||
}
|
}
|
||||||
create("dev") {
|
create("dev") {
|
||||||
resConfigs("en", "xxhdpi")
|
resourceConfigurations.addAll(listOf("en", "xxhdpi"))
|
||||||
dimension = "default"
|
dimension = "default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
exclude("META-INF/DEPENDENCIES")
|
resources.excludes.addAll(listOf(
|
||||||
exclude("LICENSE.txt")
|
"META-INF/DEPENDENCIES",
|
||||||
exclude("META-INF/LICENSE")
|
"LICENSE.txt",
|
||||||
exclude("META-INF/LICENSE.txt")
|
"META-INF/LICENSE",
|
||||||
exclude("META-INF/NOTICE")
|
"META-INF/LICENSE.txt",
|
||||||
exclude("META-INF/*.kotlin_module")
|
"META-INF/README.md",
|
||||||
|
"META-INF/NOTICE",
|
||||||
|
"META-INF/*.kotlin_module",
|
||||||
|
"META-INF/*.version",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
@ -107,12 +109,17 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
|
|
||||||
|
// Disable some unused things
|
||||||
|
aidl = false
|
||||||
|
renderScript = false
|
||||||
|
shaders = false
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
disable("MissingTranslation", "ExtraTranslation")
|
disable.addAll(listOf("MissingTranslation", "ExtraTranslation"))
|
||||||
isAbortOnError = false
|
abortOnError = false
|
||||||
isCheckReleaseBuilds = false
|
checkReleaseBuilds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@ -126,150 +133,121 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(kotlinx.reflect)
|
||||||
|
|
||||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
implementation(kotlinx.bundles.coroutines)
|
||||||
|
|
||||||
val coroutinesVersion = "1.5.1"
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
|
||||||
|
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
implementation("org.tachiyomi:source-api:1.1")
|
implementation(libs.tachiyomi.api)
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
implementation(androidx.annotation)
|
||||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
implementation(androidx.appcompat)
|
||||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
implementation(androidx.biometricktx)
|
||||||
implementation("androidx.browser:browser:1.3.0")
|
implementation(androidx.constraintlayout)
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
implementation(androidx.coordinatorlayout)
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0")
|
implementation(androidx.corektx)
|
||||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
implementation(androidx.splashscreen)
|
||||||
implementation("androidx.core:core-ktx:1.7.0-alpha01")
|
implementation(androidx.recyclerview)
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
|
implementation(androidx.swiperefreshlayout)
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
implementation(androidx.viewpager)
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
|
||||||
|
|
||||||
val lifecycleVersion = "2.4.0-alpha01"
|
implementation(androidx.bundles.lifecycle)
|
||||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation("androidx.work:work-runtime-ktx:2.6.0-beta01")
|
implementation(androidx.work.runtime)
|
||||||
|
|
||||||
// UI library
|
// RX
|
||||||
implementation("com.google.android.material:material:1.5.0-alpha01")
|
implementation(libs.bundles.reactivex)
|
||||||
|
implementation(libs.flowreactivenetwork)
|
||||||
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
|
||||||
|
|
||||||
// ReactiveX
|
|
||||||
implementation("io.reactivex:rxandroid:1.2.1")
|
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
|
||||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
|
||||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
val okhttpVersion = "4.9.1"
|
implementation(libs.bundles.okhttp)
|
||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
implementation(libs.okio)
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
|
||||||
implementation("com.squareup.okio:okio:2.10.0")
|
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
implementation(libs.conscrypt.android)
|
||||||
|
|
||||||
// JSON
|
// Data serialization (JSON, protobuf)
|
||||||
val kotlinSerializationVersion = "1.2.2"
|
implementation(kotlinx.bundles.serialization)
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
|
||||||
implementation("com.google.code.gson:gson:2.8.7")
|
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
implementation(libs.bundles.js.engine)
|
||||||
|
|
||||||
// Disk
|
|
||||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
|
||||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
|
||||||
implementation("com.github.junrar:junrar:7.4.0")
|
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation("org.jsoup:jsoup:1.14.1")
|
implementation(libs.jsoup)
|
||||||
|
|
||||||
|
// Disk
|
||||||
|
implementation(libs.disklrucache)
|
||||||
|
implementation(libs.unifile)
|
||||||
|
implementation(libs.junrar)
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
implementation(libs.bundles.sqlite)
|
||||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||||
implementation("com.github.requery:sqlite-android:3.36.0")
|
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
implementation(libs.preferencektx)
|
||||||
|
implementation(libs.flowpreferences)
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
val nucleusVersion = "3.0.0"
|
implementation(libs.bundles.nucleus)
|
||||||
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
|
|
||||||
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
|
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation(libs.injekt.core)
|
||||||
|
|
||||||
// Image library
|
// Image loading
|
||||||
val coilVersion = "1.3.2"
|
implementation(libs.bundles.coil)
|
||||||
implementation("io.coil-kt:coil:$coilVersion")
|
|
||||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
|
||||||
|
|
||||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
implementation(libs.subsamplingscaleimageview) {
|
||||||
exclude(module = "image-decoder")
|
exclude(module = "image-decoder")
|
||||||
}
|
}
|
||||||
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
|
implementation(libs.image.decoder)
|
||||||
|
|
||||||
// Logging
|
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
|
||||||
|
|
||||||
// Crash reports
|
|
||||||
implementation("ch.acra:acra-http:5.8.1")
|
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
implementation(libs.natural.comparator)
|
||||||
|
|
||||||
// UI
|
// UI libraries
|
||||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
implementation(libs.material)
|
||||||
implementation("eu.davidea:flexible-adapter:5.1.0")
|
implementation(libs.androidprocessbutton)
|
||||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
implementation(libs.flexible.adapter.core)
|
||||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
implementation(libs.flexible.adapter.ui)
|
||||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
implementation(libs.viewstatepageradapter)
|
||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
implementation(libs.photoview)
|
||||||
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
implementation(libs.directionalviewpager) {
|
||||||
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
|
}
|
||||||
|
implementation(libs.insetter)
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
val conductorVersion = "3.0.0"
|
implementation(libs.bundles.conductor)
|
||||||
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
|
||||||
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
|
||||||
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
val flowbindingVersion = "1.2.0"
|
implementation(libs.bundles.flowbinding)
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
// Logging
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
implementation(libs.logcat)
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
// Crash reports/analytics
|
||||||
|
implementation(libs.acra.http)
|
||||||
|
"standardImplementation"(libs.firebase.analytics)
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
implementation(libs.aboutlibraries.core)
|
||||||
|
|
||||||
|
// Shizuku
|
||||||
|
implementation(libs.bundles.shizuku)
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation(libs.junit)
|
||||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
testImplementation(libs.assertj.core)
|
||||||
testImplementation("org.mockito:mockito-core:1.10.19")
|
testImplementation(libs.mockito.core)
|
||||||
|
|
||||||
val robolectricVersion = "3.1.4"
|
testImplementation(libs.bundles.robolectric)
|
||||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
|
||||||
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
// debugImplementation(libs.leakcanary.android)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
@ -278,12 +256,12 @@ tasks {
|
|||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-Xopt-in=kotlin.Experimental",
|
"-Xopt-in=kotlin.Experimental",
|
||||||
"-Xopt-in=kotlin.RequiresOptIn",
|
"-Xopt-in=kotlin.RequiresOptIn",
|
||||||
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
|
"-Xopt-in=kotlin.ExperimentalStdlibApi",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
"-Xopt-in=kotlinx.coroutines.FlowPreview",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
|
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
"-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
|
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,40 +277,8 @@ tasks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath(kotlin("gradle-plugin", version = BuildPluginsVersion.KOTLIN))
|
classpath(kotlinx.gradle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Git is needed in your system PATH for these commands to work.
|
|
||||||
// If it's not installed, you can return a random value as a workaround
|
|
||||||
fun getCommitCount(): String {
|
|
||||||
return runCommand("git rev-list --count HEAD")
|
|
||||||
// return "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGitSha(): String {
|
|
||||||
return runCommand("git rev-parse --short HEAD")
|
|
||||||
// return "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBuildTime(): String {
|
|
||||||
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
|
||||||
df.timeZone = TimeZone.getTimeZone("UTC")
|
|
||||||
return df.format(Date())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runCommand(command: String): String {
|
|
||||||
val byteOut = ByteArrayOutputStream()
|
|
||||||
project.exec {
|
|
||||||
commandLine = command.split(" ")
|
|
||||||
standardOutput = byteOut
|
|
||||||
}
|
|
||||||
return String(byteOut.toByteArray()).trim()
|
|
||||||
}
|
|
||||||
|
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@ -12,6 +12,7 @@
|
|||||||
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||||
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
||||||
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
||||||
|
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
||||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||||
|
|
||||||
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
||||||
|
@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@android:color/transparent"/>
|
<background android:drawable="@android:color/transparent"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
@ -18,6 +18,7 @@
|
|||||||
<!-- For managing extensions -->
|
<!-- For managing extensions -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
<!-- To view extension packages in API 30+ -->
|
<!-- To view extension packages in API 30+ -->
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
|
||||||
@ -177,17 +178,16 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.updater.UpdaterService"
|
android:name=".data.updater.AppUpdateService"
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".data.backup.BackupCreateService"
|
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.backup.BackupRestoreService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service android:name=".extension.util.ExtensionInstallService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
@ -198,6 +198,19 @@
|
|||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="rikka.shizuku.ShizukuProvider"
|
||||||
|
android:authorities="${applicationId}.shizuku"
|
||||||
|
android:multiprocess="false"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
|
||||||
|
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||||
|
android:value="false" />
|
||||||
|
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -12,46 +12,52 @@ import android.webkit.WebView
|
|||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
import coil.decode.GifDecoder
|
import coil.decode.GifDecoder
|
||||||
import coil.decode.ImageDecoderDecoder
|
import coil.decode.ImageDecoderDecoder
|
||||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
import coil.disk.DiskCache
|
||||||
|
import coil.util.DebugLogger
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
||||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
||||||
|
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
|
||||||
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import logcat.AndroidLogcatLogger
|
||||||
|
import logcat.LogPriority
|
||||||
|
import logcat.LogcatLogger
|
||||||
import org.acra.config.httpSender
|
import org.acra.config.httpSender
|
||||||
import org.acra.ktx.initAcra
|
import org.acra.ktx.initAcra
|
||||||
import org.acra.sender.HttpSender
|
import org.acra.sender.HttpSender
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
|
|
||||||
open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super<Application>.onCreate()
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
@ -87,7 +93,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
this@App,
|
this@App,
|
||||||
0,
|
0,
|
||||||
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
||||||
PendingIntent.FLAG_ONE_SHOT
|
PendingIntent.FLAG_ONE_SHOT,
|
||||||
)
|
)
|
||||||
setContentIntent(pendingIntent)
|
setContentIntent(pendingIntent)
|
||||||
}
|
}
|
||||||
@ -106,37 +112,60 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
|
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
|
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||||
|
|
||||||
|
if (!LogcatLogger.isInstalled && preferences.verboseLogging()) {
|
||||||
|
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun newImageLoader(): ImageLoader {
|
||||||
return ImageLoader.Builder(this).apply {
|
return ImageLoader.Builder(this).apply {
|
||||||
componentRegistry {
|
val callFactoryInit = { Injekt.get<NetworkHelper>().client }
|
||||||
add(TachiyomiImageDecoder(this@App.resources))
|
val diskCacheInit = { CoilDiskCache.get(this@App) }
|
||||||
|
components {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
add(ImageDecoderDecoder(this@App))
|
add(ImageDecoderDecoder.Factory())
|
||||||
} else {
|
} else {
|
||||||
add(GifDecoder())
|
add(GifDecoder.Factory())
|
||||||
}
|
}
|
||||||
add(ByteBufferFetcher())
|
add(TachiyomiImageDecoder.Factory())
|
||||||
add(MangaCoverFetcher())
|
add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
|
||||||
|
add(MangaCoverKeyer())
|
||||||
}
|
}
|
||||||
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
callFactory(callFactoryInit)
|
||||||
crossfade(300)
|
diskCache(diskCacheInit)
|
||||||
|
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||||
|
if (preferences.verboseLogging()) logger(DebugLogger())
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
@Suppress("unused")
|
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
||||||
fun onAppBackgrounded() {
|
|
||||||
if (preferences.lockAppAfter().get() >= 0) {
|
|
||||||
SecureActivityDelegate.locked = true
|
SecureActivityDelegate.locked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getPackageName(): String {
|
||||||
|
try {
|
||||||
|
// Override the value passed as X-Requested-With in WebView requests
|
||||||
|
val stackTrace = Thread.currentThread().stackTrace
|
||||||
|
for (element in stackTrace) {
|
||||||
|
if ("org.chromium.base.BuildInfo".equals(element.className, ignoreCase = true)) {
|
||||||
|
if ("getAll".equals(element.methodName, ignoreCase = true)) {
|
||||||
|
return WebViewUtil.SPOOF_PACKAGE_NAME
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
return super.getPackageName()
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun setupAcra() {
|
protected open fun setupAcra() {
|
||||||
if (BuildConfig.FLAVOR != "dev") {
|
if (BuildConfig.FLAVOR != "dev") {
|
||||||
initAcra {
|
initAcra {
|
||||||
@ -152,7 +181,11 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupNotificationChannels() {
|
protected open fun setupNotificationChannels() {
|
||||||
Notifications.createChannels(this)
|
try {
|
||||||
|
Notifications.createChannels(this)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class DisableIncognitoReceiver : BroadcastReceiver() {
|
private inner class DisableIncognitoReceiver : BroadcastReceiver() {
|
||||||
@ -176,8 +209,27 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
|
||||||
|
/**
|
||||||
|
* Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
|
||||||
|
*/
|
||||||
|
internal object CoilDiskCache {
|
||||||
|
|
||||||
|
private const val FOLDER_NAME = "image_cache"
|
||||||
|
private var instance: DiskCache? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun get(context: Context): DiskCache {
|
||||||
|
return instance ?: run {
|
||||||
|
val safeCacheDir = context.cacheDir.apply { mkdirs() }
|
||||||
|
// Create the singleton disk cache instance.
|
||||||
|
DiskCache.Builder()
|
||||||
|
.directory(safeCacheDir.resolve(FOLDER_NAME))
|
||||||
|
.build()
|
||||||
|
.also { instance = it }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
app/src/main/java/eu/kanade/tachiyomi/AppInfo.kt
Normal file
11
app/src/main/java/eu/kanade/tachiyomi/AppInfo.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by extensions.
|
||||||
|
*
|
||||||
|
* @since extension-lib 1.3
|
||||||
|
*/
|
||||||
|
object AppInfo {
|
||||||
|
fun getVersionCode() = BuildConfig.VERSION_CODE
|
||||||
|
fun getVersionName() = BuildConfig.VERSION_NAME
|
||||||
|
}
|
@ -7,7 +7,9 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
|
|||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
@ -23,6 +25,8 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
addSingleton(app)
|
addSingleton(app)
|
||||||
|
|
||||||
|
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||||
|
|
||||||
addSingletonFactory { PreferencesHelper(app) }
|
addSingletonFactory { PreferencesHelper(app) }
|
||||||
|
|
||||||
addSingletonFactory { DatabaseHelper(app) }
|
addSingletonFactory { DatabaseHelper(app) }
|
||||||
@ -41,7 +45,9 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { TrackManager(app) }
|
addSingletonFactory { TrackManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
addSingletonFactory { DelayedTrackingStore(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { ImageSaver(app) }
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
ContextCompat.getMainExecutor(app).execute {
|
ContextCompat.getMainExecutor(app).execute {
|
||||||
|
@ -5,16 +5,21 @@ import androidx.core.content.edit
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
|
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
|
import eu.kanade.tachiyomi.util.preference.minusAssign
|
||||||
|
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -32,30 +37,29 @@ object Migrations {
|
|||||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||||
val context = preferences.context
|
val context = preferences.context
|
||||||
|
|
||||||
// Cancel app updater job for debug builds that don't include it
|
|
||||||
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
|
||||||
UpdaterJob.cancelTask(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
val oldVersion = preferences.lastVersionCode().get()
|
val oldVersion = preferences.lastVersionCode().get()
|
||||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
|
// Always set up background tasks to ensure they're running
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
|
AppUpdateJob.setupTask(context)
|
||||||
|
}
|
||||||
|
ExtensionUpdateJob.setupTask(context)
|
||||||
|
LibraryUpdateJob.setupTask(context)
|
||||||
|
BackupCreatorJob.setupTask(context)
|
||||||
|
|
||||||
// Fresh install
|
// Fresh install
|
||||||
if (oldVersion == 0) {
|
if (oldVersion == 0) {
|
||||||
// Set up default background tasks
|
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
|
||||||
UpdaterJob.setupTask(context)
|
|
||||||
}
|
|
||||||
ExtensionUpdateJob.setupTask(context)
|
|
||||||
LibraryUpdateJob.setupTask(context)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
if (oldVersion < 14) {
|
if (oldVersion < 14) {
|
||||||
// Restore jobs after upgrading to Evernote's job scheduler.
|
// Restore jobs after upgrading to Evernote's job scheduler.
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
UpdaterJob.setupTask(context)
|
AppUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
LibraryUpdateJob.setupTask(context)
|
LibraryUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
@ -88,7 +92,7 @@ object Migrations {
|
|||||||
if (oldVersion < 43) {
|
if (oldVersion < 43) {
|
||||||
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
|
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
UpdaterJob.setupTask(context)
|
AppUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
LibraryUpdateJob.setupTask(context)
|
LibraryUpdateJob.setupTask(context)
|
||||||
BackupCreatorJob.setupTask(context)
|
BackupCreatorJob.setupTask(context)
|
||||||
@ -98,8 +102,6 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 44) {
|
if (oldVersion < 44) {
|
||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@ -111,7 +113,6 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 52) {
|
if (oldVersion < 52) {
|
||||||
// Migrate library filters to tri-state versions
|
// Migrate library filters to tri-state versions
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
fun convertBooleanPrefToTriState(key: String): Int {
|
fun convertBooleanPrefToTriState(key: String): Int {
|
||||||
val oldPrefValue = prefs.getBoolean(key, false)
|
val oldPrefValue = prefs.getBoolean(key, false)
|
||||||
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE.value
|
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE.value
|
||||||
@ -140,7 +141,6 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 57) {
|
if (oldVersion < 57) {
|
||||||
// Migrate DNS over HTTPS setting
|
// Migrate DNS over HTTPS setting
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
||||||
if (wasDohEnabled) {
|
if (wasDohEnabled) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
@ -151,7 +151,6 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 59) {
|
if (oldVersion < 59) {
|
||||||
// Reset rotation to Free after replacing Lock
|
// Reset rotation to Free after replacing Lock
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
if (prefs.contains("pref_rotation_type_key")) {
|
if (prefs.contains("pref_rotation_type_key")) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt("pref_rotation_type_key", 1)
|
putInt("pref_rotation_type_key", 1)
|
||||||
@ -160,17 +159,16 @@ object Migrations {
|
|||||||
|
|
||||||
// Disable update check for Android 5.x users
|
// Disable update check for Android 5.x users
|
||||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||||
UpdaterJob.cancelTask(context)
|
AppUpdateJob.cancelTask(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 60) {
|
if (oldVersion < 60) {
|
||||||
// Re-enable update check that was prevously accidentally disabled for M
|
// Re-enable update check that was prevously accidentally disabled for M
|
||||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
|
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
|
||||||
UpdaterJob.setupTask(context)
|
AppUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate Rotation and Viewer values to default values for viewer_flags
|
// Migrate Rotation and Viewer values to default values for viewer_flags
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
||||||
1 -> OrientationType.FREE.flagValue
|
1 -> OrientationType.FREE.flagValue
|
||||||
2 -> OrientationType.PORTRAIT.flagValue
|
2 -> OrientationType.PORTRAIT.flagValue
|
||||||
@ -199,8 +197,6 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 64) {
|
if (oldVersion < 64) {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
||||||
|
|
||||||
@ -232,11 +228,45 @@ object Migrations {
|
|||||||
putString(PreferenceKeys.librarySortingDirection, newSortingDirection.name)
|
putString(PreferenceKeys.librarySortingDirection, newSortingDirection.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 65) {
|
if (oldVersion < 70) {
|
||||||
if (preferences.lang().get() in listOf("en-US", "en-GB")) {
|
if (preferences.enabledLanguages().isSet()) {
|
||||||
preferences.lang().set("en")
|
preferences.enabledLanguages() += "all"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 71) {
|
||||||
|
// Handle removed every 3, 4, 6, and 8 hour library updates
|
||||||
|
val updateInterval = preferences.libraryUpdateInterval().get()
|
||||||
|
if (updateInterval in listOf(3, 4, 6, 8)) {
|
||||||
|
preferences.libraryUpdateInterval().set(12)
|
||||||
|
LibraryUpdateJob.setupTask(context, 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 72) {
|
||||||
|
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
|
||||||
|
if (!oldUpdateOngoingOnly) {
|
||||||
|
preferences.libraryUpdateMangaRestriction() -= MANGA_NON_COMPLETED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 75) {
|
||||||
|
val oldSecureScreen = prefs.getBoolean("secure_screen", false)
|
||||||
|
if (oldSecureScreen) {
|
||||||
|
preferences.secureScreen().set(PreferenceValues.SecureScreenMode.ALWAYS)
|
||||||
|
}
|
||||||
|
if (DeviceUtil.isMiui && preferences.extensionInstaller().get() == PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER) {
|
||||||
|
preferences.extensionInstaller().set(PreferenceValues.ExtensionInstaller.LEGACY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 76) {
|
||||||
|
BackupCreatorJob.setupTask(context)
|
||||||
|
}
|
||||||
|
if (oldVersion < 77) {
|
||||||
|
val oldReaderTap = prefs.getBoolean("reader_tap", false)
|
||||||
|
if (!oldReaderTap) {
|
||||||
|
preferences.navigationModePager().set(5)
|
||||||
|
preferences.navigationModeWebtoon().set(5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.annotations
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
annotation class Nsfw
|
|
@ -21,7 +21,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
|||||||
internal val trackManager: TrackManager by injectLazy()
|
internal val trackManager: TrackManager by injectLazy()
|
||||||
protected val preferences: PreferencesHelper by injectLazy()
|
protected val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
|
abstract fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns manga
|
* Returns manga
|
||||||
|
@ -112,7 +112,7 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
|
|||||||
internal fun showRestoreProgress(
|
internal fun showRestoreProgress(
|
||||||
progress: Int,
|
progress: Int,
|
||||||
amount: Int,
|
amount: Int,
|
||||||
title: String
|
title: String,
|
||||||
) {
|
) {
|
||||||
notifier.showRestoreProgress(title, progress, amount)
|
notifier.showRestoreProgress(title, progress, amount)
|
||||||
}
|
}
|
||||||
|
@ -14,3 +14,5 @@ abstract class AbstractBackupRestoreValidator {
|
|||||||
|
|
||||||
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ValidatorParseException(e: Exception) : RuntimeException(e)
|
||||||
|
@ -11,4 +11,15 @@ object BackupConst {
|
|||||||
|
|
||||||
const val BACKUP_TYPE_LEGACY = 0
|
const val BACKUP_TYPE_LEGACY = 0
|
||||||
const val BACKUP_TYPE_FULL = 1
|
const val BACKUP_TYPE_FULL = 1
|
||||||
|
|
||||||
|
// Filter options
|
||||||
|
internal const val BACKUP_CATEGORY = 0x1
|
||||||
|
internal const val BACKUP_CATEGORY_MASK = 0x1
|
||||||
|
internal const val BACKUP_CHAPTER = 0x2
|
||||||
|
internal const val BACKUP_CHAPTER_MASK = 0x2
|
||||||
|
internal const val BACKUP_HISTORY = 0x4
|
||||||
|
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||||
|
internal const val BACKUP_TRACK = 0x8
|
||||||
|
internal const val BACKUP_TRACK_MASK = 0x8
|
||||||
|
internal const val BACKUP_ALL = 0xF
|
||||||
}
|
}
|
||||||
|
@ -1,114 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.os.PowerManager
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for backing up library information to a JSON file.
|
|
||||||
*/
|
|
||||||
class BackupCreateService : Service() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// Filter options
|
|
||||||
internal const val BACKUP_CATEGORY = 0x1
|
|
||||||
internal const val BACKUP_CATEGORY_MASK = 0x1
|
|
||||||
internal const val BACKUP_CHAPTER = 0x2
|
|
||||||
internal const val BACKUP_CHAPTER_MASK = 0x2
|
|
||||||
internal const val BACKUP_HISTORY = 0x4
|
|
||||||
internal const val BACKUP_HISTORY_MASK = 0x4
|
|
||||||
internal const val BACKUP_TRACK = 0x8
|
|
||||||
internal const val BACKUP_TRACK_MASK = 0x8
|
|
||||||
internal const val BACKUP_ALL = 0xF
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 =
|
|
||||||
context.isServiceRunning(BackupCreateService::class.java)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a backup from library
|
|
||||||
*
|
|
||||||
* @param context context of application
|
|
||||||
* @param uri path of Uri
|
|
||||||
* @param flags determines what to backup
|
|
||||||
*/
|
|
||||||
fun start(context: Context, uri: Uri, flags: Int) {
|
|
||||||
if (!isRunning(context)) {
|
|
||||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
|
||||||
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
|
||||||
}
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wake lock that will be held until the service is destroyed.
|
|
||||||
*/
|
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
|
||||||
|
|
||||||
private lateinit var notifier: BackupNotifier
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
notifier = BackupNotifier(this)
|
|
||||||
wakeLock = acquireWakeLock(javaClass.name)
|
|
||||||
|
|
||||||
startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stopService(name: Intent?): Boolean {
|
|
||||||
destroyJob()
|
|
||||||
return super.stopService(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
destroyJob()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun destroyJob() {
|
|
||||||
if (wakeLock.isHeld) {
|
|
||||||
wakeLock.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method needs to be implemented, but it's not used/needed.
|
|
||||||
*/
|
|
||||||
override fun onBind(intent: Intent): IBinder? = null
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
if (intent == null) return START_NOT_STICKY
|
|
||||||
|
|
||||||
try {
|
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)!!
|
|
||||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
|
||||||
val backupFileUri = FullBackupManager(this).createBackup(uri, backupFlags, false)?.toUri()
|
|
||||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
|
||||||
notifier.showBackupComplete(unifile)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
notifier.showBackupError(e.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
stopSelf(startId)
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +1,24 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import androidx.work.workDataOf
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -18,36 +28,71 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val uri = preferences.backupsDirectory().get().toUri()
|
val notifier = BackupNotifier(context)
|
||||||
val flags = BackupCreateService.BACKUP_ALL
|
val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) }
|
||||||
|
?: preferences.backupsDirectory().get().toUri()
|
||||||
|
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
|
||||||
|
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
|
||||||
|
|
||||||
|
context.notificationManager.notify(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
|
||||||
return try {
|
return try {
|
||||||
FullBackupManager(context).createBackup(uri, flags, true)
|
val location = FullBackupManager(context).createBackup(uri, flags, isAutoBackup)
|
||||||
|
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
if (!isAutoBackup) notifier.showBackupError(e.message)
|
||||||
Result.failure()
|
Result.failure()
|
||||||
|
} finally {
|
||||||
|
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "BackupCreator"
|
fun isManualJobRunning(context: Context): Boolean {
|
||||||
|
val list = WorkManager.getInstance(context).getWorkInfosByTag(TAG_MANUAL).get()
|
||||||
|
return list.find { it.state == WorkInfo.State.RUNNING } != null
|
||||||
|
}
|
||||||
|
|
||||||
fun setupTask(context: Context, prefInterval: Int? = null) {
|
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val interval = prefInterval ?: preferences.backupInterval().get()
|
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||||
|
val workManager = WorkManager.getInstance(context)
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||||
interval.toLong(),
|
interval.toLong(),
|
||||||
TimeUnit.HOURS,
|
TimeUnit.HOURS,
|
||||||
10,
|
10,
|
||||||
TimeUnit.MINUTES
|
TimeUnit.MINUTES,
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG_AUTO)
|
||||||
|
.setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
|
workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||||
} else {
|
} else {
|
||||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
workManager.cancelUniqueWork(TAG_AUTO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startNow(context: Context, uri: Uri, flags: Int) {
|
||||||
|
val inputData = workDataOf(
|
||||||
|
IS_AUTO_BACKUP_KEY to false,
|
||||||
|
LOCATION_URI_KEY to uri.toString(),
|
||||||
|
BACKUP_FLAGS_KEY to flags,
|
||||||
|
)
|
||||||
|
val request = OneTimeWorkRequestBuilder<BackupCreatorJob>()
|
||||||
|
.addTag(TAG_MANUAL)
|
||||||
|
.setInputData(inputData)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context).enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val TAG_AUTO = "BackupCreator"
|
||||||
|
private const val TAG_MANUAL = "$TAG_AUTO:manual"
|
||||||
|
|
||||||
|
private const val IS_AUTO_BACKUP_KEY = "is_auto_backup" // Boolean
|
||||||
|
private const val LOCATION_URI_KEY = "location_uri" // String
|
||||||
|
private const val BACKUP_FLAGS_KEY = "backup_flags" // Int
|
||||||
|
@ -73,7 +73,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_share_24dp,
|
R.drawable.ic_share_24dp,
|
||||||
context.getString(R.string.action_share),
|
context.getString(R.string.action_share),
|
||||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE),
|
||||||
)
|
)
|
||||||
|
|
||||||
show(Notifications.ID_BACKUP_COMPLETE)
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
@ -97,7 +97,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_close_24dp,
|
R.drawable.ic_close_24dp,
|
||||||
context.getString(R.string.action_stop),
|
context.getString(R.string.action_stop),
|
||||||
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS)
|
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,8 +124,8 @@ class BackupNotifier(private val context: Context) {
|
|||||||
R.string.restore_duration,
|
R.string.restore_duration,
|
||||||
TimeUnit.MILLISECONDS.toMinutes(time),
|
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||||
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
|
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
|
||||||
TimeUnit.MILLISECONDS.toMinutes(time)
|
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
@ -139,10 +139,12 @@ class BackupNotifier(private val context: Context) {
|
|||||||
val destFile = File(path, file)
|
val destFile = File(path, file)
|
||||||
val uri = destFile.getUriCompat(context)
|
val uri = destFile.getUriCompat(context)
|
||||||
|
|
||||||
|
val errorLogIntent = NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||||
|
setContentIntent(errorLogIntent)
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_folder_24dp,
|
R.drawable.ic_folder_24dp,
|
||||||
context.getString(R.string.action_show_errors),
|
context.getString(R.string.action_show_errors),
|
||||||
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
errorLogIntent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,13 +13,14 @@ import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestore
|
|||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import logcat.LogPriority
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores backup.
|
* Restores backup.
|
||||||
@ -128,7 +129,7 @@ class BackupRestoreService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
logcat(LogPriority.ERROR, exception)
|
||||||
backupRestore?.writeErrorLog()
|
backupRestore?.writeErrorLog()
|
||||||
|
|
||||||
notifier.showRestoreError(exception.message)
|
notifier.showRestoreError(exception.message)
|
||||||
|
@ -4,14 +4,14 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
|
||||||
@ -26,11 +26,13 @@ import eu.kanade.tachiyomi.data.database.models.History
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import logcat.LogPriority
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import timber.log.Timber
|
import java.io.FileOutputStream
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
@ -41,9 +43,9 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
* Create backup Json file from database
|
* Create backup Json file from database
|
||||||
*
|
*
|
||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isAutoBackup backup called from scheduled backup job
|
||||||
*/
|
*/
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
override fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
||||||
// Create root object
|
// Create root object
|
||||||
var backup: Backup? = null
|
var backup: Backup? = null
|
||||||
|
|
||||||
@ -53,13 +55,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
backup = Backup(
|
backup = Backup(
|
||||||
backupManga(databaseManga, flags),
|
backupManga(databaseManga, flags),
|
||||||
backupCategories(),
|
backupCategories(),
|
||||||
backupExtensionInfo(databaseManga)
|
emptyList(),
|
||||||
|
backupExtensionInfo(databaseManga),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var file: UniFile? = null
|
||||||
try {
|
try {
|
||||||
val file: UniFile = (
|
file = (
|
||||||
if (isJob) {
|
if (isAutoBackup) {
|
||||||
// Get dir of file and create
|
// Get dir of file and create
|
||||||
var dir = UniFile.fromUri(context, uri)
|
var dir = UniFile.fromUri(context, uri)
|
||||||
dir = dir.createDirectory("automatic")
|
dir = dir.createDirectory("automatic")
|
||||||
@ -81,11 +85,24 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
)
|
)
|
||||||
?: throw Exception("Couldn't create backup file")
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
|
||||||
|
if (!file.isFile) {
|
||||||
|
throw IllegalStateException("Failed to get handle on file")
|
||||||
|
}
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||||
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
file.openOutputStream().also {
|
||||||
return file.uri.toString()
|
// Force overwrite old file
|
||||||
|
(it as? FileOutputStream)?.channel?.truncate(0)
|
||||||
|
}.sink().gzip().buffer().use { it.write(byteArray) }
|
||||||
|
val fileUri = file.uri
|
||||||
|
|
||||||
|
// Make sure it's a valid backup file
|
||||||
|
FullBackupRestoreValidator().validate(context, fileUri)
|
||||||
|
|
||||||
|
return fileUri.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
logcat(LogPriority.ERROR, e)
|
||||||
|
file?.delete()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
|||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@ -33,7 +34,8 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
var backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
||||||
|
sourceMapping = backupMaps.map { it.sourceId to it.name }.toMap()
|
||||||
|
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
backup.backupManga.forEach {
|
backup.backupManga.forEach {
|
||||||
@ -62,7 +64,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
val manga = backupManga.getMangaImpl()
|
val manga = backupManga.getMangaImpl()
|
||||||
val chapters = backupManga.getChaptersImpl()
|
val chapters = backupManga.getChaptersImpl()
|
||||||
val categories = backupManga.categories
|
val categories = backupManga.categories
|
||||||
val history = backupManga.history
|
val history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead) } + backupManga.history
|
||||||
val tracks = backupManga.getTrackingImpl()
|
val tracks = backupManga.getTrackingImpl()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -91,7 +93,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>
|
backupCategories: List<BackupCategory>,
|
||||||
) {
|
) {
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
@ -121,7 +123,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>
|
backupCategories: List<BackupCategory>,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val fetchedManga = backupManager.restoreManga(manga)
|
val fetchedManga = backupManager.restoreManga(manga)
|
||||||
@ -141,7 +143,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>
|
backupCategories: List<BackupCategory>,
|
||||||
) {
|
) {
|
||||||
backupManager.restoreChaptersForManga(backupManga, chapters)
|
backupManager.restoreChaptersForManga(backupManga, chapters)
|
||||||
|
|
||||||
|
@ -4,12 +4,14 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.ValidatorParseException
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.source
|
import okio.source
|
||||||
|
|
||||||
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
@ -19,14 +21,20 @@ class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
override fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val backupManager = FullBackupManager(context)
|
val backupManager = FullBackupManager(context)
|
||||||
|
|
||||||
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
val backup = try {
|
||||||
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
val backupString =
|
||||||
|
context.contentResolver.openInputStream(uri)!!.source().gzip().buffer()
|
||||||
|
.use { it.readByteArray() }
|
||||||
|
backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw ValidatorParseException(e)
|
||||||
|
}
|
||||||
|
|
||||||
if (backup.backupManga.isEmpty()) {
|
if (backup.backupManga.isEmpty()) {
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
val sources = backup.backupSources.associate { it.sourceId to it.name }
|
||||||
val missingSources = sources
|
val missingSources = sources
|
||||||
.filter { sourceManager.get(it.key) == null }
|
.filter { sourceManager.get(it.key) == null }
|
||||||
.values
|
.values
|
||||||
|
@ -8,5 +8,6 @@ data class Backup(
|
|||||||
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
||||||
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||||
// Bump by 100 to specify this is a 0.x value
|
// Bump by 100 to specify this is a 0.x value
|
||||||
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
|
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
||||||
|
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||||
)
|
)
|
||||||
|
@ -26,7 +26,7 @@ class BackupCategory(
|
|||||||
return BackupCategory(
|
return BackupCategory(
|
||||||
name = category.name,
|
name = category.name,
|
||||||
order = category.order,
|
order = category.order,
|
||||||
flags = category.flags
|
flags = category.flags,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ data class BackupChapter(
|
|||||||
lastPageRead = chapter.last_page_read,
|
lastPageRead = chapter.last_page_read,
|
||||||
dateFetch = chapter.date_fetch,
|
dateFetch = chapter.date_fetch,
|
||||||
dateUpload = chapter.date_upload,
|
dateUpload = chapter.date_upload,
|
||||||
sourceOrder = chapter.source_order
|
sourceOrder = chapter.source_order,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,13 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BackupHistory(
|
data class BrokenBackupHistory(
|
||||||
@ProtoNumber(0) var url: String,
|
@ProtoNumber(0) var url: String,
|
||||||
@ProtoNumber(1) var lastRead: Long
|
@ProtoNumber(1) var lastRead: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupHistory(
|
||||||
|
@ProtoNumber(1) var url: String,
|
||||||
|
@ProtoNumber(2) var lastRead: Long,
|
||||||
)
|
)
|
||||||
|
@ -33,8 +33,9 @@ data class BackupManga(
|
|||||||
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
||||||
@ProtoNumber(100) var favorite: Boolean = true,
|
@ProtoNumber(100) var favorite: Boolean = true,
|
||||||
@ProtoNumber(101) var chapterFlags: Int = 0,
|
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||||
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(),
|
||||||
@ProtoNumber(103) var viewer_flags: Int? = null
|
@ProtoNumber(103) var viewer_flags: Int? = null,
|
||||||
|
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
|
||||||
) {
|
) {
|
||||||
fun getMangaImpl(): MangaImpl {
|
fun getMangaImpl(): MangaImpl {
|
||||||
return MangaImpl().apply {
|
return MangaImpl().apply {
|
||||||
@ -82,7 +83,7 @@ data class BackupManga(
|
|||||||
dateAdded = manga.date_added,
|
dateAdded = manga.date_added,
|
||||||
viewer = manga.readingModeType,
|
viewer = manga.readingModeType,
|
||||||
viewer_flags = manga.viewer_flags,
|
viewer_flags = manga.viewer_flags,
|
||||||
chapterFlags = manga.chapter_flags
|
chapterFlags = manga.chapter_flags,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,15 +5,21 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BackupSource(
|
data class BrokenBackupSource(
|
||||||
@ProtoNumber(0) var name: String = "",
|
@ProtoNumber(0) var name: String = "",
|
||||||
@ProtoNumber(1) var sourceId: Long
|
@ProtoNumber(1) var sourceId: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupSource(
|
||||||
|
@ProtoNumber(1) var name: String = "",
|
||||||
|
@ProtoNumber(2) var sourceId: Long,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun copyFrom(source: Source): BackupSource {
|
fun copyFrom(source: Source): BackupSource {
|
||||||
return BackupSource(
|
return BackupSource(
|
||||||
name = source.name,
|
name = source.name,
|
||||||
sourceId = source.id
|
sourceId = source.id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,8 +32,7 @@ data class BackupTracking(
|
|||||||
media_id = this@BackupTracking.mediaId
|
media_id = this@BackupTracking.mediaId
|
||||||
library_id = this@BackupTracking.libraryId
|
library_id = this@BackupTracking.libraryId
|
||||||
title = this@BackupTracking.title
|
title = this@BackupTracking.title
|
||||||
// convert from float to int because of 1.x types
|
last_chapter_read = this@BackupTracking.lastChapterRead
|
||||||
last_chapter_read = this@BackupTracking.lastChapterRead.toInt()
|
|
||||||
total_chapters = this@BackupTracking.totalChapters
|
total_chapters = this@BackupTracking.totalChapters
|
||||||
score = this@BackupTracking.score
|
score = this@BackupTracking.score
|
||||||
status = this@BackupTracking.status
|
status = this@BackupTracking.status
|
||||||
@ -51,14 +50,13 @@ data class BackupTracking(
|
|||||||
// forced not null so its compatible with 1.x backup system
|
// forced not null so its compatible with 1.x backup system
|
||||||
libraryId = track.library_id!!,
|
libraryId = track.library_id!!,
|
||||||
title = track.title,
|
title = track.title,
|
||||||
// convert to float for 1.x
|
lastChapterRead = track.last_chapter_read,
|
||||||
lastChapterRead = track.last_chapter_read.toFloat(),
|
|
||||||
totalChapters = track.total_chapters,
|
totalChapters = track.total_chapters,
|
||||||
score = track.score,
|
score = track.score,
|
||||||
status = track.status,
|
status = track.status,
|
||||||
startedReadingDate = track.started_reading_date,
|
startedReadingDate = track.started_reading_date,
|
||||||
finishedReadingDate = track.finished_reading_date,
|
finishedReadingDate = track.finished_reading_date,
|
||||||
trackingUrl = track.tracking_url
|
trackingUrl = track.tracking_url,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,9 +55,9 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
* Create backup Json file from database
|
* Create backup Json file from database
|
||||||
*
|
*
|
||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isAutoBackup backup called from scheduled backup job
|
||||||
*/
|
*/
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean) =
|
override fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean) =
|
||||||
throw IllegalStateException("Legacy backup creation is not supported")
|
throw IllegalStateException("Legacy backup creation is not supported")
|
||||||
|
|
||||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||||
|
@ -13,13 +13,12 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import kotlinx.serialization.json.intOrNull
|
import kotlinx.serialization.json.intOrNull
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okio.buffer
|
|
||||||
import okio.source
|
import okio.source
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@ -28,8 +27,8 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
override suspend fun performRestore(uri: Uri): Boolean {
|
override suspend fun performRestore(uri: Uri): Boolean {
|
||||||
// Read the json and create a Json Object,
|
// Read the json and create a Json Object,
|
||||||
// cannot use the backupManager json deserializer one because its not initialized yet
|
// cannot use the backupManager json deserializer one because its not initialized yet
|
||||||
val backupObject = Json.decodeFromString<JsonObject>(
|
val backupObject = Json.decodeFromStream<JsonObject>(
|
||||||
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
context.contentResolver.openInputStream(uri)!!,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get parser version
|
// Get parser version
|
||||||
@ -110,7 +109,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<String>,
|
categories: List<String>,
|
||||||
history: List<DHistory>,
|
history: List<DHistory>,
|
||||||
tracks: List<Track>
|
tracks: List<Track>,
|
||||||
) {
|
) {
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
@ -140,7 +139,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<String>,
|
categories: List<String>,
|
||||||
history: List<DHistory>,
|
history: List<DHistory>,
|
||||||
tracks: List<Track>
|
tracks: List<Track>,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val fetchedManga = backupManager.fetchManga(source, manga)
|
val fetchedManga = backupManager.fetchManga(source, manga)
|
||||||
@ -162,7 +161,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<String>,
|
categories: List<String>,
|
||||||
history: List<DHistory>,
|
history: List<DHistory>,
|
||||||
tracks: List<Track>
|
tracks: List<Track>,
|
||||||
) {
|
) {
|
||||||
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
||||||
updateChapters(source, backupManga, chapters)
|
updateChapters(source, backupManga, chapters)
|
||||||
|
@ -4,12 +4,12 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.ValidatorParseException
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import okio.buffer
|
|
||||||
import okio.source
|
|
||||||
|
|
||||||
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
@ -19,9 +19,13 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
override fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val backupManager = LegacyBackupManager(context)
|
val backupManager = LegacyBackupManager(context)
|
||||||
|
|
||||||
val backup = backupManager.parser.decodeFromString<Backup>(
|
val backup = try {
|
||||||
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
backupManager.parser.decodeFromStream<Backup>(
|
||||||
)
|
context.contentResolver.openInputStream(uri)!!,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw ValidatorParseException(e)
|
||||||
|
}
|
||||||
|
|
||||||
if (backup.version == null) {
|
if (backup.version == null) {
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||||
@ -53,12 +57,10 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
|
fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
|
||||||
return extensionsMapping
|
return extensionsMapping.associate {
|
||||||
.map {
|
val items = it.split(":")
|
||||||
val items = it.split(":")
|
items[0].toLong() to items[1]
|
||||||
items[0].toLong() to items[1]
|
}
|
||||||
}
|
|
||||||
.toMap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ data class Backup(
|
|||||||
val version: Int? = null,
|
val version: Int? = null,
|
||||||
var mangas: MutableList<MangaObject> = mutableListOf(),
|
var mangas: MutableList<MangaObject> = mutableListOf(),
|
||||||
var categories: List<@Contextual Category>? = null,
|
var categories: List<@Contextual Category>? = null,
|
||||||
var extensions: List<String>? = null
|
var extensions: List<String>? = null,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val CURRENT_VERSION = 2
|
const val CURRENT_VERSION = 2
|
||||||
@ -33,5 +33,5 @@ data class MangaObject(
|
|||||||
var chapters: List<@Contextual Chapter>? = null,
|
var chapters: List<@Contextual Chapter>? = null,
|
||||||
var categories: List<String>? = null,
|
var categories: List<String>? = null,
|
||||||
var track: List<@Contextual Track>? = null,
|
var track: List<@Contextual Track>? = null,
|
||||||
var history: List<@Contextual DHistory>? = null
|
var history: List<@Contextual DHistory>? = null,
|
||||||
)
|
)
|
||||||
|
@ -27,7 +27,7 @@ open class CategoryBaseSerializer<T : Category> : KSerializer<T> {
|
|||||||
buildJsonArray {
|
buildJsonArray {
|
||||||
add(value.name)
|
add(value.name)
|
||||||
add(value.order)
|
add(value.order)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ open class ChapterBaseSerializer<T : Chapter> : KSerializer<T> {
|
|||||||
if (value.last_page_read != 0) {
|
if (value.last_page_read != 0) {
|
||||||
put(LAST_READ, value.last_page_read)
|
put(LAST_READ, value.last_page_read)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ object HistoryTypeSerializer : KSerializer<DHistory> {
|
|||||||
buildJsonArray {
|
buildJsonArray {
|
||||||
add(value.url)
|
add(value.url)
|
||||||
add(value.lastRead)
|
add(value.lastRead)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ object HistoryTypeSerializer : KSerializer<DHistory> {
|
|||||||
val array = decoder.decodeJsonElement().jsonArray
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
return DHistory(
|
return DHistory(
|
||||||
url = array[0].jsonPrimitive.content,
|
url = array[0].jsonPrimitive.content,
|
||||||
lastRead = array[1].jsonPrimitive.long
|
lastRead = array[1].jsonPrimitive.long,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ open class MangaBaseSerializer<T : Manga> : KSerializer<T> {
|
|||||||
add(value.source)
|
add(value.source)
|
||||||
add(value.viewer_flags)
|
add(value.viewer_flags)
|
||||||
add(value.chapter_flags)
|
add(value.chapter_flags)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import kotlinx.serialization.encoding.Encoder
|
|||||||
import kotlinx.serialization.json.JsonDecoder
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
import kotlinx.serialization.json.JsonEncoder
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.float
|
||||||
import kotlinx.serialization.json.int
|
import kotlinx.serialization.json.int
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
@ -32,7 +33,7 @@ open class TrackBaseSerializer<T : Track> : KSerializer<T> {
|
|||||||
put(LIBRARY, value.library_id)
|
put(LIBRARY, value.library_id)
|
||||||
put(LAST_READ, value.last_chapter_read)
|
put(LAST_READ, value.last_chapter_read)
|
||||||
put(TRACKING_URL, value.tracking_url)
|
put(TRACKING_URL, value.tracking_url)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +47,7 @@ open class TrackBaseSerializer<T : Track> : KSerializer<T> {
|
|||||||
sync_id = jsonObject[SYNC]!!.jsonPrimitive.int
|
sync_id = jsonObject[SYNC]!!.jsonPrimitive.int
|
||||||
media_id = jsonObject[MEDIA]!!.jsonPrimitive.int
|
media_id = jsonObject[MEDIA]!!.jsonPrimitive.int
|
||||||
library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long
|
library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long
|
||||||
last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.int
|
last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.float
|
||||||
tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content
|
tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content
|
||||||
} as T
|
} as T
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||||
PARAMETER_APP_VERSION,
|
PARAMETER_APP_VERSION,
|
||||||
PARAMETER_VALUE_COUNT,
|
PARAMETER_VALUE_COUNT,
|
||||||
PARAMETER_CACHE_SIZE
|
PARAMETER_CACHE_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -104,7 +104,7 @@ class CoverCache(private val context: Context) {
|
|||||||
* Clear coil's memory cache.
|
* Clear coil's memory cache.
|
||||||
*/
|
*/
|
||||||
fun clearMemoryCache() {
|
fun clearMemoryCache() {
|
||||||
context.imageLoader.memoryCache.clear()
|
context.imageLoader.memoryCache?.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCacheDir(dir: String): File {
|
private fun getCacheDir(dir: String): File {
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
|
||||||
|
|
||||||
import coil.bitmap.BitmapPool
|
|
||||||
import coil.decode.DataSource
|
|
||||||
import coil.decode.Options
|
|
||||||
import coil.fetch.FetchResult
|
|
||||||
import coil.fetch.Fetcher
|
|
||||||
import coil.fetch.SourceResult
|
|
||||||
import coil.size.Size
|
|
||||||
import okio.buffer
|
|
||||||
import okio.source
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
|
|
||||||
class ByteBufferFetcher : Fetcher<ByteBuffer> {
|
|
||||||
override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
|
|
||||||
return SourceResult(
|
|
||||||
source = ByteArrayInputStream(data.array()).source().buffer(),
|
|
||||||
mimeType = null,
|
|
||||||
dataSource = DataSource.MEMORY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun key(data: ByteBuffer): String? = null
|
|
||||||
}
|
|
@ -1,148 +1,261 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
import coil.bitmap.BitmapPool
|
import coil.ImageLoader
|
||||||
import coil.decode.DataSource
|
import coil.decode.DataSource
|
||||||
import coil.decode.Options
|
import coil.decode.ImageSource
|
||||||
|
import coil.disk.DiskCache
|
||||||
import coil.fetch.FetchResult
|
import coil.fetch.FetchResult
|
||||||
import coil.fetch.Fetcher
|
import coil.fetch.Fetcher
|
||||||
import coil.fetch.SourceResult
|
import coil.fetch.SourceResult
|
||||||
import coil.network.HttpException
|
import coil.network.HttpException
|
||||||
import coil.request.get
|
import coil.request.Options
|
||||||
import coil.size.Size
|
import coil.request.Parameters
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.internal.closeQuietly
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
|
import okio.Source
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import okio.source
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
* A [Fetcher] that fetches cover image for [Manga] object.
|
||||||
|
*
|
||||||
|
* It uses [Manga.thumbnail_url] if custom cover is not set by the user.
|
||||||
|
* Disk caching for library items is handled by [CoverCache], otherwise
|
||||||
|
* handled by Coil's [DiskCache].
|
||||||
*
|
*
|
||||||
* Available request parameter:
|
* Available request parameter:
|
||||||
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
||||||
*/
|
*/
|
||||||
class MangaCoverFetcher : Fetcher<Manga> {
|
class MangaCoverFetcher(
|
||||||
private val coverCache: CoverCache by injectLazy()
|
private val manga: Manga,
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceLazy: Lazy<HttpSource?>,
|
||||||
private val defaultClient = Injekt.get<NetworkHelper>().coilClient
|
private val options: Options,
|
||||||
|
private val coverCache: CoverCache,
|
||||||
|
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||||
|
private val diskCacheLazy: Lazy<DiskCache>,
|
||||||
|
) : Fetcher {
|
||||||
|
|
||||||
override fun key(data: Manga): String? {
|
// For non-custom cover
|
||||||
if (data.thumbnail_url.isNullOrBlank()) return null
|
private val diskCacheKey: String? by lazy { MangaCoverKeyer().key(manga, options) }
|
||||||
return data.thumbnail_url!!
|
private lateinit var url: String
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
|
override suspend fun fetch(): FetchResult {
|
||||||
// Use custom cover if exists
|
// Use custom cover if exists
|
||||||
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
|
val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true
|
||||||
val customCoverFile = coverCache.getCustomCoverFile(data)
|
val customCoverFile = coverCache.getCustomCoverFile(manga)
|
||||||
if (useCustomCover && customCoverFile.exists()) {
|
if (useCustomCover && customCoverFile.exists()) {
|
||||||
return fileLoader(customCoverFile)
|
return fileLoader(customCoverFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cover = data.thumbnail_url
|
// diskCacheKey is thumbnail_url
|
||||||
return when (getResourceType(cover)) {
|
url = diskCacheKey ?: error("No cover specified")
|
||||||
Type.URL -> httpLoader(data, options)
|
return when (getResourceType(url)) {
|
||||||
Type.File -> fileLoader(data)
|
Type.URL -> httpLoader()
|
||||||
|
Type.File -> fileLoader(File(url.substringAfter("file://")))
|
||||||
null -> error("Invalid image")
|
null -> error("Invalid image")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
private fun fileLoader(file: File): FetchResult {
|
||||||
|
return SourceResult(
|
||||||
|
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = DataSource.DISK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun httpLoader(): FetchResult {
|
||||||
// Only cache separately if it's a library item
|
// Only cache separately if it's a library item
|
||||||
val coverCacheFile = if (manga.favorite) {
|
val libraryCoverCacheFile = if (manga.favorite) {
|
||||||
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
if (libraryCoverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
||||||
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
return fileLoader(libraryCoverCacheFile)
|
||||||
return fileLoader(coverCacheFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val (response, body) = awaitGetCall(manga, options)
|
var snapshot = readFromDiskCache()
|
||||||
if (!response.isSuccessful) {
|
try {
|
||||||
body.close()
|
// Fetch from disk cache
|
||||||
|
if (snapshot != null) {
|
||||||
|
val snapshotCoverCache = moveSnapshotToCoverCache(snapshot, libraryCoverCacheFile)
|
||||||
|
if (snapshotCoverCache != null) {
|
||||||
|
// Read from cover cache after added to library
|
||||||
|
return fileLoader(snapshotCoverCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from snapshot
|
||||||
|
return SourceResult(
|
||||||
|
source = snapshot.toImageSource(),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = DataSource.DISK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from network
|
||||||
|
val response = executeNetworkRequest()
|
||||||
|
val responseBody = checkNotNull(response.body) { "Null response source" }
|
||||||
|
try {
|
||||||
|
// Read from cover cache after library manga cover updated
|
||||||
|
val responseCoverCache = writeResponseToCoverCache(response, libraryCoverCacheFile)
|
||||||
|
if (responseCoverCache != null) {
|
||||||
|
return fileLoader(responseCoverCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from disk cache
|
||||||
|
snapshot = writeToDiskCache(snapshot, response)
|
||||||
|
if (snapshot != null) {
|
||||||
|
return SourceResult(
|
||||||
|
source = snapshot.toImageSource(),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = DataSource.NETWORK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from response if cache is unused or unusable
|
||||||
|
return SourceResult(
|
||||||
|
source = ImageSource(source = responseBody.source(), context = options.context),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
responseBody.closeQuietly()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
snapshot?.closeQuietly()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun executeNetworkRequest(): Response {
|
||||||
|
val client = sourceLazy.value?.client ?: callFactoryLazy.value
|
||||||
|
val response = client.newCall(newRequest()).await()
|
||||||
|
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||||
|
response.body?.closeQuietly()
|
||||||
throw HttpException(response)
|
throw HttpException(response)
|
||||||
}
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
|
private fun newRequest(): Request {
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
val request = Request.Builder()
|
||||||
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
.url(url)
|
||||||
coverCacheFile.parentFile?.mkdirs()
|
.headers(sourceLazy.value?.headers ?: options.headers)
|
||||||
if (coverCacheFile.exists()) {
|
// Support attaching custom data to the network request.
|
||||||
coverCacheFile.delete()
|
.tag(Parameters::class.java, options.parameters)
|
||||||
}
|
|
||||||
coverCacheFile.sink().buffer().use { output ->
|
val diskRead = options.diskCachePolicy.readEnabled
|
||||||
output.writeAll(input)
|
val networkRead = options.networkCachePolicy.readEnabled
|
||||||
}
|
when {
|
||||||
|
!networkRead && diskRead -> {
|
||||||
|
request.cacheControl(CacheControl.FORCE_CACHE)
|
||||||
|
}
|
||||||
|
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
||||||
|
request.cacheControl(CacheControl.FORCE_NETWORK)
|
||||||
|
} else {
|
||||||
|
request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
||||||
|
}
|
||||||
|
!networkRead && !diskRead -> {
|
||||||
|
// This causes the request to fail with a 504 Unsatisfiable Request.
|
||||||
|
request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SourceResult(
|
return request.build()
|
||||||
source = body.source(),
|
|
||||||
mimeType = "image/*",
|
|
||||||
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
|
private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? {
|
||||||
val call = getCall(manga, options)
|
if (cacheFile == null) return null
|
||||||
val response = call.await()
|
return try {
|
||||||
return response to checkNotNull(response.body) { "Null response source" }
|
diskCacheLazy.value.run {
|
||||||
}
|
fileSystem.source(snapshot.data).use { input ->
|
||||||
|
writeSourceToCoverCache(input, cacheFile)
|
||||||
private fun getCall(manga: Manga, options: Options): Call {
|
}
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
remove(diskCacheKey!!)
|
||||||
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
|
||||||
if (source != null) {
|
|
||||||
it.headers(source.headers)
|
|
||||||
}
|
}
|
||||||
|
cacheFile.takeIf { it.exists() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to write snapshot data to cover cache ${cacheFile.name}" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val networkRead = options.networkCachePolicy.readEnabled
|
private fun writeResponseToCoverCache(response: Response, cacheFile: File?): File? {
|
||||||
val diskRead = options.diskCachePolicy.readEnabled
|
if (cacheFile == null || !options.diskCachePolicy.writeEnabled) return null
|
||||||
when {
|
return try {
|
||||||
!networkRead && diskRead -> {
|
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||||
it.cacheControl(CacheControl.FORCE_CACHE)
|
writeSourceToCoverCache(input, cacheFile)
|
||||||
}
|
|
||||||
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
|
||||||
it.cacheControl(CacheControl.FORCE_NETWORK)
|
|
||||||
} else {
|
|
||||||
it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
|
||||||
}
|
|
||||||
!networkRead && !diskRead -> {
|
|
||||||
// This causes the request to fail with a 504 Unsatisfiable Request.
|
|
||||||
it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.build()
|
cacheFile.takeIf { it.exists() }
|
||||||
|
} catch (e: Exception) {
|
||||||
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
|
logcat(LogPriority.ERROR, e) { "Failed to write response data to cover cache ${cacheFile.name}" }
|
||||||
return client.newCall(request)
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileLoader(manga: Manga): FetchResult {
|
private fun writeSourceToCoverCache(input: Source, cacheFile: File) {
|
||||||
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
|
cacheFile.parentFile?.mkdirs()
|
||||||
|
cacheFile.delete()
|
||||||
|
try {
|
||||||
|
cacheFile.sink().buffer().use { output ->
|
||||||
|
output.writeAll(input)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
cacheFile.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileLoader(file: File): FetchResult {
|
private fun readFromDiskCache(): DiskCache.Snapshot? {
|
||||||
return SourceResult(
|
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
|
||||||
source = file.source().buffer(),
|
}
|
||||||
mimeType = "image/*",
|
|
||||||
dataSource = DataSource.DISK
|
private fun writeToDiskCache(
|
||||||
)
|
snapshot: DiskCache.Snapshot?,
|
||||||
|
response: Response,
|
||||||
|
): DiskCache.Snapshot? {
|
||||||
|
if (!options.diskCachePolicy.writeEnabled) {
|
||||||
|
snapshot?.closeQuietly()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val editor = if (snapshot != null) {
|
||||||
|
snapshot.closeAndEdit()
|
||||||
|
} else {
|
||||||
|
diskCacheLazy.value.edit(diskCacheKey!!)
|
||||||
|
} ?: return null
|
||||||
|
try {
|
||||||
|
diskCacheLazy.value.fileSystem.write(editor.data) {
|
||||||
|
response.body!!.source().readAll(this)
|
||||||
|
}
|
||||||
|
return editor.commitAndGet()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
try {
|
||||||
|
editor.abort()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
|
||||||
|
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getResourceType(cover: String?): Type? {
|
private fun getResourceType(cover: String?): Type? {
|
||||||
@ -158,6 +271,20 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
File, URL
|
File, URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Factory(
|
||||||
|
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||||
|
private val diskCacheLazy: Lazy<DiskCache>,
|
||||||
|
) : Fetcher.Factory<Manga> {
|
||||||
|
|
||||||
|
private val coverCache: CoverCache by injectLazy()
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||||
|
val source = lazy { sourceManager.get(data.source) as? HttpSource }
|
||||||
|
return MangaCoverFetcher(data, source, options, coverCache, callFactoryLazy, diskCacheLazy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val USE_CUSTOM_COVER = "use_custom_cover"
|
const val USE_CUSTOM_COVER = "use_custom_cover"
|
||||||
|
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import coil.key.Keyer
|
||||||
|
import coil.request.Options
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
|
||||||
|
class MangaCoverKeyer : Keyer<Manga> {
|
||||||
|
override fun key(data: Manga, options: Options): String? {
|
||||||
|
return data.thumbnail_url?.takeIf { it.isNotBlank() }
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import coil.bitmap.BitmapPool
|
import coil.ImageLoader
|
||||||
import coil.decode.DecodeResult
|
import coil.decode.DecodeResult
|
||||||
import coil.decode.Decoder
|
import coil.decode.Decoder
|
||||||
import coil.decode.Options
|
import coil.decode.ImageDecoderDecoder
|
||||||
import coil.size.Size
|
import coil.decode.ImageSource
|
||||||
|
import coil.fetch.SourceResult
|
||||||
|
import coil.request.Options
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import tachiyomi.decoder.ImageDecoder
|
import tachiyomi.decoder.ImageDecoder
|
||||||
@ -15,26 +16,10 @@ import tachiyomi.decoder.ImageDecoder
|
|||||||
/**
|
/**
|
||||||
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
||||||
*/
|
*/
|
||||||
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
|
||||||
|
|
||||||
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
|
override suspend fun decode(): DecodeResult {
|
||||||
val type = source.peek().inputStream().use {
|
val decoder = resources.sourceOrNull()?.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())
|
ImageDecoder.newInstance(it.inputStream())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,8 +31,31 @@ class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
|||||||
check(bitmap != null) { "Failed to decode image." }
|
check(bitmap != null) { "Failed to decode image." }
|
||||||
|
|
||||||
return DecodeResult(
|
return DecodeResult(
|
||||||
drawable = bitmap.toDrawable(resources),
|
drawable = bitmap.toDrawable(options.context.resources),
|
||||||
isSampled = false
|
isSampled = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Factory : Decoder.Factory {
|
||||||
|
|
||||||
|
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||||
|
if (!isApplicable(result.source.source())) return null
|
||||||
|
return TachiyomiImageDecoder(result.source, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isApplicable(source: BufferedSource): 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 fun equals(other: Any?) = other is ImageDecoderDecoder.Factory
|
||||||
|
|
||||||
|
override fun hashCode() = javaClass.hashCode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 12
|
const val DATABASE_VERSION = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@ -46,7 +46,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
// Fix kissmanga covers after supporting cloudflare
|
// Fix kissmanga covers after supporting cloudflare
|
||||||
db.execSQL(
|
db.execSQL(
|
||||||
"""UPDATE mangas SET thumbnail_url =
|
"""UPDATE mangas SET thumbnail_url =
|
||||||
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4"""
|
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (oldVersion < 3) {
|
if (oldVersion < 3) {
|
||||||
@ -85,6 +85,15 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
if (oldVersion < 12) {
|
if (oldVersion < 12) {
|
||||||
db.execSQL(MangaTable.addNextUpdateCol)
|
db.execSQL(MangaTable.addNextUpdateCol)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 13) {
|
||||||
|
db.execSQL(TrackTable.renameTableToTemp)
|
||||||
|
db.execSQL(TrackTable.createTableQuery)
|
||||||
|
db.execSQL(TrackTable.insertFromTempTable)
|
||||||
|
db.execSQL(TrackTable.dropTempTable)
|
||||||
|
}
|
||||||
|
if (oldVersion < 14) {
|
||||||
|
db.execSQL(ChapterTable.fixDateUploadIfNeeded)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
|
|||||||
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
|
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
|
||||||
CategoryPutResolver(),
|
CategoryPutResolver(),
|
||||||
CategoryGetResolver(),
|
CategoryGetResolver(),
|
||||||
CategoryDeleteResolver()
|
CategoryDeleteResolver(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
||||||
@ -40,17 +40,17 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
|
|||||||
COL_ID to obj.id,
|
COL_ID to obj.id,
|
||||||
COL_NAME to obj.name,
|
COL_NAME to obj.name,
|
||||||
COL_ORDER to obj.order,
|
COL_ORDER to obj.order,
|
||||||
COL_FLAGS to obj.flags
|
COL_FLAGS to obj.flags,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
|
||||||
id = cursor.getInt(cursor.getColumnIndex(COL_ID))
|
id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME))
|
||||||
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
|
order = cursor.getInt(cursor.getColumnIndexOrThrow(COL_ORDER))
|
||||||
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_FLAGS))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
|||||||
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
|
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
|
||||||
ChapterPutResolver(),
|
ChapterPutResolver(),
|
||||||
ChapterGetResolver(),
|
ChapterGetResolver(),
|
||||||
ChapterDeleteResolver()
|
ChapterDeleteResolver(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
||||||
@ -56,25 +56,25 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
|||||||
COL_DATE_UPLOAD to obj.date_upload,
|
COL_DATE_UPLOAD to obj.date_upload,
|
||||||
COL_LAST_PAGE_READ to obj.last_page_read,
|
COL_LAST_PAGE_READ to obj.last_page_read,
|
||||||
COL_CHAPTER_NUMBER to obj.chapter_number,
|
COL_CHAPTER_NUMBER to obj.chapter_number,
|
||||||
COL_SOURCE_ORDER to obj.source_order
|
COL_SOURCE_ORDER to obj.source_order,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
url = cursor.getString(cursor.getColumnIndexOrThrow(COL_URL))
|
||||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME))
|
||||||
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
|
scanlator = cursor.getString(cursor.getColumnIndexOrThrow(COL_SCANLATOR))
|
||||||
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
read = cursor.getInt(cursor.getColumnIndexOrThrow(COL_READ)) == 1
|
||||||
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
bookmark = cursor.getInt(cursor.getColumnIndexOrThrow(COL_BOOKMARK)) == 1
|
||||||
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
date_fetch = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_FETCH))
|
||||||
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
|
date_upload = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_UPLOAD))
|
||||||
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
|
last_page_read = cursor.getInt(cursor.getColumnIndexOrThrow(COL_LAST_PAGE_READ))
|
||||||
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
|
chapter_number = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_CHAPTER_NUMBER))
|
||||||
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
|
source_order = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SOURCE_ORDER))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
|
|||||||
class HistoryTypeMapping : SQLiteTypeMapping<History>(
|
class HistoryTypeMapping : SQLiteTypeMapping<History>(
|
||||||
HistoryPutResolver(),
|
HistoryPutResolver(),
|
||||||
HistoryGetResolver(),
|
HistoryGetResolver(),
|
||||||
HistoryDeleteResolver()
|
HistoryDeleteResolver(),
|
||||||
)
|
)
|
||||||
|
|
||||||
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
||||||
@ -40,17 +40,17 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
|
|||||||
COL_ID to obj.id,
|
COL_ID to obj.id,
|
||||||
COL_CHAPTER_ID to obj.chapter_id,
|
COL_CHAPTER_ID to obj.chapter_id,
|
||||||
COL_LAST_READ to obj.last_read,
|
COL_LAST_READ to obj.last_read,
|
||||||
COL_TIME_READ to obj.time_read
|
COL_TIME_READ to obj.time_read,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
|
chapter_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_CHAPTER_ID))
|
||||||
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
|
last_read = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_READ))
|
||||||
time_read = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ))
|
time_read = cursor.getLong(cursor.getColumnIndexOrThrow(COL_TIME_READ))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
|
|||||||
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
|
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
|
||||||
MangaCategoryPutResolver(),
|
MangaCategoryPutResolver(),
|
||||||
MangaCategoryGetResolver(),
|
MangaCategoryGetResolver(),
|
||||||
MangaCategoryDeleteResolver()
|
MangaCategoryDeleteResolver(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
||||||
@ -37,16 +37,16 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
|||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
COL_ID to obj.id,
|
COL_ID to obj.id,
|
||||||
COL_MANGA_ID to obj.manga_id,
|
COL_MANGA_ID to obj.manga_id,
|
||||||
COL_CATEGORY_ID to obj.category_id
|
COL_CATEGORY_ID to obj.category_id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
||||||
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
|
category_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_CATEGORY_ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_NEXT_UPDATE
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
||||||
@ -34,7 +33,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
|
|||||||
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
|
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
|
||||||
MangaPutResolver(),
|
MangaPutResolver(),
|
||||||
MangaGetResolver(),
|
MangaGetResolver(),
|
||||||
MangaDeleteResolver()
|
MangaDeleteResolver(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||||
@ -63,35 +62,33 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
||||||
COL_FAVORITE to obj.favorite,
|
COL_FAVORITE to obj.favorite,
|
||||||
COL_LAST_UPDATE to obj.last_update,
|
COL_LAST_UPDATE to obj.last_update,
|
||||||
COL_NEXT_UPDATE to obj.next_update,
|
|
||||||
COL_INITIALIZED to obj.initialized,
|
COL_INITIALIZED to obj.initialized,
|
||||||
COL_VIEWER to obj.viewer_flags,
|
COL_VIEWER to obj.viewer_flags,
|
||||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||||
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
||||||
COL_DATE_ADDED to obj.date_added
|
COL_DATE_ADDED to obj.date_added,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseMangaGetResolver {
|
interface BaseMangaGetResolver {
|
||||||
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
source = cursor.getLong(cursor.getColumnIndexOrThrow(COL_SOURCE))
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
url = cursor.getString(cursor.getColumnIndexOrThrow(COL_URL))
|
||||||
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
|
artist = cursor.getString(cursor.getColumnIndexOrThrow(COL_ARTIST))
|
||||||
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
|
author = cursor.getString(cursor.getColumnIndexOrThrow(COL_AUTHOR))
|
||||||
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION))
|
description = cursor.getString(cursor.getColumnIndexOrThrow(COL_DESCRIPTION))
|
||||||
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE))
|
genre = cursor.getString(cursor.getColumnIndexOrThrow(COL_GENRE))
|
||||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
|
||||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
|
||||||
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
thumbnail_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_THUMBNAIL_URL))
|
||||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
favorite = cursor.getInt(cursor.getColumnIndexOrThrow(COL_FAVORITE)) == 1
|
||||||
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
last_update = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_UPDATE))
|
||||||
next_update = cursor.getLong(cursor.getColumnIndex(COL_NEXT_UPDATE))
|
initialized = cursor.getInt(cursor.getColumnIndexOrThrow(COL_INITIALIZED)) == 1
|
||||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
viewer_flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_VIEWER))
|
||||||
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
chapter_flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_CHAPTER_FLAGS))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
cover_last_modified = cursor.getLong(cursor.getColumnIndexOrThrow(COL_COVER_LAST_MODIFIED))
|
||||||
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
date_added = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_ADDED))
|
||||||
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
|||||||
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
||||||
TrackPutResolver(),
|
TrackPutResolver(),
|
||||||
TrackGetResolver(),
|
TrackGetResolver(),
|
||||||
TrackDeleteResolver()
|
TrackDeleteResolver(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class TrackPutResolver : DefaultPutResolver<Track>() {
|
class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||||
@ -58,26 +58,26 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
|||||||
COL_TRACKING_URL to obj.tracking_url,
|
COL_TRACKING_URL to obj.tracking_url,
|
||||||
COL_SCORE to obj.score,
|
COL_SCORE to obj.score,
|
||||||
COL_START_DATE to obj.started_reading_date,
|
COL_START_DATE to obj.started_reading_date,
|
||||||
COL_FINISH_DATE to obj.finished_reading_date
|
COL_FINISH_DATE to obj.finished_reading_date,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class TrackGetResolver : DefaultGetResolver<Track>() {
|
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
||||||
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
sync_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SYNC_ID))
|
||||||
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
|
media_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_MEDIA_ID))
|
||||||
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
|
library_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LIBRARY_ID))
|
||||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
|
||||||
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
last_chapter_read = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_LAST_CHAPTER_READ))
|
||||||
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
total_chapters = cursor.getInt(cursor.getColumnIndexOrThrow(COL_TOTAL_CHAPTERS))
|
||||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
|
||||||
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
score = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_SCORE))
|
||||||
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
|
tracking_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_TRACKING_URL))
|
||||||
started_reading_date = cursor.getLong(cursor.getColumnIndex(COL_START_DATE))
|
started_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_START_DATE))
|
||||||
finished_reading_date = cursor.getLong(cursor.getColumnIndex(COL_FINISH_DATE))
|
finished_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_FINISH_DATE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
@ -37,6 +39,6 @@ interface Category : Serializable {
|
|||||||
this.name = name
|
this.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createDefault(): Category = create("Default").apply { id = 0 }
|
fun createDefault(context: Context): Category = create(context.getString(R.string.label_default)).apply { id = 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,14 @@ package eu.kanade.tachiyomi.data.database.models
|
|||||||
|
|
||||||
class LibraryManga : MangaImpl() {
|
class LibraryManga : MangaImpl() {
|
||||||
|
|
||||||
var unread: Int = 0
|
var unreadCount: Int = 0
|
||||||
|
var readCount: Int = 0
|
||||||
|
|
||||||
|
val totalChapters
|
||||||
|
get() = readCount + unreadCount
|
||||||
|
|
||||||
|
val hasStarted
|
||||||
|
get() = readCount > 0
|
||||||
|
|
||||||
var category: Int = 0
|
var category: Int = 0
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,6 @@ interface Manga : SManga {
|
|||||||
// last time the chapter list changed in any way
|
// last time the chapter list changed in any way
|
||||||
var last_update: Long
|
var last_update: Long
|
||||||
|
|
||||||
// predicted next update time based on latest (by date) 4 chapters' deltas
|
|
||||||
var next_update: Long
|
|
||||||
|
|
||||||
var date_added: Long
|
var date_added: Long
|
||||||
|
|
||||||
var viewer_flags: Int
|
var viewer_flags: Int
|
||||||
@ -36,7 +33,8 @@ interface Manga : SManga {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getGenres(): List<String>? {
|
fun getGenres(): List<String>? {
|
||||||
return genre?.split(", ")?.map { it.trim() }
|
if (genre.isNullOrBlank()) return null
|
||||||
|
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setChapterFlags(flag: Int, mask: Int) {
|
private fun setChapterFlags(flag: Int, mask: Int) {
|
||||||
@ -127,6 +125,6 @@ fun Manga.toMangaInfo(): MangaInfo {
|
|||||||
genres = this.getGenres() ?: emptyList(),
|
genres = this.getGenres() ?: emptyList(),
|
||||||
key = this.url,
|
key = this.url,
|
||||||
status = this.status,
|
status = this.status,
|
||||||
title = this.title
|
title = this.title,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,6 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var last_update: Long = 0
|
override var last_update: Long = 0
|
||||||
|
|
||||||
override var next_update: Long = 0
|
|
||||||
|
|
||||||
override var date_added: Long = 0
|
override var date_added: Long = 0
|
||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
data class SourceIdMangaCount(val source: Long, val count: Int)
|
@ -16,7 +16,7 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
var last_chapter_read: Int
|
var last_chapter_read: Float
|
||||||
|
|
||||||
var total_chapters: Int
|
var total_chapters: Int
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
override lateinit var title: String
|
override lateinit var title: String
|
||||||
|
|
||||||
override var last_chapter_read: Int = 0
|
override var last_chapter_read: Float = 0F
|
||||||
|
|
||||||
override var total_chapters: Int = 0
|
override var total_chapters: Int = 0
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ interface CategoryQueries : DbProvider {
|
|||||||
Query.builder()
|
Query.builder()
|
||||||
.table(CategoryTable.TABLE)
|
.table(CategoryTable.TABLE)
|
||||||
.orderBy(CategoryTable.COL_ORDER)
|
.orderBy(CategoryTable.COL_ORDER)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ interface CategoryQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getCategoriesForMangaQuery())
|
.query(getCategoriesForMangaQuery())
|
||||||
.args(manga.id)
|
.args(manga.id)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ interface ChapterQueries : DbProvider {
|
|||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_MANGA_ID} = ?")
|
.where("${ChapterTable.COL_MANGA_ID} = ?")
|
||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ interface ChapterQueries : DbProvider {
|
|||||||
.query(getRecentsQuery())
|
.query(getRecentsQuery())
|
||||||
.args(date.time)
|
.args(date.time)
|
||||||
.observesTables(ChapterTable.TABLE)
|
.observesTables(ChapterTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
@ -46,7 +46,7 @@ interface ChapterQueries : DbProvider {
|
|||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_ID} = ?")
|
.where("${ChapterTable.COL_ID} = ?")
|
||||||
.whereArgs(id)
|
.whereArgs(id)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ interface ChapterQueries : DbProvider {
|
|||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_URL} = ?")
|
.where("${ChapterTable.COL_URL} = ?")
|
||||||
.whereArgs(url)
|
.whereArgs(url)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ interface ChapterQueries : DbProvider {
|
|||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
||||||
.whereArgs(url, mangaId)
|
.whereArgs(url, mangaId)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ interface HistoryQueries : DbProvider {
|
|||||||
.query(getRecentMangasQuery(search))
|
.query(getRecentMangasQuery(search))
|
||||||
.args(date.time, limit, offset)
|
.args(date.time, limit, offset)
|
||||||
.observesTables(HistoryTable.TABLE)
|
.observesTables(HistoryTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
@ -44,7 +44,7 @@ interface HistoryQueries : DbProvider {
|
|||||||
.query(getHistoryByMangaId())
|
.query(getHistoryByMangaId())
|
||||||
.args(mangaId)
|
.args(mangaId)
|
||||||
.observesTables(HistoryTable.TABLE)
|
.observesTables(HistoryTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ interface HistoryQueries : DbProvider {
|
|||||||
.query(getHistoryByChapterUrl())
|
.query(getHistoryByChapterUrl())
|
||||||
.args(chapterUrl)
|
.args(chapterUrl)
|
||||||
.observesTables(HistoryTable.TABLE)
|
.observesTables(HistoryTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ interface HistoryQueries : DbProvider {
|
|||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(HistoryTable.TABLE)
|
.table(HistoryTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ interface HistoryQueries : DbProvider {
|
|||||||
.table(HistoryTable.TABLE)
|
.table(HistoryTable.TABLE)
|
||||||
.where("${HistoryTable.COL_LAST_READ} = ?")
|
.where("${HistoryTable.COL_LAST_READ} = ?")
|
||||||
.whereArgs(0)
|
.whereArgs(0)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ interface MangaCategoryQueries : DbProvider {
|
|||||||
.table(MangaCategoryTable.TABLE)
|
.table(MangaCategoryTable.TABLE)
|
||||||
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
||||||
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.Queries
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
|
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
@ -7,7 +8,14 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
|||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.*
|
import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
|
||||||
|
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.SourceIdMangaCountGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
@ -21,11 +29,26 @@ interface MangaQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(libraryQuery)
|
.query(libraryQuery)
|
||||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getDuplicateLibraryManga(manga: Manga) = db.get()
|
||||||
|
.`object`(Manga::class.java)
|
||||||
|
.withQuery(
|
||||||
|
Query.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_FAVORITE} = 1 AND LOWER(${MangaTable.COL_TITLE}) = ? AND ${MangaTable.COL_SOURCE} != ?")
|
||||||
|
.whereArgs(
|
||||||
|
manga.title.lowercase(),
|
||||||
|
manga.source,
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
|
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
|
||||||
var queryBuilder = Query.builder()
|
var queryBuilder = Query.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
@ -49,7 +72,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
|
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
|
||||||
.whereArgs(url, sourceId)
|
.whereArgs(url, sourceId)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -60,10 +83,21 @@ interface MangaQueries : DbProvider {
|
|||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
.whereArgs(id)
|
.whereArgs(id)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getSourceIdsWithNonLibraryManga() = db.get()
|
||||||
|
.listOfObjects(SourceIdMangaCount::class.java)
|
||||||
|
.withQuery(
|
||||||
|
RawQuery.builder()
|
||||||
|
.query(getSourceIdsWithNonLibraryMangaQuery())
|
||||||
|
.observesTables(MangaTable.TABLE)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||||
|
|
||||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
||||||
@ -75,7 +109,7 @@ interface MangaQueries : DbProvider {
|
|||||||
|
|
||||||
fun updateChapterFlags(manga: List<Manga>) = db.put()
|
fun updateChapterFlags(manga: List<Manga>) = db.put()
|
||||||
.objects(manga)
|
.objects(manga)
|
||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags, true))
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateViewerFlags(manga: Manga) = db.put()
|
fun updateViewerFlags(manga: Manga) = db.put()
|
||||||
@ -85,12 +119,7 @@ interface MangaQueries : DbProvider {
|
|||||||
|
|
||||||
fun updateViewerFlags(manga: List<Manga>) = db.put()
|
fun updateViewerFlags(manga: List<Manga>) = db.put()
|
||||||
.objects(manga)
|
.objects(manga)
|
||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun updateNextUpdated(manga: Manga) = db.put()
|
|
||||||
.`object`(manga)
|
|
||||||
.withPutResolver(MangaNextUpdatedPutResolver())
|
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateLastUpdated(manga: Manga) = db.put()
|
fun updateLastUpdated(manga: Manga) = db.put()
|
||||||
@ -117,13 +146,13 @@ interface MangaQueries : DbProvider {
|
|||||||
|
|
||||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||||
|
|
||||||
fun deleteMangasNotInLibrary() = db.delete()
|
fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) = db.delete()
|
||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)})")
|
||||||
.whereArgs(0)
|
.whereArgs(0, *sourceIds.toTypedArray())
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -131,7 +160,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -141,7 +170,7 @@ interface MangaQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getLastReadMangaQuery())
|
.query(getLastReadMangaQuery())
|
||||||
.observesTables(MangaTable.TABLE)
|
.observesTables(MangaTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -151,7 +180,7 @@ interface MangaQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getTotalChapterMangaQuery())
|
.query(getTotalChapterMangaQuery())
|
||||||
.observesTables(MangaTable.TABLE)
|
.observesTables(MangaTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -161,7 +190,7 @@ interface MangaQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getLatestChapterMangaQuery())
|
.query(getLatestChapterMangaQuery())
|
||||||
.observesTables(MangaTable.TABLE)
|
.observesTables(MangaTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -171,7 +200,7 @@ interface MangaQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getChapterFetchDateMangaQuery())
|
.query(getChapterFetchDateMangaQuery())
|
||||||
.observesTables(MangaTable.TABLE)
|
.observesTables(MangaTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
||||||
@ -7,21 +8,28 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCateg
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the manga from the library, with their categories and unread count.
|
* Query to get the manga from the library, with their categories, read and unread count.
|
||||||
*/
|
*/
|
||||||
val libraryQuery =
|
val libraryQuery =
|
||||||
"""
|
"""
|
||||||
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
||||||
FROM (
|
FROM (
|
||||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}
|
SELECT ${Manga.TABLE}.*, COALESCE(C.unreadCount, 0) AS ${Manga.COMPUTED_COL_UNREAD_COUNT}, COALESCE(R.readCount, 0) AS ${Manga.COMPUTED_COL_READ_COUNT}
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unreadCount
|
||||||
FROM ${Chapter.TABLE}
|
FROM ${Chapter.TABLE}
|
||||||
WHERE ${Chapter.COL_READ} = 0
|
WHERE ${Chapter.COL_READ} = 0
|
||||||
GROUP BY ${Chapter.COL_MANGA_ID}
|
GROUP BY ${Chapter.COL_MANGA_ID}
|
||||||
) AS C
|
) AS C
|
||||||
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS readCount
|
||||||
|
FROM ${Chapter.TABLE}
|
||||||
|
WHERE ${Chapter.COL_READ} = 1
|
||||||
|
GROUP BY ${Chapter.COL_MANGA_ID}
|
||||||
|
) AS R
|
||||||
|
ON ${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID}
|
||||||
WHERE ${Manga.COL_FAVORITE} = 1
|
WHERE ${Manga.COL_FAVORITE} = 1
|
||||||
GROUP BY ${Manga.COL_ID}
|
GROUP BY ${Manga.COL_ID}
|
||||||
ORDER BY ${Manga.COL_TITLE}
|
ORDER BY ${Manga.COL_TITLE}
|
||||||
@ -142,3 +150,14 @@ fun getCategoriesForMangaQuery() =
|
|||||||
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
||||||
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
/** Query to get the list of sources in the database that have
|
||||||
|
* non-library manga, and how many
|
||||||
|
*/
|
||||||
|
fun getSourceIdsWithNonLibraryMangaQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${Manga.COL_SOURCE}, COUNT(*) as ${SourceIdMangaCountGetResolver.COL_COUNT}
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
WHERE ${Manga.COL_FAVORITE} = 0
|
||||||
|
GROUP BY ${Manga.COL_SOURCE}
|
||||||
|
"""
|
||||||
|
@ -15,7 +15,7 @@ interface TrackQueries : DbProvider {
|
|||||||
.withQuery(
|
.withQuery(
|
||||||
Query.builder()
|
Query.builder()
|
||||||
.table(TrackTable.TABLE)
|
.table(TrackTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ interface TrackQueries : DbProvider {
|
|||||||
.table(TrackTable.TABLE)
|
.table(TrackTable.TABLE)
|
||||||
.where("${TrackTable.COL_MANGA_ID} = ?")
|
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ interface TrackQueries : DbProvider {
|
|||||||
.table(TrackTable.TABLE)
|
.table(TrackTable.TABLE)
|
||||||
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||||
.whereArgs(manga.id, sync.id)
|
.whereArgs(manga.id, sync.id)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,6 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
|||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
ChapterTable.COL_READ to chapter.read,
|
ChapterTable.COL_READ to chapter.read,
|
||||||
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,6 @@ class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
|
|||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
ChapterTable.COL_READ to chapter.read,
|
ChapterTable.COL_READ to chapter.read,
|
||||||
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,6 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
|||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
ChapterTable.COL_READ to chapter.read,
|
ChapterTable.COL_READ to chapter.read,
|
||||||
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
|
|||||||
|
|
||||||
fun mapToContentValues(chapter: Chapter) =
|
fun mapToContentValues(chapter: Chapter) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
ChapterTable.COL_SOURCE_ORDER to chapter.source_order
|
ChapterTable.COL_SOURCE_ORDER to chapter.source_order,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -24,12 +24,10 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
.table(updateQuery.table())
|
.table(updateQuery.table())
|
||||||
.where(updateQuery.where())
|
.where(updateQuery.where())
|
||||||
.whereArgs(updateQuery.whereArgs())
|
.whereArgs(updateQuery.whereArgs())
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
|
|
||||||
val putResult: PutResult
|
cursor.use { putCursor ->
|
||||||
|
|
||||||
putResult = cursor.use { putCursor ->
|
|
||||||
if (putCursor.count == 0) {
|
if (putCursor.count == 0) {
|
||||||
val insertQuery = mapToInsertQuery(history)
|
val insertQuery = mapToInsertQuery(history)
|
||||||
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
||||||
@ -39,26 +37,16 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
putResult
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates update query
|
|
||||||
* @param obj history object
|
|
||||||
*/
|
|
||||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||||
.table(HistoryTable.TABLE)
|
.table(HistoryTable.TABLE)
|
||||||
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
||||||
.whereArgs(obj.chapter_id)
|
.whereArgs(obj.chapter_id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
/**
|
private fun mapToUpdateContentValues(history: History) =
|
||||||
* Create content query
|
|
||||||
* @param history object
|
|
||||||
*/
|
|
||||||
fun mapToUpdateContentValues(history: History) =
|
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
HistoryTable.COL_LAST_READ to history.last_read
|
HistoryTable.COL_LAST_READ to history.last_read,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,9 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
|
|||||||
val manga = LibraryManga()
|
val manga = LibraryManga()
|
||||||
|
|
||||||
mapBaseFromCursor(manga, cursor)
|
mapBaseFromCursor(manga, cursor)
|
||||||
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
manga.unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COMPUTED_COL_UNREAD_COUNT))
|
||||||
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
|
manga.category = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_CATEGORY))
|
||||||
|
manga.readCount = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COMPUTED_COL_READ_COUNT))
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
|||||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||||
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
||||||
manga.id = chapter.manga_id
|
manga.id = chapter.manga_id
|
||||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
manga.url = cursor.getString(cursor.getColumnIndexOrThrow("mangaUrl"))
|
||||||
|
|
||||||
return MangaChapter(manga, chapter)
|
return MangaChapter(manga, chapter)
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>()
|
|||||||
|
|
||||||
// Make certain column conflicts are dealt with
|
// Make certain column conflicts are dealt with
|
||||||
manga.id = chapter.manga_id
|
manga.id = chapter.manga_id
|
||||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
manga.url = cursor.getString(cursor.getColumnIndexOrThrow("mangaUrl"))
|
||||||
chapter.id = history.chapter_id
|
chapter.id = history.chapter_id
|
||||||
|
|
||||||
// Return result
|
// Return result
|
||||||
|
2
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaCoverLastModifiedPutResolver.kt
2
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaCoverLastModifiedPutResolver.kt
@ -27,6 +27,6 @@ class MangaCoverLastModifiedPutResolver : PutResolver<Manga>() {
|
|||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
MangaTable.COL_COVER_LAST_MODIFIED to manga.cover_last_modified
|
MangaTable.COL_COVER_LAST_MODIFIED to manga.cover_last_modified,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
|
|||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
MangaTable.COL_FAVORITE to manga.favorite
|
MangaTable.COL_FAVORITE to manga.favorite,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
import kotlin.reflect.KProperty1
|
import kotlin.reflect.KProperty1
|
||||||
|
|
||||||
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>, private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>) : PutResolver<Manga>() {
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
@ -20,24 +20,14 @@ class MangaFlagsPutResolver(private val colName: String, private val fieldGetter
|
|||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga): UpdateQuery {
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
val builder = UpdateQuery.builder()
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
return if (updateAll) {
|
.whereArgs(manga.id)
|
||||||
builder
|
.build()
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.build()
|
|
||||||
} else {
|
|
||||||
builder
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
colName to fieldGetter.get(manga)
|
colName to fieldGetter.get(manga),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
|
|||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
MangaTable.COL_LAST_UPDATE to manga.last_update
|
MangaTable.COL_LAST_UPDATE to manga.last_update,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -27,6 +27,6 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
|
|||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
MangaTable.COL_TITLE to manga.title
|
MangaTable.COL_TITLE to manga.title,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
23
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/SourceIdMangaCountGetResolver.kt
Normal file
23
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/SourceIdMangaCountGetResolver.kt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
class SourceIdMangaCountGetResolver : DefaultGetResolver<SourceIdMangaCount>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val INSTANCE = SourceIdMangaCountGetResolver()
|
||||||
|
const val COL_COUNT = "manga_count"
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("Range")
|
||||||
|
override fun mapFromCursor(cursor: Cursor): SourceIdMangaCount {
|
||||||
|
val sourceID = cursor.getLong(cursor.getColumnIndexOrThrow(MangaTable.COL_SOURCE))
|
||||||
|
val count = cursor.getInt(cursor.getColumnIndexOrThrow(COL_COUNT))
|
||||||
|
|
||||||
|
return SourceIdMangaCount(sourceID, count)
|
||||||
|
}
|
||||||
|
}
|
@ -62,4 +62,7 @@ object ChapterTable {
|
|||||||
|
|
||||||
val addScanlator: String
|
val addScanlator: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
||||||
|
|
||||||
|
val fixDateUploadIfNeeded: String
|
||||||
|
get() = "UPDATE $TABLE SET $COL_DATE_UPLOAD = $COL_DATE_FETCH WHERE $COL_DATE_UPLOAD = 0"
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_LAST_UPDATE = "last_update"
|
const val COL_LAST_UPDATE = "last_update"
|
||||||
|
|
||||||
|
// Not actually used anymore
|
||||||
const val COL_NEXT_UPDATE = "next_update"
|
const val COL_NEXT_UPDATE = "next_update"
|
||||||
|
|
||||||
const val COL_DATE_ADDED = "date_added"
|
const val COL_DATE_ADDED = "date_added"
|
||||||
@ -38,12 +39,15 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_CHAPTER_FLAGS = "chapter_flags"
|
const val COL_CHAPTER_FLAGS = "chapter_flags"
|
||||||
|
|
||||||
const val COL_UNREAD = "unread"
|
|
||||||
|
|
||||||
const val COL_CATEGORY = "category"
|
const val COL_CATEGORY = "category"
|
||||||
|
|
||||||
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
||||||
|
|
||||||
|
// Not an actual value but computed when created
|
||||||
|
const val COMPUTED_COL_UNREAD_COUNT = "unread_count"
|
||||||
|
|
||||||
|
const val COMPUTED_COL_READ_COUNT = "read_count"
|
||||||
|
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() =
|
get() =
|
||||||
"""CREATE TABLE $TABLE(
|
"""CREATE TABLE $TABLE(
|
||||||
|
@ -39,7 +39,7 @@ object TrackTable {
|
|||||||
$COL_MEDIA_ID INTEGER NOT NULL,
|
$COL_MEDIA_ID INTEGER NOT NULL,
|
||||||
$COL_LIBRARY_ID INTEGER,
|
$COL_LIBRARY_ID INTEGER,
|
||||||
$COL_TITLE TEXT NOT NULL,
|
$COL_TITLE TEXT NOT NULL,
|
||||||
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
|
$COL_LAST_CHAPTER_READ REAL NOT NULL,
|
||||||
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
||||||
$COL_STATUS INTEGER NOT NULL,
|
$COL_STATUS INTEGER NOT NULL,
|
||||||
$COL_SCORE FLOAT NOT NULL,
|
$COL_SCORE FLOAT NOT NULL,
|
||||||
@ -62,4 +62,19 @@ object TrackTable {
|
|||||||
|
|
||||||
val addFinishDate: String
|
val addFinishDate: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
|
||||||
|
|
||||||
|
val renameTableToTemp: String
|
||||||
|
get() =
|
||||||
|
"ALTER TABLE $TABLE RENAME TO ${TABLE}_tmp"
|
||||||
|
|
||||||
|
val insertFromTempTable: String
|
||||||
|
get() =
|
||||||
|
"""
|
||||||
|
|INSERT INTO $TABLE($COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE)
|
||||||
|
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|
||||||
|
|FROM ${TABLE}_tmp
|
||||||
|
""".trimMargin()
|
||||||
|
|
||||||
|
val dropTempTable: String
|
||||||
|
get() = "DROP TABLE ${TABLE}_tmp"
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ class DownloadCache(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val provider: DownloadProvider,
|
private val provider: DownloadProvider,
|
||||||
private val sourceManager: SourceManager,
|
private val sourceManager: SourceManager,
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
private val preferences: PreferencesHelper = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -143,7 +143,7 @@ class DownloadCache(
|
|||||||
mangaDirs.values.forEach { mangaDir ->
|
mangaDirs.values.forEach { mangaDir ->
|
||||||
val chapterDirs = mangaDir.dir.listFiles()
|
val chapterDirs = mangaDir.dir.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.mapNotNull { it.name }
|
.mapNotNull { it.name?.replace(".cbz", "") }
|
||||||
.toHashSet()
|
.toHashSet()
|
||||||
|
|
||||||
mangaDir.files = chapterDirs
|
mangaDir.files = chapterDirs
|
||||||
@ -236,7 +236,7 @@ class DownloadCache(
|
|||||||
*/
|
*/
|
||||||
private class RootDirectory(
|
private class RootDirectory(
|
||||||
val dir: UniFile,
|
val dir: UniFile,
|
||||||
var files: Map<Long, SourceDirectory> = hashMapOf()
|
var files: Map<Long, SourceDirectory> = hashMapOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -244,7 +244,7 @@ class DownloadCache(
|
|||||||
*/
|
*/
|
||||||
private class SourceDirectory(
|
private class SourceDirectory(
|
||||||
val dir: UniFile,
|
val dir: UniFile,
|
||||||
var files: Map<String, MangaDirectory> = hashMapOf()
|
var files: Map<String, MangaDirectory> = hashMapOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -252,7 +252,7 @@ class DownloadCache(
|
|||||||
*/
|
*/
|
||||||
private class MangaDirectory(
|
private class MangaDirectory(
|
||||||
val dir: UniFile,
|
val dir: UniFile,
|
||||||
var files: Set<String> = hashSetOf()
|
var files: Set<String> = hashSetOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
@ -13,8 +14,11 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,7 +28,10 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class DownloadManager(private val context: Context) {
|
class DownloadManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val db: DatabaseHelper = Injekt.get(),
|
||||||
|
) {
|
||||||
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
@ -32,7 +39,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
||||||
*/
|
*/
|
||||||
private val provider = DownloadProvider(context)
|
val provider = DownloadProvider(context)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache of downloaded chapters.
|
* Cache of downloaded chapters.
|
||||||
@ -217,7 +224,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param download the download to cancel.
|
* @param download the download to cancel.
|
||||||
*/
|
*/
|
||||||
fun deletePendingDownload(download: Download) {
|
fun deletePendingDownload(download: Download) {
|
||||||
deleteChapters(listOf(download.chapter), download.manga, download.source)
|
deleteChapters(listOf(download.chapter), download.manga, download.source, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deletePendingDownloads(vararg downloads: Download) {
|
fun deletePendingDownloads(vararg downloads: Download) {
|
||||||
@ -225,7 +232,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
downloadsByManga.map { entry ->
|
downloadsByManga.map { entry ->
|
||||||
val manga = entry.value.first().manga
|
val manga = entry.value.first().manga
|
||||||
val source = entry.value.first().source
|
val source = entry.value.first().source
|
||||||
deleteChapters(entry.value.map { it.chapter }, manga, source)
|
deleteChapters(entry.value.map { it.chapter }, manga, source, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,9 +242,15 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param chapters the list of chapters to delete.
|
* @param chapters the list of chapters to delete.
|
||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
* @param source the source of the chapters.
|
* @param source the source of the chapters.
|
||||||
|
* @param isCancelling true if it's simply cancelling a download
|
||||||
*/
|
*/
|
||||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source, isCancelling: Boolean = false): List<Chapter> {
|
||||||
val filteredChapters = getChaptersToDelete(chapters)
|
val filteredChapters = if (isCancelling) {
|
||||||
|
chapters
|
||||||
|
} else {
|
||||||
|
getChaptersToDelete(chapters, manga)
|
||||||
|
}
|
||||||
|
|
||||||
launchIO {
|
launchIO {
|
||||||
removeFromDownloadQueue(filteredChapters)
|
removeFromDownloadQueue(filteredChapters)
|
||||||
|
|
||||||
@ -290,7 +303,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
*/
|
*/
|
||||||
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
||||||
pendingDeleter.addChapters(getChaptersToDelete(chapters), manga)
|
pendingDeleter.addChapters(getChaptersToDelete(chapters, manga), manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -314,24 +327,37 @@ class DownloadManager(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||||
val oldNames = provider.getValidChapterDirNames(oldChapter)
|
val oldNames = provider.getValidChapterDirNames(oldChapter)
|
||||||
val newName = provider.getChapterDirName(newChapter)
|
|
||||||
val mangaDir = provider.getMangaDir(manga, source)
|
val mangaDir = provider.getMangaDir(manga, source)
|
||||||
|
|
||||||
// Assume there's only 1 version of the chapter name formats present
|
// Assume there's only 1 version of the chapter name formats present
|
||||||
val oldFolder = oldNames.asSequence()
|
val oldDownload = oldNames.asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
.firstOrNull()
|
.firstOrNull() ?: return
|
||||||
|
|
||||||
if (oldFolder?.renameTo(newName) == true) {
|
var newName = provider.getChapterDirName(newChapter)
|
||||||
|
if (oldDownload.isFile && oldDownload.name?.endsWith(".cbz") == true) {
|
||||||
|
newName += ".cbz"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldDownload.renameTo(newName)) {
|
||||||
cache.removeChapter(oldChapter, manga)
|
cache.removeChapter(oldChapter, manga)
|
||||||
cache.addChapter(newName, mangaDir, manga)
|
cache.addChapter(newName, mangaDir, manga)
|
||||||
} else {
|
} else {
|
||||||
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
logcat(LogPriority.ERROR) { "Could not rename downloaded chapter: ${oldNames.joinToString()}." }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getChaptersToDelete(chapters: List<Chapter>): List<Chapter> {
|
private fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
|
||||||
return if (!preferences.removeBookmarkedChapters()) {
|
// Retrieve the categories that are set to exclude from being deleted on read
|
||||||
|
val categoriesToExclude = preferences.removeExcludeCategories().get().map(String::toInt)
|
||||||
|
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
|
.mapNotNull { it.id }
|
||||||
|
.takeUnless { it.isEmpty() }
|
||||||
|
?: listOf(0)
|
||||||
|
|
||||||
|
return if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) {
|
||||||
|
chapters.filterNot { it.read }
|
||||||
|
} else if (!preferences.removeBookmarkedChapters()) {
|
||||||
chapters.filterNot { it.bookmark }
|
chapters.filterNot { it.bookmark }
|
||||||
} else {
|
} else {
|
||||||
chapters
|
chapters
|
||||||
|
@ -52,7 +52,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Updated when error is thrown
|
* Updated when error is thrown
|
||||||
*/
|
*/
|
||||||
var errorThrown = false
|
private var errorThrown = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updated when paused
|
* Updated when paused
|
||||||
@ -93,18 +93,19 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_pause_24dp,
|
R.drawable.ic_pause_24dp,
|
||||||
context.getString(R.string.action_pause),
|
context.getString(R.string.action_pause),
|
||||||
NotificationReceiver.pauseDownloadsPendingBroadcast(context)
|
NotificationReceiver.pauseDownloadsPendingBroadcast(context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadingProgressText = context.getString(
|
val downloadingProgressText = context.getString(
|
||||||
R.string.chapter_downloading_progress,
|
R.string.chapter_downloading_progress,
|
||||||
download.downloadedImages,
|
download.downloadedImages,
|
||||||
download.pages!!.size
|
download.pages!!.size,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (preferences.hideNotificationContent()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
setContentTitle(downloadingProgressText)
|
setContentTitle(downloadingProgressText)
|
||||||
|
setContentText(null)
|
||||||
} else {
|
} else {
|
||||||
val title = download.manga.title.chop(15)
|
val title = download.manga.title.chop(15)
|
||||||
val quotedTitle = Pattern.quote(title)
|
val quotedTitle = Pattern.quote(title)
|
||||||
@ -137,13 +138,13 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_play_arrow_24dp,
|
R.drawable.ic_play_arrow_24dp,
|
||||||
context.getString(R.string.action_resume),
|
context.getString(R.string.action_resume),
|
||||||
NotificationReceiver.resumeDownloadsPendingBroadcast(context)
|
NotificationReceiver.resumeDownloadsPendingBroadcast(context),
|
||||||
)
|
)
|
||||||
// Clear action
|
// Clear action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_close_24dp,
|
R.drawable.ic_close_24dp,
|
||||||
context.getString(R.string.action_cancel_all),
|
context.getString(R.string.action_cancel_all),
|
||||||
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
NotificationReceiver.clearDownloadsPendingBroadcast(context),
|
||||||
)
|
)
|
||||||
|
|
||||||
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
@ -187,8 +188,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
fun onWarning(reason: String) {
|
fun onWarning(reason: String) {
|
||||||
with(errorNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(reason)
|
setStyle(NotificationCompat.BigTextStyle().bigText(reason))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
@ -208,15 +209,14 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* @param error string containing error information.
|
* @param error string containing error information.
|
||||||
* @param chapter string containing chapter title.
|
* @param chapter string containing chapter title.
|
||||||
*/
|
*/
|
||||||
fun onError(error: String? = null, chapter: String? = null) {
|
fun onError(error: String? = null, chapter: String? = null, mangaTitle: String? = null) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(errorNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(
|
setContentTitle(
|
||||||
chapter
|
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_notifier_downloader_title),
|
||||||
?: context.getString(R.string.download_notifier_downloader_title)
|
|
||||||
)
|
)
|
||||||
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user