Compare commits

...

309 Commits

Author SHA1 Message Date
Aria Moradi 2c6d043277 bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 15s
2021-11-29 19:24:07 +03:30
Aria Moradi 9a226f7b64 update 2021-11-29 19:14:40 +03:30
Aria Moradi 3b73a0fd72 empty searchTerm support (#259)
* linter run

* convert search params to queryParams
2021-11-29 18:42:53 +03:30
Aria Moradi 2478aa77cd add support for MultiSelectListPreference (#258)
* add support for MultiSelectListPreference

* Update AndroidCompat/src/main/java/xyz/nulldev/androidcompat/io/sharedprefs/JavaSharedPreferences.kt

Co-authored-by: Mitchell Syer <Mitchellptbo@gmail.com>

* don't convert to list

* fix by @Syer10

Co-authored-by: Mitchell Syer <Mitchellptbo@gmail.com>
2021-11-28 23:11:56 +03:30
Aria Moradi a41068dbc9 migrate application directories (#255) 2021-11-28 19:56:06 +03:30
Aria Moradi 5e47b7ae6b Update README.md 2021-11-16 21:37:44 +03:30
Aria Moradi debf45a7d5 Update README.md 2021-11-16 21:09:16 +03:30
Aria Moradi e7041e8c8c Fix first page not being detected correctly (#253)
* fix first page not being recognized correctly

* fix first page not being recognized correctly
2021-11-15 23:49:02 +03:30
Aria Moradi bd960992bc cleanup directory names (#251) 2021-11-15 23:40:49 +03:30
Aria Moradi 0c5f6b432c update 2021-11-15 12:22:49 +03:30
Aria Moradi 49232edbd5 update 2021-11-15 12:06:04 +03:30
Mitchell Syer b02884f58d Add a Kotlin DSL for endpoint documentation (#249) 2021-11-14 18:16:39 +03:30
Aria Moradi 845b588426 Mention the existence of Mahor's Tachidesk-GTK 2021-11-13 13:51:20 +03:30
Aria Moradi 3a97d7c8be update 2021-11-13 13:27:17 +03:30
Sascha Hahne 2cb2ded2d9 Implement Update of Library/Category (#235)
* Implement Update Controller tests

* Basic Threading and notify

* WIP

* Reworked using coroutines

* Use Map for JobSummary Tracking

* Change Tests

* Clean up

* Changes based on review

* Rethrow cancellationexception

* Clean up

* Fix Merge Error

* Actually handle messages

* Clean up

* Remove useless annotation
2021-11-10 22:38:41 +03:30
Aria Moradi 14e02bee6c update 2021-11-10 12:17:26 +03:30
Aria Moradi 30f7cdc1ba add pagination to recentChapters (#246)
* add pagination to recentChapters

* Use kotlin native library

Co-authored-by: Mitchell Syer <Mitchellptbo@gmail.com>

Co-authored-by: Mitchell Syer <Mitchellptbo@gmail.com>
2021-11-08 18:45:52 +03:30
Mitchell Syer 420d14fc37 Fix Manga Meta, add Manga Meta test (#245)
* Fix Manga Meta, add Manga Meta test

* Tweak assertion strings
2021-11-08 03:08:22 +03:30
Aria Moradi 3d7953d977 add manga data to download queue object (#244)
* add manga data to download queue object

* fix lint erro
2021-11-07 21:32:57 +03:30
Aria Moradi 35238b3da1 stop supporting zero based image storage (#242)
* stop supporting zero based image storage, closes #210

* add test
2021-11-07 21:27:11 +03:30
Aria Moradi 446f4283e0 Update CONTRIBUTING.md 2021-11-02 15:58:30 +03:30
Aria Moradi 8062dd3709 convert request type 2021-11-02 12:14:44 +03:30
Aria Moradi 41c8fde8c5 ignore build artifacts generated by teting 2021-11-02 12:13:33 +03:30
Aria Moradi d90b986d19 implement Source Filters 2021-11-02 04:15:08 +03:30
Aria Moradi 64ea8416b2 refactor 2021-11-01 23:46:46 +03:30
Aria Moradi 100a4c9d35 refactor 2021-11-01 18:02:06 +03:30
Aria Moradi 4ef6dec89a cleanup 2021-11-01 02:10:51 +03:30
Aria Moradi a14cdc48bd fix credit 2021-11-01 02:09:14 +03:30
Aria Moradi 8a4ddbc6df update 2021-11-01 02:05:02 +03:30
Aria Moradi ee33acc561 allow injecting Sources 2021-10-31 18:05:55 +03:30
Aria Moradi 5fe69becf3 improve tests 2021-10-31 17:31:46 +03:30
Aria Moradi d460d3ccdf change windows bundle names 2021-10-31 14:06:59 +03:30
Sascha Hahne 0ee74943e8 Fix category reorder Endpoint. Added Test for Category Reorder (#232)
* Fix category reorder Endpoint. Added Test for Category Reorder

* Remove streams and use kotlin filtering
2021-10-30 14:46:22 +03:30
Aria Moradi 82837e38d2 update 2021-10-30 14:25:23 +03:30
Sascha Hahne 91df90d760 Fix broken test (#231) 2021-10-29 19:40:20 +03:30
Mitchell Syer 1ee37da720 Fix unread and download counts casing (#230) 2021-10-28 19:11:10 +03:30
Aria Moradi 6f8fc5b69d Update README.md 2021-10-28 12:35:24 +03:30
Sascha Hahne be1918c769 add Cache Header to Thumbnail Response for improved library performance (#228) 2021-10-26 21:46:38 +03:30
Sascha Hahne 0057b35a0a Expose unread and download count of Manga in category api (#227)
* #224 Created view for unread and download badges

* #224 Basic test structure

* Created test and cleaned up a bit

* Move counts to MangaDataClass and delete MangaViewDataClass

* Readded trailing space

* Removed SQL view and calculate with joins now
2021-10-25 13:41:07 +03:30
Sascha Hahne d12974702a Fix tests (#226) 2021-10-24 23:19:27 +03:30
Aria Moradi 921c41689d update 2021-10-24 23:08:56 +03:30
Aria Moradi 6389899507 remove anime support 2021-10-24 22:58:25 +03:30
Aria Moradi 92ede2a2b3 Update README.md 2021-10-24 17:28:02 +03:30
Aria Moradi 826a63ed71 Update README.md 2021-10-24 16:57:06 +03:30
Aria Moradi d1576a2a72 Update README.md 2021-10-24 14:27:12 +03:30
Aria Moradi 95f218d704 Update README.md 2021-10-23 20:36:45 +03:30
Aria Moradi 315d3a0ac0 Update CONTRIBUTING.md 2021-10-23 20:04:10 +03:30
Aria Moradi 954818cef2 Update CONTRIBUTING.md 2021-10-23 20:02:42 +03:30
Aria Moradi 5a95ca9b1b Update CONTRIBUTING.md 2021-10-23 20:00:35 +03:30
Aria Moradi c1e6f4c26e better cleaning algorithm 2021-10-23 19:27:50 +03:30
Aria Moradi 7c603258fb Update README.md 2021-10-21 14:32:58 +03:30
Mahor fcbc582686 Update README.md (#223)
Add PPA's description for Ubuntu-based distributions
2021-10-20 12:15:46 +03:30
Aria Moradi 9c4906b90b cleanup 2021-10-20 08:34:07 +03:30
Aria Moradi d35e31e02d bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 16s
CI Publish / Build artifacts and release (push) Failing after 15s
2021-10-18 14:41:01 +03:30
Aria Moradi 6c4ca36c09 update WebUI 2021-10-18 14:37:13 +03:30
Aria Moradi 70355dc505 update 2021-10-11 00:21:10 +03:30
Aria Moradi 20aeaf2a05 update 2021-10-11 00:18:49 +03:30
Mitchell Syer 1f13e1d08b Use a custom task to run electron (#220) 2021-10-11 00:16:32 +03:30
Aria Moradi eb9d35c123 code cleanup 2021-10-11 00:15:15 +03:30
Mitchell Syer 45808cd530 Support using a CatalogueSource instead of only HttpSources (#219) 2021-10-11 00:01:04 +03:30
Aria Moradi fd715a3f92 fix workflow 2021-10-10 23:52:35 +03:30
Aria Moradi e3b32367a7 update 2021-10-10 23:51:21 +03:30
Aria Moradi bf9554a746 update 2021-10-10 23:32:19 +03:30
Aria Moradi b9f8ca1488 update 2021-10-10 23:01:58 +03:30
Aria Moradi e8c4159678 use correct conversion units 2021-10-10 21:31:38 +03:30
Aria Moradi e57e71629e update 2021-10-10 21:05:24 +03:30
Aria Moradi 2bfd9d24a4 remove isNsfw annotation detection 2021-10-10 21:04:28 +03:30
Aria Moradi b18b8fe22f update 2021-10-10 20:58:10 +03:30
Aria Moradi b154ff2f9d fix export chapter ordering, include new props in backup 2021-10-10 20:53:50 +03:30
Aria Moradi e9b764b63c update 2021-10-10 12:27:50 +03:30
Aria Moradi 7216b97d92 mimic Tachyomi's behaviour more closely, fixes ReadComicOnline (EN) 2021-10-10 12:18:21 +03:30
Aria Moradi 0e9d93b194 prepare CHANGELOG 2021-10-06 21:18:21 +03:30
Aria Moradi 2cbee62f0a update docs 2021-09-28 18:06:20 +03:30
Aria Moradi 379e9da5fe clenup 2021-09-28 18:00:24 +03:30
Aria Moradi ae7caa4901 merge 2021-09-28 17:57:55 +03:30
Aria Moradi cd8b4c9dd7 convert android.jar lib to a maven repo 2021-09-28 17:55:50 +03:30
Aria Moradi 60cd61dfd2 Update README.md 2021-09-28 03:42:20 +03:30
Aria Moradi 5a6637d9fc Update README.md 2021-09-28 03:39:45 +03:30
Aria Moradi dca7ed23f5 Update README.md 2021-09-28 03:36:06 +03:30
Aria Moradi 8cb5791f3b Update README.md 2021-09-28 03:34:14 +03:30
Aria Moradi 9b67f2c58f Update CHANGELOG.md 2021-09-28 03:24:57 +03:30
Aria Moradi 3815810d4f Update for release 2021-09-28 01:11:24 +03:30
Aria Moradi 819ceba17d bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 16s
2021-09-28 00:52:49 +03:30
Aria Moradi 0aa0d62e03 update changelog file and it's template 2021-09-28 00:51:05 +03:30
Aria Moradi b3e2a35880 update WebUI 2021-09-28 00:50:33 +03:30
Aria Moradi 15ec20c65d fix sorting 2021-09-27 20:27:40 +03:30
Aria Moradi d4d6d7e12f add recentChapters endpoint 2021-09-27 18:27:05 +03:30
Aria Moradi 2e7a4f1421 remove no longer relevant comment 2021-09-27 14:44:48 +03:30
Aria Moradi ab8a52faf3 rename ChapterTable.chapterIndex to ChapterTable.sourceOrder 2021-09-27 14:36:06 +03:30
Aria Moradi bd465559fb Update README.md 2021-09-26 23:48:29 +03:30
Aria Moradi 13ec45a95c aftermath of adding kotlinter to all modules 2021-09-25 04:34:02 +03:30
Mitchell Syer 13b034875b Workaround StdLib issue and add KtLint to all modules (#206)
* Workaround buildconfig kotlin stdlib issue

* Add KtLint to all modules
2021-09-25 04:31:03 +03:30
Aria Moradi bb701fb088 fix macOS-arm64 java path 2021-09-24 14:06:19 +03:30
Aria Moradi b367414865 changes 2021-09-24 13:56:26 +03:30
Aria Moradi 4b00eec608 update CHANGELOG 2021-09-19 18:01:13 +04:30
Aria Moradi 5e11b51152 update CHANGELOG 2021-09-19 17:59:37 +04:30
Aria Moradi 9fb43b996e CHANGELOG update 2021-09-19 17:39:28 +04:30
Aria Moradi bc2072e81f bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 13s
CI Publish / Build artifacts and release (push) Failing after 15s
2021-09-19 17:36:46 +04:30
Aria Moradi f36bc3f643 update WebUI 2021-09-19 17:34:18 +04:30
Aria Moradi f7901ad843 fix windows paths 2021-09-19 16:43:16 +04:30
Aria Moradi 3771030ed6 closes #202 2021-09-19 14:24:13 +04:30
Aria Moradi 57197e58b5 fix Task path 2021-09-19 14:14:42 +04:30
Aria Moradi ac601399ac update WebUI 2021-09-19 14:14:21 +04:30
Aria Moradi 6a0e221153 fix compile 2021-09-19 01:01:20 +04:30
Aria Moradi 6a949fc851 Minor cleanup 2021-09-19 00:59:04 +04:30
Aria Moradi f1a077dc2f update CHANGELOG 2021-09-18 22:09:34 +04:30
Mitchell Syer f20962b02b Gradle Updates (#199)
* Cleanup and update gradle, update dependencies

* Duplicate Jsoup
2021-09-18 22:07:19 +04:30
Mitchell Syer 77e057f244 Update BytecodeEditor to use Java NIO Paths (#200) 2021-09-18 21:57:15 +04:30
Aria Moradi 2c5114c770 bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 13s
CI Publish / Build artifacts and release (push) Failing after 16s
2021-09-18 21:07:38 +04:30
Aria Moradi a30895a199 update WebUI 2021-09-18 21:03:01 +04:30
Aria Moradi 6d46d4b3da closes #131 2021-09-18 20:30:56 +04:30
Aria Moradi 006efbbb77 add ChapterRecognition from tachiyomi, closes #10 2021-09-18 19:40:44 +04:30
Aria Moradi 52334087ad add support for Archive chapters to Local source 2021-09-18 19:14:06 +04:30
Aria Moradi ea8fb2c70a add comment 2021-09-18 16:11:25 +04:30
Aria Moradi 531d148718 don't save Local source chapters into disk again! 2021-09-18 14:53:59 +04:30
Aria Moradi d83ddea323 better URLs 2021-09-18 14:49:38 +04:30
Aria Moradi b5dea34090 update WebUI 2021-09-18 02:39:15 +04:30
Aria Moradi 7d8e3202b5 lint 2021-09-18 02:28:52 +04:30
Aria Moradi d956e0af4b add Local Source (#196) 2021-09-18 02:20:30 +04:30
Aria Moradi 9ad70990b5 update WebUI 2021-09-18 02:19:43 +04:30
Aria Moradi 82d711f077 fix typo 2021-09-18 02:19:38 +04:30
Aria Moradi 5fc28ef711 handle when title is changing properly 2021-09-18 02:10:35 +04:30
Aria Moradi 1f3dc682e2 migrate to kotlinx.json 2021-09-18 01:43:10 +04:30
Aria Moradi cce7768246 clean up imports 2021-09-18 01:03:06 +04:30
Aria Moradi 01172b0664 better exception messages 2021-09-18 01:00:42 +04:30
Aria Moradi 1ca11fdd34 add Local Source 2021-09-18 00:47:50 +04:30
Aria Moradi 52a064ae45 lint 2021-09-17 16:32:14 +04:30
Aria Moradi 6c62ddf927 depricate zero based chapters 2021-09-17 16:31:47 +04:30
Aria Moradi 47e04c08d0 fix lint complain 2021-09-17 15:17:18 +04:30
Aria Moradi d1601874b4 [SKIP CI] update CHANGELOG.md 2021-09-17 01:31:13 +04:30
Aria Moradi 6bc19af041 add ability to delete downloaded chapters 2021-09-17 01:29:42 +04:30
Aria Moradi 4e72a3886f actaully make sure the chapter exists 2021-09-17 00:38:15 +04:30
Aria Moradi 8e6b219eea update CHANGELOG.md 2021-09-14 04:18:19 +04:30
Aria Moradi bd638251e4 add BasicAuth support 2021-09-14 04:17:01 +04:30
Aria Moradi 0173d5e4b3 migrate to Javalin 4 2021-09-14 03:23:00 +04:30
Aria Moradi 4f364e134b remove expand char limit on MangaTable columns 2021-09-13 20:57:41 +04:30
Aria Moradi 94d2519717 add CHANGELOG for the next release 2021-09-13 20:14:02 +04:30
Aria Moradi 6a11d2e357 bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build artifacts and release (push) Failing after 16s
2021-09-13 19:32:05 +04:30
Aria Moradi 0a9e0bc9e4 update WebUI 2021-09-13 19:31:07 +04:30
Aria Moradi 5914e367d1 better CHANGELOG 2021-09-13 19:29:54 +04:30
Mitchell Syer aeaed888d4 Update Proto models to match Tachi preview (#192) 2021-09-12 04:04:48 +04:30
Aria Moradi a0f054b005 CHANGELOG.md 2021-09-11 19:47:06 +04:30
Aria Moradi 4498e9d444 beter handling of uninstalling Extensions 2021-09-11 16:26:04 +04:30
Aria Moradi 43e0763fef fix a bug where if two sources return the same URL, a false duplicate might be detected 2021-09-11 14:47:03 +04:30
Aria Moradi a519c8a482 better PR links 2021-09-11 03:18:36 +04:30
Aria Moradi 19bc595a2a update CHANGELOG.md 2021-09-11 03:13:26 +04:30
Aria Moradi db07825c58 remove println instance 2021-09-11 03:09:26 +04:30
Aria Moradi b199e3bf0e fix installing apk with weird name 2021-09-11 01:21:22 +04:30
Aria Moradi 1f9ea0891e add some logging 2021-09-11 00:48:17 +04:30
Aria Moradi 0a7aa48f1e fix apk log and apk name 2021-09-11 00:14:52 +04:30
Aria Moradi 4b65b7da6c rm dummy 2021-09-10 21:42:52 +04:30
Mosei 183a7dac4b Create docker_build_stable (#184)
* Create docker_build_stable

Add docker build workflow

* Rename docker_build_stable to docker_build_stable.yml

* Update docker_build_stable.yml

* Update docker_build_stable.yml
2021-09-10 21:42:31 +04:30
Aria Moradi 6726f008c1 add dummy file to trigger workflow 2021-09-10 21:22:28 +04:30
Mosei 89cf0c140f Update publish.yml (#190) 2021-09-10 20:49:57 +04:30
Mosei 504025ce80 Update build_push.yml (#191) 2021-09-10 20:49:33 +04:30
Aria Moradi fee9e914f1 fix exposed crash 2021-09-10 00:45:00 +04:30
Aria Moradi 76efa71c68 update WebUI 2021-09-09 23:05:58 +04:30
Aria Moradi 26e61959ae update WebUI 2021-09-09 16:46:19 +04:30
Aria Moradi 9a8956ef9d update dependencies 2021-09-09 16:41:41 +04:30
Aria Moradi 10d3ffc2f6 better UX 2021-09-09 06:05:09 +04:30
Aria Moradi 090399f61d add support for installing external APK 2021-09-09 05:22:45 +04:30
Aria Moradi ae7d975a92 run won't get stuck now 2021-09-09 05:15:51 +04:30
Aria Moradi 55ec6bcafe add TODO for changing later 2021-09-09 05:15:08 +04:30
Aria Moradi f0566d15af fix compiler warning 2021-09-08 20:27:29 +04:30
Aria Moradi a730b692bc update WebUI 2021-09-08 20:24:22 +04:30
Aria Moradi 826d767423 [SKIP CI] update CHANGELOG.md 2021-09-08 20:23:45 +04:30
Aria Moradi 6d227c7fcd Fix typo 2021-09-06 22:23:26 +04:30
Aria Moradi 7d9d97840e Update README.md 2021-09-06 14:41:00 +04:30
Mosei 110ded45a0 Update README.md (#189)
Remove depreciated arbuilder’s docker link
2021-09-05 21:16:25 +04:30
Aria Moradi 7872b593c7 Credit to @Syer10 2021-09-05 17:50:45 +04:30
Aria Moradi 90be30bddb add support for the new gnere type in WebUI 2021-09-05 01:40:34 +04:30
Mitchell Syer a298c61dab Change type of Manga.genre to a List<String> (#188) 2021-09-05 01:36:52 +04:30
Aria Moradi eb416c45bd remove not-code changes from CHANGELOG 2021-09-05 00:54:01 +04:30
Aria Moradi b05b817aeb Update README.md 2021-09-03 18:52:06 +04:30
Aria Moradi 666602283a update
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 15s
2021-09-03 07:59:41 +04:30
Aria Moradi ac040a4bae fix oopsies n'shit 2021-09-03 07:57:53 +04:30
Aria Moradi b4982c8f22 update 2021-09-03 07:55:15 +04:30
Aria Moradi ce3ad92095 remove node module cache, won't need it anymore 2021-09-03 07:54:07 +04:30
Aria Moradi 8e1ac8698c bump version 2021-09-03 07:48:31 +04:30
Aria Moradi b60a39c7cb update 2021-09-03 07:46:11 +04:30
Aria Moradi 3b3e8ba4c8 add new unix bundles to release 2021-09-03 07:45:11 +04:30
Aria Moradi e387f6d3be update 2021-09-03 07:44:00 +04:30
Aria Moradi 799d469cb6 bump electron version 2021-09-03 07:43:13 +04:30
Aria Moradi a54a596fa7 fix macOS bundler 2021-09-03 07:39:28 +04:30
Aria Moradi 92d73d0285 add linux and macOS bundler script 2021-09-03 06:50:42 +04:30
Aria Moradi acb752e4e8 update jre 2021-09-03 05:43:33 +04:30
Aria Moradi 9e377abba6 update CHANGELOG 2021-09-03 04:55:01 +04:30
Aria Moradi 04552c0923 update WebUI 2021-09-03 04:52:34 +04:30
Aria Moradi 2e1fb85b73 [skip ci] update changelog 2021-09-03 04:49:38 +04:30
Aria Moradi 50db32d9b4 this is dumb 2021-09-03 04:48:32 +04:30
Aria Moradi 28743d953e [skip ci] r857 2021-09-03 04:41:00 +04:30
Aria Moradi b109d26aa7 fixed typo 2021-09-03 04:39:45 +04:30
Aria Moradi 7fd21bd06d [skip ci] r855 2021-09-02 17:45:52 +04:30
Aria Moradi 12e0ffb466 text cleanup 2021-09-02 17:43:43 +04:30
Aria Moradi 9259341df8 missed this changelog 2021-09-02 17:31:30 +04:30
Aria Moradi 8e8aca7e7b Update CONTRIBUTING.md 2021-09-02 17:29:26 +04:30
Aria Moradi 7f0bcd987b Update CONTRIBUTING.md 2021-09-02 17:24:32 +04:30
Aria Moradi ef21de95cb add continues changelog file 2021-09-02 17:20:53 +04:30
Aria Moradi ca3246de02 bump version 2021-09-02 15:21:27 +04:30
Aria Moradi f0940b7926 bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 15s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-08-31 17:43:23 +04:30
Aria Moradi 0066e0b901 suppress warnings 2021-08-31 17:33:29 +04:30
Aria Moradi 9771f566b0 better comments 2021-08-30 02:48:10 +04:30
Aria Moradi 38ad4c6dec refactor 2021-08-30 02:44:35 +04:30
Aria Moradi 37cf80a188 code cleanup 2021-08-30 02:38:15 +04:30
Aria Moradi c86ee53f66 resolve compiler warnings 2021-08-29 22:25:43 +04:30
Aria Moradi c2cea7e797 can serialize Search Filters 2021-08-29 22:19:44 +04:30
Aria Moradi a8ef6cdd4f change category re-order url 2021-08-29 21:52:23 +04:30
Aria Moradi 53d157fee8 update 2021-08-29 21:47:09 +04:30
Aria Moradi c2e07b13f6 fix categories not being normalized 2021-08-29 20:09:17 +04:30
Aria Moradi 2e8cc48311 update WebUI 2021-08-29 02:11:11 +04:30
Aria Moradi f6f811eb77 update WebUI 2021-08-29 01:59:36 +04:30
Aria Moradi ac5528fb15 add when the statement was true 2021-08-27 04:49:53 +04:30
Aria Moradi 940d2b7862 bump version 2021-08-26 22:31:06 +04:30
Aria Moradi 835fe3dad3 sorround with try, catch as it might throw an exception 2021-08-26 22:24:54 +04:30
Aria Moradi dfaecc08c5 add realUrl to Manga, reperesents open in WebView URL 2021-08-26 22:11:51 +04:30
Aria Moradi 87f5e9b847 fix migration number 2021-08-26 22:10:51 +04:30
Aria Moradi 3d3939e808 better logs 2021-08-26 22:10:27 +04:30
Aria Moradi 90822e3858 merge manga data while restoring backup 2021-08-26 16:28:45 +04:30
Aria Moradi 14eec47e9c correct value for inLibrary 2021-08-26 01:34:56 +04:30
Aria Moradi 15ed3fcc69 actual fix for source order 2021-08-26 01:31:59 +04:30
Aria Moradi fd8fa9f3ef fix chapter restore order 2021-08-26 01:28:42 +04:30
Aria Moradi b81075f4a7 fix docker builds faling? 2021-08-24 22:23:39 +04:30
Aria Moradi f11a52e8e1 we don't need that feild anymore 2021-08-24 22:23:00 +04:30
Aria Moradi 9c007483d4 better method of detemining if a source is Nsfw 2021-08-24 02:44:13 +04:30
Aria Moradi ff4e818e4c add some comments 2021-08-23 21:48:27 +04:30
Aria Moradi 45a50ca0c1 add isNsfw to SourceDataClass 2021-08-23 21:46:28 +04:30
Aria Moradi 65d9021c37 close response 2021-08-23 06:10:31 +04:30
Aria Moradi 66481a0391 NPE fix suggested by @syer10 2021-08-23 06:05:04 +04:30
Aria Moradi a14a82bc9a fix oppsie, sync dependencies with tachiyomi 2021-08-23 05:27:39 +04:30
Aria Moradi 756c57a16e also intercept on 403 2021-08-23 04:56:27 +04:30
Aria Moradi 8b19e34dc5 Update README.md 2021-08-23 04:38:32 +04:30
Aria Moradi 50083019ee add copyright notices 2021-08-23 04:37:30 +04:30
Aria Moradi 155272e638 add new keys 2021-08-23 04:28:07 +04:30
Aria Moradi 08443ceb3d remove comment 2021-08-23 04:20:04 +04:30
Aria Moradi c215696f04 have a lighter log level 2021-08-23 04:17:40 +04:30
Aria Moradi 5ca42bf9b6 make it compile 2021-08-23 04:02:55 +04:30
Aria Moradi 3272b9dec5 add CloudflareInterceptor from TachiWeb-Server 2021-08-23 03:45:10 +04:30
Aria Moradi 2ebd5da4aa bump kotlinter version 2021-08-22 19:00:33 +04:30
Aria Moradi 34f024ace2 migrate dex2jar dependency to @ThexXTURBOXx version 2021-08-21 16:36:34 +04:30
Aria Moradi b31f2d50f6 No more legacy backup 2021-08-21 06:39:12 +04:30
Aria Moradi da44d3b2b4 bump to v0.4.7
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-08-21 06:30:45 +04:30
Aria Moradi 99ec2aca6a update WebUI 2021-08-21 06:27:14 +04:30
Aria Moradi 6c278604ec got rid of legacy backups 2021-08-21 06:23:58 +04:30
Aria Moradi 1e094a467a not TODO 2021-08-21 06:13:55 +04:30
Aria Moradi 978ccfeeba the true commit 2021-08-21 06:12:22 +04:30
Aria Moradi e93d66d8a1 add backup validation endpoints 2021-08-21 06:08:17 +04:30
Aria Moradi c29a749833 proto export support 2021-08-21 05:48:05 +04:30
Aria Moradi b08d5d1261 all forms of Default are illegal 2021-08-21 05:10:09 +04:30
Aria Moradi 9b129789e9 all forms of Default are illegal 2021-08-21 05:05:01 +04:30
Aria Moradi a76a6d2798 creating a categort named Default is illegal 2021-08-21 03:58:46 +04:30
Aria Moradi 086a760378 update WebUI 2021-08-21 03:55:22 +04:30
Aria Moradi f78c8d4fd8 Default is now a category, no more library 2021-08-21 03:54:16 +04:30
Aria Moradi 7b91489997 better print 2021-08-21 01:20:18 +04:30
Aria Moradi 36a8980c95 TODO block no longer relevant 2021-08-21 01:11:47 +04:30
Aria Moradi 7c65640cb7 include extra chapter data in restore 2021-08-21 00:37:50 +04:30
Aria Moradi d70e68495a restoring with clean db and not installed extensions work 2021-08-21 00:18:03 +04:30
Aria Moradi 2586202772 better comments 2021-08-19 21:11:53 +04:30
Aria Moradi b5f771368a put back dex2jar where it should be 2021-08-19 03:15:35 +04:30
Aria Moradi 0c28320ce3 better debug launcher 2021-08-19 02:54:36 +04:30
Aria Moradi c8b4fbc36b new observation 2021-08-19 02:06:48 +04:30
Aria Moradi e9b07849fe move dex2jar to server, lint 2021-08-19 01:47:26 +04:30
Aria Moradi 409260af6f Merge pull request #176 from Suwayomi/protobuf
protobuf backup support
2021-08-19 00:47:32 +04:30
Aria Moradi d3d53d1a4e initial support for portobuf backup 2021-08-19 00:46:45 +04:30
Aria Moradi e2db191f70 consolidate the external backup api 2021-08-18 23:34:39 +04:30
Aria Moradi d61816734d add all proto backup classes we need 2021-08-18 22:58:56 +04:30
Aria Moradi f4dad8058f Merge branch 'master' into protobuf 2021-08-18 21:51:12 +04:30
Aria Moradi 70bdb375c3 Update README.md 2021-08-18 19:10:46 +04:30
Aria Moradi e724ab0a29 Update README.md 2021-08-18 19:10:16 +04:30
Aria Moradi 7d0ee2ac11 Update README.md 2021-08-18 06:17:04 +04:30
Aria Moradi 59b7e852e2 Update README.md 2021-08-18 06:15:44 +04:30
Aria Moradi b2eb1a391d Update README.md 2021-08-18 05:31:34 +04:30
Aria Moradi 9b3aee98d3 Update CONTRIBUTING.md 2021-08-18 05:30:19 +04:30
Aria Moradi 0476f4144c Update CONTRIBUTING.md 2021-08-18 05:29:24 +04:30
Aria Moradi ed77f45fae Update CONTRIBUTING.md 2021-08-18 05:28:26 +04:30
Aria Moradi 0cd529e746 Update CONTRIBUTING.md 2021-08-18 05:26:55 +04:30
Aria Moradi 5969048318 Update README.md 2021-08-18 05:19:42 +04:30
Aria Moradi d1a7f8baa0 Update README.md 2021-08-18 05:04:22 +04:30
Aria Moradi 18dc936002 Update README.md (#177)
* Update README.md

* Update README.md
2021-08-18 04:50:10 +04:30
Aria Moradi b4b7b5d572 bump to v0.4.6
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-08-18 04:29:14 +04:30
Aria Moradi 291c2e692d clean up build.gradle files, move constants to buildSrc 2021-08-18 04:24:58 +04:30
Aria Moradi 8a9a4f21b1 remove some stuff we don't use 2021-08-18 04:06:13 +04:30
Aria Moradi cd31332b39 better download progress 2021-08-18 03:55:52 +04:30
Aria Moradi cc8d2162a0 fix compile issue 2021-08-18 02:59:07 +04:30
Aria Moradi e6313cdc67 yeet improvments from jui 2021-08-18 01:21:17 +04:30
Aria Moradi 5af64892e7 Merge branch 'master' into protobuf 2021-08-18 00:28:42 +04:30
Aria Moradi a5578a7ac7 fix compile warnings 2021-08-17 23:54:02 +04:30
Aria Moradi fcdda6406e update dependencies 2021-08-17 23:53:41 +04:30
Aria Moradi 9bdd9f8aa6 better endpoint urls based on suggestion from @mgn-norm 2021-08-17 21:45:19 +04:30
Aria Moradi f3856f051b protobuf backup endpoints 2021-08-17 20:09:31 +04:30
Aria Moradi d3a6662c60 make it compile 2021-08-15 03:16:13 +04:30
Aria Moradi 5474eddf84 fix some inconsitencies 2021-08-15 02:41:23 +04:30
Aria Moradi b666cd47d4 fix shouldOverwrite 2021-08-15 00:25:08 +04:30
Aria Moradi 8a986383fe fixes #175, better webUI download task 2021-08-14 17:10:41 +04:30
Aria Moradi 9fa17f617e add anime seach functionality 2021-08-11 08:47:07 +04:30
Aria Moradi e46e165704 bump to v0.4.5
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-08-11 00:39:08 +04:30
Aria Moradi a6af423fb4 Merge branch 'master' of github.com:Suwayomi/Tachidesk-Server 2021-08-10 09:42:41 +04:30
Aria Moradi 3397e694c0 sync anime lib implementation with 12 (#133)
* sync anime lib implementation with 11

* fix wrong api

* delete unused classes

* adapt to lib 12

* add LICENSE for eu.kanade.tachiyomi

* changes for lib 12

* update to lib 12

* update webUI
2021-08-10 09:42:14 +04:30
Aria Moradi 77ff82505e fix wrong api 2021-08-10 04:01:08 +04:30
Aria Moradi c3f2838270 fix name tag generator 2021-08-10 03:40:49 +04:30
Aria Moradi aed7f205b6 update webUI 2021-08-10 03:39:36 +04:30
Aria Moradi 95b3587f7a update webUI 2021-08-10 02:04:35 +04:30
Aria Moradi a4baeb995a refactor endpoints to the new styles 2021-08-09 23:18:41 +04:30
Aria Moradi 032ab54206 fix tag generator 2021-08-09 23:18:06 +04:30
Aria Moradi 1f9c1eb1c0 only open browser when appropriate 2021-08-09 07:15:41 +04:30
Aria Moradi a213e568ba update property strings 2021-08-09 06:59:16 +04:30
Aria Moradi 7aeaeb4b86 move copyright notice to it's place 2021-08-09 06:49:12 +04:30
Aria Moradi 81aef4b8fa remove un-used files 2021-08-09 06:48:02 +04:30
Aria Moradi 31f0b6a16c ability to override server.conf with java -D arguments 2021-08-09 06:45:49 +04:30
Aria Moradi 44e3a682fd update Project name 2021-08-09 01:27:09 +04:30
242 changed files with 6998 additions and 10756 deletions
-4
View File
@@ -45,10 +45,6 @@ jobs:
mkdir -p ~/.gradle mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download android.jar
run: |
cd master
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Build Jar - name: Build Jar
uses: eskatos/gradle-command-action@v1 uses: eskatos/gradle-command-action@v1
+6 -23
View File
@@ -47,11 +47,6 @@ jobs:
mkdir -p ~/.gradle mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download android.jar
run: |
cd master
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Build Jar - name: Build Jar
uses: eskatos/gradle-command-action@v1 uses: eskatos/gradle-command-action@v1
env: env:
@@ -64,30 +59,22 @@ jobs:
dependencies-cache-enabled: true dependencies-cache-enabled: true
configuration-cache-enabled: true configuration-cache-enabled: true
# - name: Mock Build and copy webUI, Build Jar
# run: |
# mkdir -p master/server/build
# cd master/server/build
# echo "test" > Tachidesk-v0.3.8-r583.jar
- name: Generate Tag Name - name: Generate Tag Name
id: GenTagName id: GenTagName
run: | run: |
cd master/server/build cd master/server/build
genTag=$(ls *.jar | sed -e's/Tachidesk-\|.jar//g') genTag=$(ls *.jar | sed -e's/Tachidesk-Server-\|.jar//g')
echo "$genTag" echo "$genTag"
echo "::set-output name=value::$genTag" echo "::set-output name=value::$genTag"
- name: make windows packages - name: make bundle packages
run: | run: |
cd master/scripts cd master/scripts
./windows-bundler.sh win32 ./windows-bundler.sh win32
./windows-bundler.sh win64 ./windows-bundler.sh win64
./unix-bundler.sh linux-x64
# - name: Mock make windows packages ./unix-bundler.sh macOS-x64
# run: | ./unix-bundler.sh macOS-arm64
# cd master/server/build
# echo test > Tachidesk-v0.3.8-r580-win32.zip
- name: Checkout preview branch - name: Checkout preview branch
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -116,11 +103,7 @@ jobs:
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }} token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
artifacts: "master/server/build/*.jar,master/server/build/*.zip" artifacts: "master/server/build/*.jar,master/server/build/*.zip,master/server/build/*.tar.gz"
owner: "Suwayomi" owner: "Suwayomi"
repo: "Tachidesk-Server-preview" repo: "Tachidesk-Server-preview"
tag: ${{ steps.GenTagName.outputs.value }} tag: ${{ steps.GenTagName.outputs.value }}
- name: Run Docker build workflow
run: |
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "preview"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
+15
View File
@@ -0,0 +1,15 @@
name: Docker Build Stable
on:
release:
types: [published]
jobs:
build_publish_docker_container:
runs-on: ubuntu-latest
steps:
- name: run docker build and publish script
run: |
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "stable"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
+5 -19
View File
@@ -46,18 +46,6 @@ jobs:
mkdir -p ~/.gradle mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download android.jar
run: |
cd master
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Cache node_modules
uses: actions/cache@v2
with:
path: |
**/webUI/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/webUI/yarn.lock') }}
- name: Build and copy webUI, Build Jar - name: Build and copy webUI, Build Jar
uses: eskatos/gradle-command-action@v1 uses: eskatos/gradle-command-action@v1
env: env:
@@ -70,23 +58,21 @@ jobs:
dependencies-cache-enabled: true dependencies-cache-enabled: true
configuration-cache-enabled: true configuration-cache-enabled: true
- name: make windows packages - name: make bundle packages
run: | run: |
cd master/scripts cd master/scripts
./windows-bundler.sh win32 ./windows-bundler.sh win32
./windows-bundler.sh win64 ./windows-bundler.sh win64
./unix-bundler.sh linux-x64
./unix-bundler.sh macOS-x64
./unix-bundler.sh macOS-arm64
- name: Upload Release - name: Upload Release
uses: xresloader/upload-to-github-release@v1 uses: xresloader/upload-to-github-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
file: "master/server/build/*.jar;master/server/build/*.zip" file: "master/server/build/*.jar;master/server/build/*.zip;master/server/build/*.tar.gz"
tags: true tags: true
draft: true draft: true
verbose: true verbose: true
- name: Run Docker build workflow
run: |
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "stable"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
+12 -7
View File
@@ -1,16 +1,21 @@
# Ignore Gradle project-specific cache directory # Ignore project-specific local files and dirs
.gradle .gradle
.idea .idea
gradle.properties gradle.properties
# But we need these
!.idea/runConfigurations
# Ignore Gradle build output directory # Ignore Gradle build output directory
build build
server/out
AndroidCompat/out
# WebUI is either to be downloaded on-demand or is a dynamic build asset
server/src/main/resources/WebUI.zip server/src/main/resources/WebUI.zip
server/tmp/
server/tachiserver-data/
# bundle asset downlaods # bundling stage downlaoded assets
OpenJDK*.zip scripts/OpenJDK*
electron-*.zip scripts/zulu*
rcedit-* scripts/electron-*
scripts/rcedit-*
-4
View File
@@ -1,4 +0,0 @@
dependencies {
// Config API, moved to the global build.gradle
// implementation("com.typesafe:config:1.4.0")
}
@@ -1,18 +1,20 @@
package xyz.nulldev.ts.config package xyz.nulldev.ts.config
import net.harawata.appdirs.AppDirsFactory
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import net.harawata.appdirs.AppDirsFactory
const val CONFIG_PREFIX = "suwayomi.tachidesk.config"
val ApplicationRootDir: String val ApplicationRootDir: String
get(): String { get(): String {
return System.getProperty( return System.getProperty(
"suwayomi.tachidesk.server.rootDir", "$CONFIG_PREFIX.server.rootDir",
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null) AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
) )
} }
@@ -6,7 +6,7 @@ import org.kodein.di.singleton
class ConfigKodeinModule { class ConfigKodeinModule {
fun create() = DI.Module("ConfigManager") { fun create() = DI.Module("ConfigManager") {
//Config module // Config module
bind<ConfigManager>() with singleton { GlobalConfigManager } bind<ConfigManager>() with singleton { GlobalConfigManager }
} }
} }
@@ -21,7 +21,7 @@ open class ConfigManager {
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>() private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
val config by lazy { loadConfigs() } val config by lazy { loadConfigs() }
//Public read-only view of modules // Public read-only view of modules
val loadedModules: Map<Class<out ConfigModule>, ConfigModule> val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
get() = generatedModules get() = generatedModules
@@ -35,35 +35,35 @@ open class ConfigManager {
/** /**
* Get a config module (Java API) * Get a config module (Java API)
*/ */
@Suppress("UNCHECKED_CAST")
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 * Load configs
*/ */
fun loadConfigs(): Config { fun loadConfigs(): Config {
//Load reference configs // Load reference configs
val compatConfig = ConfigFactory.parseResources("compat-reference.conf") val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
val serverConfig = ConfigFactory.parseResources("server-reference.conf") val serverConfig = ConfigFactory.parseResources("server-reference.conf")
val baseConfig = val baseConfig =
ConfigFactory.parseMap( ConfigFactory.parseMap(
mapOf( mapOf(
"ts.server.rootDir" to ApplicationRootDir "androidcompat.rootDir" to "$ApplicationRootDir/android-compat" // override AndroidCompat's rootDir
)
) )
)
//Load user config // Load user config
val userConfig = val userConfig =
File(ApplicationRootDir, "server.conf").let { File(ApplicationRootDir, "server.conf").let {
ConfigFactory.parseFile(it) ConfigFactory.parseFile(it)
} }
val config = ConfigFactory.empty() val config = ConfigFactory.empty()
.withFallback(baseConfig) .withFallback(baseConfig)
.withFallback(userConfig) .withFallback(userConfig)
.withFallback(compatConfig) .withFallback(compatConfig)
.withFallback(serverConfig) .withFallback(serverConfig)
.resolve() .resolve()
// set log level early // set log level early
if (debugLogsEnabled(config)) { if (debugLogsEnabled(config)) {
@@ -1,8 +1,44 @@
package xyz.nulldev.ts.config 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.Config
import io.github.config4k.getValue
import kotlin.reflect.KProperty
/** /**
* Abstract config module. * Abstract config module.
*/ */
@Suppress("UNUSED_PARAMETER")
abstract class ConfigModule(config: Config) abstract class ConfigModule(config: Config)
/**
* Abstract jvm-commandline-argument-overridable config module.
*/
abstract class SystemPropertyOverridableConfigModule(config: Config, moduleName: String) : ConfigModule(config) {
val overridableConfig = SystemPropertyOverrideDelegate(config, moduleName)
}
/** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */
class SystemPropertyOverrideDelegate(val config: Config, val moduleName: String) {
inline operator fun <R, reified T> getValue(thisRef: R, property: KProperty<*>): T {
val configValue: T = config.getValue(thisRef, property)
val combined = System.getProperty(
"$CONFIG_PREFIX.$moduleName.${property.name}",
configValue.toString()
)
return when (T::class.simpleName) {
"Int" -> combined.toInt()
"Boolean" -> combined.toBoolean()
// add more types as needed
else -> combined // covers String
} as T
}
}
@@ -16,5 +16,5 @@ fun setLogLevel(level: Level) {
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = level (KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = level
} }
fun debugLogsEnabled(config: Config) fun debugLogsEnabled(config: Config) =
= System.getProperty("suwayomi.tachidesk.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean() System.getProperty("suwayomi.tachidesk.config.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
@@ -3,4 +3,4 @@ package xyz.nulldev.ts.config.util
import com.typesafe.config.Config import com.typesafe.config.Config
operator fun Config.get(key: String) = getString(key) operator fun Config.get(key: String) = getString(key)
?: throw IllegalStateException("Could not find value for config entry: $key!") ?: throw IllegalStateException("Could not find value for config entry: $key!")
@@ -1 +0,0 @@
xyz.nulldev.ts.api.v2.java.impl.ServerAPIImpl
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long
@@ -1,22 +0,0 @@
[
{
"label": "Sync",
"icon": "import_export",
"type": "nested",
"prefs": []
},
{
"label": "Server",
"icon": "dns",
"type": "nested",
"prefs": [
{
"label": "Password authentication",
"type": "text-password",
"default": "",
"key": "pref_ts_server_password",
"hint": "Enter a password"
}
]
}
]
+7 -56
View File
@@ -1,77 +1,28 @@
plugins {
application
kotlin("plugin.serialization")
}
repositories {
mavenCentral()
maven {
url = uri("https://jitpack.io")
}
maven {
url = uri("https://maven.google.com")
}
}
dependencies { dependencies {
// Android stub library // Android stub library
implementation(fileTree("lib/")) implementation("com.github.Suwayomi:android-jar:1.0.0")
// Android JAR libs
// compileOnly( fileTree(dir: new File(rootProject.rootDir, "libs/other"), include: "*.jar")
// JSON
compileOnly("com.google.code.gson:gson:2.8.6")
// Javassist
compileOnly("org.javassist:javassist:3.27.0-GA")
// XML // XML
compileOnly(group= "xmlpull", name= "xmlpull", version= "1.1.3.1") compileOnly("xmlpull:xmlpull:1.1.3.4a")
// Config API // Config API
implementation(project(":AndroidCompat:Config")) implementation(project(":AndroidCompat:Config"))
// APK sig verifier // APK sig verifier
compileOnly("com.android.tools.build:apksig:4.2.0-alpha13") compileOnly("com.android.tools.build:apksig:7.1.0-alpha12")
// AndroidX annotations // AndroidX annotations
compileOnly("androidx.annotation:annotation:1.2.0-alpha01") compileOnly("androidx.annotation:annotation:1.2.0")
// substitute for duktape-android // 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") // slimmer version of 'org.mozilla:rhino'
implementation("org.mozilla:rhino-runtime:1.7.13") implementation("org.mozilla:rhino-engine:1.7.13") // provides the same interface as 'javax.script' a.k.a Nashorn
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
implementation("org.mozilla:rhino-engine:1.7.13")
// Kotlin wrapper around Java Preferences, makes certain things easier // Kotlin wrapper around Java Preferences, makes certain things easier
val multiplatformSettingsVersion = "0.7.7" val multiplatformSettingsVersion = "0.8"
implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion") implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion")
implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion") implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion")
// Android version of SimpleDateFormat // Android version of SimpleDateFormat
implementation("com.ibm.icu:icu4j:69.1") implementation("com.ibm.icu:icu4j:69.1")
} }
tasks {
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
}
}
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
//
//// Copy JVM core patches
//task copyJVMPatches(type: Copy) {
// from fatJarTask.outputs.files
// into 'src/main/resources/patches'
//}
//
//compileOnly(Java.dependsOn gradle.includedBuild('dex2jar').task(':dex-translator:assemble')
//compileOnly(Java.dependsOn copyJVMPatches
//copyJVMPatches.dependsOn fatJarTask
//
-1
View File
@@ -1 +0,0 @@
android.jar
@@ -9,8 +9,10 @@ import android.content.Context
class PreferenceManager { class PreferenceManager {
companion object { companion object {
@JvmStatic @JvmStatic
fun getDefaultSharedPreferences(context: Context) fun getDefaultSharedPreferences(context: Context) =
= context.getSharedPreferences(context.applicationInfo.packageName, context.getSharedPreferences(
Context.MODE_PRIVATE)!! context.applicationInfo.packageName,
Context.MODE_PRIVATE
)!!
} }
} }
@@ -13,33 +13,49 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.Set; import java.util.Set;
public class MultiSelectListPreference extends DialogPreference { public class MultiSelectListPreference extends DialogPreference {
// reference: https://android.googlesource.com/platform/frameworks/support/+/996971f962fcd554339a7cb2859cef9ca89dbcb7/preference/preference/src/main/java/androidx/preference/MultiSelectListPreference.java
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them // Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them
public MultiSelectListPreference(Context context) { super(context); } private CharSequence[] entries;
private CharSequence[] entryValues;
public MultiSelectListPreference(Context context) {
super(context);
}
public void setEntries(CharSequence[] entries) {
this.entries = entries;
}
public CharSequence[] getEntries() {
return entries;
}
public void setEntryValues(CharSequence[] entryValues) {
this.entryValues = entryValues;
}
public CharSequence[] getEntryValues() {
return entryValues;
}
@JsonIgnore @JsonIgnore
public void setEntries(CharSequence[] entries) { throw new RuntimeException("Stub!"); } public void setValues(Set<String> values) {
throw new RuntimeException("Stub!");
}
@JsonIgnore @JsonIgnore
public CharSequence[] getEntries() { throw new RuntimeException("Stub!"); } public Set<String> getValues() {
throw new RuntimeException("Stub!");
}
@JsonIgnore public int findIndexOfValue(String value) {
public void setEntryValues(CharSequence[] entryValues) { throw new RuntimeException("Stub!"); } throw new RuntimeException("Stub!");
}
@JsonIgnore
public CharSequence[] getEntryValues() { throw new RuntimeException("Stub!"); }
@JsonIgnore
public void setValues(Set<String> values) { throw new RuntimeException("Stub!"); }
@JsonIgnore
public Set<String> getValues() { throw new RuntimeException("Stub!"); }
public int findIndexOfValue(String value) { throw new RuntimeException("Stub!"); }
/** Tachidesk specific API */ /** Tachidesk specific API */
@Override @Override
public String getDefaultValueType() { public String getDefaultValueType() {
return "Set"; return "Set<String>";
} }
} }
@@ -10,6 +10,7 @@ package androidx.preference;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.Set;
/** /**
* A minimal implementation of androidx.preference.Preference * A minimal implementation of androidx.preference.Preference
@@ -113,18 +114,22 @@ public class Preference {
} }
/** Tachidesk specific API */ /** Tachidesk specific API */
@SuppressWarnings("unchecked")
public Object getCurrentValue() { public Object getCurrentValue() {
switch (getDefaultValueType()) { switch (getDefaultValueType()) {
case "String": case "String":
return sharedPreferences.getString(key, (String)defaultValue); return sharedPreferences.getString(key, (String)defaultValue);
case "Boolean": case "Boolean":
return sharedPreferences.getBoolean(key, (Boolean)defaultValue); return sharedPreferences.getBoolean(key, (Boolean)defaultValue);
case "Set<String>":
return sharedPreferences.getStringSet(key, (Set<String>)defaultValue);
default: default:
throw new RuntimeException("Unsupported type"); throw new RuntimeException("Unsupported type");
} }
} }
/** Tachidesk specific API */ /** Tachidesk specific API */
@SuppressWarnings("unchecked")
public void saveNewValue(Object value) { public void saveNewValue(Object value) {
switch (getDefaultValueType()) { switch (getDefaultValueType()) {
case "String": case "String":
@@ -133,6 +138,9 @@ public class Preference {
case "Boolean": case "Boolean":
sharedPreferences.edit().putBoolean(key, (Boolean)value).apply(); sharedPreferences.edit().putBoolean(key, (Boolean)value).apply();
break; break;
case "Set<String>":
sharedPreferences.edit().putStringSet(key, (Set<String>)value).apply();
break;
default: default:
throw new RuntimeException("Unsupported type"); throw new RuntimeException("Unsupported type");
} }
@@ -13,7 +13,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
public class TwoStatePreference extends Preference { public class TwoStatePreference extends Preference {
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them // Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them
public TwoStatePreference(Context context) { super(context); } public TwoStatePreference(Context context) {
super(context);
setDefaultValue(false);
}
@JsonIgnore @JsonIgnore
public boolean isChecked() { throw new RuntimeException("Stub!"); } public boolean isChecked() { throw new RuntimeException("Stub!"); }
@@ -1,2 +0,0 @@
package com.f2prateek;
//TODO Consider if we can change this package into an Android dependency
@@ -1,34 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This file may have been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
final class BooleanAdapter implements Preference.Adapter<Boolean> {
static final BooleanAdapter INSTANCE = new BooleanAdapter();
@Override public Boolean get(@NonNull String key, @NonNull SharedPreferences preferences) {
return preferences.getBoolean(key, false);
}
@Override public void set(@NonNull String key, @NonNull Boolean value,
@NonNull SharedPreferences.Editor editor) {
editor.putBoolean(key, value);
}
}
@@ -1,40 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This file may have been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
final class EnumAdapter<T extends Enum<T>> implements Preference.Adapter<T> {
private final Class<T> enumClass;
EnumAdapter(Class<T> enumClass) {
this.enumClass = enumClass;
}
@Override public T get(@NonNull String key, @NonNull SharedPreferences preferences) {
String value = preferences.getString(key, null);
assert value != null; // Not called unless key is present.
return Enum.valueOf(enumClass, value);
}
@Override
public void set(@NonNull String key, @NonNull T value, @NonNull SharedPreferences.Editor editor) {
editor.putString(key, value.name());
}
}
@@ -1,34 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This file may have been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
final class FloatAdapter implements Preference.Adapter<Float> {
static final FloatAdapter INSTANCE = new FloatAdapter();
@Override public Float get(@NonNull String key, @NonNull SharedPreferences preferences) {
return preferences.getFloat(key, 0f);
}
@Override public void set(@NonNull String key, @NonNull Float value,
@NonNull SharedPreferences.Editor editor) {
editor.putFloat(key, value);
}
}
@@ -1,34 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This file may have been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
final class IntegerAdapter implements Preference.Adapter<Integer> {
static final IntegerAdapter INSTANCE = new IntegerAdapter();
@Override public Integer get(@NonNull String key, @NonNull SharedPreferences preferences) {
return preferences.getInt(key, 0);
}
@Override public void set(@NonNull String key, @NonNull Integer value,
@NonNull SharedPreferences.Editor editor) {
editor.putInt(key, value);
}
}
@@ -1,34 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This file may have been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
final class LongAdapter implements Preference.Adapter<Long> {
static final LongAdapter INSTANCE = new LongAdapter();
@Override public Long get(@NonNull String key, @NonNull SharedPreferences preferences) {
return preferences.getLong(key, 0L);
}
@Override public void set(@NonNull String key, @NonNull Long value,
@NonNull SharedPreferences.Editor editor) {
editor.putLong(key, value);
}
}
@@ -1,127 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This file has been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import rx.Observable;
import rx.functions.Action1;
/** A preference of type {@link T}. Instances can be created from {@link RxSharedPreferences}. */
public final class Preference<T> {
/** Stores and retrieves instances of {@code T} in {@link SharedPreferences}. */
public interface Adapter<T> {
/** Retrieve the value for {@code key} from {@code preferences}. */
T get(@NonNull String key, @NonNull SharedPreferences preferences);
/**
* Store non-null {@code value} for {@code key} in {@code editor}.
* <p>
* Note: Implementations <b>must not</b> call {@code commit()} or {@code apply()} on
* {@code editor}.
*/
void set(@NonNull String key, @NonNull T value, @NonNull SharedPreferences.Editor editor);
}
private final SharedPreferences preferences;
private final String key;
private final T defaultValue;
private final Adapter<T> adapter;
private final Observable<T> values;
Preference(SharedPreferences preferences, final String key, T defaultValue, Adapter<T> adapter,
Observable<String> keyChanges) {
this.preferences = preferences;
this.key = key;
this.defaultValue = defaultValue;
this.adapter = adapter;
this.values = keyChanges
.filter(key::equals)
.startWith("<init>") // Dummy value to trigger initial load.
.onBackpressureLatest()
.map(ignored -> get());
}
/** The key for which this preference will store and retrieve values. */
@NonNull
public String key() {
return key;
}
/** The value used if none is stored. May be {@code null}. */
@Nullable
public T defaultValue() {
return defaultValue;
}
/**
* Retrieve the current value for this preference. Returns {@link #defaultValue()} if no value is
* set.
*/
@Nullable
public T get() {
if (!preferences.contains(key)) {
return defaultValue;
}
return adapter.get(key, preferences);
}
/**
* Change this preference's stored value to {@code value}. A value of {@code null} will delete the
* preference.
*/
public void set(@Nullable T value) {
SharedPreferences.Editor editor = preferences.edit();
if (value == null) {
editor.remove(key);
} else {
adapter.set(key, value, editor);
}
editor.apply();
}
/** Returns true if this preference has a stored value. */
public boolean isSet() {
return preferences.contains(key);
}
/** Delete the stored value for this preference, if any. */
public void delete() {
set(null);
}
/**
* Observe changes to this preference. The current value or {@link #defaultValue()} will be
* emitted on first subscribe.
*/
@CheckResult @NonNull
public Observable<T> asObservable() {
return values;
}
/**
* An action which stores a new value for this preference. Passing {@code null} will delete the
* preference.
*/
@CheckResult @NonNull
public Action1<? super T> asAction() {
return (Action1<T>) this::set;
}
}
@@ -1,178 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This file has been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.annotation.TargetApi;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Collections;
import java.util.Set;
import rx.Observable;
import rx.subscriptions.Subscriptions;
import static android.os.Build.VERSION_CODES.HONEYCOMB;
import static com.f2prateek.rx.preferences.Preconditions.checkNotNull;
/** A factory for reactive {@link Preference} objects. */
public final class RxSharedPreferences {
private static final Float DEFAULT_FLOAT = 0f;
private static final Integer DEFAULT_INTEGER = 0;
private static final Boolean DEFAULT_BOOLEAN = Boolean.FALSE;
private static final Long DEFAULT_LONG = 0L;
/** Create an instance of {@link RxSharedPreferences} for {@code preferences}. */
@CheckResult @NonNull
public static RxSharedPreferences create(@NonNull SharedPreferences preferences) {
checkNotNull(preferences, "preferences == null");
return new RxSharedPreferences(preferences);
}
private final SharedPreferences preferences;
private final Observable<String> keyChanges;
private RxSharedPreferences(final SharedPreferences preferences) {
this.preferences = preferences;
this.keyChanges = Observable.create((Observable.OnSubscribe<String>) subscriber -> {
final OnSharedPreferenceChangeListener listener = (preferences1, key) -> subscriber.onNext(key);
preferences.registerOnSharedPreferenceChangeListener(listener);
subscriber.add(Subscriptions.create(() -> preferences.unregisterOnSharedPreferenceChangeListener(listener)));
}).share();
}
/** Create a boolean preference for {@code key}. Default is {@code false}. */
@CheckResult @NonNull
public Preference<Boolean> getBoolean(@NonNull String key) {
return getBoolean(key, DEFAULT_BOOLEAN);
}
/** Create a boolean preference for {@code key} with a default of {@code defaultValue}. */
@CheckResult @NonNull
public Preference<Boolean> getBoolean(@NonNull String key, @Nullable Boolean defaultValue) {
checkNotNull(key, "key == null");
return new Preference<>(preferences, key, defaultValue, BooleanAdapter.INSTANCE, keyChanges);
}
/** Create an enum preference for {@code key}. Default is {@code null}. */
@CheckResult @NonNull
public <T extends Enum<T>> Preference<T> getEnum(@NonNull String key,
@NonNull Class<T> enumClass) {
return getEnum(key, null, enumClass);
}
/** Create an enum preference for {@code key} with a default of {@code defaultValue}. */
@CheckResult @NonNull
public <T extends Enum<T>> Preference<T> getEnum(@NonNull String key, @Nullable T defaultValue,
@NonNull Class<T> enumClass) {
checkNotNull(key, "key == null");
checkNotNull(enumClass, "enumClass == null");
Preference.Adapter<T> adapter = new EnumAdapter<>(enumClass);
return new Preference<>(preferences, key, defaultValue, adapter, keyChanges);
}
/** Create a float preference for {@code key}. Default is {@code 0}. */
@CheckResult @NonNull
public Preference<Float> getFloat(@NonNull String key) {
return getFloat(key, DEFAULT_FLOAT);
}
/** Create a float preference for {@code key} with a default of {@code defaultValue}. */
@CheckResult @NonNull
public Preference<Float> getFloat(@NonNull String key, @Nullable Float defaultValue) {
checkNotNull(key, "key == null");
return new Preference<>(preferences, key, defaultValue, FloatAdapter.INSTANCE, keyChanges);
}
/** Create an integer preference for {@code key}. Default is {@code 0}. */
@CheckResult @NonNull
public Preference<Integer> getInteger(@NonNull String key) {
//noinspection UnnecessaryBoxing
return getInteger(key, DEFAULT_INTEGER);
}
/** Create an integer preference for {@code key} with a default of {@code defaultValue}. */
@CheckResult @NonNull
public Preference<Integer> getInteger(@NonNull String key, @Nullable Integer defaultValue) {
checkNotNull(key, "key == null");
return new Preference<>(preferences, key, defaultValue, IntegerAdapter.INSTANCE, keyChanges);
}
/** Create a long preference for {@code key}. Default is {@code 0}. */
@CheckResult @NonNull
public Preference<Long> getLong(@NonNull String key) {
//noinspection UnnecessaryBoxing
return getLong(key, DEFAULT_LONG);
}
/** Create a long preference for {@code key} with a default of {@code defaultValue}. */
@CheckResult @NonNull
public Preference<Long> getLong(@NonNull String key, @Nullable Long defaultValue) {
checkNotNull(key, "key == null");
return new Preference<>(preferences, key, defaultValue, LongAdapter.INSTANCE, keyChanges);
}
/** Create a preference of type {@code T} for {@code key}. Default is {@code null}. */
@CheckResult @NonNull
public <T> Preference<T> getObject(@NonNull String key, @NonNull Preference.Adapter<T> adapter) {
return getObject(key, null, adapter);
}
/**
* Create a preference for type {@code T} for {@code key} with a default of {@code defaultValue}.
*/
@CheckResult @NonNull
public <T> Preference<T> getObject(@NonNull String key, @Nullable T defaultValue,
@NonNull Preference.Adapter<T> adapter) {
checkNotNull(key, "key == null");
checkNotNull(adapter, "adapter == null");
return new Preference<>(preferences, key, defaultValue, adapter, keyChanges);
}
/** Create a string preference for {@code key}. Default is {@code null}. */
@CheckResult @NonNull
public Preference<String> getString(@NonNull String key) {
return getString(key, null);
}
/** Create a string preference for {@code key} with a default of {@code defaultValue}. */
@CheckResult @NonNull
public Preference<String> getString(@NonNull String key, @Nullable String defaultValue) {
checkNotNull(key, "key == null");
return new Preference<>(preferences, key, defaultValue, StringAdapter.INSTANCE, keyChanges);
}
/** Create a string set preference for {@code key}. Default is an empty set. */
@TargetApi(HONEYCOMB)
@CheckResult @NonNull
public Preference<Set<String>> getStringSet(@NonNull String key) {
return getStringSet(key, Collections.emptySet());
}
/** Create a string set preference for {@code key} with a default of {@code defaultValue}. */
@TargetApi(HONEYCOMB)
@CheckResult @NonNull
public Preference<Set<String>> getStringSet(@NonNull String key,
@NonNull Set<String> defaultValue) {
checkNotNull(key, "key == null");
return new Preference<>(preferences, key, defaultValue, StringSetAdapter.INSTANCE, keyChanges);
}
}
@@ -1,17 +0,0 @@
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
final class StringAdapter implements Preference.Adapter<String> {
static final StringAdapter INSTANCE = new StringAdapter();
@Override public String get(@NonNull String key, @NonNull SharedPreferences preferences) {
return preferences.getString(key, null);
}
@Override public void set(@NonNull String key, @NonNull String value,
@NonNull SharedPreferences.Editor editor) {
editor.putString(key, value);
}
}
@@ -1,22 +0,0 @@
package com.f2prateek.rx.preferences;
import android.annotation.TargetApi;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import java.util.Set;
import static android.os.Build.VERSION_CODES.HONEYCOMB;
@TargetApi(HONEYCOMB)
final class StringSetAdapter implements Preference.Adapter<Set<String>> {
static final StringSetAdapter INSTANCE = new StringSetAdapter();
@Override public Set<String> get(@NonNull String key, @NonNull SharedPreferences preferences) {
return preferences.getStringSet(key, null);
}
@Override public void set(@NonNull String key, @NonNull Set<String> value,
@NonNull SharedPreferences.Editor editor) {
editor.putStringSet(key, value);
}
}
@@ -1,7 +0,0 @@
package com.github.pwittchen.reactivenetwork.library
import android.net.NetworkInfo
class Connectivity {
val state = NetworkInfo.State.CONNECTED
}
@@ -1,14 +0,0 @@
package com.github.pwittchen.reactivenetwork.library
import android.content.Context
import rx.Observable
/**
* Created by nulldev on 12/29/16.
*/
class ReactiveNetwork {
companion object {
fun observeNetworkConnectivity(context: Context) = Observable.just(Connectivity())!!
}
}
@@ -5,4 +5,4 @@ import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory
class RequerySQLiteOpenHelperFactory { class RequerySQLiteOpenHelperFactory {
fun create(configuration: SupportSQLiteOpenHelper.Configuration) = FrameworkSQLiteOpenHelperFactory().create(configuration) fun create(configuration: SupportSQLiteOpenHelper.Configuration) = FrameworkSQLiteOpenHelperFactory().create(configuration)
} }
@@ -1,5 +0,0 @@
package kotlinx.coroutines.experimental.android
import kotlinx.coroutines.GlobalScope
val UI = GlobalScope.coroutineContext
@@ -14,4 +14,4 @@ class AndroidCompat {
application.attach(context) application.attach(context)
application.onCreate() application.onCreate()
} }
} }
@@ -2,7 +2,6 @@ package xyz.nulldev.androidcompat
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.bytecode.ModApplier
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
import xyz.nulldev.androidcompat.config.FilesConfigModule import xyz.nulldev.androidcompat.config.FilesConfigModule
import xyz.nulldev.androidcompat.config.SystemConfigModule import xyz.nulldev.androidcompat.config.SystemConfigModule
@@ -12,15 +11,10 @@ import xyz.nulldev.ts.config.GlobalConfigManager
* Initializes the Android compatibility module * Initializes the Android compatibility module
*/ */
class AndroidCompatInitializer { class AndroidCompatInitializer {
val modApplier by lazy { ModApplier() }
fun init() { fun init() {
modApplier.apply()
DI.global.addImport(AndroidCompatModule().create()) DI.global.addImport(AndroidCompatModule().create())
//Register config modules // Register config modules
GlobalConfigManager.registerModules( GlobalConfigManager.registerModules(
FilesConfigModule.register(GlobalConfigManager.config), FilesConfigModule.register(GlobalConfigManager.config),
ApplicationInfoConfigModule.register(GlobalConfigManager.config), ApplicationInfoConfigModule.register(GlobalConfigManager.config),
@@ -29,7 +29,7 @@ class AndroidCompatModule {
bind<PackageController>() with singleton { PackageController() } bind<PackageController>() with singleton { PackageController() }
//Context // Context
bind<CustomContext>() with singleton { CustomContext() } bind<CustomContext>() with singleton { CustomContext() }
bind<Context>() with singleton { bind<Context>() with singleton {
val context: Context by DI.global.instance<CustomContext>() val context: Context by DI.global.instance<CustomContext>()
@@ -1,22 +0,0 @@
package xyz.nulldev.androidcompat.bytecode
import javassist.CtClass
import mu.KotlinLogging
/**
* Applies Javassist modifications
*/
class ModApplier {
val logger = KotlinLogging.logger {}
fun apply() {
logger.info { "Applying Javassist mods..." }
val modifiedClasses = mutableListOf<CtClass>()
modifiedClasses.forEach {
it.toClass()
}
}
}
@@ -1,6 +1,7 @@
package xyz.nulldev.androidcompat.config package xyz.nulldev.androidcompat.config
import com.typesafe.config.Config import com.typesafe.config.Config
import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule import xyz.nulldev.ts.config.ConfigModule
/** /**
@@ -8,11 +9,11 @@ import xyz.nulldev.ts.config.ConfigModule
*/ */
class ApplicationInfoConfigModule(config: Config) : ConfigModule(config) { class ApplicationInfoConfigModule(config: Config) : ConfigModule(config) {
val packageName = config.getString("packageName")!! val packageName: String by config
val debug = config.getBoolean("debug") val debug: Boolean by config
companion object { companion object {
fun register(config: Config) fun register(config: Config) =
= ApplicationInfoConfigModule(config.getConfig("android.app")) ApplicationInfoConfigModule(config.getConfig("android.app"))
} }
} }
@@ -1,6 +1,7 @@
package xyz.nulldev.androidcompat.config package xyz.nulldev.androidcompat.config
import com.typesafe.config.Config import com.typesafe.config.Config
import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule import xyz.nulldev.ts.config.ConfigModule
/** /**
@@ -8,26 +9,26 @@ import xyz.nulldev.ts.config.ConfigModule
*/ */
class FilesConfigModule(config: Config) : ConfigModule(config) { class FilesConfigModule(config: Config) : ConfigModule(config) {
val dataDir = config.getString("dataDir")!! val dataDir: String by config
val filesDir = config.getString("filesDir")!! val filesDir: String by config
val noBackupFilesDir = config.getString("noBackupFilesDir")!! val noBackupFilesDir: String by config
val externalFilesDirs: MutableList<String> = config.getStringList("externalFilesDirs")!! val externalFilesDirs: MutableList<String> by config
val obbDirs: MutableList<String> = config.getStringList("obbDirs")!! val obbDirs: MutableList<String> by config
val cacheDir = config.getString("cacheDir")!! val cacheDir: String by config
val codeCacheDir = config.getString("codeCacheDir")!! val codeCacheDir: String by config
val externalCacheDirs: MutableList<String> = config.getStringList("externalCacheDirs")!! val externalCacheDirs: MutableList<String> by config
val externalMediaDirs: MutableList<String> = config.getStringList("externalMediaDirs")!! val externalMediaDirs: MutableList<String> by config
val rootDir = config.getString("rootDir")!! val rootDir: String by config
val externalStorageDir = config.getString("externalStorageDir")!! val externalStorageDir: String by config
val downloadCacheDir = config.getString("downloadCacheDir")!! val downloadCacheDir: String by config
val databasesDir = config.getString("databasesDir")!! val databasesDir: String by config
val prefsDir = config.getString("prefsDir")!! val prefsDir: String by config
val packageDir = config.getString("packageDir")!! val packageDir: String by config
companion object { companion object {
fun register(config: Config) fun register(config: Config) =
= FilesConfigModule(config.getConfig("android.files")) FilesConfigModule(config.getConfig("android.files"))
} }
} }
@@ -1,10 +1,11 @@
package xyz.nulldev.androidcompat.config package xyz.nulldev.androidcompat.config
import com.typesafe.config.Config import com.typesafe.config.Config
import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule import xyz.nulldev.ts.config.ConfigModule
class SystemConfigModule(val config: Config) : ConfigModule(config) { class SystemConfigModule(val config: Config) : ConfigModule(config) {
val isDebuggable = config.getBoolean("isDebuggable") val isDebuggable: Boolean by config
val propertyPrefix = "properties." val propertyPrefix = "properties."
@@ -15,7 +16,7 @@ class SystemConfigModule(val config: Config) : ConfigModule(config) {
fun hasProperty(property: String) = config.hasPath("$propertyPrefix$property") fun hasProperty(property: String) = config.hasPath("$propertyPrefix$property")
companion object { companion object {
fun register(config: Config) fun register(config: Config) =
= SystemConfigModule(config.getConfig("android.system")) SystemConfigModule(config.getConfig("android.system"))
} }
} }
@@ -4,11 +4,21 @@ import java.io.InputStream
import java.io.Reader import java.io.Reader
import java.math.BigDecimal import java.math.BigDecimal
import java.net.URL import java.net.URL
import java.sql.*
import java.sql.Array import java.sql.Array
import java.sql.Blob
import java.sql.Clob
import java.sql.Date import java.sql.Date
import java.util.* import java.sql.NClob
import java.sql.Ref
import java.sql.ResultSet
import java.sql.ResultSetMetaData
import java.sql.RowId
import java.sql.SQLXML
import java.sql.Time
import java.sql.Timestamp
import java.util.Calendar
@Suppress("UNCHECKED_CAST")
class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent { class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
private val cachedContent = mutableListOf<ResultSetEntry>() private val cachedContent = mutableListOf<ResultSetEntry>()
@@ -19,7 +29,7 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
val parentMetadata = parent.metaData val parentMetadata = parent.metaData
val columnCount = parentMetadata.columnCount val columnCount = parentMetadata.columnCount
val columnLabels = (1 .. columnCount).map { val columnLabels = (1..columnCount).map {
parentMetadata.getColumnLabel(it) parentMetadata.getColumnLabel(it)
}.toTypedArray() }.toTypedArray()
@@ -31,10 +41,10 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
// How can we optimize this? // How can we optimize this?
// We need to fill the cache as the set is loaded // We need to fill the cache as the set is loaded
//Fill cache // Fill cache
while(parent.next()) { while (parent.next()) {
cachedContent += ResultSetEntry().apply { cachedContent += ResultSetEntry().apply {
for(i in 1 .. columnCount) for (i in 1..columnCount)
data += parent.getObject(i) data += parent.getObject(i)
} }
resultSetLength++ resultSetLength++
@@ -50,8 +60,8 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
private fun internalMove(row: Int) { private fun internalMove(row: Int) {
if(cursor < 0) cursor = 0 if (cursor < 0) cursor = 0
else if(cursor > resultSetLength + 1) cursor = resultSetLength + 1 else if (cursor > resultSetLength + 1) cursor = resultSetLength + 1
else cursor = row else cursor = row
} }
@@ -65,10 +75,10 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return obj(cachedFindColumn(column)) return obj(cachedFindColumn(column))
} }
private fun cachedFindColumn(column: String?) private fun cachedFindColumn(column: String?) =
= columnCache.getOrPut(column!!, { columnCache.getOrPut(column!!, {
findColumn(column) findColumn(column)
}) })
override fun getNClob(columnIndex: Int): NClob { override fun getNClob(columnIndex: Int): NClob {
return obj(columnIndex) as NClob return obj(columnIndex) as NClob
@@ -147,27 +157,27 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getDate(columnIndex: Int): Date { override fun getDate(columnIndex: Int): Date {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getDate(columnLabel: String?): Date { override fun getDate(columnLabel: String?): Date {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getDate(columnIndex: Int, cal: Calendar?): Date { override fun getDate(columnIndex: Int, cal: Calendar?): Date {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getDate(columnLabel: String?, cal: Calendar?): Date { override fun getDate(columnLabel: String?, cal: Calendar?): Date {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun beforeFirst() { override fun beforeFirst() {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -192,12 +202,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getBigDecimal(columnIndex: Int, scale: Int): BigDecimal { override fun getBigDecimal(columnIndex: Int, scale: Int): BigDecimal {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getBigDecimal(columnLabel: String?, scale: Int): BigDecimal { override fun getBigDecimal(columnLabel: String?, scale: Int): BigDecimal {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -226,22 +236,22 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getTime(columnIndex: Int): Time { override fun getTime(columnIndex: Int): Time {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTime(columnLabel: String?): Time { override fun getTime(columnLabel: String?): Time {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTime(columnIndex: Int, cal: Calendar?): Time { override fun getTime(columnIndex: Int, cal: Calendar?): Time {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTime(columnLabel: String?, cal: Calendar?): Time { override fun getTime(columnLabel: String?, cal: Calendar?): Time {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -262,28 +272,28 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun absolute(row: Int): Boolean { override fun absolute(row: Int): Boolean {
if(row > 0) { if (row > 0) {
internalMove(row) internalMove(row)
} else { } else {
last() last()
for(i in 1 .. row) for (i in 1..row)
previous() previous()
} }
return cursorValid() return cursorValid()
} }
override fun getSQLXML(columnIndex: Int): SQLXML? { override fun getSQLXML(columnIndex: Int): SQLXML? {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getSQLXML(columnLabel: String?): SQLXML? { override fun getSQLXML(columnLabel: String?): SQLXML? {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun <T : Any?> unwrap(iface: Class<T>?): T { override fun <T : Any?> unwrap(iface: Class<T>?): T {
if(thisIsWrapperFor(iface)) if (thisIsWrapperFor(iface))
return this as T return this as T
else else
return parent.unwrap(iface) return parent.unwrap(iface)
@@ -416,12 +426,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getBlob(columnIndex: Int): Blob { override fun getBlob(columnIndex: Int): Blob {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getBlob(columnLabel: String?): Blob { override fun getBlob(columnLabel: String?): Blob {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -490,12 +500,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getObject(columnIndex: Int, map: MutableMap<String, Class<*>>?): Any { override fun getObject(columnIndex: Int, map: MutableMap<String, Class<*>>?): Any {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getObject(columnLabel: String?, map: MutableMap<String, Class<*>>?): Any { override fun getObject(columnLabel: String?, map: MutableMap<String, Class<*>>?): Any {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -521,9 +531,9 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
private fun castToLong(obj: Any?): Long { private fun castToLong(obj: Any?): Long {
if(obj == null) return 0 if (obj == null) return 0
else if(obj is Long) return obj else if (obj is Long) return obj
else if(obj is Number) return obj.toLong() else if (obj is Number) return obj.toLong()
else throw IllegalStateException("Object is not a long!") else throw IllegalStateException("Object is not a long!")
} }
@@ -536,12 +546,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getClob(columnIndex: Int): Clob { override fun getClob(columnIndex: Int): Clob {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getClob(columnLabel: String?): Clob { override fun getClob(columnLabel: String?): Clob {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -594,12 +604,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getArray(columnIndex: Int): Array { override fun getArray(columnIndex: Int): Array {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getArray(columnLabel: String?): Array { override fun getArray(columnLabel: String?): Array {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -678,32 +688,32 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getTimestamp(columnIndex: Int): Timestamp { override fun getTimestamp(columnIndex: Int): Timestamp {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTimestamp(columnLabel: String?): Timestamp { override fun getTimestamp(columnLabel: String?): Timestamp {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTimestamp(columnIndex: Int, cal: Calendar?): Timestamp { override fun getTimestamp(columnIndex: Int, cal: Calendar?): Timestamp {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTimestamp(columnLabel: String?, cal: Calendar?): Timestamp { override fun getTimestamp(columnLabel: String?, cal: Calendar?): Timestamp {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getRef(columnIndex: Int): Ref { override fun getRef(columnIndex: Int): Ref {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getRef(columnLabel: String?): Ref { override fun getRef(columnLabel: String?): Ref {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -782,12 +792,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getRowId(columnIndex: Int): RowId { override fun getRowId(columnIndex: Int): RowId {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getRowId(columnLabel: String?): RowId { override fun getRowId(columnLabel: String?): RowId {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -838,4 +848,4 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
class ResultSetEntry { class ResultSetEntry {
val data = mutableListOf<Any?>() val data = mutableListOf<Any?>()
} }
} }
@@ -12,11 +12,11 @@ import com.russhwolf.settings.ExperimentalSettingsApi
import com.russhwolf.settings.ExperimentalSettingsImplementation import com.russhwolf.settings.ExperimentalSettingsImplementation
import com.russhwolf.settings.JvmPreferencesSettings import com.russhwolf.settings.JvmPreferencesSettings
import com.russhwolf.settings.serialization.decodeValue import com.russhwolf.settings.serialization.decodeValue
import com.russhwolf.settings.serialization.decodeValueOrNull
import com.russhwolf.settings.serialization.encodeValue import com.russhwolf.settings.serialization.encodeValue
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.SetSerializer import kotlinx.serialization.builtins.SetSerializer
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import java.util.prefs.PreferenceChangeListener import java.util.prefs.PreferenceChangeListener
import java.util.prefs.Preferences import java.util.prefs.Preferences
@@ -40,13 +40,13 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
} }
} }
override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? { override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? {
try { try {
return if (defValues != null) { return if (defValues != null) {
preferences.decodeValue(SetSerializer(String.serializer()).nullable, key, defValues) preferences.decodeValue(SetSerializer(String.serializer()), key, defValues)
} else { } else {
preferences.decodeValue(SetSerializer(String.serializer()).nullable, key, null) preferences.decodeValueOrNull(SetSerializer(String.serializer()), key)
}?.toMutableSet() }
} catch (e: SerializationException) { } catch (e: SerializationException) {
throw ClassCastException("$key was not a StringSet") throw ClassCastException("$key was not a StringSet")
} }
@@ -174,4 +174,4 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
javaPreferences.removeNode() javaPreferences.removeNode()
return true return true
} }
} }
@@ -14,8 +14,6 @@ import java.io.File
import javax.imageio.ImageIO import javax.imageio.ImageIO
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
data class InstalledPackage(val root: File) { data class InstalledPackage(val root: File) {
val apk = File(root, "package.apk") val apk = File(root, "package.apk")
val jar = File(root, "translated.jar") val jar = File(root, "translated.jar")
@@ -40,20 +38,24 @@ data class InstalledPackage(val root: File) {
}?.filter { }?.filter {
it.tagName == "meta-data" it.tagName == "meta-data"
}?.map { }?.map {
putString(it.attributes.getNamedItem("android:name").nodeValue, putString(
it.attributes.getNamedItem("android:value").nodeValue) it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue
)
} }
} }
it.signatures = (parsed.apkSingers.flatMap { it.certificateMetas } it.signatures = (
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72 parsed.apkSingers.flatMap { it.certificateMetas }
.map { Signature(it.data) }.toTypedArray() /*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray()
} }
fun verify(): Boolean { fun verify(): Boolean {
val res = ApkVerifier.Builder(apk) val res = ApkVerifier.Builder(apk)
.build() .build()
.verify() .verify()
return res.isVerified return res.isVerified
} }
@@ -69,7 +71,7 @@ data class InstalledPackage(val root: File) {
}.sortedByDescending { it.width * it.height }.firstOrNull() ?: return }.sortedByDescending { it.width * it.height }.firstOrNull() ?: return
ImageIO.write(read, "png", icon) ImageIO.write(read, "png", icon)
} catch(e: Exception) { } catch (e: Exception) {
icon.delete() icon.delete()
} }
} }
@@ -77,7 +79,7 @@ data class InstalledPackage(val root: File) {
fun writeJar() { fun writeJar() {
try { try {
Dex2jar.from(apk).to(jar.toPath()) Dex2jar.from(apk).to(jar.toPath())
} catch(e: Exception) { } catch (e: Exception) {
jar.delete() jar.delete()
} }
} }
@@ -92,4 +94,4 @@ data class InstalledPackage(val root: File) {
return out return out
} }
} }
} }
@@ -48,7 +48,7 @@ class PackageController {
if (!installed.jar.exists()) { if (!installed.jar.exists()) {
throw IllegalStateException("Failed to translate APK dex!") throw IllegalStateException("Failed to translate APK dex!")
} }
} catch(t: Throwable) { } catch (t: Throwable) {
root.deleteRecursively() root.deleteRecursively()
throw t throw t
} }
@@ -63,7 +63,7 @@ class PackageController {
} }
fun deletePackage(pack: InstalledPackage) { fun deletePackage(pack: InstalledPackage) {
if(!pack.root.exists()) error("Package was never installed!") if (!pack.root.exists()) error("Package was never installed!")
val packageName = pack.info.packageName val packageName = pack.info.packageName
pack.root.deleteRecursively() pack.root.deleteRecursively()
@@ -74,7 +74,7 @@ class PackageController {
fun findPackage(packageName: String): InstalledPackage? { fun findPackage(packageName: String): InstalledPackage? {
val file = File(androidFiles.packagesDir, packageName) val file = File(androidFiles.packagesDir, packageName)
return if(file.exists()) return if (file.exists())
InstalledPackage(file) InstalledPackage(file)
else else
null null
@@ -84,4 +84,4 @@ class PackageController {
val pkgName = ApkParsers.getMetaInfo(apkFile).packageName val pkgName = ApkParsers.getMetaInfo(apkFile).packageName
return findPackage(pkgName)?.jar return findPackage(pkgName)?.jar
} }
} }
@@ -24,4 +24,4 @@ fun ApkMeta.toPackageInfo(apk: File): PackageInfo {
sourceDir = apk.absolutePath sourceDir = apk.absolutePath
} }
} }
} }
@@ -24,4 +24,4 @@ interface Resource {
fun getType(): Class<out Resource> fun getType(): Class<out Resource>
fun getValue(): Any? fun getValue(): Any?
} }
@@ -18,7 +18,7 @@ class ServiceSupport {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
fun startService(context: Context, intent: Intent) { fun startService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) {
val name = intentToClassName(intent) val name = intentToClassName(intent)
logger.debug { "Starting service: $name" } logger.debug { "Starting service: $name" }
@@ -27,15 +27,15 @@ class ServiceSupport {
runningServices[name] = service runningServices[name] = service
//Setup service // Setup service
thread { thread {
callOnCreate(service) callOnCreate(service)
//TODO Handle more complex cases // TODO Handle more complex cases
service.onStartCommand(intent, 0, 0) service.onStartCommand(intent, 0, 0)
} }
} }
fun stopService(context: Context, intent: Intent) { fun stopService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) {
val name = intentToClassName(intent) val name = intentToClassName(intent)
stopService(name) stopService(name)
} }
@@ -43,7 +43,7 @@ class ServiceSupport {
fun stopService(name: String) { fun stopService(name: String) {
logger.debug { "Stopping service: $name" } logger.debug { "Stopping service: $name" }
val service = runningServices.remove(name) val service = runningServices.remove(name)
if(service == null) { if (service == null) {
logger.warn { "An attempt was made to stop a service that is not running: $name" } logger.warn { "An attempt was made to stop a service that is not running: $name" }
} else { } else {
thread { thread {
@@ -63,6 +63,6 @@ class ServiceSupport {
fun serviceInstanceFromClass(className: String): Service { fun serviceInstanceFromClass(className: String): Service {
val clazzObj = Class.forName(className) val clazzObj = Class.forName(className)
return clazzObj.getDeclaredConstructor().newInstance() as? Service return clazzObj.getDeclaredConstructor().newInstance() as? Service
?: throw IllegalArgumentException("$className is not a Service!") ?: throw IllegalArgumentException("$className is not a Service!")
} }
} }
@@ -25,8 +25,9 @@ object KodeinGlobalHelper {
* Get a dependency * Get a dependency
*/ */
@JvmStatic @JvmStatic
@Suppress("UNCHECKED_CAST")
fun <T : Any> instance(type: Class<T>, kodein: DI? = null): T { fun <T : Any> instance(type: Class<T>, kodein: DI? = null): T {
return when(type) { return when (type) {
AndroidFiles::class.java -> { AndroidFiles::class.java -> {
val instance: AndroidFiles by (kodein ?: kodein()).instance() val instance: AndroidFiles by (kodein ?: kodein()).instance()
instance as T instance as T
@@ -63,5 +64,4 @@ object KodeinGlobalHelper {
fun <T : Any> instance(type: Class<T>): T { fun <T : Any> instance(type: Class<T>): T {
return instance(type, null) return instance(type, null)
} }
} }
@@ -1,36 +1,12 @@
# AndroidComapt Root dir
androidcompat.rootDir = androidcompat-root
# Allow/disallow preference changes (useful for demos) ####################### `android.files` (FilesConfigModule) #######################
ts.server.allowConfigChanges = true
# Enable the WebUI? Note: The API and multi-user sync server ui will remain available even if the WebUI is disabled
ts.server.enableWebUi = true
# 'true' to use the old, buggy/memory-leaking WebUI
ts.server.useOldWebUi = false
# 'true' to pretty print all JSON API responses
ts.server.prettyPrintApi = false
# List of blacklisted/whitelisted API endpoints/operation IDs
ts.server.disabledApiEndpoints = []
ts.server.enabledApiEndpoints = []
# Message to print in the console when the API has finished booting
ts.server.httpInitializedPrintMessage = ""
# Use external folder for static files
ts.server.useExternalStaticFiles = false
ts.server.externalStaticFilesFolder = ""
# Root storage dir
ts.server.rootDir = tachiserver-data
# Dir to store JVM patches
ts.server.patchesDir = ${ts.server.rootDir}/patches
# Storage dir for the emulated Android app # Storage dir for the emulated Android app
android.files.rootDir = ${ts.server.rootDir}/android-compat/appdata android.files.rootDir = ${androidcompat.rootDir}/appdata
# External storage dir for the emulated Android app's # External storage dir for the emulated Android app's
android.files.externalStorageDir = ${ts.server.rootDir}/android-compat/extappdata android.files.externalStorageDir = ${androidcompat.rootDir}/extappdata
# Internal Android directories # Internal Android directories
android.files.dataDir = ${android.files.rootDir}/data android.files.dataDir = ${android.files.rootDir}/data
@@ -48,37 +24,16 @@ android.files.externalCacheDirs = [${android.files.externalStorageDir}/cache]
android.files.externalMediaDirs = [${android.files.externalStorageDir}/media] android.files.externalMediaDirs = [${android.files.externalStorageDir}/media]
android.files.downloadCacheDir = ${android.files.externalStorageDir}/downloadCache android.files.downloadCacheDir = ${android.files.externalStorageDir}/downloadCache
android.files.packageDir = ${ts.server.rootDir}/android-compat/packages android.files.packageDir = ${androidcompat.rootDir}/android-compat/packages
####################### `android.app` (ApplicationInfoConfigModule) #######################
# Emulated Android app package name # Emulated Android app package name
android.app.packageName = eu.kanade.tachiyomi android.app.packageName = eu.kanade.tachiyomi
# Debug mode for the emulated Android app # Debug mode for the emulated Android app
android.app.debug = true android.app.debug = true
####################### `android.system` (SystemConfigModule) #######################
# Whether or not the emulated Android system is debuggable # Whether or not the emulated Android system is debuggable
android.system.isDebuggable = true android.system.isDebuggable = true
# Is the multi-user sync server enabled? Does not affect the single-user sync server included in the API.
ts.syncd.enable = false
# The URL of this server (displayed in the sync server web ui)
ts.syncd.baseUrl = "http://example.com"
# 'true' to disable the API and only enable the multi-user sync server
ts.syncd.syncOnlyMode = false
# The root directory to store synchronized data
ts.syncd.rootDir = ${ts.server.rootDir}/sync/accounts
# Location to store config files for the sandbox
ts.syncd.sandboxedConfig = ${ts.server.rootDir}/sync/sandboxed_config.config
# Recaptcha stuff for signup/login
ts.syncd.recaptcha.siteKey = ""
ts.syncd.recaptcha.secret = ""
# Sync server display name
ts.syncd.name = "Tachiyomi sync server"
# Header used to forward the IP to the multi-user sync server if the server is behind a reverse proxy
ts.syncd.ipHeader = ""
+11
View File
@@ -0,0 +1,11 @@
# Server: v0.X.Y-next + WebUI: rXXX
## TL;DR
- N/A
## Tachidesk-Server Changelog
- N/A
## Tachidesk-WebUI Changelog
- N/A
+387
View File
@@ -0,0 +1,387 @@
# Server: v0.6.0 + WebUI: r893
## TL;DR
- WebUI design went through a whole lot of changes, including
- Got rid of hamburger menu, now we have a custom mobile navbar
- Unread and Download count badges
- Back button so better electron experience
- There's a whole lot more that I'm too lazy to explore.
- Completely removed anime support
- Fixed category reordering
- Added support for search filters(Server side only)
- Added support for updating library(Server side only)
- A bunch of API breaking changes(hence why bumping to v0.6.0)!
## Tachidesk-Server Changelog
- (r996) cleanup (by @AriaMoradi)
- (r999) better cleaning algorithm (by @AriaMoradi)
- (r1007) remove anime support (by @AriaMoradi)
- (r1009) Fix tests ([#226](https://github.com/Suwayomi/Tachidesk-Server/pull/226) by @ntbm)
- (r1010) Expose unread and download count of Manga in category api ([#227](https://github.com/Suwayomi/Tachidesk-Server/pull/227) by @ntbm)
- (r1011) add Cache Header to Thumbnail Response for improved library performance ([#228](https://github.com/Suwayomi/Tachidesk-Server/pull/228) by @ntbm)
- (r1013) Fix unread and download counts casing ([#230](https://github.com/Suwayomi/Tachidesk-Server/pull/230) by @Syer10)
- (r1014) Fix broken test ([#231](https://github.com/Suwayomi/Tachidesk-Server/pull/231) by @ntbm)
- (r1016) Fix category reorder Endpoint. Added Test for Category Reorder ([#232](https://github.com/Suwayomi/Tachidesk-Server/pull/232) by @ntbm)
- (r1017) change windows bundle names (by @AriaMoradi)
- (r1018) improve tests (by @AriaMoradi)
- (r1019) allow injecting Sources (by @AriaMoradi)
- (r1020) update (by @AriaMoradi)
- (r1021) fix credit (by @AriaMoradi)
- (r1022) cleanup (by @AriaMoradi)
- (r1023) refactor (by @AriaMoradi)
- (r1024) refactor (by @AriaMoradi)
- (r1025) implement Source Filters (by @AriaMoradi)
- (r1026) ignore build artifacts generated by teting (by @AriaMoradi)
- (r1027) convert request type (by @AriaMoradi)
- (r1028) Update CONTRIBUTING.md (by @AriaMoradi)
- (r1029) stop supporting zero based image storage ([#242](https://github.com/Suwayomi/src/pull/242) by @AriaMoradi)
- (r1030) add manga data to download queue object ([#244](https://github.com/Suwayomi/src/pull/244) by @AriaMoradi)
- (r1031) Fix Manga Meta, add Manga Meta test ([#245](https://github.com/Suwayomi/src/pull/245) by @Syer10)
- (r1032) add pagination to recentChapters ([#246](https://github.com/Suwayomi/src/pull/246) by @AriaMoradi)
- (r1033) update (by @AriaMoradi)
- (r1034) Implement Update of Library/Category ([#235](https://github.com/Suwayomi/src/pull/235) by @ntbm)
- (r1035) update (by @AriaMoradi)
- (r1036) Mention the existence of Mahor's Tachidesk-GTK (by @AriaMoradi)
- (r1037) Add a Kotlin DSL for endpoint documentation ([#249](https://github.com/Suwayomi/Tachidesk-Server/pull/249) by @Syer10)
- (r1038) update (by @AriaMoradi)
- (r1039) update (by @AriaMoradi)
- (r1040) cleanup directory names ([#251](https://github.com/Suwayomi/Tachidesk-Server/pull/251) by @AriaMoradi)
- (r1041) Fix first page not being detected correctly ([#253](https://github.com/Suwayomi/Tachidesk-Server/pull/253) by @AriaMoradi)
- (r1042) Update README.md (by @AriaMoradi)
- (r1043) Update README.md (by @AriaMoradi)
- (r1044) migrate application directories ([#255](https://github.com/Suwayomi/Tachidesk-Server/pull/255) by @AriaMoradi)
- (r1045) add support for MultiSelectListPreference ([#258](https://github.com/Suwayomi/Tachidesk-Server/pull/258) by @AriaMoradi)
- (r1046) empty searchTerm support ([#259](https://github.com/Suwayomi/Tachidesk-Server/pull/259) by @AriaMoradi)
## Tachidesk-WebUI
- (r821) add Permanent sidebar for desktop widths([#46](https://github.com/Suwayomi/Tachidesk-WebUI/pull/46) by @abhijeetChawla)
- (r822) Fix Local Source being missing (by @AriaMoradi)
- (r823) fix the ugliness of bare messages (by @AriaMoradi)
- (r824) add pull request template (by @AriaMoradi)
- (r825) add Unread badges ([#48](https://github.com/Suwayomi/Tachidesk-WebUI/pull/48) by @ntbm)
- (r826) Back button implementation ([#47](https://github.com/Suwayomi/Tachidesk-WebUI/pull/47) by @abhijeetChawla)
- (r827) remove redundant '/manga' prefix from paths (by @AriaMoradi)
- (r828) refactor (by @AriaMoradi)
- (r829) put Sources and Extensions in the same screen (by @AriaMoradi)
- (r830) Set Fallback Image for broken Thumbnails ([#50](https://github.com/Suwayomi/Tachidesk-WebUI/pull/50) by @ntbm)
- (r833) Apply Api changes for unread badges ([#52](https://github.com/Suwayomi/Tachidesk-WebUI/pull/52) by @ntbm)
- (r834) add EmptyView to DownloadQueue, refactro strings ([#53](https://github.com/Suwayomi/Tachidesk-WebUI/pull/53) by @abhijeetChawla)
- (r835) Bottom navbar for mobile ([#51](https://github.com/Suwayomi/Tachidesk-WebUI/pull/51) by @abhijeetChawla)
- (r836) Implement Unread Filter for Library ([#54](https://github.com/Suwayomi/Tachidesk-WebUI/pull/54) by @ntbm)
- (r837) fix navbar broken logic (by @AriaMoradi)
- (r838) fix navbar (by @AriaMoradi)
- (r839) refactor (by @AriaMoradi)
- (r840) refactor (by @AriaMoradi)
- (r841) refactor (by @AriaMoradi)
- (r842) show different NavbarItems depending on device width (by @AriaMoradi)
- (r843) remove text decoration (by @AriaMoradi)
- (r844) fancy icon based on if path selected (by @AriaMoradi)
- (r845) custom Extension icon, google's version is shit (by @AriaMoradi)
- (r846) refactor (by @AriaMoradi)
- (r848) move info (by @AriaMoradi)
- (r849) add Search to Library ([#55](https://github.com/Suwayomi/Tachidesk-WebUI/pull/55) by @ntbm)
- (r850) add aspect ratio to the manga card. ([#56](https://github.com/Suwayomi/Tachidesk-WebUI/pull/56) by @abhijeetChawla)
- (r851) better wording (by @AriaMoradi)
- (r852) reorder nav buttons (by @AriaMoradi)
- (r853) nicer gradient (by @AriaMoradi)
- (r854) refactor MangaCard (by @AriaMoradi)
- (r855) closes #58 (by @AriaMoradi
- (r856) Add Resume Reading FAB Manga screen ([#59](https://github.com/Suwayomi/Tachidesk-WebUI/pull/59) by @abhijeetChawla)
- (r857) add filter and badge for `downloadCount` ([#62](https://github.com/Suwayomi/Tachidesk-WebUI/pull/62) by @abhijeetChawla)
- (r858) add issue template (by @AriaMoradi)
- (r859) Change color of navbar in light mode ([#65](https://github.com/Suwayomi/Tachidesk-WebUI/pull/65) by @abhijeetChawla)
- (r860) fix manga FAB margins ([#66](https://github.com/Suwayomi/Tachidesk-WebUI/pull/66) by @AriaMoradi)
- (r861) remove extra scrollbar on mobile ([#67](https://github.com/Suwayomi/Tachidesk-WebUI/pull/67) by @AriaMoradi)
- (r862) Fix Bad messages in Library Appbar search ([#70](https://github.com/Suwayomi/Tachidesk-WebUI/pull/70) by @ntbm)
- (r863) ban the style prop (by @AriaMoradi)
- (r864) Updates pagination update ([#68](https://github.com/Suwayomi/Tachidesk-WebUI/pull/68) by @AriaMoradi)
- (r865) make the whole chapter card into a button ([#73](https://github.com/Suwayomi/Tachidesk-WebUI/pull/73) by @AriaMoradi)
- (r866) fix chapter actions not working if manga is not fetched online ([#74](https://github.com/Suwayomi/Tachidesk-WebUI/pull/74) by @AriaMoradi)
- (r867) migrate some components to Mui5 new styling system ([#72](https://github.com/Suwayomi/Tachidesk-WebUI/pull/72) by @abhijeetChawla)
- (r868) load first page on read manga ([#76](https://github.com/Suwayomi/Tachidesk-WebUI/pull/76) by @AriaMoradi)
- (r869) Revert "migrate some components to Mui5 new styling system ([#72](https://github.com/Suwayomi/Tachidesk-WebUI/pull/72))" (by @AriaMoradi)
- (r870) migrate Backup to Mui 5 ([#106](https://github.com/Suwayomi/Tachidesk-WebUI/pull/106) by @AriaMoradi)
- (r871) migrate EmptyView to Mui 5 ([#95](https://github.com/Suwayomi/Tachidesk-WebUI/pull/95) by @AriaMoradi)
- (r872) migrate CategorySelect to Mui 5 ([#85](https://github.com/Suwayomi/Tachidesk-WebUI/pull/85) by @AriaMoradi)
- (r873) migrate LibraryOptions to Mui 5 ([#83](https://github.com/Suwayomi/Tachidesk-WebUI/pull/83) by @AriaMoradi)
- (r874) migrate ChapterCard.tsx to Mui 5 ([#80](https://github.com/Suwayomi/Tachidesk-WebUI/pull/80) by @AriaMoradi)
- (r875) migrate App.tsx to Mui 5 ([#79](https://github.com/Suwayomi/Tachidesk-WebUI/pull/79) by @AriaMoradi)
- (r876) migrate SourceConfigure to Mui 5 ([#103](https://github.com/Suwayomi/Tachidesk-WebUI/pull/103) by @AriaMoradi)
- (r877) migrate Settings to Mui 5 ([#102](https://github.com/Suwayomi/Tachidesk-WebUI/pull/102) by @AriaMoradi)
- (r878) migrate Updates to Mui 5 ([#104](https://github.com/Suwayomi/Tachidesk-WebUI/pull/104) by @AriaMoradi)
- (r879) Save tabs number in Url to persist tab when go to other paths ([#78](https://github.com/Suwayomi/Tachidesk-WebUI/pull/78) by @abhijeetChawla)
- (r880) migrate LangSelect to Mui 5 ([#86](https://github.com/Suwayomi/Tachidesk-WebUI/pull/86) by @AriaMoradi)
- (r881) migrate ExtensionCard.tsx to Mui 5 ([#81](https://github.com/Suwayomi/Tachidesk-WebUI/pull/81) by @AriaMoradi)
- (r882) migrate SingleSearch to Mui 5 ([#101](https://github.com/Suwayomi/Tachidesk-WebUI/pull/101) by @AriaMoradi)
- (r883) migrate LoadingPlaceholder to Mui 5 ([#96](https://github.com/Suwayomi/Tachidesk-WebUI/pull/96) by @AriaMoradi)
- (r884) migrate About to Mui 5 ([#105](https://github.com/Suwayomi/Tachidesk-WebUI/pull/105) by @AriaMoradi)
- (r885) migrate SourceCard to Mui 5 ([#82](https://github.com/Suwayomi/Tachidesk-WebUI/pull/82) by @AriaMoradi)
- (r886) migrate Manga to Mui 5 ([#99](https://github.com/Suwayomi/Tachidesk-WebUI/pull/99) by @AriaMoradi)
- (r887) migrate Browse to Mui 5 ([#98](https://github.com/Suwayomi/Tachidesk-WebUI/pull/98) by @AriaMoradi)
- (r888) migrate DesktopSideBar to Mui 5 ([#87](https://github.com/Suwayomi/Tachidesk-WebUI/pull/87) by @AriaMoradi)
- (r889) cleanup library ([#107](https://github.com/Suwayomi/Tachidesk-WebUI/pull/107) by @AriaMoradi)
- (r890) support for new searchTerm (by @AriaMoradi)
- (r891) Revert "support for new searchTerm" (by @AriaMoradi)
- (r892) add support for emptySearch ([#109](https://github.com/Suwayomi/Tachidesk-WebUI/pull/109) by @AriaMoradi)
- (r893) add support for MultiSelectListPreference ([#108](https://github.com/Suwayomi/Tachidesk-WebUI/pull/108) by @AriaMoradi)
# Server: v0.5.4 + WebUI: r820
## TL;DR
- Fixed ReadComicOnline, Toonily and possibly other sources not working
- Backup and Restore now includes Updates tab data
- Removed Anime support from WebUI, Anime support will also be removed from Tachidesk-Server in a future update
## Tachidesk-Server Changelog
- (r973) convert android.jar lib to a maven repo
- (r978) mimic Tachiyomi's behaviour more closely, fixes ReadComicOnline (EN)
- (r980) fix export chapter ordering, include new props in backup
- (r982) remove isNsfw annotation detection
- (r984) use correct time conversion units when doing backups
- (r989) Support using a CatalogueSource instead of only HttpSources ([#219](https://github.com/Suwayomi/Tachidesk-Server/pull/219) by @Syer10)
- (r991) Use a custom task to run electron ([#220](https://github.com/Suwayomi/Tachidesk-Server/pull/220) by @Syer10)
## Tachidesk-WebUI Changelog
- (r810) fix wrong strings in set Server Address dialog, fixes [#39](https://github.com/Suwayomi/Tachidesk-WebUI/issues/39)
- (r811) fix chapterFetch loop
- (r812) fix overlapping requests
- (r813) fix typo
- (r814) Better portrait support ([#41](https://github.com/Suwayomi/Tachidesk-WebUI/issues/41) by @minhe7735)
- (r815) fixes Reader navbar colors when in light mode ([#43](https://github.com/Suwayomi/Tachidesk-WebUI/issues/43) by @abhijeetChawla)
- (r816) default languages cleanup, force Local source enabled
- (r817) force Local source at LangSelect
- (r818) rename ExtensionLangSelect: generic name for generic use
- (r819) don't show anime anymore
- (r820) Remove Anime support
# Server: v0.5.3 + WebUI: r809
## TL;DR
- added support for a equivalent page to Tachiyomi's Updates tab
- fix launchers not working on macOS M1/arm64
## Tachidesk-Server Changelog
- (r956) fix macOS-arm64 bundle launchers not working
- (r957) Workaround StdLib issue and add KtLint to all modules ([#206](https://github.com/Suwayomi/Tachidesk-Server/pull/206) by @Syer10)
- (r960-r963) Add recently updated chapters(Updates) endpoint
## Tachidesk-WebUI Changelog
- (r808) fix chapter list not calling onlineFetch=true
- (r809) add support for Updates
# Server: v0.5.2 + WebUI: r807
## TL;DR
- Fixed Local source not working on Windows
- Fixed Chapter numbers being shown incorrectly
## Tachidesk-Server
### Public API
#### Non-breaking changes
- N/A
#### Breaking changes
- N/A
#### Bug fixes
- (r948) Fix ManaToki (KO) and NewToki (KO) (issue [#202](https://github.com/Suwayomi/Tachidesk-Server/issue/202))
- (r949) Local source: fix windows paths
### Private API
- (r941) Update BytecodeEditor to use Java NIO Paths ([#200](https://github.com/Suwayomi/Tachidesk-Server/pull/200) by @Syer10)
- (r942) Gradle Updates ([#199](https://github.com/Suwayomi/Tachidesk-Server/pull/199) by @Syer10)
## Tachidesk-WebUI
#### Visible changes
- (r804) update text positioning on Reader and Player ([#35](https://github.com/Suwayomi/Tachidesk-WebUI/pull/35) by @voltrare)
- (r806) Source card for Local source is different
- (r807) add Local source guide
#### Bug fixes
- (r805) fix chapter name
#### Internal changes
- N/A
# Server: v0.5.1 + WebUI: r803
## TL;DR
- Loading sources' manga list is at least twice as fast
- Added support for Tachiyomi's Local source
- Added BasicAuth support, now you can protect your Tachidesk instance if you are running it on a public server
- Added ability to turn off cache for image requests
## Tachidesk-Server
### Public API
#### Non-breaking changes
- (r915) add BasicAuth support
- (r918) add ability to delete downloaded chapters
- (r923-r930) add Local Source
- (r938) add ability to turn off cache for image requests
#### Breaking changes
- N/A
#### Bug fixes
- (r917) detect if a downloaded chapter is missing
### Private API
- (r913) remove expand char limit on MangaTable columns
- (r914) migrate to Javalin 4
- (r921) depricate zero based chapters
- (r937) add ChapterRecognition from tachiyomi, closes #10
## Tachidesk-WebUI
#### Visible changes
- (r790) nice looking progress percentage
- (r791) show a Delete button for downloaded chapters
- (r792) Update hover effect using more of Material-UI color pallete ([#29](https://github.com/Suwayomi/Tachidesk-WebUI/pull/29) by @voltrare)
- (r793) Optimize images ([#32](https://github.com/Suwayomi/Tachidesk-WebUI/pull/32) by @phanirithvij)
- (r794) try fix #30 ([#31](https://github.com/Suwayomi/Tachidesk-WebUI/pull/31) by @phanirithvij)
- (r795) fix viewing page number when the string is long
- (r796) show proper display name for source
- (r797) fail gracefully when a thumbnail has errors
- (r798) fix when a source fails to load mangas
- (r800) add Local source ([#31](https://github.com/Suwayomi/Tachidesk-WebUI/pull/31))
- (r803) add support for useCache
#### Bug fixes
- N/A
#### Internal changes
- N/A
# Server: v0.5.0 + WebUI: r789
## TL;DR
- You can now install APK extensions from the extensions page
- WebUI now comes with an updated Material Design looks and is faster a little bit.
- WebUI now shows Nsfw content by default, disable it in settings if you prefer to not see Nsfw stuff
- Added support for configuration of sources, this enables MangaDex, Komga, Cubari and many other sources
- Chapters in the Manga page and Sources in the source page now look nicer and will glow with mouse hover
## Tachidesk-Server
### Public API
#### Non-breaking changes
- (r888) add installing APK from external sources endpoint
#### Breaking changes
- (r877 [#188](https://github.com/Suwayomi/Tachidesk-Server/pull/188) by @Syer10) `MangaDataClass.genre` changed type to `List<String>`
#### Bug fixes
- (r899-r901) fix when an external apk is installed and it doesn't have the default tachiyomi-extensions name
- (r905) fix a bug where if two sources return the same URL, a false duplicate might be detected
### Private API
- (r887) the `run` task won't call `downloadWebUI` now
- (r902) cleanup print/ln instances
- (r906) better handling of uninstalling Extensions
## Tachidesk-WebUI
#### Visible changes
- (r770) add support for the new genre type
- (r771) set the default value of `showNsfw` to `true` so we won't have visual artifacts with a clean install
- (r774 [#21](https://github.com/Suwayomi/Tachidesk-WebUI/pull/21) by @voltrare) `ReaderNavbar.jsx`: Swap close and retract Navbar buttons
- (r775 [#23](https://github.com/Suwayomi/Tachidesk-WebUI/pull/23) by @voltrare) `yarn.lock`: Fixes version inconsistency after commit 9b866811b
- (r776 [#23](https://github.com/Suwayomi/Tachidesk-WebUI/pull/23) by @voltrare) add margin between Source and Extension cards, make the Search button look nicer
- (r777) add support for installing external APK files
- (r778) fix the makeToaster?
- (r779) Action button for installing external extension
- (r780 Suwayomi/Tachidesk-WebUI#25) add on hover, active effect to Chapter/Episode card
- (r782-r785) updating material-ui to v5 changed the theme
- (r785-r788) better `SourceCard` looks on mobile, move `SourceDataClass.isConfigurable` gear button to `SourceMangas`
- (r789) implement source configuration
#### Bug fixes
- N/A
#### Internal changes
- (r782-r785) update dependencies, migrate material-ui from v4 to v5
# Server: v0.4.9 + WebUI: r769
## Tachidesk-Server
### Public API
#### Non-breaking changes
- N/A
#### Breaking changes
- (r857) renamed: `SourceDataClass.isNSFW` -> `SourceDataClass.isNsfw`
#### Bug fixes
- N/A
### Private API
- (r850) Bump WebUI version to r767
- (r861) Bump WebUI version to r769
#### Non-code changes
- (r851) Add this changelog file and `CHANGELOG-TEMPLATE.md`
- (r852-r853) `CONTRIBUTING.md`: Add a note about this maintaining this file changelog
- (r855) `CONTRIBUTING.md`: text cleanup
- (r859) `CONTRIBUTING.md`: remove dumb rule
- (r862) `windows-bundler.sh`: update jre
- (r864) add linux and macOS bundler script and launcher scripts
- (r865) fix macOS bundler script and launcher scripts
- (r866) bump electron version to v14.0.0
- (r868) add linux and macOS bundlers to the publish workflow
- (r871) `publish.yml`: remove node module cache, won't need it anymore
- (r873) `publish.yml` and `build_push.yml`: fix oopsies
## Tachidesk-WebUI
#### Visible changes
- (r767-r769) Support for hiding NSFW content in settings screen, extensions screen, sources screen
#### Bug fixes
- N/A
#### Internal changes
- (r767) Remove some duplicate dependency declaration from `package.json`
#### Non-code changes
- (r42-r45) Change `README.md`: some links and stuff
- (r45-r765) Add all of the commit history from when WebUI was separated from Server, jumping from r45 to r765 (r45 is exactly the same as r765)
- (r766) Steal `.gitattributes` from Tachidesk-Server
- (r767) Dependency cleanup in `package.json`
# Server: v0.4.8 + WebUI: r41
## Tachidesk-Server
### Public API
#### Non-breaking changes
- Added support for serializing Search Filters
- `SourceDataClass` now has a `isNsfw` key
#### Breaking changes
- N/A
#### Bug fixes
- Fixed a bug where backup restore reversed chapter order
- Open Site feature now works properly (https://github.com/Suwayomi/Tachidesk-WebUI/issues/19)
### Private API
- Added `CloudflareInterceptor` from TachiWeb-Server
- Restoring backup for mangas in library(merging manga data) is now supported
## Tachidesk-WebUI
#### Visible changes
- Better looking manga card titles
- Better reader title, next, prev buttons
#### Bug fixes
- Open Site feature now works properly (https://github.com/Suwayomi/Tachidesk-WebUI/issues/19)
- Re-ordering categories now works
#### Internal changes
- N/A
+41 -37
View File
@@ -2,51 +2,55 @@
## Where should I start? ## Where should I start?
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap. 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. **Note 1:** Notify the developers on [Suwayomi discord](https://discord.gg/DDZdqZWaHA) (#tachidesk-server and #tachidesk-webui channels) 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? **Note 2:** Your pull request will be squashed into a single commit.
### Project goals and vision
- Porting Tachiyomi and covering it's features
- Syncing with Tachiyomi, [main issue](https://github.com/Suwayomi/Tachidesk-Server/issues/159)
- Generally rejecting features that Tachiyomi(main app) doesn't have,
- Unless it's something that makes sense for desktop sizes or desktop form factor (keyboard + mouse)
- Additional/crazy features can go in forks and alternative clients
- [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI) should
- be responsive
- support both desktop and mobile form factors well
## How does Tachidesk-Server work?
This project has two components: 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`. 1. **Server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run jar libraries converted from apk extensions. All this concludes to serving a REST API.
2. **webUI:** A react SPA(`create-react-app`) project that works with the server to do the presentation. 2. **WebUI:** A React SPA(`create-react-app`) project that works with the server to do the presentation located at https://github.com/Suwayomi/Tachidesk-WebUI
## Why a web app? ## Why a web app?
This structure is chosen to This structure is chosen to
- Achieve the maximum multi-platform-ness - 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 - Gives the ability to access Tachidesk-Server from a remote client e.g., your phone, tablet or smart TV
- Eaise development of alternative user intefaces for Tachidesk - Ease development of user interfaces 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 ## Building from source
### Prerequisites ### Prerequisites
You need these software packages installed in order to build the project 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
First Build the jar, then cd into the `scripts` directory and run `./windows<bits>-bundler.sh` (or `./windows<bits>-bundler.ps1` if you are on windows), the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win64.zip`.
## Running in development mode
First satisfy [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 opened 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.
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
### building the full-blown jar (Tachidesk-Server + Tachidesk-WebUI bundle)
Run `./gradlew server:downloadWebUI server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx.jar`.
### building without `webUI` bundled (server only)
Delete `server/src/main/resources/WebUI.zip` if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx.jar`.
### building the Windows package
First Build the jar, then cd into the `scripts` directory and run `./windows-bundler.sh win32` or `./windows-bundler.sh win64` depending on the target architecture, the resulting built zip package file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx-winXX.zip`.
## Running in development mode
run `./gradlew :server:run --stacktrace` to run the server
## Running tests
run `./gradlew :server:test` to execute all tests
to test a specific class run `./gradlew :server:test --tests <package.with.classname>`
## Building the android-jar maven repository
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,
then use `AndroidCompat/lib/android.jar` to manually create a maven repository inside the `android-jar` git branch.
Update the dependency declaration afterwards.
+106 -26
View File
@@ -3,56 +3,111 @@
|-------|----------|---------|---------| |-------|----------|---------|---------|
| ![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-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) | | ![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-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) |
# Tachidesk ## Table of Content
- [What is Tachidesk?](#what-is-tachidesk)
- [Tachidesk client projects](#tachidesk-client-projects)
* [Is this application usable? Should I test it?](#is-this-application-usable-should-i-test-it)
- [Downloading and Running the app](#downloading-and-running-the-app)
* [Using Operating System Specific Bundles](#using-operating-system-specific-bundles)
- [Launcher Scripts](#launcher-scripts)
+ [Windows](#windows)
+ [macOS](#macos)
+ [GNU/Linux](#gnulinux)
* [Other methods of getting Tachidesk](#other-methods-of-getting-tachidesk)
+ [Arch Linux](#arch-linux)
+ [Ubuntu-based distributions](#ubuntu-based-distributions)
+ [Docker](#docker)
* [Advanced Methods](#advanced-methods)
+ [Running the jar release directly](#running-the-jar-release-directly)
+ [Using Tachidesk Remotely](#using-tachidesk-remotely)
- [Syncing With Tachiyomi](#syncing-with-tachiyomi)
- [Troubleshooting and Support](#troubleshooting-and-support)
- [Contributing and Technical info](#contributing-and-technical-info)
- [Credit](#credit)
- [License](#license)
<!-- Generated with https://ecotrust-canada.github.io/markdown-toc/ -->
# What is Tachidesk?
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/> <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/). A free and open source manga reader server that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi. 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. `Tachidesk` is a general term used to describe the combination of Tachidesk-Server(this project) and one of our clients.
Think of it roughly like the concept of "distribution" in GNU/Linux distributions, in which Linux(Tachidesk-Server) is the kernel and the difference is which desktop environment(Tachidesk client) you get with it.
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature. Tachidesk-Server 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.
**Tachidesk needs serious front-end dev help for it's reader and other parts, if you like the app and want to see it become better please don't hesitate to contribute some code!** Ability to sync with Tachiyomi is a planned feature.
# Tachidesk client projects
**You need a client/user interface app as a front-end for Tachidesk-Server, if you Directly Download Tachidesk-Server you'll get a bundled version of [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI) with it.**
Here's a list of known clients/user interfaces for Tachidesk-Server:
- [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI): The web/ElectronJS front-end that Tachidesk-Server is traditionally shipped with. Usually gets new features faster.
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Tachidesk-Server. Currently the most advanced. Downlading Tachidesk-JUI is not recommened for now, the current release is getting obsolete, a new version is to be released soon(TM).
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), in super early stage of development.
- [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client, in super early stage of development.
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stage of development. Seemingly abandoned.
## Is this application usable? Should I test it? ## Is this application usable? Should I test it?
Here is a list of current features: Here is a list of current features:
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources. - 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. - A library to save your mangas and categories to put them into
- Searching and browsing installed sources. - Searching and browsing installed sources
- A decent chapter reader.
- Ability to download Manga for offline read - Ability to download Manga for offline read
- Backup and restore support powered by Tachiyomi Legacy Backups - Backup and restore support powered by Tachiyomi-compatible Backups
- Viewing latest updated chapters.
**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. **Note:** These are capabilities of Tachidesk-Server, the actual working support is provided by each front-end app, checkout their respective readme for more info.
## Downloading and Running the app # Downloading and Running the app
### All Operating Systems ## Using Operating System Specific Bundles
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. To facilitate the use of Tachidesk we provide bundle releases that include The Java Runtime Environment, ElectronJS and 3 Tachidesk Launcher Scripts.
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases). If a bundle for your operating system or cpu architecture is not provided then refer to [Advanced Methods](#advanced-methods)
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. #### Launcher Scripts
- `Tachidesk Electron Launcher`: Launches Tachidesk inside Electron as a desktop applicaton
- `Tachidesk Browser Launcher`: Launches Tachidesk in a browser window
- `Tachidesk Debug Launcher`: Launches Tachidesk with debug logs attached. If Tachidesk doesn't work for you, running this can give you insight into why.
**Node:** Linux launcher scripts are named a bit differently but work the same.
### Windows ### Windows
Download the latest "Stable" win32 or win64 (depending on your system, usually you want win64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases). Download the latest `win32`(Windows 32-bit) or `win64`(Windows 64-bit) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/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-win64.zip` and run one of the Launcher files depending on what you want(see bellow). The rest works like the previous section. Unzip the downloaded file and double click on one of the launcher scripts.
#### Windows Launchers
- `Tachidesk Electron Launcher.bat`: Launches Tachidesk inside Electron as a desktop applicaton
- `Tachidesk Browser Launcher.bat`: Launches Tachidesk in a browser window
- `Tachidesk Debug Launcher.bat`: Launches Tachidesk with debug logs attached. If Tachidesk doesn't work for you, running this can give you insight into why.
### macOS
Download the latest `macOS-x64`(older macOS systems) or `macOS-arm64`(Apple M1) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
Unzip the downloaded file and double click on one of the launcher scripts.
### GNU/Linux
Download the latest `linux-x64`(x86_64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
`tar xvf` the downloaded file and double click on one of the launcher scripts or run them using the terminal.
## Other methods of getting Tachidesk
### Arch Linux ### Arch Linux
You can install Tachidesk from the AUR You can install Tachidesk from the AUR
``` ```
yay -S tachidesk yay -S tachidesk
``` ```
### Ubuntu-based distributions
More information can be found on the [PPA's page](https://launchpad.net/~suwayomi/+archive/ubuntu/tachidesk).
```
sudo add-apt-repository ppa:suwayomi/tachidesk
sudo apt update
sudo apt install tachidesk
```
### Docker ### Docker
Check our Offical Docker release [Tachidesk Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) or use [arbuilder's](https://github.com/arbuilder/Tachidesk-docker) tachidesk docker repo for installation. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk). By default the server will be running on http://localhost:4567 open this url in your browser. Check our Official Docker release [Tachidesk Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) for running Tachidesk Server in a docker container. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk). By default the server will be running on http://localhost:4567 open this url in your browser.
Install from the command line: Install from the command line:
``` ```
@@ -63,8 +118,33 @@ Run Container from the command line:
$ docker run -p 4567:4567 ghcr.io/suwayomi/tachidesk $ docker run -p 4567:4567 ghcr.io/suwayomi/tachidesk
``` ```
## Advanced Methods
### Running the jar release directly
In order to run the app you need the following:
- The jar release of Tachidesk-Server
- The Java Runtime Environment(JRE) 8 or newer
- A Browser like Google Chrome, Firefox, Edge, etc.
- ElectronJS (optional)
Download the latest `.jar` release from [the releases section](https://github.com/Suwayomi/Tachidesk-Server/releases) or a preview jar build from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
Make sure you have The Java Runtime Environment installed on your system, Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically.
### Using Tachidesk Remotely ### 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. You can run Tachidesk on your computer or a server and connect to it remotely through one of our clients or the bundled web interface with a web browser. This method of using Tachidesk is requires a bit of networking/firewall/port forwarding/server configuration/etc. knowledge on your side, if you can run a Minecraft server and configure it, then you are good to go.
Check out [this wiki page](https://github.com/Suwayomi/Tachidesk-Server/wiki/Configuring-Tachidesk-Server) for a guide on configuring Tachidesk-Server.
If you face issues with your setup then we are happy to provide help, just join our discord server(a discord badge is on the top of the page, you are just a click clack away!).
## Syncing With Tachiyomi
### The Tachidesk extension
- You can install the `Tachidesk` extension inside tachiyomi.
- The extension will load Tachidesk library.
- By manipulating filters you can browse your categories.
### Other methods
Checkout [this issue](https://github.com/Suwayomi/Tachidesk-Server/issues/159) for tracking progress.
## Troubleshooting and Support ## Troubleshooting and Support
See [this troubleshooting wiki page](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting). See [this troubleshooting wiki page](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting).
@@ -75,9 +155,9 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md).
## Credit ## Credit
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. 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`. 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` and `Copyright 2019 Andy Bao and contributors`.
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also 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` and `Copyright 2015 Javier Tomás`.
You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0 You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
+45 -21
View File
@@ -1,8 +1,12 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jmailen.gradle.kotlinter.tasks.FormatTask
import org.jmailen.gradle.kotlinter.tasks.LintTask
plugins { plugins {
kotlin("jvm") version "1.4.32" kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version "1.4.32" apply false kotlin("plugin.serialization") version kotlinVersion
id("org.jmailen.kotlinter") version "3.6.0"
id("com.github.gmazzo.buildconfig") version "3.0.3" apply false
} }
allprojects { allprojects {
@@ -12,10 +16,9 @@ allprojects {
repositories { repositories {
mavenCentral() mavenCentral()
maven("https://maven.google.com/") google()
maven("https://jitpack.io") maven("https://jitpack.io")
maven("https://oss.sonatype.org/content/repositories/snapshots/") maven("https://github.com/Suwayomi/Tachidesk-Server/raw/android-jar/")
maven("https://dl.google.com/dl/android/maven2/")
} }
} }
@@ -27,18 +30,38 @@ val projects = listOf(
configure(projects) { configure(projects) {
apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
apply(plugin = "org.jmailen.kotlinter")
java { java {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
tasks.withType<KotlinCompile> { tasks {
kotlinOptions { withType<KotlinCompile> {
jvmTarget = JavaVersion.VERSION_1_8.toString() dependsOn(formatKotlin)
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs = listOf(
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}
withType<LintTask> {
source(files("src/kotlin"))
}
withType<FormatTask> {
source(files("src/kotlin"))
} }
} }
dependencies { dependencies {
// Kotlin // Kotlin
implementation(kotlin("stdlib-jdk8")) implementation(kotlin("stdlib-jdk8"))
@@ -46,31 +69,30 @@ configure(projects) {
testImplementation(kotlin("test-junit5")) testImplementation(kotlin("test-junit5"))
// coroutines // coroutines
val coroutinesVersion = "1.4.3" val coroutinesVersion = "1.5.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
val kotlinSerializationVersion = "1.1.0" val kotlinSerializationVersion = "1.3.0-RC"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
// Dependency Injection // Dependency Injection
implementation("org.kodein.di:kodein-di-conf-jvm:7.5.0") implementation("org.kodein.di:kodein-di-conf-jvm:7.8.0")
// Logging // Logging
implementation("org.slf4j:slf4j-api:1.7.30") implementation("org.slf4j:slf4j-api:1.7.32")
implementation("ch.qos.logback:logback-classic:1.2.3") implementation("ch.qos.logback:logback-classic:1.2.6")
implementation("io.github.microutils:kotlin-logging:2.0.6") implementation("io.github.microutils:kotlin-logging:2.0.11")
// ReactiveX // ReactiveX
implementation("io.reactivex:rxjava:1.3.8") implementation("io.reactivex:rxjava:1.3.8")
implementation("io.reactivex:rxkotlin:1.0.0") implementation("io.reactivex:rxkotlin:1.0.0")
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0") implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
// JSoup // dependency both in AndroidCompat and extensions, version locked by Tachiyomi app/extensions
implementation("org.jsoup:jsoup:1.13.1") implementation("org.jsoup:jsoup:1.14.2")
// dependency of :AndroidCompat:Config // dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.1") implementation("com.typesafe:config:1.4.1")
@@ -79,13 +101,15 @@ configure(projects) {
// to get application content root // to get application content root
implementation("net.harawata:appdirs:1.2.1") implementation("net.harawata:appdirs:1.2.1")
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon // dex2jar
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon") val dex2jarVersion = "v26"
implementation("com.github.ThexXTURBOXx.dex2jar:dex-translator:$dex2jarVersion")
implementation("com.github.ThexXTURBOXx.dex2jar:dex-tools:$dex2jarVersion")
// APK parser // APK parser
implementation("net.dongliu:apk-parser:2.6.10") implementation("net.dongliu:apk-parser:2.6.10")
// Jackson // dependency both in AndroidCompat and server, version locked by javalin
implementation("com.fasterxml.jackson.core:jackson-annotations:2.10.3") implementation("com.fasterxml.jackson.core:jackson-annotations:2.12.4")
} }
} }
+11
View File
@@ -0,0 +1,11 @@
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
dependencies {
implementation("net.lingala.zip4j:zip4j:2.9.0")
}
+33
View File
@@ -0,0 +1,33 @@
import java.io.BufferedReader
/*
* 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/. */
const val kotlinVersion = "1.5.30"
const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.6.0"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r893"
// counts commits on the master branch
val tachideskRevision = runCatching {
System.getenv("ProductRevision") ?: Runtime
.getRuntime()
.exec("git rev-list HEAD --count")
.let { process ->
process.waitFor()
val output = process.inputStream.use {
it.bufferedReader().use(BufferedReader::readText)
}
process.destroy()
"r" + output.trim()
}
}.getOrDefault("r0")
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
+3
View File
@@ -0,0 +1,3 @@
cd "`dirname "$0"`"
./jre/Contents/Home/bin/java -jar Tachidesk.jar
@@ -1 +1,7 @@
jre\bin\java -Dsuwayomi.tachidesk.server.debugLogsEnabled=true -jar Tachidesk.jar :: cleaner output
@echo off
jre\bin\java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
:: Prevent cmd from closing when Tachidesk crashes
pause
+3
View File
@@ -0,0 +1,3 @@
cd "`dirname "$0"`"
./jre/Contents/Home/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
@@ -1 +1 @@
jre\bin\javaw "-Dsuwayomi.tachidesk.server.webInterface=electron" "-Dsuwayomi.tachidesk.server.electronPath=electron/electron.exe" -jar Tachidesk.jar jre\bin\javaw "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
+3
View File
@@ -0,0 +1,3 @@
cd "`dirname "$0"`"
./jre/Contents/Home/bin/java "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/Electron.app/Contents/MacOS/Electron" -jar Tachidesk.jar
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
./jre/bin/java -jar Tachidesk.jar
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
./jre/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
./jre/bin/java "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron" -jar Tachidesk.jar
+89
View File
@@ -0,0 +1,89 @@
#/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/.
electron_version="v14.0.0"
if [ $1 = "linux-x64" ]; then
jre="OpenJDK8U-jre_x64_linux_hotspot_8u302b08.tar.gz"
jre_release="jdk8u302-b08"
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
jre_dir="$jre_release-jre"
electron="electron-$electron_version-linux-x64.zip"
elif [ $1 = "macOS-x64" ]; then
jre="OpenJDK8U-jre_x64_mac_hotspot_8u302b08.tar.gz"
jre_release="jdk8u302-b08"
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
jre_dir="$jre_release-jre"
electron="electron-$electron_version-darwin-x64.zip"
elif [ $1 = "macOS-arm64" ]; then
jre="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64.tar.gz"
jre_release="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64"
jre_url="https://cdn.azul.com/zulu/bin/$jre"
jre_dir="$jre_release/zulu-8.jre"
electron="electron-$electron_version-darwin-arm64.zip"
else
echo "Unsupported arch value: $1"
exit 1
fi
arch="$1"
os=$(echo $arch | cut -d '-' -f1)
echo "creating $arch bundle"
jar=$(ls ../server/build/*.jar | tail -n1)
jar_name=$(echo $jar | cut -d'/' -f4)
release_name=$(echo $jar_name | sed 's/.jar//')-$arch
# make release dir
mkdir $release_name
echo "Dealing with jre..."
if [ ! -f $jre ]; then
curl -L $jre_url -o $jre
fi
tar xvf $jre
mv $jre_dir $release_name/jre
echo "Dealing with electron"
if [ ! -f $electron ]; then
curl -L "https://github.com/electron/electron/releases/download/$electron_version/$electron" -o $electron
fi
unzip $electron -d $release_name/electron
# copy artifacts
cp $jar $release_name/Tachidesk.jar
if [ $os = linux ]; then
cp "resources/tachidesk-browser-launcher.sh" $release_name
cp "resources/tachidesk-debug-launcher.sh" $release_name
cp "resources/tachidesk-electron-launcher.sh" $release_name
elif [ $os = macOS ]; then
cp "resources/Tachidesk Browser Launcher.command" $release_name
cp "resources/Tachidesk Debug Launcher.command" $release_name
cp "resources/Tachidesk Electron Launcher.command" $release_name
fi
archive_name=""
if [ $os = linux ]; then
archive_name=$release_name.tar.gz
GZIP=-9 tar cvzf $archive_name $release_name
elif [ $os = macOS ]; then
archive_name=$release_name.zip
zip -9 -r $archive_name $release_name
fi
rm -rf $release_name
# clean up from possible previous runs
if [ -f ../server/build/$archive_name ]; then
rm ../server/build/$archive_name
fi
mv $archive_name ../server/build/
+10 -6
View File
@@ -6,19 +6,23 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this # 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/. # file, You can obtain one at https://mozilla.org/MPL/2.0/.
electron_version="v12.0.9" electron_version="v14.0.0"
if [ $1 = "win32" ]; then if [ $1 = "win32" ]; then
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
arch="win32" jre_release="jdk8u292-b10"
jre_url="https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/$jre_release/$jre"
arch="windows-x86"
electron="electron-$electron_version-win32-ia32.zip" electron="electron-$electron_version-win32-ia32.zip"
else else
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" jre="OpenJDK8U-jre_x64_windows_hotspot_8u302b08.zip"
arch="win64" jre_release="jdk8u302-b08"
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
arch="windows-x64"
electron="electron-$electron_version-win32-x64.zip" electron="electron-$electron_version-win32-x64.zip"
fi fi
jre_dir="jdk8u292-b10-jre" jre_dir="$jre_release-jre"
echo "creating windows bundle" echo "creating windows bundle"
@@ -33,7 +37,7 @@ mkdir $release_name
echo "Dealing with jre..." echo "Dealing with jre..."
if [ ! -f $jre ]; then if [ ! -f $jre ]; then
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/$jre" -o $jre curl -L $jre_url -o $jre
fi fi
unzip $jre unzip $jre
mv $jre_dir $release_name/jre mv $jre_dir $release_name/jre
+81 -100
View File
@@ -1,24 +1,10 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import de.undercouch.gradle.tasks.download.Download
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
import java.time.Instant import java.time.Instant
plugins { plugins {
application application
id("com.github.johnrengelman.shadow") version "7.0.0" id("com.github.johnrengelman.shadow") version "7.0.0"
id("org.jmailen.kotlinter") version "3.4.3" id("com.github.gmazzo.buildconfig")
id("de.fuerstenau.buildconfig") version "1.1.8"
}
repositories {
maven {
url = uri("https://repo1.maven.org/maven2/")
}
maven {
url = uri("https://jitpack.io")
}
} }
dependencies { dependencies {
@@ -30,13 +16,15 @@ dependencies {
implementation("com.squareup.okio:okio:2.10.0") implementation("com.squareup.okio:okio:2.10.0")
// Javalin api // Javalin api
implementation("io.javalin:javalin:3.13.6") implementation("io.javalin:javalin:4.1.1")
// jackson version is tied to javalin, ref: `io.javalin.core.util.OptionalDependency` implementation("io.javalin:javalin-openapi:4.1.1")
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3") // jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.3") val jacksonVersion = "2.12.4"
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
// Exposed ORM // Exposed ORM
val exposedVersion = "0.31.1" val exposedVersion = "0.34.1"
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion") implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
@@ -45,29 +33,32 @@ dependencies {
implementation("com.h2database:h2:1.4.200") implementation("com.h2database:h2:1.4.200")
// Exposed Migrations // Exposed Migrations
val exposedMigrationsVersion = "3.1.0" implementation("com.github.Suwayomi:exposed-migrations:3.1.4")
implementation("com.github.Suwayomi:exposed-migrations:$exposedMigrationsVersion")
// tray icon // tray icon
implementation("com.dorkbox:SystemTray:4.1") implementation("com.dorkbox:SystemTray:4.1")
implementation("com.dorkbox:Utilities:1.9") implementation("com.dorkbox:Utilities:1.9") // version locked by SystemTray
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference // dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
implementation("com.github.inorichi.injekt:injekt-core:65b0440") implementation("com.github.inorichi.injekt:injekt-core:65b0440")
implementation("com.squareup.okhttp3:okhttp:4.9.1") implementation("com.squareup.okhttp3:okhttp:4.9.1")
implementation("io.reactivex:rxjava:1.3.8") implementation("io.reactivex:rxjava:1.3.8")
implementation("org.jsoup:jsoup:1.13.1") implementation("org.jsoup:jsoup:1.14.2")
implementation("com.google.code.gson:gson:2.8.6") implementation("com.google.code.gson:gson:2.8.8")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0") implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// Sort
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
// asm for fixing SimpleDateFormat (must match Dex2Jar version) // asm for ByteCodeEditor(fixing SimpleDateFormat) (must match Dex2Jar version)
implementation("org.ow2.asm:asm-debug-all:5.0.3") implementation("org.ow2.asm:asm:9.2")
// extracting zip files // Disk & File
implementation("net.lingala.zip4j:zip4j:2.9.0") implementation("net.lingala.zip4j:zip4j:2.9.0")
implementation("com.github.junrar:junrar:7.4.0")
// CloudflareInterceptor
implementation("net.sourceforge.htmlunit:htmlunit:2.52.0")
// Source models and interfaces from Tachiyomi 1.x // Source models and interfaces from Tachiyomi 1.x
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi // using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
@@ -79,17 +70,13 @@ dependencies {
// uncomment to test extensions directly // uncomment to test extensions directly
// implementation(fileTree("lib/")) // implementation(fileTree("lib/"))
implementation(kotlin("script-runtime"))
testImplementation("io.mockk:mockk:1.9.3")
} }
val MainClass = "suwayomi.tachidesk.MainKt"
application { application {
mainClass.set(MainClass) mainClass.set(MainClass)
// for testing electron
// applicationDefaultJvmArgs = listOf(
// "-Dsuwayomi.tachidesk.webInterface=electron",
// "-Dsuwayomi.tachidesk.electronPath=/usr/bin/electron"
// )
} }
sourceSets { sourceSets {
@@ -100,83 +87,52 @@ sourceSets {
} }
} }
// should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.4"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r20"
// counts commit count on master
val tachideskRevision = runCatching {
System.getenv("ProductRevision") ?: Runtime
.getRuntime()
.exec("git rev-list HEAD --count")
.let { process ->
process.waitFor()
val output = process.inputStream.use {
it.bufferedReader().use(BufferedReader::readText)
}
process.destroy()
"r" + output.trim()
}
}.getOrDefault("r0")
buildConfig { buildConfig {
clsName = "BuildConfig" className("BuildConfig")
packageName = "suwayomi.tachidesk.server" packageName("suwayomi.tachidesk.server")
useKotlinOutput()
buildConfigField("String", "NAME", rootProject.name) fun quoteWrap(obj: Any): String = """"$obj""""
buildConfigField("String", "VERSION", tachideskVersion)
buildConfigField("String", "REVISION", tachideskRevision) buildConfigField("String", "NAME", quoteWrap(rootProject.name))
buildConfigField("String", "BUILD_TYPE", if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview") buildConfigField("String", "VERSION", quoteWrap(tachideskVersion))
buildConfigField("String", "REVISION", quoteWrap(tachideskRevision))
buildConfigField("String", "BUILD_TYPE", quoteWrap(if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview"))
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString()) buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
buildConfigField("String", "WEBUI_REPO", "https://github.com/Suwayomi/Tachidesk-WebUI-preview") buildConfigField("String", "WEBUI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-WebUI-preview"))
buildConfigField("String", "WEBUI_TAG", webUIRevisionTag) buildConfigField("String", "WEBUI_TAG", quoteWrap(webUIRevisionTag))
buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk") buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Tachidesk"))
buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA") buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA"))
} }
tasks { tasks {
shadowJar { shadowJar {
manifest { manifest {
attributes( attributes(
mapOf( "Main-Class" to MainClass,
"Main-Class" to MainClass, "Implementation-Title" to rootProject.name,
"Implementation-Title" to rootProject.name, "Implementation-Vendor" to "The Suwayomi Project",
"Implementation-Vendor" to "The Suwayomi Project", "Specification-Version" to tachideskVersion,
"Specification-Version" to tachideskVersion, "Implementation-Version" to tachideskRevision
"Implementation-Version" to tachideskRevision
)
) )
} }
archiveBaseName.set(rootProject.name) archiveBaseName.set(rootProject.name)
archiveVersion.set(tachideskVersion) archiveVersion.set(tachideskVersion)
archiveClassifier.set(tachideskRevision) archiveClassifier.set(tachideskRevision)
} destinationDirectory.set(File("$rootDir/server/build"))
withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf(
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
)
}
} }
test { test {
useJUnit() useJUnitPlatform()
} testLogging {
showStandardStreams = true
withType<ShadowJar> { events("passed", "skipped", "failed")
destinationDirectory.set(File("$rootDir/server/build")) }
dependsOn("formatKotlin", "lintKotlin")
}
named("run") {
dependsOn("formatKotlin", "lintKotlin")
} }
named<Copy>("processResources") { named<Copy>("processResources") {
@@ -184,16 +140,41 @@ tasks {
mustRunAfter("downloadWebUI") mustRunAfter("downloadWebUI")
} }
register<de.undercouch.gradle.tasks.download.Download>("downloadWebUI") { register<Download>("downloadWebUI") {
src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip") src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip")
dest("src/main/resources/WebUI.zip") dest("src/main/resources/WebUI.zip")
fun shouldOverwrite(): Boolean {
val zipPath = project.projectDir.absolutePath + "/src/main/resources/WebUI.zip"
val zipFile = net.lingala.zip4j.ZipFile(zipPath)
var shouldOverwrite = true
if (zipFile.isValidZipFile) {
val zipRevision = zipFile.getInputStream(zipFile.getFileHeader("revision")).bufferedReader().use {
it.readText().trim()
}
if (zipRevision == webUIRevisionTag) {
shouldOverwrite = false
}
}
return shouldOverwrite
}
overwrite(shouldOverwrite())
} }
withType<LintTask> { register("runElectron") {
source(files("src/kotlin")) group = "application"
} finalizedBy(run)
doFirst {
withType<FormatTask> { application.applicationDefaultJvmArgs = listOf(
source(files("src/kotlin")) "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron",
// Change this to the installed electron application
"-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron"
)
}
} }
} }
@@ -0,0 +1,6 @@
package eu.kanade.tachiyomi;
public class BuildConfig {
public static final int VERSION_CODE = -1;
public static final String VERSION_NAME = "stub";
}
@@ -1,30 +1,13 @@
/* Copyright 2015 Javier Tomás
Copyright 2014 Prateek Srivastava
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
This file may have been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
final class Preconditions {
static void checkNotNull(Object o, String message) {
if (o == null) {
throw new NullPointerException(message);
}
}
private Preconditions() {
throw new AssertionError("No instances");
}
}
@@ -1,46 +0,0 @@
package eu.kanade.tachiyomi.animesource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import rx.Observable
interface AnimeCatalogueSource : AnimeSource {
/**
* An ISO 639-1 compliant language code (two letters in lower case).
*/
val lang: String
/**
* Whether the source has support for latest updates.
*/
val supportsLatest: Boolean
/**
* Returns an observable containing a page with a list of anime.
*
* @param page the page number to retrieve.
*/
fun fetchPopularAnime(page: Int): Observable<AnimesPage>
/**
* Returns an observable containing a page with a list of anime.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage>
/**
* Returns an observable containing a page with a list of latest anime updates.
*
* @param page the page number to retrieve.
*/
fun fetchLatestUpdates(page: Int): Observable<AnimesPage>
/**
* Returns the list of filters for the source.
*/
fun getFilterList(): AnimeFilterList
}
@@ -1,77 +0,0 @@
package eu.kanade.tachiyomi.animesource
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import rx.Observable
/**
* A basic interface for creating a source. It could be an online source, a local source, etc...
*/
interface AnimeSource {
/**
* Id for the source. Must be unique.
*/
val id: Long
/**
* Name of the source.
*/
val name: String
/**
* Returns an observable with the updated details for a anime.
*
* @param anime the anime to update.
*/
// @Deprecated("Use getAnimeDetails instead")
fun fetchAnimeDetails(anime: SAnime): Observable<SAnime>
/**
* Returns an observable with all the available episodes for an anime.
*
* @param anime the anime to update.
*/
// @Deprecated("Use getEpisodeList instead")
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>>
/**
* Returns an observable with a link for the episode of an anime.
*
* @param episode the episode to get the link for.
*/
// @Deprecated("Use getEpisodeList instead")
fun fetchEpisodeLink(episode: SEpisode): Observable<String>
// /**
// * [1.x API] Get the updated details for a anime.
// */
// @Suppress("DEPRECATION")
// override suspend fun getAnimeDetails(anime: AnimeInfo): AnimeInfo {
// val sAnime = anime.toSAnime()
// val networkAnime = fetchAnimeDetails(sAnime).awaitSingle()
// sAnime.copyFrom(networkAnime)
// return sAnime.toAnimeInfo()
// }
// /**
// * [1.x API] Get all the available episodes for a anime.
// */
// @Suppress("DEPRECATION")
// override suspend fun getEpisodeList(anime: AnimeInfo): List<EpisodeInfo> {
// return fetchEpisodeList(anime.toSAnime()).awaitSingle()
// .map { it.toEpisodeInfo() }
// }
// /**
// * [1.x API] Get a link for the episode of an anime.
// */
// @Suppress("DEPRECATION")
// override suspend fun getEpisodeLink(episode: EpisodeInfo): String {
// return fetchEpisodeLink(episode.toSEpisode()).awaitSingle()
// }
}
// fun AnimeSource.icon(): Drawable? = Injekt.get<AnimeExtensionManager>().getAppIconForSource(this)
// fun AnimeSource.getPreferenceKey(): String = "source_$id"
@@ -1,12 +0,0 @@
package eu.kanade.tachiyomi.animesource
/**
* A factory for creating sources at runtime.
*/
interface AnimeSourceFactory {
/**
* Create a new copy of the sources
* @return The created sources
*/
fun createSources(): List<AnimeSource>
}
@@ -1,76 +0,0 @@
package eu.kanade.tachiyomi.animesource
import android.content.Context
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import rx.Observable
open class AnimeSourceManager(private val context: Context) {
private val sourcesMap = mutableMapOf<Long, AnimeSource>()
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
init {
createInternalSources().forEach { registerSource(it) }
}
open fun get(sourceKey: Long): AnimeSource? {
return sourcesMap[sourceKey]
}
fun getOrStub(sourceKey: Long): AnimeSource {
return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
StubSource(sourceKey)
}
}
fun getOnlineSources() = sourcesMap.values.filterIsInstance<AnimeHttpSource>()
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<AnimeCatalogueSource>()
internal fun registerSource(source: AnimeSource) {
if (!sourcesMap.containsKey(source.id)) {
sourcesMap[source.id] = source
}
if (!stubSourcesMap.containsKey(source.id)) {
stubSourcesMap[source.id] = StubSource(source.id)
}
}
internal fun unregisterSource(source: AnimeSource) {
sourcesMap.remove(source.id)
}
private fun createInternalSources(): List<AnimeSource> = listOf(
// LocalAnimeSource(context)
)
inner class StubSource(override val id: Long) : AnimeSource {
override val name: String
get() = id.toString()
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchEpisodeLink(episode: SEpisode): Observable<String> {
return Observable.error(getSourceNotInstalledException())
}
override fun toString(): String {
return name
}
private fun getSourceNotInstalledException(): Exception {
// return Exception(context.getString(R.string.source_not_installed, id.toString()))
return Exception("source not found")
}
}
}
@@ -1,8 +0,0 @@
package eu.kanade.tachiyomi.animesource
import androidx.preference.PreferenceScreen
interface ConfigurableAnimeSource : AnimeSource {
fun setupPreferenceScreen(screen: PreferenceScreen)
}
@@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.animesource.model
sealed class AnimeFilter<T>(val name: String, var state: T) {
open class Header(name: String) : AnimeFilter<Any>(name, 0)
open class Separator(name: String = "") : AnimeFilter<Any>(name, 0)
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : AnimeFilter<Int>(name, state)
abstract class Text(name: String, state: String = "") : AnimeFilter<String>(name, state)
abstract class CheckBox(name: String, state: Boolean = false) : AnimeFilter<Boolean>(name, state)
abstract class TriState(name: String, state: Int = STATE_IGNORE) : AnimeFilter<Int>(name, state) {
fun isIgnored() = state == STATE_IGNORE
fun isIncluded() = state == STATE_INCLUDE
fun isExcluded() = state == STATE_EXCLUDE
companion object {
const val STATE_IGNORE = 0
const val STATE_INCLUDE = 1
const val STATE_EXCLUDE = 2
}
}
abstract class Group<V>(name: String, state: List<V>) : AnimeFilter<List<V>>(name, state)
abstract class Sort(name: String, val values: Array<String>, state: Selection? = null) :
AnimeFilter<Sort.Selection?>(name, state) {
data class Selection(val index: Int, val ascending: Boolean)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AnimeFilter<*>) return false
return name == other.name && state == other.state
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + (state?.hashCode() ?: 0)
return result
}
}
@@ -1,6 +0,0 @@
package eu.kanade.tachiyomi.animesource.model
data class AnimeFilterList(val list: List<AnimeFilter<*>>) : List<AnimeFilter<*>> by list {
constructor(vararg fs: AnimeFilter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
}
@@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.animesource.model
data class AnimesPage(val animes: List<SAnime>, val hasNextPage: Boolean)
@@ -1,67 +0,0 @@
package eu.kanade.tachiyomi.animesource.model
import java.io.Serializable
interface SAnime : Serializable {
var url: String
var title: String
var artist: String?
var author: String?
var description: String?
var genre: String?
var status: Int
var thumbnail_url: String?
var initialized: Boolean
fun copyFrom(other: SAnime) {
if (other.title != null) {
title = other.title
}
if (other.author != null) {
author = other.author
}
if (other.artist != null) {
artist = other.artist
}
if (other.description != null) {
description = other.description
}
if (other.genre != null) {
genre = other.genre
}
if (other.thumbnail_url != null) {
thumbnail_url = other.thumbnail_url
}
status = other.status
if (!initialized) {
initialized = other.initialized
}
}
companion object {
const val UNKNOWN = 0
const val ONGOING = 1
const val COMPLETED = 2
const val LICENSED = 3
fun create(): SAnime {
return SAnimeImpl()
}
}
}
@@ -1,22 +0,0 @@
package eu.kanade.tachiyomi.animesource.model
class SAnimeImpl : SAnime {
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 initialized: Boolean = false
}
@@ -1,30 +0,0 @@
package eu.kanade.tachiyomi.animesource.model
import java.io.Serializable
interface SEpisode : Serializable {
var url: String
var name: String
var date_upload: Long
var episode_number: Float
var scanlator: String?
fun copyFrom(other: SEpisode) {
name = other.name
url = other.url
date_upload = other.date_upload
episode_number = other.episode_number
scanlator = other.scanlator
}
companion object {
fun create(): SEpisode {
return SEpisodeImpl()
}
}
}
@@ -1,14 +0,0 @@
package eu.kanade.tachiyomi.animesource.model
class SEpisodeImpl : SEpisode {
override lateinit var url: String
override lateinit var name: String
override var date_upload: Long = 0
override var episode_number: Float = -1f
override var scanlator: String? = null
}
@@ -1,388 +0,0 @@
package eu.kanade.tachiyomi.animesource.online
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.source.model.Page
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.net.URI
import java.net.URISyntaxException
import java.security.MessageDigest
/**
* A simple implementation for sources from a website.
*/
abstract class AnimeHttpSource : AnimeCatalogueSource {
/**
* Network service.
*/
protected val network: NetworkHelper by injectLazy()
// /**
// * Preferences that a source may need.
// */
// val preferences: SharedPreferences by lazy {
// Injekt.get<Application>().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
// }
/**
* Base url of the website without the trailing slash, like: http://mysite.com
*/
abstract val baseUrl: String
/**
* Version id used to generate the source id. If the site completely changes and urls are
* incompatible, you may increase this value and it'll be considered as a new source.
*/
open val versionId = 1
/**
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
* of the MD5 of the string: sourcename/language/versionId
* Note the generated id sets the sign bit to 0.
*/
override val id by lazy {
val key = "${name.toLowerCase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
}
/**
* Headers used for requests.
*/
val headers: Headers by lazy { headersBuilder().build() }
/**
* Default network client for doing requests.
*/
open val client: OkHttpClient
get() = network.client
/**
* Headers builder for requests. Implementations can override this method for custom headers.
*/
protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", DEFAULT_USER_AGENT)
}
/**
* Visible name of the source.
*/
override fun toString() = "$name (${lang.toUpperCase()})"
/**
* Returns an observable containing a page with a list of anime. Normally it's not needed to
* override this method.
*
* @param page the page number to retrieve.
*/
override fun fetchPopularAnime(page: Int): Observable<AnimesPage> {
return client.newCall(popularAnimeRequest(page))
.asObservableSuccess()
.map { response ->
popularAnimeParse(response)
}
}
/**
* Returns the request for the popular anime given the page.
*
* @param page the page number to retrieve.
*/
protected abstract fun popularAnimeRequest(page: Int): Request
/**
* Parses the response from the site and returns a [AnimesPage] object.
*
* @param response the response from the site.
*/
protected abstract fun popularAnimeParse(response: Response): AnimesPage
/**
* Returns an observable containing a page with a list of anime. Normally it's not needed to
* override this method.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return client.newCall(searchAnimeRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
/**
* Returns the request for the search anime given the page.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
protected abstract fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request
/**
* Parses the response from the site and returns a [AnimesPage] object.
*
* @param response the response from the site.
*/
protected abstract fun searchAnimeParse(response: Response): AnimesPage
/**
* Returns an observable containing a page with a list of latest anime updates.
*
* @param page the page number to retrieve.
*/
override fun fetchLatestUpdates(page: Int): Observable<AnimesPage> {
return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
.map { response ->
latestUpdatesParse(response)
}
}
/**
* Returns the request for latest anime given the page.
*
* @param page the page number to retrieve.
*/
protected abstract fun latestUpdatesRequest(page: Int): Request
/**
* Parses the response from the site and returns a [AnimesPage] object.
*
* @param response the response from the site.
*/
protected abstract fun latestUpdatesParse(response: Response): AnimesPage
/**
* Returns an observable with the updated details for a anime. Normally it's not needed to
* override this method.
*
* @param anime the anime to be updated.
*/
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
return client.newCall(animeDetailsRequest(anime))
.asObservableSuccess()
.map { response ->
animeDetailsParse(response).apply { initialized = true }
}
}
/**
* Returns the request for the details of a anime. Override only if it's needed to change the
* url, send different headers or request method like POST.
*
* @param anime the anime to be updated.
*/
open fun animeDetailsRequest(anime: SAnime): Request {
return GET(baseUrl + anime.url, headers)
}
/**
* Parses the response from the site and returns the details of a anime.
*
* @param response the response from the site.
*/
protected abstract fun animeDetailsParse(response: Response): SAnime
/**
* Returns an observable with the updated episode list for a anime. Normally it's not needed to
* override this method. If a anime is licensed an empty episode list observable is returned
*
* @param anime the anime to look for episodes.
*/
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
return if (anime.status != SAnime.LICENSED) {
client.newCall(episodeListRequest(anime))
.asObservableSuccess()
.map { response ->
episodeListParse(response)
}
} else {
Observable.error(Exception("Licensed - No episodes to show"))
}
}
override fun fetchEpisodeLink(episode: SEpisode): Observable<String> {
return client.newCall(episodeLinkRequest(episode))
.asObservableSuccess()
.map { response ->
episodeLinkParse(response)
}
}
/**
* Returns the request for updating the episode list. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param anime the anime to look for episodes.
*/
protected open fun episodeListRequest(anime: SAnime): Request {
return GET(baseUrl + anime.url, headers)
}
/**
* Returns the request for getting the episode link. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param episode the episode to look for links.
*/
protected open fun episodeLinkRequest(episode: SEpisode): Request {
return GET(baseUrl + episode.url, headers)
}
/**
* Parses the response from the site and returns a list of episodes.
*
* @param response the response from the site.
*/
protected abstract fun episodeListParse(response: Response): List<SEpisode>
/**
* Parses the response from the site and returns a list of episodes.
*
* @param response the response from the site.
*/
protected abstract fun episodeLinkParse(response: Response): String
/**
* Returns the request for getting the page list. Override only if it's needed to override the
* url, send different headers or request method like POST.
*
* @param episode the episode whose page list has to be fetched.
*/
protected open fun pageListRequest(episode: SEpisode): Request {
return GET(baseUrl + episode.url, headers)
}
/**
* Parses the response from the site and returns a list of pages.
*
* @param response the response from the site.
*/
protected abstract fun pageListParse(response: Response): List<Page>
/**
* Returns an observable with the page containing the source url of the image. If there's any
* error, it will return null instead of throwing an exception.
*
* @param page the page whose source image has to be fetched.
*/
open fun fetchImageUrl(page: Page): Observable<String> {
return client.newCall(imageUrlRequest(page))
.asObservableSuccess()
.map { imageUrlParse(it) }
}
/**
* Returns the request for getting the url to the source image. Override only if it's needed to
* override the url, send different headers or request method like POST.
*
* @param page the episode whose page list has to be fetched
*/
protected open fun imageUrlRequest(page: Page): Request {
return GET(page.url, headers)
}
/**
* Parses the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
protected abstract fun imageUrlParse(response: Response): String
/**
* Returns an observable with the response of the source image.
*
* @param page the page whose source image has to be downloaded.
*/
fun fetchImage(page: Page): Observable<Response> {
return client.newCallWithProgress(imageRequest(page), page)
.asObservableSuccess()
}
/**
* Returns the request for getting the source image. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param page the episode whose page list has to be fetched
*/
protected open fun imageRequest(page: Page): Request {
return GET(page.imageUrl!!, headers)
}
/**
* Assigns the url of the episode without the scheme and domain. It saves some redundancy from
* database and the urls could still work after a domain change.
*
* @param url the full url to the episode.
*/
fun SEpisode.setUrlWithoutDomain(url: String) {
this.url = getUrlWithoutDomain(url)
}
/**
* Assigns the url of the anime without the scheme and domain. It saves some redundancy from
* database and the urls could still work after a domain change.
*
* @param url the full url to the anime.
*/
fun SAnime.setUrlWithoutDomain(url: String) {
this.url = getUrlWithoutDomain(url)
}
/**
* Returns the url of the given string without the scheme and domain.
*
* @param orig the full url.
*/
private fun getUrlWithoutDomain(orig: String): String {
return try {
val uri = URI(orig)
var out = uri.path
if (uri.query != null) {
out += "?" + uri.query
}
if (uri.fragment != null) {
out += "#" + uri.fragment
}
out
} catch (e: URISyntaxException) {
orig
}
}
/**
* Called before inserting a new episode into database. Use it if you need to override episode
* fields, like the title or the episode number. Do not change anything to [anime].
*
* @param episode the episode to be added.
* @param anime the anime of the episode.
*/
open fun prepareNewEpisode(episode: SEpisode, anime: SAnime) {
}
/**
* Returns the list of filters for the source.
*/
override fun getFilterList() = AnimeFilterList()
companion object {
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
}
}
@@ -1,26 +0,0 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.source.model.Page
import rx.Observable
fun AnimeHttpSource.getImageUrl(page: Page): Observable<Page> {
page.status = Page.LOAD_PAGE
return fetchImageUrl(page)
.doOnError { page.status = Page.ERROR }
.onErrorReturn { null }
.doOnNext { page.imageUrl = it }
.map { page }
}
fun AnimeHttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages)
.filter { !it.imageUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
}
fun AnimeHttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages)
.filter { it.imageUrl.isNullOrEmpty() }
.concatMap { getImageUrl(it) }
}
@@ -1,222 +0,0 @@
package eu.kanade.tachiyomi.animesource.online
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
/**
* A simple implementation for sources from a website using Jsoup, an HTML parser.
*/
abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
/**
* Parses the response from the site and returns a [AnimesPage] object.
*
* @param response the response from the site.
*/
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = document.select(popularAnimeSelector()).map { element ->
popularAnimeFromElement(element)
}
val hasNextPage = popularAnimeNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return AnimesPage(animes, hasNextPage)
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each anime.
*/
protected abstract fun popularAnimeSelector(): String
/**
* Returns a anime from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [popularAnimeSelector].
*/
protected abstract fun popularAnimeFromElement(element: Element): SAnime
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
protected abstract fun popularAnimeNextPageSelector(): String?
/**
* Parses the response from the site and returns a [AnimesPage] object.
*
* @param response the response from the site.
*/
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = document.select(searchAnimeSelector()).map { element ->
searchAnimeFromElement(element)
}
val hasNextPage = searchAnimeNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return AnimesPage(animes, hasNextPage)
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each anime.
*/
protected abstract fun searchAnimeSelector(): String
/**
* Returns a anime from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [searchAnimeSelector].
*/
protected abstract fun searchAnimeFromElement(element: Element): SAnime
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
protected abstract fun searchAnimeNextPageSelector(): String?
/**
* Parses the response from the site and returns a [AnimesPage] object.
*
* @param response the response from the site.
*/
override fun latestUpdatesParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = document.select(latestUpdatesSelector()).map { element ->
latestUpdatesFromElement(element)
}
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return AnimesPage(animes, hasNextPage)
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each anime.
*/
protected abstract fun latestUpdatesSelector(): String
/**
* Returns a anime from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [latestUpdatesSelector].
*/
protected abstract fun latestUpdatesFromElement(element: Element): SAnime
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
protected abstract fun latestUpdatesNextPageSelector(): String?
/**
* Parses the response from the site and returns the details of a anime.
*
* @param response the response from the site.
*/
override fun animeDetailsParse(response: Response): SAnime {
return animeDetailsParse(response.asJsoup())
}
/**
* Returns the details of the anime from the given [document].
*
* @param document the parsed document.
*/
protected abstract fun animeDetailsParse(document: Document): SAnime
/**
* Parses the response from the site and returns a list of episodes.
*
* @param response the response from the site.
*/
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
return document.select(episodeListSelector()).map { episodeFromElement(it) }
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each episode.
*/
protected abstract fun episodeListSelector(): String
/**
* Parses the response from the site and returns a list of episodes.
*
* @param response the response from the site.
*/
override fun episodeLinkParse(response: Response): String {
val document = response.asJsoup()
return linkFromElement(document.select(episodeLinkSelector()).first())
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each episode.
*/
protected abstract fun episodeLinkSelector(): String
/**
* Returns a episode from the given element.
*
* @param element an element obtained from [episodeListSelector].
*/
protected abstract fun episodeFromElement(element: Element): SEpisode
/**
* Returns a episode from the given element.
*
* @param element an element obtained from [episodeListSelector].
*/
protected abstract fun linkFromElement(element: Element): String
/**
* Parses the response from the site and returns the page list.
*
* @param response the response from the site.
*/
override fun pageListParse(response: Response): List<Page> {
return pageListParse(response.asJsoup())
}
/**
* Returns a page list from the given document.
*
* @param document the parsed document.
*/
protected abstract fun pageListParse(document: Document): List<Page>
/**
* Parse the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
override fun imageUrlParse(response: Response): String {
return imageUrlParse(response.asJsoup())
}
/**
* Returns the absolute url to the source image from the document.
*
* @param document the parsed document.
*/
protected abstract fun imageUrlParse(document: Document): String
}
@@ -1,5 +0,0 @@
package eu.kanade.tachiyomi.annoations
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Nsfw
@@ -1,177 +0,0 @@
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
// import android.os.Handler
// import android.os.Looper
// import android.webkit.WebSettings
// import android.webkit.WebView
// import android.widget.Toast
// import eu.kanade.tachiyomi.R
// import eu.kanade.tachiyomi.util.lang.launchUI
// import eu.kanade.tachiyomi.util.system.WebViewClientCompat
// import eu.kanade.tachiyomi.util.system.WebViewUtil
// import eu.kanade.tachiyomi.util.system.isOutdated
// import eu.kanade.tachiyomi.util.system.setDefaultSettings
// import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Interceptor
import okhttp3.Response
// import uy.kohesive.injekt.injectLazy
class CloudflareInterceptor() : Interceptor {
// private val handler = Handler(Looper.getMainLooper())
// private val networkHelper = NetworkHelper()
/**
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
* blocking the main thread too much. If used too often we could consider moving it to the
* Application class.
*/
// private val initWebView by lazy {
// WebSettings.getDefaultUserAgent(context)
// }
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
return chain.proceed(originalRequest)
// if (!WebViewUtil.supportsWebView(context)) {
// launchUI {
// context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
// }
// return chain.proceed(originalRequest)
// }
//
// initWebView
//
// val response = chain.proceed(originalRequest)
//
// // Check if Cloudflare anti-bot is on
// if (response.code != 503 || response.header("Server") !in SERVER_CHECK) {
// return response
// }
//
// try {
// response.close()
// networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0)
// val oldCookie = networkHelper.cookieManager.get(originalRequest.url)
// .firstOrNull { it.name == "cf_clearance" }
// resolveWithWebView(originalRequest, oldCookie)
//
// return chain.proceed(originalRequest)
// } catch (e: Exception) {
// // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// // we don't crash the entire app
// throw IOException(e)
// }
}
//
// // @SuppressLint("SetJavaScriptEnabled")
// private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
// // We need to lock this thread until the WebView finds the challenge solution url, because
// // OkHttp doesn't support asynchronous interceptors.
// val latch = CountDownLatch(1)
//
// var webView: WebView? = null
//
// var challengeFound = false
// var cloudflareBypassed = false
// var isWebViewOutdated = false
//
// val origRequestUrl = request.url.toString()
// val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
// headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
//
// handler.post {
// val webview = WebView(context)
// webView = webview
// webview.setDefaultSettings()
//
// // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
// webview.settings.userAgentString = request.header("User-Agent")
// ?: HttpSource.DEFAULT_USERAGENT
//
// webview.webViewClient = object : WebViewClientCompat() {
// override fun onPageFinished(view: WebView, url: String) {
// fun isCloudFlareBypassed(): Boolean {
// return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl())
// .firstOrNull { it.name == "cf_clearance" }
// .let { it != null && it != oldCookie }
// }
//
// if (isCloudFlareBypassed()) {
// cloudflareBypassed = true
// latch.countDown()
// }
//
// // HTTP error codes are only received since M
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
// url == origRequestUrl && !challengeFound
// ) {
// // The first request didn't return the challenge, abort.
// latch.countDown()
// }
// }
//
// override fun onReceivedErrorCompat(
// view: WebView,
// errorCode: Int,
// description: String?,
// failingUrl: String,
// isMainFrame: Boolean
// ) {
// if (isMainFrame) {
// if (errorCode == 503) {
// // Found the Cloudflare challenge page.
// challengeFound = true
// } else {
// // Unlock thread, the challenge wasn't found.
// latch.countDown()
// }
// }
// }
// }
//
// webView?.loadUrl(origRequestUrl, headers)
// }
//
// // Wait a reasonable amount of time to retrieve the solution. The minimum should be
// // around 4 seconds but it can take more due to slow networks or server issues.
// latch.await(12, TimeUnit.SECONDS)
//
// handler.post {
// if (!cloudflareBypassed) {
// isWebViewOutdated = webView?.isOutdated() == true
// }
//
// webView?.stopLoading()
// webView?.destroy()
// }
//
// // Throw exception if we failed to bypass Cloudflare
// if (!cloudflareBypassed) {
// // Prompt user to update WebView if it seems too outdated
// if (isWebViewOutdated) {
// context.toast(R.string.information_webview_outdated, Toast.LENGTH_LONG)
// }
//
// throw Exception(context.getString(R.string.information_cloudflare_bypass_failure))
// }
// }
//
// companion object {
// private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
// private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance")
// }
}
@@ -10,69 +10,63 @@ package eu.kanade.tachiyomi.network
// import android.content.Context // import android.content.Context
// import eu.kanade.tachiyomi.BuildConfig // import eu.kanade.tachiyomi.BuildConfig
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper // import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import android.content.Context
// import okhttp3.HttpUrl.Companion.toHttpUrl // import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
// import okhttp3.dnsoverhttps.DnsOverHttps // import okhttp3.dnsoverhttps.DnsOverHttps
// import okhttp3.logging.HttpLoggingInterceptor // import okhttp3.logging.HttpLoggingInterceptor
// import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
import android.content.Context
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import suwayomi.tachidesk.server.serverConfig
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@Suppress("UNUSED_PARAMETER")
class NetworkHelper(context: Context) { class NetworkHelper(context: Context) {
// private val preferences: PreferencesHelper by injectLazy() // private val preferences: PreferencesHelper by injectLazy()
// private val cacheDir = File(context.cacheDir, "network_cache") // private val cacheDir = File(context.cacheDir, "network_cache")
private val cacheSize = 5L * 1024 * 1024 // 5 MiB // private val cacheSize = 5L * 1024 * 1024 // 5 MiB
val cookieManager = MemoryCookieJar() val cookieManager = PersistentCookieJar(context)
val client by lazy { private val baseClientBuilder: OkHttpClient.Builder
val builder = OkHttpClient.Builder() get() {
.cookieJar(cookieManager) val builder = OkHttpClient.Builder()
// .cache(Cache(cacheDir, cacheSize)) .cookieJar(cookieManager)
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.MINUTES) .readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.MINUTES) .addInterceptor(UserAgentInterceptor())
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
// .addInterceptor(UserAgentInterceptor()) if (serverConfig.debugLogsEnabled) {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
}
builder.addInterceptor(httpLoggingInterceptor)
}
// if (BuildConfig.DEBUG) { // when (preferences.dohProvider()) {
// val httpLoggingInterceptor = HttpLoggingInterceptor().apply { // PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
// level = HttpLoggingInterceptor.Level.HEADERS // PREF_DOH_GOOGLE -> builder.dohGoogle()
// } // }
// builder.addInterceptor(httpLoggingInterceptor)
// }
// if (preferences.enableDoh()) { return builder
// builder.dns( }
// DnsOverHttps.Builder().client(builder.build())
// .url("https://cloudflare-dns.com/dns-query".toHttpUrl())
// .bootstrapDnsHosts(
// listOf(
// InetAddress.getByName("162.159.36.1"),
// InetAddress.getByName("162.159.46.1"),
// InetAddress.getByName("1.1.1.1"),
// InetAddress.getByName("1.0.0.1"),
// InetAddress.getByName("162.159.132.53"),
// InetAddress.getByName("2606:4700:4700::1111"),
// InetAddress.getByName("2606:4700:4700::1001"),
// InetAddress.getByName("2606:4700:4700::0064"),
// InetAddress.getByName("2606:4700:4700::6400")
// )
// )
// .build()
// )
// }
builder.build() // val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
} val client by lazy { baseClientBuilder.build() }
val cloudflareClient by lazy { val cloudflareClient by lazy {
client.newBuilder() client.newBuilder()
.addInterceptor(CloudflareInterceptor()) .addInterceptor(CloudflareInterceptor())
.build() .build()
} }
// Tachidesk -->
val cookies: PersistentCookieStore
get() = cookieManager.store
// Tachidesk <--
} }
@@ -1,14 +1,19 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
// import kotlinx.coroutines.suspendCancellableCoroutine // import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly
import rx.Observable import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resumeWithException
fun Call.asObservable(): Observable<Response> { fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber -> return Observable.unsafeCreate { subscriber ->
@@ -48,36 +53,38 @@ fun Call.asObservable(): Observable<Response> {
} }
// Based on https://github.com/gildor/kotlin-coroutines-okhttp // Based on https://github.com/gildor/kotlin-coroutines-okhttp
// suspend fun Call.await(assertSuccess: Boolean = false): Response { suspend fun Call.await(): Response {
// return suspendCancellableCoroutine { continuation -> return suspendCancellableCoroutine { continuation ->
// enqueue( enqueue(
// object : Callback { object : Callback {
// override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
// if (assertSuccess && !response.isSuccessful) { if (!response.isSuccessful) {
// continuation.resumeWithException(Exception("HTTP error ${response.code}")) continuation.resumeWithException(Exception("HTTP error ${response.code}"))
// return return
// } }
//
// continuation.resume(response) continuation.resume(response) {
// } response.body?.closeQuietly()
// }
// override fun onFailure(call: Call, e: IOException) { }
// // Don't bother with resuming the continuation if it is already cancelled.
// if (continuation.isCancelled) return override fun onFailure(call: Call, e: IOException) {
// continuation.resumeWithException(e) // Don't bother with resuming the continuation if it is already cancelled.
// } if (continuation.isCancelled) return
// } continuation.resumeWithException(e)
// ) }
// }
// continuation.invokeOnCancellation { )
// try {
// cancel() continuation.invokeOnCancellation {
// } catch (ex: Throwable) { try {
// // Ignore cancel exception cancel()
// } } catch (ex: Throwable) {
// } // Ignore cancel exception
// } }
// } }
}
}
fun Call.asObservableSuccess(): Observable<Response> { fun Call.asObservableSuccess(): Observable<Response> {
return asObservable() return asObservable()
@@ -103,6 +110,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
// return progressClient.newCall(request) // return progressClient.newCall(request)
// } // }
@Suppress("UNUSED_PARAMETER")
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder() val progressClient = newBuilder()
// .cache(null) // .cache(null)
@@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.network
import android.content.Context
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
// from TachiWeb-Server
class PersistentCookieJar(context: Context) : CookieJar {
val store = PersistentCookieStore(context)
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
store.addAll(url, cookies)
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
return store.get(url)
}
}
@@ -0,0 +1,79 @@
package eu.kanade.tachiyomi.network
import android.content.Context
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.net.URI
import java.util.concurrent.ConcurrentHashMap
// from TachiWeb-Server
class PersistentCookieStore(context: Context) {
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
init {
for ((key, value) in prefs.all) {
@Suppress("UNCHECKED_CAST")
val cookies = value as? Set<String>
if (cookies != null) {
try {
val url = "http://$key".toHttpUrlOrNull() ?: continue
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
.filter { !it.hasExpired() }
cookieMap.put(key, nonExpiredCookies)
} catch (e: Exception) {
// Ignore
}
}
}
}
@Synchronized
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
val key = url.toUri().host
// Append or replace the cookies for this domain.
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
for (cookie in cookies) {
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
val pos = cookiesForDomain.indexOfFirst { it.name == cookie.name }
if (pos == -1) {
cookiesForDomain.add(cookie)
} else {
cookiesForDomain[pos] = cookie
}
}
cookieMap.put(key, cookiesForDomain)
// Get cookies to be stored in disk
val newValues = cookiesForDomain.asSequence()
.filter { it.persistent && !it.hasExpired() }
.map(Cookie::toString)
.toSet()
prefs.edit().putStringSet(key, newValues).apply()
}
@Synchronized
fun removeAll() {
prefs.edit().clear().apply()
cookieMap.clear()
}
fun remove(uri: URI) {
prefs.edit().remove(uri.host).apply()
cookieMap.remove(uri.host)
}
fun get(url: HttpUrl) = get(url.toUri().host)
fun get(uri: URI) = get(uri.host)
private fun get(url: String): List<Cookie> {
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
}
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt
}
@@ -0,0 +1,110 @@
package eu.kanade.tachiyomi.network.interceptor
import com.gargoylesoftware.htmlunit.BrowserVersion
import com.gargoylesoftware.htmlunit.WebClient
import com.gargoylesoftware.htmlunit.html.HtmlPage
import eu.kanade.tachiyomi.network.NetworkHelper
import mu.KotlinLogging
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.io.IOException
// from TachiWeb-Server
class CloudflareInterceptor : Interceptor {
private val logger = KotlinLogging.logger {}
private val network: NetworkHelper by injectLazy()
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
logger.trace { "CloudflareInterceptor is being used." }
val response = chain.proceed(originalRequest)
// Check if Cloudflare anti-bot is on
if (response.code != 503 || response.header("Server") !in SERVER_CHECK) {
return response
}
logger.debug { "Cloudflare anti-bot is on, CloudflareInterceptor is kicking in..." }
return try {
response.close()
network.cookies.remove(originalRequest.url.toUri())
chain.proceed(resolveChallenge(response))
} catch (e: Exception) {
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// we don't crash the entire app
throw IOException(e)
}
}
private fun resolveChallenge(response: Response): Request {
val browserVersion = BrowserVersion.BrowserVersionBuilder(BrowserVersion.BEST_SUPPORTED)
.setUserAgent(response.request.header("User-Agent") ?: BrowserVersion.BEST_SUPPORTED.userAgent)
.build()
val convertedCookies = WebClient(browserVersion).use { webClient ->
webClient.options.isThrowExceptionOnFailingStatusCode = false
webClient.options.isThrowExceptionOnScriptError = false
webClient.getPage<HtmlPage>(response.request.url.toString())
webClient.waitForBackgroundJavaScript(10000)
// Challenge solved, process cookies
webClient.cookieManager.cookies.filter {
// Only include Cloudflare cookies
it.name.startsWith("__cf") || it.name.startsWith("cf_")
}.map {
// Convert cookies -> OkHttp format
Cookie.Builder()
.domain(it.domain.removePrefix("."))
.expiresAt(it.expires?.time ?: Long.MAX_VALUE)
.name(it.name)
.path(it.path)
.value(it.value).apply {
if (it.isHttpOnly) httpOnly()
if (it.isSecure) secure()
}.build()
}
}
// Copy cookies to cookie store
convertedCookies.forEach {
network.cookies.addAll(
HttpUrl.Builder()
.scheme("http")
.host(it.domain)
.build(),
listOf(it)
)
}
// Merge new and existing cookies for this request
// Find the cookies that we need to merge into this request
val convertedForThisRequest = convertedCookies.filter {
it.matches(response.request.url)
}
// Extract cookies from current request
val existingCookies = Cookie.parseAll(
response.request.url,
response.request.headers
)
// Filter out existing values of cookies that we are about to merge in
val filteredExisting = existingCookies.filter { existing ->
convertedForThisRequest.none { converted -> converted.name == existing.name }
}
val newCookies = filteredExisting + convertedForThisRequest
return response.request.newBuilder()
.header("Cookie", newCookies.map { it.toString() }.joinToString("; "))
.build()
}
companion object {
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("cf_clearance")
}
}
@@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.network.interceptor
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.Interceptor
import okhttp3.Response
class UserAgentInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
val newRequest = originalRequest
.newBuilder()
.removeHeader("User-Agent")
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
.build()
chain.proceed(newRequest)
} else {
chain.proceed(originalRequest)
}
}
}
@@ -1,358 +0,0 @@
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()
// }
}
@@ -1,77 +0,0 @@
package eu.kanade.tachiyomi.source
import android.content.Context
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.source.online.HttpSource
import rx.Observable
open class SourceManager(private val context: Context) {
private val sourcesMap = mutableMapOf<Long, Source>()
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
init {
createInternalSources().forEach { registerSource(it) }
}
open fun get(sourceKey: Long): Source? {
return sourcesMap[sourceKey]
}
fun getOrStub(sourceKey: Long): Source {
return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
StubSource(sourceKey)
}
}
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
internal fun registerSource(source: Source, overwrite: Boolean = false) {
if (overwrite || !sourcesMap.containsKey(source.id)) {
sourcesMap[source.id] = source
}
if (overwrite || !stubSourcesMap.containsKey(source.id)) {
stubSourcesMap[source.id] = StubSource(source.id)
}
}
internal fun unregisterSource(source: Source) {
sourcesMap.remove(source.id)
}
private fun createInternalSources(): List<Source> = listOf(
// LocalSource(context)
)
private inner class StubSource(override val id: Long) : Source {
override val name: String
get() = id.toString()
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return Observable.error(getSourceNotInstalledException())
}
override fun toString(): String {
return name
}
private fun getSourceNotInstalledException(): Exception {
// return Exception(context.getString(R.string.source_not_installed, id.toString()))
return Exception("source not found")
}
}
}

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