89 Commits

Author SHA1 Message Date
Shaun Tenner
e0b4e3edf0 Update +page.svelte 2025-04-15 15:28:10 +02:00
Shaun Tenner
683a456202 Merge pull request #50 from Gnathonic/improve_compatibility_with_extensions
Improve language extension compatibility
2025-02-07 19:02:32 +02:00
Shaun Tenner
9a4c86870a Merge pull request #49 from v2lmmj04/patch-1
Update underscore prefix for image filenames
2025-02-07 18:38:34 +02:00
Gnathonic
bc5b9ccd16 Add keyed blocks to enhance MangaPage reactivity
Keyed blocks improve DOM update handling. This improves compatibility with some language learning extensions. For example, this prevents Migaku from persisting textboxes from previous pages as one pages through their comics.
2025-01-24 18:19:35 -07:00
v2lmmj04
6c16008aae Change prefix to "mokuro_*"
This was an alternative I was thinking of initially since it better namespaces the files, but wasn't sure about.

I saw this commit ended up going with this approach, so I'm aligning the PR to it in the end:

6d550cbe8a
2025-01-09 19:51:49 -08:00
v2lmmj04
b281052908 Remove underscore prefix for image filenames 2025-01-05 14:51:37 -08:00
Shaun Tenner
52619b1a9b Merge pull request #42 from nyanSpruk/patch-1
Update README.md - typo
2024-11-16 10:18:42 +02:00
Nik Jan Špruk
a30c526a63 Update README.md - typo
Fixed typo from useage to usage.
2024-11-15 20:55:48 +01:00
Shaun Tenner
7fb55f5399 Merge pull request #41 from AnonMiraj/main
Add way to import from other sources
2024-11-14 19:34:43 +02:00
AnonMiraj
6220b6172e Add way to import from other sources 2024-11-13 20:54:10 +02:00
ZXY101
a9f7bca27c Fix title text 2024-08-19 22:02:24 +02:00
Shaun Tenner
0ebeb46171 Merge pull request #23 from kenrick95/kenrick/invert-colors-settings
Settings to invert colors of the images
2024-07-10 23:46:54 +02:00
Kenrick
1fdbdf5185 Settings to invert colors of the images
Useful for novels, for reading in dark mode
2024-07-08 23:15:17 +08:00
ZXY101
109241e584 Add default fullscreen setting 2024-05-25 22:30:16 +02:00
ZXY101
ec632b534b Fix progress 2024-05-18 19:25:19 +02:00
ZXY101
bc7cb721d3 Update loading 2024-05-18 17:26:12 +02:00
ZXY101
5a5dc189fb Improve download 2024-05-18 17:08:40 +02:00
ZXY101
7baf586d3b Merge branch 'main' of https://github.com/ZXY101/z-reader 2024-05-18 16:02:36 +02:00
ZXY101
3e487caad9 Update version 2024-05-18 16:02:19 +02:00
Shaun Tenner
44b8989f2e Merge pull request #17 from ZXY101/gdrive-integration
Add google drive intergration
2024-05-18 15:47:06 +02:00
ZXY101
b4573fd955 Complete Gdrive integration 2024-05-18 15:44:16 +02:00
ZXY101
08eee8f62c Add title to cloud route 2024-03-26 02:17:47 +02:00
ZXY101
37c4aa1572 Update drive styling 2024-03-26 02:16:29 +02:00
ZXY101
3b237e15f5 Update scope 2024-03-26 01:57:17 +02:00
ZXY101
edea5aea08 Update scope 2024-03-26 01:55:39 +02:00
Shaun Tenner
940313310b Merge pull request #13 from ZXY101/google-drive
Add Google drive functionality
2024-03-26 01:23:40 +02:00
ZXY101
6daeb966c5 Add profile uploading + cleanup 2024-03-26 01:21:20 +02:00
ZXY101
8ecc96e92e Update drive styling 2024-03-14 07:53:42 +02:00
ZXY101
cc3448f6ba Add gdrive volume data uploading/downloading 2024-03-14 07:31:13 +02:00
ZXY101
491b9603af Add google drive support 2024-03-13 09:35:44 +02:00
ZXY101
bec5e84067 Update edge buttons 2024-03-13 05:05:51 +02:00
ZXY101
8ef7f6447f Reverse textbox sorting 2024-03-12 10:49:31 +02:00
ZXY101
d57de2ec56 Vix volume sorting 2024-03-12 10:31:56 +02:00
ZXY101
1007b3e4ae Update anki connect quick action 2024-03-12 10:27:17 +02:00
ZXY101
39d4e0f072 Update bounds 2024-03-12 10:17:35 +02:00
ZXY101
5d3f17679b Add id for manga panel 2024-03-10 05:42:01 +02:00
ZXY101
032d56a20e Improve unzip speed 2024-03-10 04:54:29 +02:00
ZXY101
282cad6558 Add line count 2024-03-10 03:32:24 +02:00
ZXY101
5bc486660c Update extatic events 2024-03-10 03:06:46 +02:00
ZXY101
e511a0a39b Add exSTATic support 2024-03-09 12:02:13 +02:00
ZXY101
18f5e3ae46 Fix spacebar navigation 2024-03-01 01:02:09 +02:00
ZXY101
050228765e Fix sort 2024-02-28 00:32:50 +02:00
ZXY101
89acdddccb Fix volume screen sorting 2024-02-28 00:22:13 +02:00
ZXY101
1946b12623 Add single volume deletion 2024-02-28 00:15:36 +02:00
Shaun Tenner
1df387526a Merge pull request #12 from ZXY101/jidoujisho-testing
Temporary Jidoujisho compatibility
2024-02-14 23:55:59 +02:00
ZXY101
491c4efab2 More QOL changes 2024-02-11 08:45:21 +02:00
ZXY101
04dd68b242 Ensure there are always volume defaults 2024-02-11 07:51:11 +02:00
ZXY101
83843ff5dd Various QOL changes 2024-02-11 07:25:41 +02:00
ZXY101
66e8dc6683 Merge branch 'master' of https://github.com/ZXY101/z-reader into jidoujisho-testing 2024-02-08 16:28:54 +02:00
ZXY101
b1c4029345 Fix zoomDefault on count up 2024-02-08 16:28:37 +02:00
ZXY101
49969caff7 Update default image for jidoujisho 2024-02-08 12:50:05 +02:00
ZXY101
34e3dbbe1b Merge branch 'master' of https://github.com/ZXY101/z-reader into jidoujisho-testing 2024-02-08 12:46:27 +02:00
ZXY101
47228f2b96 Fix arrow direction 2024-02-08 12:41:38 +02:00
ZXY101
87be518e02 Dont zoomDefault on timer update 2024-02-08 11:00:59 +02:00
ZXY101
a1a582f786 Minor QOL fixes 2024-02-07 17:18:38 +02:00
ZXY101
50ef5c9f40 Attemp to get jidoujisho support working 2024-02-07 11:19:18 +02:00
ZXY101
28155496fc Make timer controls manual 2024-02-05 18:02:20 +02:00
ZXY101
1c92a749be Refactor stats, add derived catalog stores, improve timer 2024-02-05 15:39:22 +02:00
ZXY101
2e28843a07 Fix ordering of smaller text boxes 2024-02-05 12:43:36 +02:00
ZXY101
159a6d50d4 Ensure images extract into volume folders 2024-02-04 16:15:27 +02:00
ZXY101
28587e15c6 Add upload instructions 2024-02-04 05:53:25 +02:00
ZXY101
5226393eac Allow single zip upload + improve manga extraction 2024-02-04 04:48:15 +02:00
ZXY101
3b2c0bedc3 Remove extra line 2024-01-30 10:30:35 +02:00
ZXY101
6ecf57ccbe Merge https://github.com/ZXY101/z-reader 2024-01-30 10:30:12 +02:00
ZXY101
8757804140 Another attempt to fix import order 2024-01-30 10:29:39 +02:00
ZXY101
dd881da036 Another attempt to fix import order 2024-01-30 10:27:34 +02:00
Shaun Tenner
14d0043bb7 Merge pull request #11 from precondition/patch-1
Lowercase extension of item before comparing with accepted imageTypes
2024-01-30 10:09:47 +02:00
precondition
5bb2a4033a Lowercase extension of item before comparing with valid imageTypes 2024-01-30 01:04:54 +01:00
ZXY101
692071eea0 Fix zip image ordering 2024-01-17 00:17:11 +02:00
ZXY101
eeef343027 Fix the fix 2024-01-13 18:21:39 +02:00
ZXY101
487a591fb6 Another attempt to fix 404 2024-01-13 18:16:49 +02:00
Shaun Tenner
edbd443137 Merge pull request #9 from ZXY101/zxy101/improvements
Catalog search, sort, order + misc cleanup
2024-01-13 17:54:21 +02:00
ZXY101
28201f5d88 Add misc settings + misc cleanup 2024-01-13 17:53:21 +02:00
ZXY101
c817cc8681 Add catalog search, fix sorting 2024-01-08 06:47:30 +02:00
ZXY101
a18f66ca37 uri decode zip namem 2024-01-07 10:05:46 +02:00
ZXY101
801ecf929e Add manga extracting 2024-01-06 17:32:37 +02:00
ZXY101
dffc3cbed0 Update package lock 2024-01-06 15:55:23 +02:00
Shaun Tenner
b07d98a1c1 Another attempt to fix order issues 2023-12-14 11:52:53 +09:00
Shaun Tenner
224f73d053 Attempt to fix order issues 2023-12-14 11:36:11 +09:00
Shaun
a2f59640af Adjust settings buttons 2023-11-13 15:20:00 +09:00
Shaun
58a4b6be16 Adjust double tap zoom 2023-11-13 14:31:32 +09:00
Shaun
5e7ec34300 Fix order for unzipped manga 2023-11-13 14:01:03 +09:00
Shaun
03370f1b9f Prevent edge swipe in reader 2023-11-09 16:29:42 +09:00
Shaun
a4e1e6f54a Add clearer anki connect instructions 2023-11-09 16:05:46 +09:00
Shaun Tenner
942d5d39f7 Update README.md 2023-10-10 03:40:03 +09:00
ZXY101
aa1f4703f1 Add vercel analytics 2023-10-07 09:58:50 +02:00
ZXY101
137ea85d1a Fix credits 2023-10-06 04:12:25 +02:00
ZXY101
40deec881b Merge https://github.com/ZXY101/z-reader 2023-10-06 01:53:31 +02:00
ZXY101
a92828dde0 Add profile importing/exporting 2023-10-06 01:53:18 +02:00
49 changed files with 1633 additions and 375 deletions

View File

@@ -10,7 +10,7 @@ https://github.com/ZXY101/mokuro-reader/assets/39561296/45a214a8-3f69-461c-87d7-
- Anki connect integration & image cropping
- Installation and offline support
## Useage:
## Usage:
You can find the reader hosted [here](https://reader.mokuro.app/).
To import your manga, process it with mokuro and then upload your manga along with the generated `.mokuro` file.
@@ -21,7 +21,7 @@ As of the moment base mokuro does not generate the `.mokuro` file, you need to i
pip install git+https://github.com/kha-white/mokuro.git@web-reader
```
Once installed and your manga is processed, import it your manga to the reader.
Once installed and your manga is processed, import your manga into the reader.
## Development:
@@ -33,7 +33,7 @@ cd mokuro-reader
Install dependencies:
```bash
npm run install
npm install
```
Start the dev server:

464
package-lock.json generated
View File

@@ -1,13 +1,17 @@
{
"name": "z-reader",
"version": "0.0.1",
"version": "0.9.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "z-reader",
"version": "0.0.1",
"version": "0.9.1",
"dependencies": {
"@types/gapi": "^0.0.47",
"@types/google.accounts": "^0.0.14",
"@types/google.picker": "^0.0.42",
"@vercel/analytics": "^1.1.0",
"@zip.js/zip.js": "^2.7.20",
"dexie": "^4.0.1-alpha.25",
"panzoom": "^9.4.3",
@@ -16,6 +20,7 @@
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.20.4",
"@types/gapi.client.drive-v3": "^0.0.4",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"autoprefixer": "^10.4.14",
@@ -479,6 +484,16 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@floating-ui/core": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz",
@@ -591,6 +606,26 @@
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"node_modules/@maxim_mazurok/gapi.client.discovery-v1": {
"version": "0.1.20200806",
"resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.discovery-v1/-/gapi.client.discovery-v1-0.1.20200806.tgz",
"integrity": "sha512-Wl6UfmZVDdWbY3PUu8E2ULk9RPLjnMqp/iOA4tcK8Ne+U/GmlnWP/e34IaZNGArfl7iXJNOG+/3Rj9L9jQyF9Q==",
"dev": true,
"dependencies": {
"@types/gapi.client": "*",
"@types/gapi.client.discovery-v1": "*"
}
},
"node_modules/@maxim_mazurok/gapi.client.drive-v3": {
"version": "0.0.20240304",
"resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.drive-v3/-/gapi.client.drive-v3-0.0.20240304.tgz",
"integrity": "sha512-PuGvDvRE70XsyUqnQCq5M2VJ7/E7u5pDvHZfBXunVlf5mYmkyAdbZg+ygJDJ2243wWpUXjmEcuxLDY2yamIq4g==",
"dev": true,
"dependencies": {
"@types/gapi.client": "*",
"@types/gapi.client.discovery-v1": "*"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -655,24 +690,26 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "1.22.3",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.22.3.tgz",
"integrity": "sha512-IpHD5wvuoOIHYaHQUBJ1zERD2Iz+fB/rBXhXjl8InKw6X4VKE9BSus+ttHhE7Ke+Ie9ecfilzX8BnWE3FeQyng==",
"version": "1.30.4",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.4.tgz",
"integrity": "sha512-JSQIQT6XvdchCRQEm7BABxPC56WP5RYVONAi+09S8tmzeP43fBsRlr95bFmsTQM2RHBldfgQk+jgdnsKI75daA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@sveltejs/vite-plugin-svelte": "^2.4.1",
"@sveltejs/vite-plugin-svelte": "^2.5.0",
"@types/cookie": "^0.5.1",
"cookie": "^0.5.0",
"devalue": "^4.3.1",
"esm-env": "^1.0.0",
"kleur": "^4.1.5",
"magic-string": "^0.30.0",
"mime": "^3.0.0",
"mrmime": "^1.0.1",
"sade": "^1.8.1",
"set-cookie-parser": "^2.6.0",
"sirv": "^2.0.2",
"undici": "~5.22.0"
"tiny-glob": "^0.2.9",
"undici": "^5.28.3"
},
"bin": {
"svelte-kit": "svelte-kit.js"
@@ -681,36 +718,36 @@
"node": "^16.14 || >=18"
},
"peerDependencies": {
"svelte": "^3.54.0 || ^4.0.0-next.0",
"svelte": "^3.54.0 || ^4.0.0-next.0 || ^5.0.0-next.0",
"vite": "^4.0.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.3.tgz",
"integrity": "sha512-NY2h+B54KHZO3kDURTdARqthn6D4YSIebtfW75NvZ/fwyk4G+AJw3V/i0OBjyN4406Ht9yZcnNWMuRUFnDNNiA==",
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.5.3.tgz",
"integrity": "sha512-erhNtXxE5/6xGZz/M9eXsmI7Pxa6MS7jyTy06zN3Ck++ldrppOnOlJwHHTsMC7DHDQdgUp4NAc4cDNQ9eGdB/w==",
"dev": true,
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^1.0.3",
"@sveltejs/vite-plugin-svelte-inspector": "^1.0.4",
"debug": "^4.3.4",
"deepmerge": "^4.3.1",
"kleur": "^4.1.5",
"magic-string": "^0.30.1",
"svelte-hmr": "^0.15.2",
"magic-string": "^0.30.3",
"svelte-hmr": "^0.15.3",
"vitefu": "^0.2.4"
},
"engines": {
"node": "^14.18.0 || >= 16"
},
"peerDependencies": {
"svelte": "^3.54.0 || ^4.0.0",
"svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-next.0",
"vite": "^4.0.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.3.tgz",
"integrity": "sha512-Khdl5jmmPN6SUsVuqSXatKpQTMIifoQPDanaxC84m9JxIibWvSABJyHpyys0Z+1yYrxY5TTEQm+6elh0XCMaOA==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz",
"integrity": "sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==",
"dev": true,
"dependencies": {
"debug": "^4.3.4"
@@ -736,6 +773,45 @@
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
"dev": true
},
"node_modules/@types/gapi": {
"version": "0.0.47",
"resolved": "https://registry.npmjs.org/@types/gapi/-/gapi-0.0.47.tgz",
"integrity": "sha512-/ZsLuq6BffMgbKMtZyDZ8vwQvTyKhKQ1G2K6VyWCgtHHhfSSXbk4+4JwImZiTjWNXfI2q1ZStAwFFHSkNoTkHA=="
},
"node_modules/@types/gapi.client": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/gapi.client/-/gapi.client-1.0.8.tgz",
"integrity": "sha512-qJQUmmumbYym3Amax0S8CVzuSngcXsC1fJdwRS2zeW5lM63zXkw4wJFP+bG0jzgi0R6EsJKoHnGNVTDbOyG1ng==",
"dev": true
},
"node_modules/@types/gapi.client.discovery-v1": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@types/gapi.client.discovery-v1/-/gapi.client.discovery-v1-0.0.4.tgz",
"integrity": "sha512-uevhRumNE65F5mf2gABLaReOmbFSXONuzFZjNR3dYv6BmkHg+wciubHrfBAsp3554zNo3Dcg6dUAlwMqQfpwjQ==",
"dev": true,
"dependencies": {
"@maxim_mazurok/gapi.client.discovery-v1": "latest"
}
},
"node_modules/@types/gapi.client.drive-v3": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@types/gapi.client.drive-v3/-/gapi.client.drive-v3-0.0.4.tgz",
"integrity": "sha512-jE37dJ0EzAdY0aJPFOp20xmec/aO0P4HtUIA9k07RMPyedFDOcuMlSac1r0PklwQdgXF7BHaMoObNHNAnwSQUQ==",
"dev": true,
"dependencies": {
"@maxim_mazurok/gapi.client.drive-v3": "latest"
}
},
"node_modules/@types/google.accounts": {
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/@types/google.accounts/-/google.accounts-0.0.14.tgz",
"integrity": "sha512-HqIVkVzpiLWhlajhQQd4rIV7czanFvXblJI2J1fSrL+VKQuQwwZ63m35D/mI0flsqKE6p/hNrAG0Yn4FD6JvNA=="
},
"node_modules/@types/google.picker": {
"version": "0.0.42",
"resolved": "https://registry.npmjs.org/@types/google.picker/-/google.picker-0.0.42.tgz",
"integrity": "sha512-7Ut1zDLCGNhyN+0fuIcncpBP9eUsIjLKz6qmVKvdkBUi1EzAskxTcIZcU/w2cO1ens/hEXlqeLd5qAKs/Kqqyw=="
},
"node_modules/@types/json-schema": {
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
@@ -942,6 +1018,14 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@vercel/analytics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.1.0.tgz",
"integrity": "sha512-k5ePYZPxitxxD1eJenPUUuH3qK+EtaA9OVD3oO0BRbyT/LarmZF0qdkptRSidip1arQxsTEIWvHbTuj1oksl+Q==",
"dependencies": {
"server-only": "^0.0.1"
}
},
"node_modules/@yr/monotone-cubic-spline": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
@@ -1219,18 +1303,6 @@
"node": "*"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dev": true,
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1250,9 +1322,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001525",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001525.tgz",
"integrity": "sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q==",
"version": "1.0.30001597",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
"integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
"dev": true,
"funding": [
{
@@ -2105,6 +2177,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globalyzer": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
"integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==",
"dev": true
},
"node_modules/globby": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
@@ -2125,6 +2203,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globrex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -2418,9 +2502,9 @@
}
},
"node_modules/magic-string": {
"version": "0.30.2",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.2.tgz",
"integrity": "sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==",
"version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
@@ -2457,18 +2541,6 @@
"node": ">=8.6"
}
},
"node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"dev": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -2556,9 +2628,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
@@ -2800,9 +2872,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.27",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==",
"version": "8.4.33",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
"integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
"dev": true,
"funding": [
{
@@ -2819,7 +2891,7 @@
}
],
"dependencies": {
"nanoid": "^3.3.6",
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
@@ -3104,9 +3176,9 @@
}
},
"node_modules/rollup": {
"version": "3.27.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.27.0.tgz",
"integrity": "sha512-aOltLCrYZ0FhJDm7fCqwTjIUEVjWjcydKBV/Zeid6Mn8BWgDCUBBWT5beM5ieForYNo/1ZHuGJdka26kvQ3Gzg==",
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
@@ -3210,6 +3282,11 @@
"node": ">=10"
}
},
"node_modules/server-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="
},
"node_modules/set-cookie-parser": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
@@ -3284,15 +3361,6 @@
"node": ">=0.10.0"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"dev": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -3499,15 +3567,15 @@
}
},
"node_modules/svelte-hmr": {
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.2.tgz",
"integrity": "sha512-q/bAruCvFLwvNbeE1x3n37TYFb3mTBJ6TrCq6p2CoFbSTNhDE9oAtEfpy+wmc9So8AG0Tja+X0/mJzX9tSfvIg==",
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
"integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==",
"dev": true,
"engines": {
"node": "^12.20 || ^14.13.1 || >= 16"
},
"peerDependencies": {
"svelte": "^3.19.0 || ^4.0.0-next.0"
"svelte": "^3.19.0 || ^4.0.0"
}
},
"node_modules/svelte-preprocess": {
@@ -3749,6 +3817,16 @@
"node": ">=0.8"
}
},
"node_modules/tiny-glob": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
"integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==",
"dev": true,
"dependencies": {
"globalyzer": "0.1.0",
"globrex": "^0.1.2"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -3841,12 +3919,13 @@
}
},
"node_modules/undici": {
"version": "5.22.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz",
"integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==",
"version": "5.28.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"dev": true,
"license": "MIT",
"dependencies": {
"busboy": "^1.6.0"
"@fastify/busboy": "^2.0.0"
},
"engines": {
"node": ">=14.0"
@@ -3898,14 +3977,15 @@
"dev": true
},
"node_modules/vite": {
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
"integrity": "sha512-6pYf9QJ1mHylfVh39HpuSfMPojPSKVxZvnclX1K1FyZ1PXDOcLBibdq5t1qxJSnL63ca8Wf4zts6mD8u8oc9Fw==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.26",
"rollup": "^3.25.2"
"postcss": "^8.4.27",
"rollup": "^3.27.1"
},
"bin": {
"vite": "bin/vite.js"
@@ -3953,12 +4033,12 @@
}
},
"node_modules/vitefu": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz",
"integrity": "sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
"integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==",
"dev": true,
"peerDependencies": {
"vite": "^3.0.0 || ^4.0.0"
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
},
"peerDependenciesMeta": {
"vite": {
@@ -4235,6 +4315,12 @@
"integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==",
"dev": true
},
"@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"dev": true
},
"@floating-ui/core": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz",
@@ -4330,6 +4416,26 @@
}
}
},
"@maxim_mazurok/gapi.client.discovery-v1": {
"version": "0.1.20200806",
"resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.discovery-v1/-/gapi.client.discovery-v1-0.1.20200806.tgz",
"integrity": "sha512-Wl6UfmZVDdWbY3PUu8E2ULk9RPLjnMqp/iOA4tcK8Ne+U/GmlnWP/e34IaZNGArfl7iXJNOG+/3Rj9L9jQyF9Q==",
"dev": true,
"requires": {
"@types/gapi.client": "*",
"@types/gapi.client.discovery-v1": "*"
}
},
"@maxim_mazurok/gapi.client.drive-v3": {
"version": "0.0.20240304",
"resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.drive-v3/-/gapi.client.drive-v3-0.0.20240304.tgz",
"integrity": "sha512-PuGvDvRE70XsyUqnQCq5M2VJ7/E7u5pDvHZfBXunVlf5mYmkyAdbZg+ygJDJ2243wWpUXjmEcuxLDY2yamIq4g==",
"dev": true,
"requires": {
"@types/gapi.client": "*",
"@types/gapi.client.discovery-v1": "*"
}
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4378,44 +4484,45 @@
}
},
"@sveltejs/kit": {
"version": "1.22.3",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.22.3.tgz",
"integrity": "sha512-IpHD5wvuoOIHYaHQUBJ1zERD2Iz+fB/rBXhXjl8InKw6X4VKE9BSus+ttHhE7Ke+Ie9ecfilzX8BnWE3FeQyng==",
"version": "1.30.4",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.4.tgz",
"integrity": "sha512-JSQIQT6XvdchCRQEm7BABxPC56WP5RYVONAi+09S8tmzeP43fBsRlr95bFmsTQM2RHBldfgQk+jgdnsKI75daA==",
"dev": true,
"requires": {
"@sveltejs/vite-plugin-svelte": "^2.4.1",
"@sveltejs/vite-plugin-svelte": "^2.5.0",
"@types/cookie": "^0.5.1",
"cookie": "^0.5.0",
"devalue": "^4.3.1",
"esm-env": "^1.0.0",
"kleur": "^4.1.5",
"magic-string": "^0.30.0",
"mime": "^3.0.0",
"mrmime": "^1.0.1",
"sade": "^1.8.1",
"set-cookie-parser": "^2.6.0",
"sirv": "^2.0.2",
"undici": "~5.22.0"
"tiny-glob": "^0.2.9",
"undici": "^5.28.3"
}
},
"@sveltejs/vite-plugin-svelte": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.3.tgz",
"integrity": "sha512-NY2h+B54KHZO3kDURTdARqthn6D4YSIebtfW75NvZ/fwyk4G+AJw3V/i0OBjyN4406Ht9yZcnNWMuRUFnDNNiA==",
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.5.3.tgz",
"integrity": "sha512-erhNtXxE5/6xGZz/M9eXsmI7Pxa6MS7jyTy06zN3Ck++ldrppOnOlJwHHTsMC7DHDQdgUp4NAc4cDNQ9eGdB/w==",
"dev": true,
"requires": {
"@sveltejs/vite-plugin-svelte-inspector": "^1.0.3",
"@sveltejs/vite-plugin-svelte-inspector": "^1.0.4",
"debug": "^4.3.4",
"deepmerge": "^4.3.1",
"kleur": "^4.1.5",
"magic-string": "^0.30.1",
"svelte-hmr": "^0.15.2",
"magic-string": "^0.30.3",
"svelte-hmr": "^0.15.3",
"vitefu": "^0.2.4"
}
},
"@sveltejs/vite-plugin-svelte-inspector": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.3.tgz",
"integrity": "sha512-Khdl5jmmPN6SUsVuqSXatKpQTMIifoQPDanaxC84m9JxIibWvSABJyHpyys0Z+1yYrxY5TTEQm+6elh0XCMaOA==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz",
"integrity": "sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==",
"dev": true,
"requires": {
"debug": "^4.3.4"
@@ -4433,6 +4540,45 @@
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
"dev": true
},
"@types/gapi": {
"version": "0.0.47",
"resolved": "https://registry.npmjs.org/@types/gapi/-/gapi-0.0.47.tgz",
"integrity": "sha512-/ZsLuq6BffMgbKMtZyDZ8vwQvTyKhKQ1G2K6VyWCgtHHhfSSXbk4+4JwImZiTjWNXfI2q1ZStAwFFHSkNoTkHA=="
},
"@types/gapi.client": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/gapi.client/-/gapi.client-1.0.8.tgz",
"integrity": "sha512-qJQUmmumbYym3Amax0S8CVzuSngcXsC1fJdwRS2zeW5lM63zXkw4wJFP+bG0jzgi0R6EsJKoHnGNVTDbOyG1ng==",
"dev": true
},
"@types/gapi.client.discovery-v1": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@types/gapi.client.discovery-v1/-/gapi.client.discovery-v1-0.0.4.tgz",
"integrity": "sha512-uevhRumNE65F5mf2gABLaReOmbFSXONuzFZjNR3dYv6BmkHg+wciubHrfBAsp3554zNo3Dcg6dUAlwMqQfpwjQ==",
"dev": true,
"requires": {
"@maxim_mazurok/gapi.client.discovery-v1": "latest"
}
},
"@types/gapi.client.drive-v3": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@types/gapi.client.drive-v3/-/gapi.client.drive-v3-0.0.4.tgz",
"integrity": "sha512-jE37dJ0EzAdY0aJPFOp20xmec/aO0P4HtUIA9k07RMPyedFDOcuMlSac1r0PklwQdgXF7BHaMoObNHNAnwSQUQ==",
"dev": true,
"requires": {
"@maxim_mazurok/gapi.client.drive-v3": "latest"
}
},
"@types/google.accounts": {
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/@types/google.accounts/-/google.accounts-0.0.14.tgz",
"integrity": "sha512-HqIVkVzpiLWhlajhQQd4rIV7czanFvXblJI2J1fSrL+VKQuQwwZ63m35D/mI0flsqKE6p/hNrAG0Yn4FD6JvNA=="
},
"@types/google.picker": {
"version": "0.0.42",
"resolved": "https://registry.npmjs.org/@types/google.picker/-/google.picker-0.0.42.tgz",
"integrity": "sha512-7Ut1zDLCGNhyN+0fuIcncpBP9eUsIjLKz6qmVKvdkBUi1EzAskxTcIZcU/w2cO1ens/hEXlqeLd5qAKs/Kqqyw=="
},
"@types/json-schema": {
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
@@ -4550,6 +4696,14 @@
"eslint-visitor-keys": "^3.3.0"
}
},
"@vercel/analytics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.1.0.tgz",
"integrity": "sha512-k5ePYZPxitxxD1eJenPUUuH3qK+EtaA9OVD3oO0BRbyT/LarmZF0qdkptRSidip1arQxsTEIWvHbTuj1oksl+Q==",
"requires": {
"server-only": "^0.0.1"
}
},
"@yr/monotone-cubic-spline": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
@@ -4744,15 +4898,6 @@
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true
},
"busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dev": true,
"requires": {
"streamsearch": "^1.1.0"
}
},
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -4766,9 +4911,9 @@
"dev": true
},
"caniuse-lite": {
"version": "1.0.30001525",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001525.tgz",
"integrity": "sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q==",
"version": "1.0.30001597",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
"integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
"dev": true
},
"chalk": {
@@ -5385,6 +5530,12 @@
"type-fest": "^0.20.2"
}
},
"globalyzer": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
"integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==",
"dev": true
},
"globby": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
@@ -5399,6 +5550,12 @@
"slash": "^3.0.0"
}
},
"globrex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true
},
"graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -5628,9 +5785,9 @@
}
},
"magic-string": {
"version": "0.30.2",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.2.tgz",
"integrity": "sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==",
"version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
"dev": true,
"requires": {
"@jridgewell/sourcemap-codec": "^1.4.15"
@@ -5658,12 +5815,6 @@
"picomatch": "^2.3.1"
}
},
"mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"dev": true
},
"min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -5730,9 +5881,9 @@
}
},
"nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true
},
"natural-compare": {
@@ -5908,12 +6059,12 @@
"dev": true
},
"postcss": {
"version": "8.4.27",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==",
"version": "8.4.33",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
"integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
"dev": true,
"requires": {
"nanoid": "^3.3.6",
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}
@@ -6069,9 +6220,9 @@
}
},
"rollup": {
"version": "3.27.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.27.0.tgz",
"integrity": "sha512-aOltLCrYZ0FhJDm7fCqwTjIUEVjWjcydKBV/Zeid6Mn8BWgDCUBBWT5beM5ieForYNo/1ZHuGJdka26kvQ3Gzg==",
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"dev": true,
"requires": {
"fsevents": "~2.3.2"
@@ -6138,6 +6289,11 @@
"lru-cache": "^6.0.0"
}
},
"server-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="
},
"set-cookie-parser": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
@@ -6194,12 +6350,6 @@
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true
},
"streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"dev": true
},
"strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -6344,9 +6494,9 @@
}
},
"svelte-hmr": {
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.2.tgz",
"integrity": "sha512-q/bAruCvFLwvNbeE1x3n37TYFb3mTBJ6TrCq6p2CoFbSTNhDE9oAtEfpy+wmc9So8AG0Tja+X0/mJzX9tSfvIg==",
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
"integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==",
"dev": true,
"requires": {}
},
@@ -6506,6 +6656,16 @@
"thenify": ">= 3.1.0 < 4"
}
},
"tiny-glob": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
"integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==",
"dev": true,
"requires": {
"globalyzer": "0.1.0",
"globrex": "^0.1.2"
}
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -6572,12 +6732,12 @@
"dev": true
},
"undici": {
"version": "5.22.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz",
"integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==",
"version": "5.28.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"dev": true,
"requires": {
"busboy": "^1.6.0"
"@fastify/busboy": "^2.0.0"
}
},
"update-browserslist-db": {
@@ -6606,21 +6766,21 @@
"dev": true
},
"vite": {
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
"integrity": "sha512-6pYf9QJ1mHylfVh39HpuSfMPojPSKVxZvnclX1K1FyZ1PXDOcLBibdq5t1qxJSnL63ca8Wf4zts6mD8u8oc9Fw==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"requires": {
"esbuild": "^0.18.10",
"fsevents": "~2.3.2",
"postcss": "^8.4.26",
"rollup": "^3.25.2"
"postcss": "^8.4.27",
"rollup": "^3.27.1"
}
},
"vitefu": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz",
"integrity": "sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
"integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==",
"dev": true,
"requires": {}
},

View File

@@ -1,6 +1,6 @@
{
"name": "z-reader",
"version": "0.9.0",
"version": "0.9.1",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -14,6 +14,7 @@
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.20.4",
"@types/gapi.client.drive-v3": "^0.0.4",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"autoprefixer": "^10.4.14",
@@ -36,6 +37,10 @@
},
"type": "module",
"dependencies": {
"@types/gapi": "^0.0.47",
"@types/google.accounts": "^0.0.14",
"@types/google.picker": "^0.0.42",
"@vercel/analytics": "^1.1.0",
"@zip.js/zip.js": "^2.7.20",
"dexie": "^4.0.1-alpha.25",
"panzoom": "^9.4.3",

View File

@@ -3,14 +3,25 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="manifest" crossorigin="use-credentials" href="manifest.json">
<link rel="manifest" crossorigin="use-credentials" href="manifest.json" />
<meta
name="viewport"
content="width=device-width, height=device-height,initial-scale=1, minimum-scale=1, user-scalable=no"
/>
<script src="https://apis.google.com/js/api.js"></script>
<script src="https://accounts.google.com/gsi/client"></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="bg-white dark:bg-gray-950 dark:text-white">
<div style="display: contents">%sveltekit.body%</div>
<div
id="popupAbout"
class="pageContainer"
style="
display: contents;
background-image: url('https://reader.mokuro.app/_app/immutable/assets/icon.06fcfdd6.webp');
"
>
%sveltekit.body%
</div>
</body>
</html>

View File

@@ -10,7 +10,7 @@ type CropperModal = {
export const cropperStore = writable<CropperModal | undefined>(undefined);
export function showCropper(image: string, sentence: string) {
export function showCropper(image: string, sentence?: string) {
cropperStore.set({
open: true,
image,

View File

@@ -64,7 +64,7 @@ export async function imageToWebp(source: File) {
}
}
export async function updateLastCard(imageData: string | null | undefined, sentence: string) {
export async function updateLastCard(imageData: string | null | undefined, sentence?: string) {
const {
overwriteImage,
enabled,
@@ -88,7 +88,7 @@ export async function updateLastCard(imageData: string | null | undefined, sente
const fields: Record<string, any> = {};
if (grabSentence) {
if (grabSentence && sentence) {
fields[sentenceField] = sentence;
}
@@ -102,7 +102,7 @@ export async function updateLastCard(imageData: string | null | undefined, sente
id,
fields,
picture: {
filename: `_${id}.webp`,
filename: `mokuro_${id}.webp`,
data: imageData.split(';base64,')[1],
fields: [pictureField],
},

View File

@@ -1,4 +1,30 @@
import { db } from '$lib/catalog/db';
import { page } from '$app/stores';
import { db, type Catalog } from '$lib/catalog/db';
import type { Volume } from '$lib/types';
import { liveQuery } from 'dexie';
import { derived, type Readable } from 'svelte/store';
export const catalog = liveQuery(() => db.catalog.toArray());
function sortManga(a: Volume, b: Volume) {
if (a.volumeName < b.volumeName) {
return -1;
}
if (a.volumeName > b.volumeName) {
return 1;
}
return 0;
}
export const manga = derived([page, catalog as unknown as Readable<Catalog[]>], ([$page, $catalog]) => {
if ($page && $catalog) {
return $catalog.find((item) => item.id === $page.params.manga)?.manga.sort(sortManga)
}
});
export const volume = derived(([page, manga]), ([$page, $manga]) => {
if ($page && $manga) {
return $manga.find((item) => item.mokuroData.volume_uuid === $page.params.volume)
}
})

View File

@@ -1,17 +1,78 @@
<script lang="ts">
import { catalog } from '$lib/catalog';
import { Button, Search, Listgroup } from 'flowbite-svelte';
import CatalogItem from './CatalogItem.svelte';
import Loader from './Loader.svelte';
import { GridOutline, SortOutline, ListOutline } from 'flowbite-svelte-icons';
import { miscSettings, updateMiscSetting } from '$lib/settings';
import CatalogListItem from './CatalogListItem.svelte';
$: sortedCatalog = $catalog
?.sort((a, b) => {
if ($miscSettings.gallerySorting === 'ASC') {
return a.manga[0].mokuroData.title.localeCompare(b.manga[0].mokuroData.title);
} else {
return b.manga[0].mokuroData.title.localeCompare(a.manga[0].mokuroData.title);
}
})
.filter((item) => {
return item.manga[0].mokuroData.title.toLowerCase().indexOf(search.toLowerCase()) !== -1;
});
let search = '';
function onLayout() {
if ($miscSettings.galleryLayout === 'list') {
updateMiscSetting('galleryLayout', 'grid');
} else {
updateMiscSetting('galleryLayout', 'list');
}
}
function onOrder() {
if ($miscSettings.gallerySorting === 'ASC') {
updateMiscSetting('gallerySorting', 'DESC');
} else {
updateMiscSetting('gallerySorting', 'ASC');
}
}
</script>
{#if $catalog}
{#if $catalog.length > 0}
<div class="flex flex-col gap-5">
<div class="flex sm:flex-row flex-col gap-5 flex-wrap justify-center sm:justify-start">
{#each $catalog as { id } (id)}
<CatalogItem {id} />
{/each}
<div class="flex gap-1 py-2">
<Search bind:value={search} />
<Button size="sm" color="alternative" on:click={onLayout}>
{#if $miscSettings.galleryLayout === 'list'}
<GridOutline />
{:else}
<ListOutline />
{/if}
</Button>
<Button size="sm" color="alternative" on:click={onOrder}>
<SortOutline />
</Button>
</div>
{#if search && sortedCatalog.length === 0}
<div class="text-center p-20">
<p>No results found.</p>
</div>
{:else}
<div class="flex sm:flex-row flex-col gap-5 flex-wrap justify-center sm:justify-start">
{#if $miscSettings.galleryLayout === 'grid'}
{#each sortedCatalog as { id } (id)}
<CatalogItem {id} />
{/each}
{:else}
<Listgroup active class="w-full">
{#each sortedCatalog as { id } (id)}
<CatalogListItem {id} />
{/each}
</Listgroup>
{/if}
</div>
{/if}
</div>
{:else}
<div class="text-center p-20">

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { catalog } from '$lib/catalog';
import { P } from 'flowbite-svelte';
export let id: string;
@@ -19,7 +18,9 @@
class="object-contain sm:w-[250px] sm:h-[350px] bg-black border-gray-900 border"
/>
{/if}
<p class="font-semibold">{manga.mokuroData.title}</p>
<p class="font-semibold sm:w-[250px] line-clamp-1">
{manga.mokuroData.title}
</p>
</div>
</a>
{/if}

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import { catalog } from '$lib/catalog';
import { ListgroupItem } from 'flowbite-svelte';
export let id: string;
$: manga = $catalog?.find((item) => item.id === id)?.manga[0];
</script>
{#if manga}
<div>
<ListgroupItem>
<a href={id} class="h-full w-full">
<div class="flex justify-between items-center">
<p class="font-semibold text-white">{manga.mokuroData.title}</p>
<img
src={URL.createObjectURL(Object.values(manga.files)[0])}
alt="img"
class="object-contain w-[50px] h-[70px] bg-black border-gray-900 border"
/>
</div>
</a>
</ListgroupItem>
</div>
{/if}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Navbar, NavBrand } from 'flowbite-svelte';
import { UserSettingsSolid, UploadSolid } from 'flowbite-svelte-icons';
import { afterNavigate } from '$app/navigation';
import { UserSettingsSolid, UploadSolid, CloudArrowUpOutline } from 'flowbite-svelte-icons';
import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/stores';
import Settings from './Settings/Settings.svelte';
import UploadModal from './UploadModal.svelte';
@@ -37,6 +37,7 @@
<div class="flex md:order-2 gap-5">
<UserSettingsSolid class="hover:text-primary-700" on:click={openSettings} />
<UploadSolid class="hover:text-primary-700" on:click={() => (uploadModalOpen = true)} />
<CloudArrowUpOutline class="hover:text-primary-700" on:click={() => goto('/cloud')} />
</div>
</Navbar>
</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { afterNavigate, beforeNavigate } from '$app/navigation';
import { cropperStore, getCroppedImg, updateLastCard, type Pixels } from '$lib/anki-connect';
import { settings } from '$lib/settings';
import { Button, Modal, Spinner } from 'flowbite-svelte';
import { onMount } from 'svelte';
import Cropper from 'svelte-easy-crop';
@@ -13,6 +14,13 @@
close();
});
beforeNavigate((nav) => {
if (open) {
nav.cancel();
close();
}
});
onMount(() => {
cropperStore.subscribe((value) => {
if (value) {
@@ -27,7 +35,7 @@
}
async function onCrop() {
if ($cropperStore?.image && $cropperStore?.sentence && pixels) {
if ($cropperStore?.image && pixels) {
loading = true;
const imageData = await getCroppedImg($cropperStore.image, pixels);
updateLastCard(imageData, $cropperStore.sentence);
@@ -43,7 +51,7 @@
<Modal title="Crop image" bind:open on:{close}>
{#if $cropperStore?.image && !loading}
<div class=" flex flex-col gap-2">
<div class="relative w-full h-[55svh] sm:h-[70svh]">
<div class="relative w-full h-[55svh] sm:h-[65svh]">
<Cropper
zoomSpeed={0.5}
maxZoom={10}
@@ -51,6 +59,12 @@
on:cropcomplete={onCropComplete}
/>
</div>
{#if $settings.ankiConnectSettings.grabSentence && $cropperStore?.sentence}
<p>
<b>Sentence:</b>
{$cropperStore?.sentence}
</p>
{/if}
<Button on:click={onCrop}>Crop</Button>
<Button on:click={close} outline color="light">Close</Button>
</div>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { Page } from '$lib/types';
import { onMount } from 'svelte';
import { afterUpdate, onMount, onDestroy } from 'svelte';
import TextBoxes from './TextBoxes.svelte';
import { zoomDefault } from '$lib/panzoom';
@@ -9,7 +9,26 @@
$: url = src ? `url(${URL.createObjectURL(src)})` : '';
let legacy: HTMLElement | null;
onMount(() => {
legacy = document.getElementById('popupAbout');
zoomDefault();
return () => {
setTimeout(() => {
zoomDefault();
}, 10);
};
});
$: {
if (legacy) {
legacy.style.backgroundImage = url;
}
}
afterUpdate(() => {
zoomDefault();
});
</script>

View File

@@ -0,0 +1,83 @@
<script lang="ts">
import { toggleFullScreen, zoomFitToScreen } from '$lib/panzoom';
import { SpeedDial, SpeedDialButton } from 'flowbite-svelte';
import { settings } from '$lib/settings';
import {
ArrowLeftOutline,
ArrowRightOutline,
CompressOutline,
ImageOutline,
ZoomOutOutline
} from 'flowbite-svelte-icons';
import { imageToWebp, showCropper, updateLastCard } from '$lib/anki-connect';
import { promptConfirmation } from '$lib/util';
export let left: (_e: any, ingoreTimeOut?: boolean) => void;
export let right: (_e: any, ingoreTimeOut?: boolean) => void;
export let src1: File;
export let src2: File | undefined;
let open = false;
function handleZoom() {
zoomFitToScreen();
open = false;
}
function handleLeft(_e: Event) {
left(_e, true);
open = false;
}
function handleRight(_e: Event) {
right(_e, true);
open = false;
}
async function onUpdateCard(src: File | undefined) {
if ($settings.ankiConnectSettings.enabled && src) {
if ($settings.ankiConnectSettings.cropImage) {
showCropper(URL.createObjectURL(src));
} else {
promptConfirmation('Add image to last created anki card?', async () => {
const imageData = await imageToWebp(src);
updateLastCard(imageData);
});
}
}
open = false;
}
</script>
{#if $settings.quickActions}
<SpeedDial
tooltip="none"
trigger="click"
defaultClass="absolute end-3 bottom-3 z-50"
color="transparent"
bind:open
>
{#if $settings.ankiConnectSettings.enabled}
<SpeedDialButton name={src2 ? '1' : undefined} on:click={() => onUpdateCard(src1)}>
<ImageOutline />
</SpeedDialButton>
{/if}
{#if $settings.ankiConnectSettings.enabled && src2}
<SpeedDialButton name="2" on:click={() => onUpdateCard(src2)}>
<ImageOutline />
</SpeedDialButton>
{/if}
<SpeedDialButton on:click={toggleFullScreen}>
<CompressOutline />
</SpeedDialButton>
<SpeedDialButton on:click={handleZoom}>
<ZoomOutOutline />
</SpeedDialButton>
<SpeedDialButton on:click={handleRight}>
<ArrowRightOutline />
</SpeedDialButton>
<SpeedDialButton on:click={handleLeft}>
<ArrowLeftOutline />
</SpeedDialButton>
</SpeedDial>
{/if}

View File

@@ -8,7 +8,7 @@
zoomFitToScreen
} from '$lib/panzoom';
import { progress, settings, updateProgress, type VolumeSettings } from '$lib/settings';
import { clamp, debounce } from '$lib/util';
import { clamp, debounce, fireExstaticEvent } from '$lib/util';
import { Input, Popover, Range, Spinner } from 'flowbite-svelte';
import MangaPage from './MangaPage.svelte';
import {
@@ -21,6 +21,9 @@
import { page as pageStore } from '$app/stores';
import SettingsButton from './SettingsButton.svelte';
import { getCharCount } from '$lib/util/count-chars';
import QuickActions from './QuickActions.svelte';
import { beforeNavigate } from '$app/navigation';
import { onMount } from 'svelte';
// TODO: Refactor this whole mess
export let volumeSettings: VolumeSettings;
@@ -64,11 +67,13 @@
return;
}
const pageClamped = clamp(newPage, 1, pages?.length);
const { charCount } = getCharCount(pages, pageClamped);
updateProgress(
volume.mokuroData.volume_uuid,
pageClamped,
getCharCount(pages, pageClamped) || 0,
pageClamped === pages.length
charCount,
pageClamped === pages.length || pageClamped === pages.length - 1
);
zoomDefault();
}
@@ -110,14 +115,19 @@
switch (action) {
case 'ArrowLeft':
case 'ArrowUp':
case 'PageUp':
left(event, true);
return;
case 'ArrowUp':
case 'PageUp':
changePage(page - navAmount, true);
return;
case 'ArrowRight':
right(event, true);
return;
case 'ArrowDown':
case 'PageDown':
right(event, true);
case 'Space':
changePage(page + navAmount, true);
return;
case 'Home':
changePage(1, true);
@@ -127,11 +137,6 @@
changePage(pages.length, true);
}
return;
case 'Space':
if (pages && page + 1 <= pages.length) {
changePage(page + 1, true);
}
return;
case 'KeyF':
toggleFullScreen();
return;
@@ -140,8 +145,9 @@
}
}
$: charCount = $settings.charCount ? getCharCount(pages, page) : 0;
$: maxCharCount = getCharCount(pages);
$: charCount = $settings.charCount ? getCharCount(pages, page).charCount : 0;
$: maxCharCount = getCharCount(pages).charCount;
$: totalLineCount = getCharCount(pages).lineCount;
let startX = 0;
let startY = 0;
@@ -190,13 +196,57 @@
const { clientX, clientY } = event;
const { scale } = $panzoomStore.getTransform();
if (scale < 0.5) {
$panzoomStore.zoomTo(clientX, clientY, 3);
if (scale < 1) {
$panzoomStore.zoomTo(clientX, clientY, 1.5);
} else {
zoomFitToScreen();
}
}
}
$: {
if (volume) {
const { charCount, lineCount } = getCharCount(pages, page);
fireExstaticEvent('mokuro-reader:page.change', {
title: volume.mokuroData.title,
volumeName: volume.mokuroData.volume,
currentCharCount: charCount,
currentPage: page,
totalPages: pages.length,
totalCharCount: maxCharCount || 0,
currentLineCount: lineCount,
totalLineCount
});
}
}
onMount(() => {
if ($settings.defaultFullscreen) {
document.documentElement.requestFullscreen();
}
});
beforeNavigate(() => {
if (document.exitFullscreen) {
document.exitFullscreen();
}
if (volume) {
const { charCount, lineCount } = getCharCount(pages, page);
fireExstaticEvent('mokuro-reader:reader.closed', {
title: volume.mokuroData.title,
volumeName: volume.mokuroData.volume,
currentCharCount: charCount,
currentPage: page,
totalPages: pages.length,
totalCharCount: maxCharCount || 0,
currentLineCount: lineCount,
totalLineCount
});
}
});
</script>
<svelte:window
@@ -208,8 +258,14 @@
<svelte:head>
<title>{volume?.mokuroData.volume || 'Volume'}</title>
</svelte:head>
<SettingsButton />
{#if volume && pages}
<QuickActions
{left}
{right}
src1={Object.values(volume?.files)[index]}
src2={!volumeSettings.singlePageView ? Object.values(volume?.files)[index + 1] : undefined}
/>
<SettingsButton />
<Cropper />
<Popover placement="bottom" trigger="click" triggeredBy="#page-num" class="z-20 w-full max-w-xs">
<div class="flex flex-col gap-3">
@@ -261,11 +317,13 @@
<Panzoom>
<button
class="h-full fixed -left-full z-10 w-full hover:bg-slate-400 opacity-[0.01]"
style:margin-left={`${$settings.edgeButtonWidth}px`}
on:mousedown={mouseDown}
on:mouseup={left}
/>
<button
class="h-full fixed -right-full z-10 w-full hover:bg-slate-400 opacity-[0.01]"
style:margin-right={`${$settings.edgeButtonWidth}px`}
on:mousedown={mouseDown}
on:mouseup={right}
/>
@@ -282,13 +340,17 @@
<div
class="flex flex-row"
class:flex-row-reverse={!volumeSettings.rightToLeft}
style:filter={`invert(${$settings.invertColors ? 1 : 0})`}
on:dblclick={onDoubleTap}
role="none"
id="manga-panel"
>
{#if showSecondPage()}
<MangaPage page={pages[index + 1]} src={Object.values(volume?.files)[index + 1]} />
{/if}
<MangaPage page={pages[index]} src={Object.values(volume?.files)[index]} />
{#key page}
{#if showSecondPage()}
<MangaPage page={pages[index + 1]} src={Object.values(volume?.files)[index + 1]} />
{/if}
<MangaPage page={pages[index]} src={Object.values(volume?.files)[index]} />
{/key}
</div>
</Panzoom>
</div>
@@ -297,15 +359,17 @@
on:mousedown={mouseDown}
on:mouseup={left}
class="left-0 top-0 absolute h-full w-16 hover:bg-slate-400 opacity-[0.01]"
style:width={`${$settings.edgeButtonWidth}px`}
/>
<button
on:mousedown={mouseDown}
on:mouseup={right}
class="right-0 top-0 absolute h-full w-16 hover:bg-slate-400 opacity-[0.01]"
style:width={`${$settings.edgeButtonWidth}px`}
/>
{/if}
{:else}
<div class="fixed z-50 left-1/2 top-1/2">
<Spinner />
</div>
{/if}
{/if}

View File

@@ -7,38 +7,46 @@
export let page: Page;
export let src: File;
$: textBoxes = page.blocks.map((block) => {
const { img_height, img_width } = page;
const { box, font_size, lines, vertical } = block;
$: textBoxes = page.blocks
.map((block) => {
const { img_height, img_width } = page;
const { box, font_size, lines, vertical } = block;
let [_xmin, _ymin, _xmax, _ymax] = box;
let [_xmin, _ymin, _xmax, _ymax] = box;
const xmin = clamp(_xmin, 0, img_width);
const ymin = clamp(_ymin, 0, img_height);
const xmax = clamp(_xmax, 0, img_width);
const ymax = clamp(_ymax, 0, img_height);
const xmin = clamp(_xmin, 0, img_width);
const ymin = clamp(_ymin, 0, img_height);
const xmax = clamp(_xmax, 0, img_width);
const ymax = clamp(_ymax, 0, img_height);
const width = xmax - xmin;
const height = ymax - ymin;
const width = xmax - xmin;
const height = ymax - ymin;
const area = width * height;
const textBox = {
left: `${xmin}px`,
top: `${ymin}px`,
width: `${width}px`,
height: `${height}px`,
fontSize: $settings.fontSize === 'auto' ? `${font_size}px` : `${$settings.fontSize}pt`,
writingMode: vertical ? 'vertical-rl' : 'horizontal-tb',
lines
};
const textBox = {
left: `${xmin}px`,
top: `${ymin}px`,
width: `${width}px`,
height: `${height}px`,
fontSize: $settings.fontSize === 'auto' ? `${font_size}px` : `${$settings.fontSize}pt`,
writingMode: vertical ? 'vertical-rl' : 'horizontal-tb',
lines,
area
};
return textBox;
});
return textBox;
})
.sort(({ area: a }, { area: b }) => {
return b - a;
});
$: fontWeight = $settings.boldFont ? 'bold' : '400';
$: display = $settings.displayOCR ? 'block' : 'none';
$: border = $settings.textBoxBorders ? '1px solid red' : 'none';
$: contenteditable = $settings.textEditable;
$: triggerMethod = $settings.ankiConnectSettings.triggerMethod || 'both';
async function onUpdateCard(lines: string[]) {
if ($settings.ankiConnectSettings.enabled) {
const sentence = lines.join(' ');
@@ -54,16 +62,23 @@
}
function onContextMenu(event: Event, lines: string[]) {
if ($settings.ankiConnectSettings.enabled) {
if (triggerMethod === 'both' || triggerMethod === 'rightClick') {
event.preventDefault();
onUpdateCard(lines);
}
}
function onDoubleTap(event: Event, lines: string[]) {
if (triggerMethod === 'both' || triggerMethod === 'doubleTap') {
event.preventDefault();
onUpdateCard(lines);
}
}
</script>
{#each textBoxes as { fontSize, height, left, lines, top, width, writingMode }, index (`text-box-${index}`)}
{#each textBoxes as { fontSize, height, left, lines, top, width, writingMode }, index (`textBox-${index}`)}
<div
class="text-box"
class="textBox"
style:width
style:height
style:left
@@ -75,7 +90,7 @@
style:writing-mode={writingMode}
role="none"
on:contextmenu={(e) => onContextMenu(e, lines)}
on:dblclick={() => onUpdateCard(lines)}
on:dblclick={(e) => onDoubleTap(e, lines)}
{contenteditable}
>
{#each lines as line}
@@ -85,7 +100,7 @@
{/each}
<style>
.text-box {
.textBox {
color: black;
padding: 0;
position: absolute;
@@ -96,13 +111,13 @@
z-index: 11;
}
.text-box:focus,
.text-box:hover {
.textBox:focus,
.textBox:hover {
background: rgb(255, 255, 255);
border: 1px solid rgba(0, 0, 0, 0);
}
.text-box p {
.textBox p {
display: none;
white-space: nowrap;
letter-spacing: 0.1em;
@@ -113,8 +128,8 @@
z-index: 11;
}
.text-box:focus p,
.text-box:hover p {
.textBox:focus p,
.textBox:hover p {
display: table;
}
</style>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import { startCount, volumeStats } from '$lib/settings';
export let count: number | undefined;
export let volumeId: string;
$: active = Boolean(count);
function onClick() {
if (count) {
clearInterval(count);
count = undefined;
} else {
count = startCount(volumeId);
}
}
</script>
<button
class:text-primary-700={!active}
class="mix-blend-difference z-10 fixed opacity-50 right-20 top-5 p-10 m-[-2.5rem]"
on:click={onClick}
>
<p>
{active ? 'Active' : 'Paused'} | Minutes read: {$volumeStats?.timeReadInMinutes}
</p>
</button>

View File

@@ -1,15 +1,8 @@
<script lang="ts">
import { READER_VERSION } from '$lib/consts';
import { showSnackbar } from '$lib/util';
import { toClipboard } from '$lib/util';
import { A, AccordionItem, Badge, Helper, Span } from 'flowbite-svelte';
import { GithubSolid } from 'flowbite-svelte-icons';
function toClipboard() {
navigator.clipboard.writeText(
'pip install git+https://github.com/kha-white/mokuro.git@web-reader'
);
showSnackbar('Copied to clipboard');
}
</script>
<AccordionItem>
@@ -38,7 +31,7 @@
</p>
<div role="none" on:click={toClipboard}>
<code class="text-primary-600 bg-slate-900"
>pip install git+https://github.com/kha-white/mokuro.git@web-reader</code
>pip3 install git+https://github.com/kha-white/mokuro.git@web-reader</code
>
</div>
</div>
@@ -48,7 +41,7 @@
</p>
<Helper
>Created by <A href="https://github.com/ZXY101">ZXY101</A> & <A
class="https://github.com/kha-white/mokuro">kha-white</A
href="https://github.com/kha-white">kha-white</A
></Helper
>
</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import { settings, updateAnkiSetting } from '$lib/settings';
import { AccordionItem, Label, Toggle, Input, Helper } from 'flowbite-svelte';
import { AccordionItem, Label, Toggle, Input, Helper, Select } from 'flowbite-svelte';
$: disabled = !$settings.ankiConnectSettings.enabled;
@@ -11,16 +12,29 @@
let pictureField = $settings.ankiConnectSettings.pictureField;
let sentenceField = $settings.ankiConnectSettings.sentenceField;
let triggerMethod = $settings.ankiConnectSettings.triggerMethod;
const triggerOptions = [
{ value: 'rightClick', name: 'Right click (long press on mobile)' },
{ value: 'doubleTap', name: 'Double tap' },
{ value: 'both', name: 'Both' },
{ value: 'neither', name: 'Neither' }
];
</script>
<AccordionItem>
<span slot="header">Anki Connect</span>
<div class="flex flex-col gap-5">
<Helper
>For anki connect integration to work, you must add the reader to your anki connect <code
class="text-primary-500">webCorsOriginList</code
> list</Helper
>For anki connect integration to work, you must add the reader (<code class="text-primary-500"
>{$page.url.origin}</code
>) to your anki connect <b class="text-primary-500">webCorsOriginList</b> list</Helper
>
<Helper>
To trigger the anki connect integration, double click or right click (long press on mobile)
any text box.
</Helper>
<div>
<Toggle bind:checked={enabled} on:change={() => updateAnkiSetting('enabled', enabled)}
>AnkiConnect Integration Enabled</Toggle
@@ -66,5 +80,15 @@
on:change={() => updateAnkiSetting('grabSentence', grabSentence)}>Grab sentence</Toggle
>
</div>
<div>
<Label>
Trigger method:
<Select
on:change={() => updateAnkiSetting('triggerMethod', triggerMethod)}
items={triggerOptions}
bind:value={triggerMethod}
/>
</Label>
</div>
</div>
</AccordionItem>

View File

@@ -7,7 +7,7 @@
renameProfile
} from '$lib/settings';
import { promptConfirmation, showSnackbar } from '$lib/util';
import { Listgroup, ListgroupItem, Modal, Input, Popover } from 'flowbite-svelte';
import { Listgroup, ListgroupItem, Modal, Input } from 'flowbite-svelte';
import {
CirclePlusSolid,
CopySolid,

View File

@@ -2,6 +2,7 @@
import { changeProfile, currentProfile, profiles } from '$lib/settings';
import { AccordionItem, Button, Select } from 'flowbite-svelte';
import ManageProfilesModal from './ManageProfilesModal.svelte';
import { showSnackbar } from '$lib/util';
export let onClose: () => void;
@@ -16,6 +17,37 @@
onClose();
}
function exportProfiles() {
const link = document.createElement('a');
const json = localStorage.getItem('profiles') || '';
link.href = URL.createObjectURL(new Blob([json], { type: 'application/json' }));
link.download = 'profiles.json';
link.click();
showSnackbar('Profiles exported');
}
let files: FileList;
function importProfile() {
const [file] = files;
const reader = new FileReader();
reader.onloadend = () => {
const imported = JSON.parse(reader.result?.toString() || '');
profiles.update((prev) => {
return {
...prev,
...imported
};
});
onClose();
showSnackbar('Profiles imported');
};
if (file) {
reader.readAsText(file);
}
}
let manageModalOpen = false;
</script>
@@ -30,5 +62,13 @@
>Manage profiles</Button
>
</div>
<hr class="border-gray-100 opacity-10" />
<div class="flex flex-col gap-2">
<input class="border border-slate-700 rounded-lg" type="file" accept=".json" bind:files />
<Button on:click={importProfile} disabled={!files} size="sm" outline color="blue"
>Import profiles</Button
>
<Button on:click={exportProfiles} size="sm" color="light">Export profiles</Button>
</div>
</div>
</AccordionItem>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { toggleFullScreen } from '$lib/panzoom';
import { isReader } from '$lib/util';
import { Button } from 'flowbite-svelte';
export let hidden = false;
function onClose() {
hidden = true;
history.back();
}
</script>
{#if isReader()}
<div class="flex flex-col gap-2">
<Button color="alternative" on:click={toggleFullScreen}>Toggle fullscreen</Button>
<Button color="alternative" on:click={onClose}>Close reader</Button>
</div>
{/if}

View File

@@ -4,9 +4,14 @@
import ReaderToggles from './ReaderToggles.svelte';
import { settings, updateSetting } from '$lib/settings';
let value = $settings.swipeThreshold;
function onChange() {
updateSetting('swipeThreshold', value);
let swipeThresholdValue = $settings.swipeThreshold;
let edgeButtonWidthValue = $settings.edgeButtonWidth;
function onSwipeChange() {
updateSetting('swipeThreshold', swipeThresholdValue);
}
function onWidthChange() {
updateSetting('edgeButtonWidth', edgeButtonWidthValue);
}
</script>
@@ -18,7 +23,17 @@
<ReaderToggles />
<div>
<Label>Swipe threshold</Label>
<Range on:change={onChange} min={20} max={90} disabled={!$settings.mobile} bind:value />
<Range
on:change={onSwipeChange}
min={20}
max={90}
disabled={!$settings.mobile}
bind:value={swipeThresholdValue}
/>
</div>
<div>
<Label>Edge button width</Label>
<Range on:change={onWidthChange} min={1} max={100} bind:value={edgeButtonWidthValue} />
</div>
</div>
</AccordionItem>

View File

@@ -3,13 +3,18 @@
import { Toggle } from 'flowbite-svelte';
$: toggles = [
{ key: 'defaultFullscreen', text: 'Open reader in fullscreen', value: $settings.defaultFullscreen },
{ key: 'textEditable', text: 'Editable text', value: $settings.textEditable },
{ key: 'textBoxBorders', text: 'Text box borders', value: $settings.textBoxBorders },
{ key: 'displayOCR', text: 'OCR enabled', value: $settings.displayOCR },
{ key: 'boldFont', text: 'Bold font', value: $settings.boldFont },
{ key: 'pageNum', text: 'Show page number', value: $settings.pageNum },
{ key: 'charCount', text: 'Show character count', value: $settings.charCount },
{ key: 'mobile', text: 'Mobile', value: $settings.mobile }
{ key: 'bounds', text: 'Bounds', value: $settings.bounds },
{ key: 'mobile', text: 'Mobile', value: $settings.mobile },
{ key: 'showTimer', text: 'Show timer', value: $settings.showTimer },
{ key: 'quickActions', text: 'Show quick actions', value: $settings.quickActions },
{ key: 'invertColors', text: 'Invert colors of the images', value: $settings.invertColors }
] as { key: SettingsKey; text: string; value: any }[];
</script>

View File

@@ -12,6 +12,8 @@
import VolumeDefaults from './Volume/VolumeDefaults.svelte';
import VolumeSettings from './Volume/VolumeSettings.svelte';
import About from './About.svelte';
import QuickAccess from './QuickAccess.svelte';
import { beforeNavigate } from '$app/navigation';
let transitionParams = {
x: 320,
@@ -29,6 +31,13 @@
function onClose() {
hidden = true;
}
beforeNavigate((nav) => {
if (!hidden) {
nav.cancel();
hidden = true;
}
});
</script>
<Drawer
@@ -47,6 +56,7 @@
</div>
<div class="flex flex-col gap-5">
<Accordion flush>
<QuickAccess bind:hidden />
{#if isReader()}
<VolumeSettings />
{:else}

View File

@@ -1,44 +1,14 @@
<script lang="ts">
import { volumes } from '$lib/settings';
import { AccordionItem, P } from 'flowbite-svelte';
$: completed = $volumes
? Object.values($volumes).reduce((total: number, { completed }) => {
if (completed) {
total++;
}
return total;
}, 0)
: 0;
$: pagesRead = $volumes
? Object.values($volumes).reduce((total: number, { progress }) => {
total += progress;
return total;
}, 0)
: 0;
$: charsRead = $volumes
? Object.values($volumes).reduce((total: number, { chars }) => {
total += chars;
return total;
}, 0)
: 0;
$: minutesRead = $volumes
? Object.values($volumes).reduce((total: number, { timeReadInMinutes }) => {
total += timeReadInMinutes;
return total;
}, 0)
: 0;
import { totalStats } from '$lib/settings';
import { AccordionItem } from 'flowbite-svelte';
</script>
<AccordionItem>
<span slot="header">Stats</span>
<div>
<p>Completed volumes: {completed}</p>
<p>Pages read: {pagesRead}</p>
<p>Characters read: {charsRead}</p>
<p>Minutes read: {minutesRead}</p>
<p>Completed volumes: {$totalStats?.completed || 0}</p>
<p>Pages read: {$totalStats?.pagesRead || 0}</p>
<p>Characters read: {$totalStats?.charsRead || 0}</p>
<p>Minutes read: {$totalStats?.minutesRead || 0}</p>
</div>
</AccordionItem>

View File

@@ -3,13 +3,13 @@
import { AccordionItem, Helper, Toggle } from 'flowbite-svelte';
$: toggles = [
{ key: 'rightToLeft', text: 'Right to left', value: $settings.volumeDefaults.rightToLeft },
{ key: 'rightToLeft', text: 'Right to left', value: $settings.volumeDefaults?.rightToLeft },
{
key: 'singlePageView',
text: 'Single page view',
value: $settings.volumeDefaults.singlePageView
value: $settings.volumeDefaults?.singlePageView
},
{ key: 'hasCover', text: 'First page is cover', value: $settings.volumeDefaults.hasCover }
{ key: 'hasCover', text: 'First page is cover', value: $settings.volumeDefaults?.hasCover }
] as { key: VolumeDefaultsKey; text: string; value: any }[];
</script>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { toggleFullScreen, zoomDefault } from '$lib/panzoom';
import { zoomDefault } from '$lib/panzoom';
import {
updateProgress,
updateVolumeSetting,
@@ -8,7 +8,7 @@
volumeSettings,
type VolumeSettingsKey
} from '$lib/settings';
import { AccordionItem, Button, Helper, Toggle } from 'flowbite-svelte';
import { AccordionItem, Helper, Toggle } from 'flowbite-svelte';
const volumeId = $page.params.volume;
@@ -37,6 +37,5 @@
{#each toggles as { key, text, value }}
<Toggle size="small" checked={value} on:change={() => onChange(key, value)}>{text}</Toggle>
{/each}
<Button color="alternative" on:click={toggleFullScreen}>Toggle fullscreen</Button>
</div>
</AccordionItem>

View File

@@ -1,10 +1,11 @@
<script lang="ts">
import { Button, Dropzone, Modal, Spinner } from 'flowbite-svelte';
import { Button, Dropzone, Modal, Spinner, Accordion, AccordionItem } from 'flowbite-svelte';
import FileUpload from './FileUpload.svelte';
import { processFiles } from '$lib/upload';
import { onMount } from 'svelte';
import { scanFiles } from '$lib/upload';
import { formatBytes } from '$lib/util/upload';
import { toClipboard } from '$lib/util';
export let open = false;
@@ -90,6 +91,33 @@
<h2 class="justify-center flex">Loading...</h2>
<div class="text-center"><Spinner /></div>
{:then}
<Accordion flush>
<AccordionItem>
<span slot="header">What to upload?</span>
<div class="flex flex-col gap-5">
<div>
<p>
Firstly, ensure that you process your manga with the <b>0.2.0-beta.6</b> of mokuro, you
can install it by running the following command:
</p>
<div role="none" on:click={toClipboard}>
<code class="text-primary-600 bg-slate-900"
>pip3 install git+https://github.com/kha-white/mokuro.git@web-reader</code
>
</div>
</div>
<p>
This will generate a <code>.mokuro</code> file for each volume processed, upload your
manga along with the <code>.mokuro</code> files.
</p>
<p>
On mobile, uploading via directory is not supported so you will need to zip your manga
first and then upload it via
<code class="text-primary-600 bg-slate-900">choose files</code>.
</p>
</div>
</AccordionItem>
</Accordion>
<Dropzone
id="dropzone"
on:drop={dropHandle}
@@ -141,7 +169,6 @@
</p>
{/if}
</Dropzone>
<p class=" text-sm text-gray-500 dark:text-gray-400 text-center">{storageSpace}</p>
<div class="flex flex-1 flex-col gap-2">
<Button outline on:click={reset} {disabled} color="dark">Reset</Button>

View File

@@ -1,34 +1,72 @@
<script lang="ts">
import { page } from '$app/stores';
import { progress } from '$lib/settings';
import { deleteVolume, progress } from '$lib/settings';
import type { Volume } from '$lib/types';
import { ListgroupItem } from 'flowbite-svelte';
import { CheckCircleSolid } from 'flowbite-svelte-icons';
import type { ListGroupItemType } from 'flowbite-svelte/dist/types';
import { promptConfirmation } from '$lib/util';
import { ListgroupItem, Frame } from 'flowbite-svelte';
import { CheckCircleSolid, TrashBinSolid } from 'flowbite-svelte-icons';
import { goto } from '$app/navigation';
import { db } from '$lib/catalog/db';
export let item: string | ListGroupItemType;
const volume = item as Volume;
export let volume: Volume;
const { volumeName, mokuroData } = volume as Volume;
const { title_uuid, volume_uuid } = mokuroData;
const volName = decodeURI(volumeName);
$: currentPage = $progress?.[volume?.mokuroData.volume_uuid || 0] || 1;
$: progressDisplay = `${currentPage} / ${volume.mokuroData.pages.length}`;
$: isComplete = currentPage === volume.mokuroData.pages.length;
$: currentPage = $progress?.[volume_uuid || 0] || 1;
$: progressDisplay = `${
currentPage === volume.mokuroData.pages.length - 1 ? currentPage + 1 : currentPage
} / ${volume.mokuroData.pages.length}`;
$: isComplete =
currentPage === volume.mokuroData.pages.length ||
currentPage === volume.mokuroData.pages.length - 1;
async function onDeleteClicked(e: Event) {
e.stopPropagation();
promptConfirmation(`Delete ${volName}?`, async () => {
const existingCatalog = await db.catalog.get(title_uuid);
const updated = existingCatalog?.manga.filter(({ mokuroData }) => {
return mokuroData.volume_uuid !== volume_uuid;
});
deleteVolume(volume_uuid);
if (updated && updated.length > 0) {
await db.catalog.update(title_uuid, { manga: updated });
goto(`/${$page.params.manga}`);
} else {
db.catalog.delete(title_uuid);
goto('/');
}
});
}
</script>
<a href={`${$page.params.manga}/${mokuroData.volume_uuid}`} class="h-full w-full">
<ListgroupItem>
<div
class:text-green-400={isComplete}
class="flex flex-row gap-5 items-center justify-between w-full"
{#if $page.params.manga}
<Frame rounded border class="divide-y divide-gray-200 dark:divide-gray-600">
<ListgroupItem
on:click={() => goto(`/${$page.params.manga}/${volume_uuid}`)}
normalClass="py-4"
>
<div>
<p class="font-semibold" class:text-white={!isComplete}>{decodeURI(volumeName)}</p>
<p>{progressDisplay}</p>
<div
class:text-green-400={isComplete}
class="flex flex-row gap-5 items-center justify-between w-full"
>
<div>
<p class="font-semibold" class:text-white={!isComplete}>{volName}</p>
<p>{progressDisplay}</p>
</div>
<div class="flex gap-2">
<TrashBinSolid
class="text-red-400 hover:text-red-500 z-10 poin"
on:click={onDeleteClicked}
/>
{#if isComplete}
<CheckCircleSolid />
{/if}
</div>
</div>
{#if isComplete}
<CheckCircleSolid />
{/if}
</div>
</ListgroupItem>
</a>
</ListgroupItem>
</Frame>
{/if}

View File

@@ -1 +1 @@
export const READER_VERSION = '0.9.0'
export const READER_VERSION = '0.9.1'

View File

@@ -147,9 +147,9 @@ export function keepInBounds() {
return
}
const { mobile } = get(settings)
const { mobile, bounds } = get(settings)
if (!mobile) {
if (!mobile && !bounds) {
return
}
@@ -161,7 +161,7 @@ export function keepInBounds() {
const width = container.offsetWidth * scale;
const height = container.offsetHeight * scale;
const marginX = innerWidth * 0.01;
const marginX = innerWidth * 0.001;
const marginY = innerHeight * 0.01;
let minX = innerWidth - width - marginX;
@@ -190,7 +190,6 @@ export function keepInBounds() {
if (x < minX) {
transform.x = minX;
}
if (x > maxX) {
transform.x = maxX;

View File

@@ -1,2 +1,3 @@
export * from './volume-data'
export * from './settings'
export * from './settings'
export * from './misc'

33
src/lib/settings/misc.ts Normal file
View File

@@ -0,0 +1,33 @@
import { browser } from '$app/environment';
import { writable } from 'svelte/store';
export type MiscSettings = {
galleryLayout: 'grid' | 'list';
gallerySorting: 'ASC' | 'DESC';
};
export type MiscSettingsKey = keyof MiscSettings;
const defaultSettings: MiscSettings = {
galleryLayout: 'grid',
gallerySorting: 'ASC',
}
const stored = browser ? window.localStorage.getItem('miscSettings') : undefined;
export const miscSettings = writable<MiscSettings>(stored ? JSON.parse(stored) : defaultSettings);
miscSettings.subscribe((miscSettings) => {
if (browser) {
window.localStorage.setItem('miscSettings', JSON.stringify(miscSettings));
}
});
export function updateMiscSetting(key: MiscSettingsKey, value: any) {
miscSettings.update((miscSettings) => {
return {
...miscSettings,
[key]: value
};
});
}

View File

@@ -1,5 +1,4 @@
import { browser } from '$app/environment';
import { zoomDefault } from '$lib/panzoom';
import { derived, get, writable } from 'svelte/store';
export type FontSize =
@@ -32,6 +31,7 @@ export type AnkiConnectSettings = {
cropImage: boolean;
overwriteImage: boolean;
grabSentence: boolean;
triggerMethod: 'rightClick' | 'doubleTap' | 'both'
}
export type VolumeDefaults = {
@@ -41,17 +41,23 @@ export type VolumeDefaults = {
}
export type Settings = {
defaultFullscreen: boolean;
textEditable: boolean;
textBoxBorders: boolean;
displayOCR: boolean;
boldFont: boolean;
pageNum: boolean;
charCount: boolean;
bounds: boolean;
mobile: boolean;
backgroundColor: string;
swipeThreshold: number;
edgeButtonWidth: number;
showTimer: boolean;
quickActions: boolean;
fontSize: FontSize;
zoomDefault: ZoomModes;
invertColors: boolean;
volumeDefaults: VolumeDefaults;
ankiConnectSettings: AnkiConnectSettings;
};
@@ -63,6 +69,7 @@ export type AnkiSettingsKey = keyof AnkiConnectSettings;
export type VolumeDefaultsKey = keyof VolumeDefaults;
const defaultSettings: Settings = {
defaultFullscreen: false,
displayOCR: true,
textEditable: false,
textBoxBorders: false,
@@ -70,10 +77,15 @@ const defaultSettings: Settings = {
pageNum: true,
charCount: false,
mobile: false,
bounds: false,
backgroundColor: '#030712',
swipeThreshold: 50,
edgeButtonWidth: 40,
showTimer: false,
quickActions: true,
fontSize: 'auto',
zoomDefault: 'zoomFitToScreen',
invertColors: false,
volumeDefaults: {
singlePageView: false,
rightToLeft: true,
@@ -85,7 +97,8 @@ const defaultSettings: Settings = {
grabSentence: false,
overwriteImage: true,
pictureField: 'Picture',
sentenceField: 'Sentence'
sentenceField: 'Sentence',
triggerMethod: 'both'
}
};
@@ -216,4 +229,4 @@ export function copyProfile(profileToCopy: string, newName: string) {
export function changeProfile(profileId: string) {
currentProfile.set(profileId)
}
}

View File

@@ -1,7 +1,9 @@
import { browser } from '$app/environment';
import { derived, get, writable } from 'svelte/store';
import { settings } from './settings';
import { settings, updateSetting, } from './settings';
import { zoomDefault } from '$lib/panzoom';
import { page } from '$app/stores';
import { manga, volume } from '$lib/catalog';
export type VolumeSettings = {
rightToLeft: boolean;
@@ -21,6 +23,13 @@ type VolumeData = {
settings: VolumeSettings;
}
type TotalStats = {
completed: number;
pagesRead: number;
charsRead: number;
minutesRead: number;
}
type Volumes = Record<string, VolumeData>;
@@ -30,7 +39,17 @@ const initial: Volumes = stored && browser ? JSON.parse(stored) : {};
export const volumes = writable<Volumes>(initial);
export function initializeVolume(volume: string) {
const { hasCover, rightToLeft, singlePageView } = get(settings).volumeDefaults
const volumeDefaults = get(settings).volumeDefaults;
if (!volumeDefaults) {
updateSetting('volumeDefaults', {
singlePageView: false,
rightToLeft: true,
hasCover: false
})
}
const { hasCover, rightToLeft, singlePageView } = volumeDefaults
volumes.update((prev) => {
return {
...prev,
@@ -132,4 +151,51 @@ export function updateVolumeSetting(volume: string, key: VolumeSettingsKey, valu
};
});
zoomDefault();
}
}
export const totalStats = derived([volumes, page], ([$volumes, $page]) => {
if ($page && $volumes) {
return Object.values($volumes).reduce<TotalStats>((stats, { chars, completed, timeReadInMinutes, progress }) => {
if (completed) {
stats.completed++;
}
stats.pagesRead += progress;
stats.minutesRead += timeReadInMinutes;
stats.charsRead += chars
return stats;
}, {
charsRead: 0,
completed: 0,
pagesRead: 0,
minutesRead: 0
})
}
})
export const mangaStats = derived([manga, volumes], ([$manga, $volumes]) => {
if ($manga && $volumes) {
return $manga.map((vol) => vol.mokuroData.volume_uuid).reduce(
(stats: any, volumeId) => {
const timeReadInMinutes = $volumes[volumeId]?.timeReadInMinutes || 0;
const chars = $volumes[volumeId]?.chars || 0;
const completed = $volumes[volumeId]?.completed || 0;
stats.timeReadInMinutes = stats.timeReadInMinutes + timeReadInMinutes;
stats.chars = stats.chars + chars;
stats.completed = stats.completed + completed;
return stats;
},
{ timeReadInMinutes: 0, chars: 0, completed: 0 }
);
}
});
export const volumeStats = derived([volume, volumes], ([$volume, $volumes]) => {
if ($volume && $volumes) {
const { chars, completed, timeReadInMinutes, progress } = $volumes[$volume.mokuroData.volume_uuid]
return { chars, completed, timeReadInMinutes, progress }
}
});

View File

@@ -2,27 +2,41 @@ import { db } from '$lib/catalog/db';
import type { Volume } from '$lib/types';
import { showSnackbar } from '$lib/util/snackbar';
import { requestPersistentStorage } from '$lib/util/upload';
import { BlobReader, ZipReader, BlobWriter, getMimeType } from '@zip.js/zip.js';
import { ZipReader, BlobWriter, getMimeType, Uint8ArrayReader } from '@zip.js/zip.js';
export * from './web-import'
const zipTypes = ['zip', 'cbz', 'ZIP', 'CBZ'];
const imageTypes = ['image/jpeg', 'image/png', 'image/webp'];
export async function unzipManga(file: File) {
const zipFileReader = new BlobReader(file);
const zipFileReader = new Uint8ArrayReader(new Uint8Array(await file.arrayBuffer()));
const zipReader = new ZipReader(zipFileReader);
const entries = await zipReader.getEntries();
const unzippedFiles: Record<string, File> = {};
for (const entry of entries) {
const sortedEntries = entries.sort((a, b) => {
return a.filename.localeCompare(b.filename, undefined, {
numeric: true,
sensitivity: 'base'
});
})
for (const entry of sortedEntries) {
const mime = getMimeType(entry.filename);
if (imageTypes.includes(mime)) {
const isMokuroFile = entry.filename.split('.').pop() === 'mokuro'
if (imageTypes.includes(mime) || isMokuroFile) {
const blob = await entry.getData?.(new BlobWriter(mime));
if (blob) {
const file = new File([blob], entry.filename, { type: mime });
const fileName = entry.filename.split('/').pop() || entry.filename;
const file = new File([blob], fileName, { type: mime });
if (!file.webkitRelativePath) {
Object.defineProperty(file, 'webkitRelativePath', {
value: entry.filename
})
}
unzippedFiles[entry.filename] = file;
}
}
@@ -90,10 +104,17 @@ export async function scanFiles(item: FileSystemEntry, files: Promise<File | und
}
}
export async function processFiles(files: File[]) {
export async function processFiles(_files: File[]) {
const volumes: Record<string, Volume> = {};
const mangas: string[] = [];
const files = _files.sort((a, b) => {
return decodeURI(a.name).localeCompare(decodeURI(b.name), undefined, {
numeric: true,
sensitivity: 'base'
});
})
for (const file of files) {
const { ext, filename, path } = getDetails(file);
@@ -148,6 +169,11 @@ export async function processFiles(files: File[]) {
if (ext && zipTypes.includes(ext)) {
const unzippedFiles = await unzipManga(file);
if (files.length === 1) {
processFiles(Object.values(unzippedFiles))
return;
}
volumes[path] = {
...volumes[path],
files: unzippedFiles

32
src/lib/util/cloud.ts Normal file
View File

@@ -0,0 +1,32 @@
type FileInfo = {
accessToken: string;
metadata: any;
fileId?: string;
localStorageId: string;
type: string;
}
const FILES_API_URL = 'https://www.googleapis.com/upload/drive/v3/files';
export async function uploadFile({ accessToken, fileId, localStorageId, metadata, type }: FileInfo) {
const json = localStorage.getItem(localStorageId) || '';
const blob = new Blob([json], { type });
const form = new FormData();
form.append('resource', new Blob([JSON.stringify(metadata)], { type }));
form.append('file', blob);
const res = await fetch(
`${FILES_API_URL}${fileId ? `/${fileId}` : ''}?uploadType=multipart`,
{
method: fileId ? 'PATCH' : 'POST',
headers: new Headers({ Authorization: 'Bearer ' + accessToken }),
body: form
}
);
return await res.json()
}

View File

@@ -12,24 +12,28 @@ import type { Page } from "$lib/types";
export function countChars(line: string) {
const isNotJapaneseRegex = /[^0-9A-Z-------\p{Radical}\p{Unified_Ideograph}]+/gimu
const cleaned = line.replace(isNotJapaneseRegex, '')
return cleaned.length;
return Array.from(cleaned).length;
}
export function getCharCount(pages: Page[], currentPage?: number) {
let charCount = 0;
let lineCount = 0;
if (pages && pages.length > 0) {
const max = currentPage || pages.length
let charCount = 0;
for (let i = 0; i < max; i++) {
const blocks = pages[i].blocks;
blocks.forEach((block) => {
lineCount += block.lines.length;
block.lines.forEach((line) => {
charCount += countChars(line);
});
});
}
return charCount;
}
return { charCount, lineCount };
}

View File

@@ -2,3 +2,5 @@ export * from './snackbar';
export * from './upload';
export * from './misc';
export * from './modals';
export * from './zip'
export * from './cloud'

View File

@@ -1,5 +1,7 @@
import { page } from "$app/stores";
import { get } from "svelte/store";
import { showSnackbar } from "./snackbar";
import { browser } from "$app/environment";
export function clamp(num: number, min: number, max: number) {
return Math.min(Math.max(num, min), max);
@@ -22,4 +24,30 @@ export function debounce(func: () => void, timeout = 50) {
clearTimeout(timer);
timer = undefined;
}
}
export function toClipboard() {
navigator.clipboard.writeText(
'pip3 install git+https://github.com/kha-white/mokuro.git@web-reader'
);
showSnackbar('Copied to clipboard');
}
type ExtaticPayload = {
title: string;
volumeName: string;
currentCharCount: number;
totalCharCount: number;
currentPage: number;
totalPages: number;
currentLineCount: number;
totalLineCount: number;
}
type ExtaticEvent = 'mokuro-reader:page.change' | 'mokuro-reader:reader.closed'
export function fireExstaticEvent(event: ExtaticEvent, payload: ExtaticPayload) {
if (browser) {
document.dispatchEvent(new CustomEvent(event, { detail: payload }))
}
}

33
src/lib/util/zip.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { Volume } from "$lib/types";
import {
BlobReader,
BlobWriter,
TextReader,
ZipWriter,
} from "@zip.js/zip.js";
export async function zipManga(manga: Volume[]) {
const zipWriter = new ZipWriter(new BlobWriter("application/zip"));
const promises = manga.map((volume) => {
const imagePromises = Object.values(volume.files).map((file) => {
return zipWriter.add(`${volume.volumeName}/${file.name}`, new BlobReader(file))
})
return [
zipWriter.add(`${volume.volumeName}.mokuro`, new TextReader(JSON.stringify(volume.mokuroData))),
...imagePromises,
]
})
await Promise.all(promises);
const zipFileBlob = await zipWriter.close();
const link = document.createElement('a');
link.href = URL.createObjectURL(zipFileBlob);
link.download = `${manga[0].mokuroData.title}.zip`;
link.click();
return false
}

View File

@@ -1,9 +1,13 @@
<script lang="ts">
import '../app.postcss';
import { dev } from '$app/environment';
import { inject } from '@vercel/analytics';
import NavBar from '$lib/components/NavBar.svelte';
import Snackbar from '$lib/components/Snackbar.svelte';
import ConfirmationPopup from '$lib/components/ConfirmationPopup.svelte';
import { settings } from '$lib/settings';
inject({ mode: dev ? 'development' : 'production' });
</script>
<div class=" h-full min-h-[100svh] text-white">

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import Catalog from '$lib/components/Catalog.svelte';
import { createProfile, deleteProfile } from '$lib/settings';
</script>
<svelte:head>

View File

@@ -4,39 +4,21 @@
import VolumeItem from '$lib/components/VolumeItem.svelte';
import { Button, Listgroup } from 'flowbite-svelte';
import { db } from '$lib/catalog/db';
import { promptConfirmation } from '$lib/util';
import { promptConfirmation, zipManga } from '$lib/util';
import { page } from '$app/stores';
import type { Volume } from '$lib/types';
import { deleteVolume, volumes } from '$lib/settings';
import { deleteVolume, mangaStats, volumes } from '$lib/settings';
function sortManga(a: Volume, b: Volume) {
if (a.volumeName < b.volumeName) {
return -1;
}
if (a.volumeName > b.volumeName) {
return 1;
}
return 0;
return a.mokuroData.volume.localeCompare(b.mokuroData.volume, undefined, {
numeric: true,
sensitivity: 'base'
});
}
$: manga = $catalog?.find((item) => item.id === $page.params.manga)?.manga.sort(sortManga);
$: stats = manga
?.map((vol) => vol.mokuroData.volume_uuid)
?.reduce(
(stats: any, volumeId) => {
const timeReadInMinutes = $volumes[volumeId]?.timeReadInMinutes || 0;
const chars = $volumes[volumeId]?.chars || 0;
const completed = $volumes[volumeId]?.completed || 0;
stats.timeReadInMinutes = stats.timeReadInMinutes + timeReadInMinutes;
stats.chars = stats.chars + chars;
stats.completed = stats.completed + completed;
return stats;
},
{ timeReadInMinutes: 0, chars: 0, completed: 0 }
);
$: loading = false;
async function confirmDelete() {
const title = manga?.[0].mokuroData.title_uuid;
@@ -52,28 +34,40 @@
function onDelete() {
promptConfirmation('Are you sure you want to delete this manga?', confirmDelete);
}
async function onExtract() {
if (manga) {
loading = true;
loading = await zipManga(manga);
}
}
</script>
<svelte:head>
<title>{manga?.[0].mokuroData.title || 'Manga'}</title>
</svelte:head>
{#if manga}
{#if manga && $mangaStats}
<div class="p-2 flex flex-col gap-5">
<div class="flex flex-row justify-between">
<div class="flex flex-col gap-2">
<h3 class="font-bold">{manga[0].mokuroData.title}</h3>
<div class="flex flex-col gap-0 sm:flex-row sm:gap-5">
<p>Volumes: {stats.completed} / {manga.length}</p>
<p>Characters read: {stats.chars}</p>
<p>Minutes read: {stats.timeReadInMinutes}</p>
<p>Volumes: {$mangaStats.completed} / {manga.length}</p>
<p>Characters read: {$mangaStats.chars}</p>
<p>Minutes read: {$mangaStats.timeReadInMinutes}</p>
</div>
</div>
<div>
<div class="sm:block flex-col flex gap-2">
<Button color="alternative" on:click={onDelete}>Remove manga</Button>
<Button color="light" on:click={onExtract} disabled={loading}>
{loading ? 'Extracting...' : 'Extract manga'}
</Button>
</div>
</div>
<Listgroup items={manga} let:item active class="flex-1 h-full w-full">
<VolumeItem {item} />
<Listgroup active class="flex-1 h-full w-full">
{#each manga as volume (volume.mokuroData.volume_uuid)}
<VolumeItem {volume} />
{/each}
</Listgroup>
</div>
{:else}

View File

@@ -3,5 +3,6 @@
<style>
:global(body.reader) {
overflow: hidden !important;
overscroll-behavior: contain;
}
</style>

View File

@@ -1,24 +1,30 @@
<script lang="ts">
import { page } from '$app/stores';
import Reader from '$lib/components/Reader/Reader.svelte';
import { initializeVolume, startCount, volumeSettings, volumes } from '$lib/settings';
import Timer from '$lib/components/Reader/Timer.svelte';
import { initializeVolume, settings, startCount, volumeSettings, volumes } from '$lib/settings';
import { onMount } from 'svelte';
const volumeId = $page.params.volume;
let count: undefined | number = undefined;
onMount(() => {
if (!$volumes?.[volumeId]) {
initializeVolume(volumeId);
}
const count = startCount(volumeId);
count = startCount(volumeId);
return () => {
clearInterval(count);
count = undefined;
};
});
</script>
{#if $volumeSettings[volumeId]}
{#if $settings.showTimer}
<Timer bind:count {volumeId} />
{/if}
<Reader volumeSettings={$volumeSettings[volumeId]} />
{/if}

View File

@@ -0,0 +1,363 @@
<script lang="ts">
import { processFiles } from '$lib/upload';
import Loader from '$lib/components/Loader.svelte';
import { formatBytes, showSnackbar, uploadFile } from '$lib/util';
import { Button, P, Progressbar } from 'flowbite-svelte';
import { onMount } from 'svelte';
import { promptConfirmation } from '$lib/util';
import { GoogleSolid } from 'flowbite-svelte-icons';
import { profiles, volumes } from '$lib/settings';
const CLIENT_ID = import.meta.env.VITE_GDRIVE_CLIENT_ID;
const API_KEY = import.meta.env.VITE_GDRIVE_API_KEY;
const DISCOVERY_DOC = 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest';
const SCOPES = 'https://www.googleapis.com/auth/drive.file';
const FOLDER_MIME_TYPE = 'application/vnd.google-apps.folder';
const READER_FOLDER = 'mokuro-reader';
const VOLUME_DATA_FILE = 'volume-data.json';
const PROFILES_FILE = 'profiles.json';
const type = 'application/json';
let tokenClient: any;
let accessToken = '';
let readerFolderId = '';
let volumeDataId = '';
let profilesId = '';
let loadingMessage = '';
let completed = 0;
let totalSize = 0;
$: progress = Math.floor((completed / totalSize) * 100).toString();
function xhrDownloadFileId(fileId: string) {
return new Promise<Blob>((resolve, reject) => {
const { access_token } = gapi.auth.getToken();
const xhr = new XMLHttpRequest();
completed = 0;
totalSize = 0;
xhr.open('GET', `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`);
xhr.setRequestHeader('Authorization', `Bearer ${access_token}`);
xhr.responseType = 'blob';
xhr.onprogress = ({ loaded, total }) => {
loadingMessage = '';
completed = loaded;
totalSize = total;
};
xhr.onabort = (event) => {
console.warn(`xhr ${fileId}: download aborted at ${event.loaded} of ${event.total}`);
showSnackbar('Download failed');
reject(new Error('Download aborted'));
};
xhr.onerror = (event) => {
console.error(`xhr ${fileId}: download error at ${event.loaded} of ${event.total}`);
showSnackbar('Download failed');
reject(new Error('Error downloading file'));
};
xhr.onload = () => {
completed = 0;
totalSize = 0;
resolve(xhr.response);
};
xhr.ontimeout = (event) => {
console.warn(`xhr ${fileId}: download timeout after ${event.loaded} of ${event.total}`);
showSnackbar('Download timed out');
reject(new Error('Timout downloading file'));
};
xhr.send();
});
}
async function connectDrive(resp?: any) {
if (resp?.error !== undefined) {
throw resp;
}
accessToken = resp?.access_token;
loadingMessage = 'Connecting to drive';
const { result: readerFolderRes } = await gapi.client.drive.files.list({
q: `mimeType='application/vnd.google-apps.folder' and name='${READER_FOLDER}'`,
fields: 'files(id)'
});
if (readerFolderRes.files?.length === 0) {
const { result: createReaderFolderRes } = await gapi.client.drive.files.create({
resource: { mimeType: FOLDER_MIME_TYPE, name: READER_FOLDER },
fields: 'id'
});
readerFolderId = createReaderFolderRes.id || '';
} else {
const id = readerFolderRes.files?.[0]?.id || '';
readerFolderId = id || '';
}
const { result: volumeDataRes } = await gapi.client.drive.files.list({
q: `'${readerFolderId}' in parents and name='${VOLUME_DATA_FILE}'`,
fields: 'files(id, name)'
});
if (volumeDataRes.files?.length !== 0) {
volumeDataId = volumeDataRes.files?.[0].id || '';
}
const { result: profilesRes } = await gapi.client.drive.files.list({
q: `'${readerFolderId}' in parents and name='${PROFILES_FILE}'`,
fields: 'files(id, name)'
});
if (profilesRes.files?.length !== 0) {
profilesId = profilesRes.files?.[0].id || '';
}
loadingMessage = '';
if (accessToken) {
showSnackbar('Connected to Google Drive');
}
}
function signIn() {
if (gapi.client.getToken() === null) {
tokenClient.requestAccessToken({ prompt: 'consent' });
} else {
tokenClient.requestAccessToken({ prompt: '' });
}
}
onMount(() => {
gapi.load('client', async () => {
await gapi.client.init({
apiKey: API_KEY,
discoveryDocs: [DISCOVERY_DOC]
});
});
gapi.load('picker', () => {});
tokenClient = google.accounts.oauth2.initTokenClient({
client_id: CLIENT_ID,
scope: SCOPES,
callback: connectDrive
});
});
function createPicker() {
const docsView = new google.picker.DocsView(google.picker.ViewId.DOCS)
.setMimeTypes('application/zip,application/x-zip-compressed')
.setMode(google.picker.DocsViewMode.LIST)
.setIncludeFolders(true)
.setParent(readerFolderId);
const picker = new google.picker.PickerBuilder()
.addView(docsView)
.setOAuthToken(accessToken)
.setAppId(CLIENT_ID)
.setDeveloperKey(API_KEY)
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.setCallback(pickerCallback)
.build();
picker.setVisible(true);
}
async function pickerCallback(data: google.picker.ResponseObject) {
try {
if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
loadingMessage = 'Downloading from drive...';
const docs = data[google.picker.Response.DOCUMENTS];
const blob = await xhrDownloadFileId(docs[0].id);
loadingMessage = 'Adding to catalog...';
const file = new File([blob], docs[0].name);
await processFiles([file]);
loadingMessage = '';
}
} catch (error) {
showSnackbar('Something went wrong');
loadingMessage = '';
console.error(error);
}
}
async function onUploadVolumeData() {
const metadata = {
mimeType: type,
name: VOLUME_DATA_FILE,
parents: [volumeDataId ? null : readerFolderId]
};
loadingMessage = 'Uploading volume data';
const res = await uploadFile({
accessToken,
fileId: volumeDataId,
metadata,
localStorageId: 'volumes',
type
});
volumeDataId = res.id;
loadingMessage = '';
if (volumeDataId) {
showSnackbar('Volume data uploaded');
}
}
async function onUploadProfiles() {
const metadata = {
mimeType: type,
name: PROFILES_FILE,
parents: [profilesId ? null : readerFolderId]
};
loadingMessage = 'Uploading profiles';
const res = await uploadFile({
accessToken,
fileId: profilesId,
metadata,
localStorageId: 'profiles',
type
});
profilesId = res.id;
loadingMessage = '';
if (profilesId) {
showSnackbar('Profiles uploaded');
}
}
async function onDownloadVolumeData() {
loadingMessage = 'Downloading volume data';
const { body } = await gapi.client.drive.files.get({
fileId: volumeDataId,
alt: 'media'
});
const downloaded = JSON.parse(body);
volumes.update((prev) => {
return {
...prev,
...downloaded
};
});
loadingMessage = '';
showSnackbar('Volume data downloaded');
}
async function onDownloadProfiles() {
loadingMessage = 'Downloading profiles';
const { body } = await gapi.client.drive.files.get({
fileId: profilesId,
alt: 'media'
});
const downloaded = JSON.parse(body);
profiles.update((prev) => {
return {
...prev,
...downloaded
};
});
loadingMessage = '';
showSnackbar('Profiles downloaded');
}
</script>
<svelte:head>
<title>Cloud</title>
</svelte:head>
<div class="p-2 h-[90svh]">
{#if loadingMessage || completed > 0}
<Loader>
{#if completed > 0}
<P>{formatBytes(completed)} / {formatBytes(totalSize)}</P>
<Progressbar {progress} />
{:else}
{loadingMessage}
{/if}
</Loader>
{:else if accessToken}
<div class="flex justify-between items-center gap-6 flex-col">
<h2 class="text-3xl font-semibold text-center pt-2">Google Drive:</h2>
<p class="text-center">
Add your zipped manga files to the <span class="text-primary-700">{READER_FOLDER}</span> folder
in your Google Drive.
</p>
<div class="flex flex-col gap-4 w-full max-w-3xl">
<Button color="blue" on:click={createPicker}>Download manga</Button>
<div class="flex-col gap-2 flex">
<Button
color="dark"
on:click={() => promptConfirmation('Upload volume data?', onUploadVolumeData)}
>
Upload volume data
</Button>
{#if volumeDataId}
<Button
color="alternative"
on:click={() =>
promptConfirmation('Download and overwrite volume data?', onDownloadVolumeData)}
>
Download volume data
</Button>
{/if}
</div>
<div class="flex-col gap-2 flex">
<Button
color="dark"
on:click={() => promptConfirmation('Upload profiles?', onUploadProfiles)}
>
Upload profiles
</Button>
{#if profilesId}
<Button
color="alternative"
on:click={() =>
promptConfirmation('Download and overwrite profiles?', onDownloadProfiles)}
>
Download profiles
</Button>
{/if}
</div>
</div>
</div>
{:else}
<div class="flex justify-center pt-0 sm:pt-32">
<button
class="w-full border rounded-lg border-slate-600 p-10 border-opacity-50 hover:bg-slate-800 max-w-3xl"
on:click={signIn}
>
<div class="flex sm:flex-row flex-col gap-2 items-center justify-center">
<GoogleSolid size="lg" />
<h2 class="text-lg">Connect to Google Drive</h2>
</div>
</button>
</div>
{/if}
</div>

View File

@@ -6,7 +6,7 @@
import { promptConfirmation, showSnackbar } from '$lib/util';
import { P, Progressbar } from 'flowbite-svelte';
import { onMount } from 'svelte';
export const BASE_URL = 'https://www.mokuro.moe/manga';
export const BASE_URL = $page.url.searchParams.get('source') || 'https://mokuro.moe/manga';
const manga = $page.url.searchParams.get('manga');
const volume = $page.url.searchParams.get('volume');
@@ -41,7 +41,8 @@
max = items.length;
for (const item of items) {
if (imageTypes.includes('.' + item.pathname.split('.').at(-1) || '')) {
const itemFileExtension = ('.' + item.pathname.split('.').at(-1)).toLowerCase();
if (imageTypes.includes(itemFileExtension || '')) {
const image = await fetch(url + item.pathname);
const blob = await image.blob();
const file = new File([blob], item.pathname.substring(1));