Compare commits

..

354 Commits

Author SHA1 Message Date
Aria Moradi 083996a48d wating on: https://github.com/Kotlin/kotlinx.coroutines/issues/261
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build FatJar (push) Failing after 17s
2021-05-17 14:30:59 +04:30
Aria Moradi 9d38f478e3 fix slow manga thumbnails issue, next manga reset page issue 2021-05-17 14:22:24 +04:30
Aria Moradi 57274a0a01 remove unused electron script 2021-05-17 12:32:15 +04:30
Aria Moradi b3b56b7fc8 also build windows package for publish 2021-05-17 12:30:15 +04:30
Forgenn 0b690577da Load next chapter when getting to the last page (#84)
* Load next chapter when scrolling to the bottom (Webtoon, Continues Vertical)

* Load next chapter when scrolling to the bottom (Paged reader)

* Added missing types to IReaderProps

* Move load next chapter when at last page to VerticalReader

* Dependency fix

* Use react history for loading next page
2021-05-17 12:27:14 +04:30
Aria Moradi e9683a3a37 bump to v0.3.3 2021-05-17 12:02:01 +04:30
Aria Moradi f8f67b3eba finish up 2021-05-17 11:59:59 +04:30
Aria Moradi 7b16b082d8 needs kt 2021-05-17 11:55:49 +04:30
Aria Moradi 2a783f0d8e btter folder name 2021-05-17 11:54:33 +04:30
Aria Moradi 42ae32de33 give the correct path 2021-05-17 11:40:38 +04:30
Aria Moradi cec7ddc486 update file permissions 2021-05-17 11:28:26 +04:30
Aria Moradi 9c55fc3868 make windows package with packr 2021-05-17 11:20:24 +04:30
Syer10 104c5a8d83 Code cleanup (#85)
* GC Unused or only used once objects

* Move things around a bit

* Revert some changes

* Fix imports

* Revert about change

* Put back logger

* Private logger

* Revert systemtray

* Move import
2021-05-17 02:48:01 +04:30
Aria Moradi 7450b16742 - Reader -> Pager
- add cloneObject
- add missing copyright notices
2021-05-17 01:38:59 +04:30
Aria Moradi 3ecd0931a1 missing from the previous commit 2021-05-17 01:37:25 +04:30
Aria Moradi 2f2a52ae2f Move navbars 2021-05-17 01:36:31 +04:30
Aria Moradi f464087c30 also handle Errors from java 2021-05-16 23:58:32 +04:30
Aria Moradi 2364960388 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-16 22:15:35 +04:30
Aria Moradi 76be4d64cd update launch4j's jre and make it 64bit 2021-05-16 12:39:05 +04:30
Aria Moradi 7d98e8ce47 [SKIP CI] correct link 2021-05-16 01:53:35 +04:30
Aria Moradi 40831fc681 [SKIP CI] new links 2021-05-16 01:52:01 +04:30
Aria Moradi e38e7ccf26 [SKIP CI] troubleshooting from the wiki 2021-05-16 01:48:49 +04:30
Aria Moradi 98b9e2f2cf [SKIP CI] no need to delete data anymore... 2021-05-16 01:47:33 +04:30
Aria Moradi 4bf3c12f76 fixed when spinner stops just after first offline chapter fetch 2021-05-16 01:07:48 +04:30
Aria Moradi bab25f9ad9 bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-15 23:24:25 +04:30
Aria Moradi a62ee8f8c3 handle reader types 2021-05-15 23:22:37 +04:30
Aria Moradi 5f23691e20 add toast lib 2021-05-15 20:37:07 +04:30
Aria Moradi 3de9ccc62f stop spinning if chapter list is empty 2021-05-15 18:51:38 +04:30
Aria Moradi 1896f7f37b remove lazy load 2021-05-15 18:26:40 +04:30
Aria Moradi 490643dc02 proof of concept readers 2021-05-15 18:17:12 +04:30
Aria Moradi 9808976088 restructure the reader 2021-05-15 17:18:57 +04:30
Aria Moradi 5a73068a10 bump to v0.3.1
CI Publish / Validate Gradle Wrapper (push) Successful in 17s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-15 14:53:44 +04:30
Aria Moradi 01d5c2540d chapter updates when pressing UI buttons 2021-05-15 13:43:26 +04:30
Aria Moradi 866b01f865 support bookmarked and isRead in webUI 2021-05-14 19:22:10 +04:30
Aria Moradi da6a953099 exposed error 2021-05-14 17:31:07 +04:30
Aria Moradi bce8d58845 make it look nicer? 2021-05-14 02:01:28 +04:30
Aria Moradi 3cfce2db04 fix chapters not shown on movbile 2021-05-13 21:40:42 +04:30
Aria Moradi 327aae5dd9 Merge pull request #79 from Suwayomi/read-category
Support more chapter parameters
2021-05-13 18:34:27 +04:30
Aria Moradi 1bdfde7032 no longer TODO 2021-05-13 18:33:24 +04:30
Aria Moradi 295a0817b0 fix wrong chapters being removed, fix da css 2021-05-13 18:29:20 +04:30
Aria Moradi a02dc02d52 remove console prints 2021-05-13 17:49:33 +04:30
Aria Moradi dc012edf7d staisfying results? with chapters scrolling 2021-05-13 17:46:40 +04:30
Aria Moradi 1e2eb11c13 update dependencies 2021-05-13 15:01:37 +04:30
Aria Moradi 3a825f4f25 fix manga thumbnail loading bug 2021-05-12 10:39:21 +04:30
Aria Moradi b9ea8c5f74 code cleanup 2021-05-12 10:20:01 +04:30
Aria Moradi 320d7e2536 reformat 2021-05-11 18:55:09 +04:30
Aria Moradi c200785479 handle front, handle orphans 2021-05-11 18:45:53 +04:30
Aria Moradi 8abb132ad6 chapter new parameters get endpoint 2021-05-11 15:50:18 +04:30
Aria Moradi 8bb2269f36 [SKIP CI] fix typo 2021-05-08 20:33:53 +04:30
Aria Moradi 9d17b26283 [SKIP CI] up for debate 2021-05-07 19:41:34 +04:30
Aria Moradi 5909f15db7 [SKIP CI] code of conduct 2021-05-07 19:39:24 +04:30
Aria Moradi 11672ca576 [SKIP CI] 2021-05-07 19:35:11 +04:30
Aria Moradi e09773def3 [SKIP CI] potential contributors 2021-05-07 19:32:32 +04:30
Aria Moradi f6d4432e6f [SKIP CI] typo 2021-05-07 19:13:48 +04:30
Aria Moradi 45a6abc5c2 [SKIP CI] improve structure 2021-05-07 19:13:04 +04:30
Aria Moradi dc5e677a38 [SKIP CI] simplify 2021-05-07 19:05:11 +04:30
Aria Moradi a82549dc17 [SKIP CI] why a web app? 2021-05-07 19:02:56 +04:30
Aria Moradi a002e19d9d [SKIP CI] move info to CONTRIBUTING.md 2021-05-07 18:58:09 +04:30
Aria Moradi cdf1f98d28 [SKIP CI] move troubleshooting to wiki 2021-05-07 18:51:47 +04:30
Aria Moradi 0ff1ebdeb7 [SKIP CI] not alternative 2021-05-07 18:36:32 +04:30
Aria Moradi 17f4a396f8 [SKIP CI] one must know friend from foe! 2021-05-07 18:29:01 +04:30
Aria Moradi 8aa3cf4368 [SKIP CI] move stuff around 2021-05-07 18:27:10 +04:30
Aria Moradi 0136c5e493 [SKIP CI] alternative ui projects 2021-05-07 18:21:32 +04:30
Aria Moradi 8b94b9ee80 [SKIP CI] add contributing.md 2021-05-07 18:17:55 +04:30
Aria Moradi bed63f19f2 [SKIP CI] move technical info to contributing.md 2021-05-07 18:04:51 +04:30
Aria Moradi e2a6545a84 [SKIP CI] 2021-05-07 15:28:30 +04:30
Aria Moradi e3d3ec6895 fix sed output 2021-05-07 14:41:19 +04:30
Aria Moradi 7ba476bd79 include v 2021-05-07 14:39:15 +04:30
Aria Moradi 2dd41ebd27 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-07 14:25:11 +04:30
Aria Moradi 038df78171 new preview version format 2021-05-07 14:24:28 +04:30
Aria Moradi 6e5ff2b508 [SKIP CI] it be "chrome OS" 2021-05-07 13:45:31 +04:30
Aria Moradi ec8d1e8680 [SKIP CI] more housekeeping! 2021-05-07 13:42:02 +04:30
Aria Moradi 1f0f0c33b7 [SKIP CI] housekeeping 2021-05-07 13:32:42 +04:30
Aria Moradi 825940fcac [SKIP CI] add running instructions 2021-05-07 13:17:15 +04:30
Aria Moradi 4618834af2 [SKIP CI] Make it simpler 2021-05-07 11:24:40 +04:30
Aria Moradi 55d968df5e [SKIP CI] add some doc comments 2021-05-06 23:06:35 +04:30
Aria Moradi 63a078cf7d Add migrations (#76)
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-06 18:46:12 +04:30
Aria Moradi 5304917e53 fix multiple version warning 2021-05-06 14:14:04 +04:30
Aria Moradi 831b74d2ec bintray is dead 2021-05-06 13:07:40 +04:30
Aria Moradi 1bad9dcd69 [SKIP CI] 2021-05-05 15:54:05 +04:30
Aria Moradi dd43716851 [SKIP CI] more info 2021-05-05 15:26:52 +04:30
Aria Moradi f2e55e95a2 reaffirmation of what Tachidesk is for some Twitter users... 2021-05-05 15:22:46 +04:30
Aria Moradi 14658a0c4d be fancy about the manifest info 😎 2021-05-04 00:15:31 +04:30
Aria Moradi 4195e7056b fix windowsPackge not building the jar 2021-05-03 23:51:48 +04:30
Aria Moradi 1d29e8b248 fix webUI not working with gradle 7.0 2021-05-03 23:21:37 +04:30
Syer10 b718c718df Download directly to file instead of a dir (#70) 2021-05-03 23:00:18 +04:30
Aria Moradi a3601cf1b5 fix pull request build 2021-05-03 22:57:54 +04:30
Aria Moradi 0236a9639b rename vals, comments 2021-05-03 22:30:43 +04:30
Syer10 5f4c7454ee Update everything (#68)
* Update everything, cleanup build.gradle.kts's

* Make requested changes
2021-05-03 22:19:09 +04:30
Aria Moradi 773120c96a make the app build 2021-05-03 20:50:09 +04:30
Aria Moradi 4b273c6bf9 add a bit of docs 2021-05-03 20:48:29 +04:30
Aria Moradi b626aa66ba Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-03 20:41:46 +04:30
Syer10 1dd029559e Stop Javalin properly on shutdown (#69) 2021-05-03 20:40:02 +04:30
Aria Moradi 59cbe5d5bc [SKIP CI] Yes, we do use git 2021-04-30 06:15:56 +04:30
Aria Moradi 40d1173653 put DBManager where it should be 2021-04-30 06:10:41 +04:30
Aria Moradi bf6a0aba5d Rework the version endpoint 2021-04-30 06:07:29 +04:30
Kolby Moroz Liebl 34d8feacdd Add version api endpoint (#66) 2021-04-30 05:47:23 +04:30
Kolby Moroz Liebl 1ea821584c [SKIP CI] Remove IE Requirement in ps1 script (#65) 2021-04-29 01:52:24 +04:30
Aria Moradi 3d2fee19bb refactor 2021-04-28 10:25:01 +04:30
Forgenn 449d12779a Truncate manga description if it's too long (#63)
* Manga description changed from 4096 to 8192

* Check that the description of a manga is not longer than 4096, trim otherwise

* Revert description length changes
2021-04-28 09:50:56 +04:30
Aria Moradi 6fb6a251e7 [SKIP CI] latest pointer link 2021-04-18 20:52:59 +04:30
Aria Moradi 4d6220f894 dir pointer 2021-04-18 20:45:15 +04:30
Aria Moradi fe747bfc52 [SKIP CI] fix link 2021-04-18 20:14:17 +04:30
Aria Moradi 0c2d038870 [SKIP CI] add logo back 2021-04-18 20:05:58 +04:30
Aria Moradi 4e3f73af75 fix string 2021-04-18 19:58:07 +04:30
Aria Moradi 63e5e1b45f add indexer 2021-04-18 19:50:26 +04:30
Aria Moradi 2e1558bd96 [SKIP CI] fix discord links, again 2021-04-18 19:40:25 +04:30
Aria Moradi 0671dee8b2 [SKIP CI] fix discord link 2021-04-18 19:38:43 +04:30
Aria Moradi 8f91b8089a [SKIP CI] fix preview 2021-04-18 19:38:05 +04:30
Aria Moradi 009b45f676 [SKIP CI] discord? 2021-04-18 19:26:28 +04:30
Aria Moradi 8f7d5eb311 [SKIP CI] badges 2021-04-18 19:22:25 +04:30
Aria Moradi f3de835ef3 Update build_push.yml 2021-04-18 18:51:58 +04:30
Aria Moradi fd6662f428 bring back the old licenses 2021-04-18 17:48:42 +04:30
Aria Moradi fde137b3ed restore categories 2021-04-18 12:40:09 +04:30
Aria Moradi a1349aa0e3 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-04-18 11:54:44 +04:30
Aria Moradi c9ef5f9b9d lint meh 2021-04-18 11:50:02 +04:30
Aria Moradi 8fbf564177 also backup chapter and category data 2021-04-18 11:48:14 +04:30
Aria Moradi ae0b1a818c [SKIP CI] update troubleshooting guide 2021-04-15 15:57:42 +04:30
Aria Moradi c04cc780b7 update discord invite
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build FatJar (push) Failing after 17s
2021-04-15 14:15:49 +04:30
Aria Moradi 71ad1bb6e3 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-04-13 10:54:50 +04:30
Aria Moradi c1be77ee9b Merge pull request #60 from Arias800/patch-1
Fix white screen if the manga doesn't have genre.
2021-04-13 10:54:08 +04:30
Aria Moradi d1fa857ffb avoid creating the jar befre the front-end is copied 2021-04-13 10:53:23 +04:30
Aria Moradi 93fd81b38b [SKIP CI] fix the glob 2021-04-13 10:36:34 +04:30
Aria Moradi 2f116b40b2 fix chapter reading not working 2021-04-13 01:23:09 +04:30
Arias800 b884d34bdf Fix white screen if the manga doesn't have genre. 2021-04-10 19:42:05 +02:00
Aria Moradi 309803368b [SKIP CI] rename workflow 2021-04-10 10:39:47 +04:30
Aria Moradi 19fc5be8f3 [SKIP CI] rename workflow 2021-04-10 10:35:28 +04:30
Aria Moradi c28fac14c0 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-04-10 10:30:47 +04:30
Aria Moradi 66e38de29f check for deps 2021-04-10 10:28:54 +04:30
Aria Moradi 282cb1d3be [CI SKIP] account for windows 2021-04-10 10:16:24 +04:30
Aria Moradi b741ded595 [SKIP CI] improve wording 2021-04-10 10:13:56 +04:30
Aria Moradi 6b290695fc [SKIP CI] update for preview builds 2021-04-10 10:09:25 +04:30
Aria Moradi 4e43c554c0 fix triggers 2021-04-10 10:01:39 +04:30
Aria Moradi 090a72b35f rename repo to preview 2021-04-10 09:57:02 +04:30
Aria Moradi 3fcc269df3 now we can deploy repo too 2021-04-10 09:54:03 +04:30
Aria Moradi 9958e0eb34 bump version 2021-04-10 09:21:42 +04:30
Aria Moradi c5269002a2 Update README.md 2021-04-10 01:18:15 +04:30
Aria Moradi 455a35f8ae front-end UI done
Publish / Validate Gradle Wrapper (push) Successful in 12s
Publish / Build FatJar (push) Failing after 16s
2021-04-10 00:44:13 +04:30
Aria Moradi 0c79f207c3 fist version of a working backup system 2021-04-09 22:59:13 +04:30
Aria Moradi cd16d32a35 first instance of legacy import 2021-04-09 19:21:04 +04:30
Aria Moradi 1989c1eb48 add copyright notice 2021-04-09 18:18:40 +04:30
Aria Moradi f56856529f Merge pull request #55 from txtsd/master
Fix blatant typo
2021-04-06 22:27:53 +04:30
txtsd 52e27a3e39 Fix typo 2021-04-06 14:52:26 +05:30
Aria Moradi 177c971b52 This is better. 2021-04-04 03:37:00 +04:30
Aria Moradi 7a52e19235 Better way of setting it maybe? 2021-04-04 03:23:50 +04:30
Aria Moradi 5171e509a5 remove TOOD 2021-04-04 03:16:02 +04:30
Aria Moradi 975a3b1828 Merge pull request #53 from Syer10/testing_suit
Add initial testing suit
2021-04-04 03:14:27 +04:30
Syer10 c11887fada Allow rootdir to be used as a argument 2021-04-03 18:35:30 -04:00
Syer10 e043cb5690 Use properties to set rootDir so that ConfigManager can use it 2021-04-03 18:12:01 -04:00
Syer10 b2d5354798 dirs -> applicationDirs 2021-04-03 17:25:53 -04:00
Syer10 a211a4143b Revert source id to long 2021-04-03 16:57:03 -04:00
Syer10 c0df7d314b Add initial testing suit 2021-04-03 16:42:13 -04:00
Aria Moradi c8a8ce07e2 fix windows path 2021-04-04 00:52:58 +04:30
Aria Moradi e0e474dfce fixes from inspector 2021-04-03 22:40:14 +04:30
Aria Moradi 7591748811 fixes from inspector 2021-04-03 20:30:28 +04:30
Aria Moradi 884308690f fixes from inspector 2021-04-03 20:08:50 +04:30
Aria Moradi 15bd5b4b7a the new and improved apk installer 2021-04-03 19:47:31 +04:30
Aria Moradi abc3a16ee3 fix typo 2021-04-03 15:40:23 +04:30
Aria Moradi bb09ccf3c0 lint 2021-04-03 15:26:23 +04:30
Aria Moradi ad2ea8095b fixes from the inspector project 2021-04-03 15:09:48 +04:30
Aria Moradi 760d1116a1 prepare to install apk from any source 2021-04-03 13:20:14 +04:30
Aria Moradi 47fcf7eb97 export extensions 2021-04-02 18:10:02 +04:30
Aria Moradi b0e90c2f63 use the correct endpoint 2021-04-02 18:05:42 +04:30
Aria Moradi f502884fdd a partially working legacy import... 2021-04-02 17:57:29 +04:30
Aria Moradi 5ed79523d2 Move getHttpSource to util as it is a util 2021-04-02 14:17:37 +04:30
Aria Moradi da5dd70969 use future properly 2021-04-02 14:06:41 +04:30
Aria Moradi 68e69085df flatten the code 2021-04-02 13:56:38 +04:30
Aria Moradi 640ce8f5d7 add future shorthand 2021-04-02 04:09:48 +04:30
Aria Moradi c960cc1ee5 update comment 2021-04-02 03:24:12 +04:30
Aria Moradi 2b2601aa4a move coroutines to root 2021-04-02 03:14:40 +04:30
Aria Moradi 99a10ec7db improve logging 2021-04-02 03:14:19 +04:30
Aria Moradi 035105adf0 refactor and more 2021-04-02 02:56:16 +04:30
Aria Moradi f983f0e359 Merge pull request #46 from Syer10/future
Implement coroutines
2021-04-02 02:53:59 +04:30
Syer10 769472b24c Implement coroutines 2021-04-01 16:07:35 -04:00
Aria Moradi 8c80ad7575 fix time between extensions list checks 2021-04-01 23:39:00 +04:30
Aria Moradi 63db2e6695 fix HNI-Scantard not installing correctly 2021-04-01 23:34:52 +04:30
Aria Moradi d6d5e97fbd Merge pull request #45 from Syer10/getRepo
Fix getRepo function
2021-04-01 23:15:45 +04:30
Syer10 1ae0a8326e Fix getRepo function 2021-04-01 14:42:13 -04:00
Aria Moradi 57693fef7b clean up 2021-03-30 21:48:05 +04:30
Aria Moradi 5656016700 let's not polute the namespace together 2021-03-30 21:10:41 +04:30
Aria Moradi 90ae180b3e let's not polute the namespace 2021-03-30 21:04:06 +04:30
Aria Moradi 2a3c78d43e refactor 2021-03-30 20:49:54 +04:30
Aria Moradi 11000af718 get gz insead of big big json 2021-03-30 20:41:20 +04:30
Aria Moradi b808121f1d Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-03-30 20:39:56 +04:30
Aria Moradi addadefeb1 some refactor and comments 2021-03-30 20:39:40 +04:30
Aria Moradi 838cd20e57 new build scripts 2021-03-30 20:21:58 +04:30
Aria Moradi 5b9219522d separate jar task from webUI copy task 2021-03-30 20:14:25 +04:30
Aria Moradi caeb4d273d refactor 2021-03-30 17:34:15 +04:30
Aria Moradi 77cf87c989 refactor 2021-03-30 17:21:41 +04:30
Aria Moradi 50c2dbed5d refactor 2021-03-30 16:52:10 +04:30
Aria Moradi 71a9396952 starts of legacy backup support 2021-03-30 01:18:57 +04:30
Aria Moradi bc3ad75328 finished Update support: webUI side 2021-03-29 02:47:51 +04:30
Aria Moradi 077bbc3c38 refactor & support for extension update: Backend 2021-03-29 00:35:58 +04:30
Aria Moradi b1b1abad1d refactor proxy 2021-03-28 20:41:39 +04:30
Aria Moradi e6ba2a0066 bump version
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 16s
2021-03-28 13:53:03 +04:30
Aria Moradi 9b56ef7d82 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-03-28 13:50:33 +04:30
Aria Moradi dd442c6653 fix extensions not being installed correctly 2021-03-28 13:40:18 +04:30
Aria Moradi 3044317b09 Update README.md 2021-03-28 06:00:34 +04:30
Aria Moradi 3a1e1e01dc continues gap switch
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 16s
2021-03-28 04:03:46 +04:30
Aria Moradi a567701639 chapter image fix for bato.to 2021-03-28 03:43:52 +04:30
Aria Moradi 1802271358 fix chapters not being loaded correctly 2021-03-28 03:19:01 +04:30
Aria Moradi 9e649eef79 fix a bug where add to library didn't work 2021-03-28 02:43:09 +04:30
Aria Moradi 1eb4a9c216 use logger to print exception 2021-03-28 02:11:40 +04:30
Aria Moradi e3f65d2192 fix URI exception 2021-03-28 01:35:46 +04:30
Aria Moradi bb09cfddb3 Migrate from Oracle Nashorn to Mozilla's Rhino 2021-03-28 01:15:50 +04:30
Aria Moradi d383939c9f improve loading spinner's place 2021-03-28 01:12:32 +04:30
Aria Moradi 32dd543562 Loading placeholder & css fixes 2021-03-27 22:21:39 +04:30
Aria Moradi 5a75f26791 even better logging messages 2021-03-27 19:40:47 +04:30
Aria Moradi 95c437efd5 better logging 2021-03-27 18:53:29 +04:30
Aria Moradi ec877f632f refactor & config improvments 2021-03-27 18:30:36 +04:30
Aria Moradi 8666cbf8bc lint some files 2021-03-26 12:15:14 +04:30
Aria Moradi 84b0c26450 change the copyright owner to Contributors to the Suwayomi project 2021-03-26 04:17:02 +04:30
Aria Moradi 64e5bbabb3 Update enters 2021-03-26 02:40:20 +04:30
Aria Moradi cc1a15e5ba Update enters 2021-03-26 02:39:54 +04:30
Aria Moradi d29e942a72 Update getAndroid.sh 2021-03-26 02:33:24 +04:30
Aria Moradi 8d86c88c38 Update getAndroid.ps1 2021-03-26 02:33:03 +04:30
Aria Moradi c7dc7421aa Update getAndroid.sh 2021-03-26 02:32:51 +04:30
Aria Moradi 34ed3e5c68 Update getAndroid.ps1 2021-03-26 02:29:31 +04:30
Aria Moradi 1a4a8af384 Merge pull request #42 from Syer10/getandroid
getAndroid.ps1: Silently continue if it cant remove the tmp folder
2021-03-26 02:21:43 +04:30
Aria Moradi 62b1e99bbf get the right dex2jar package please 2021-03-26 02:07:00 +04:30
Syer10 1aa3b76934 getAndroid.ps1: Silently continue if it cant remove the tmp folder 2021-03-25 16:47:30 -04:00
Aria Moradi 3e53c50f64 better spelling 2021-03-25 18:32:31 +04:30
Aria Moradi 430386bc84 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-03-25 18:21:57 +04:30
Aria Moradi 30049e8152 Update README.md 2021-03-25 18:21:14 +04:30
Aria Moradi 34d9a7a233 better use of dex2jar, get dex2jar from jitpack 2021-03-25 18:03:32 +04:30
Aria Moradi 183972475b better loging 2021-03-25 16:53:36 +04:30
Aria Moradi fd46727f8e Merge pull request #40 from Syer10/Powerhsell
Create getAndroid.ps1
2021-03-25 05:07:03 +04:30
Syer10 f6ce010aa2 Update readme 2021-03-24 20:34:32 -04:00
Syer10 d0ff30df9f Create getAndroid.ps1 2021-03-24 20:28:34 -04:00
Aria Moradi 8e449abd67 Merge pull request #39 from Syer10/LF
Standardize Line endings
2021-03-25 04:53:22 +04:30
Syer10 2986130268 Standardize Line endings 2021-03-24 19:51:18 -04:00
Aria Moradi 1c0c09f2f2 add server Desktop Environment 2021-03-25 04:03:48 +04:30
Aria Moradi 44100cb5b6 closes #37 2021-03-25 01:03:51 +04:30
Aria Moradi cfc6e5cd2a Fix for jip's Arch Linux 2021-03-24 19:28:27 +04:30
Aria Moradi c067d14c2c Update README.md 2021-03-24 19:26:37 +04:30
Aria Moradi aded854a2b Update README.md 2021-03-24 19:24:50 +04:30
Aria Moradi e79d0b9dd2 search loading message 2021-03-24 03:31:38 +04:30
Aria Moradi a0115d88b0 fix search bugs 2021-03-24 03:28:02 +04:30
Aria Moradi 85ec2ed367 drawer hide on click outside of it 2021-03-23 04:28:23 +04:30
Aria Moradi bf908c4d17 chapter prev/next UI+Backend 2021-03-23 03:50:55 +04:30
Aria Moradi f41c5c9428 bump version
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 16s
2021-03-19 14:55:48 +03:30
Aria Moradi 04837983fa reader ui changes 2021-03-19 14:52:20 +03:30
Aria Moradi 5d484b012c new layout for manga page for >md 2021-03-18 22:55:17 +03:30
Aria Moradi 436a8d0585 improvments on the reader 2021-03-18 21:46:24 +03:30
Aria Moradi 28cc0a6f84 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-03-17 19:22:44 +03:30
Aria Moradi 26cc2f2c96 MangaDetails component improved drastically 2021-03-17 19:17:03 +03:30
Aria Moradi 149107e749 fix material error 2021-03-16 23:42:51 +03:30
Aria Moradi a74936c5f5 Update README.md 2021-03-16 23:24:14 +03:30
Aria Moradi ff8c8913d4 Update README.md 2021-03-16 23:14:36 +03:30
Aria Moradi 83426e1302 Update README.md 2021-03-16 23:13:44 +03:30
Aria Moradi 9cd93d467c bump version
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 17s
2021-03-16 16:15:20 +03:30
Aria Moradi 257f8a5a27 fix extensions not showing the all pesudo-language 2021-03-16 16:10:06 +03:30
Aria Moradi 79bab08cae improvements 2021-03-16 16:04:29 +03:30
Aria Moradi 4e699e4f5a update dex2jar 2021-03-16 15:44:50 +03:30
Aria Moradi 1128f40bac closes #32 2021-03-14 23:57:33 +03:30
Aria Moradi 53ef836326 fix windows path 2021-03-14 20:28:23 +03:30
Aria Moradi b8df0e89e5 Don't show installed if nothing is installed 2021-03-14 14:09:31 +03:30
Aria Moradi 472bfec6bf improve docs 2021-03-14 01:26:52 +03:30
Aria Moradi c1b86cedd2 move getAndroid.sh 2021-03-14 01:02:43 +03:30
Aria Moradi 428c65f075 Enforce more limits on the issue format. 2021-03-13 22:59:37 +03:30
Aria Moradi 92ed48f7f6 bump version to v0.2.4
Publish / Validate Gradle Wrapper (push) Successful in 12s
Publish / Build FatJar (push) Failing after 15s
2021-03-13 11:08:39 +03:30
Aria Moradi 13e84bc492 Maskable icons 2021-03-13 11:06:22 +03:30
Aria Moradi 0ef86c34b7 server configuration fam 2021-03-11 14:43:29 +03:30
Aria Moradi 7e1a4259d7 fix langs not showing correctly 2021-03-09 18:05:34 +03:30
Aria Moradi c842c51fb6 section sources by lang 2021-03-09 16:44:09 +03:30
Aria Moradi 6f2f228e08 section extension languages 2021-03-08 21:04:42 +03:30
Aria Moradi c78eaa8b96 add issue closer 2021-03-08 13:47:58 +03:30
Aria Moradi f9606526d2 add issue closer 2021-03-08 13:39:25 +03:30
Aria Moradi fe4cc9ea2c add issue closer 2021-03-08 13:32:17 +03:30
Aria Moradi 54d0c05fcc add issue closer 2021-03-08 13:31:03 +03:30
Aria Moradi 2f7df73a37 add issue closer 2021-03-08 13:22:44 +03:30
Aria Moradi cf19f3626b improve text 2021-03-08 13:01:23 +03:30
Aria Moradi ff2da5e59b issue template 2021-03-08 12:57:12 +03:30
Aria Moradi e03922e518 fix PWA icons 2021-03-07 23:08:30 +03:30
Aria Moradi 893fba5b8c fix image urls 2021-03-07 22:35:27 +03:30
Aria Moradi c1786f8e24 migrate to axios, front-end part of configurable ServerAddress 2021-03-07 22:25:29 +03:30
Aria Moradi a59f974537 fix #25 2021-03-07 22:12:38 +03:30
Aria Moradi 7157e07328 better messages, axios client 2021-03-07 16:27:13 +03:30
Aria Moradi 954084bd82 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-03-07 10:51:24 +03:30
Aria Moradi 0915ba40f6 🤌 Tachidesk's logo! 2021-02-25 21:54:49 +03:30
Aria Moradi de30d55bcf darkTheme in localStorage 2021-02-25 14:38:16 +03:30
Aria Moradi af1c34fba5 v0.2.3
Publish / Validate Gradle Wrapper (push) Successful in 12s
Publish / Build FatJar (push) Failing after 16s
2021-02-24 12:27:28 +03:30
Aria Moradi 7b7d93786f Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-24 12:09:39 +03:30
Aria Moradi 7c1c504482 new icon, fix headless systems crashing 2021-02-24 11:55:43 +03:30
Aria Moradi 33b22fcab6 Update README.md 2021-02-22 14:54:04 +03:30
Aria Moradi ab0566dcba Update README.md 2021-02-22 14:51:39 +03:30
Aria Moradi c4f2cc7189 Update README.md 2021-02-22 14:49:17 +03:30
Aria Moradi 4626d99590 Update README.md 2021-02-22 14:48:17 +03:30
Aria Moradi 6465ca8a19 Update README.md 2021-02-22 01:29:55 +03:30
Aria Moradi 15b9d151df Update README.md 2021-02-22 01:28:13 +03:30
Aria Moradi dd1b6c86cd Update README.md 2021-02-22 01:23:44 +03:30
Aria Moradi 9613cda79a new icons by @as280093 2021-02-21 23:37:11 +03:30
Aria Moradi 648b8e5960 bump version: v0.2.2
Publish / Validate Gradle Wrapper (push) Successful in 15s
Publish / Build FatJar (push) Failing after 16s
2021-02-21 04:42:33 +03:30
Aria Moradi ce545b1fd5 fix some bugs 2021-02-21 04:41:56 +03:30
Aria Moradi 9151034fbc category done! 2021-02-21 04:27:41 +03:30
Aria Moradi 312a8baa13 hide menu button for now 2021-02-20 02:59:32 +03:30
Aria Moradi 18b6168cd1 theme select in settings 2021-02-20 02:57:52 +03:30
Aria Moradi 9a282c3bf4 redirect / to library 2021-02-20 02:41:30 +03:30
Aria Moradi 2bbebe4c30 fix removing manga from library not working 2021-02-20 02:34:26 +03:30
Aria Moradi 162961b560 fix tabs 2021-02-20 02:28:55 +03:30
Aria Moradi f1cc37d0db finished the category screen 2021-02-20 01:23:52 +03:30
Aria Moradi 5a9d216fb7 bump version
Publish / Validate Gradle Wrapper (push) Successful in 10s
Publish / Build FatJar (push) Failing after 2m8s
2021-02-14 23:18:14 +03:30
Aria Moradi bf37d3be7c fix syntax 2021-02-14 22:51:22 +03:30
Aria Moradi 7fd57aaed8 try new release action 2021-02-14 22:49:40 +03:30
Aria Moradi d996c44b24 try publish wiht draft 2021-02-14 22:20:50 +03:30
Aria Moradi 6f3052dd1b category backend 2021-02-14 01:10:43 +03:30
Aria Moradi d2b1bfdcdd Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-13 22:45:18 +03:30
Aria Moradi 945fb99594 Update README.md 2021-02-13 21:25:49 +03:30
Aria Moradi 09d624a4e2 add library 2021-02-13 21:12:18 +03:30
Aria Moradi eb90db7ce6 Update README.md 2021-02-13 17:18:31 +03:30
Aria Moradi b56f9391b8 Update README.md 2021-02-13 17:07:39 +03:30
Aria Moradi c181478909 Update README.md 2021-02-13 17:06:37 +03:30
Aria Moradi 76b31e734c Update README.md 2021-02-13 17:06:16 +03:30
Aria Moradi ed8bd76d95 dummy file to trigger actions 2021-02-13 15:34:17 +03:30
Aria Moradi 3051a72d7f add node_modules cache 2021-02-13 15:30:15 +03:30
Aria Moradi 3a33bf3a5d just download android.jar to improve build time 2021-02-13 15:18:57 +03:30
Aria Moradi 7959ba2664 [RELEASE CI] test new release 2021-02-13 14:50:46 +03:30
Aria Moradi fe6568b82c [RELEASE CI] test new release 2021-02-13 14:39:16 +03:30
Aria Moradi c228648bb6 [RELEASE CI] test new release 2021-02-13 14:15:38 +03:30
Aria Moradi fdaeb6d1fa [RELEASE CI] test new release 2021-02-13 14:01:01 +03:30
Aria Moradi ba45e18399 [RELEASE CI] test new release 2021-02-13 13:39:52 +03:30
Aria Moradi 3e2bf877d4 [RELEASE CI] test new release 2021-02-13 13:32:59 +03:30
Aria Moradi c80d344046 [RELEASE CI] test new release 2021-02-13 13:21:13 +03:30
Aria Moradi 2364f10d8d [RELEASE CI] test new release 2021-02-13 13:13:15 +03:30
Aria Moradi 2602275c20 [RELEASE CI] test new release 2021-02-13 13:12:40 +03:30
Aria Moradi d113311f4e [RELEASE CI] test new release 2021-02-13 12:57:01 +03:30
Aria Moradi 8d95701e8e [RELEASE CI] test new release 2021-02-13 12:55:57 +03:30
Aria Moradi 0d2c54a5ed [RELEASE CI] test new release 2021-02-13 12:54:36 +03:30
Aria Moradi 6506c84b85 publish? 2021-02-08 05:36:19 +03:30
Aria Moradi 69bb38b487 [CI RELEASE] do it 2021-02-08 05:12:13 +03:30
Aria Moradi 95e17f2b50 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-08 05:11:48 +03:30
Aria Moradi 9625da9221 [RLEASE CI] add upload release binaries action 2021-02-08 05:11:21 +03:30
Aria Moradi c1659f1cf2 refactor, add todos for library and category 2021-02-06 18:48:59 +03:30
Aria Moradi c46ee764ac Update README.md 2021-02-05 11:47:17 +03:30
Aria Moradi 7aada85f76 Update README.md 2021-02-05 11:46:29 +03:30
Aria Moradi 145cbe3e4f Update README.md 2021-02-05 11:45:56 +03:30
Aria Moradi cb8dd8259d Update README.md 2021-02-05 11:44:24 +03:30
Aria Moradi b8e721fd27 Update README.md 2021-02-05 01:48:59 +03:30
Aria Moradi 7917b5384c Update README.md 2021-02-05 01:17:03 +03:30
Aria Moradi 087b7554bf cleanup 2021-02-05 01:09:11 +03:30
Aria Moradi fb5f851a2a Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-05 00:57:16 +03:30
Aria Moradi 7ac51f8c2a Update README.md 2021-02-05 00:50:56 +03:30
Aria Moradi e5e40a986c Update README.md 2021-02-05 00:46:46 +03:30
Aria Moradi 7a27436868 now done with lfs track 2021-02-05 00:20:25 +03:30
Aria Moradi a5bab7425d [CI RELEASE] try lfs fix 2021-02-05 00:11:20 +03:30
Aria Moradi 93d5ab3739 [CI RELEASE] v.0.2.0 2021-02-04 23:55:01 +03:30
Aria Moradi 3146fefb55 change build scripts 2021-02-04 23:47:16 +03:30
Aria Moradi 1ea51bb9df add launch4j 2021-02-04 23:40:40 +03:30
Aria Moradi 98bd664ab6 Tray Icon 2021-02-04 18:02:46 +03:30
Aria Moradi 61aee2e784 hint added 2021-02-04 18:02:34 +03:30
Aria Moradi 22bf49078f cached response for source list iconUrl 2021-02-04 14:53:34 +03:30
Aria Moradi 7284e0d4ae cached extension icon 2021-02-04 14:47:27 +03:30
Aria Moradi d39d075b1a [CI RELEASE] dummy file to trigger CI 2021-02-04 04:55:36 +03:30
Aria Moradi 0f6749b0c1 now support backward writing! 2021-02-04 04:48:15 +03:30
209 changed files with 10738 additions and 4029 deletions
+26 -5
View File
@@ -1,6 +1,27 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
* text=auto
* text eol=lf
# Windows forced line-endings
/.idea/* text eol=crlf
*.bat text eol=crlf
*.ps1 text eol=crlf
# Gradle wrapper
*.jar binary
# Images
*.webp binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.gz binary
*.zip binary
*.7z binary
*.ttf binary
*.eot binary
*.woff binary
*.pyc binary
*.swp binary
*.pdf binary
+44
View File
@@ -0,0 +1,44 @@
---
name: "🐞 Bug report"
title: "[Bug] <short description>"
about: "Report a bug"
labels: "bug"
---
**PLEASE READ THIS**
I acknowledge that:
- I have updated to the latest version of the app.
- I have tried the troubleshooting guide described in `README.md`
- If this is a request for adding/changing an extension it should be brought up to Tachiyomi: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
- If this is an issue with some extension not working properly, It does work inside Tachiyomi as intended.
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## Device information
- Tachidesk version: (Example: v0.2.3-r255-win32)
- Server Operating System: (Example: Ubuntu 20.04)
- Server Desktop Environment: N/A or (Example: Gnome 40)
- Server JVM version: bundled with win32 or (Example: Java 8 Update 281 or OpenJDK 8u281)
- Client Operating System: <usually the same as above Server Operating System>
- Client Web Browser: (Example: Google Chrome 89.0.4389.82)
## Steps to reproduce
1. First Step
2. Second Step
### Expected behavior
Describe what should have happened. Remove this line after you are done.
### Actual behavior
Describe what happens instead. Remove this line after you are done.
## Other details
Describe additional details If necessary. Remove this line after you are done.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+29
View File
@@ -0,0 +1,29 @@
---
name: "🌟 Feature request"
title: "[Feature Request] <short description>"
about: "Suggest a feature to improve the project"
labels: "enhancement"
---
**PLEASE READ THIS**
I acknowledge that:
- I have updated to the latest version of the app.
- I have tried the troubleshooting guide described in `README.md`
- If this is a request for adding/changing an extension it should be brought up to Tachiyomi: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
- If this is an issue with some extension not working properly, It does work in Tachiyomi application as intended.
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## What feature should be added to Tachidesk?
Explain What the feature is and how it should work in detail. Remove this line after you are done.
## Why/Project's Benefit/Existing Problem
Explain why this should be added. Remove this line after you are done.
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
cp master/server/build/Tachidesk-*.jar preview
cd preview
new_jar_build=$(ls *.jar| tail -1)
echo "last jar build file name: $new_jar_build"
cp -f $new_jar_build Tachidesk-latest.jar
rm -rf latest_pointer/*
cp $new_jar_build latest_pointer
cp ../master/server/build/Tachidesk-*.zip latest_pointer
latest=$(ls *.jar | tail -n1 | sed -e's/Tachidesk-\|.jar//g')
echo "{ \"latest\": \"$latest\" }" > index.json
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git status
if [ -n "$(git status --porcelain)" ]; then
git add .
git commit -m "Update preview repository"
git push
else
echo "No changes to commit"
fi
-18
View File
@@ -1,18 +0,0 @@
#!/bin/bash
cp ../master/repo/* .
new_build=$(ls | tail -1)
echo "New build file name: $new_build"
cp -f $new_build Tachidesk-latest.jar
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git status
if [ -n "$(git status --porcelain)" ]; then
git add .
git commit -m "Update repo"
git push
else
echo "No changes to commit"
fi
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
# Get last commit message
last_commit_log=$(git log -1 --pretty=format:"%s")
echo "last commit log: $last_commit_log"
filter_count=$(echo "$last_commit_log" | grep -c '\[RELEASE CI\]' )
echo "count is: $filter_count"
if [ "$filter_count" -gt 0 ]; then
mkdir -p repo/
cp server/build/Tachidesk-*.jar repo/
fi
+68
View File
@@ -0,0 +1,68 @@
name: CI Pull Request
on:
pull_request:
jobs:
check_wrapper:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v2
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
build:
name: Build FatJar
needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest
steps:
- name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.5.0
with:
access_token: ${{ github.token }}
- name: Checkout master branch
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
path: master
fetch-depth: 0
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Copy CI gradle.properties
run: |
cd master
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download android.jar
run: |
cd master
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Cache node_modules
uses: actions/cache@v2
with:
path: |
**/react/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
- name: Build and copy webUI, Build Jar and launch4j
uses: eskatos/gradle-command-action@v1
with:
build-root-directory: master
wrapper-directory: master
arguments: :webUI:copyBuild :server:windowsPackage --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
@@ -1,10 +1,9 @@
name: CI
name: CI build
on:
push:
branches:
- master
pull_request:
jobs:
check_wrapper:
@@ -48,37 +47,39 @@ jobs:
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download and process android.jar
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
- name: Download android.jar
run: |
cd master
./scripts/getAndroid.sh
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Build the Jar
- name: Cache node_modules
uses: actions/cache@v2
with:
path: |
**/react/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
- name: Build and copy webUI, Build Jar
uses: eskatos/gradle-command-action@v1
with:
build-root-directory: master
wrapper-directory: master
arguments: :server:shadowJar --stacktrace
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
- name: Create repo artifacts
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
- name: make windows package
run: |
cd master
./.github/scripts/create-repo.sh
cd master/scripts
./windows-bundler.sh
- name: Checkout repo branch
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
- name: Checkout preview branch
uses: actions/checkout@v2
with:
ref: repo
path: repo
ref: preview
path: preview
- name: Deploy repo
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
- name: Deploy preview
run: |
cd repo
../master/.github/scripts/commit-repo.sh
./master/.github/scripts/commit-preview.sh
+37
View File
@@ -0,0 +1,37 @@
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.0
with:
repo-token: ${{ github.token }}
rules: |
[
{
"type": "title",
"regex": ".*<short description>*",
"message": "You did not fill out the description in the title"
},
{
"type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
"message": "The acknowledgment section was not removed"
},
{
"type": "body",
"regex": "(Tachidesk version|Server Operating System|Server Desktop Environment|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
"message": "The requested information was not filled out"
},
{
"type": "body",
"regex": ".*Remove this line after you are done.*",
"message": "The lines requesting to be removed were not removed."
}
]
+84
View File
@@ -0,0 +1,84 @@
name: CI Publish
on:
push:
tags:
- 'v*'
jobs:
check_wrapper:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v2
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
build:
name: Build FatJar
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: Checkout master branch
uses: actions/checkout@v2
with:
ref: master
path: master
fetch-depth: 0
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Copy CI gradle.properties
run: |
cd master
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download android.jar
run: |
cd master
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Cache node_modules
uses: actions/cache@v2
with:
path: |
**/react/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
- name: Build and copy webUI, Build Jar
uses: eskatos/gradle-command-action@v1
with:
build-root-directory: master
wrapper-directory: master
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
- name: make windows package
run: |
cd master/scripts
./windows-bundler.sh
- name: Upload Release
uses: xresloader/upload-to-github-release@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
file: "master/server/build/*.jar;master/server/build/*.zip"
tags: true
draft: true
verbose: true
+3
View File
@@ -1,8 +1,11 @@
# Ignore Gradle project-specific cache directory
.gradle
.idea
gradle.properties
# Ignore Gradle build output directory
build
server/src/main/resources/react
server/tmp/
server/tachiserver-data/
@@ -1,4 +1,4 @@
dependencies {
// Config API
// Config API, moved to the global build.gradle
// implementation("com.typesafe:config:1.4.0")
}
@@ -0,0 +1,18 @@
package xyz.nulldev.ts.config
import net.harawata.appdirs.AppDirsFactory
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
val ApplicationRootDir: String
get(): String {
return System.getProperty(
"ir.armor.tachidesk.rootDir",
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
)
}
@@ -1,5 +1,12 @@
package xyz.nulldev.ts.config
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
@@ -10,48 +17,44 @@ import java.io.File
* Manages app config.
*/
open class ConfigManager {
private val generatedModules
= mutableMapOf<Class<out ConfigModule>, ConfigModule>()
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
val config by lazy { loadConfigs() }
//Public read-only view of modules
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
get() = generatedModules
open val configFolder: String
get() = System.getProperty("compat-configdirs") ?: "tachiserver-data/config"
val logger = KotlinLogging.logger {}
/**
* Get a config module
*/
inline fun <reified T : ConfigModule> module(): T
= loadedModules[T::class.java] as T
inline fun <reified T : ConfigModule> module(): T = loadedModules[T::class.java] as T
/**
* Get a config module (Java API)
*/
fun <T : ConfigModule> module(type: Class<T>): T
= loadedModules[type] as T
fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
/**
* Load configs
*/
fun loadConfigs(): Config {
val configs = mutableListOf<Config>()
//Load reference configs
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
//Load reference config
configs += ConfigFactory.parseResources("reference.conf")
//Load user config
val userConfig =
File(ApplicationRootDir, "server.conf").let {
ConfigFactory.parseFile(it)
}
//Load custom configs from dir
File(configFolder).listFiles()?.map {
ConfigFactory.parseFile(it)
}?.filterNotNull()?.forEach {
configs += it.withFallback(configs.last())
}
val config = configs.last().resolve()
val config = ConfigFactory.empty()
.withFallback(userConfig)
.withFallback(compatConfig)
.withFallback(serverConfig)
.resolve()
logger.debug {
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
@@ -61,7 +64,7 @@ open class ConfigManager {
}
fun registerModule(module: ConfigModule) {
generatedModules.put(module.javaClass, module)
generatedModules[module.javaClass] = module
}
fun registerModules(vararg modules: ConfigModule) {
@@ -1,35 +0,0 @@
package xyz.nulldev.ts.config
import com.typesafe.config.Config
import java.io.File
class ServerConfig(config: Config) : ConfigModule(config) {
val ip = config.getString("ip")
val port = config.getInt("port")
val allowConfigChanges = config.getBoolean("allowConfigChanges")
val enableWebUi = config.getBoolean("enableWebUi")
val useOldWebUi = config.getBoolean("useOldWebUi")
val prettyPrintApi = config.getBoolean("prettyPrintApi")
// TODO Apply to operation IDs
val disabledApiEndpoints = config.getStringList("disabledApiEndpoints").map(String::toLowerCase)
val enabledApiEndpoints = config.getStringList("enabledApiEndpoints").map(String::toLowerCase)
val httpInitializedPrintMessage = config.getString("httpInitializedPrintMessage")
val useExternalStaticFiles = config.getBoolean("useExternalStaticFiles")
val externalStaticFilesFolder = config.getString("externalStaticFilesFolder")
val rootDir = registerFile(config.getString("rootDir"))
val patchesDir = registerFile(config.getString("patchesDir"))
fun registerFile(file: String): File {
return File(file).apply {
mkdirs()
}
}
companion object {
fun register(config: Config)
= ServerConfig(config.getConfig("ts.server"))
}
}
+6 -16
View File
@@ -6,7 +6,6 @@ plugins {
repositories {
mavenCentral()
jcenter()
maven {
url = uri("https://jitpack.io")
}
@@ -18,9 +17,7 @@ repositories {
dependencies {
// Android stub library
// compileOnly( fileTree(File(rootProject.rootDir, "libs/android"), include: "*.jar")
implementation(fileTree("lib/"))
implementation(fileTree("${rootProject.rootDir}/server/lib/dex2jar/"))
// Android JAR libs
@@ -32,22 +29,11 @@ dependencies {
// Javassist
compileOnly( "org.javassist:javassist:3.27.0-GA")
// Coroutines
val kotlinx_coroutines_version = "1.4.2"
compileOnly( "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version")
compileOnly( "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlinx_coroutines_version")
// XML
compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
// Config API
implementation( project(":AndroidCompat:Config"))
// dex2jar
// compileOnly( "dex2jar:dex-translator")
// APK parser
compileOnly("net.dongliu:apk-parser:2.6.10")
implementation(project(":AndroidCompat:Config"))
// APK sig verifier
compileOnly("com.android.tools.build:apksig:4.2.0-alpha13")
@@ -55,7 +41,11 @@ dependencies {
// AndroidX annotations
compileOnly( "androidx.annotation:annotation:1.2.0-alpha01")
// compileOnly("io.reactivex:rxjava:1.3.8")
// substitute for duktape-android
// 'org.mozilla:rhino' includes some code that we don't need so use 'org.mozilla:rhino-runtime' instead
implementation("org.mozilla:rhino-runtime:1.7.13")
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
implementation("org.mozilla:rhino-engine:1.7.13")
}
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
+99
View File
@@ -0,0 +1,99 @@
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# This is a windows only PowerShell script to create android.jar stubs
# foolproof against running from AndroidCompat dir instead of running from project root
if ($(Split-Path -Path (Get-Location) -Leaf) -eq "AndroidCompat" ) {
Set-Location ..
}
Write-Output "Getting required Android.jar..."
Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" -UseBasicParsing).content
$android_jar = (Get-Location).Path + "\tmp\android.jar"
[IO.File]::WriteAllBytes($android_jar, [Convert]::FromBase64String($androidEncoded))
# We need to remove any stub classes that we have implementations for
Write-Output "Patching JAR..."
function Remove-Files-Zip($zipfile, $path)
{
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression') | Out-Null
$stream = New-Object IO.FileStream($zipfile, [IO.FileMode]::Open)
$mode = [IO.Compression.ZipArchiveMode]::Update
$zip = New-Object IO.Compression.ZipArchive($stream, $mode)
($zip.Entries | Where-Object { $_.FullName -like $path }) | ForEach-Object { Write-Output "Deleting: $($_.FullName)"; $_.Delete() }
$zip.Dispose()
$stream.Close()
$stream.Dispose()
}
Write-Output "Removing org.json..."
Remove-Files-Zip $android_jar 'org/json/*'
Write-Output "Removing org.apache..."
Remove-Files-Zip $android_jar 'org/apache/*'
Write-Output "Removing org.w3c..."
Remove-Files-Zip $android_jar 'org/w3c/*'
Write-Output "Removing org.xml..."
Remove-Files-Zip $android_jar 'org/xml/*'
Write-Output "Removing org.xmlpull..."
Remove-Files-Zip $android_jar 'org/xmlpull/*'
Write-Output "Removing junit..."
Remove-Files-Zip $android_jar 'junit/*'
Write-Output "Removing javax..."
Remove-Files-Zip $android_jar 'javax/*'
Write-Output "Removing java..."
Remove-Files-Zip $android_jar 'java/*'
Write-Output "Removing overriden classes..."
Remove-Files-Zip $android_jar 'android/app/Application.class'
Remove-Files-Zip $android_jar 'android/app/Service.class'
Remove-Files-Zip $android_jar 'android/net/Uri.class'
Remove-Files-Zip $android_jar 'android/net/Uri$Builder.class'
Remove-Files-Zip $android_jar 'android/os/Environment.class'
Remove-Files-Zip $android_jar 'android/text/format/Formatter.class'
Remove-Files-Zip $android_jar 'android/text/Html.class'
function Dedupe($path)
{
Push-Location $path
$classes = Get-ChildItem . *.* -Recurse | Where-Object { !$_.PSIsContainer }
$classes | ForEach-Object {
"Processing class: $($_.FullName)"
Remove-Files-Zip $android_jar "$($_.Name).class" | Out-Null
Remove-Files-Zip $android_jar "$($_.Name)$*.class" | Out-Null
Remove-Files-Zip $android_jar "$($_.Name)Kt.class" | Out-Null
Remove-Files-Zip $android_jar "$($_.Name)Kt$*.class" | Out-Null
}
Pop-Location
}
Dedupe "AndroidCompat/src/main/java"
Dedupe "server/src/main/java"
Dedupe "server/src/main/kotlin"
Write-Output "Copying Android.jar to library folder..."
Move-Item -Force $android_jar "AndroidCompat/lib/android.jar"
Write-Output "Cleaning up..."
Remove-Item -Recurse -Force "tmp"
Write-Output "Done!"
@@ -1,4 +1,30 @@
#!/usr/bin/env bash
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# This is a bash script to create android.jar stubs
for dep in "curl" "base64" "zip"
do
which $dep >/dev/null 2>&1 || { echo >&2 "Error: This script needs $dep installed."; abort=yes; }
done
if [ $abort = yes ]; then
echo "Some of the dependencies didn't exist. Aborting."
exit 1
fi
# foolproof against running from AndroidCompat dir instead of running from project root
if [ "$(basename $(pwd))" = "AndroidCompat" ]; then
cd ..
fi
echo "Getting required Android.jar..."
rm -rf "tmp"
mkdir -p "tmp"
@@ -6,7 +32,7 @@ pushd "tmp"
curl "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" | base64 --decode > android.jar
# We need to remove any stub classes that we might use
# We need to remove any stub classes that we have implementations for
echo "Patching JAR..."
echo "Removing org.json..."
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.duktape;
import kotlin.NotImplementedError;
@@ -22,11 +23,18 @@ import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.io.Closeable;
/** A simple EMCAScript (Javascript) interpreter. */
/* Note (March 2021):
* The old implementation for duktape-android used the nashorn engine which is deprecated.
* This new implementation uses Mozilla's Rhino: https://github.com/mozilla/rhino
*/
/**
* A simple EMCAScript (Javascript) interpreter.
*/
public final class Duktape implements Closeable, AutoCloseable {
private ScriptEngineManager factory = new ScriptEngineManager();
private ScriptEngine engine = factory.getEngineByName("JavaScript");
private ScriptEngine engine = factory.getEngineByName("rhino");
/**
* Create a new interpreter instance. Calls to this method <strong>must</strong> matched with
@@ -38,17 +46,6 @@ public final class Duktape implements Closeable, AutoCloseable {
private Duktape() {}
/**
* Evaluate {@code script} and return a result. {@code fileName} will be used in error
* reporting. Note that the result must be one of the supported Java types or the call will
* return null.
*
* @throws DuktapeException if there is an error evaluating the script.
*/
public synchronized Object evaluate(String script, String fileName) {
throw new NotImplementedError("Not implemented!");
}
/**
* Evaluate {@code script} and return a result. Note that the result must be one of the
* supported Java types or the call will return null.
@@ -0,0 +1,30 @@
package com.squareup.duktape;
/* part of tachiyomi-extensions which is licensed under Apache License Version 2.0 */
import java.io.Closeable;
import java.io.IOException;
/** This is the reference Duktape stub that tachiyomi's extensions depend on.
* Intended to be used as a reference.
*/
public class DuktapeStub implements Closeable {
public static Duktape create() {
throw new RuntimeException("Stub!");
}
@Override
public synchronized void close() throws IOException {
throw new RuntimeException("Stub!");
}
public synchronized Object evaluate(String script) {
throw new RuntimeException("Stub!");
}
public synchronized <T> void set(String name, Class<T> type, T object) {
throw new RuntimeException("Stub!");
}
}
@@ -22,7 +22,7 @@ data class InstalledPackage(val root: File) {
val icon = File(root, "icon.png")
val info: PackageInfo
get() = ApkParsers.getMetaInfo(apk).toPackageInfo(root, apk).also {
get() = ApkParsers.getMetaInfo(apk).toPackageInfo(apk).also {
val parsed = ApkFile(apk)
val dbFactory = DocumentBuilderFactory.newInstance()
val dBuilder = dbFactory.newDocumentBuilder()
@@ -82,12 +82,14 @@ data class InstalledPackage(val root: File) {
}
}
private fun NodeList.toList(): List<Node> {
val out = mutableListOf<Node>()
companion object {
fun NodeList.toList(): List<Node> {
val out = mutableListOf<Node>()
for(i in 0 until length)
out += item(i)
for (i in 0 until length)
out += item(i)
return out
return out
}
}
}
@@ -6,7 +6,7 @@ import android.content.pm.PackageInfo
import net.dongliu.apk.parser.bean.ApkMeta
import java.io.File
fun ApkMeta.toPackageInfo(root: File, apk: File): PackageInfo {
fun ApkMeta.toPackageInfo(apk: File): PackageInfo {
return PackageInfo().also {
it.packageName = packageName
it.versionCode = versionCode.toInt()
@@ -1,6 +1,3 @@
# Server ip and port bindings
ts.server.ip = 0.0.0.0
ts.server.port = 4567
# Allow/disallow preference changes (useful for demos)
ts.server.allowConfigChanges = true
+5
View File
@@ -0,0 +1,5 @@
# Code Of Conduct
- Don't be a dick.
# expanding the code of conduct!
The contents of this document is up for debate and improvement! Discussions on discord.
+52
View File
@@ -0,0 +1,52 @@
# Contributing
## Where should I start?
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
**Note to potential contributors:** Notify the developers on Suwayomi discord (#programming channel) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
## How does Tachidesk work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA(`create-react-app`) project that works with the server to do the presentation.
## Why a web app?
This structure is chosen to
- Achieve the maximum multi-platform-ness
- Gives the ability to acces Tachidesk from a remote web browser e.g. your phone, tablet or smart TV
- Eaise development of alternative user intefaces for Tachidesk
## User Interfaces for Tachidesk server
Currently there are three known interfaces for Tachidesk:
1. [webUI](https://github.com/Suwayomi/Tachidesk/tree/master/webUI/react): The react SPA that Tachidesk is traditionally shipped with.
2. [TachideskJUI](https://github.com/Suwayomi/TachideskJUI): A Jetbrains Compose Native app, re-uses components made for the upcoming Tachiyomi 1.x
3. [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stages of development.
## Building from source
### Prerequisites
You need these software packages installed in order to build the project
### Server
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
- Android stubs jar
- Manual download: Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
- Automated download: Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
### webUI
- Nodejs LTS or latest
- Yarn
- Git
### building the full-blown jar
Run `./gradlew :webUI:copyBuild server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building without `webUI` bundled(server only)
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building the Windows package
Run `./gradlew :server:windowsPackage` to build a server only bundle and `./gradlew :webUI:copyBuild :server:windowsPackage` to get a full bundle , the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
## Running in development mode
First satistify [the prerequisites](#prerequisites)
### server
run `./gradlew :server:run --stacktrace` to run the server
### webUI
How to do it is described in `webUI/react/README.md` but for short,
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
then `yarn start` to start the development server, if a new browser window doesn't get opned automatically,
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
and supports HMR and all the other goodies you'll need.
+52 -41
View File
@@ -1,59 +1,70 @@
| Build | Stable | Preview | Support Server |
|-------|----------|---------|---------|
| ![CI](https://github.com/Suwayomi/Tachidesk/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Tachidesk.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Tachidesk/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Tachidesk/raw/preview/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Tachidesk/tree/preview/latest_pointer) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) |
# Tachidesk
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
## How do I run the thing?
#### Prerequisites
You should have java 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
## Is this application usable? Should I test it?
Here is a list of current features:
#### Running pre-built jar packages
Download the latest (or a working more stable) release from [the repo branch](https://github.com/AriaMoradi/Tachidesk/tree/repo) or obtain it from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
- A library to save your mangas and categories to put them into.
- Searching and browsing installed sources.
- A decent chapter reader.
- Ability to download Mangas for offline read(This partially works)
- Backup and restore support powered by Tachiyomi Legacy Backups
Double click on the jar file or run `java -jar Tachidesk-latest.jar` or `java -jar Tachidesk-vX.Y.Z-rxxx.jar`
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting) if it happens.
The server will be running on `http://localhost:4567` open this url in your browser.
## Downloading and Running the app
### All Operating Systems
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed(Google is your friend for seeking assitance). Also an internet connection is required as almost everything this app does is downloading stuff.
#### Running on Docker
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview branch](https://github.com/Suwayomi/Tachidesk/tree/preview).
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` (or `java -jar Tachidesk-latest.jar` if you have the latest preview) from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
### Windows
Download the latest win32 release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`. The rest works like the previous section.
### Arch Linux
You can install Tachidesk from the AUR
```
yay -S tachidesk
```
Or the latest preview version
```
yay -S tachidesk-preview
```
### Docker
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
## Building from source
### Get Android stubs jar
#### Manual download
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
#### Building from source(needs `bash`, `curl`, `base64`, `zip` to work)
Run `scripts/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository.
### building the jar
Run `./gradlew shadowJar` the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
## Running for development purposes
### `server` module
Run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
### `webUI` module
How to do it is described in `webUI/react/README.md` but for short,
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
then `yarn start` to start the client if a new browser window doesn't start automatically,
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
and supports HMR and all the other goodies you'll need.
### Using Tachidesk Remotely
You can run Tachidesk on your computer or a server and connect to it remotely through the web interface with a web browser on any device including a mobile or tablet or even your smart TV!, this method of using Tachidesk is only recommended if you are a power user and know what you are doing.
## Is this application usable? Should I test it?
If you'd ask me, I'd tell you If you want to read your manga **online** from tachiyomi or in one place and bypass all the ads, you can use Tachidesk.
## Troubleshooting and Support
See [this troubleshooting wiki page](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting).
There are almost no quality of life features, including no library, no downloading for offline enjoyment and sadly no MangaDex search.
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented.
## How does it work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA project that works with the server to do the presentation.
## Support
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support.
## Contributing and Technical info
See [CONTRIBUTING.md](./CONTRIBUTING.md).
## Credit
The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
The `AndroidCompat` module was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
@@ -63,7 +74,7 @@ Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this pro
## License
Copyright (C) 2020-2021 Aria Moradi and contributors
Copyright (C) Contributors to the Suwayomi project
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
+31 -28
View File
@@ -1,33 +1,30 @@
import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.jetbrains.kotlin.jvm") version "1.4.21" apply false // Also in buildSrc Config.kt
id("java")
kotlin("jvm") version "1.4.32"
}
allprojects {
group = "xyz.nulldev.ts"
group = "ir.armor.tachidesk"
version = "1.0"
repositories {
jcenter()
mavenCentral()
maven("https://maven.google.com/")
maven("https://jitpack.io")
maven("https://oss.sonatype.org/content/repositories/snapshots/")
maven("https://dl.bintray.com/inorichi/maven")
maven("https://dl.google.com/dl/android/maven2/")
}
}
val javaProjects = listOf(
val projects = listOf(
project(":AndroidCompat"),
project(":AndroidCompat:Config"),
project(":server")
)
configure(javaProjects) {
apply(plugin = "java")
configure(projects) {
apply(plugin = "org.jetbrains.kotlin.jvm")
java {
@@ -35,34 +32,32 @@ configure(javaProjects) {
targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
// Kotlin
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
testImplementation(kotlin("test", version = "1.4.21"))
}
}
implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("reflect"))
testImplementation(kotlin("test"))
// coroutines
val coroutinesVersion = "1.4.3"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
configure(listOf(
project(":AndroidCompat"),
project(":server"),
project(":AndroidCompat:Config")
)) {
dependencies {
// Dependency Injection
implementation("org.kodein.di:kodein-di-conf-jvm:7.1.0")
implementation("org.kodein.di:kodein-di-conf-jvm:7.5.0")
// Logging
implementation("org.slf4j:slf4j-api:1.7.30")
implementation("org.slf4j:slf4j-simple:1.7.30")
implementation("io.github.microutils:kotlin-logging:2.0.3")
implementation("ch.qos.logback:logback-classic:1.2.3")
implementation("io.github.microutils:kotlin-logging:2.0.6")
// RxJava
implementation("io.reactivex:rxjava:1.3.8")
@@ -71,10 +66,18 @@ configure(listOf(
// JSoup
implementation("org.jsoup:jsoup:1.13.1")
// Kotlin
implementation(kotlin("reflect", version = "1.4.21"))
// dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.0")
implementation("com.typesafe:config:1.4.1")
implementation("io.github.config4k:config4k:0.4.2")
// to get application content root
implementation("net.harawata:appdirs:1.2.1")
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
// APK parser
implementation("net.dongliu:apk-parser:2.6.10")
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+1
View File
@@ -0,0 +1 @@
jre\bin\java -Dir.armor.tachidesk.debugLogsEnabled=true -jar Tachidesk.jar
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
echo "Downloading packr jar..."
packr="packr-all-4.0.0.jar"
curl -L "https://github.com/libgdx/packr/releases/download/4.0.0/packr-all-4.0.0.jar" -o $packr
echo "Downloading jre..."
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -o $jre
echo "creating windows bundle"
jar=$(ls ../server/build/Tachidesk-*.jar)
jar_name=$(echo $jar | cut -d'/' -f4)
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win64
cp $jar "Tachidesk.jar"
java -jar $packr \
--platform windows64 \
--jdk $jre \
--executable Tachidesk \
--classpath Tachidesk.jar \
--mainclass ir.armor.tachidesk.MainKt \
--vmargs Xmx4G \
--output $release_name
cp resources/Tachidesk-debug.bat $release_name
zip_name=$release_name.zip
zip -9 -r $zip_name $release_name
cp $zip_name ../server/build/
+113 -74
View File
@@ -1,102 +1,91 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jmailen.gradle.kotlinter.tasks.FormatTask
import org.jmailen.gradle.kotlinter.tasks.LintTask
import java.io.BufferedReader
plugins {
// id("org.jetbrains.kotlin.jvm") version "1.4.21"
application
id("com.github.johnrengelman.shadow") version "6.1.0"
id("org.jmailen.kotlinter") version "3.3.0"
id("com.github.johnrengelman.shadow") version "7.0.0"
id("org.jmailen.kotlinter") version "3.4.3"
id("edu.sc.seis.launch4j") version "2.5.0"
id("de.fuerstenau.buildconfig") version "1.1.8"
}
val TachideskVersion = "v0.1.5"
repositories {
mavenCentral()
jcenter()
maven {
url = uri("https://jitpack.io")
}
}
dependencies {
// implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
// Source models and interfaces from Tachiyomi 1.x
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
// implementation("tachiyomi.sourceapi:source-api:1.1")
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
val okhttp_version = "4.10.0-RC1"
implementation("com.squareup.okhttp3:okhttp:$okhttp_version")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttp_version")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version")
implementation("com.squareup.okio:okio:2.9.0")
val okhttpVersion = "4.10.0-RC1"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
implementation("com.squareup.okio:okio:2.10.0")
// retrofit
val retrofit_version = "2.9.0"
implementation("com.squareup.retrofit2:retrofit:$retrofit_version")
// Retrofit
val retrofitVersion = "2.9.0"
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
implementation("com.squareup.retrofit2:converter-gson:$retrofit_version")
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofit_version")
implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion")
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofitVersion")
// reactivex
// Reactivex
implementation("io.reactivex:rxjava:1.3.8")
// implementation("io.reactivex:rxandroid:1.2.1")
// implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
// implementation("com.github.pwittchen:reactivenetwork:0.13.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
implementation("org.jsoup:jsoup:1.13.1")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
implementation("com.squareup.duktape:duktape-android:1.3.0")
val coroutinesVersion = "1.3.9"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
// dex2jar
implementation(fileTree("lib/dex2jar/"))
// api
implementation("io.javalin:javalin:3.12.0")
implementation("org.slf4j:slf4j-simple:1.8.0-beta4")
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
// to get application content root
implementation("net.harawata:appdirs:1.2.0")
implementation("io.javalin:javalin:3.13.6")
implementation("com.fasterxml.jackson.core:jackson-databind:2.12.3")
// Exposed ORM
val exposed_version = "0.28.1"
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation ("com.h2database:h2:1.4.199")
val exposedVersion = "0.31.1"
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
// current database driver
implementation("com.h2database:h2:1.4.200")
// tray icon
implementation("com.dorkbox:SystemTray:4.1")
implementation("com.dorkbox:Utilities:1.9")
implementation("com.google.guava:guava:30.1.1-jre")
// AndroidCompat
implementation(project(":AndroidCompat"))
implementation(project(":AndroidCompat:Config"))
// uncomment to test extensions directly
// implementation(fileTree("lib/"))
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
// Testing
testImplementation(kotlin("test-junit5"))
}
val MainClass = "ir.armor.tachidesk.MainKt"
application {
val name = "ir.armor.tachidesk.Main"
mainClass.set(name)
// Required by ShadowJar.
mainClassName = name
mainClass.set(MainClass)
}
sourceSets {
@@ -107,7 +96,11 @@ sourceSets {
}
}
val TachideskRevision = Runtime
// should be bumped with each stable release
val tachideskVersion = "v0.3.3"
// counts commit count on master
val tachideskRevision = Runtime
.getRuntime()
.exec("git rev-list master --count")
.let { process ->
@@ -116,41 +109,87 @@ val TachideskRevision = Runtime
it.bufferedReader().use(BufferedReader::readText)
}
process.destroy()
"r"+output.trim()
"r" + output.trim()
}
buildConfig {
appName = rootProject.name
clsName = "BuildConfig"
packageName = "ir.armor.tachidesk.server"
version = tachideskVersion
buildConfigField("String", "name", rootProject.name) // alias for BuildConfig.NAME
buildConfigField("String", "version", tachideskVersion) // alias for BuildConfig.VERSION
buildConfigField("String", "revision", tachideskRevision)
buildConfigField("boolean", "debug", project.hasProperty("debugApp").toString())
}
launch4j { //used for windows
mainClassName = MainClass
bundledJrePath = "jre"
bundledJre64Bit = true
jreMinVersion = "8"
outputDir = "${rootProject.name}-$tachideskVersion-$tachideskRevision-win64"
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
jar = "${projectDir}/build/${rootProject.name}-$tachideskVersion-$tachideskRevision.jar"
}
tasks {
jar {
manifest {
attributes(
mapOf(
"Main-Class" to "com.example.MainKt", //will make your jar (produced by jar task) runnable
"ImplementationTitle" to project.name,
"Implementation-Version" to project.version)
"Main-Class" to MainClass,
"Implementation-Title" to rootProject.name,
"Implementation-Vendor" to "The Suwayomi Project",
"Specification-Version" to tachideskVersion,
"Implementation-Version" to tachideskRevision
)
)
}
}
shadowJar {
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
archiveBaseName.set("Tachidesk")
archiveVersion.set(TachideskVersion)
archiveClassifier.set(TachideskRevision)
archiveBaseName.set(rootProject.name)
archiveVersion.set(tachideskVersion)
archiveClassifier.set(tachideskRevision)
}
withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf(
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
)
}
}
test {
useJUnit()
}
withType<ShadowJar> {
destinationDirectory.set(File("$rootDir/server/build"))
dependsOn("formatKotlin", "lintKotlin")
}
named("run") {
dependsOn("formatKotlin", "lintKotlin")
}
named<Copy>("processResources") {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
mustRunAfter(":webUI:copyBuild")
}
withType<LintTask> {
source(files("src"))
}
withType<FormatTask> {
source(files("src"))
}
}
tasks.withType<ShadowJar> {
destinationDir = File("$rootDir/server/build")
dependsOn("lintKotlin")
}
tasks.named("processResources") {
dependsOn(":webUI:copyBuild")
}
tasks.named("run") {
dependsOn("formatKotlin", "lintKotlin")
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,252 +0,0 @@
package ir.armor.tachidesk;
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
public class APKExtractor {
// decompressXML -- Parse the 'compressed' binary form of Android XML docs
// such as for AndroidManifest.xml in .apk files
public static int endDocTag = 0x00100101;
public static int startTag = 0x00100102;
public static int endTag = 0x00100103;
static void prt(String str) {
//System.err.print(str);
}
public static String decompressXML(byte[] xml) {
StringBuilder finalXML = new StringBuilder();
// Compressed XML file/bytes starts with 24x bytes of data,
// 9 32 bit words in little endian order (LSB first):
// 0th word is 03 00 08 00
// 3rd word SEEMS TO BE: Offset at then of StringTable
// 4th word is: Number of strings in string table
// WARNING: Sometime I indiscriminently display or refer to word in
// little endian storage format, or in integer format (ie MSB first).
int numbStrings = LEW(xml, 4 * 4);
// StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
// of the length/string data in the StringTable.
int sitOff = 0x24; // Offset of start of StringIndexTable
// StringTable, each string is represented with a 16 bit little endian
// character count, followed by that number of 16 bit (LE) (Unicode)
// chars.
int stOff = sitOff + numbStrings * 4; // StringTable follows
// StrIndexTable
// XMLTags, The XML tag tree starts after some unknown content after the
// StringTable. There is some unknown data after the StringTable, scan
// forward from this point to the flag for the start of an XML start
// tag.
int xmlTagOff = LEW(xml, 3 * 4); // Start from the offset in the 3rd
// word.
// Scan forward until we find the bytes: 0x02011000(x00100102 in normal
// int)
for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) {
if (LEW(xml, ii) == startTag) {
xmlTagOff = ii;
break;
}
} // end of hack, scanning for start of first start tag
// XML tags and attributes:
// Every XML start and end tag consists of 6 32 bit words:
// 0th word: 02011000 for startTag and 03011000 for endTag
// 1st word: a flag?, like 38000000
// 2nd word: Line of where this tag appeared in the original source file
// 3rd word: FFFFFFFF ??
// 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
// 5th word: StringIndex of Element Name
// (Note: 01011000 in 0th word means end of XML document, endDocTag)
// Start tags (not end tags) contain 3 more words:
// 6th word: 14001400 meaning??
// 7th word: Number of Attributes that follow this tag(follow word 8th)
// 8th word: 00000000 meaning??
// Attributes consist of 5 words:
// 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
// 1st word: StringIndex of Attribute Name
// 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId
// used
// 3rd word: Flags?
// 4th word: str ind of attr value again, or ResourceId of value
// TMP, dump string table to tr for debugging
// tr.addSelect("strings", null);
// for (int ii=0; ii<numbStrings; ii++) {
// // Length of string starts at StringTable plus offset in StrIndTable
// String str = compXmlString(xml, sitOff, stOff, ii);
// tr.add(String.valueOf(ii), str);
// }
// tr.parent();
// Step through the XML tree element tags and attributes
int off = xmlTagOff;
int indent = 0;
int startTagLineNo = -2;
while (off < xml.length) {
int tag0 = LEW(xml, off);
// int tag1 = LEW(xml, off+1*4);
int lineNo = LEW(xml, off + 2 * 4);
// int tag3 = LEW(xml, off+3*4);
int nameNsSi = LEW(xml, off + 4 * 4);
int nameSi = LEW(xml, off + 5 * 4);
if (tag0 == startTag) { // XML START TAG
int tag6 = LEW(xml, off + 6 * 4); // Expected to be 14001400
int numbAttrs = LEW(xml, off + 7 * 4); // Number of Attributes
// to follow
// int tag8 = LEW(xml, off+8*4); // Expected to be 00000000
off += 9 * 4; // Skip over 6+3 words of startTag data
String name = compXmlString(xml, sitOff, stOff, nameSi);
// tr.addSelect(name, null);
startTagLineNo = lineNo;
// Look for the Attributes
StringBuffer sb = new StringBuffer();
for (int ii = 0; ii < numbAttrs; ii++) {
int attrNameNsSi = LEW(xml, off); // AttrName Namespace Str
// Ind, or FFFFFFFF
int attrNameSi = LEW(xml, off + 1 * 4); // AttrName String
// Index
int attrValueSi = LEW(xml, off + 2 * 4); // AttrValue Str
// Ind, or
// FFFFFFFF
int attrFlags = LEW(xml, off + 3 * 4);
int attrResId = LEW(xml, off + 4 * 4); // AttrValue
// ResourceId or dup
// AttrValue StrInd
off += 5 * 4; // Skip over the 5 words of an attribute
String attrName = compXmlString(xml, sitOff, stOff,
attrNameSi);
String attrValue = attrValueSi != -1 ? compXmlString(xml,
sitOff, stOff, attrValueSi) : "resourceID 0x"
+ Integer.toHexString(attrResId);
sb.append(" " + attrName + "=\"" + attrValue + "\"");
// tr.add(attrName, attrValue);
}
finalXML.append("<" + name + sb + ">");
prtIndent(indent, "<" + name + sb + ">");
indent++;
} else if (tag0 == endTag) { // XML END TAG
indent--;
off += 6 * 4; // Skip over 6 words of endTag data
String name = compXmlString(xml, sitOff, stOff, nameSi);
finalXML.append("</" + name + ">");
prtIndent(indent, "</" + name + "> (line " + startTagLineNo
+ "-" + lineNo + ")");
// tr.parent(); // Step back up the NobTree
} else if (tag0 == endDocTag) { // END OF XML DOC TAG
break;
} else {
prt(" Unrecognized tag code '" + Integer.toHexString(tag0)
+ "' at offset " + off);
break;
}
} // end of while loop scanning tags and attributes of XML tree
//prt(" end at offset " + off);
return finalXML.toString();
} // end of decompressXML
public static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) {
if (strInd < 0)
return null;
int strOff = stOff + LEW(xml, sitOff + strInd * 4);
return compXmlStringAt(xml, strOff);
}
public static String spaces = " ";
public static void prtIndent(int indent, String str) {
prt(spaces.substring(0, Math.min(indent * 2, spaces.length())) + str);
}
// compXmlStringAt -- Return the string stored in StringTable format at
// offset strOff. This offset points to the 16 bit string length, which
// is followed by that number of 16 bit (Unicode) chars.
public static String compXmlStringAt(byte[] arr, int strOff) {
int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff;
byte[] chars = new byte[strLen];
for (int ii = 0; ii < strLen; ii++) {
chars[ii] = arr[strOff + 2 + ii * 2];
}
return new String(chars); // Hack, just use 8 byte chars
} // end of compXmlStringAt
// LEW -- Return value of a Little Endian 32 bit word from the byte array
// at offset off.
public static int LEW(byte[] arr, int off) {
return arr[off + 3] << 24 & 0xff000000 | arr[off + 2] << 16 & 0xff0000
| arr[off + 1] << 8 & 0xff00 | arr[off] & 0xFF;
} // end of LEW
public static Document loadXMLFromString(String xml) throws Exception {
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
return docBuilder.parse(new InputSource(new StringReader(xml)));
}
public static String extract_dex_and_read_className(String filePath, String dexPath) throws IOException {
ZipFile zip = null;
zip = new ZipFile(filePath);
ZipEntry androidManifest = zip.getEntry("AndroidManifest.xml");
ZipEntry classesDex = zip.getEntry("classes.dex");
// write dex file
InputStream dexStream = zip.getInputStream(classesDex);
try (OutputStream os = Files.newOutputStream(Paths.get(dexPath))) {
byte[] buffer = new byte[1024];
int len;
while ((len = dexStream.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
}
// read xml file
InputStream is = zip.getInputStream(androidManifest);
byte[] buf = new byte[1024000]; // 100 kb
is.read(buf);
is.close();
zip.close();
String xml = APKExtractor.decompressXML(buf);
try {
Document xmlDoc = loadXMLFromString(xml);
String pkg = xmlDoc.getDocumentElement().getAttribute("package");
NodeList nodes = xmlDoc.getElementsByTagName("meta-data");
for (int i = 0; i < nodes.getLength(); i++) {
NamedNodeMap attributes = nodes.item(i).getAttributes();
System.out.println(attributes.getNamedItem("name").getNodeValue());
if (attributes.getNamedItem("name").getNodeValue().equals("tachiyomi.extension.class"))
return pkg + attributes.getNamedItem("value").getNodeValue();
}
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
}
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.app.Application
import android.content.Context
// import android.content.res.Configuration
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.app.Application
import com.google.gson.Gson
// import eu.kanade.tachiyomi.data.cache.ChapterCache
@@ -1,51 +1,23 @@
package eu.kanade.tachiyomi.extension.api
// import android.content.Context
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
// import kotlinx.coroutines.Dispatchers
// import kotlinx.coroutines.withContext
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
// import uy.kohesive.injekt.injectLazy
internal class ExtensionGithubApi {
// private val preferences: PreferencesHelper by injectLazy()
suspend fun findExtensions(): List<Extension.Available> {
val service: ExtensionGithubService = ExtensionGithubService.create()
val response = service.getRepo()
return parseResponse(response)
}
// suspend fun checkForUpdates(): List<Extension.Installed> {
// val extensions = fin dExtensions()
//
// // preferences.lastExtCheck().set(Date().time)
//
// val installedExtensions = ExtensionLoader.loadExtensions(context)
// .filterIsInstance<LoadResult.Success>()
// .map { it.extension }
//
// val extensionsWithUpdate = mutableListOf<Extension.Installed>()
// for (installedExt in installedExtensions) {
// val pkgName = installedExt.pkgName
// val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
//
// val hasUpdate = availableExt.versionCode > installedExt.versionCode
// if (hasUpdate) {
// extensionsWithUpdate.add(installedExt)
// }
// }
//
// return extensionsWithUpdate
// }
object ExtensionGithubApi {
const val BASE_URL = "https://raw.githubusercontent.com"
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
private fun parseResponse(json: JsonArray): List<Extension.Available> {
return json
@@ -68,16 +40,14 @@ internal class ExtensionGithubApi {
}
}
fun getApkUrl(extension: Extension.Available): String {
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
suspend fun findExtensions(): List<Extension.Available> {
val service: ExtensionGithubService = ExtensionGithubService.create()
val response = service.getRepo()
return parseResponse(response)
}
fun getApkUrl(extension: ExtensionDataClass): String {
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
}
companion object {
const val BASE_URL = "https://raw.githubusercontent.com/"
const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo"
}
}
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.api
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import okhttp3.MediaType.Companion.toMediaType
@@ -9,8 +10,6 @@ import retrofit2.Retrofit
import retrofit2.http.GET
import uy.kohesive.injekt.injectLazy
// import uy.kohesive.injekt.injectLazy
/**
* Used to get the extension repo listing from GitHub.
*/
@@ -30,6 +29,7 @@ interface ExtensionGithubService {
.build()
}
@ExperimentalSerializationApi
fun create(): ExtensionGithubService {
val adapter = Retrofit.Builder()
.baseUrl(ExtensionGithubApi.BASE_URL)
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.extension.util
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// import android.annotation.SuppressLint
// import android.content.Context
// import android.content.pm.PackageInfo
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.network
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// import android.annotation.SuppressLint
// import android.content.Context
// import android.os.Build
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.network
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.network
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// import android.content.Context
// import eu.kanade.tachiyomi.BuildConfig
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -0,0 +1,358 @@
package eu.kanade.tachiyomi.source
import android.content.Context
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import rx.Observable
// import com.github.junrar.Archive
// import com.google.gson.JsonParser
// import eu.kanade.tachiyomi.R
// import eu.kanade.tachiyomi.source.model.Filter
// import eu.kanade.tachiyomi.source.model.FilterList
// import eu.kanade.tachiyomi.source.model.MangasPage
// import eu.kanade.tachiyomi.source.model.Page
// import eu.kanade.tachiyomi.source.model.SChapter
// import eu.kanade.tachiyomi.source.model.SManga
// import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
// import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
// import eu.kanade.tachiyomi.util.storage.DiskUtil
// import eu.kanade.tachiyomi.util.storage.EpubFile
// import eu.kanade.tachiyomi.util.system.ImageUtil
// import rx.Observable
// import timber.log.Timber
// import java.io.File
// import java.io.FileInputStream
// import java.io.InputStream
// import java.util.Locale
// import java.util.concurrent.TimeUnit
// import java.util.zip.ZipFile
class LocalSource(private val context: Context) : CatalogueSource {
companion object {
const val ID = 0L
// const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
//
// private const val COVER_NAME = "cover.jpg"
// private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
//
// private val POPULAR_FILTERS = FilterList(OrderBy())
// private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
// private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
//
// fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
// val dir = getBaseDirectories(context).firstOrNull()
// if (dir == null) {
// input.close()
// return null
// }
// val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
//
// // It might not exist if using the external SD card
// cover.parentFile?.mkdirs()
// input.use {
// cover.outputStream().use {
// input.copyTo(it)
// }
// }
// return cover
// }
//
// private fun getBaseDirectories(context: Context): List<File> {
// val c = context.getString(R.string.app_name) + File.separator + "local"
// return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
// }
}
override val id = ID
override val name = "Local source"
override val lang = ""
override val supportsLatest = true
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
TODO("Not yet implemented")
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
TODO("Not yet implemented")
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
TODO("Not yet implemented")
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
TODO("Not yet implemented")
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
TODO("Not yet implemented")
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
TODO("Not yet implemented")
}
override fun getFilterList(): FilterList {
TODO("Not yet implemented")
}
//
// override fun toString() = context.getString(R.string.local_source)
//
// override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
//
// override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
// val baseDirs = getBaseDirectories(context)
//
// val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
// var mangaDirs = baseDirs
// .asSequence()
// .mapNotNull { it.listFiles()?.toList() }
// .flatten()
// .filter { it.isDirectory }
// .filterNot { it.name.startsWith('.') }
// .filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
// .distinctBy { it.name }
//
// val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
// when (state?.index) {
// 0 -> {
// mangaDirs = if (state.ascending) {
// mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
// } else {
// mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
// }
// }
// 1 -> {
// mangaDirs = if (state.ascending) {
// mangaDirs.sortedBy(File::lastModified)
// } else {
// mangaDirs.sortedByDescending(File::lastModified)
// }
// }
// }
//
// val mangas = mangaDirs.map { mangaDir ->
// SManga.create().apply {
// title = mangaDir.name
// url = mangaDir.name
//
// // Try to find the cover
// for (dir in baseDirs) {
// val cover = File("${dir.absolutePath}/$url", COVER_NAME)
// if (cover.exists()) {
// thumbnail_url = cover.absolutePath
// break
// }
// }
//
// val chapters = fetchChapterList(this).toBlocking().first()
// if (chapters.isNotEmpty()) {
// val chapter = chapters.last()
// val format = getFormat(chapter)
// if (format is Format.Epub) {
// EpubFile(format.file).use { epub ->
// epub.fillMangaMetadata(this)
// }
// }
//
// // Copy the cover from the first chapter found.
// if (thumbnail_url == null) {
// try {
// val dest = updateCover(chapter, this)
// thumbnail_url = dest?.absolutePath
// } catch (e: Exception) {
// Timber.e(e)
// }
// }
// }
// }
// }
//
// return Observable.just(MangasPage(mangas.toList(), false))
// }
//
// override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
//
// override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
// getBaseDirectories(context)
// .asSequence()
// .mapNotNull { File(it, manga.url).listFiles()?.toList() }
// .flatten()
// .firstOrNull { it.extension == "json" }
// ?.apply {
// val reader = this.inputStream().bufferedReader()
// val json = JsonParser.parseReader(reader).asJsonObject
//
// manga.title = json["title"]?.asString ?: manga.title
// manga.author = json["author"]?.asString ?: manga.author
// manga.artist = json["artist"]?.asString ?: manga.artist
// manga.description = json["description"]?.asString ?: manga.description
// manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString }
// ?: manga.genre
// manga.status = json["status"]?.asInt ?: manga.status
// }
//
// return Observable.just(manga)
// }
//
// override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
// val chapters = getBaseDirectories(context)
// .asSequence()
// .mapNotNull { File(it, manga.url).listFiles()?.toList() }
// .flatten()
// .filter { it.isDirectory || isSupportedFile(it.extension) }
// .map { chapterFile ->
// SChapter.create().apply {
// url = "${manga.url}/${chapterFile.name}"
// name = if (chapterFile.isDirectory) {
// chapterFile.name
// } else {
// chapterFile.nameWithoutExtension
// }
// date_upload = chapterFile.lastModified()
//
// val format = getFormat(this)
// if (format is Format.Epub) {
// EpubFile(format.file).use { epub ->
// epub.fillChapterMetadata(this)
// }
// }
//
// val chapNameCut = stripMangaTitle(name, manga.title)
// if (chapNameCut.isNotEmpty()) name = chapNameCut
// ChapterRecognition.parseChapterNumber(this, manga)
// }
// }
// .sortedWith(
// Comparator { c1, c2 ->
// val c = c2.chapter_number.compareTo(c1.chapter_number)
// if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
// }
// )
// .toList()
//
// return Observable.just(chapters)
// }
//
// /**
// * Strips the manga title from a chapter name, matching only based on alphanumeric and whitespace
// * characters.
// */
// private fun stripMangaTitle(chapterName: String, mangaTitle: String): String {
// var chapterNameIndex = 0
// var mangaTitleIndex = 0
// while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
// val chapterChar = chapterName[chapterNameIndex]
// val mangaChar = mangaTitle[mangaTitleIndex]
// if (!chapterChar.equals(mangaChar, true)) {
// val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
// val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
//
// if (!invalidChapterChar && !invalidMangaChar) {
// return chapterName
// }
//
// if (invalidChapterChar) {
// chapterNameIndex++
// }
//
// if (invalidMangaChar) {
// mangaTitleIndex++
// }
// } else {
// chapterNameIndex++
// mangaTitleIndex++
// }
// }
//
// return chapterName.substring(chapterNameIndex).trimStart(' ', '-', '_', ',', ':')
// }
//
// override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
// return Observable.error(Exception("Unused"))
// }
//
// private fun isSupportedFile(extension: String): Boolean {
// return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
// }
//
// fun getFormat(chapter: SChapter): Format {
// val baseDirs = getBaseDirectories(context)
//
// for (dir in baseDirs) {
// val chapFile = File(dir, chapter.url)
// if (!chapFile.exists()) continue
//
// return getFormat(chapFile)
// }
// throw Exception("Chapter not found")
// }
//
// private fun getFormat(file: File): Format {
// val extension = file.extension
// return if (file.isDirectory) {
// Format.Directory(file)
// } else if (extension.equals("zip", true) || extension.equals("cbz", true)) {
// Format.Zip(file)
// } else if (extension.equals("rar", true) || extension.equals("cbr", true)) {
// Format.Rar(file)
// } else if (extension.equals("epub", true)) {
// Format.Epub(file)
// } else {
// throw Exception("Invalid chapter format")
// }
// }
//
// private fun updateCover(chapter: SChapter, manga: SManga): File? {
// return when (val format = getFormat(chapter)) {
// is Format.Directory -> {
// val entry = format.file.listFiles()
// ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
// ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
//
// entry?.let { updateCover(context, manga, it.inputStream()) }
// }
// is Format.Zip -> {
// ZipFile(format.file).use { zip ->
// val entry = zip.entries().toList()
// .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
// .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
//
// entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
// }
// }
// is Format.Rar -> {
// Archive(format.file).use { archive ->
// val entry = archive.fileHeaders
// .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
// .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
//
// entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
// }
// }
// is Format.Epub -> {
// EpubFile(format.file).use { epub ->
// val entry = epub.getImagesFromPages()
// .firstOrNull()
// ?.let { epub.getEntry(it) }
//
// entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
// }
// }
// }
// }
//
// private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Selection(0, true))
//
// override fun getFilterList() = FilterList(OrderBy())
//
// sealed class Format {
// data class Directory(val file: File) : Format()
// data class Zip(val file: File) : Format()
// data class Rar(val file: File) : Format()
// data class Epub(val file: File) : Format()
// }
}
@@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.util.lang
import java.security.MessageDigest
object Hash {
private val chars = charArrayOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f'
)
private val MD5 get() = MessageDigest.getInstance("MD5")
private val SHA256 get() = MessageDigest.getInstance("SHA-256")
fun sha256(bytes: ByteArray): String {
return encodeHex(SHA256.digest(bytes))
}
fun sha256(string: String): String {
return sha256(string.toByteArray())
}
fun md5(bytes: ByteArray): String {
return encodeHex(MD5.digest(bytes))
}
fun md5(string: String): String {
return md5(string.toByteArray())
}
private fun encodeHex(data: ByteArray): String {
val l = data.size
val out = CharArray(l shl 1)
var i = 0
var j = 0
while (i < l) {
out[j++] = chars[(240 and data[i].toInt()).ushr(4)]
out[j++] = chars[15 and data[i].toInt()]
i++
}
return String(out)
}
}
@@ -1,14 +0,0 @@
package ir.armor.tachidesk
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import net.harawata.appdirs.AppDirsFactory
object Config {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
val extensionsRoot = "$dataRoot/extensions"
val thumbnailsRoot = "$dataRoot/thumbnails"
val mangaRoot = "$dataRoot/manga"
}
@@ -1,172 +1,16 @@
package ir.armor.tachidesk
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.App
import io.javalin.Javalin
import ir.armor.tachidesk.util.applicationSetup
import ir.armor.tachidesk.util.getChapter
import ir.armor.tachidesk.util.getChapterList
import ir.armor.tachidesk.util.getExtensionList
import ir.armor.tachidesk.util.getManga
import ir.armor.tachidesk.util.getMangaList
import ir.armor.tachidesk.util.getPageImage
import ir.armor.tachidesk.util.getSource
import ir.armor.tachidesk.util.getSourceList
import ir.armor.tachidesk.util.getThumbnail
import ir.armor.tachidesk.util.installAPK
import ir.armor.tachidesk.util.removeExtension
import ir.armor.tachidesk.util.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch
import org.kodein.di.DI
import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.AndroidCompat
import xyz.nulldev.androidcompat.AndroidCompatInitializer
import xyz.nulldev.ts.config.ConfigKodeinModule
import xyz.nulldev.ts.config.GlobalConfigManager
import ir.armor.tachidesk.server.JavalinSetup.javalinSetup
import ir.armor.tachidesk.server.applicationSetup
class Main {
companion object {
val androidCompat by lazy { AndroidCompat() }
fun registerConfigModules() {
GlobalConfigManager.registerModules(
// ServerConfig.register(GlobalConfigManager.config),
// SyncConfigModule.register(GlobalConfigManager.config)
)
}
@JvmStatic
fun main(args: Array<String>) {
// System.getProperties()["proxySet"] = "true"
// System.getProperties()["socksProxyHost"] = "127.0.0.1"
// System.getProperties()["socksProxyPort"] = "2020"
// make sure everything we need exists
applicationSetup()
registerConfigModules()
// Load config API
DI.global.addImport(ConfigKodeinModule().create())
// Load Android compatibility dependencies
AndroidCompatInitializer().init()
// start app
androidCompat.startApp(App())
// Thread(getMangaUpdateQueueThread).start()
val app = Javalin.create { config ->
try {
this::class.java.classLoader.getResource("/react/index.html")
config.addStaticFiles("/react")
config.addSinglePageRoot("/", "/react/index.html")
} catch (e: RuntimeException) {
println("Warning: react build files are missing.")
}
}.start(4567)
app.before() { ctx ->
// allow the client which is running on another port
ctx.header("Access-Control-Allow-Origin", "*")
}
app.get("/api/v1/extension/list") { ctx ->
ctx.json(getExtensionList())
}
app.get("/api/v1/extension/install/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
println("installing $apkName")
ctx.status(
installAPK(apkName)
)
}
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
println("uninstalling $apkName")
removeExtension(apkName)
ctx.status(200)
}
app.get("/api/v1/source/list") { ctx ->
ctx.json(getSourceList())
}
app.get("/api/v1/source/:sourceId") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(getSource(sourceId))
}
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId, pageNum, popular = true))
}
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId, pageNum, popular = false))
}
app.get("/api/v1/manga/:mangaId/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getManga(mangaId))
}
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val result = getThumbnail(mangaId)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapterList(mangaId))
}
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
val chapterId = ctx.pathParam("chapterId").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapter(chapterId, mangaId))
}
app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx ->
val chapterId = ctx.pathParam("chapterId").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
val index = ctx.pathParam("index").toInt()
val result = getPageImage(mangaId, chapterId, index)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// global search
app.get("/api/v1/search/:searchTerm") { ctx ->
val searchTerm = ctx.pathParam("searchTerm")
ctx.json(sourceGlobalSearch(searchTerm))
}
// single source search
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm")
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
}
// source filter list
app.get("/api/v1/source/:sourceId/filters/") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(sourceFilters(sourceId))
}
}
}
fun main() {
applicationSetup()
javalinSetup()
}
@@ -1,36 +0,0 @@
package ir.armor.tachidesk.database
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.Config
import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.ExtensionsTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
object DBMangaer {
val db by lazy {
Database.connect("jdbc:h2:${Config.dataRoot}/database", "org.h2.Driver")
}
}
fun makeDataBaseTables() {
// mention db object to connect
DBMangaer.db
// val db = DBMangaer.db
// db.useNestedTransactions = true
transaction {
SchemaUtils.create(ExtensionsTable)
SchemaUtils.create(SourceTable)
SchemaUtils.create(MangaTable)
SchemaUtils.create(ChapterTable)
SchemaUtils.create(PageTable)
}
}
@@ -1,16 +0,0 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class ChapterDataClass(
val id: Int,
val url: String,
val name: String,
val date_upload: Long,
val chapter_number: Float,
val scanlator: String?,
val mangaId: Int,
val pageCount: Int? = null,
)
@@ -1,13 +0,0 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class SourceDataClass(
val id: String,
val name: String,
val lang: String,
val iconUrl: String,
val supportsLatest: Boolean
)
@@ -1,25 +0,0 @@
package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.table.ExtensionsTable
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<ExtensionEntity>(ExtensionsTable)
var name by ExtensionsTable.name
var pkgName by ExtensionsTable.pkgName
var versionName by ExtensionsTable.versionName
var versionCode by ExtensionsTable.versionCode
var lang by ExtensionsTable.lang
var isNsfw by ExtensionsTable.isNsfw
var apkName by ExtensionsTable.apkName
var iconUrl by ExtensionsTable.iconUrl
var installed by ExtensionsTable.installed
var classFQName by ExtensionsTable.classFQName
}
@@ -1,27 +0,0 @@
package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.table.MangaTable
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
class MangaEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<MangaEntity>(MangaTable)
var url by MangaTable.url
var title by MangaTable.title
var initialized by MangaTable.initialized
var artist by MangaTable.artist
var author by MangaTable.author
var description by MangaTable.description
var genre by MangaTable.genre
var status by MangaTable.status
var thumbnail_url by MangaTable.thumbnail_url
var sourceReference by MangaEntity referencedOn MangaTable.sourceReference
}
@@ -1,21 +0,0 @@
package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.dao.EntityClass
import org.jetbrains.exposed.dao.LongEntity
import org.jetbrains.exposed.dao.id.EntityID
class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
companion object : EntityClass<Long, SourceEntity>(SourceTable, null)
var sourceId by SourceTable.id
var name by SourceTable.name
var lang by SourceTable.lang
var extension by ExtensionEntity referencedOn SourceTable.extension
var partOfFactorySource by SourceTable.partOfFactorySource
var positionInFactorySource by SourceTable.positionInFactorySource
}
@@ -1,17 +0,0 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable
object ChapterTable : IntIdTable() {
val url = varchar("url", 2048)
val name = varchar("name", 512)
val date_upload = long("date_upload").default(0)
val chapter_number = float("chapter_number").default(-1f)
val scanlator = varchar("scanlator", 128).nullable()
val manga = reference("manga", MangaTable)
}
@@ -1,21 +0,0 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable
object ExtensionsTable : IntIdTable() {
val name = varchar("name", 128)
val pkgName = varchar("pkg_name", 128)
val versionName = varchar("version_name", 16)
val versionCode = integer("version_code")
val lang = varchar("lang", 10)
val isNsfw = bool("is_nsfw")
val apkName = varchar("apk_name", 1024)
val iconUrl = varchar("icon_url", 2048)
val installed = bool("installed").default(false)
val classFQName = varchar("class_name", 256).default("") // fully qualified name
}
@@ -0,0 +1,78 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
import ir.armor.tachidesk.model.database.table.CategoryTable
import ir.armor.tachidesk.model.database.table.toDataClass
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
object Category {
/**
* The new category will be placed at the end of the list
*/
fun createCategory(name: String) {
transaction {
val count = CategoryTable.selectAll().count()
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
CategoryTable.insert {
it[CategoryTable.name] = name
it[CategoryTable.order] = count.toInt() + 1
}
}
}
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
transaction {
CategoryTable.update({ CategoryTable.id eq categoryId }) {
if (name != null) it[CategoryTable.name] = name
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
}
}
}
/**
* Move the category from position `from` to `to`
*/
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
transaction {
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
categories.add(to - 1, categories.removeAt(from - 1))
categories.forEachIndexed { index, cat ->
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
it[CategoryTable.order] = index + 1
}
}
}
}
fun removeCategory(categoryId: Int) {
transaction {
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
}
}
fun getCategoryList(): List<CategoryDataClass> {
return transaction {
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
}
}
}
}
@@ -0,0 +1,72 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
import ir.armor.tachidesk.model.database.table.CategoryTable
import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.database.table.toDataClass
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
import ir.armor.tachidesk.model.dataclass.MangaDataClass
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
object CategoryManga {
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
transaction {
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
CategoryMangaTable.insert {
it[CategoryMangaTable.category] = categoryId
it[CategoryMangaTable.manga] = mangaId
}
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.defaultCategory] = false
}
}
}
}
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
transaction {
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.defaultCategory] = true
}
}
}
}
/**
* list of mangas that belong to a category
*/
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
return transaction {
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
MangaTable.toDataClass(it)
}
}
}
/**
* list of categories that a manga belongs to
*/
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
return transaction {
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
}
}
}
}
@@ -0,0 +1,204 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.impl.Manga.getManga
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.model.database.table.ChapterTable
import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.database.table.PageTable
import ir.armor.tachidesk.model.database.table.toDataClass
import ir.armor.tachidesk.model.dataclass.ChapterDataClass
import org.jetbrains.exposed.sql.SortOrder.DESC
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
object Chapter {
/** get chapter list when showing a manga */
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean): List<ChapterDataClass> {
return if (!onlineFetch) {
transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.chapterIndex to DESC)
.map {
ChapterTable.toDataClass(it)
}
}
} else {
val mangaDetails = getManga(mangaId)
val source = getHttpSource(mangaDetails.sourceId.toLong())
val chapterList = source.fetchChapterList(
SManga.create().apply {
title = mangaDetails.title
url = mangaDetails.url
}
).awaitSingle()
val chapterCount = chapterList.count()
transaction {
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
if (chapterEntry == null) {
ChapterTable.insert {
it[url] = fetchedChapter.url
it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[manga] = mangaId
}
} else {
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[manga] = mangaId
}
}
}
}
// clear any orphaned chapters that are in the db but not in `chapterList`
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
if (dbChapterCount > chapterCount) { // we got some clean up due
val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId } }
dbChapterList.forEach {
if (it[ChapterTable.chapterIndex] >= chapterList.size ||
chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url]
) {
transaction {
PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] }
ChapterTable.deleteWhere { ChapterTable.id eq it[ChapterTable.id] }
}
}
}
}
val dbChapterMap = transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.associateBy({ it[ChapterTable.url] }, { it })
}
return chapterList.mapIndexed { index, it ->
val dbChapter = dbChapterMap.getValue(it.url)
ChapterDataClass(
it.url,
it.name,
it.date_upload,
it.chapter_number,
it.scanlator,
mangaId,
dbChapter[ChapterTable.isRead],
dbChapter[ChapterTable.isBookmarked],
dbChapter[ChapterTable.lastPageRead],
chapterCount - index,
)
}
}
}
/** used to display a chapter, get a chapter in order to show it's pages */
suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
val chapterEntry = transaction {
ChapterTable.select {
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.firstOrNull()!!
}
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val pageList = source.fetchPageList(
SChapter.create().apply {
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
).awaitSingle()
val chapterId = chapterEntry[ChapterTable.id].value
val chapterCount = transaction { ChapterTable.selectAll().count() }
// update page list for this chapter
transaction {
pageList.forEach { page ->
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
if (pageEntry == null) {
PageTable.insert {
it[index] = page.index
it[url] = page.url
it[imageUrl] = page.imageUrl
it[chapter] = chapterId
}
} else {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
it[url] = page.url
it[imageUrl] = page.imageUrl
}
}
}
}
return ChapterDataClass(
chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator],
mangaId,
chapterEntry[ChapterTable.isRead],
chapterEntry[ChapterTable.isBookmarked],
chapterEntry[ChapterTable.lastPageRead],
chapterEntry[ChapterTable.chapterIndex],
chapterCount.toInt(),
pageList.count()
)
}
fun modifyChapter(mangaId: Int, chapterIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
transaction {
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) { update ->
isRead?.also {
update[ChapterTable.isRead] = it
}
isBookmarked?.also {
update[ChapterTable.isBookmarked] = it
}
lastPageRead?.also {
update[ChapterTable.lastPageRead] = it
}
}
}
markPrevRead?.let {
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex less chapterIndex) }) {
it[ChapterTable.isRead] = markPrevRead
}
}
}
}
}
@@ -0,0 +1,251 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.net.Uri
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.util.PackageTools.EXTENSION_FEATURE
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MAX
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MIN
import ir.armor.tachidesk.impl.util.PackageTools.METADATA_NSFW
import ir.armor.tachidesk.impl.util.PackageTools.METADATA_SOURCE_CLASS
import ir.armor.tachidesk.impl.util.PackageTools.dex2jar
import ir.armor.tachidesk.impl.util.PackageTools.getPackageInfo
import ir.armor.tachidesk.impl.util.PackageTools.getSignatureHash
import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources
import ir.armor.tachidesk.impl.util.PackageTools.trustedSignatures
import ir.armor.tachidesk.impl.util.await
import ir.armor.tachidesk.model.database.table.ExtensionTable
import ir.armor.tachidesk.model.database.table.SourceTable
import ir.armor.tachidesk.server.ApplicationDirs
import mu.KotlinLogging
import okhttp3.Request
import okio.buffer
import okio.sink
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
object Extension {
private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
data class InstallableAPK(
val apkFilePath: String,
val pkgName: String
)
suspend fun installExtension(pkgName: String): Int {
logger.debug("Installing $pkgName")
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
return installAPK {
val apkURL = ExtensionGithubApi.getApkUrl(extensionRecord)
val apkName = Uri.parse(apkURL).lastPathSegment!!
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
// download apk file
downloadAPKFile(apkURL, apkSavePath)
apkSavePath
}
}
suspend fun installAPK(fetcher: suspend () -> String): Int {
val apkFilePath = fetcher()
val apkName = File(apkFilePath).name
// check if we don't have the extension already installed
// if it's installed and we want to update, it first has to be uninstalled
val isInstalled = transaction {
ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()
}?.get(ExtensionTable.isInstalled) ?: false
if (!isInstalled) {
val fileNameWithoutType = apkName.substringBefore(".apk")
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
val jarFilePath = "$dirPathWithoutType.jar"
val dexFilePath = "$dirPathWithoutType.dex"
val packageInfo = getPackageInfo(apkFilePath)
val pkgName = packageInfo.packageName
if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) {
throw Exception("This apk is not a Tachiyomi extension")
}
// Validate lib version
val libVersion = packageInfo.versionName.substringBeforeLast('.').toDouble()
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
throw Exception(
"Lib version is $libVersion, while only versions " +
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
)
}
val signatureHash = getSignatureHash(packageInfo)
if (signatureHash == null) {
throw Exception("Package $pkgName isn't signed")
} else if (signatureHash !in trustedSignatures) {
// TODO: allow trusting keys
throw Exception("This apk is not a signed with the official tachiyomi signature")
}
val isNsfw = packageInfo.applicationInfo.metaData.getString(METADATA_NSFW) == "1"
val className = packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
logger.debug("Main class for extension is $className")
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
// clean up
// File(apkFilePath).delete()
File(dexFilePath).delete()
// collect sources from the extension
val sources: List<CatalogueSource> = when (val instance = loadExtensionSources(jarFilePath, className)) {
is Source -> listOf(instance)
is SourceFactory -> instance.createSources()
else -> throw RuntimeException("Unknown source class type! ${instance.javaClass}")
}.map { it as CatalogueSource }
val langs = sources.map { it.lang }.toSet()
val extensionLang = when (langs.size) {
0 -> ""
1 -> langs.first()
else -> "all"
}
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ")
// update extension info
transaction {
if (ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
ExtensionTable.insert {
it[this.apkName] = apkName
it[name] = extensionName
it[this.pkgName] = packageInfo.packageName
it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode
it[lang] = extensionLang
it[this.isNsfw] = isNsfw
}
}
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
it[this.isInstalled] = true
it[this.classFQName] = className
}
val extensionId = ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!![ExtensionTable.id].value
sources.forEach { httpSource ->
SourceTable.insert {
it[id] = httpSource.id
it[name] = httpSource.name
it[lang] = httpSource.lang
it[extension] = extensionId
}
logger.debug("Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}")
}
}
return 201 // we installed successfully
} else {
return 302 // extension was already installed
}
}
private val network: NetworkHelper by injectLazy()
private suspend fun downloadAPKFile(url: String, savePath: String) {
val request = Request.Builder().url(url).build()
val response = network.client.newCall(request).await()
val downloadedFile = File(savePath)
downloadedFile.sink().buffer().use { sink ->
response.body!!.source().use { source ->
sink.writeAll(source)
sink.flush()
}
}
}
fun uninstallExtension(pkgName: String) {
logger.debug("Uninstalling $pkgName")
val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!! }
val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk")
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
transaction {
val extensionId = extensionRecord[ExtensionTable.id].value
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
if (extensionRecord[ExtensionTable.isObsolete])
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
else
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
it[isInstalled] = false
}
}
if (File(jarPath).exists()) {
File(jarPath).delete()
}
}
suspend fun updateExtension(pkgName: String): Int {
val targetExtension = ExtensionsList.updateMap.remove(pkgName)!!
uninstallExtension(pkgName)
transaction {
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
it[name] = targetExtension.name
it[versionName] = targetExtension.versionName
it[versionCode] = targetExtension.versionCode
it[lang] = targetExtension.lang
it[isNsfw] = targetExtension.isNsfw
it[apkName] = targetExtension.apkName
it[iconUrl] = targetExtension.iconUrl
it[hasUpdate] = false
}
}
return installExtension(pkgName)
}
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
val saveDir = "${applicationDirs.extensionsRoot}/icon"
return getCachedImageResponse(saveDir, apkName) {
network.client.newCall(
GET(iconUrl)
).await()
}
}
fun getExtensionIconUrl(apkName: String): String {
return "/api/v1/extension/icon/$apkName"
}
}
@@ -0,0 +1,132 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
import ir.armor.tachidesk.model.database.table.ExtensionTable
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
import mu.KotlinLogging
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import java.util.concurrent.ConcurrentHashMap
object ExtensionsList {
private val logger = KotlinLogging.logger {}
var lastUpdateCheck: Long = 0
var updateMap = ConcurrentHashMap<String, Extension.Available>()
/** 60,000 milliseconds = 60 seconds */
private const val ExtensionUpdateDelayTime = 60 * 1000
suspend fun getExtensionList(): List<ExtensionDataClass> {
// update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty
if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) {
logger.debug("Getting extensions list from the internet")
lastUpdateCheck = System.currentTimeMillis()
val foundExtensions = ExtensionGithubApi.findExtensions()
updateExtensionDatabase(foundExtensions)
} else {
logger.debug("used cached extension list")
}
return extensionTableAsDataClass()
}
fun extensionTableAsDataClass() = transaction {
ExtensionTable.selectAll().map {
ExtensionDataClass(
it[ExtensionTable.apkName],
getExtensionIconUrl(it[ExtensionTable.apkName]),
it[ExtensionTable.name],
it[ExtensionTable.pkgName],
it[ExtensionTable.versionName],
it[ExtensionTable.versionCode],
it[ExtensionTable.lang],
it[ExtensionTable.isNsfw],
it[ExtensionTable.isInstalled],
it[ExtensionTable.hasUpdate],
it[ExtensionTable.isObsolete],
)
}
}
private fun updateExtensionDatabase(foundExtensions: List<Extension.Available>) {
transaction {
foundExtensions.forEach { foundExtension ->
val extensionRecord = ExtensionTable.select { ExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
if (extensionRecord != null) {
if (extensionRecord[ExtensionTable.isInstalled]) {
when {
foundExtension.versionCode > extensionRecord[ExtensionTable.versionCode] -> {
// there is an update
ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) {
it[hasUpdate] = true
}
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
}
foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode] -> {
// some how the user installed an invalid version
ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) {
it[isObsolete] = true
}
}
}
} else {
// extension is not installed so we can overwrite the data without a care
ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) {
it[name] = foundExtension.name
it[versionName] = foundExtension.versionName
it[versionCode] = foundExtension.versionCode
it[lang] = foundExtension.lang
it[isNsfw] = foundExtension.isNsfw
it[apkName] = foundExtension.apkName
it[iconUrl] = foundExtension.iconUrl
}
}
} else {
// insert new record
ExtensionTable.insert {
it[name] = foundExtension.name
it[pkgName] = foundExtension.pkgName
it[versionName] = foundExtension.versionName
it[versionCode] = foundExtension.versionCode
it[lang] = foundExtension.lang
it[isNsfw] = foundExtension.isNsfw
it[apkName] = foundExtension.apkName
it[iconUrl] = foundExtension.iconUrl
}
}
}
// deal with obsolete extensions
ExtensionTable.selectAll().forEach { extensionRecord ->
val foundExtension = foundExtensions.find { it.pkgName == extensionRecord[ExtensionTable.pkgName] }
if (foundExtension == null) {
// not in the repo, so this extensions is obsolete
if (extensionRecord[ExtensionTable.isInstalled]) {
// is installed so we should mark it as obsolete
ExtensionTable.update({ ExtensionTable.pkgName eq extensionRecord[ExtensionTable.pkgName] }) {
it[isObsolete] = true
}
} else {
// is not installed so we can remove the record without a care
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq extensionRecord[ExtensionTable.pkgName] }
}
}
}
}
}
}
@@ -0,0 +1,56 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.impl.Manga.getManga
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.database.table.toDataClass
import ir.armor.tachidesk.model.dataclass.MangaDataClass
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
object Library {
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
// ..implement that shit at some time...
// ..also Consider to rename it to `isDefault`
suspend fun addMangaToLibrary(mangaId: Int) {
val manga = getManga(mangaId)
if (!manga.inLibrary) {
transaction {
MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = true
}
}
}
}
suspend fun removeMangaFromLibrary(mangaId: Int) {
val manga = getManga(mangaId)
if (manga.inLibrary) {
transaction {
MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = false
it[defaultCategory] = true
}
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
}
}
}
fun getLibraryMangas(): List<MangaDataClass> {
return transaction {
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
MangaTable.toDataClass(it)
}
}
}
}
@@ -0,0 +1,139 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
import ir.armor.tachidesk.impl.Source.getSource
import ir.armor.tachidesk.impl.util.CachedImageResponse.clearCachedImage
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.await
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.model.database.table.MangaStatus
import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.dataclass.MangaDataClass
import ir.armor.tachidesk.server.ApplicationDirs
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import java.io.InputStream
object Manga {
private fun truncate(text: String?, maxLength: Int): String? {
return if (text?.length ?: 0 > maxLength)
text?.take(maxLength - 3) + "..."
else
text
}
suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
return if (mangaEntry[MangaTable.initialized] && !onlineFetch) {
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].toString(),
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
proxyThumbnailUrl(mangaId),
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary],
getSource(mangaEntry[MangaTable.sourceReference]),
false
)
} else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val fetchedManga = source.fetchMangaDetails(
SManga.create().apply {
url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title]
}
).awaitSingle()
transaction {
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.initialized] = true
it[MangaTable.artist] = fetchedManga.artist
it[MangaTable.author] = fetchedManga.author
it[MangaTable.description] = truncate(fetchedManga.description, 4096)
it[MangaTable.genre] = fetchedManga.genre
it[MangaTable.status] = fetchedManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
}
}
clearMangaThumbnail(mangaId)
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].toString(),
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
proxyThumbnailUrl(mangaId),
true,
fetchedManga.artist,
fetchedManga.author,
fetchedManga.description,
fetchedManga.genre,
MangaStatus.valueOf(fetchedManga.status).name,
false,
getSource(mangaEntry[MangaTable.sourceReference]),
true
)
}
}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString()
return getCachedImageResponse(saveDir, fileName) {
getManga(mangaId) // make sure is initialized
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId)
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]!!
source.client.newCall(
GET(thumbnailUrl, source.headers)
).await()
}
}
suspend fun clearMangaThumbnail(mangaId: Int) {
val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString()
clearCachedImage(saveDir, fileName)
}
}
@@ -0,0 +1,102 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.MangasPage
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.model.database.table.MangaStatus
import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.dataclass.MangaDataClass
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
object MangaList {
fun proxyThumbnailUrl(mangaId: Int): String {
return "/api/v1/manga/$mangaId/thumbnail"
}
suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId)
val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).awaitSingle()
} else {
if (source.supportsLatest)
source.fetchLatestUpdates(pageNum).awaitSingle()
else
throw Exception("Source $source doesn't support latest")
}
return mangasPage.processEntries(sourceId)
}
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
val mangasPage = this
val mangaList = transaction {
return@transaction mangasPage.mangas.map { manga ->
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
if (mangaEntry == null) { // create manga entry
val mangaId = MangaTable.insertAndGetId {
it[url] = manga.url
it[title] = manga.title
it[artist] = manga.artist
it[author] = manga.author
it[description] = manga.description
it[genre] = manga.genre
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url
it[sourceReference] = sourceId
}.value
MangaDataClass(
mangaId,
sourceId.toString(),
manga.url,
manga.title,
proxyThumbnailUrl(mangaId),
manga.initialized,
manga.artist,
manga.author,
manga.description,
manga.genre,
MangaStatus.valueOf(manga.status).name
)
} else {
val mangaId = mangaEntry[MangaTable.id].value
MangaDataClass(
mangaId,
sourceId.toString(),
manga.url,
manga.title,
proxyThumbnailUrl(mangaId),
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary]
)
}
}
}
return PagedMangaListDataClass(
mangaList,
mangasPage.hasNextPage
)
}
}
@@ -0,0 +1,100 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.model.database.table.ChapterTable
import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.database.table.PageTable
import ir.armor.tachidesk.model.database.table.SourceTable
import ir.armor.tachidesk.server.ApplicationDirs
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import java.io.File
import java.io.InputStream
object Page {
/**
* A page might have a imageUrl ready from the get go, or we might need to
* go an extra step and call fetchImageUrl to get it.
*/
suspend fun getTrueImageUrl(page: Page, source: HttpSource): String {
if (page.imageUrl == null) {
page.imageUrl = source.fetchImageUrl(page).awaitSingle()
}
return page.imageUrl!!
}
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction {
ChapterTable.select {
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.firstOrNull()!!
}
val chapterId = chapterEntry[ChapterTable.id].value
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
val tachiPage = Page(
pageEntry[PageTable.index],
pageEntry[PageTable.url],
pageEntry[PageTable.imageUrl]
)
if (pageEntry[PageTable.imageUrl] == null) {
val trueImageUrl = getTrueImageUrl(tachiPage, source)
transaction {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
it[imageUrl] = trueImageUrl
}
}
}
val saveDir = getChapterDir(mangaId, chapterId)
File(saveDir).mkdirs()
val fileName = index.toString()
return getCachedImageResponse(saveDir, fileName) {
source.fetchImage(tachiPage).awaitSingle()
}
}
// TODO: rewrite this to match tachiyomi
private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getChapterDir(mangaId: Int, chapterId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId)
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
val chapterDir = when {
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
else -> chapterEntry[ChapterTable.name]
}
val mangaTitle = mangaEntry[MangaTable.title]
val sourceName = source.toString()
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
// make sure dirs exist
File(mangaDir).mkdirs()
return mangaDir
}
}
@@ -1,30 +1,42 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
import ir.armor.tachidesk.impl.MangaList.processEntries
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
fun sourceFilters(sourceId: Long) {
val source = getHttpSource(sourceId)
// source.getFilterList().toItems()
}
object Search {
// TODO
fun sourceFilters(sourceId: Long) {
val source = getHttpSource(sourceId)
// source.getFilterList().toItems()
}
fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
val source = getHttpSource(sourceId)
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first()
return searchManga.processEntries(sourceId)
}
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
val source = getHttpSource(sourceId)
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).awaitSingle()
return searchManga.processEntries(sourceId)
}
fun sourceGlobalSearch(searchTerm: String) {
}
fun sourceGlobalSearch(searchTerm: String) {
// TODO
}
data class FilterWrapper(
val type: String,
val filter: Any
)
data class FilterWrapper(
val type: String,
val filter: Any
)
/**
* Note: Exhentai had a filter serializer (now in SY) that we might be able to steal
*/
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
// return mapNotNull { filter ->
// when (filter) {
@@ -60,3 +72,4 @@ data class FilterWrapper(
// }
// }
// }
}
@@ -0,0 +1,50 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.model.database.table.ExtensionTable
import ir.armor.tachidesk.model.database.table.SourceTable
import ir.armor.tachidesk.model.dataclass.SourceDataClass
import mu.KotlinLogging
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
object Source {
private val logger = KotlinLogging.logger {}
fun getSourceList(): List<SourceDataClass> {
return transaction {
SourceTable.selectAll().map {
SourceDataClass(
it[SourceTable.id].value.toString(),
it[SourceTable.name],
it[SourceTable.lang],
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
getHttpSource(it[SourceTable.id].value).supportsLatest
)
}
}
}
fun getSource(sourceId: Long): SourceDataClass {
return transaction {
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
SourceDataClass(
sourceId.toString(),
source?.get(SourceTable.name),
source?.get(SourceTable.lang),
source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
source?.let { getHttpSource(sourceId).supportsLatest }
)
}
}
}
@@ -0,0 +1,16 @@
package ir.armor.tachidesk.impl.backup
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class BackupFlags(
val includeManga: Boolean,
val includeCategories: Boolean,
val includeChapters: Boolean,
val includeTracking: Boolean,
val includeHistory: Boolean,
)
@@ -0,0 +1,45 @@
package ir.armor.tachidesk.impl.backup.legacy
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import com.github.salomonbrys.kotson.registerTypeAdapter
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import ir.armor.tachidesk.impl.backup.legacy.models.DHistory
import ir.armor.tachidesk.impl.backup.legacy.serializer.CategoryTypeAdapter
import ir.armor.tachidesk.impl.backup.legacy.serializer.ChapterTypeAdapter
import ir.armor.tachidesk.impl.backup.legacy.serializer.HistoryTypeAdapter
import ir.armor.tachidesk.impl.backup.legacy.serializer.MangaTypeAdapter
import ir.armor.tachidesk.impl.backup.legacy.serializer.TrackTypeAdapter
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
import ir.armor.tachidesk.impl.backup.models.MangaImpl
import ir.armor.tachidesk.impl.backup.models.TrackImpl
import java.util.Date
open class LegacyBackupBase {
protected val parser: Gson = when (version) {
2 -> GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
.create()
else -> throw Exception("Unknown backup version")
}
protected var sourceMapping: Map<Long, String> = emptyMap()
protected val errors = mutableListOf<Pair<Date, String>>()
companion object {
internal const val version = 2
}
}
@@ -0,0 +1,154 @@
package ir.armor.tachidesk.impl.backup.legacy
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import com.github.salomonbrys.kotson.set
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.source.LocalSource
import ir.armor.tachidesk.impl.Category.getCategoryList
import ir.armor.tachidesk.impl.CategoryManga.getMangaCategories
import ir.armor.tachidesk.impl.backup.BackupFlags
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
import ir.armor.tachidesk.impl.backup.legacy.models.Backup.CURRENT_VERSION
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
import ir.armor.tachidesk.impl.backup.models.Manga
import ir.armor.tachidesk.impl.backup.models.MangaImpl
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.model.database.table.ChapterTable
import ir.armor.tachidesk.model.database.table.MangaTable
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
object LegacyBackupExport : LegacyBackupBase() {
suspend fun createLegacyBackup(flags: BackupFlags): String? {
// Create root object
val root = JsonObject()
// Create manga array
val mangaEntries = JsonArray()
// Create category array
val categoryEntries = JsonArray()
// Create extension ID/name mapping
val extensionEntries = JsonArray()
// Add values to root
root[Backup.VERSION] = CURRENT_VERSION
root[Backup.MANGAS] = mangaEntries
root[Backup.CATEGORIES] = categoryEntries
root[Backup.EXTENSIONS] = extensionEntries
transaction {
val mangas = MangaTable.select { (MangaTable.inLibrary eq true) }
val extensions: MutableSet<String> = mutableSetOf()
// Backup library manga and its dependencies
mangas.map {
MangaImpl.fromQuery(it)
}.forEach { manga ->
mangaEntries.add(backupMangaObject(manga, flags))
// Maintain set of extensions/sources used (excludes local source)
if (manga.source != LocalSource.ID) {
getHttpSource(manga.source).let {
extensions.add("${it.id}:${it.name}")
}
}
}
// Backup categories
if (flags.includeCategories) {
backupCategories(categoryEntries)
}
// Backup extension ID/name mapping
backupExtensionInfo(extensionEntries, extensions)
}
return parser.toJson(root)
}
private fun backupMangaObject(manga: Manga, options: BackupFlags): JsonElement {
// Entry for this manga
val entry = JsonObject()
// Backup manga fields
entry[Backup.MANGA] = parser.toJsonTree(manga)
val mangaId = manga.id!!.toInt()
// Check if user wants chapter information in backup
if (options.includeChapters) {
// Backup all the chapters
val chapters = ChapterTable.select { ChapterTable.manga eq mangaId }.map { ChapterImpl.fromQuery(it) }
if (chapters.count() > 0) {
val chaptersJson = parser.toJsonTree(chapters)
if (chaptersJson.asJsonArray.size() > 0) {
entry[Backup.CHAPTERS] = chaptersJson
}
}
}
// Check if user wants category information in backup
if (options.includeCategories) {
// Backup categories for this manga
val categoriesForManga = getMangaCategories(mangaId)
if (categoriesForManga.isNotEmpty()) {
val categoriesNames = categoriesForManga.map { it.name }
entry[Backup.CATEGORIES] = parser.toJsonTree(categoriesNames)
}
}
// Check if user wants track information in backup
if (options.includeTracking) { // TODO
// val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
// if (tracks.isNotEmpty()) {
// entry[TRACK] = parser.toJsonTree(tracks)
// }
}
//
// // Check if user wants history information in backup
if (options.includeHistory) { // TODO
// val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
// if (historyForManga.isNotEmpty()) {
// val historyData = historyForManga.mapNotNull { history ->
// val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
// url?.let { DHistory(url, history.last_read) }
// }
// val historyJson = parser.toJsonTree(historyData)
// if (historyJson.asJsonArray.size() > 0) {
// entry[HISTORY] = historyJson
// }
// }
}
return entry
}
private fun backupCategories(root: JsonArray) {
val categories = getCategoryList().map {
CategoryImpl().apply {
name = it.name
order = it.order
}
}
categories.forEach { root.add(parser.toJsonTree(it)) }
}
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
extensions.sorted().forEach {
root.add(it)
}
}
}
@@ -0,0 +1,212 @@
package ir.armor.tachidesk.impl.backup.legacy
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.impl.Category.createCategory
import ir.armor.tachidesk.impl.Category.getCategoryList
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupValidator.ValidationResult
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupValidator.validate
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
import ir.armor.tachidesk.impl.backup.legacy.models.DHistory
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
import ir.armor.tachidesk.impl.backup.models.Chapter
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
import ir.armor.tachidesk.impl.backup.models.Manga
import ir.armor.tachidesk.impl.backup.models.MangaImpl
import ir.armor.tachidesk.impl.backup.models.Track
import ir.armor.tachidesk.impl.backup.models.TrackImpl
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.model.database.table.MangaTable
import mu.KotlinLogging
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import java.io.InputStream
import java.util.Date
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
private val logger = KotlinLogging.logger {}
object LegacyBackupImport : LegacyBackupBase() {
suspend fun restoreLegacyBackup(sourceStream: InputStream): ValidationResult {
val reader = sourceStream.bufferedReader()
val json = JsonParser.parseReader(reader).asJsonObject
val validationResult = validate(json)
val mangasJson = json.get(Backup.MANGAS).asJsonArray
// Restore categories
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
// Store source mapping for error messages
sourceMapping = LegacyBackupValidator.getSourceMapping(json)
// Restore individual manga
mangasJson.forEach {
restoreManga(it.asJsonObject)
}
logger.info {
"""
Restore Errors:
${
errors.map {
"${it.first} - ${it.second}"
}.joinToString("\n")
}
Restore Summary:
- Missing Sources:
${validationResult.missingSources.joinToString("\n")}
- Missing Trackers:
${validationResult.missingTrackers.joinToString("\n")}
""".trimIndent()
}
return validationResult
}
private fun restoreCategories(jsonCategories: JsonElement) {
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
val dbCategories = getCategoryList()
// Iterate over them and create missing categories
backupCategories.forEach { category ->
if (dbCategories.none { it.name == category.name }) {
createCategory(category.name)
}
}
}
private suspend fun restoreManga(mangaJson: JsonObject) {
val manga = parser.fromJson<MangaImpl>(
mangaJson.get(
Backup.MANGA
)
)
val chapters = parser.fromJson<List<ChapterImpl>>(
mangaJson.get(Backup.CHAPTERS)
?: JsonArray()
)
val categories = parser.fromJson<List<String>>(
mangaJson.get(Backup.CATEGORIES)
?: JsonArray()
)
val history = parser.fromJson<List<DHistory>>(
mangaJson.get(Backup.HISTORY)
?: JsonArray()
)
val tracks = parser.fromJson<List<TrackImpl>>(
mangaJson.get(Backup.TRACK)
?: JsonArray()
)
val source = try {
getHttpSource(manga.source)
} catch (e: NullPointerException) {
null
}
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
logger.debug("Restoring Manga: ${manga.title} from $sourceName")
try {
if (source != null) {
restoreMangaData(manga, source, chapters, categories, history, tracks)
} else {
errors.add(Date() to "${manga.title} [$sourceName]: Source not found: $sourceName (${manga.source})")
}
} catch (e: Exception) {
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
}
}
/**
* @param manga manga data from json
* @param source source to get manga data from
* @param chapters chapters data from json
* @param categories categories data from json
* @param history history data from json
* @param tracks tracking data from json
*/
private suspend fun restoreMangaData(
manga: Manga,
source: Source,
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
) {
val fetchedManga = fetchManga(source, manga)
updateChapters(source, fetchedManga, chapters)
// TODO
// backupManager.restoreCategoriesForManga(manga, categories)
// backupManager.restoreHistoryForManga(history)
// backupManager.restoreTrackForManga(manga, tracks)
// updateTracking(fetchedManga, tracks)
}
/**
* Fetches manga information
*
* @param source source of manga
* @param manga manga that needs updating
* @return Updated manga.
*/
private suspend fun fetchManga(source: Source, manga: Manga): SManga {
// make sure we have the manga record in library
transaction {
if (MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }.firstOrNull() == null) {
MangaTable.insert {
it[url] = manga.url
it[title] = manga.title
it[sourceReference] = manga.source
}
}
MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) {
it[MangaTable.inLibrary] = true
}
}
// update manga details
val fetchedManga = source.fetchMangaDetails(manga).awaitSingle()
transaction {
MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) {
it[artist] = fetchedManga.artist
it[author] = fetchedManga.author
it[description] = fetchedManga.description
it[genre] = fetchedManga.genre
it[status] = fetchedManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
}
}
return fetchedManga
}
private fun updateChapters(source: Source, fetchedManga: SManga, chapters: List<Chapter>) {
// TODO("Not yet implemented")
}
}
@@ -0,0 +1,71 @@
package ir.armor.tachidesk.impl.backup.legacy
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import com.google.gson.JsonObject
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
import ir.armor.tachidesk.model.database.table.SourceTable
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
object LegacyBackupValidator {
data class ValidationResult(val missingSources: List<String>, val missingTrackers: List<String>)
/**
* Checks for critical backup file data.
*
* @throws Exception if version or manga cannot be found.
* @return List of missing sources or missing trackers.
*/
fun validate(json: JsonObject): ValidationResult {
val version = json.get(Backup.VERSION)
val mangasJson = json.get(Backup.MANGAS)
if (version == null || mangasJson == null) {
throw Exception("File is missing data.")
}
val mangas = mangasJson.asJsonArray
if (mangas.size() == 0) {
throw Exception("Backup does not contain any manga.")
}
val sources = getSourceMapping(json)
val missingSources = transaction {
sources
.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
.map { "${it.value} (${it.key})" }
.sorted()
}
val trackers = mangas
.filter { it.asJsonObject.has("track") }
.flatMap { it.asJsonObject["track"].asJsonArray }
.map { it.asJsonObject["s"].asInt }
.distinct()
val missingTrackers = listOf("")
// val missingTrackers = trackers
// .mapNotNull { trackManager.getService(it) }
// .filter { !it.isLogged }
// .map { context.getString(it.nameRes()) }
// .sorted()
return ValidationResult(missingSources, missingTrackers)
}
fun getSourceMapping(json: JsonObject): Map<Long, String> {
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
return extensionsMapping.asJsonArray
.map {
val items = it.asString.split(":")
items[0].toLong() to items[1]
}
.toMap()
}
}
@@ -0,0 +1,25 @@
package ir.armor.tachidesk.impl.backup.legacy.models
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Json values
*/
object Backup {
const val CURRENT_VERSION = 2
const val MANGA = "manga"
const val MANGAS = "mangas"
const val TRACK = "track"
const val CHAPTERS = "chapters"
const val CATEGORIES = "categories"
const val EXTENSIONS = "extensions"
const val HISTORY = "history"
const val VERSION = "version"
fun getDefaultFilename(): String {
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
return "tachiyomi_$date.json"
}
}
@@ -0,0 +1,3 @@
package ir.armor.tachidesk.impl.backup.legacy.models
data class DHistory(val url: String, val lastRead: Long)
@@ -0,0 +1,31 @@
package ir.armor.tachidesk.impl.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
/**
* JSON Serializer used to write / read [CategoryImpl] to / from json
*/
object CategoryTypeAdapter {
fun build(): TypeAdapter<CategoryImpl> {
return typeAdapter {
write {
beginArray()
value(it.name)
value(it.order)
endArray()
}
read {
beginArray()
val category = CategoryImpl()
category.name = nextString()
category.order = nextInt()
endArray()
category
}
}
}
}
@@ -0,0 +1,59 @@
package ir.armor.tachidesk.impl.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonToken
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
/**
* JSON Serializer used to write / read [ChapterImpl] to / from json
*/
object ChapterTypeAdapter {
private const val URL = "u"
private const val READ = "r"
private const val BOOKMARK = "b"
private const val LAST_READ = "l"
fun build(): TypeAdapter<ChapterImpl> {
return typeAdapter {
write {
if (it.read || it.bookmark || it.last_page_read != 0) {
beginObject()
name(URL)
value(it.url)
if (it.read) {
name(READ)
value(1)
}
if (it.bookmark) {
name(BOOKMARK)
value(1)
}
if (it.last_page_read != 0) {
name(LAST_READ)
value(it.last_page_read)
}
endObject()
}
}
read {
val chapter = ChapterImpl()
beginObject()
while (hasNext()) {
if (peek() == JsonToken.NAME) {
when (nextName()) {
URL -> chapter.url = nextString()
READ -> chapter.read = nextInt() == 1
BOOKMARK -> chapter.bookmark = nextInt() == 1
LAST_READ -> chapter.last_page_read = nextInt()
}
}
}
endObject()
chapter
}
}
}
}
@@ -0,0 +1,32 @@
package ir.armor.tachidesk.impl.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import ir.armor.tachidesk.impl.backup.legacy.models.DHistory
/**
* JSON Serializer used to write / read [DHistory] to / from json
*/
object HistoryTypeAdapter {
fun build(): TypeAdapter<DHistory> {
return typeAdapter {
write {
if (it.lastRead != 0L) {
beginArray()
value(it.url)
value(it.lastRead)
endArray()
}
}
read {
beginArray()
val url = nextString()
val lastRead = nextLong()
endArray()
DHistory(url, lastRead)
}
}
}
}
@@ -0,0 +1,37 @@
package ir.armor.tachidesk.impl.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import ir.armor.tachidesk.impl.backup.models.MangaImpl
/**
* JSON Serializer used to write / read [MangaImpl] to / from json
*/
object MangaTypeAdapter {
fun build(): TypeAdapter<MangaImpl> {
return typeAdapter {
write {
beginArray()
value(it.url)
value(it.title)
value(it.source)
value(it.viewer)
value(it.chapter_flags)
endArray()
}
read {
beginArray()
val manga = MangaImpl()
manga.url = nextString()
manga.title = nextString()
manga.source = nextLong()
manga.viewer = nextInt()
manga.chapter_flags = nextInt()
endArray()
manga
}
}
}
}
@@ -0,0 +1,59 @@
package ir.armor.tachidesk.impl.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonToken
import ir.armor.tachidesk.impl.backup.models.TrackImpl
/**
* JSON Serializer used to write / read [TrackImpl] to / from json
*/
object TrackTypeAdapter {
private const val SYNC = "s"
private const val MEDIA = "r"
private const val LIBRARY = "ml"
private const val TITLE = "t"
private const val LAST_READ = "l"
private const val TRACKING_URL = "u"
fun build(): TypeAdapter<TrackImpl> {
return typeAdapter {
write {
beginObject()
name(TITLE)
value(it.title)
name(SYNC)
value(it.sync_id)
name(MEDIA)
value(it.media_id)
name(LIBRARY)
value(it.library_id)
name(LAST_READ)
value(it.last_chapter_read)
name(TRACKING_URL)
value(it.tracking_url)
endObject()
}
read {
val track = TrackImpl()
beginObject()
while (hasNext()) {
if (peek() == JsonToken.NAME) {
when (nextName()) {
TITLE -> track.title = nextString()
SYNC -> track.sync_id = nextInt()
MEDIA -> track.media_id = nextInt()
LIBRARY -> track.library_id = nextLong()
LAST_READ -> track.last_chapter_read = nextInt()
TRACKING_URL -> track.tracking_url = nextString()
}
}
}
endObject()
track
}
}
}
}
@@ -0,0 +1,23 @@
package ir.armor.tachidesk.impl.backup.models
import java.io.Serializable
interface Category : Serializable {
var id: Int?
var name: String
var order: Int
var flags: Int
companion object {
fun create(name: String): Category = CategoryImpl().apply {
this.name = name
}
fun createDefault(): Category = create("Default").apply { id = 0 }
}
}
@@ -0,0 +1,24 @@
package ir.armor.tachidesk.impl.backup.models
class CategoryImpl : Category {
override var id: Int? = null
override lateinit var name: String
override var order: Int = 0
override var flags: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val category = other as Category
return name == category.name
}
override fun hashCode(): Int {
return name.hashCode()
}
}
@@ -0,0 +1,31 @@
package ir.armor.tachidesk.impl.backup.models
import eu.kanade.tachiyomi.source.model.SChapter
import java.io.Serializable
interface Chapter : SChapter, Serializable {
var id: Long?
var manga_id: Long?
var read: Boolean
var bookmark: Boolean
var last_page_read: Int
var date_fetch: Long
var source_order: Int
val isRecognizedNumber: Boolean
get() = chapter_number >= 0f
companion object {
fun create(): Chapter = ChapterImpl().apply {
chapter_number = -1f
}
}
}
@@ -0,0 +1,57 @@
package ir.armor.tachidesk.impl.backup.models
import ir.armor.tachidesk.model.database.table.ChapterTable
import org.jetbrains.exposed.sql.ResultRow
class ChapterImpl : Chapter {
override var id: Long? = null
override var manga_id: Long? = null
override lateinit var url: String
override lateinit var name: String
override var scanlator: String? = null
override var read: Boolean = false
override var bookmark: Boolean = false
override var last_page_read: Int = 0
override var date_fetch: Long = 0
override var date_upload: Long = 0
override var chapter_number: Float = 0f
override var source_order: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val chapter = other as Chapter
if (url != chapter.url) return false
return id == chapter.id
}
override fun hashCode(): Int {
return url.hashCode() + id.hashCode()
}
// Tachidesk -->
companion object {
fun fromQuery(chapterRecord: ResultRow): ChapterImpl {
return ChapterImpl().apply {
url = chapterRecord[ChapterTable.url]
read = chapterRecord[ChapterTable.isRead]
bookmark = chapterRecord[ChapterTable.isBookmarked]
last_page_read = chapterRecord[ChapterTable.lastPageRead]
}
}
}
// Tachidesk <--
}
@@ -0,0 +1,42 @@
package ir.armor.tachidesk.impl.backup.models
import java.io.Serializable
/**
* Object containing the history statistics of a chapter
*/
interface History : Serializable {
/**
* Id of history object.
*/
var id: Long?
/**
* Chapter id of history object.
*/
var chapter_id: Long
/**
* Last time chapter was read in time long format
*/
var last_read: Long
/**
* Total time chapter was read - todo not yet implemented
*/
var time_read: Long
companion object {
/**
* History constructor
*
* @param chapter chapter object
* @return history object
*/
fun create(chapter: Chapter): History = HistoryImpl().apply {
this.chapter_id = chapter.id!!
}
}
}
@@ -0,0 +1,27 @@
package ir.armor.tachidesk.impl.backup.models
/**
* Object containing the history statistics of a chapter
*/
class HistoryImpl : History {
/**
* Id of history object.
*/
override var id: Long? = null
/**
* Chapter id of history object.
*/
override var chapter_id: Long = 0
/**
* Last time chapter was read in time long format
*/
override var last_read: Long = 0
/**
* Total time chapter was read - todo not yet implemented
*/
override var time_read: Long = 0
}
@@ -0,0 +1,8 @@
package ir.armor.tachidesk.impl.backup.models
class LibraryManga : MangaImpl() {
var unread: Int = 0
var category: Int = 0
}
@@ -0,0 +1,115 @@
package ir.armor.tachidesk.impl.backup.models
import eu.kanade.tachiyomi.source.model.SManga
// import tachiyomi.source.model.MangaInfo
interface Manga : SManga {
var id: Long?
var source: Long
/** is in library */
var favorite: Boolean
var last_update: Long
var date_added: Long
var viewer: Int
var chapter_flags: Int
var cover_last_modified: Long
fun setChapterOrder(order: Int) {
setFlags(order, SORT_MASK)
}
fun sortDescending(): Boolean {
return chapter_flags and SORT_MASK == SORT_DESC
}
fun getGenres(): List<String>? {
return genre?.split(", ")?.map { it.trim() }
}
private fun setFlags(flag: Int, mask: Int) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
}
// Used to display the chapter's title one way or another
var displayMode: Int
get() = chapter_flags and DISPLAY_MASK
set(mode) = setFlags(mode, DISPLAY_MASK)
var readFilter: Int
get() = chapter_flags and READ_MASK
set(filter) = setFlags(filter, READ_MASK)
var downloadedFilter: Int
get() = chapter_flags and DOWNLOADED_MASK
set(filter) = setFlags(filter, DOWNLOADED_MASK)
var bookmarkedFilter: Int
get() = chapter_flags and BOOKMARKED_MASK
set(filter) = setFlags(filter, BOOKMARKED_MASK)
var sorting: Int
get() = chapter_flags and SORTING_MASK
set(sort) = setFlags(sort, SORTING_MASK)
companion object {
const val SORT_DESC = 0x00000000
const val SORT_ASC = 0x00000001
const val SORT_MASK = 0x00000001
// Generic filter that does not filter anything
const val SHOW_ALL = 0x00000000
const val SHOW_UNREAD = 0x00000002
const val SHOW_READ = 0x00000004
const val READ_MASK = 0x00000006
const val SHOW_DOWNLOADED = 0x00000008
const val SHOW_NOT_DOWNLOADED = 0x00000010
const val DOWNLOADED_MASK = 0x00000018
const val SHOW_BOOKMARKED = 0x00000020
const val SHOW_NOT_BOOKMARKED = 0x00000040
const val BOOKMARKED_MASK = 0x00000060
const val SORTING_SOURCE = 0x00000000
const val SORTING_NUMBER = 0x00000100
const val SORTING_UPLOAD_DATE = 0x00000200
const val SORTING_MASK = 0x00000300
const val DISPLAY_NAME = 0x00000000
const val DISPLAY_NUMBER = 0x00100000
const val DISPLAY_MASK = 0x00100000
fun create(source: Long): Manga = MangaImpl().apply {
this.source = source
}
fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply {
url = pathUrl
this.title = title
this.source = source
}
}
}
// fun Manga.toMangaInfo(): MangaInfo {
// return MangaInfo(
// artist = this.artist ?: "",
// author = this.author ?: "",
// cover = this.thumbnail_url ?: "",
// description = this.description ?: "",
// genres = this.getGenres() ?: emptyList(),
// key = this.url,
// status = this.status,
// title = this.title
// )
// }
@@ -0,0 +1,20 @@
package ir.armor.tachidesk.impl.backup.models
class MangaCategory {
var id: Long? = null
var manga_id: Long = 0
var category_id: Int = 0
companion object {
fun create(manga: Manga, category: Category): MangaCategory {
val mc = MangaCategory()
mc.manga_id = manga.id!!
mc.category_id = category.id!!
return mc
}
}
}
@@ -0,0 +1,3 @@
package ir.armor.tachidesk.impl.backup.models
class MangaChapter(val manga: Manga, val chapter: Chapter)
@@ -0,0 +1,10 @@
package ir.armor.tachidesk.impl.backup.models
/**
* Object containing manga, chapter and history
*
* @param manga object containing manga
* @param chapter object containing chater
* @param history object containing history
*/
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
@@ -0,0 +1,79 @@
package ir.armor.tachidesk.impl.backup.models
import ir.armor.tachidesk.model.database.table.MangaTable
import org.jetbrains.exposed.sql.ResultRow
open class MangaImpl : Manga {
override var id: Long? = 0
override var source: Long = -1
override lateinit var url: String
override lateinit var title: String
override var artist: String? = null
override var author: String? = null
override var description: String? = null
override var genre: String? = null
override var status: Int = 0
override var thumbnail_url: String? = null
override var favorite: Boolean = false
override var last_update: Long = 0
override var date_added: Long = 0
override var initialized: Boolean = false
/** Reader mode value
* ref: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt#L8
* 0 -> Default
* 1 -> Left to Right
* 2 -> Right to Left
* 3 -> Vertical
* 4 -> Webtoon
* 5 -> Continues Vertical
*/
override var viewer: Int = 0
/** Contains some useful info about
*/
override var chapter_flags: Int = 0
override var cover_last_modified: Long = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val manga = other as Manga
if (url != manga.url) return false
return id == manga.id
}
override fun hashCode(): Int {
return url.hashCode() + id.hashCode()
}
// Tachidesk -->
companion object {
fun fromQuery(mangaRecord: ResultRow): MangaImpl {
return MangaImpl().apply {
url = mangaRecord[MangaTable.url]
title = mangaRecord[MangaTable.title]
source = mangaRecord[MangaTable.sourceReference]
viewer = 0 // TODO: implement
chapter_flags = 0 // TODO: implement
}
}
}
// Tachidesk <--
}

Some files were not shown because too many files have changed in this diff Show More