Compare commits

..

181 Commits

Author SHA1 Message Date
Aria Moradi 92ed48f7f6 bump version to v0.2.4
Publish / Validate Gradle Wrapper (push) Successful in 12s
Publish / Build FatJar (push) Failing after 15s
2021-03-13 11:08:39 +03:30
Aria Moradi 13e84bc492 Maskable icons 2021-03-13 11:06:22 +03:30
Aria Moradi 0ef86c34b7 server configuration fam 2021-03-11 14:43:29 +03:30
Aria Moradi 7e1a4259d7 fix langs not showing correctly 2021-03-09 18:05:34 +03:30
Aria Moradi c842c51fb6 section sources by lang 2021-03-09 16:44:09 +03:30
Aria Moradi 6f2f228e08 section extension languages 2021-03-08 21:04:42 +03:30
Aria Moradi c78eaa8b96 add issue closer 2021-03-08 13:47:58 +03:30
Aria Moradi f9606526d2 add issue closer 2021-03-08 13:39:25 +03:30
Aria Moradi fe4cc9ea2c add issue closer 2021-03-08 13:32:17 +03:30
Aria Moradi 54d0c05fcc add issue closer 2021-03-08 13:31:03 +03:30
Aria Moradi 2f7df73a37 add issue closer 2021-03-08 13:22:44 +03:30
Aria Moradi cf19f3626b improve text 2021-03-08 13:01:23 +03:30
Aria Moradi ff2da5e59b issue template 2021-03-08 12:57:12 +03:30
Aria Moradi e03922e518 fix PWA icons 2021-03-07 23:08:30 +03:30
Aria Moradi 893fba5b8c fix image urls 2021-03-07 22:35:27 +03:30
Aria Moradi c1786f8e24 migrate to axios, front-end part of configurable ServerAddress 2021-03-07 22:25:29 +03:30
Aria Moradi a59f974537 fix #25 2021-03-07 22:12:38 +03:30
Aria Moradi 7157e07328 better messages, axios client 2021-03-07 16:27:13 +03:30
Aria Moradi 954084bd82 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-03-07 10:51:24 +03:30
Aria Moradi 0915ba40f6 🤌 Tachidesk's logo! 2021-02-25 21:54:49 +03:30
Aria Moradi de30d55bcf darkTheme in localStorage 2021-02-25 14:38:16 +03:30
Aria Moradi af1c34fba5 v0.2.3
Publish / Validate Gradle Wrapper (push) Successful in 12s
Publish / Build FatJar (push) Failing after 16s
2021-02-24 12:27:28 +03:30
Aria Moradi 7b7d93786f Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-24 12:09:39 +03:30
Aria Moradi 7c1c504482 new icon, fix headless systems crashing 2021-02-24 11:55:43 +03:30
Aria Moradi 33b22fcab6 Update README.md 2021-02-22 14:54:04 +03:30
Aria Moradi ab0566dcba Update README.md 2021-02-22 14:51:39 +03:30
Aria Moradi c4f2cc7189 Update README.md 2021-02-22 14:49:17 +03:30
Aria Moradi 4626d99590 Update README.md 2021-02-22 14:48:17 +03:30
Aria Moradi 6465ca8a19 Update README.md 2021-02-22 01:29:55 +03:30
Aria Moradi 15b9d151df Update README.md 2021-02-22 01:28:13 +03:30
Aria Moradi dd1b6c86cd Update README.md 2021-02-22 01:23:44 +03:30
Aria Moradi 9613cda79a new icons by @as280093 2021-02-21 23:37:11 +03:30
Aria Moradi 648b8e5960 bump version: v0.2.2
Publish / Validate Gradle Wrapper (push) Successful in 15s
Publish / Build FatJar (push) Failing after 16s
2021-02-21 04:42:33 +03:30
Aria Moradi ce545b1fd5 fix some bugs 2021-02-21 04:41:56 +03:30
Aria Moradi 9151034fbc category done! 2021-02-21 04:27:41 +03:30
Aria Moradi 312a8baa13 hide menu button for now 2021-02-20 02:59:32 +03:30
Aria Moradi 18b6168cd1 theme select in settings 2021-02-20 02:57:52 +03:30
Aria Moradi 9a282c3bf4 redirect / to library 2021-02-20 02:41:30 +03:30
Aria Moradi 2bbebe4c30 fix removing manga from library not working 2021-02-20 02:34:26 +03:30
Aria Moradi 162961b560 fix tabs 2021-02-20 02:28:55 +03:30
Aria Moradi f1cc37d0db finished the category screen 2021-02-20 01:23:52 +03:30
Aria Moradi 5a9d216fb7 bump version
Publish / Validate Gradle Wrapper (push) Successful in 10s
Publish / Build FatJar (push) Failing after 2m8s
2021-02-14 23:18:14 +03:30
Aria Moradi bf37d3be7c fix syntax 2021-02-14 22:51:22 +03:30
Aria Moradi 7fd57aaed8 try new release action 2021-02-14 22:49:40 +03:30
Aria Moradi d996c44b24 try publish wiht draft 2021-02-14 22:20:50 +03:30
Aria Moradi 6f3052dd1b category backend 2021-02-14 01:10:43 +03:30
Aria Moradi d2b1bfdcdd Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-13 22:45:18 +03:30
Aria Moradi 945fb99594 Update README.md 2021-02-13 21:25:49 +03:30
Aria Moradi 09d624a4e2 add library 2021-02-13 21:12:18 +03:30
Aria Moradi eb90db7ce6 Update README.md 2021-02-13 17:18:31 +03:30
Aria Moradi b56f9391b8 Update README.md 2021-02-13 17:07:39 +03:30
Aria Moradi c181478909 Update README.md 2021-02-13 17:06:37 +03:30
Aria Moradi 76b31e734c Update README.md 2021-02-13 17:06:16 +03:30
Aria Moradi ed8bd76d95 dummy file to trigger actions 2021-02-13 15:34:17 +03:30
Aria Moradi 3051a72d7f add node_modules cache 2021-02-13 15:30:15 +03:30
Aria Moradi 3a33bf3a5d just download android.jar to improve build time 2021-02-13 15:18:57 +03:30
Aria Moradi 7959ba2664 [RELEASE CI] test new release 2021-02-13 14:50:46 +03:30
Aria Moradi fe6568b82c [RELEASE CI] test new release 2021-02-13 14:39:16 +03:30
Aria Moradi c228648bb6 [RELEASE CI] test new release 2021-02-13 14:15:38 +03:30
Aria Moradi fdaeb6d1fa [RELEASE CI] test new release 2021-02-13 14:01:01 +03:30
Aria Moradi ba45e18399 [RELEASE CI] test new release 2021-02-13 13:39:52 +03:30
Aria Moradi 3e2bf877d4 [RELEASE CI] test new release 2021-02-13 13:32:59 +03:30
Aria Moradi c80d344046 [RELEASE CI] test new release 2021-02-13 13:21:13 +03:30
Aria Moradi 2364f10d8d [RELEASE CI] test new release 2021-02-13 13:13:15 +03:30
Aria Moradi 2602275c20 [RELEASE CI] test new release 2021-02-13 13:12:40 +03:30
Aria Moradi d113311f4e [RELEASE CI] test new release 2021-02-13 12:57:01 +03:30
Aria Moradi 8d95701e8e [RELEASE CI] test new release 2021-02-13 12:55:57 +03:30
Aria Moradi 0d2c54a5ed [RELEASE CI] test new release 2021-02-13 12:54:36 +03:30
Aria Moradi 6506c84b85 publish? 2021-02-08 05:36:19 +03:30
Aria Moradi 69bb38b487 [CI RELEASE] do it 2021-02-08 05:12:13 +03:30
Aria Moradi 95e17f2b50 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-08 05:11:48 +03:30
Aria Moradi 9625da9221 [RLEASE CI] add upload release binaries action 2021-02-08 05:11:21 +03:30
Aria Moradi c1659f1cf2 refactor, add todos for library and category 2021-02-06 18:48:59 +03:30
Aria Moradi c46ee764ac Update README.md 2021-02-05 11:47:17 +03:30
Aria Moradi 7aada85f76 Update README.md 2021-02-05 11:46:29 +03:30
Aria Moradi 145cbe3e4f Update README.md 2021-02-05 11:45:56 +03:30
Aria Moradi cb8dd8259d Update README.md 2021-02-05 11:44:24 +03:30
Aria Moradi b8e721fd27 Update README.md 2021-02-05 01:48:59 +03:30
Aria Moradi 7917b5384c Update README.md 2021-02-05 01:17:03 +03:30
Aria Moradi 087b7554bf cleanup 2021-02-05 01:09:11 +03:30
Aria Moradi fb5f851a2a Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-05 00:57:16 +03:30
Aria Moradi 7ac51f8c2a Update README.md 2021-02-05 00:50:56 +03:30
Aria Moradi e5e40a986c Update README.md 2021-02-05 00:46:46 +03:30
Aria Moradi 7a27436868 now done with lfs track 2021-02-05 00:20:25 +03:30
Aria Moradi a5bab7425d [CI RELEASE] try lfs fix 2021-02-05 00:11:20 +03:30
Aria Moradi 93d5ab3739 [CI RELEASE] v.0.2.0 2021-02-04 23:55:01 +03:30
Aria Moradi 3146fefb55 change build scripts 2021-02-04 23:47:16 +03:30
Aria Moradi 1ea51bb9df add launch4j 2021-02-04 23:40:40 +03:30
Aria Moradi 98bd664ab6 Tray Icon 2021-02-04 18:02:46 +03:30
Aria Moradi 61aee2e784 hint added 2021-02-04 18:02:34 +03:30
Aria Moradi 22bf49078f cached response for source list iconUrl 2021-02-04 14:53:34 +03:30
Aria Moradi 7284e0d4ae cached extension icon 2021-02-04 14:47:27 +03:30
Aria Moradi d39d075b1a [CI RELEASE] dummy file to trigger CI 2021-02-04 04:55:36 +03:30
Aria Moradi 0f6749b0c1 now support backward writing! 2021-02-04 04:48:15 +03:30
Aria Moradi 771030b911 [CI RELEASE] v.0.1.5 2021-02-04 04:28:00 +03:30
Aria Moradi 8d5744a2cf fix chapter naming, db naming 2021-02-04 04:27:25 +03:30
Aria Moradi a58aab9004 [CI RELEASE] v.0.1.4 2021-02-04 04:01:20 +03:30
Aria Moradi 61bd32f7f0 don't cancel me shit 2021-02-04 03:52:49 +03:30
Aria Moradi 63a444bd81 calling HttpSource.imageRequest now 2021-02-04 03:42:30 +03:30
Aria Moradi 8f28c3b74b eh missing shit from last commit 2021-02-04 00:38:23 +03:30
Aria Moradi d766206343 fix sqlite locking fuckery by replacing it with h2 2021-02-04 00:32:01 +03:30
Aria Moradi 172f83f5b3 Manga dir 2021-02-03 23:35:34 +03:30
Aria Moradi 9e308025c3 some initial code for MangaDex login 2021-02-03 22:07:39 +03:30
Aria Moradi aaa6a16778 Update README.md 2021-01-30 01:07:42 +03:30
Aria Moradi 2a21da2210 [SKIP CI] update README.md 2021-01-29 19:11:38 +03:30
Aria Moradi d1cd2cfc8c [RELEASE CI] bump version 2021-01-29 15:26:08 +03:30
Aria Moradi 832c224ed4 uninstalling extensions implemented 2021-01-29 15:23:29 +03:30
Aria Moradi 99316f4bd5 revert react changes 2021-01-29 14:25:18 +03:30
Aria Moradi 9caae5f1e5 thumbnail caching 2021-01-29 14:19:24 +03:30
Aria Moradi 345be95ce9 [RELEASE CI] bump version 2021-01-28 15:13:56 +03:30
Aria Moradi 6fe68841b7 [SKIP CI] add docker thanks to @arbuilder 2021-01-28 15:08:37 +03:30
Aria Moradi eaff2c15a9 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-01-26 23:33:14 +03:30
Aria Moradi 5eb8dc66a8 add license notice to everything 2021-01-26 23:32:12 +03:30
Aria Moradi 49715c81e4 Update README.md 2021-01-26 23:11:20 +03:30
Aria Moradi 3398409555 Update README.md 2021-01-26 23:07:54 +03:30
Aria Moradi f05aa0589a [SKIP CI] add apache 2 license link 2021-01-26 23:02:04 +03:30
Aria Moradi fbc71ce781 fix nav buttons 2021-01-23 02:57:32 +03:30
Aria Moradi ca9c671886 more css hacks: scroll bar 2021-01-23 02:53:00 +03:30
Aria Moradi bd109ba11f some css hacks 2021-01-23 02:32:18 +03:30
Aria Moradi 0ff770a98b fluid manga grid 2021-01-23 01:58:18 +03:30
Aria Moradi ed7bb408a3 fix light theme AppBar 2021-01-23 01:36:56 +03:30
Aria Moradi 84676b9156 remove some wierdness 2021-01-23 01:33:12 +03:30
Aria Moradi dcdd50ffe1 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-01-23 01:25:49 +03:30
Aria Moradi afb21c59f0 DarkTheme! my eyes can rest now :) 2021-01-23 01:20:16 +03:30
Aria Moradi e219179519 Update README.md 2021-01-23 00:28:32 +03:30
Aria Moradi 15a2115c5a Update README.md 2021-01-23 00:26:43 +03:30
Aria Moradi 94c6f33925 Update README.md 2021-01-23 00:23:40 +03:30
Aria Moradi 202e38871d [RELEASE CI] hotfix 2021-01-22 21:33:54 +03:30
Aria Moradi 3f75b84651 fix button text 2021-01-22 21:33:12 +03:30
Aria Moradi f171b785a0 [RELEASE CI] fix linting error 2021-01-22 21:29:09 +03:30
Aria Moradi 088dd6a856 [RELEASE CI] Milestone: The application is considered usable. 2021-01-22 21:18:14 +03:30
Aria Moradi 6318628ea2 [RELEASE CI] bump version 2021-01-22 21:15:14 +03:30
Aria Moradi 0757ea5d0d implemented infinite scroll 2021-01-22 21:11:00 +03:30
Aria Moradi 2c76ad9b74 [RELEASE CI] Search implemented, other improvements 2021-01-22 18:14:59 +03:30
Aria Moradi 7d1c63e181 single source search done 2021-01-22 18:07:31 +03:30
Aria Moradi 6401b946b6 add page title 2021-01-22 17:00:33 +03:30
Aria Moradi 9a61f58043 add kotlinter 2021-01-22 12:34:03 +03:30
Aria Moradi b854fdeadb fix matching 2021-01-22 11:50:33 +03:30
Aria Moradi 6eb4f1ba88 Update README.md 2021-01-22 11:25:11 +03:30
Aria Moradi ded5e3a73a Update README.md 2021-01-22 11:13:25 +03:30
Aria Moradi 8d9f5b0bd9 [RELEASE CI] bug fixes, half-baked search 2021-01-22 02:39:24 +03:30
Aria Moradi 5cbb2a0481 fix SPA urls 2021-01-22 02:33:41 +03:30
Aria Moradi a3b17365b7 [RELEASE CI] fix build issues 2021-01-22 01:55:41 +03:30
Aria Moradi 2847554b8f [RELEASE CI] test release 2021-01-22 01:47:28 +03:30
Aria Moradi fb166eadd4 Update README.md 2021-01-22 01:45:14 +03:30
Aria Moradi 046c11f785 Update README.md 2021-01-22 01:40:47 +03:30
Aria Moradi eac7436b18 clean up bulid file name 2021-01-22 01:37:24 +03:30
Aria Moradi 1a23804e51 only release when told to :D 2021-01-22 00:40:05 +03:30
Aria Moradi 08be142858 Update README.md 2021-01-22 00:36:00 +03:30
Aria Moradi f421dbfe69 Update README.md 2021-01-22 00:34:49 +03:30
Aria Moradi 0d7ad65dbe Update README.md 2021-01-22 00:27:16 +03:30
Aria Moradi 9e946406bc Update README.md 2021-01-22 00:25:47 +03:30
Aria Moradi 9142d46fae Update README.md 2021-01-22 00:23:53 +03:30
Aria Moradi 39ed134f96 improve builds 2021-01-21 23:04:47 +03:30
Aria Moradi 7e4b495398 dummy file to trigger gh actions 2021-01-21 22:53:00 +03:30
Aria Moradi c12242b760 rm package-lock in favor of yarn 2021-01-21 22:43:17 +03:30
Aria Moradi 8b2fd28a54 remove shit, add shit to improve performance 2021-01-21 22:39:22 +03:30
Aria Moradi 466f21a7e8 set -e is the poison 2021-01-21 22:27:03 +03:30
Aria Moradi 26a7e2f1cd fix again? 2021-01-21 16:27:27 +03:30
Aria Moradi c155d78d52 fix build? 2021-01-21 16:20:22 +03:30
Aria Moradi 687dad5fc0 new scripts 2021-01-21 16:01:55 +03:30
Aria Moradi 33c4cdbc48 remove dummyFile 2021-01-21 15:38:20 +03:30
Aria Moradi e46cb9738f add latest revision 2021-01-21 15:35:14 +03:30
Aria Moradi aef37fcc9e add revision 2021-01-21 15:34:21 +03:30
Aria Moradi ac51f40a91 dummy file to trigger gh actions 2021-01-21 15:14:00 +03:30
Aria Moradi cd82af2d76 fix yarn build task 2021-01-21 15:04:11 +03:30
Aria Moradi 790040cd68 add stacktrace 2021-01-21 14:41:45 +03:30
Aria Moradi 95465ec265 update getAndroid 2021-01-21 14:33:02 +03:30
Aria Moradi 5313d91bf2 remove unfinished code for now 2021-01-21 14:29:13 +03:30
Aria Moradi 63783984c6 add getAndroid to workflow 2021-01-21 14:23:22 +03:30
Aria Moradi 97b9b1b6c9 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-01-21 14:15:34 +03:30
Aria Moradi 34a7c24e0b add build workflow 2021-01-21 14:15:05 +03:30
Aria Moradi 12765a771f Update README.md 2021-01-21 13:58:49 +03:30
Aria Moradi 39850c71b0 Update README.md 2021-01-21 13:58:15 +03:30
Aria Moradi 9e43645a67 Update README.md 2021-01-21 13:42:17 +03:30
Aria Moradi 7dc7f4d905 Update README.md 2021-01-21 13:41:03 +03:30
Aria Moradi c537c1bf29 manga search UI done 2021-01-20 15:26:52 +03:30
Aria Moradi 5f3ddbd1b2 Update README.md 2021-01-20 12:24:47 +03:30
Aria Moradi f297e3790c Update README.md 2021-01-20 03:42:01 +03:30
Aria Moradi ef3532357f Update README.md 2021-01-20 03:41:22 +03:30
Aria Moradi 48f29edf6c add dummy file 2021-01-20 03:30:20 +03:30
117 changed files with 3880 additions and 719 deletions
+43
View File
@@ -0,0 +1,43 @@
---
name: "🐞 Bug report"
title: "[Bug] <short description>"
about: "Report a bug"
labels: "bug"
---
**PLEASE READ THIS**
I acknowledge that:
- I have updated to the latest version of the app.
- I have tried the troubleshooting guide described in `README.md`
- If this is a request for adding/changing an extension it should be brought up to Tachiyomi: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
- If this is an issue with some extension not working properly, It does work inside Tachiyomi as intended.
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## Device information
- Tachidesk version: (Example: v0.2.3-r255-win32)
- Server Operating System: (Example: Ubuntu 20.04)
- Server JVM version: bundled with win32 or (Example: Java 8 Update 281 or OpenJDK 8u281)
- Client Operating System: <usually the same as above Server Operating System>
- Client Web Browser: (Example: Google Chrome 89.0.4389.82)
## Steps to reproduce
1. First Step
2. Second Step
### Expected behavior
Describe what should have happened
### Actual behavior
Describe what happens instead
## Other details
Describe additional details If necessary
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+29
View File
@@ -0,0 +1,29 @@
---
name: "🌟 Feature request"
title: "[Feature Request] <short description>"
about: "Suggest a feature to improve the project"
labels: "enhancement"
---
**PLEASE READ THIS**
I acknowledge that:
- I have updated to the latest version of the app.
- I have tried the troubleshooting guide described in `README.md`
- If this is a request for adding/changing an extension it should be brought up to Tachiyomi: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
- If this is an issue with some extension not working properly, It does work in Tachiyomi application as intended.
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## What feature should be added to Tachidesk?
Explain What the feature is and how it should work in detail
## Why/Project's Benefit/Existing Problem
Explain why this should be added
@@ -0,0 +1,7 @@
org.gradle.daemon=false
org.gradle.jvmargs=-Xmx5120m
org.gradle.workers.max=5
org.gradle.parallel=true
kotlin.incremental=false
kotlin.compiler.execution.strategy=in-process
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
git lfs install
#git lfs track "*.zip"
cp ../master/repo/* .
new_jar_build=$(ls *.jar| tail -1)
echo "last jar build file name: $new_jar_build"
new_win32_build=$(ls *.zip| tail -1)
echo "last win32 build file name: $new_win32_build"
cp -f $new_jar_build Tachidesk-latest.jar
cp -f $new_win32_build Tachidesk-latest-win32.zip
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git status
if [ -n "$(git status --porcelain)" ]; then
git add .
git commit -m "Update repo"
git push
else
echo "No changes to commit"
fi
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# Get last commit message
#last_commit_log=$(git log -1 --pretty=format:"%s")
#echo "last commit log: $last_commit_log"
#
#filter_count=$(echo "$last_commit_log" | grep -e '\[RELEASE CI\]' -e '\[CI RELEASE\]' | wc -c)
#echo "count is: $filter_count"
mkdir -p repo/
cp server/build/Tachidesk-*.jar repo/
cp server/build/Tachidesk-*.zip repo/
ls repo
pwd
#if [ "$filter_count" -gt 0 ]; then
# cp server/build/Tachidesk-*.jar repo/
# cp server/build/Tachidesk-*.zip repo/
#fi
+71
View File
@@ -0,0 +1,71 @@
name: CI
on:
push:
branches:
- master
pull_request:
jobs:
check_wrapper:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v2
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
build:
name: Build FatJar
needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest
steps:
- name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.5.0
with:
access_token: ${{ github.token }}
- name: Checkout master branch
uses: actions/checkout@v2
with:
ref: master
path: master
fetch-depth: 0
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Copy CI gradle.properties
run: |
cd master
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download android.jar
run: |
cd master
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Cache node_modules
uses: actions/cache@v2
with:
path: |
**/react/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
- name: Build Jar and launch4j
uses: eskatos/gradle-command-action@v1
with:
build-root-directory: master
wrapper-directory: master
arguments: :server:windowsPackage --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
+32
View File
@@ -0,0 +1,32 @@
name: Issue closer
on:
issues:
types: [opened, edited, reopened]
jobs:
autoclose:
runs-on: ubuntu-latest
steps:
- name: Autoclose issues
uses: arkon/issue-closer-action@v3.0
with:
repo-token: ${{ github.token }}
rules: |
[
{
"type": "title",
"regex": ".*<short description>*",
"message": "You did not fill out the description in the title"
},
{
"type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
"message": "The acknowledgment section was not removed"
},
{
"type": "body",
"regex": "(Tachidesk version|Server Operating System|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
"message": "The requested information was not filled out"
}
]
+115
View File
@@ -0,0 +1,115 @@
name: Publish
on:
push:
tags:
- 'v*'
jobs:
check_wrapper:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v2
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
build:
name: Build FatJar
needs: check_wrapper
runs-on: ubuntu-latest
steps:
- name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.5.0
with:
access_token: ${{ github.token }}
- name: Checkout master branch
uses: actions/checkout@v2
with:
ref: master
path: master
fetch-depth: 0
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Copy CI gradle.properties
run: |
cd master
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download android.jar
run: |
cd master
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Cache node_modules
uses: actions/cache@v2
with:
path: |
**/react/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Build Jar and launch4j
uses: eskatos/gradle-command-action@v1
with:
build-root-directory: master
wrapper-directory: master
arguments: :server:windowsPackage --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
- name: Create repo artifacts
run: |
cd master
./.github/scripts/create-repo.sh
- name: Upload Release
uses: xresloader/upload-to-github-release@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
file: "master/repo/*"
tags: true
draft: true
verbose: true
# - name: Create Release
# id: create_release
# uses: actions/create-release@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# tag_name: ${{ github.ref }}
# release_name: Release ${{ github.ref }}
# body: |
# Release body
# draft: false
# prerelease: true
#
# - name: Get the Ref
# id: get-ref
# uses: ankitvgupta/ref-to-tag-action@master
# with:
# ref: ${{ github.ref }}
# head_ref: ${{ github.head_ref }}
#
# - name: Get the tag
# run: echo "The tag was ${{ steps.get-ref.outputs.tag }}"
#
# - name: Upload Release
# uses: AButler/upload-release-assets@v2.0
# with:
# files: 'master/repo/*'
# repo-token: ${{ secrets.GITHUB_TOKEN }}
# release-tag: ${{ steps.get-ref.outputs.tag }}
+1 -1
View File
@@ -1,4 +1,4 @@
dependencies { dependencies {
// Config API // Config API, moved to the global build.gradle
// implementation("com.typesafe:config:1.4.0") // implementation("com.typesafe:config:1.4.0")
} }
@@ -4,54 +4,55 @@ import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigRenderOptions
import mu.KotlinLogging import mu.KotlinLogging
import net.harawata.appdirs.AppDirsFactory
import java.io.File import java.io.File
/** /**
* Manages app config. * Manages app config.
*/ */
open class ConfigManager { open class ConfigManager {
private val generatedModules private val dataRoot by lazy { AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!! }
= 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
open val configFolder: String open val appConfigFile: String = "$dataRoot/server.conf"
get() = System.getProperty("compat-configdirs") ?: "tachiserver-data/config"
val logger = KotlinLogging.logger {} val logger = KotlinLogging.logger {}
/** /**
* Get a config module * Get a config module
*/ */
inline fun <reified T : ConfigModule> module(): T inline fun <reified T : ConfigModule> module(): T = loadedModules[T::class.java] as T
= loadedModules[T::class.java] as T
/** /**
* Get a config module (Java API) * Get a config module (Java API)
*/ */
fun <T : ConfigModule> module(type: Class<T>): T fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
= loadedModules[type] as T
/** /**
* Load configs * Load configs
*/ */
fun loadConfigs(): Config { fun loadConfigs(): Config {
val configs = mutableListOf<Config>() //Load reference configs
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
//Load reference config //Load user config
configs += ConfigFactory.parseResources("reference.conf") val userConfig =
File(appConfigFile).let{
ConfigFactory.parseFile(it)
}
//Load custom configs from dir val config = ConfigFactory.empty()
File(configFolder).listFiles()?.map { .withFallback(userConfig)
ConfigFactory.parseFile(it) .withFallback(compatConfig)
}?.filterNotNull()?.forEach { .withFallback(serverConfig)
configs += it.withFallback(configs.last()) .resolve()
}
val config = configs.last().resolve()
logger.debug { logger.debug {
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true)) "Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
@@ -1,35 +0,0 @@
package xyz.nulldev.ts.config
import com.typesafe.config.Config
import java.io.File
class ServerConfig(config: Config) : ConfigModule(config) {
val ip = config.getString("ip")
val port = config.getInt("port")
val allowConfigChanges = config.getBoolean("allowConfigChanges")
val enableWebUi = config.getBoolean("enableWebUi")
val useOldWebUi = config.getBoolean("useOldWebUi")
val prettyPrintApi = config.getBoolean("prettyPrintApi")
// TODO Apply to operation IDs
val disabledApiEndpoints = config.getStringList("disabledApiEndpoints").map(String::toLowerCase)
val enabledApiEndpoints = config.getStringList("enabledApiEndpoints").map(String::toLowerCase)
val httpInitializedPrintMessage = config.getString("httpInitializedPrintMessage")
val useExternalStaticFiles = config.getBoolean("useExternalStaticFiles")
val externalStaticFilesFolder = config.getString("externalStaticFilesFolder")
val rootDir = registerFile(config.getString("rootDir"))
val patchesDir = registerFile(config.getString("patchesDir"))
fun registerFile(file: String): File {
return File(file).apply {
mkdirs()
}
}
companion object {
fun register(config: Config)
= ServerConfig(config.getConfig("ts.server"))
}
}
@@ -1,6 +1,3 @@
# Server ip and port bindings
ts.server.ip = 0.0.0.0
ts.server.port = 4567
# Allow/disallow preference changes (useful for demos) # Allow/disallow preference changes (useful for demos)
ts.server.allowConfigChanges = true ts.server.allowConfigChanges = true
+76 -17
View File
@@ -1,33 +1,92 @@
![image](https://github.com/AriaMoradi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png)
# Tachidesk # Tachidesk
A not so much port of [Tachiyomi](https://tachiyomi.org/) to the web (and later Electron for the desktop experience)! A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
## Is this application usable? Should I test it?
Here is a list of current features:
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
- A library to save your mangas and categories to put them into.
- Searching and browsing installed sources.
- A minimal chapter reader.
- Ability to download Mangas for offline read(This partially works)
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens.
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented in more detail.
## Downloading and Running the app
#### Prerequisites
You should have The Java Runtime Environment(JRE) 8 or newer (if you're not planning to use the Windows specific build) and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
#### Download the app
Download the latest jar or windows(win32) release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
#### Running pre-built jar packages
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` 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.
#### Running pre-built Windows packages
Windows specific builds have java bundled inside them, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`, the rest will work like the jar release.
#### Running on Docker
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
## General troubleshooting
If the app breaks try deleting the directory below and re-running the app (**This will delete all your data!**) and if the problem persists open an issue.
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
On Windows 7 and later : `C:\Users\<Account>\AppData\Tachidesk`
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
## Support and help
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support and help.
## How does it work?
This project has two components: This project has two components:
1. **server:** contains some of the original Tachiyomi code and serves a REST API 1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react project that works with the server to do the presentation 2. **webUI:** A react SPA project that works with the server to do the presentation.
## How do I run the thing? ## Building from source
### Get Android stubs jar(do this only once) ### Get Android stubs jar
#### Manual download #### Manual download
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`. Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
#### Building from source(needs `bash`, `curl`, `base64`, `zip` to work) #### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
run `scripts/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository. Run `scripts/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository.
### building the jar ### building the jar
run `./gradlew :server:fatJar` the resulting jar file will be `server/build/server-1.0-all.jar`. Simply double click on it or run `java -jar server-1.0-all.jar`. The server will be running on `http://localhost:4567` open this url in your browser. Run `./gradlew shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
## running for development purposes ### building the Windows package
### The Server Run `./gradlew windowsPackage`, the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
run `./gradlew :server:run -x :webUI:yarn_build --stacktrace` to run the server ## Running for development purposes
### the webUI ### `server` module
how to do it is described in `webUI/react/README.md` but for short, Run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
### `webUI` module
How to do it is described in `webUI/react/README.md` but for short,
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once) first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
then `yarn start` to start the client if a new browser window doesn't start automatically, then `yarn start` to start the client if a new browser window doesn't start automatically,
then open `http://127.0.0.1:3000` in a modern browser. 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.
## Is the application usable? Should I test it? ## Credit
Checkout [the state of project](https://github.com/AriaMoradi/Tachidesk/issues/2) to see what's implemented. The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this project.
## License ## License
Copyright (C) 2020 Aria Moradi Copyright (C) 2020-2021 Aria Moradi and contributors
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
+3
View File
@@ -76,5 +76,8 @@ configure(listOf(
// dependency of :AndroidCompat:Config // dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.0") implementation("com.typesafe:config:1.4.0")
// to get application content root
implementation("net.harawata:appdirs:1.2.0")
} }
} }
+2 -3
View File
@@ -58,9 +58,8 @@ function dedup() {
pushd .. pushd ..
dedup AndroidCompat/src/main/java dedup AndroidCompat/src/main/java
dedup TachiServer/src/main/java dedup server/src/main/java
dedup Tachiyomi-App/src/main/java dedup server/src/main/kotlin
dedup Tachiyomi-App/src/compat/java
popd popd
popd popd
+94 -9
View File
@@ -1,11 +1,16 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import java.io.BufferedReader
plugins { plugins {
// id("org.jetbrains.kotlin.jvm") version "1.4.21" // id("org.jetbrains.kotlin.jvm") version "1.4.21"
application application
id("com.github.johnrengelman.shadow") version "6.1.0" id("com.github.johnrengelman.shadow") version "6.1.0"
id("org.jmailen.kotlinter") version "3.3.0"
id("edu.sc.seis.launch4j") version "2.4.9"
} }
val TachideskVersion = "v0.2.4"
repositories { repositories {
mavenCentral() mavenCentral()
@@ -67,15 +72,16 @@ dependencies {
implementation("org.slf4j:slf4j-api:1.8.0-beta4") implementation("org.slf4j:slf4j-api:1.8.0-beta4")
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3") implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
// to get application content root
implementation("net.harawata:appdirs:1.2.0")
// Exposed ORM // Exposed ORM
val exposed_version = "0.28.1" val exposed_version = "0.28.1"
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version") implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version") implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-jdbc:$exposed_version") implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation ("org.xerial:sqlite-jdbc:3.30.1") implementation("com.h2database:h2:1.4.199")
// tray icon
implementation("com.dorkbox:SystemTray:3.17")
// AndroidCompat // AndroidCompat
implementation(project(":AndroidCompat")) implementation(project(":AndroidCompat"))
@@ -86,8 +92,8 @@ dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
} }
val name = "ir.armor.tachidesk.Main"
application { application {
val name = "ir.armor.tachidesk.Main"
mainClass.set(name) mainClass.set(name)
// Required by ShadowJar. // Required by ShadowJar.
@@ -102,6 +108,19 @@ sourceSets {
} }
} }
val TachideskRevision = Runtime
.getRuntime()
.exec("git rev-list master --count")
.let { process ->
process.waitFor()
val output = process.inputStream.use {
it.bufferedReader().use(BufferedReader::readText)
}
process.destroy()
"r" + output.trim()
}
tasks { tasks {
jar { jar {
manifest { manifest {
@@ -115,15 +134,81 @@ tasks {
} }
shadowJar { shadowJar {
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
archiveBaseName.set("Tachidesk")
archiveVersion.set(TachideskVersion)
archiveClassifier.set(TachideskRevision)
} }
} }
launch4j { //used for windows
mainClassName = name
bundledJrePath = "jre"
bundledJre64Bit = true
jreMinVersion = "8"
outputDir = "Tachidesk-$TachideskVersion-$TachideskRevision-win32"
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
jar = "${projectDir}/build/Tachidesk-$TachideskVersion-$TachideskRevision.jar"
}
tasks.register<Zip>("windowsPackage") {
from(fileTree("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32"))
destinationDirectory.set(File("$buildDir"))
archiveFileName.set("Tachidesk-$TachideskVersion-$TachideskRevision-win32.zip")
dependsOn("windowsPackageWorkaround2")
}
tasks.register<Delete>("windowsPackageWorkaround2") {
delete(
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jre",
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/lib",
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/server.exe",
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32"
)
dependsOn("windowsPackageWorkaround")
}
tasks.register<Copy>("windowsPackageWorkaround") {
from("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
dependsOn("deleteUnwantedJreDir")
}
tasks.register<Delete>("deleteUnwantedJreDir") {
delete(
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jdk8u282-b08-jre"
)
dependsOn("addJreToDistributable")
}
tasks.register<Copy>("addJreToDistributable") {
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
eachFile {
path = path.replace(".*-jre".toRegex(),"jre")
}
dependsOn("downloadJre")
dependsOn("createExe")
}
tasks.register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
src("https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u282-b08/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
dest(buildDir)
overwrite(false)
onlyIfModified(true)
}
tasks.withType<ShadowJar> { tasks.withType<ShadowJar> {
destinationDir = File("$rootDir/server/build") destinationDir = File("$rootDir/server/build")
//dependsOn(":webUI:copyBuild") dependsOn("lintKotlin")
} }
tasks.named("processResources") { tasks.named("processResources") {
dependsOn(":webUI:copyBuild") dependsOn(":webUI:copyBuild")
} }
tasks.named("run") {
dependsOn("formatKotlin", "lintKotlin")
}
@@ -1,5 +1,9 @@
package ir.armor.tachidesk; package ir.armor.tachidesk;
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap; import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
@@ -2,9 +2,9 @@ package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
//import android.content.res.Configuration // import android.content.res.Configuration
//import android.support.multidex.MultiDex // import android.support.multidex.MultiDex
//import timber.log.Timber // import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.registry.default.DefaultRegistrar import uy.kohesive.injekt.registry.default.DefaultRegistrar
@@ -2,19 +2,22 @@ package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import com.google.gson.Gson import com.google.gson.Gson
//import eu.kanade.tachiyomi.data.cache.ChapterCache // import eu.kanade.tachiyomi.data.cache.ChapterCache
//import eu.kanade.tachiyomi.data.cache.CoverCache // import eu.kanade.tachiyomi.data.cache.CoverCache
//import eu.kanade.tachiyomi.data.database.DatabaseHelper // import eu.kanade.tachiyomi.data.database.DatabaseHelper
//import eu.kanade.tachiyomi.data.download.DownloadManager // import eu.kanade.tachiyomi.data.download.DownloadManager
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper // import eu.kanade.tachiyomi.data.preference.PreferencesHelper
//import eu.kanade.tachiyomi.data.sync.LibrarySyncManager // import eu.kanade.tachiyomi.data.sync.LibrarySyncManager
//import eu.kanade.tachiyomi.data.track.TrackManager // import eu.kanade.tachiyomi.data.track.TrackManager
//import eu.kanade.tachiyomi.extension.ExtensionManager // import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import rx.Observable import rx.Observable
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.api.* import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
@@ -56,11 +59,9 @@ class AppModule(val app: Application) : InjektModule {
} }
// rxAsync { get<DatabaseHelper>() } // rxAsync { get<DatabaseHelper>() }
} }
private fun rxAsync(block: () -> Unit) { private fun rxAsync(block: () -> Unit) {
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe() Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
} }
} }
@@ -1,17 +1,17 @@
package eu.kanade.tachiyomi.extension.api package eu.kanade.tachiyomi.extension.api
//import android.content.Context // import android.content.Context
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper // import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
//import kotlinx.coroutines.Dispatchers // import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.withContext // import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.int import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
//import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
internal class ExtensionGithubApi { internal class ExtensionGithubApi {
@@ -27,7 +27,7 @@ internal class ExtensionGithubApi {
// suspend fun checkForUpdates(): List<Extension.Installed> { // suspend fun checkForUpdates(): List<Extension.Installed> {
// val extensions = fin dExtensions() // val extensions = fin dExtensions()
// //
//// preferences.lastExtCheck().set(Date().time) // // preferences.lastExtCheck().set(Date().time)
// //
// val installedExtensions = ExtensionLoader.loadExtensions(context) // val installedExtensions = ExtensionLoader.loadExtensions(context)
// .filterIsInstance<LoadResult.Success>() // .filterIsInstance<LoadResult.Success>()
@@ -49,23 +49,23 @@ internal class ExtensionGithubApi {
private fun parseResponse(json: JsonArray): List<Extension.Available> { private fun parseResponse(json: JsonArray): List<Extension.Available> {
return json return json
.filter { element -> .filter { element ->
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val libVersion = versionName.substringBeforeLast('.').toDouble() val libVersion = versionName.substringBeforeLast('.').toDouble()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
} }
.map { element -> .map { element ->
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ") val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1 val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}" val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon) Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
} }
} }
fun getApkUrl(extension: Extension.Available): String { fun getApkUrl(extension: Extension.Available): String {
@@ -9,7 +9,7 @@ import retrofit2.Retrofit
import retrofit2.http.GET import retrofit2.http.GET
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
//import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
/** /**
* Used to get the extension repo listing from GitHub. * Used to get the extension repo listing from GitHub.
@@ -1,28 +1,22 @@
package eu.kanade.tachiyomi.extension.util package eu.kanade.tachiyomi.extension.util
//import android.annotation.SuppressLint // import android.annotation.SuppressLint
//import android.content.Context // import android.content.Context
//import android.content.pm.PackageInfo // import android.content.pm.PackageInfo
//import android.content.pm.PackageManager // import android.content.pm.PackageManager
//import dalvik.system.PathClassLoader // import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.annoations.Nsfw // import eu.kanade.tachiyomi.data.preference.PreferenceValues
//import eu.kanade.tachiyomi.data.preference.PreferenceValues // import eu.kanade.tachiyomi.data.preference.PreferencesHelper
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper // import eu.kanade.tachiyomi.util.lang.Hash
import eu.kanade.tachiyomi.extension.model.Extension // import kotlinx.coroutines.async
import eu.kanade.tachiyomi.extension.model.LoadResult // import kotlinx.coroutines.runBlocking
import eu.kanade.tachiyomi.source.CatalogueSource // import timber.log.Timber
import eu.kanade.tachiyomi.source.Source // import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.source.SourceFactory
//import eu.kanade.tachiyomi.util.lang.Hash
//import kotlinx.coroutines.async
//import kotlinx.coroutines.runBlocking
//import timber.log.Timber
//import uy.kohesive.injekt.injectLazy
/** /**
* Class that handles the loading of the extensions installed in the system. * Class that handles the loading of the extensions installed in the system.
*/ */
//@SuppressLint("PackageManagerGetSignatures") // @SuppressLint("PackageManagerGetSignatures")
internal object ExtensionLoader { internal object ExtensionLoader {
// private val preferences: PreferencesHelper by injectLazy() // private val preferences: PreferencesHelper by injectLazy()
@@ -1,30 +1,23 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
//import android.annotation.SuppressLint // import android.annotation.SuppressLint
//import android.content.Context // import android.content.Context
//import android.os.Build // import android.os.Build
//import android.os.Handler // import android.os.Handler
//import android.os.Looper // import android.os.Looper
//import android.webkit.WebSettings // import android.webkit.WebSettings
//import android.webkit.WebView // import android.webkit.WebView
//import android.widget.Toast // import android.widget.Toast
//import eu.kanade.tachiyomi.R // import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.HttpSource // import eu.kanade.tachiyomi.util.lang.launchUI
//import eu.kanade.tachiyomi.util.lang.launchUI // import eu.kanade.tachiyomi.util.system.WebViewClientCompat
//import eu.kanade.tachiyomi.util.system.WebViewClientCompat // import eu.kanade.tachiyomi.util.system.WebViewUtil
//import eu.kanade.tachiyomi.util.system.WebViewUtil // import eu.kanade.tachiyomi.util.system.isOutdated
//import eu.kanade.tachiyomi.util.system.isOutdated // import eu.kanade.tachiyomi.util.system.setDefaultSettings
//import eu.kanade.tachiyomi.util.system.setDefaultSettings // import eu.kanade.tachiyomi.util.system.toast
//import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
//import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class CloudflareInterceptor() : Interceptor { class CloudflareInterceptor() : Interceptor {
@@ -77,7 +70,7 @@ class CloudflareInterceptor() : Interceptor {
// } // }
} }
// //
//// @SuppressLint("SetJavaScriptEnabled") // // @SuppressLint("SetJavaScriptEnabled")
// private fun resolveWithWebView(request: Request, oldCookie: Cookie?) { // private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
// // We need to lock this thread until the WebView finds the challenge solution url, because // // We need to lock this thread until the WebView finds the challenge solution url, because
// // OkHttp doesn't support asynchronous interceptors. // // OkHttp doesn't support asynchronous interceptors.
@@ -0,0 +1,72 @@
package eu.kanade.tachiyomi.network
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
class MemoryCookieJar : CookieJar {
private val cache = mutableSetOf<WrappedCookie>()
@Synchronized
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookiesToRemove = mutableSetOf<WrappedCookie>()
val validCookies = mutableSetOf<WrappedCookie>()
cache.forEach { cookie ->
if (cookie.isExpired()) {
cookiesToRemove.add(cookie)
} else if (cookie.matches(url)) {
validCookies.add(cookie)
}
}
cache.removeAll(cookiesToRemove)
return validCookies.toList().map(WrappedCookie::unwrap)
}
@Synchronized
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val cookiesToAdd = cookies.map { WrappedCookie.wrap(it) }
cache.removeAll(cookiesToAdd)
cache.addAll(cookiesToAdd)
}
@Synchronized
fun clear() {
cache.clear()
}
}
class WrappedCookie private constructor(val cookie: Cookie) {
fun unwrap() = cookie
fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
fun matches(url: HttpUrl) = cookie.matches(url)
override fun equals(other: Any?): Boolean {
if (other !is WrappedCookie) return false
return other.cookie.name == cookie.name &&
other.cookie.domain == cookie.domain &&
other.cookie.path == cookie.path &&
other.cookie.secure == cookie.secure &&
other.cookie.hostOnly == cookie.hostOnly
}
override fun hashCode(): Int {
var hash = 17
hash = 31 * hash + cookie.name.hashCode()
hash = 31 * hash + cookie.domain.hashCode()
hash = 31 * hash + cookie.path.hashCode()
hash = 31 * hash + if (cookie.secure) 0 else 1
hash = 31 * hash + if (cookie.hostOnly) 0 else 1
return hash
}
companion object {
fun wrap(cookie: Cookie) = WrappedCookie(cookie)
}
}
@@ -1,17 +1,14 @@
package eu.kanade.tachiyomi.network 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 android.content.Context
import okhttp3.Cache // import okhttp3.HttpUrl.Companion.toHttpUrl
//import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient 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 java.io.File
import java.net.InetAddress
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class NetworkHelper(context: Context) { class NetworkHelper(context: Context) {
@@ -22,14 +19,17 @@ class NetworkHelper(context: Context) {
private val cacheSize = 5L * 1024 * 1024 // 5 MiB private val cacheSize = 5L * 1024 * 1024 // 5 MiB
// val cookieManager = AndroidCookieJar() val cookieManager = MemoryCookieJar()
val client by lazy { val client by lazy {
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
// .cookieJar(cookieManager) .cookieJar(cookieManager)
// .cache(Cache(cacheDir, cacheSize)) // .cache(Cache(cacheDir, cacheSize))
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.MINUTES)
.writeTimeout(5, TimeUnit.MINUTES)
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
// .addInterceptor(UserAgentInterceptor()) // .addInterceptor(UserAgentInterceptor())
// if (BuildConfig.DEBUG) { // if (BuildConfig.DEBUG) {
@@ -1,18 +1,14 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
//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 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.resume
import kotlin.coroutines.resumeWithException
fun Call.asObservable(): Observable<Response> { fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber -> return Observable.unsafeCreate { subscriber ->
@@ -38,7 +34,7 @@ fun Call.asObservable(): Observable<Response> {
} }
override fun unsubscribe() { override fun unsubscribe() {
call.cancel() // call.cancel()
} }
override fun isUnsubscribed(): Boolean { override fun isUnsubscribed(): Boolean {
@@ -52,7 +48,7 @@ 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(assertSuccess: Boolean = false): Response {
// return suspendCancellableCoroutine { continuation -> // return suspendCancellableCoroutine { continuation ->
// enqueue( // enqueue(
// object : Callback { // object : Callback {
@@ -81,20 +77,21 @@ fun Call.asObservable(): Observable<Response> {
// } // }
// } // }
// } // }
//} // }
fun Call.asObservableSuccess(): Observable<Response> { fun Call.asObservableSuccess(): Observable<Response> {
return asObservable().doOnNext { response -> return asObservable()
if (!response.isSuccessful) { .doOnNext { response ->
response.close() if (!response.isSuccessful) {
throw Exception("HTTP error ${response.code}") response.close()
throw Exception("HTTP error ${response.code}")
}
} }
}
} }
//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(nasObservableSuccessull)
// .addNetworkInterceptor { chain -> // .addNetworkInterceptor { chain ->
// val originalResponse = chain.proceed(chain.request()) // val originalResponse = chain.proceed(chain.request())
// originalResponse.newBuilder() // originalResponse.newBuilder()
@@ -104,11 +101,11 @@ fun Call.asObservableSuccess(): Observable<Response> {
// .build() // .build()
// //
// return progressClient.newCall(request) // return progressClient.newCall(request)
//} // }
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)
// .addNetworkInterceptor { chain -> // .addNetworkInterceptor { chain ->
// val originalResponse = chain.proceed(chain.request()) // val originalResponse = chain.proceed(chain.request())
// originalResponse.newBuilder() // originalResponse.newBuilder()
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
//import androidx.preference.PreferenceScreen // import androidx.preference.PreferenceScreen
interface ConfigurableSource : Source { interface ConfigurableSource : Source {
@@ -1,13 +1,13 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
//import android.graphics.drawable.Drawable // import android.graphics.drawable.Drawable
//import eu.kanade.tachiyomi.extension.ExtensionManager // import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import rx.Observable import rx.Observable
//import uy.kohesive.injekt.Injekt // import uy.kohesive.injekt.Injekt
//import uy.kohesive.injekt.api.get // import uy.kohesive.injekt.api.get
/** /**
* A basic interface for creating a source. It could be an online source, a local source, etc... * A basic interface for creating a source. It could be an online source, a local source, etc...
@@ -46,6 +46,6 @@ interface Source {
fun fetchPageList(chapter: SChapter): Observable<List<Page>> fun fetchPageList(chapter: SChapter): Observable<List<Page>>
} }
//fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this) // fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
//fun Source.getPreferenceKey(): String = "source_$id" // fun Source.getPreferenceKey(): String = "source_$id"
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
//import android.content.Context // import android.content.Context
//import eu.kanade.tachiyomi.R // import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
@@ -9,7 +9,7 @@ open class Page(
val url: String = "", val url: String = "",
var imageUrl: String? = null, var imageUrl: String? = null,
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions @Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
): ProgressListener { ) : ProgressListener {
val number: Int val number: Int
get() = index + 1 get() = index + 1
@@ -12,7 +12,7 @@ interface SChapter : Serializable {
var chapter_number: Float var chapter_number: Float
var scanlator: String? var scanlator: String?
fun copyFrom(other: SChapter) { fun copyFrom(other: SChapter) {
name = other.name name = other.name
@@ -16,7 +16,7 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
//import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import java.security.MessageDigest import java.security.MessageDigest
@@ -29,7 +29,7 @@ abstract class HttpSource : CatalogueSource {
/** /**
* Network service. * Network service.
*/ */
protected val network: NetworkHelper by injectLazy() val network: NetworkHelper by injectLazy()
// /** // /**
// * Preferences that a source may need. // * Preferences that a source may need.
@@ -311,7 +311,7 @@ abstract class HttpSource : CatalogueSource {
* *
* @param page the chapter whose page list has to be fetched * @param page the chapter whose page list has to be fetched
*/ */
protected open fun imageRequest(page: Page): Request { open fun imageRequest(page: Page): Request {
return GET(page.imageUrl!!, headers) return GET(page.imageUrl!!, headers)
} }
@@ -1,8 +0,0 @@
package ir.armor.tachidesk
import net.harawata.appdirs.AppDirsFactory
object Config {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk",null, null)
val extensionsRoot = "$dataRoot/extensions"
}
+196 -47
View File
@@ -1,79 +1,109 @@
package ir.armor.tachidesk package ir.armor.tachidesk
import eu.kanade.tachiyomi.App /* 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 io.javalin.Javalin import io.javalin.Javalin
import ir.armor.tachidesk.util.* import ir.armor.tachidesk.util.addMangaToCategory
import org.kodein.di.DI import ir.armor.tachidesk.util.addMangaToLibrary
import org.kodein.di.conf.global import ir.armor.tachidesk.util.createCategory
import xyz.nulldev.androidcompat.AndroidCompat import ir.armor.tachidesk.util.getCategoryList
import xyz.nulldev.androidcompat.AndroidCompatInitializer import ir.armor.tachidesk.util.getCategoryMangaList
import xyz.nulldev.ts.config.ConfigKodeinModule import ir.armor.tachidesk.util.getChapter
import xyz.nulldev.ts.config.GlobalConfigManager import ir.armor.tachidesk.util.getChapterList
import ir.armor.tachidesk.util.getExtensionIcon
import ir.armor.tachidesk.util.getExtensionList
import ir.armor.tachidesk.util.getLibraryMangas
import ir.armor.tachidesk.util.getManga
import ir.armor.tachidesk.util.getMangaCategories
import ir.armor.tachidesk.util.getMangaList
import ir.armor.tachidesk.util.getPageImage
import ir.armor.tachidesk.util.getSource
import ir.armor.tachidesk.util.getSourceList
import ir.armor.tachidesk.util.getThumbnail
import ir.armor.tachidesk.util.installAPK
import ir.armor.tachidesk.util.openInBrowser
import ir.armor.tachidesk.util.removeCategory
import ir.armor.tachidesk.util.removeExtension
import ir.armor.tachidesk.util.removeMangaFromCategory
import ir.armor.tachidesk.util.removeMangaFromLibrary
import ir.armor.tachidesk.util.reorderCategory
import ir.armor.tachidesk.util.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch
import ir.armor.tachidesk.util.updateCategory
class Main { class Main {
companion object { companion object {
val androidCompat by lazy { AndroidCompat() }
fun registerConfigModules() {
GlobalConfigManager.registerModules(
// ServerConfig.register(GlobalConfigManager.config),
// SyncConfigModule.register(GlobalConfigManager.config)
)
}
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
// make sure everything we need exists serverSetup()
applicationSetup()
registerConfigModules()
//Load config API
DI.global.addImport(ConfigKodeinModule().create())
//Load Android compatibility dependencies
AndroidCompatInitializer().init()
// start app
androidCompat.startApp(App())
var hasWebUiBundled: Boolean = false
val app = Javalin.create { config -> val app = Javalin.create { config ->
// config.addSinglePageRoot("/", "") try {
config.addStaticFiles("/react") this::class.java.classLoader.getResource("/react/index.html")
}.start(4567) hasWebUiBundled = true
config.addStaticFiles("/react")
config.addSinglePageRoot("/", "/react/index.html")
} catch (e: RuntimeException) {
app.before() { ctx -> println("Warning: react build files are missing.")
// allow the client which is running on another port hasWebUiBundled = false
ctx.header("Access-Control-Allow-Origin", "*") }
config.enableCorsForAllOrigins()
}.start(serverConfig.ip, serverConfig.port)
if (hasWebUiBundled) {
openInBrowser()
} }
app.get("/api/v1/extension/list") { ctx -> app.get("/api/v1/extension/list") { ctx ->
ctx.json(getExtensionList()) ctx.json(getExtensionList())
} }
app.get("/api/v1/extension/install/:apkName") { ctx -> app.get("/api/v1/extension/install/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName") val apkName = ctx.pathParam("apkName")
println(apkName) println("installing $apkName")
ctx.status( ctx.status(
installAPK(apkName) installAPK(apkName)
) )
} }
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
println("uninstalling $apkName")
removeExtension(apkName)
ctx.status(200)
}
app.get("/api/v1/extension/icon/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
val result = getExtensionIcon(apkName)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
app.get("/api/v1/source/list") { ctx -> app.get("/api/v1/source/list") { ctx ->
ctx.json(getSourceList()) ctx.json(getSourceList())
} }
app.get("/api/v1/source/:sourceId") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(getSource(sourceId))
}
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx -> app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt() val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId,pageNum,popular = true)) ctx.json(getMangaList(sourceId, pageNum, popular = true))
} }
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx -> app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt() val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId,pageNum,popular = false)) ctx.json(getMangaList(sourceId, pageNum, popular = false))
} }
app.get("/api/v1/manga/:mangaId/") { ctx -> app.get("/api/v1/manga/:mangaId/") { ctx ->
@@ -81,6 +111,50 @@ class Main {
ctx.json(getManga(mangaId)) ctx.json(getManga(mangaId))
} }
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val result = getThumbnail(mangaId)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// adds the manga to library
app.get("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
addMangaToLibrary(mangaId)
ctx.status(200)
}
// removes the manga from the library
app.delete("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
removeMangaFromLibrary(mangaId)
ctx.status(200)
}
// adds the manga to category
app.get("api/v1/manga/:mangaId/category/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getMangaCategories(mangaId))
}
// adds the manga to category
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val categoryId = ctx.pathParam("categoryId").toInt()
addMangaToCategory(mangaId, categoryId)
ctx.status(200)
}
// removes the manga from the category
app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val categoryId = ctx.pathParam("categoryId").toInt()
removeMangaFromCategory(mangaId, categoryId)
ctx.status(200)
}
app.get("/api/v1/manga/:mangaId/chapters") { ctx -> app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapterList(mangaId)) ctx.json(getChapterList(mangaId))
@@ -89,11 +163,86 @@ class Main {
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx -> app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
val chapterId = ctx.pathParam("chapterId").toInt() val chapterId = ctx.pathParam("chapterId").toInt()
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getPages(chapterId,mangaId)) ctx.json(getChapter(chapterId, mangaId))
}
app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx ->
val chapterId = ctx.pathParam("chapterId").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
val index = ctx.pathParam("index").toInt()
val result = getPageImage(mangaId, chapterId, index)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// global search
app.get("/api/v1/search/:searchTerm") { ctx ->
val searchTerm = ctx.pathParam("searchTerm")
ctx.json(sourceGlobalSearch(searchTerm))
}
// single source search
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm")
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
}
// source filter list
app.get("/api/v1/source/:sourceId/filters/") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(sourceFilters(sourceId))
}
// lists mangas that have no category assigned
app.get("/api/v1/library/") { ctx ->
ctx.json(getLibraryMangas())
}
// category list
app.get("/api/v1/category/") { ctx ->
ctx.json(getCategoryList())
}
// category create
app.post("/api/v1/category/") { ctx ->
val name = ctx.formParam("name")!!
createCategory(name)
ctx.status(200)
}
// category modification
app.patch("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId")!!.toInt()
val name = ctx.formParam("name")
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
updateCategory(categoryId, name, isLanding)
ctx.status(200)
}
// category re-ordering
app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
val from = ctx.formParam("from")!!.toInt()
val to = ctx.formParam("to")!!.toInt()
reorderCategory(categoryId, from, to)
ctx.status(200)
}
// category delete
app.delete("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
removeCategory(categoryId)
ctx.status(200)
}
// returns the manga list associated with a category
app.get("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
ctx.json(getCategoryMangaList(categoryId))
} }
} }
} }
} }
@@ -0,0 +1,25 @@
package ir.armor.tachidesk
import com.typesafe.config.Config
import xyz.nulldev.ts.config.ConfigModule
import java.io.File
class ServerConfig(config: Config) : ConfigModule(config) {
val ip = config.getString("ip")
val port = config.getInt("port")
// proxy
val socksProxy = config.getBoolean("socksProxy")
val socksProxyHost = config.getString("socksProxyHost")
val socksProxyPort = config.getString("socksProxyPort")
fun registerFile(file: String): File {
return File(file).apply {
mkdirs()
}
}
companion object {
fun register(config: Config) = ServerConfig(config.getConfig("server"))
}
}
@@ -0,0 +1,60 @@
package ir.armor.tachidesk
import eu.kanade.tachiyomi.App
import ir.armor.tachidesk.database.makeDataBaseTables
import ir.armor.tachidesk.util.systemTray
import net.harawata.appdirs.AppDirsFactory
import org.kodein.di.DI
import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.AndroidCompat
import xyz.nulldev.androidcompat.AndroidCompatInitializer
import xyz.nulldev.ts.config.ConfigKodeinModule
import xyz.nulldev.ts.config.GlobalConfigManager
import java.io.File
object applicationDirs {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!!
val extensionsRoot = "$dataRoot/extensions"
val thumbnailsRoot = "$dataRoot/thumbnails"
val mangaRoot = "$dataRoot/manga"
}
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
val systemTray by lazy { systemTray() }
val androidCompat by lazy { AndroidCompat() }
fun serverSetup() {
// register server config
GlobalConfigManager.registerModule(
ServerConfig.register(GlobalConfigManager.config)
)
// make dirs we need
listOf(
applicationDirs.dataRoot,
applicationDirs.extensionsRoot,
"${applicationDirs.extensionsRoot}/icon",
applicationDirs.thumbnailsRoot
).forEach {
File(it).mkdirs()
}
makeDataBaseTables()
// create system tray
systemTray
// Load config API
DI.global.addImport(ConfigKodeinModule().create())
// Load Android compatibility dependencies
AndroidCompatInitializer().init()
// start app
androidCompat.startApp(App())
// socks proxy settings
System.getProperties()["proxySet"] = serverConfig.socksProxy.toString()
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
}
@@ -1,9 +1,16 @@
package ir.armor.tachidesk.database package ir.armor.tachidesk.database
import ir.armor.tachidesk.Config /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.table.CategoryMangaTable
import ir.armor.tachidesk.database.table.CategoryTable
import ir.armor.tachidesk.database.table.ChapterTable import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.ExtensionsTable import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.SchemaUtils
@@ -11,18 +18,24 @@ import org.jetbrains.exposed.sql.transactions.transaction
object DBMangaer { object DBMangaer {
val db by lazy { val db by lazy {
Database.connect("jdbc:sqlite:${Config.dataRoot}/database.db", "org.sqlite.JDBC") Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
} }
} }
fun makeDataBaseTables() { fun makeDataBaseTables() {
// mention db object to connect // must mention db object so the lazy block executes
DBMangaer.db val db = DBMangaer.db
db.useNestedTransactions = true
transaction { transaction {
SchemaUtils.create(ExtensionsTable) SchemaUtils.createMissingTablesAndColumns(
SchemaUtils.create(SourceTable) ExtensionTable,
SchemaUtils.create(MangaTable) SourceTable,
SchemaUtils.create(ChapterTable) MangaTable,
ChapterTable,
PageTable,
CategoryTable,
CategoryMangaTable,
)
} }
} }
@@ -0,0 +1,12 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class CategoryDataClass(
val id: Int,
val order: Int,
val name: String,
val isLanding: Boolean
)
@@ -1,11 +1,16 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class ChapterDataClass( data class ChapterDataClass(
val id: Int, val id: Int,
val url: String, val url: String,
val name: String, val name: String,
val date_upload: Long, val date_upload: Long,
val chapter_number: Float, val chapter_number: Float,
val scanlator: String?, val scanlator: String?,
val mangaId: Int, val mangaId: Int,
) val pageCount: Int? = null,
)
@@ -1,14 +1,18 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class ExtensionDataClass( data class ExtensionDataClass(
val name: String, val name: String,
val pkgName: String, val pkgName: String,
val versionName: String, val versionName: String,
val versionCode: Int, val versionCode: Int,
val lang: String, val lang: String,
val isNsfw: Boolean, val isNsfw: Boolean,
val apkName: String, val apkName: String,
val iconUrl: String, val iconUrl: String,
val installed: Boolean, val installed: Boolean,
val classFQName: String, val classFQName: String,
) )
@@ -1,20 +1,30 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.table.MangaStatus import ir.armor.tachidesk.database.table.MangaStatus
data class MangaDataClass( data class MangaDataClass(
val id: Int, val id: Int,
val sourceId: Long, val sourceId: Long,
val url: String, val url: String,
val title: String, val title: String,
val thumbnail_url: String? = null, val thumbnailUrl: String? = null,
val initialized: Boolean = false, val initialized: Boolean = false,
val artist: String? = null, val artist: String? = null,
val author: String? = null, val author: String? = null,
val description: String? = null, val description: String? = null,
val genre: String? = null, val genre: String? = null,
val status: String = MangaStatus.UNKNOWN.name val status: String = MangaStatus.UNKNOWN.name,
) val inLibrary: Boolean = false
)
data class PagedMangaListDataClass(
val mangaList: List<MangaDataClass>,
val hasNextPage: Boolean
)
@@ -1,6 +1,10 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class PageDataClass( data class PageDataClass(
val index: Int, val index: Int,
var imageUrl: String, var imageUrl: String,
) )
@@ -1,9 +1,13 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class SourceDataClass( data class SourceDataClass(
val id: String, val id: String,
val name: String, val name: String,
val lang: String, val lang: String,
val iconUrl: String, val iconUrl: String,
val supportsLatest: Boolean val supportsLatest: Boolean
) )
@@ -1,21 +1,25 @@
package ir.armor.tachidesk.database.entity package ir.armor.tachidesk.database.entity
import ir.armor.tachidesk.database.table.ExtensionsTable /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.table.ExtensionTable
import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) { class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<ExtensionEntity>(ExtensionsTable) companion object : IntEntityClass<ExtensionEntity>(ExtensionTable)
var name by ExtensionsTable.name var name by ExtensionTable.name
var pkgName by ExtensionsTable.pkgName var pkgName by ExtensionTable.pkgName
var versionName by ExtensionsTable.versionName var versionName by ExtensionTable.versionName
var versionCode by ExtensionsTable.versionCode var versionCode by ExtensionTable.versionCode
var lang by ExtensionsTable.lang var lang by ExtensionTable.lang
var isNsfw by ExtensionsTable.isNsfw var isNsfw by ExtensionTable.isNsfw
var apkName by ExtensionsTable.apkName var apkName by ExtensionTable.apkName
var iconUrl by ExtensionsTable.iconUrl var iconUrl by ExtensionTable.iconUrl
var installed by ExtensionsTable.installed var installed by ExtensionTable.installed
var classFQName by ExtensionsTable.classFQName var classFQName by ExtensionTable.classFQName
} }
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.database.entity package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.IntEntityClass
@@ -20,4 +24,4 @@ class MangaEntity(id: EntityID<Int>) : IntEntity(id) {
var thumbnail_url by MangaTable.thumbnail_url var thumbnail_url by MangaTable.thumbnail_url
var sourceReference by MangaEntity referencedOn MangaTable.sourceReference var sourceReference by MangaEntity referencedOn MangaTable.sourceReference
} }
@@ -1,7 +1,12 @@
package ir.armor.tachidesk.database.entity package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.table.SourceTable import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.dao.* import org.jetbrains.exposed.dao.EntityClass
import org.jetbrains.exposed.dao.LongEntity
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
class SourceEntity(id: EntityID<Long>) : LongEntity(id) { class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
@@ -13,4 +18,4 @@ class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
var extension by ExtensionEntity referencedOn SourceTable.extension var extension by ExtensionEntity referencedOn SourceTable.extension
var partOfFactorySource by SourceTable.partOfFactorySource var partOfFactorySource by SourceTable.partOfFactorySource
var positionInFactorySource by SourceTable.positionInFactorySource var positionInFactorySource by SourceTable.positionInFactorySource
} }
@@ -0,0 +1,12 @@
package ir.armor.tachidesk.database.table
import org.jetbrains.exposed.dao.id.IntIdTable
/* 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/. */
object CategoryMangaTable : IntIdTable() {
val category = reference("category", CategoryTable)
val manga = reference("manga", MangaTable)
}
@@ -0,0 +1,22 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow
object CategoryTable : IntIdTable() {
val name = varchar("name", 64)
val isLanding = bool("is_landing").default(false)
val order = integer("order").default(0)
}
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
categoryEntry[CategoryTable.id].value,
categoryEntry[CategoryTable.order],
categoryEntry[CategoryTable.name],
categoryEntry[CategoryTable.isLanding],
)
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
import eu.kanade.tachiyomi.source.model.SManga /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
object ChapterTable : IntIdTable() { object ChapterTable : IntIdTable() {
@@ -8,7 +11,7 @@ object ChapterTable : IntIdTable() {
val name = varchar("name", 512) val name = varchar("name", 512)
val date_upload = long("date_upload").default(0) val date_upload = long("date_upload").default(0)
val chapter_number = float("chapter_number").default(-1f) val chapter_number = float("chapter_number").default(-1f)
val scanlator = varchar("scanlator",128).nullable() val scanlator = varchar("scanlator", 128).nullable()
val manga = reference("manga", MangaTable) val manga = reference("manga", MangaTable)
} }
@@ -1,9 +1,12 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
object ExtensionTable : IntIdTable() {
object ExtensionsTable : IntIdTable() {
val name = varchar("name", 128) val name = varchar("name", 128)
val pkgName = varchar("pkg_name", 128) val pkgName = varchar("pkg_name", 128)
val versionName = varchar("version_name", 16) val versionName = varchar("version_name", 16)
@@ -15,4 +18,4 @@ object ExtensionsTable : IntIdTable() {
val installed = bool("installed").default(false) val installed = bool("installed").default(false)
val classFQName = varchar("class_name", 256).default("") // fully qualified name val classFQName = varchar("class_name", 256).default("") // fully qualified name
} }
@@ -1,7 +1,14 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.util.proxyThumbnailUrl
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow
object MangaTable : IntIdTable() { object MangaTable : IntIdTable() {
val url = varchar("url", 2048) val url = varchar("url", 2048)
@@ -17,10 +24,32 @@ object MangaTable : IntIdTable() {
val status = integer("status").default(SManga.UNKNOWN) val status = integer("status").default(SManga.UNKNOWN)
val thumbnail_url = varchar("thumbnail_url", 2048).nullable() val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
val inLibrary = bool("in_library").default(false)
val defaultCategory = bool("default_category").default(true)
// source is used by some ancestor of IntIdTable // source is used by some ancestor of IntIdTable
val sourceReference = reference("source", SourceTable) val sourceReference = reference("source", SourceTable)
} }
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
MangaDataClass(
mangaEntry[MangaTable.id].value,
mangaEntry[sourceReference].value,
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
proxyThumbnailUrl(mangaEntry[MangaTable.id].value),
mangaEntry[MangaTable.initialized],
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary]
)
enum class MangaStatus(val status: Int) { enum class MangaStatus(val status: Int) {
UNKNOWN(0), UNKNOWN(0),
ONGOING(1), ONGOING(1),
@@ -30,4 +59,4 @@ enum class MangaStatus(val status: Int) {
companion object { companion object {
fun valueOf(value: Int): MangaStatus = values().find { it.status == value } ?: UNKNOWN fun valueOf(value: Int): MangaStatus = values().find { it.status == value } ?: UNKNOWN
} }
} }
@@ -0,0 +1,15 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable
object PageTable : IntIdTable() {
val index = integer("index")
val url = varchar("url", 2048)
val imageUrl = varchar("imageUrl", 2048).nullable()
val chapter = reference("chapter", ChapterTable)
}
@@ -1,12 +1,16 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IdTable import org.jetbrains.exposed.dao.id.IdTable
object SourceTable : IdTable<Long>() { object SourceTable : IdTable<Long>() {
override val id = long("id").entityId() override val id = long("id").entityId()
val name= varchar("name", 128) val name = varchar("name", 128)
val lang = varchar("lang", 10) val lang = varchar("lang", 10)
val extension = reference("extension", ExtensionsTable) val extension = reference("extension", ExtensionTable)
val partOfFactorySource = bool("part_of_factory_source").default(false) val partOfFactorySource = bool("part_of_factory_source").default(false)
val positionInFactorySource = integer("position_in_factory_source").nullable() val positionInFactorySource = integer("position_in_factory_source").nullable()
} }
@@ -0,0 +1,66 @@
package ir.armor.tachidesk.util
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
import ir.armor.tachidesk.database.table.CategoryMangaTable
import ir.armor.tachidesk.database.table.CategoryTable
import ir.armor.tachidesk.database.table.toDataClass
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
/* 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/. */
fun createCategory(name: String) {
transaction {
val count = CategoryTable.selectAll().count()
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
CategoryTable.insert {
it[CategoryTable.name] = name
it[CategoryTable.order] = count.toInt() + 1
}
}
}
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
transaction {
CategoryTable.update({ CategoryTable.id eq categoryId }) {
if (name != null) it[CategoryTable.name] = name
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
}
}
}
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
transaction {
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
categories.add(to - 1, categories.removeAt(from - 1))
categories.forEachIndexed { index, cat ->
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
it[CategoryTable.order] = index + 1
}
}
}
}
fun removeCategory(categoryId: Int) {
transaction {
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
}
}
fun getCategoryList(): List<CategoryDataClass> {
return transaction {
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
}
}
}
@@ -0,0 +1,61 @@
package ir.armor.tachidesk.util
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.CategoryMangaTable
import ir.armor.tachidesk.database.table.CategoryTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.toDataClass
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
/* 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/. */
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
transaction {
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
CategoryMangaTable.insert {
it[CategoryMangaTable.category] = categoryId
it[CategoryMangaTable.manga] = mangaId
}
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.defaultCategory] = false
}
}
}
}
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
transaction {
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.defaultCategory] = true
}
}
}
}
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
return transaction {
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
MangaTable.toDataClass(it)
}
}
}
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
return transaction {
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
}
}
}
@@ -1,28 +1,30 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
import eu.kanade.tachiyomi.source.model.Page /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.database.dataclass.ChapterDataClass import ir.armor.tachidesk.database.dataclass.ChapterDataClass
import ir.armor.tachidesk.database.dataclass.PageDataClass
import ir.armor.tachidesk.database.entity.MangaEntity
import ir.armor.tachidesk.database.table.ChapterTable import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
fun getChapterList(mangaId: Int): List<ChapterDataClass> { fun getChapterList(mangaId: Int): List<ChapterDataClass> {
val mangaDetails = getManga(mangaId) val mangaDetails = getManga(mangaId)
val source = getHttpSource(mangaDetails.sourceId) val source = getHttpSource(mangaDetails.sourceId)
val chapterList = source.fetchChapterList( val chapterList = source.fetchChapterList(
SManga.create().apply { SManga.create().apply {
title = mangaDetails.title title = mangaDetails.title
url = mangaDetails.url url = mangaDetails.url
} }
).toBlocking().first() ).toBlocking().first()
return transaction { return transaction {
@@ -41,47 +43,59 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
} }
} }
return@transaction chapterList.map { return@transaction chapterList.map {
ChapterDataClass( ChapterDataClass(
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value, ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
it.url, it.url,
it.name, it.name,
it.date_upload, it.date_upload,
it.chapter_number, it.chapter_number,
it.scanlator, it.scanlator,
mangaId mangaId
) )
} }
} }
} }
fun getPages(chapterId: Int, mangaId: Int): List<PageDataClass> { fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
return transaction { return transaction {
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value) val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val pagesList = source.fetchPageList( val pageList = source.fetchPageList(
SChapter.create().apply { SChapter.create().apply {
url = chapterEntry[ChapterTable.url] url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name] name = chapterEntry[ChapterTable.name]
} }
).toBlocking().first() ).toBlocking().first()
return@transaction pagesList.map { val chapter = ChapterDataClass(
PageDataClass( chapterEntry[ChapterTable.id].value,
it.index, chapterEntry[ChapterTable.url],
getTrueImageUrl(it,source) chapterEntry[ChapterTable.name],
) chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator],
mangaId,
pageList.count()
)
pageList.forEach { page ->
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
if (pageEntry == null) {
transaction {
PageTable.insert {
it[index] = page.index
it[url] = page.url
it[imageUrl] = page.imageUrl
it[this.chapter] = chapterId
}
}
}
} }
return@transaction chapter
} }
}
fun getTrueImageUrl(page: Page, source: HttpSource): String {
return if ( page.imageUrl == null){
source.fetchImageUrl(page).toBlocking().first()!!
} else page.imageUrl!!
} }
@@ -1,35 +1,42 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
/* 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.googlecode.dex2jar.tools.Dex2jarCmd import com.googlecode.dex2jar.tools.Dex2jarCmd
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.APKExtractor import ir.armor.tachidesk.APKExtractor
import ir.armor.tachidesk.Config import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.table.ExtensionsTable import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.SourceTable import ir.armor.tachidesk.database.table.SourceTable
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.Request import okhttp3.Request
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.InputStream
import java.net.URL import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
fun installAPK(apkName: String): Int { fun installAPK(apkName: String): Int {
val extensionRecord = getExtensionList(true).first { it.apkName == apkName } val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
val fileNameWithoutType = apkName.substringBefore(".apk") val fileNameWithoutType = apkName.substringBefore(".apk")
val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType" val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
// check if we don't have the dex file already downloaded // check if we don't have the dex file already downloaded
val dexPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar" val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
if (!File(dexPath).exists()) { if (!File(jarPath).exists()) {
runBlocking { runBlocking {
val api = ExtensionGithubApi() val api = ExtensionGithubApi()
val apkToDownload = api.getApkUrl(extensionRecord) val apkToDownload = api.getApkUrl(extensionRecord)
@@ -41,7 +48,6 @@ fun installAPK(apkName: String): Int {
// download apk file // download apk file
downloadAPKFile(apkToDownload, apkFilePath) downloadAPKFile(apkToDownload, apkFilePath)
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath) val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
println(className) println(className)
// dex -> jar // dex -> jar
@@ -57,10 +63,10 @@ fun installAPK(apkName: String): Int {
val instance = classToLoad.newInstance() val instance = classToLoad.newInstance()
val extensionId = transaction { val extensionId = transaction {
return@transaction ExtensionsTable.select { ExtensionsTable.name eq extensionRecord.name }.first()[ExtensionsTable.id] return@transaction ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
} }
if (instance is HttpSource) {// single source if (instance is HttpSource) { // single source
val httpSource = instance as HttpSource val httpSource = instance as HttpSource
transaction { transaction {
// SourceEntity.new { // SourceEntity.new {
@@ -80,7 +86,6 @@ fun installAPK(apkName: String): Int {
// println(httpSource.name) // println(httpSource.name)
// println() // println()
} }
} else { // multi source } else { // multi source
val sourceFactory = instance as SourceFactory val sourceFactory = instance as SourceFactory
transaction { transaction {
@@ -105,12 +110,11 @@ fun installAPK(apkName: String): Int {
// update extension info // update extension info
transaction { transaction {
ExtensionsTable.update({ ExtensionsTable.name eq extensionRecord.name }) { ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
it[installed] = true it[installed] = true
it[classFQName] = className it[classFQName] = className
} }
} }
} }
return 201 // we downloaded successfully return 201 // we downloaded successfully
} else { } else {
@@ -122,10 +126,47 @@ val networkHelper: NetworkHelper by injectLazy()
private fun downloadAPKFile(url: String, apkPath: String) { private fun downloadAPKFile(url: String, apkPath: String) {
val request = Request.Builder().url(url).build() val request = Request.Builder().url(url).build()
val response = networkHelper.client.newCall(request).execute() val response = networkHelper.client.newCall(request).execute()
val downloadedFile = File(apkPath) val downloadedFile = File(apkPath)
val sink = downloadedFile.sink().buffer() val sink = downloadedFile.sink().buffer()
sink.writeAll(response.body!!.source()) sink.writeAll(response.body!!.source())
sink.close() sink.close()
} }
fun removeExtension(pkgName: String) {
val extensionRecord = getExtensionList(true).first { it.apkName == pkgName }
val fileNameWithoutType = pkgName.substringBefore(".apk")
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
transaction {
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
it[ExtensionTable.installed] = false
}
}
if (File(jarPath).exists()) {
File(jarPath).delete()
}
}
val network: NetworkHelper by injectLazy()
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
val saveDir = "${applicationDirs.extensionsRoot}/icon"
val fileName = apkName
return getCachedResponse(saveDir, fileName) {
network.client.newCall(
GET(iconUrl)
).execute()
}
}
fun getExtensionIconUrl(apkName: String): String {
return "/api/v1/extension/icon/$apkName"
}
@@ -1,11 +1,14 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
import ir.armor.tachidesk.database.table.ExtensionsTable import ir.armor.tachidesk.database.table.ExtensionTable
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
@@ -18,7 +21,7 @@ private object Data {
private fun extensionDatabaseIsEmtpy(): Boolean { private fun extensionDatabaseIsEmtpy(): Boolean {
return transaction { return transaction {
return@transaction ExtensionsTable.selectAll().count() == 0L return@transaction ExtensionTable.selectAll().count() == 0L
} }
} }
@@ -33,10 +36,10 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
foundExtensions = api.findExtensions() foundExtensions = api.findExtensions()
transaction { transaction {
foundExtensions.forEach { foundExtension -> foundExtensions.forEach { foundExtension ->
val extensionRecord = ExtensionsTable.select { ExtensionsTable.name eq foundExtension.name }.firstOrNull() val extensionRecord = ExtensionTable.select { ExtensionTable.name eq foundExtension.name }.firstOrNull()
if (extensionRecord != null) { if (extensionRecord != null) {
// update the record // update the record
ExtensionsTable.update({ ExtensionsTable.name eq foundExtension.name }) { ExtensionTable.update({ ExtensionTable.name eq foundExtension.name }) {
it[name] = foundExtension.name it[name] = foundExtension.name
it[pkgName] = foundExtension.pkgName it[pkgName] = foundExtension.pkgName
it[versionName] = foundExtension.versionName it[versionName] = foundExtension.versionName
@@ -48,7 +51,7 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
} }
} else { } else {
// insert new record // insert new record
ExtensionsTable.insert { ExtensionTable.insert {
it[name] = foundExtension.name it[name] = foundExtension.name
it[pkgName] = foundExtension.pkgName it[pkgName] = foundExtension.pkgName
it[versionName] = foundExtension.versionName it[versionName] = foundExtension.versionName
@@ -62,23 +65,24 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
} }
} }
} }
} else {
println("used cached extension list")
} }
return transaction { return transaction {
return@transaction ExtensionsTable.selectAll().map { return@transaction ExtensionTable.selectAll().map {
ExtensionDataClass( ExtensionDataClass(
it[ExtensionsTable.name], it[ExtensionTable.name],
it[ExtensionsTable.pkgName], it[ExtensionTable.pkgName],
it[ExtensionsTable.versionName], it[ExtensionTable.versionName],
it[ExtensionsTable.versionCode], it[ExtensionTable.versionCode],
it[ExtensionsTable.lang], it[ExtensionTable.lang],
it[ExtensionsTable.isNsfw], it[ExtensionTable.isNsfw],
it[ExtensionsTable.apkName], it[ExtensionTable.apkName],
it[ExtensionsTable.iconUrl], getExtensionIconUrl(it[ExtensionTable.apkName]),
it[ExtensionsTable.installed], it[ExtensionTable.installed],
it[ExtensionsTable.classFQName] it[ExtensionTable.classFQName]
) )
} }
} }
} }
@@ -0,0 +1,82 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import okhttp3.Response
import okio.BufferedSource
import okio.buffer
import okio.sink
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Paths
// fun writeStream(fileStream: InputStream, path: String) {
// Files.newOutputStream(Paths.get(path)).use { os ->
// val buffer = ByteArray(128 * 1024)
// var len: Int
// while (fileStream.read(buffer).also { len = it } > 0) {
// os.write(buffer, 0, len)
// }
// }
// }
fun pathToInputStream(path: String): InputStream {
return BufferedInputStream(FileInputStream(path))
}
fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
File(directoryPath).listFiles().forEach { file ->
if (file.name.startsWith(fileName))
return "$directoryPath/${file.name}"
}
return null
}
/**
* Saves the given source to an output stream and closes both resources.
*
* @param stream the stream where the source is copied.
*/
private fun BufferedSource.saveTo(stream: OutputStream) {
use { input ->
stream.sink().buffer().use {
it.writeAll(input)
it.flush()
}
}
}
fun getCachedResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> {
val cachedFile = findFileNameStartingWith(saveDir, fileName)
val filePath = "$saveDir/$fileName"
if (cachedFile != null) {
val fileType = cachedFile.substringAfter(filePath)
return Pair(
pathToInputStream(cachedFile),
"image/$fileType"
)
}
val response = fetcher()
if (response.code == 200) {
val contentType = response.headers["content-type"]!!
val fullPath = filePath + "." + contentType.substringAfter("image/")
Files.newOutputStream(Paths.get(fullPath)).use { os ->
response.body!!.source().saveTo(os)
}
return Pair(
pathToInputStream(fullPath),
contentType
)
} else {
throw Exception("request error! ${response.code}")
}
}
@@ -0,0 +1,48 @@
package ir.armor.tachidesk.util
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.CategoryMangaTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.toDataClass
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
/* 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/. */
fun addMangaToLibrary(mangaId: Int) {
val manga = getManga(mangaId)
if (!manga.inLibrary) {
transaction {
MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = true
}
}
}
}
fun removeMangaFromLibrary(mangaId: Int) {
val manga = getManga(mangaId)
if (manga.inLibrary) {
transaction {
MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = false
it[defaultCategory] = true
}
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
}
}
}
fun getLibraryMangas(): List<MangaDataClass> {
return transaction {
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
MangaTable.toDataClass(it)
}
}
}
@@ -1,78 +1,103 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.dataclass.MangaDataClass import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.MangaStatus import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import java.io.InputStream
fun getManga(mangaId: Int): MangaDataClass { fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
return transaction { var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
return@transaction if (mangaEntry[MangaTable.initialized]) { return if (mangaEntry[MangaTable.initialized]) {
MangaDataClass( MangaDataClass(
mangaId, mangaId,
mangaEntry[MangaTable.sourceReference].value, mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url], mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title], mangaEntry[MangaTable.title],
mangaEntry[MangaTable.thumbnail_url], if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
true, true,
mangaEntry[MangaTable.artist], mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author], mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description], mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre], mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
) mangaEntry[MangaTable.inLibrary]
} else { // initialize manga )
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value) } else { // initialize manga
val fetchedManga = source.fetchMangaDetails( val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
SManga.create().apply { val fetchedManga = source.fetchMangaDetails(
url = mangaEntry[MangaTable.url] SManga.create().apply {
title = mangaEntry[MangaTable.title] url = mangaEntry[MangaTable.url]
} title = mangaEntry[MangaTable.title]
).toBlocking().first()
// update database
MangaTable.update({ MangaTable.id eq mangaId }) {
// it[url] = fetchedManga.url
// it[title] = fetchedManga.title
it[initialized] = true
it[artist] = fetchedManga.artist
it[author] = fetchedManga.author
it[description] = fetchedManga.description
it[genre] = fetchedManga.genre
it[status] = fetchedManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
it[thumbnail_url] = fetchedManga.thumbnail_url
} }
).toBlocking().first()
mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! transaction {
MangaTable.update({ MangaTable.id eq mangaId }) {
MangaDataClass( it[MangaTable.initialized] = true
mangaId,
mangaEntry[MangaTable.sourceReference].value,
it[MangaTable.artist] = fetchedManga.artist
mangaEntry[MangaTable.url], it[MangaTable.author] = fetchedManga.author
mangaEntry[MangaTable.title], it[MangaTable.description] = fetchedManga.description
mangaEntry[MangaTable.thumbnail_url], it[MangaTable.genre] = fetchedManga.genre
it[MangaTable.status] = fetchedManga.status
true, if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
mangaEntry[MangaTable.artist], }
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
)
} }
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
true,
fetchedManga.artist,
fetchedManga.author,
fetchedManga.description,
fetchedManga.genre,
MangaStatus.valueOf(fetchedManga.status).name,
false
)
} }
} }
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString()
return getCachedResponse(saveDir, fileName) {
val sourceId = mangaEntry[MangaTable.sourceReference].value
val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
}
source.client.newCall(
GET(thumbnailUrl, source.headers)
).execute()
}
}
@@ -0,0 +1,95 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.FormBody
import okhttp3.OkHttpClient
import java.net.URLEncoder
class MangaDexHelper(private val mangaDexSource: HttpSource) {
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
private fun clientBuilder(
r18Toggle: Int,
okHttpClient: OkHttpClient = mangaDexSource.network.client
): OkHttpClient = okHttpClient.newBuilder()
.addNetworkInterceptor { chain ->
val originalCookies = chain.request().header("Cookie") ?: ""
val newReq = chain
.request()
.newBuilder()
.header("Cookie", "$originalCookies; ${cookiesHeader(r18Toggle)}")
.build()
chain.proceed(newReq)
}.build()
private fun cookiesHeader(r18Toggle: Int): String {
val cookies = mutableMapOf<String, String>()
cookies["mangadex_h_toggle"] = r18Toggle.toString()
return buildCookies(cookies)
}
private fun buildCookies(cookies: Map<String, String>) =
cookies.entries.joinToString(separator = "; ", postfix = ";") {
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
}
// fun isLogged(): Boolean {
// val httpUrl = mangaDexSource.baseUrl.toHttpUrlOrNull()!!
// return network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
// }
fun login(username: String, password: String, twoFactorCode: String = ""): Boolean {
val formBody = FormBody.Builder()
.add("login_username", username)
.add("login_password", password)
.add("no_js", "1")
.add("remember_me", "1")
twoFactorCode.let {
formBody.add("two_factor", it)
}
val response = clientBuilder().newCall(
POST(
"${mangaDexSource.baseUrl}/ajax/actions.ajax.php?function=login",
mangaDexSource.headers,
formBody.build()
)
).execute()
return response.body!!.string().isEmpty()
}
//
// fun logout(): Boolean {
// return withContext(Dispatchers.IO) {
// // https://mangadex.org/ajax/actions.ajax.php?function=logout
// val httpUrl = baseUrl.toHttpUrlOrNull()!!
// val listOfDexCookies = network.cookieManager.get(httpUrl)
// val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
// val token = cookie?.value
// if (token.isNullOrEmpty()) {
// return@withContext true
// }
// val result = clientBuilder().newCall(
// POSTWithCookie(
// "$baseUrl/ajax/actions.ajax.php?function=logout",
// REMEMBER_ME,
// token,
// headers
// )
// ).execute()
// val resultStr = result.body!!.string()
// if (resultStr.contains("success", true)) {
// network.cookieManager.remove(httpUrl)
// return@withContext true
// }
//
// false
// }
// }
}
@@ -1,15 +1,23 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.MangasPage
import ir.armor.tachidesk.database.dataclass.MangaDataClass import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
import ir.armor.tachidesk.database.table.MangaStatus import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<MangaDataClass> { fun proxyThumbnailUrl(mangaId: Int): String {
return "/api/v1/manga/$mangaId/thumbnail"
}
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId.toLong()) val source = getHttpSource(sourceId.toLong())
val mangasPage = if (popular) { val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).toBlocking().first() source.fetchPopularManga(pageNum).toBlocking().first()
@@ -19,11 +27,16 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
else else
throw Exception("Source $source doesn't support latest") throw Exception("Source $source doesn't support latest")
} }
return transaction { return mangasPage.processEntries(sourceId)
}
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
val mangasPage = this
val mangaList = transaction {
return@transaction mangasPage.mangas.map { manga -> return@transaction mangasPage.mangas.map { manga ->
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull() var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
var mangaEntityId = if (mangaEntry == null) { // create manga entry if (mangaEntry == null) { // create manga entry
MangaTable.insertAndGetId { val mangaId = MangaTable.insertAndGetId {
it[url] = manga.url it[url] = manga.url
it[title] = manga.title it[title] = manga.title
@@ -32,21 +45,18 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
it[description] = manga.description it[description] = manga.description
it[genre] = manga.genre it[genre] = manga.genre
it[status] = manga.status it[status] = manga.status
it[thumbnail_url] = manga.genre it[thumbnail_url] = manga.thumbnail_url
it[sourceReference] = sourceId it[sourceReference] = sourceId
}.value }.value
} else {
mangaEntry[MangaTable.id].value
}
MangaDataClass( MangaDataClass(
mangaEntityId, mangaId,
sourceId.toLong(), sourceId,
manga.url, manga.url,
manga.title, manga.title,
manga.thumbnail_url, proxyThumbnailUrl(mangaId),
manga.initialized, manga.initialized,
@@ -54,8 +64,32 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
manga.author, manga.author,
manga.description, manga.description,
manga.genre, manga.genre,
MangaStatus.valueOf(manga.status).name, MangaStatus.valueOf(manga.status).name
) )
} else {
val mangaId = mangaEntry[MangaTable.id].value
MangaDataClass(
mangaId,
sourceId,
manga.url,
manga.title,
proxyThumbnailUrl(mangaId),
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary]
)
}
} }
} }
} return PagedMangaListDataClass(
mangaList,
mangasPage.hasNextPage
)
}
@@ -0,0 +1,76 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import java.io.File
import java.io.InputStream
fun getTrueImageUrl(page: Page, source: HttpSource): String {
if (page.imageUrl == null) {
page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!!
}
return page.imageUrl!!
}
fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
val tachiPage = Page(
pageEntry[PageTable.index],
pageEntry[PageTable.url],
pageEntry[PageTable.imageUrl]
)
if (pageEntry[PageTable.imageUrl] == null) {
transaction {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
it[imageUrl] = getTrueImageUrl(tachiPage, source)
}
}
}
val saveDir = getChapterDir(mangaId, chapterId)
File(saveDir).mkdirs()
val fileName = index.toString()
return getCachedResponse(saveDir, fileName) {
source.fetchImage(tachiPage).toBlocking().first()
}
}
fun getChapterDir(mangaId: Int, chapterId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val sourceId = mangaEntry[MangaTable.sourceReference].value
val source = getHttpSource(sourceId)
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
val chapterDir = when {
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
else -> chapterEntry[ChapterTable.name]
}
val mangaTitle = mangaEntry[MangaTable.title]
val sourceName = source.toString()
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
// make sure dirs exist
File(mangaDir).mkdirs()
return mangaDir
}
@@ -0,0 +1,62 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
fun sourceFilters(sourceId: Long) {
val source = getHttpSource(sourceId)
// source.getFilterList().toItems()
}
fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
val source = getHttpSource(sourceId)
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first()
return searchManga.processEntries(sourceId)
}
fun sourceGlobalSearch(searchTerm: String) {
}
data class FilterWrapper(
val type: String,
val filter: Any
)
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
// return mapNotNull { filter ->
// when (filter) {
// is Filter.Header -> FilterWrapper("Header",filter)
// is Filter.Separator -> FilterWrapper("Separator",filter)
// is Filter.CheckBox -> FilterWrapper("CheckBox",filter)
// is Filter.TriState -> FilterWrapper("TriState",filter)
// is Filter.Text -> FilterWrapper("Text",filter)
// is Filter.Select<*> -> FilterWrapper("Select",filter)
// is Filter.Group<*> -> {
// val group = GroupItem(filter)
// val subItems = filter.state.mapNotNull {
// when (it) {
// is Filter.CheckBox -> FilterWrapper("CheckBox",filter)
// is Filter.TriState -> FilterWrapper("TriState",filter)
// is Filter.Text -> FilterWrapper("Text",filter)
// is Filter.Select<*> -> FilterWrapper("Select",filter)
// else -> null
// } as? ISectionable<*, *>
// }
// subItems.forEach { it.header = group }
// group.subItems = subItems
// group
// }
// is Filter.Sort -> {
// val group = SortGroup(filter)
// val subItems = filter.values.map {
// SortItem(it, group)
// }
// group.subItems = subItems
// group
// }
// }
// }
// }
@@ -1,19 +1,23 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.Config import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.dataclass.SourceDataClass import ir.armor.tachidesk.database.dataclass.SourceDataClass
import ir.armor.tachidesk.database.entity.ExtensionEntity import ir.armor.tachidesk.database.entity.ExtensionEntity
import ir.armor.tachidesk.database.entity.SourceEntity import ir.armor.tachidesk.database.entity.SourceEntity
import ir.armor.tachidesk.database.table.ExtensionsTable import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.SourceTable import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import java.net.URL import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
import java.util.* import java.util.Locale
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>() private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
private val extensionCache = mutableListOf<Pair<String, Any>>() private val extensionCache = mutableListOf<Pair<String, Any>>()
@@ -32,23 +36,23 @@ fun getHttpSource(sourceId: Long): HttpSource {
val apkName = extensionRecord.apkName val apkName = extensionRecord.apkName
val className = extensionRecord.classFQName val className = extensionRecord.classFQName
val jarName = apkName.substringBefore(".apk") + ".jar" val jarName = apkName.substringBefore(".apk") + ".jar"
val jarPath = "${Config.extensionsRoot}/$jarName" val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
println(jarName) println(jarName)
val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath } val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath }
var usedCached = false var usedCached = false
val instance = val instance =
if (cachedExtensionPair != null) { if (cachedExtensionPair != null) {
usedCached = true usedCached = true
println("Used cached Extension") println("Used cached Extension")
cachedExtensionPair.second cachedExtensionPair.second
} else { } else {
println("No Extension cache") println("No Extension cache")
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader) val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
val classToLoad = Class.forName(className, true, child) val classToLoad = Class.forName(className, true, child)
classToLoad.newInstance() classToLoad.newInstance()
} }
if (sourceRecord.partOfFactorySource) { if (sourceRecord.partOfFactorySource) {
return@transaction if (usedCached) { return@transaction if (usedCached) {
(instance as List<HttpSource>)[sourceRecord.positionInFactorySource!!] (instance as List<HttpSource>)[sourceRecord.positionInFactorySource!!]
@@ -71,12 +75,26 @@ fun getSourceList(): List<SourceDataClass> {
return transaction { return transaction {
return@transaction SourceTable.selectAll().map { return@transaction SourceTable.selectAll().map {
SourceDataClass( SourceDataClass(
it[SourceTable.id].value.toString(), it[SourceTable.id].value.toString(),
it[SourceTable.name], it[SourceTable.name],
Locale(it[SourceTable.lang]).getDisplayLanguage(Locale(it[SourceTable.lang])), it[SourceTable.lang],
ExtensionsTable.select { ExtensionsTable.id eq it[SourceTable.extension] }.first()[ExtensionsTable.iconUrl], getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
getHttpSource(it[SourceTable.id].value).supportsLatest getHttpSource(it[SourceTable.id].value).supportsLatest
) )
} }
} }
} }
fun getSource(sourceId: Long): SourceDataClass {
return transaction {
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
return@transaction SourceDataClass(
source[SourceTable.id].value.toString(),
source[SourceTable.name],
Locale(source[SourceTable.lang]).getDisplayLanguage(Locale(source[SourceTable.lang])),
ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl],
getHttpSource(source[SourceTable.id].value).supportsLatest
)
}
}
@@ -1,14 +1,67 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
import ir.armor.tachidesk.Config /* This Source Code Form is subject to the terms of the Mozilla Public
import ir.armor.tachidesk.database.makeDataBaseTables * License, v. 2.0. If a copy of the MPL was not distributed with this
import java.io.File * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
fun applicationSetup() { import dorkbox.systemTray.MenuItem
// make dirs we need import dorkbox.systemTray.SystemTray
File(Config.dataRoot).mkdirs() import dorkbox.systemTray.SystemTray.TrayType
File(Config.extensionsRoot).mkdirs() import dorkbox.util.CacheUtil
import dorkbox.util.Desktop
import ir.armor.tachidesk.Main
import java.awt.event.ActionListener
import java.io.IOException
fun openInBrowser() {
try {
Desktop.browseURL("http://127.0.0.1:4567")
} catch (e1: IOException) {
e1.printStackTrace()
}
}
makeDataBaseTables() fun systemTray(): SystemTray? {
} try {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
SystemTray.DEBUG = true; // for test apps, we always want to run in debug mode
if (System.getProperty("os.name").startsWith("Windows"))
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
CacheUtil.clear()
val systemTray = SystemTray.get() ?: return null
val mainMenu = systemTray.menu
mainMenu.add(
MenuItem(
"Open Tachidesk",
ActionListener {
try {
Desktop.browseURL("http://127.0.0.1:4567")
} catch (e: IOException) {
e.printStackTrace()
}
}
)
)
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
// systemTray.setTooltip("Tachidesk")
systemTray.setImage(icon)
// systemTray.status = "No Mail"
systemTray.getMenu().add(
MenuItem("Quit") {
systemTray.shutdown()
System.exit(0)
}
)
return systemTray
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

@@ -0,0 +1,8 @@
# Server ip and port bindings
server.ip = 0.0.0.0
server.port = 4567
# Socks5 proxy
server.socksProxy = false
server.socksProxyHost = ""
server.socksProxyPort = ""
+2 -2
View File
@@ -4,11 +4,11 @@ plugins {
node { node {
workDir = file("${project.projectDir}/react/") workDir = file("${project.projectDir}/react/")
nodeModulesDir = file("${project.projectDir}/react/node_modules") nodeModulesDir = file("${project.projectDir}/react/")
} }
tasks.named("yarn_build") { tasks.named("yarn_build") {
dependsOn("yarn_install") dependsOn("yarn") // install node_moduels
} }
tasks.register<Copy>("copyBuild") { tasks.register<Copy>("copyBuild") {
-10
View File
@@ -1,10 +0,0 @@
{
"systemParams": "linux-x64-88",
"modulesFolders": [],
"flags": [],
"linkedModules": [],
"topLevelPatterns": [],
"lockfileEntries": {},
"files": [],
"artifacts": {}
}
+2
View File
@@ -13,5 +13,7 @@ module.exports = {
// Indent props with 4 spaces // Indent props with 4 spaces
'react/jsx-indent-props': ['error', 4], 'react/jsx-indent-props': ['error', 4],
'no-plusplus': ['error', { 'allowForLoopAfterthoughts': true }]
}, },
}; };
+1
View File
@@ -1,3 +1,4 @@
node_modules/ node_modules/
.eslintcache .eslintcache
.vscode .vscode
.env
+3 -1
View File
@@ -8,8 +8,10 @@
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"axios": "^0.21.1",
"fontsource-roboto": "^4.0.0", "fontsource-roboto": "^4.0.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-beautiful-dnd": "^13.0.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "4.0.1", "react-scripts": "4.0.1",
@@ -47,4 +49,4 @@
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"typescript": "^4.1.0" "typescript": "^4.1.0"
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

+3 -3
View File
@@ -7,9 +7,9 @@
<meta name="theme-color" content="#000000"/> <meta name="theme-color" content="#000000"/>
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="A manga reader that runs tachiyomi's extensions"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/> <link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png"/>
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>Tachidesk</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

+12 -6
View File
@@ -1,6 +1,6 @@
{ {
"short_name": "React App", "short_name": "Tachidesk",
"name": "Create React App Sample", "name": "Tachidesk",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
@@ -8,18 +8,24 @@
"type": "image/x-icon" "type": "image/x-icon"
}, },
{ {
"src": "logo192.png", "src": "favicon.png",
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
}, },
{ {
"src": "logo512.png", "src": "favicon.png",
"type": "image/png", "type": "image/png",
"sizes": "512x512" "sizes": "512x512"
},
{
"src": "favicon.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
} }
], ],
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"theme_color": "#000000", "theme_color": "#ff2323",
"background_color": "#ffffff" "background_color": "#ff2323"
} }
+99 -29
View File
@@ -1,43 +1,113 @@
import React from 'react'; /* 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 React, { useState } from 'react';
import { import {
BrowserRouter as Router, Route, Switch, BrowserRouter as Router, Redirect, Route, Switch,
} from 'react-router-dom'; } from 'react-router-dom';
import { Container } from '@material-ui/core';
import CssBaseline from '@material-ui/core/CssBaseline';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
import NavBar from './components/NavBar'; import NavBar from './components/NavBar';
import Home from './screens/Home';
import Sources from './screens/Sources'; import Sources from './screens/Sources';
import Extensions from './screens/Extensions'; import Extensions from './screens/Extensions';
import MangaList from './screens/MangaList'; import SourceMangas from './screens/SourceMangas';
import Manga from './screens/Manga'; import Manga from './screens/Manga';
import Reader from './screens/Reader'; import Reader from './screens/Reader';
import Search from './screens/SearchSingle';
import NavbarContext from './context/NavbarContext';
import DarkTheme from './context/DarkTheme';
import Library from './screens/Library';
import Settings from './screens/Settings';
import Categories from './screens/settings/Categories';
import useLocalStorage from './util/useLocalStorage';
export default function App() { export default function App() {
const [title, setTitle] = useState<string>('Tachidesk');
const [action, setAction] = useState<any>(<div />);
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
const navBarContext = {
title, setTitle, action, setAction,
};
const darkThemeContext = { darkTheme, setDarkTheme };
const theme = React.useMemo(
() => createMuiTheme({
palette: {
type: darkTheme ? 'dark' : 'light',
},
overrides: {
MuiCssBaseline: {
'@global': {
'*::-webkit-scrollbar': {
width: '10px',
background: darkTheme ? '#222' : '#e1e1e1',
},
'*::-webkit-scrollbar-thumb': {
background: darkTheme ? '#111' : '#aaa',
borderRadius: '5px',
},
},
},
},
}),
[darkTheme],
);
return ( return (
<Router> <Router>
<NavBar /> <ThemeProvider theme={theme}>
<NavbarContext.Provider value={navBarContext}>
<Switch> <CssBaseline />
<Route path="/extensions"> <NavBar />
<Extensions /> <Container maxWidth={false} disableGutters>
</Route> <Switch>
<Route path="/sources/:sourceId/popular/"> <Route path="/sources/:sourceId/search/">
<MangaList popular /> <Search />
</Route> </Route>
<Route path="/sources/:sourceId/latest/"> <Route path="/extensions">
<MangaList popular={false} /> <Extensions />
</Route> </Route>
<Route path="/sources"> <Route path="/sources/:sourceId/popular/">
<Sources /> <SourceMangas popular />
</Route> </Route>
<Route path="/manga/:mangaId/chapter/:chapterId"> <Route path="/sources/:sourceId/latest/">
<Reader /> <SourceMangas popular={false} />
</Route> </Route>
<Route path="/manga/:id"> <Route path="/sources">
<Manga /> <Sources />
</Route> </Route>
<Route path="/"> <Route path="/manga/:mangaId/chapter/:chapterId">
<Home /> <Reader />
</Route> </Route>
</Switch> <Route path="/manga/:id">
<Manga />
</Route>
<Route path="/library">
<Library />
</Route>
<Route path="/settings/categories">
<Categories />
</Route>
<Route path="/settings">
<DarkTheme.Provider value={darkThemeContext}>
<Settings />
</DarkTheme.Provider>
</Route>
<Route
exact
path="/"
render={() => (
<Redirect to="/library" />
)}
/>
</Switch>
</Container>
</NavbarContext.Provider>
</ThemeProvider>
</Router> </Router>
); );
} }
@@ -0,0 +1,113 @@
/* 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 React, { useEffect, useState } from 'react';
import { makeStyles, createStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import Dialog from '@material-ui/core/Dialog';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormGroup from '@material-ui/core/FormGroup';
import client from '../util/client';
const useStyles = makeStyles(() => createStyles({
paper: {
maxHeight: 435,
width: '80%',
},
}));
interface IProps {
open: boolean
setOpen: (value: boolean) => void
mangaId: number
}
interface ICategoryInfo {
category: ICategory
selected: boolean
}
export default function CategorySelect(props: IProps) {
const classes = useStyles();
const { open, setOpen, mangaId } = props;
const [categoryInfos, setCategoryInfos] = useState<ICategoryInfo[]>([]);
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
useEffect(() => {
let tmpCategoryInfos: ICategoryInfo[] = [];
client.get('/api/v1/category/')
.then((response) => response.data)
.then((data: ICategory[]) => {
tmpCategoryInfos = data.map((category) => ({ category, selected: false }));
})
.then(() => {
client.get(`/api/v1/manga/${mangaId}/category/`)
.then((response) => response.data)
.then((data: ICategory[]) => {
data.forEach((category) => {
tmpCategoryInfos[category.order - 1].selected = true;
});
setCategoryInfos(tmpCategoryInfos);
});
});
}, [updateTriggerHolder, open]);
const handleCancel = () => {
setOpen(false);
};
const handleOk = () => {
setOpen(false);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, categoryId: number) => {
const { checked } = event.target as HTMLInputElement;
const method = checked ? client.get : client.delete;
method(`/api/v1/manga/${mangaId}/category/${categoryId}`)
.then(() => triggerUpdate());
};
return (
<Dialog
classes={classes}
maxWidth="xs"
open={open}
>
<DialogTitle>Set categories</DialogTitle>
<DialogContent dividers>
<FormGroup>
{categoryInfos.map((categoryInfo) => (
<FormControlLabel
control={(
<Checkbox
checked={categoryInfo.selected}
onChange={(e) => handleChange(e, categoryInfo.category.id)}
color="default"
/>
)}
label={categoryInfo.category.name}
/>
))}
</FormGroup>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel} color="primary">
Cancel
</Button>
<Button onClick={handleOk} color="primary">
Ok
</Button>
</DialogActions>
</Dialog>
);
}
@@ -1,3 +1,7 @@
/* 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 React from 'react'; import React from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
+36 -7
View File
@@ -1,3 +1,7 @@
/* 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 React, { useState } from 'react'; import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
@@ -5,6 +9,8 @@ import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar'; import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -34,24 +40,47 @@ const useStyles = makeStyles((theme) => ({
interface IProps { interface IProps {
extension: IExtension extension: IExtension
notifyInstall: () => void
} }
export default function ExtensionCard(props: IProps) { export default function ExtensionCard(props: IProps) {
const { const {
extension: { extension: {
name, lang, versionName, iconUrl, installed, apkName, name, lang, versionName, installed, apkName, iconUrl,
}, },
notifyInstall,
} = props; } = props;
const [installedState, setInstalledState] = useState<string>((installed ? 'installed' : 'install')); const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles(); const classes = useStyles();
const langPress = lang === 'all' ? 'All' : lang.toUpperCase(); const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
function install() { function install() {
setInstalledState('installing'); setInstalledState('installing');
fetch(`http://127.0.0.1:4567/api/v1/extension/install/${apkName}`).then(() => { client.get(`/api/v1/extension/install/${apkName}`)
setInstalledState('installed'); .then(() => {
}); setInstalledState('uninstall');
notifyInstall();
});
}
function uninstall() {
setInstalledState('uninstalling');
client.get(`/api/v1/extension/uninstall/${apkName}`)
.then(() => {
// setInstalledState('install');
notifyInstall();
});
}
function handleButtonClick() {
if (installedState === 'install') {
install();
} else {
uninstall();
}
} }
return ( return (
@@ -62,7 +91,7 @@ export default function ExtensionCard(props: IProps) {
variant="rounded" variant="rounded"
className={classes.icon} className={classes.icon}
alt={name} alt={name}
src={iconUrl} src={serverAddress + iconUrl}
/> />
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2"> <Typography variant="h5" component="h2">
@@ -76,7 +105,7 @@ export default function ExtensionCard(props: IProps) {
</div> </div>
</div> </div>
<Button variant="outlined" onClick={() => install()}>{installedState}</Button> <Button variant="outlined" onClick={() => handleButtonClick()}>{installedState}</Button>
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -0,0 +1,105 @@
/* 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 React, { useState } from 'react';
import { makeStyles, createStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import Dialog from '@material-ui/core/Dialog';
import Switch from '@material-ui/core/Switch';
import IconButton from '@material-ui/core/IconButton';
import FilterListIcon from '@material-ui/icons/FilterList';
import { List, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
import ListItem from '@material-ui/core/ListItem';
import { langCodeToName } from '../util/language';
const useStyles = makeStyles(() => createStyles({
paper: {
maxHeight: 435,
width: '80%',
},
}));
interface IProps {
shownLangs: string[]
setShownLangs: (arg0: string[]) => void
allLangs: string[]
}
export default function ExtensionLangSelect(props: IProps) {
const { shownLangs, setShownLangs, allLangs } = props;
// hold a copy and only sate state on parent when OK pressed, improves performance
const [mShownLangs, setMShownLangs] = useState(shownLangs);
const classes = useStyles();
const [open, setOpen] = useState<boolean>(false);
const handleCancel = () => {
setOpen(false);
};
const handleOk = () => {
setOpen(false);
setShownLangs(mShownLangs);
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, lang: string) => {
const { checked } = event.target as HTMLInputElement;
if (checked) {
setMShownLangs([...mShownLangs, lang]);
} else {
const clone = JSON.parse(JSON.stringify(mShownLangs));
clone.splice(clone.indexOf(lang), 1);
setMShownLangs(clone);
}
};
return (
<>
<IconButton
onClick={() => setOpen(true)}
aria-label="display more actions"
edge="end"
color="inherit"
>
<FilterListIcon />
</IconButton>
<Dialog
classes={classes}
maxWidth="xs"
open={open}
>
<DialogTitle>Enabled Languages</DialogTitle>
<DialogContent dividers style={{ padding: 0 }}>
<List>
{allLangs.map((lang) => (
<ListItem key={lang}>
<ListItemText primary={langCodeToName(lang)} />
<ListItemSecondaryAction>
<Switch
checked={mShownLangs.indexOf(lang) !== -1}
onChange={(e) => handleChange(e, lang)}
/>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel} color="primary">
Cancel
</Button>
<Button onClick={handleOk} color="primary">
Ok
</Button>
</DialogActions>
</Dialog>
</>
);
}
+30 -19
View File
@@ -1,3 +1,7 @@
/* 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 React from 'react'; import React from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
@@ -5,6 +9,8 @@ import CardActionArea from '@material-ui/core/CardActionArea';
import CardMedia from '@material-ui/core/CardMedia'; import CardMedia from '@material-ui/core/CardMedia';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Grid } from '@material-ui/core';
import useLocalStorage from '../util/useLocalStorage';
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
@@ -39,31 +45,36 @@ const useStyles = makeStyles({
interface IProps { interface IProps {
manga: IManga manga: IManga
} }
export default function MangaCard(props: IProps) { const MangaCard = React.forwardRef((props: IProps, ref) => {
const { const {
manga: { manga: {
id, title, thumbnailUrl, id, title, thumbnailUrl,
}, },
} = props; } = props;
const classes = useStyles(); const classes = useStyles();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
return ( return (
<Link to={`/manga/${id}/`}> <Grid item xs={6} sm={4} md={3} lg={2}>
<Card className={classes.root}> <Link to={`/manga/${id}/`}>
<CardActionArea> <Card className={classes.root} ref={ref}>
<div className={classes.wrapper}> <CardActionArea>
<CardMedia <div className={classes.wrapper}>
className={classes.image} <CardMedia
component="img" className={classes.image}
alt={title} component="img"
image={thumbnailUrl} alt={title}
title={title} image={serverAddress + thumbnailUrl}
/> title={title}
<div className={classes.gradient} /> />
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography> <div className={classes.gradient} />
</div> <Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
</CardActionArea> </div>
</Card> </CardActionArea>
</Link> </Card>
</Link>
</Grid>
); );
} });
export default MangaCard;
+59 -4
View File
@@ -1,17 +1,72 @@
import React from 'react'; /* 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 { Button, createStyles, makeStyles } from '@material-ui/core';
import React, { useState } from 'react';
import client from '../util/client';
import CategorySelect from './CategorySelect';
const useStyles = makeStyles(() => createStyles({
root: {
display: 'flex',
flexDirection: 'row-reverse',
'& button': {
marginLeft: 10,
},
},
}));
interface IProps{ interface IProps{
manga: IManga | undefined manga: IManga
} }
export default function MangaDetails(props: IProps) { export default function MangaDetails(props: IProps) {
const classes = useStyles();
const { manga } = props; const { manga } = props;
const [inLibrary, setInLibrary] = useState<string>(
manga.inLibrary ? 'In Library' : 'Not In Library',
);
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
function addToLibrary() {
setInLibrary('adding');
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('In Library');
});
}
function removeFromLibrary() {
setInLibrary('removing');
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('Not In Library');
});
}
function handleButtonClick() {
if (inLibrary === 'Not In Library') {
addToLibrary();
} else {
removeFromLibrary();
}
}
return ( return (
<> <div>
<h1> <h1>
{manga && manga.title} {manga && manga.title}
</h1> </h1>
</> <div className={classes.root}>
<Button variant="outlined" onClick={() => handleButtonClick()}>{inLibrary}</Button>
{inLibrary === 'In Library'
&& <Button variant="outlined" onClick={() => setCategoryDialogOpen(true)}>Edit Categories</Button>}
</div>
<CategorySelect
open={categoryDialogOpen}
setOpen={setCategoryDialogOpen}
mangaId={manga.id}
/>
</div>
); );
} }
+59
View File
@@ -0,0 +1,59 @@
/* 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 React, { useEffect, useRef } from 'react';
import Grid from '@material-ui/core/Grid';
import MangaCard from './MangaCard';
interface IProps{
mangas: IManga[]
message?: string
hasNextPage: boolean
lastPageNum: number
setLastPageNum: (lastPageNum: number) => void
}
export default function MangaGrid(props: IProps) {
const {
mangas, message, hasNextPage, lastPageNum, setLastPageNum,
} = props;
let mapped;
const lastManga = useRef<HTMLInputElement>();
const scrollHandler = () => {
if (lastManga.current) {
const rect = lastManga.current.getBoundingClientRect();
if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) {
setLastPageNum(lastPageNum + 1);
}
}
};
useEffect(() => {
window.addEventListener('scroll', scrollHandler, true);
return () => {
window.removeEventListener('scroll', scrollHandler, true);
};
}, [hasNextPage, mangas]);
if (mangas.length === 0) {
mapped = <h3>{message}</h3>;
} else {
mapped = mangas.map((it, idx) => {
if (idx === mangas.length - 1) {
return <MangaCard manga={it} ref={lastManga} />;
}
return <MangaCard manga={it} />;
});
}
return (
<Grid container spacing={1} xs={12} style={{ margin: 0, padding: '5px' }}>
{mapped}
</Grid>
);
}
MangaGrid.defaultProps = {
message: 'loading...',
};
+74 -3
View File
@@ -1,11 +1,23 @@
import React, { useState } from 'react'; /* eslint-disable @typescript-eslint/no-unused-vars */
// TODO: remove above!
/* 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 React, { useContext, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import MoreIcon from '@material-ui/icons/MoreVert';
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu'; import MenuIcon from '@material-ui/icons/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import TemporaryDrawer from './TemporaryDrawer'; import TemporaryDrawer from './TemporaryDrawer';
import NavBarContext from '../context/NavbarContext';
import DarkTheme from '../context/DarkTheme';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -19,13 +31,35 @@ const useStyles = makeStyles((theme) => ({
}, },
})); }));
// const theme = createMuiTheme({
// overrides: {
// MuiAppBar: {
// colorPrimary: { backgroundColor: '#FFC0CB' },
// },
// },
// palette: { type: 'dark' },
// });
export default function NavBar() { export default function NavBar() {
const classes = useStyles(); const classes = useStyles();
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const { title, action } = useContext(NavBarContext);
const open = Boolean(anchorEl);
const { darkTheme } = useContext(DarkTheme);
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return ( return (
<div className={classes.root}> <div className={classes.root}>
<AppBar position="static"> <AppBar position="static" color={darkTheme ? 'default' : 'primary'}>
<Toolbar> <Toolbar>
<IconButton <IconButton
edge="start" edge="start"
@@ -38,8 +72,45 @@ export default function NavBar() {
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Typography variant="h6" className={classes.title}> <Typography variant="h6" className={classes.title}>
Tachidesk {title}
</Typography> </Typography>
{action}
{/* <IconButton
onClick={handleMenu}
aria-label="display more actions"
edge="end"
color="inherit"
>
<FilterListIcon />
</IconButton> */}
{/* <Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={open}
onClose={handleClose}
>
<MenuItem
onClick={() => { setDarkTheme(true); handleClose(); }}
>
Dark Theme
</MenuItem>
<MenuItem
onClick={() => { setDarkTheme(false); handleClose(); }}
>
Light Theme
</MenuItem>
</Menu> */}
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} /> <TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
+13 -4
View File
@@ -1,3 +1,7 @@
/* 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 React from 'react'; import React from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
@@ -5,6 +9,8 @@ import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar'; import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import useLocalStorage from '../util/useLocalStorage';
import { langCodeToName } from '../util/language';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -43,6 +49,8 @@ export default function SourceCard(props: IProps) {
}, },
} = props; } = props;
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles(); const classes = useStyles();
return ( return (
@@ -53,20 +61,21 @@ export default function SourceCard(props: IProps) {
variant="rounded" variant="rounded"
className={classes.icon} className={classes.icon}
alt={name} alt={name}
src={iconUrl} src={serverAddress + iconUrl}
/> />
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2"> <Typography variant="h5" component="h2">
{name} {name}
</Typography> </Typography>
<Typography variant="caption" display="block" gutterBottom> <Typography variant="caption" display="block" gutterBottom>
{lang} {langCodeToName(lang)}
</Typography> </Typography>
</div> </div>
</div> </div>
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
{supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `sources/${id}/latest/`; }}>Latest</Button>} <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/search/`; }}>Search</Button>
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `sources/${id}/popular/`; }}>Browse</Button> {supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/latest/`; }}>Latest</Button>}
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/popular/`; }}>Browse</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -1,3 +1,7 @@
/* 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 React from 'react'; import React from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer'; import Drawer from '@material-ui/core/Drawer';
@@ -32,6 +36,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
onKeyDown={() => setDrawerOpen(false)} onKeyDown={() => setDrawerOpen(false)}
> >
<List> <List>
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Library">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Library" />
</ListItem>
</Link>
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}> <Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Extensions"> <ListItem button key="Extensions">
<ListItemIcon> <ListItemIcon>
@@ -48,6 +60,22 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<ListItemText primary="Sources" /> <ListItemText primary="Sources" />
</ListItem> </ListItem>
</Link> </Link>
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="settings">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItem>
</Link>
{/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Search">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Global Search" />
</ListItem>
</Link> */}
</List> </List>
</div> </div>
); );
+17
View File
@@ -0,0 +1,17 @@
/* 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 React from 'react';
type ContextType = {
darkTheme: boolean
setDarkTheme: React.Dispatch<React.SetStateAction<boolean>>
};
const DarkTheme = React.createContext<ContextType>({
darkTheme: true,
setDarkTheme: ():void => {},
});
export default DarkTheme;
+21
View File
@@ -0,0 +1,21 @@
/* 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 React from 'react';
type ContextType = {
title: string
setTitle: React.Dispatch<React.SetStateAction<string>>
action: any
setAction: React.Dispatch<React.SetStateAction<any>>
};
const NavBarContext = React.createContext<ContextType>({
title: 'Tachidesk',
setTitle: ():void => {},
action: <div />,
setAction: ():void => {},
});
export default NavBarContext;
+4
View File
@@ -1,3 +1,7 @@
/* 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/. */
body { body {
margin: 0; margin: 0;
} }
+4 -6
View File
@@ -1,7 +1,10 @@
/* 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 React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals';
import './index.css'; import './index.css';
// roboto font // roboto font
import 'fontsource-roboto'; import 'fontsource-roboto';
@@ -12,8 +15,3 @@ ReactDOM.render(
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root'), document.getElementById('root'),
); );
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
+4
View File
@@ -1 +1,5 @@
/* 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/. */
/// <reference types="react-scripts" /> /// <reference types="react-scripts" />
-17
View File
@@ -1,17 +0,0 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry) {
import('web-vitals').then(({
getCLS, getFID, getFCP, getLCP, getTTFB,
}) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
+92 -13
View File
@@ -1,21 +1,100 @@
import React, { useEffect, useState } from 'react'; /* 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 React, { useContext, useEffect, useState } from 'react';
import ExtensionCard from '../components/ExtensionCard'; import ExtensionCard from '../components/ExtensionCard';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
import ExtensionLangSelect from '../components/ExtensionLangSelect';
import { defualtLangs, langCodeToName, langSortCmp } from '../util/language';
const allLangs: string[] = [];
function groupExtensions(extensions: IExtension[]) {
allLangs.length = 0; // empty the array
const result = { installed: [] } as any;
extensions.sort((a, b) => ((a.apkName > b.apkName) ? 1 : -1));
extensions.forEach((extension) => {
if (result[extension.lang] === undefined) {
result[extension.lang] = [];
if (extension.lang !== 'all') { allLangs.push(extension.lang); }
}
if (extension.installed) {
result.installed.push(extension);
} else {
result[extension.lang].push(extension);
}
});
// put english first for convience
allLangs.sort(langSortCmp);
return result;
}
export default function Extensions() { export default function Extensions() {
let mapped; const { setTitle, setAction } = useContext(NavbarContext);
const [extensions, setExtensions] = useState<IExtension[]>([]); const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', defualtLangs());
useEffect(() => { useEffect(() => {
fetch('http://127.0.0.1:4567/api/v1/extension/list') setTitle('Extensions');
.then((response) => response.json()) setAction(
.then((data) => setExtensions(data)); <ExtensionLangSelect
}, []); shownLangs={shownLangs}
setShownLangs={setShownLangs}
allLangs={allLangs}
/>,
);
}, [shownLangs]);
if (extensions.length === 0) { const [extensionsRaw, setExtensionsRaw] = useState<IExtension[]>([]);
mapped = <h3>wait</h3>; const [extensions, setExtensions] = useState<any>({});
} else {
mapped = extensions.map((it) => <ExtensionCard extension={it} />); const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
useEffect(() => {
client.get('/api/v1/extension/list')
.then((response) => response.data)
.then((data) => setExtensionsRaw(data));
}, [updateTriggerHolder]);
useEffect(() => {
if (extensionsRaw.length > 0) {
const groupedExtension = groupExtensions(extensionsRaw);
setExtensions(groupedExtension);
}
}, [extensionsRaw]);
if (Object.entries(extensions).length === 0) {
return <h3>loading...</h3>;
} }
return (
return <h2>{mapped}</h2>; <>
{
Object.entries(extensions).map(([lang, list]) => (
(['installed', ...shownLangs].indexOf(lang) !== -1
&& (
<React.Fragment key={lang}>
<h1 key={lang} style={{ marginLeft: 25 }}>
{langCodeToName(lang)}
</h1>
{(list as IExtension[]).map((it) => (
<ExtensionCard
key={it.apkName}
extension={it}
notifyInstall={() => {
triggerUpdate();
}}
/>
))}
</React.Fragment>
))
))
}
</>
);
} }

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