44 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
26 changed files with 782 additions and 127 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.

194
package-lock.json generated
View File

@@ -1,13 +1,16 @@
{
"name": "z-reader",
"version": "0.9.0",
"version": "0.9.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "z-reader",
"version": "0.9.0",
"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",
@@ -17,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",
@@ -481,10 +485,11 @@
}
},
"node_modules/@fastify/busboy": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
"integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==",
"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"
}
@@ -601,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",
@@ -665,11 +690,12 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "1.30.3",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.3.tgz",
"integrity": "sha512-0DzVXfU4h+tChFvoc8C61IqErCyskD4ydSIDjpKS2lYlEzIYrtYrY7juSqACFxqcvZAnOEXvSY+zZ8br0+ZMMg==",
"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.5.0",
"@types/cookie": "^0.5.1",
@@ -683,7 +709,7 @@
"set-cookie-parser": "^2.6.0",
"sirv": "^2.0.2",
"tiny-glob": "^0.2.9",
"undici": "~5.26.2"
"undici": "^5.28.3"
},
"bin": {
"svelte-kit": "svelte-kit.js"
@@ -747,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",
@@ -1257,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": [
{
@@ -3854,10 +3919,11 @@
}
},
"node_modules/undici": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz",
"integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==",
"version": "5.28.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
@@ -3911,10 +3977,11 @@
"dev": true
},
"node_modules/vite": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
"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.27",
@@ -4249,9 +4316,9 @@
"dev": true
},
"@fastify/busboy": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
"integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==",
"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": {
@@ -4349,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",
@@ -4397,9 +4484,9 @@
}
},
"@sveltejs/kit": {
"version": "1.30.3",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.3.tgz",
"integrity": "sha512-0DzVXfU4h+tChFvoc8C61IqErCyskD4ydSIDjpKS2lYlEzIYrtYrY7juSqACFxqcvZAnOEXvSY+zZ8br0+ZMMg==",
"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.5.0",
@@ -4414,7 +4501,7 @@
"set-cookie-parser": "^2.6.0",
"sirv": "^2.0.2",
"tiny-glob": "^0.2.9",
"undici": "~5.26.2"
"undici": "^5.28.3"
}
},
"@sveltejs/vite-plugin-svelte": {
@@ -4453,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",
@@ -4785,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": {
@@ -6606,9 +6732,9 @@
"dev": true
},
"undici": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz",
"integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==",
"version": "5.28.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"dev": true,
"requires": {
"@fastify/busboy": "^2.0.0"
@@ -6640,9 +6766,9 @@
"dev": true
},
"vite": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
"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",

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,9 @@
},
"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",

View File

@@ -3,17 +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
id="popupAbout"
class="pageContainer"
style="display: contents; background-image: url('https://reader.mokuro.app/_app/immutable/assets/icon.06fcfdd6.webp');">%sveltekit.body%</div>
style="
display: contents;
background-image: url('https://reader.mokuro.app/_app/immutable/assets/icon.06fcfdd6.webp');
"
>
%sveltekit.body%
</div>
</body>
</html>

View File

@@ -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

@@ -18,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

@@ -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,10 +1,11 @@
<script lang="ts">
import { zoomFitToScreen } from '$lib/panzoom';
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';
@@ -13,7 +14,8 @@
export let left: (_e: any, ingoreTimeOut?: boolean) => void;
export let right: (_e: any, ingoreTimeOut?: boolean) => void;
export let src: File;
export let src1: File;
export let src2: File | undefined;
let open = false;
@@ -32,8 +34,8 @@
open = false;
}
async function onUpdateCard() {
if ($settings.ankiConnectSettings.enabled) {
async function onUpdateCard(src: File | undefined) {
if ($settings.ankiConnectSettings.enabled && src) {
if ($settings.ankiConnectSettings.cropImage) {
showCropper(URL.createObjectURL(src));
} else {
@@ -52,15 +54,22 @@
tooltip="none"
trigger="click"
defaultClass="absolute end-3 bottom-3 z-50"
outline
color="dark"
color="transparent"
bind:open
>
{#if $settings.ankiConnectSettings.enabled}
<SpeedDialButton on:click={onUpdateCard}>
<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>

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 {
@@ -22,6 +22,8 @@
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;
@@ -65,10 +67,12 @@
return;
}
const pageClamped = clamp(newPage, 1, pages?.length);
const { charCount } = getCharCount(pages, pageClamped);
updateProgress(
volume.mokuroData.volume_uuid,
pageClamped,
getCharCount(pages, pageClamped) || 0,
charCount,
pageClamped === pages.length || pageClamped === pages.length - 1
);
zoomDefault();
@@ -122,6 +126,7 @@
return;
case 'ArrowDown':
case 'PageDown':
case 'Space':
changePage(page + navAmount, true);
return;
case 'Home':
@@ -132,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;
@@ -145,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;
@@ -202,6 +203,50 @@
}
}
}
$: {
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
@@ -214,7 +259,12 @@
<title>{volume?.mokuroData.volume || 'Volume'}</title>
</svelte:head>
{#if volume && pages}
<QuickActions {left} {right} src={Object.values(volume?.files)[index]} />
<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">
@@ -290,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"
>
{#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>
@@ -305,11 +359,13 @@
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}

View File

@@ -37,7 +37,7 @@
return textBox;
})
.sort(({ area: a }, { area: b }) => {
return a - b;
return b - a;
});
$: fontWeight = $settings.boldFont ? 'bold' : '400';

View File

@@ -18,7 +18,8 @@
const triggerOptions = [
{ value: 'rightClick', name: 'Right click (long press on mobile)' },
{ value: 'doubleTap', name: 'Double tap' },
{ value: 'both', name: 'Both' }
{ value: 'both', name: 'Both' },
{ value: 'neither', name: 'Neither' }
];
</script>

View File

@@ -3,11 +3,18 @@
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={() => history.back()}>Close reader</Button>
<Button color="alternative" on:click={onClose}>Close reader</Button>
</div>
{/if}

View File

@@ -3,15 +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: '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: '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

@@ -56,7 +56,7 @@
</div>
<div class="flex flex-col gap-5">
<Accordion flush>
<QuickAccess />
<QuickAccess bind:hidden />
{#if isReader()}
<VolumeSettings />
{:else}

View File

@@ -1,40 +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;
$: 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>
{#if $page.params.manga}
<a href={`/${$page.params.manga}/${mokuroData.volume_uuid}`} class="h-full w-full">
<ListgroupItem>
<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
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}>{decodeURI(volumeName)}</p>
<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>
</ListgroupItem>
</a>
</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

@@ -41,12 +41,14 @@ 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;
@@ -55,6 +57,7 @@ export type Settings = {
quickActions: boolean;
fontSize: FontSize;
zoomDefault: ZoomModes;
invertColors: boolean;
volumeDefaults: VolumeDefaults;
ankiConnectSettings: AnkiConnectSettings;
};
@@ -66,6 +69,7 @@ export type AnkiSettingsKey = keyof AnkiConnectSettings;
export type VolumeDefaultsKey = keyof VolumeDefaults;
const defaultSettings: Settings = {
defaultFullscreen: false,
displayOCR: true,
textEditable: false,
textBoxBorders: false,
@@ -73,13 +77,15 @@ const defaultSettings: Settings = {
pageNum: true,
charCount: false,
mobile: false,
bounds: false,
backgroundColor: '#030712',
swipeThreshold: 50,
edgeButtonWidth: 10,
edgeButtonWidth: 40,
showTimer: false,
quickActions: true,
fontSize: 'auto',
zoomDefault: 'zoomFitToScreen',
invertColors: false,
volumeDefaults: {
singlePageView: false,
rightToLeft: true,

View File

@@ -2,16 +2,15 @@ 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();

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

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

View File

@@ -1,6 +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);
@@ -31,3 +32,22 @@ export function toClipboard() {
);
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 }))
}
}

View File

@@ -10,34 +10,14 @@
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() {
@@ -84,8 +64,10 @@
</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

@@ -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');