mirror of
https://github.com/hkalexling/Mango.git
synced 2026-04-25 00:00:52 -04:00
Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5b6fb781f | |||
| 8dfdab9d73 | |||
| 3a95270dfb | |||
| 2960ca54df | |||
| f5fe3c6b1c | |||
| a612cc15fb | |||
| c9c0818069 | |||
| 2f8efc382f | |||
| a0fb1880bd | |||
| a408f14425 | |||
| 243b6c8927 | |||
| ff3a44d017 | |||
| 67ef1f7112 | |||
| 5d7b8a1ef9 | |||
| a68f3eea95 | |||
| 220fc42bf2 | |||
| a45e6ea3da | |||
| 88394d4636 | |||
| ef1ab940f5 | |||
| 97a1c408d8 | |||
| abbf77df13 | |||
| 3b4021f680 | |||
| 68b1923cb6 | |||
| 3cdd4b29a5 | |||
| af84c0f6de | |||
| 85a65f84d0 | |||
| 5027a911cd | |||
| ac63bf7599 | |||
| 30b0e0b8fb | |||
| ddda058d8d | |||
| 46db25e8e0 | |||
| c07f421322 | |||
| 99a77966ad | |||
| d00b917575 | |||
| 4fd8334c37 | |||
| 3aa4630558 | |||
| cde5af7066 | |||
| eb528e1726 | |||
| 5e01cc38fe | |||
| 9a787ccbc3 | |||
| 8a83c0df4e | |||
| 87dea01917 | |||
| 586ee4f0ba | |||
| 53f3387e1a | |||
| be5d1918aa | |||
| df2cc0ffa9 | |||
| b8cfc3a201 | |||
| 8dc60ac2ea | |||
| 1719335d02 | |||
| 0cd46abc66 | |||
| e4fd7c58ee | |||
| d4abee52db | |||
| d29c94e898 | |||
| 1c19a91ee2 | |||
| 7eb5c253e9 | |||
| 22a660aabf | |||
| 6e9466c9d2 | |||
| ab34fb260c | |||
| 0e9a659828 | |||
| 361d37d742 | |||
| c6adb4ee18 | |||
| 8349fb68a4 | |||
| 0e1e4de528 | |||
| b47788a85a | |||
| f7004549b8 | |||
| 8d99400c5f | |||
| ce59acae7a | |||
| 37c5911a23 | |||
| 8694b4beaf | |||
| 3b315ad880 | |||
| 33107670ce | |||
| f116e2f1d0 | |||
| ebf6221876 | |||
| 2a910335af | |||
| 9ea26474b4 | |||
| df8a6ee6da | |||
| 70ea1711ce | |||
| 2773c1e67f | |||
| dcfd1c8765 | |||
| 10b6047df8 | |||
| 8de735a2ca | |||
| 6c2350c9c7 | |||
| a994c43857 | |||
| 7e4532fb14 | |||
| d184d6fba5 | |||
| 92f5a90629 | |||
| 2a36804e8d | |||
| 87b6e79952 | |||
| b75a838e14 | |||
| ae7c72ab85 | |||
| 5cee68d76c | |||
| f444496915 | |||
| a812e3ed46 | |||
| 1be089b53e | |||
| a7f4e161de | |||
| ba31eb0071 | |||
| 192474c950 | |||
| 87b72fbd30 | |||
| 6acfa02314 | |||
| bdba7bdd13 |
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
lib
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
open_collective: mango
|
||||||
patreon: hkalexling
|
patreon: hkalexling
|
||||||
ko_fi: hkalexling
|
ko_fi: hkalexling
|
||||||
|
|||||||
@@ -17,15 +17,24 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: apk add --no-cache yarn yaml sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static
|
run: apk add --no-cache yarn yaml sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static libjpeg-turbo-dev libpng-dev tiff-dev
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make static
|
run: make static || make static
|
||||||
- name: Linter
|
- name: Linter
|
||||||
run: make check
|
run: make check
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test
|
run: make test
|
||||||
- name: Upload artifact
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: mango
|
name: mango
|
||||||
path: mango
|
path: mango
|
||||||
|
- name: build arm32v7 object file
|
||||||
|
run: make arm32v7 || make arm32v7
|
||||||
|
- name: build arm64v8 object file
|
||||||
|
run: make arm64v8 || make arm64v8
|
||||||
|
- name: Upload object files
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: object files
|
||||||
|
path: ./*.o
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ mango
|
|||||||
.env
|
.env
|
||||||
*.md
|
*.md
|
||||||
public/css/uikit.css
|
public/css/uikit.css
|
||||||
|
public/img/*.svg
|
||||||
|
public/js/*.min.js
|
||||||
|
|||||||
+2
-3
@@ -3,9 +3,8 @@ FROM crystallang/crystal:0.34.0-alpine AS builder
|
|||||||
WORKDIR /Mango
|
WORKDIR /Mango
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY package*.json .
|
RUN apk add --no-cache yarn yaml sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static libjpeg-turbo-dev libpng-dev tiff-dev
|
||||||
RUN apk add --no-cache yarn yaml sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static \
|
RUN make static || make static
|
||||||
&& make static
|
|
||||||
|
|
||||||
FROM library/alpine
|
FROM library/alpine
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM arm32v7/ubuntu:18.04
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y wget git make llvm-8 llvm-8-dev g++ libsqlite3-dev libyaml-dev libgc-dev libssl-dev libcrypto++-dev libevent-dev libgmp-dev zlib1g-dev libpcre++-dev pkg-config libarchive-dev libxml2-dev libacl1-dev nettle-dev liblzo2-dev liblzma-dev libbz2-dev libjpeg-turbo8-dev libpng-dev libtiff-dev
|
||||||
|
|
||||||
|
RUN git clone https://github.com/crystal-lang/crystal && cd crystal && git checkout 0.34.0 && make deps && cd ..
|
||||||
|
RUN git clone https://github.com/kostya/myhtml && cd myhtml/src/ext && git checkout v1.5.0 && make && cd ..
|
||||||
|
RUN git clone https://github.com/jessedoyle/duktape.cr && cd duktape.cr/ext && git checkout v0.20.0 && make && cd ..
|
||||||
|
RUN git clone https://github.com/hkalexling/image_size.cr && cd image_size.cr && git checkout v0.2.0 && make && cd ..
|
||||||
|
|
||||||
|
COPY mango-arm32v7.o .
|
||||||
|
|
||||||
|
RUN cc 'mango-arm32v7.o' -o 'mango' -rdynamic -lxml2 -L/image_size.cr/ext/libwebp -lwebp -L/image_size.cr/ext/stbi -lstbi /myhtml/src/ext/modest-c/lib/libmodest_static.a -L/duktape.cr/src/.build/lib -L/duktape.cr/src/.build/include -lduktape -lm `pkg-config libarchive --libs` -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'` -lgmp -lsqlite3 -lyaml -lpcre -lm /usr/lib/arm-linux-gnueabihf/libgc.so -lpthread /crystal/src/ext/libcrystal.a -levent -lrt -ldl -L/usr/bin/../lib/crystal/lib -L/usr/bin/../lib/crystal/lib
|
||||||
|
|
||||||
|
CMD ["./mango"]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM arm64v8/ubuntu:18.04
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y wget git make llvm-8 llvm-8-dev g++ libsqlite3-dev libyaml-dev libgc-dev libssl-dev libcrypto++-dev libevent-dev libgmp-dev zlib1g-dev libpcre++-dev pkg-config libarchive-dev libxml2-dev libacl1-dev nettle-dev liblzo2-dev liblzma-dev libbz2-dev libjpeg-turbo8-dev libpng-dev libtiff-dev
|
||||||
|
|
||||||
|
RUN git clone https://github.com/crystal-lang/crystal && cd crystal && git checkout 0.34.0 && make deps && cd ..
|
||||||
|
RUN git clone https://github.com/kostya/myhtml && cd myhtml/src/ext && git checkout v1.5.0 && make && cd ..
|
||||||
|
RUN git clone https://github.com/jessedoyle/duktape.cr && cd duktape.cr/ext && git checkout v0.20.0 && make && cd ..
|
||||||
|
RUN git clone https://github.com/hkalexling/image_size.cr && cd image_size.cr && git checkout v0.2.0 && make && cd ..
|
||||||
|
|
||||||
|
COPY mango-arm64v8.o .
|
||||||
|
|
||||||
|
RUN cc 'mango-arm64v8.o' -o 'mango' -rdynamic -lxml2 -L/image_size.cr/ext/libwebp -lwebp -L/image_size.cr/ext/stbi -lstbi /myhtml/src/ext/modest-c/lib/libmodest_static.a -L/duktape.cr/src/.build/lib -L/duktape.cr/src/.build/include -lduktape -lm `pkg-config libarchive --libs` -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'` -lgmp -lsqlite3 -lyaml -lpcre -lm /usr/lib/arm-linux-gnueabihf/libgc.so -lpthread /crystal/src/ext/libcrystal.a -levent -lrt -ldl -L/usr/bin/../lib/crystal/lib -L/usr/bin/../lib/crystal/lib
|
||||||
|
|
||||||
|
CMD ["./mango"]
|
||||||
@@ -7,11 +7,15 @@ uglify:
|
|||||||
yarn
|
yarn
|
||||||
yarn uglify
|
yarn uglify
|
||||||
|
|
||||||
|
setup: libs
|
||||||
|
yarn
|
||||||
|
yarn gulp dev
|
||||||
|
|
||||||
build: libs
|
build: libs
|
||||||
crystal build src/mango.cr --release --progress
|
crystal build src/mango.cr --release --progress --error-trace
|
||||||
|
|
||||||
static: uglify | libs
|
static: uglify | libs
|
||||||
crystal build src/mango.cr --release --progress --static
|
crystal build src/mango.cr --release --progress --static --error-trace
|
||||||
|
|
||||||
libs:
|
libs:
|
||||||
shards install --production
|
shards install --production
|
||||||
@@ -27,6 +31,12 @@ check:
|
|||||||
./bin/ameba
|
./bin/ameba
|
||||||
./dev/linewidth.sh
|
./dev/linewidth.sh
|
||||||
|
|
||||||
|
arm32v7:
|
||||||
|
crystal build src/mango.cr --release --progress --error-trace --cross-compile --target='arm-linux-gnueabihf' -o mango-arm32v7
|
||||||
|
|
||||||
|
arm64v8:
|
||||||
|
crystal build src/mango.cr --release --progress --error-trace --cross-compile --target='aarch64-linux-gnu' -o mango-arm64v8
|
||||||
|
|
||||||
install:
|
install:
|
||||||
cp mango $(INSTALL_DIR)/mango
|
cp mango $(INSTALL_DIR)/mango
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Mango is a self-hosted manga server and reader. Its features include
|
|||||||
- Supports nested folders in library
|
- Supports nested folders in library
|
||||||
- Automatically stores reading progress
|
- Automatically stores reading progress
|
||||||
- Built-in [MangaDex](https://mangadex.org/) downloader
|
- Built-in [MangaDex](https://mangadex.org/) downloader
|
||||||
|
- Supports [plugins](https://github.com/hkalexling/mango-plugins) to download from thrid-party sites
|
||||||
- The web reader is responsive and works well on mobile, so there is no need for a mobile app
|
- The web reader is responsive and works well on mobile, so there is no need for a mobile app
|
||||||
- All the static files are embedded in the binary, so the deployment process is easy and painless
|
- All the static files are embedded in the binary, so the deployment process is easy and painless
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
|
|||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
```
|
```
|
||||||
Mango - Manga Server and Web Reader. Version 0.8.0
|
Mango - Manga Server and Web Reader. Version 0.12.1
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
@@ -138,8 +139,12 @@ Mobile UI:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
<a href="https://casinoshunter.com/online-casinos/"><img src="https://i.imgur.com/EJb3wBo.png" width="150" height="auto"></a>
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
Please check the [development guideline](https://github.com/hkalexling/Mango/wiki/Development) if you are interest in code contributions.
|
Please check the [development guideline](https://github.com/hkalexling/Mango/wiki/Development) if you are interested in code contributions.
|
||||||
|
|
||||||
[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/0)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/1)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/2)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/3)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/4)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/5)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/6)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/7)
|
[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/0)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/1)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/2)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/3)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/4)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/5)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/6)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/7)
|
||||||
|
|||||||
+35
-2
@@ -3,10 +3,29 @@ const minify = require("gulp-babel-minify");
|
|||||||
const minifyCss = require('gulp-minify-css');
|
const minifyCss = require('gulp-minify-css');
|
||||||
const less = require('gulp-less');
|
const less = require('gulp-less');
|
||||||
|
|
||||||
|
gulp.task('copy-uikit-js', () => {
|
||||||
|
return gulp.src([
|
||||||
|
'node_modules/uikit/dist/js/uikit.min.js',
|
||||||
|
'node_modules/uikit/dist/js/uikit-icons.min.js'
|
||||||
|
])
|
||||||
|
.pipe(gulp.dest('public/js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('copy-fontawesome', () => {
|
||||||
|
return gulp.src([
|
||||||
|
'node_modules/@fortawesome/fontawesome-free/js/fontawesome.min.js',
|
||||||
|
'node_modules/@fortawesome/fontawesome-free/js/solid.min.js'
|
||||||
|
])
|
||||||
|
.pipe(gulp.dest('public/js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('copy-js', gulp.series('copy-uikit-js', 'copy-fontawesome'));
|
||||||
|
|
||||||
gulp.task('minify-js', () => {
|
gulp.task('minify-js', () => {
|
||||||
return gulp.src('public/js/*.js')
|
return gulp.src('public/js/*.js')
|
||||||
.pipe(minify({
|
.pipe(minify({
|
||||||
removeConsole: true
|
removeConsole: true,
|
||||||
|
builtIns: false
|
||||||
}))
|
}))
|
||||||
.pipe(gulp.dest('dist/js'));
|
.pipe(gulp.dest('dist/js'));
|
||||||
});
|
});
|
||||||
@@ -23,6 +42,11 @@ gulp.task('minify-css', () => {
|
|||||||
.pipe(gulp.dest('dist/css'));
|
.pipe(gulp.dest('dist/css'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task('copy-uikit-icons', () => {
|
||||||
|
return gulp.src('node_modules/uikit/src/images/backgrounds/*.svg')
|
||||||
|
.pipe(gulp.dest('public/img'));
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task('img', () => {
|
gulp.task('img', () => {
|
||||||
return gulp.src('public/img/*')
|
return gulp.src('public/img/*')
|
||||||
.pipe(gulp.dest('dist/img'));
|
.pipe(gulp.dest('dist/img'));
|
||||||
@@ -33,4 +57,13 @@ gulp.task('copy-files', () => {
|
|||||||
.pipe(gulp.dest('dist'));
|
.pipe(gulp.dest('dist'));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('default', gulp.parallel('minify-js', gulp.series('less', 'minify-css'), 'img', 'copy-files'));
|
gulp.task('default', gulp.parallel(
|
||||||
|
gulp.series('copy-js', 'minify-js'),
|
||||||
|
gulp.series('less', 'minify-css'),
|
||||||
|
gulp.series('copy-uikit-icons', 'img'),
|
||||||
|
'copy-files'
|
||||||
|
));
|
||||||
|
|
||||||
|
gulp.task('dev', gulp.parallel(
|
||||||
|
'copy-js', 'less', 'copy-uikit-icons'
|
||||||
|
));
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"uglify": "gulp"
|
"uglify": "gulp"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||||
"uikit": "^3.5.4"
|
"uikit": "^3.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-7
@@ -7,6 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.uk-card-media-top {
|
.uk-card-media-top {
|
||||||
|
width: 100%;
|
||||||
height: 250px;
|
height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,18 +31,18 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uk-list li {
|
.uk-list li:not(.nopointer) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reader-bg {
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
#scan-status {
|
#scan-status {
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reader-bg {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
.break-word {
|
.break-word {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
@@ -63,11 +64,11 @@
|
|||||||
background: #F4E487;
|
background: #F4E487;
|
||||||
}
|
}
|
||||||
|
|
||||||
#selectable .ui-selecting.dark {
|
.uk-light #selectable .ui-selecting {
|
||||||
background: #5E5731;
|
background: #5E5731;
|
||||||
}
|
}
|
||||||
|
|
||||||
#selectable .ui-selected.dark {
|
.uk-light #selectable .ui-selected {
|
||||||
background: #9D9252;
|
background: #9D9252;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,3 +98,57 @@ td>.uk-dropdown {
|
|||||||
.item .uk-card-title {
|
.item .uk-card-title {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grayscale {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-light .uk-navbar-dropdown,
|
||||||
|
.uk-light .uk-modal-header,
|
||||||
|
.uk-light .uk-modal-body,
|
||||||
|
.uk-light .uk-modal-footer {
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-light .uk-dropdown {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-light .uk-navbar-dropdown,
|
||||||
|
.uk-light .uk-dropdown {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-light .uk-nav-header,
|
||||||
|
.uk-light .uk-description-list>dt {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
[x-cloak] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#select-bar-controls a {
|
||||||
|
transform: scale(1.5, 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#select-bar-controls a:hover {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-section {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#totop-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 100vh;
|
||||||
|
right: 2em;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#totop-wrapper a {
|
||||||
|
position: fixed;
|
||||||
|
position: sticky;
|
||||||
|
top: calc(100vh - 5em);
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,3 +31,15 @@
|
|||||||
background-color: @global-secondary-background;
|
background-color: @global-secondary-background;
|
||||||
color: @global-inverse-color;
|
color: @global-inverse-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@internal-divider-icon-image: "../img/divider-icon.svg";
|
||||||
|
@internal-form-select-image: "../img/form-select.svg";
|
||||||
|
@internal-form-datalist-image: "../img/form-datalist.svg";
|
||||||
|
@internal-form-radio-image: "../img/form-radio.svg";
|
||||||
|
@internal-form-checkbox-image: "../img/form-checkbox.svg";
|
||||||
|
@internal-form-checkbox-indeterminate-image: "../img/form-checkbox-indeterminate.svg";
|
||||||
|
@internal-nav-parent-close-image: "../img/nav-parent-close.svg";
|
||||||
|
@internal-nav-parent-open-image: "../img/nav-parent-open.svg";
|
||||||
|
@internal-list-bullet-image: "../img/list-bullet.svg";
|
||||||
|
@internal-accordion-open-image: "../img/accordion-open.svg";
|
||||||
|
@internal-accordion-close-image: "../img/accordion-close.svg";
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 272 KiB |
+24
-9
@@ -1,13 +1,14 @@
|
|||||||
var scanning = false;
|
let scanning = false;
|
||||||
function scan() {
|
|
||||||
|
const scan = () => {
|
||||||
scanning = true;
|
scanning = true;
|
||||||
$('#scan-status > div').removeAttr('hidden');
|
$('#scan-status > div').removeAttr('hidden');
|
||||||
$('#scan-status > span').attr('hidden', '');
|
$('#scan-status > span').attr('hidden', '');
|
||||||
var color = $('#scan').css('color');
|
const color = $('#scan').css('color');
|
||||||
$('#scan').css('color', 'gray');
|
$('#scan').css('color', 'gray');
|
||||||
$.post(base_url + 'api/admin/scan', function (data) {
|
$.post(base_url + 'api/admin/scan', (data) => {
|
||||||
var ms = data.milliseconds;
|
const ms = data.milliseconds;
|
||||||
var titles = data.titles;
|
const titles = data.titles;
|
||||||
$('#scan-status > span').text('Scanned ' + titles + ' titles in ' + ms + 'ms');
|
$('#scan-status > span').text('Scanned ' + titles + ' titles in ' + ms + 'ms');
|
||||||
$('#scan-status > span').removeAttr('hidden');
|
$('#scan-status > span').removeAttr('hidden');
|
||||||
$('#scan').css('color', color);
|
$('#scan').css('color', color);
|
||||||
@@ -15,11 +16,25 @@ function scan() {
|
|||||||
scanning = false;
|
scanning = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
$(function() {
|
|
||||||
$('li').click(function() {
|
String.prototype.capitalize = function() {
|
||||||
url = $(this).attr('data-url');
|
return this.charAt(0).toUpperCase() + this.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
$('li').click((e) => {
|
||||||
|
const url = $(e.currentTarget).attr('data-url');
|
||||||
if (url) {
|
if (url) {
|
||||||
$(location).attr('href', url);
|
$(location).attr('href', url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setting = loadThemeSetting();
|
||||||
|
$('#theme-select').val(setting.capitalize());
|
||||||
|
|
||||||
|
$('#theme-select').change((e) => {
|
||||||
|
const newSetting = $(e.currentTarget).val().toLowerCase();
|
||||||
|
saveThemeSetting(newSetting);
|
||||||
|
setTheme();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -119,11 +119,12 @@ const load = () => {
|
|||||||
const dropdown = obj.status_message.length > 0 ? `<div uk-dropdown>${obj.status_message}</div>` : '';
|
const dropdown = obj.status_message.length > 0 ? `<div uk-dropdown>${obj.status_message}</div>` : '';
|
||||||
const retryBtn = obj.status_message.length > 0 ? `<a onclick="refresh('${obj.id}')" uk-icon="refresh"></a>` : '';
|
const retryBtn = obj.status_message.length > 0 ? `<a onclick="refresh('${obj.id}')" uk-icon="refresh"></a>` : '';
|
||||||
return `<tr id="chapter-${obj.id}">
|
return `<tr id="chapter-${obj.id}">
|
||||||
<td><a href="${baseURL}/chapter/${obj.id}">${obj.title}</a></td>
|
<td>${obj.plugin_id ? obj.title : `<a href="${baseURL}/chapter/${obj.id}">${obj.title}</a>`}</td>
|
||||||
<td><a href="${baseURL}/manga/${obj.manga_id}">${obj.manga_title}</a></td>
|
<td>${obj.plugin_id ? obj.manga_title : `<a href="${baseURL}/manga/${obj.manga_id}">${obj.manga_title}</a>`}</td>
|
||||||
<td>${obj.success_count}/${obj.pages}</td>
|
<td>${obj.success_count}/${obj.pages}</td>
|
||||||
<td>${moment(obj.time).fromNow()}</td>
|
<td>${moment(obj.time).fromNow()}</td>
|
||||||
<td>${statusSpan} ${dropdown}</td>
|
<td>${statusSpan} ${dropdown}</td>
|
||||||
|
<td>${obj.plugin_id || ""}</td>
|
||||||
<td>
|
<td>
|
||||||
<a onclick="remove('${obj.id}')" uk-icon="trash"></a>
|
<a onclick="remove('${obj.id}')" uk-icon="trash"></a>
|
||||||
${retryBtn}
|
${retryBtn}
|
||||||
|
|||||||
+42
-45
@@ -32,42 +32,41 @@ const download = () => {
|
|||||||
const chapters = globalChapters.filter(c => ids.indexOf(c.id) >= 0);
|
const chapters = globalChapters.filter(c => ids.indexOf(c.id) >= 0);
|
||||||
console.log(ids);
|
console.log(ids);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
url: base_url + 'api/admin/mangadex/download',
|
url: base_url + 'api/admin/mangadex/download',
|
||||||
data: JSON.stringify({chapters: chapters}),
|
data: JSON.stringify({
|
||||||
contentType: "application/json",
|
chapters: chapters
|
||||||
dataType: 'json'
|
}),
|
||||||
})
|
contentType: "application/json",
|
||||||
.done(data => {
|
dataType: 'json'
|
||||||
console.log(data);
|
})
|
||||||
if (data.error) {
|
.done(data => {
|
||||||
alert('danger', `Failed to add chapters to the download queue. Error: ${data.error}`);
|
console.log(data);
|
||||||
return;
|
if (data.error) {
|
||||||
}
|
alert('danger', `Failed to add chapters to the download queue. Error: ${data.error}`);
|
||||||
const successCount = parseInt(data.success);
|
return;
|
||||||
const failCount = parseInt(data.fail);
|
}
|
||||||
UIkit.modal.confirm(`${successCount} of ${successCount + failCount} chapters added to the download queue. Proceed to the download manager?`).then(() => {
|
const successCount = parseInt(data.success);
|
||||||
window.location.href = base_url + 'admin/downloads';
|
const failCount = parseInt(data.fail);
|
||||||
|
UIkit.modal.confirm(`${successCount} of ${successCount + failCount} chapters added to the download queue. Proceed to the download manager?`).then(() => {
|
||||||
|
window.location.href = base_url + 'admin/downloads';
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.fail((jqXHR, status) => {
|
||||||
|
alert('danger', `Failed to add chapters to the download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
$('#download-spinner').attr('hidden', '');
|
||||||
|
$('#download-btn').removeAttr('hidden');
|
||||||
});
|
});
|
||||||
styleModal();
|
|
||||||
})
|
|
||||||
.fail((jqXHR, status) => {
|
|
||||||
alert('danger', `Failed to add chapters to the download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
|
||||||
})
|
|
||||||
.always(() => {
|
|
||||||
$('#download-spinner').attr('hidden', '');
|
|
||||||
$('#download-btn').removeAttr('hidden');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
styleModal();
|
|
||||||
};
|
};
|
||||||
const toggleSpinner = () => {
|
const toggleSpinner = () => {
|
||||||
var attr = $('#spinner').attr('hidden');
|
var attr = $('#spinner').attr('hidden');
|
||||||
if (attr) {
|
if (attr) {
|
||||||
$('#spinner').removeAttr('hidden');
|
$('#spinner').removeAttr('hidden');
|
||||||
$('#search-btn').attr('hidden', '');
|
$('#search-btn').attr('hidden', '');
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$('#search-btn').removeAttr('hidden');
|
$('#search-btn').removeAttr('hidden');
|
||||||
$('#spinner').attr('hidden', '');
|
$('#spinner').attr('hidden', '');
|
||||||
}
|
}
|
||||||
@@ -98,8 +97,7 @@ const search = () => {
|
|||||||
const path = new URL(input).pathname;
|
const path = new URL(input).pathname;
|
||||||
const match = /\/title\/([0-9]+)/.exec(path);
|
const match = /\/title\/([0-9]+)/.exec(path);
|
||||||
int_id = parseInt(match[1]);
|
int_id = parseInt(match[1]);
|
||||||
}
|
} catch (e) {
|
||||||
catch(e) {
|
|
||||||
int_id = parseInt(input);
|
int_id = parseInt(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,8 +137,12 @@ const search = () => {
|
|||||||
const comp = (a, b) => {
|
const comp = (a, b) => {
|
||||||
var ai;
|
var ai;
|
||||||
var bi;
|
var bi;
|
||||||
try {ai = parseFloat(a);} catch(e) {}
|
try {
|
||||||
try {bi = parseFloat(b);} catch(e) {}
|
ai = parseFloat(a);
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
bi = parseFloat(b);
|
||||||
|
} catch (e) {}
|
||||||
if (typeof ai === 'undefined') return -1;
|
if (typeof ai === 'undefined') return -1;
|
||||||
if (typeof bi === 'undefined') return 1;
|
if (typeof bi === 'undefined') return 1;
|
||||||
if (ai < bi) return 1;
|
if (ai < bi) return 1;
|
||||||
@@ -176,8 +178,7 @@ const parseRange = str => {
|
|||||||
if (!matches) {
|
if (!matches) {
|
||||||
alert('danger', `Failed to parse filter input ${str}`);
|
alert('danger', `Failed to parse filter input ${str}`);
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
} else if (typeof matches[1] !== 'undefined' && typeof matches[2] !== 'undefined') {
|
||||||
else if (typeof matches[1] !== 'undefined' && typeof matches[2] !== 'undefined') {
|
|
||||||
// e.g., <= 30
|
// e.g., <= 30
|
||||||
num = parseInt(matches[2]);
|
num = parseInt(matches[2]);
|
||||||
if (isNaN(num)) {
|
if (isNaN(num)) {
|
||||||
@@ -194,8 +195,7 @@ const parseRange = str => {
|
|||||||
case '>=':
|
case '>=':
|
||||||
return [num, null];
|
return [num, null];
|
||||||
}
|
}
|
||||||
}
|
} else if (typeof matches[3] !== 'undefined') {
|
||||||
else if (typeof matches[3] !== 'undefined') {
|
|
||||||
// a single number
|
// a single number
|
||||||
num = parseInt(matches[3]);
|
num = parseInt(matches[3]);
|
||||||
if (isNaN(num)) {
|
if (isNaN(num)) {
|
||||||
@@ -203,8 +203,7 @@ const parseRange = str => {
|
|||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
return [num, num];
|
return [num, num];
|
||||||
}
|
} else if (typeof matches[4] !== 'undefined' && typeof matches[5] !== 'undefined') {
|
||||||
else if (typeof matches[4] !== 'undefined' && typeof matches[5] !== 'undefined') {
|
|
||||||
// e.g., 10 - 23
|
// e.g., 10 - 23
|
||||||
num = parseInt(matches[4]);
|
num = parseInt(matches[4]);
|
||||||
const n2 = parseInt(matches[5]);
|
const n2 = parseInt(matches[5]);
|
||||||
@@ -213,8 +212,7 @@ const parseRange = str => {
|
|||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
return [num, n2];
|
return [num, n2];
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// empty or space only
|
// empty or space only
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
@@ -280,8 +278,7 @@ const buildTable = () => {
|
|||||||
const group_str = Object.entries(chp.groups).map(([k, v]) => {
|
const group_str = Object.entries(chp.groups).map(([k, v]) => {
|
||||||
return `<a href="${baseURL }/group/${v}">${k}</a>`;
|
return `<a href="${baseURL }/group/${v}">${k}</a>`;
|
||||||
}).join(' | ');
|
}).join(' | ');
|
||||||
const dark = getTheme() === 'dark' ? 'dark' : '';
|
return `<tr class="ui-widget-content">
|
||||||
return `<tr class="ui-widget-content ${dark}">
|
|
||||||
<td><a href="${baseURL}/chapter/${chp.id}">${chp.id}</a></td>
|
<td><a href="${baseURL}/chapter/${chp.id}">${chp.id}</a></td>
|
||||||
<td>${chp.title}</td>
|
<td>${chp.title}</td>
|
||||||
<td>${chp.language}</td>
|
<td>${chp.language}</td>
|
||||||
@@ -302,7 +299,7 @@ const buildTable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const unescapeHTML = (str) => {
|
const unescapeHTML = (str) => {
|
||||||
var elt = document.createElement("span");
|
var elt = document.createElement("span");
|
||||||
elt.innerHTML = str;
|
elt.innerHTML = str;
|
||||||
return elt.innerText;
|
return elt.innerText;
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
-5
File diff suppressed because one or more lines are too long
@@ -0,0 +1,142 @@
|
|||||||
|
const loadPlugin = id => {
|
||||||
|
localStorage.setItem('plugin', id);
|
||||||
|
const url = `${location.protocol}//${location.host}${location.pathname}`;
|
||||||
|
const newURL = `${url}?${$.param({
|
||||||
|
plugin: id
|
||||||
|
})}`;
|
||||||
|
window.location.href = newURL;
|
||||||
|
};
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
var storedID = localStorage.getItem('plugin');
|
||||||
|
if (storedID && storedID !== pid) {
|
||||||
|
loadPlugin(storedID);
|
||||||
|
} else {
|
||||||
|
$('#controls').removeAttr('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#search-input').keypress(event => {
|
||||||
|
if (event.which === 13) {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('#plugin-select').val(pid);
|
||||||
|
$('#plugin-select').change(() => {
|
||||||
|
const id = $('#plugin-select').val();
|
||||||
|
loadPlugin(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let mangaTitle = "";
|
||||||
|
let searching = false;
|
||||||
|
const search = () => {
|
||||||
|
if (searching)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const query = $('#search-input').val();
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: base_url + 'api/admin/plugin/list',
|
||||||
|
data: JSON.stringify({
|
||||||
|
query: query,
|
||||||
|
plugin: pid
|
||||||
|
}),
|
||||||
|
contentType: "application/json",
|
||||||
|
dataType: 'json'
|
||||||
|
})
|
||||||
|
.done(data => {
|
||||||
|
console.log(data);
|
||||||
|
if (data.error) {
|
||||||
|
alert('danger', `Search failed. Error: ${data.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mangaTitle = data.title;
|
||||||
|
$('#title-text').text(data.title);
|
||||||
|
buildTable(data.chapters);
|
||||||
|
})
|
||||||
|
.fail((jqXHR, status) => {
|
||||||
|
alert('danger', `Search failed. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
|
})
|
||||||
|
.always(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTable = (chapters) => {
|
||||||
|
$('#table').attr('hidden', '');
|
||||||
|
$('table').empty();
|
||||||
|
|
||||||
|
const keys = Object.keys(chapters[0]).map(k => `<th>${k}</th>`).join('');
|
||||||
|
const thead = `<thead><tr>${keys}</tr></thead>`;
|
||||||
|
$('table').append(thead);
|
||||||
|
|
||||||
|
const rows = chapters.map(ch => {
|
||||||
|
const tds = Object.values(ch).map(v => `<td>${v}</td>`).join('');
|
||||||
|
return `<tr data-id="${ch.id}" data-title="${ch.title}">${tds}</tr>`;
|
||||||
|
});
|
||||||
|
const tbody = `<tbody id="selectable">${rows}</tbody>`;
|
||||||
|
$('table').append(tbody);
|
||||||
|
|
||||||
|
$('#selectable').selectable({
|
||||||
|
filter: 'tr'
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#table table').tablesorter();
|
||||||
|
$('#table').removeAttr('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAll = () => {
|
||||||
|
$('tbody > tr').each((i, e) => {
|
||||||
|
$(e).addClass('ui-selected');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const unselect = () => {
|
||||||
|
$('tbody > tr').each((i, e) => {
|
||||||
|
$(e).removeClass('ui-selected');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const download = () => {
|
||||||
|
const selected = $('tbody > tr.ui-selected');
|
||||||
|
if (selected.length === 0) return;
|
||||||
|
UIkit.modal.confirm(`Download ${selected.length} selected chapters?`).then(() => {
|
||||||
|
$('#download-btn').attr('hidden', '');
|
||||||
|
$('#download-spinner').removeAttr('hidden');
|
||||||
|
const chapters = selected.map((i, e) => {
|
||||||
|
return {
|
||||||
|
id: $(e).attr('data-id'),
|
||||||
|
title: $(e).attr('data-title')
|
||||||
|
}
|
||||||
|
}).get();
|
||||||
|
console.log(chapters);
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: base_url + 'api/admin/plugin/download',
|
||||||
|
data: JSON.stringify({
|
||||||
|
plugin: pid,
|
||||||
|
chapters: chapters,
|
||||||
|
title: mangaTitle
|
||||||
|
}),
|
||||||
|
contentType: "application/json",
|
||||||
|
dataType: 'json'
|
||||||
|
})
|
||||||
|
.done(data => {
|
||||||
|
console.log(data);
|
||||||
|
if (data.error) {
|
||||||
|
alert('danger', `Failed to add chapters to the download queue. Error: ${data.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const successCount = parseInt(data.success);
|
||||||
|
const failCount = parseInt(data.fail);
|
||||||
|
UIkit.modal.confirm(`${successCount} of ${successCount + failCount} chapters added to the download queue. Proceed to the download manager?`).then(() => {
|
||||||
|
window.location.href = base_url + 'admin/downloads';
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.fail((jqXHR, status) => {
|
||||||
|
alert('danger', `Failed to add chapters to the download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
$('#download-spinner').attr('hidden', '');
|
||||||
|
$('#download-btn').removeAttr('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
+149
-69
@@ -1,81 +1,161 @@
|
|||||||
$(function() {
|
$(() => {
|
||||||
function bind() {
|
getPages();
|
||||||
var controller = new ScrollMagic.Controller();
|
|
||||||
|
|
||||||
// replace history on scroll
|
$('#page-select').change(() => {
|
||||||
$('img').each(function(idx){
|
const p = parseInt($('#page-select').val());
|
||||||
var scene = new ScrollMagic.Scene({
|
toPage(p);
|
||||||
triggerElement: $(this).get(),
|
});
|
||||||
triggerHook: 'onEnter',
|
});
|
||||||
reverse: true
|
|
||||||
})
|
|
||||||
.addTo(controller)
|
|
||||||
.on('enter', function(event){
|
|
||||||
current = $(event.target.triggerElement()).attr('id');
|
|
||||||
replaceHistory(current);
|
|
||||||
})
|
|
||||||
.on('leave', function(event){
|
|
||||||
var prev = $(event.target.triggerElement()).prev();
|
|
||||||
current = $(prev).attr('id');
|
|
||||||
replaceHistory(current);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// poor man's infinite scroll
|
/**
|
||||||
var scene = new ScrollMagic.Scene({
|
* Set an alpine.js property
|
||||||
triggerElement: $('.next-url').get(),
|
*
|
||||||
triggerHook: 'onEnter',
|
* @function setProp
|
||||||
offset: -500
|
* @param {string} key - Key of the data property
|
||||||
})
|
* @param {*} prop - The data property
|
||||||
.addTo(controller)
|
*/
|
||||||
.on('enter', function(){
|
const setProp = (key, prop) => {
|
||||||
var nextURL = $('.next-url').attr('href');
|
$('#root').get(0).__x.$data[key] = prop;
|
||||||
$('.next-url').remove();
|
};
|
||||||
if (!nextURL) {
|
|
||||||
console.log('No .next-url found. Reached end of page');
|
/**
|
||||||
var lastURL = $('img').last().attr('id');
|
* Get dimension of the pages in the entry from the API and update the view
|
||||||
// load the reader URL for the last page to update reading progrss to 100%
|
*/
|
||||||
$.get(lastURL);
|
const getPages = () => {
|
||||||
$('#next-btn').removeAttr('hidden');
|
$.get(`${base_url}api/dimensions/${tid}/${eid}`)
|
||||||
return;
|
.then(data => {
|
||||||
}
|
if (!data.success && data.error)
|
||||||
$('#hidden').load(encodeURI(nextURL) + ' .uk-container', function(res, status, xhr){
|
throw new Error(resp.error);
|
||||||
if (status === 'error') console.log(xhr.statusText);
|
const dimensions = data.dimensions;
|
||||||
if (status === 'success') {
|
|
||||||
console.log(nextURL + ' loaded');
|
const items = dimensions.map((d, i) => {
|
||||||
// new page loaded to #hidden, we now append it
|
return {
|
||||||
$('.uk-section > .uk-container').append($('#hidden .uk-container').children());
|
id: i + 1,
|
||||||
$('#hidden').empty();
|
url: `${base_url}api/page/${tid}/${eid}/${i+1}`,
|
||||||
bind();
|
width: d.width,
|
||||||
}
|
height: d.height
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
bind();
|
setProp('items', items);
|
||||||
});
|
setProp('loading', false);
|
||||||
$('#page-select').change(function(){
|
|
||||||
jumpTo(parseInt($('#page-select').val()));
|
waitForPage(items.length, () => {
|
||||||
});
|
toPage(page);
|
||||||
function showControl(idx) {
|
setupScroller();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
const errMsg = `Failed to get the page dimensions. ${e}`;
|
||||||
|
console.error(e);
|
||||||
|
setProp('alertClass', 'uk-alert-danger');
|
||||||
|
setProp('msg', errMsg);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jump to a specific page
|
||||||
|
*
|
||||||
|
* @function toPage
|
||||||
|
* @param {number} idx - One-based index of the page
|
||||||
|
*/
|
||||||
|
const toPage = (idx) => {
|
||||||
|
$(`#${idx}`).get(0).scrollIntoView(true);
|
||||||
|
UIkit.modal($('#modal-sections')).hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a page exists every 100ms. If so, invoke the callback function.
|
||||||
|
*
|
||||||
|
* @function waitForPage
|
||||||
|
* @param {number} idx - One-based index of the page
|
||||||
|
* @param {function} cb - Callback function
|
||||||
|
*/
|
||||||
|
const waitForPage = (idx, cb) => {
|
||||||
|
if ($(`#${idx}`).length > 0) return cb();
|
||||||
|
setTimeout(() => {
|
||||||
|
waitForPage(idx, cb)
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the control modal
|
||||||
|
*
|
||||||
|
* @function showControl
|
||||||
|
* @param {object} event - The onclick event that triggers the function
|
||||||
|
*/
|
||||||
|
const showControl = (event) => {
|
||||||
|
const idx = parseInt($(event.currentTarget).attr('id'));
|
||||||
const pageCount = $('#page-select > option').length;
|
const pageCount = $('#page-select > option').length;
|
||||||
const progressText = `Progress: ${idx}/${pageCount} (${(idx/pageCount * 100).toFixed(1)}%)`;
|
const progressText = `Progress: ${idx}/${pageCount} (${(idx/pageCount * 100).toFixed(1)}%)`;
|
||||||
$('#progress-label').text(progressText);
|
$('#progress-label').text(progressText);
|
||||||
$('#page-select').val(idx);
|
$('#page-select').val(idx);
|
||||||
UIkit.modal($('#modal-sections')).show();
|
UIkit.modal($('#modal-sections')).show();
|
||||||
styleModal();
|
|
||||||
}
|
}
|
||||||
function jumpTo(page) {
|
|
||||||
var ary = window.location.pathname.split('/');
|
/**
|
||||||
ary[ary.length - 1] = page;
|
* Redirect to a URL
|
||||||
ary.shift(); // remove leading `/`
|
*
|
||||||
ary.unshift(window.location.origin);
|
* @function redirect
|
||||||
window.location.replace(ary.join('/'));
|
* @param {string} url - The target URL
|
||||||
}
|
*/
|
||||||
function replaceHistory(url) {
|
const redirect = (url) => {
|
||||||
history.replaceState(null, "", url);
|
|
||||||
console.log('reading ' + url);
|
|
||||||
}
|
|
||||||
function redirect(url) {
|
|
||||||
window.location.replace(url);
|
window.location.replace(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the address bar history and save th ereading progress if necessary
|
||||||
|
*
|
||||||
|
* @function replaceHistory
|
||||||
|
* @param {number} idx - One-based index of the current page
|
||||||
|
*/
|
||||||
|
const replaceHistory = (idx) => {
|
||||||
|
const ary = window.location.pathname.split('/');
|
||||||
|
ary[ary.length - 1] = idx;
|
||||||
|
ary.shift(); // remove leading `/`
|
||||||
|
ary.unshift(window.location.origin);
|
||||||
|
const url = ary.join('/');
|
||||||
|
saveProgress(idx);
|
||||||
|
history.replaceState(null, "", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the scroll handler that calls `replaceHistory` when an image
|
||||||
|
* enters the view port
|
||||||
|
*
|
||||||
|
* @function setupScroller
|
||||||
|
*/
|
||||||
|
const setupScroller = () => {
|
||||||
|
$('#root img').each((idx, el) => {
|
||||||
|
$(el).on('inview', (event, inView) => {
|
||||||
|
if (inView) {
|
||||||
|
const current = $(event.currentTarget).attr('id');
|
||||||
|
replaceHistory(current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastSavedPage = page;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the backend reading progress if the current page is more than
|
||||||
|
* five pages away from the last saved page
|
||||||
|
*
|
||||||
|
* @function saveProgress
|
||||||
|
* @param {number} idx - One-based index of the page
|
||||||
|
*/
|
||||||
|
const saveProgress = (idx) => {
|
||||||
|
if (Math.abs(idx - lastSavedPage) < 5) return;
|
||||||
|
lastSavedPage = idx;
|
||||||
|
|
||||||
|
const url = `${base_url}api/progress/${tid}/${idx}?${$.param({entry: eid})}`;
|
||||||
|
$.post(url)
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) throw new Error(data.error);
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
alert('danger', e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
Vendored
-5
File diff suppressed because one or more lines are too long
+43
-19
@@ -1,22 +1,44 @@
|
|||||||
const getTheme = () => {
|
// https://flaviocopes.com/javascript-detect-dark-mode/
|
||||||
var theme = localStorage.getItem('theme');
|
const preferDarkMode = () => {
|
||||||
if (!theme) theme = 'light';
|
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
return theme;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveTheme = theme => {
|
const validThemeSetting = (theme) => {
|
||||||
localStorage.setItem('theme', theme);
|
return ['dark', 'light', 'system'].indexOf(theme) >= 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// dark / light / system
|
||||||
|
const loadThemeSetting = () => {
|
||||||
|
let str = localStorage.getItem('theme');
|
||||||
|
if (!str || !validThemeSetting(str)) str = 'light';
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
// dark / light
|
||||||
|
const loadTheme = () => {
|
||||||
|
let setting = loadThemeSetting();
|
||||||
|
if (setting === 'system') {
|
||||||
|
setting = preferDarkMode() ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
return setting;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveThemeSetting = setting => {
|
||||||
|
if (!validThemeSetting(setting)) setting = 'light';
|
||||||
|
localStorage.setItem('theme', setting);
|
||||||
|
};
|
||||||
|
|
||||||
|
// when toggled, Auto will be changed to light or dark
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
const theme = getTheme();
|
const theme = loadTheme();
|
||||||
const newTheme = theme === 'dark' ? 'light' : 'dark';
|
const newTheme = theme === 'dark' ? 'light' : 'dark';
|
||||||
|
saveThemeSetting(newTheme);
|
||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
saveTheme(newTheme);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTheme = themeStr => {
|
const setTheme = (theme) => {
|
||||||
if (themeStr === 'dark') {
|
if (!theme) theme = loadTheme();
|
||||||
|
if (theme === 'dark') {
|
||||||
$('html').css('background', 'rgb(20, 20, 20)');
|
$('html').css('background', 'rgb(20, 20, 20)');
|
||||||
$('body').addClass('uk-light');
|
$('body').addClass('uk-light');
|
||||||
$('.uk-card').addClass('uk-card-secondary');
|
$('.uk-card').addClass('uk-card-secondary');
|
||||||
@@ -31,18 +53,20 @@ const setTheme = themeStr => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const styleModal = () => {
|
|
||||||
const color = getTheme() === 'dark' ? '#222' : '';
|
|
||||||
$('.uk-modal-header').css('background', color);
|
|
||||||
$('.uk-modal-body').css('background', color);
|
|
||||||
$('.uk-modal-footer').css('background', color);
|
|
||||||
};
|
|
||||||
|
|
||||||
// do it before document is ready to prevent the initial flash of white on
|
// do it before document is ready to prevent the initial flash of white on
|
||||||
// most pages
|
// most pages
|
||||||
setTheme(getTheme());
|
setTheme();
|
||||||
|
|
||||||
$(() => {
|
$(() => {
|
||||||
// hack for the reader page
|
// hack for the reader page
|
||||||
setTheme(getTheme());
|
setTheme();
|
||||||
|
|
||||||
|
// on system dark mode setting change
|
||||||
|
if (window.matchMedia) {
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
.addEventListener('change', event => {
|
||||||
|
if (loadThemeSetting() === 'system')
|
||||||
|
setTheme(event.matches ? 'dark' : 'light');
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+61
-3
@@ -55,10 +55,9 @@ function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTi
|
|||||||
|
|
||||||
$('#modal-edit-btn').attr('onclick', `edit("${entryID}")`);
|
$('#modal-edit-btn').attr('onclick', `edit("${entryID}")`);
|
||||||
|
|
||||||
$('#modal-download-btn').attr('href', `/opds/download/${titleID}/${entryID}`);
|
$('#modal-download-btn').attr('href', `${base_url}opds/download/${titleID}/${entryID}`);
|
||||||
|
|
||||||
UIkit.modal($('#modal')).show();
|
UIkit.modal($('#modal')).show();
|
||||||
styleModal();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateProgress = (tid, eid, page) => {
|
const updateProgress = (tid, eid, page) => {
|
||||||
@@ -144,7 +143,6 @@ const edit = (eid) => {
|
|||||||
setupUpload(eid);
|
setupUpload(eid);
|
||||||
|
|
||||||
UIkit.modal($('#edit-modal')).show();
|
UIkit.modal($('#edit-modal')).show();
|
||||||
styleModal();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setupUpload = (eid) => {
|
const setupUpload = (eid) => {
|
||||||
@@ -184,3 +182,63 @@ const setupUpload = (eid) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deselectAll = () => {
|
||||||
|
$('.item .uk-card').each((i, e) => {
|
||||||
|
const data = e.__x.$data;
|
||||||
|
data['selected'] = false;
|
||||||
|
});
|
||||||
|
$('#select-bar')[0].__x.$data['count'] = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAll = () => {
|
||||||
|
let count = 0;
|
||||||
|
$('.item .uk-card').each((i, e) => {
|
||||||
|
const data = e.__x.$data;
|
||||||
|
if (!data['disabled']) {
|
||||||
|
data['selected'] = true;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('#select-bar')[0].__x.$data['count'] = count;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedIDs = () => {
|
||||||
|
const ary = [];
|
||||||
|
$('.item .uk-card').each((i, e) => {
|
||||||
|
const data = e.__x.$data;
|
||||||
|
if (!data['disabled'] && data['selected']) {
|
||||||
|
const item = $(e).closest('.item');
|
||||||
|
ary.push($(item).attr('id'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ary;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkProgress = (action, el) => {
|
||||||
|
const tid = $(el).attr('data-id');
|
||||||
|
const ids = selectedIDs();
|
||||||
|
const url = `${base_url}api/bulk-progress/${action}/${tid}`;
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: url,
|
||||||
|
contentType: "application/json",
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
ids: ids
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.done(data => {
|
||||||
|
if (data.error) {
|
||||||
|
alert('danger', `Failed to mark entries as ${action}. Error: ${data.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
location.reload();
|
||||||
|
})
|
||||||
|
.fail((jqXHR, status) => {
|
||||||
|
alert('danger', `Failed to mark entries as ${action}. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
deselectAll();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
+17
-1
@@ -6,7 +6,7 @@ shards:
|
|||||||
|
|
||||||
archive:
|
archive:
|
||||||
github: hkalexling/archive.cr
|
github: hkalexling/archive.cr
|
||||||
version: 0.3.0
|
version: 0.4.0
|
||||||
|
|
||||||
baked_file_system:
|
baked_file_system:
|
||||||
github: schovi/baked_file_system
|
github: schovi/baked_file_system
|
||||||
@@ -20,10 +20,22 @@ shards:
|
|||||||
github: crystal-lang/crystal-db
|
github: crystal-lang/crystal-db
|
||||||
version: 0.9.0
|
version: 0.9.0
|
||||||
|
|
||||||
|
duktape:
|
||||||
|
github: jessedoyle/duktape.cr
|
||||||
|
version: 0.20.0
|
||||||
|
|
||||||
exception_page:
|
exception_page:
|
||||||
github: crystal-loot/exception_page
|
github: crystal-loot/exception_page
|
||||||
version: 0.1.4
|
version: 0.1.4
|
||||||
|
|
||||||
|
http_proxy:
|
||||||
|
github: mamantoha/http_proxy
|
||||||
|
version: 0.7.1
|
||||||
|
|
||||||
|
image_size:
|
||||||
|
github: hkalexling/image_size.cr
|
||||||
|
version: 0.2.0
|
||||||
|
|
||||||
kemal:
|
kemal:
|
||||||
github: kemalcr/kemal
|
github: kemalcr/kemal
|
||||||
version: 0.26.1
|
version: 0.26.1
|
||||||
@@ -36,6 +48,10 @@ shards:
|
|||||||
github: jeromegn/kilt
|
github: jeromegn/kilt
|
||||||
version: 0.4.0
|
version: 0.4.0
|
||||||
|
|
||||||
|
myhtml:
|
||||||
|
github: kostya/myhtml
|
||||||
|
version: 1.5.1
|
||||||
|
|
||||||
radix:
|
radix:
|
||||||
github: luislavena/radix
|
github: luislavena/radix
|
||||||
version: 0.3.9
|
version: 0.3.9
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: mango
|
name: mango
|
||||||
version: 0.8.0
|
version: 0.12.1
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Alex Ling <hkalexling@gmail.com>
|
- Alex Ling <hkalexling@gmail.com>
|
||||||
@@ -27,3 +27,12 @@ dependencies:
|
|||||||
github: crystal-ameba/ameba
|
github: crystal-ameba/ameba
|
||||||
clim:
|
clim:
|
||||||
github: at-grandpa/clim
|
github: at-grandpa/clim
|
||||||
|
duktape:
|
||||||
|
github: jessedoyle/duktape.cr
|
||||||
|
version: ~> 0.20.0
|
||||||
|
myhtml:
|
||||||
|
github: kostya/myhtml
|
||||||
|
http_proxy:
|
||||||
|
github: mamantoha/http_proxy
|
||||||
|
image_size:
|
||||||
|
github: hkalexling/image_size.cr
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
require "./spec_helper"
|
|
||||||
|
|
||||||
include MangaDex
|
|
||||||
|
|
||||||
describe Queue do
|
|
||||||
it "creates DB at given path" do
|
|
||||||
with_queue do |_, path|
|
|
||||||
File.exists?(path).should be_true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "pops nil when empty" do
|
|
||||||
with_queue do |queue|
|
|
||||||
queue.pop.should be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "inserts multiple jobs" do
|
|
||||||
with_queue do |queue|
|
|
||||||
j1 = Job.new "1", "1", "title", "manga_title", JobStatus::Error,
|
|
||||||
Time.utc
|
|
||||||
j2 = Job.new "2", "2", "title", "manga_title", JobStatus::Completed,
|
|
||||||
Time.utc
|
|
||||||
j3 = Job.new "3", "3", "title", "manga_title", JobStatus::Pending,
|
|
||||||
Time.utc
|
|
||||||
j4 = Job.new "4", "4", "title", "manga_title",
|
|
||||||
JobStatus::Downloading, Time.utc
|
|
||||||
count = queue.push [j1, j2, j3, j4]
|
|
||||||
count.should eq 4
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "pops pending job" do
|
|
||||||
with_queue do |queue|
|
|
||||||
job = queue.pop
|
|
||||||
job.should_not be_nil
|
|
||||||
job.not_nil!.id.should eq "3"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "correctly counts jobs" do
|
|
||||||
with_queue do |queue|
|
|
||||||
queue.count.should eq 4
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "deletes job" do
|
|
||||||
with_queue do |queue|
|
|
||||||
queue.delete "4"
|
|
||||||
queue.count.should eq 3
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "sets status" do
|
|
||||||
with_queue do |queue|
|
|
||||||
job = queue.pop.not_nil!
|
|
||||||
queue.set_status JobStatus::Downloading, job
|
|
||||||
job = queue.pop
|
|
||||||
job.should_not be_nil
|
|
||||||
job.not_nil!.status.should eq JobStatus::Downloading
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "sets number of pages" do
|
|
||||||
with_queue do |queue|
|
|
||||||
job = queue.pop.not_nil!
|
|
||||||
queue.set_pages 100, job
|
|
||||||
job = queue.pop
|
|
||||||
job.should_not be_nil
|
|
||||||
job.not_nil!.pages.should eq 100
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "adds fail/success counts" do
|
|
||||||
with_queue do |queue|
|
|
||||||
job = queue.pop.not_nil!
|
|
||||||
queue.add_success job
|
|
||||||
queue.add_success job
|
|
||||||
queue.add_fail job
|
|
||||||
job = queue.pop
|
|
||||||
job.should_not be_nil
|
|
||||||
job.not_nil!.success_count.should eq 2
|
|
||||||
job.not_nil!.fail_count.should eq 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "appends status message" do
|
|
||||||
with_queue do |queue|
|
|
||||||
job = queue.pop.not_nil!
|
|
||||||
queue.add_message "hello", job
|
|
||||||
queue.add_message "world", job
|
|
||||||
job = queue.pop
|
|
||||||
job.should_not be_nil
|
|
||||||
job.not_nil!.status_message.should eq "\nhello\nworld"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "cleans up" do
|
|
||||||
with_queue do
|
|
||||||
true
|
|
||||||
end
|
|
||||||
State.reset
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+2
-11
@@ -1,6 +1,8 @@
|
|||||||
require "spec"
|
require "spec"
|
||||||
|
require "../src/queue"
|
||||||
require "../src/server"
|
require "../src/server"
|
||||||
require "../src/config"
|
require "../src/config"
|
||||||
|
require "../src/main_fiber"
|
||||||
|
|
||||||
class State
|
class State
|
||||||
@@hash = {} of String => String
|
@@hash = {} of String => String
|
||||||
@@ -52,14 +54,3 @@ def with_storage
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_queue
|
|
||||||
with_default_config do
|
|
||||||
temp_queue_db = get_tempfile "mango-test-queue-db"
|
|
||||||
queue = MangaDex::Queue.new temp_queue_db.path
|
|
||||||
clear = yield queue, temp_queue_db.path
|
|
||||||
if clear == true
|
|
||||||
temp_queue_db.delete
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
+4
-6
@@ -16,6 +16,8 @@ class Config
|
|||||||
property log_level : String = "info"
|
property log_level : String = "info"
|
||||||
property upload_path : String = File.expand_path "~/mango/uploads",
|
property upload_path : String = File.expand_path "~/mango/uploads",
|
||||||
home: true
|
home: true
|
||||||
|
property plugin_path : String = File.expand_path "~/mango/plugins",
|
||||||
|
home: true
|
||||||
property mangadex = Hash(String, String | Int32).new
|
property mangadex = Hash(String, String | Int32).new
|
||||||
|
|
||||||
@[YAML::Field(ignore: true)]
|
@[YAML::Field(ignore: true)]
|
||||||
@@ -50,12 +52,8 @@ class Config
|
|||||||
config.fill_defaults
|
config.fill_defaults
|
||||||
return config
|
return config
|
||||||
end
|
end
|
||||||
puts "The config file #{cfg_path} does not exist." \
|
puts "The config file #{cfg_path} does not exist. " \
|
||||||
" Do you want mango to dump the default config there? [Y/n]"
|
"Dumping the default config there."
|
||||||
input = gets
|
|
||||||
if input && input.downcase == "n"
|
|
||||||
abort "Aborting..."
|
|
||||||
end
|
|
||||||
default = self.allocate
|
default = self.allocate
|
||||||
default.path = path
|
default.path = path
|
||||||
default.fill_defaults
|
default.fill_defaults
|
||||||
|
|||||||
+65
-19
@@ -1,20 +1,15 @@
|
|||||||
|
require "image_size"
|
||||||
|
|
||||||
class Entry
|
class Entry
|
||||||
property zip_path : String, book : Title, title : String,
|
property zip_path : String, book : Title, title : String,
|
||||||
size : String, pages : Int32, id : String, title_id : String,
|
size : String, pages : Int32, id : String, encoded_path : String,
|
||||||
encoded_path : String, encoded_title : String, mtime : Time
|
encoded_title : String, mtime : Time, err_msg : String?
|
||||||
|
|
||||||
def initialize(path, @book, @title_id, storage)
|
def initialize(@zip_path, @book, storage)
|
||||||
@zip_path = path
|
@encoded_path = URI.encode @zip_path
|
||||||
@encoded_path = URI.encode path
|
@title = File.basename @zip_path, File.extname @zip_path
|
||||||
@title = File.basename path, File.extname path
|
|
||||||
@encoded_title = URI.encode @title
|
@encoded_title = URI.encode @title
|
||||||
@size = (File.size path).humanize_bytes
|
@size = (File.size @zip_path).humanize_bytes
|
||||||
file = ArchiveFile.new path
|
|
||||||
@pages = file.entries.count do |e|
|
|
||||||
SUPPORTED_IMG_TYPES.includes? \
|
|
||||||
MIME.from_filename? e.filename
|
|
||||||
end
|
|
||||||
file.close
|
|
||||||
id = storage.get_id @zip_path, false
|
id = storage.get_id @zip_path, false
|
||||||
if id.nil?
|
if id.nil?
|
||||||
id = random_str
|
id = random_str
|
||||||
@@ -26,14 +21,37 @@ class Entry
|
|||||||
end
|
end
|
||||||
@id = id
|
@id = id
|
||||||
@mtime = File.info(@zip_path).modification_time
|
@mtime = File.info(@zip_path).modification_time
|
||||||
|
|
||||||
|
unless File.readable? @zip_path
|
||||||
|
@err_msg = "File #{@zip_path} is not readable."
|
||||||
|
Logger.warn "#{@err_msg} Please make sure the " \
|
||||||
|
"file permission is configured correctly."
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
archive_exception = validate_archive @zip_path
|
||||||
|
unless archive_exception.nil?
|
||||||
|
@err_msg = "Archive error: #{archive_exception}"
|
||||||
|
Logger.warn "Unable to extract archive #{@zip_path}. " \
|
||||||
|
"Ignoring it. #{@err_msg}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
file = ArchiveFile.new @zip_path
|
||||||
|
@pages = file.entries.count do |e|
|
||||||
|
SUPPORTED_IMG_TYPES.includes? \
|
||||||
|
MIME.from_filename? e.filename
|
||||||
|
end
|
||||||
|
file.close
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_json(json : JSON::Builder)
|
def to_json(json : JSON::Builder)
|
||||||
json.object do
|
json.object do
|
||||||
{% for str in ["zip_path", "title", "size", "id", "title_id",
|
{% for str in ["zip_path", "title", "size", "id",
|
||||||
"encoded_path", "encoded_title"] %}
|
"encoded_path", "encoded_title"] %}
|
||||||
json.field {{str}}, @{{str.id}}
|
json.field {{str}}, @{{str.id}}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
json.field "title_id", @book.id
|
||||||
json.field "display_name", @book.display_name @title
|
json.field "display_name", @book.display_name @title
|
||||||
json.field "cover_url", cover_url
|
json.field "cover_url", cover_url
|
||||||
json.field "pages" { json.number @pages }
|
json.field "pages" { json.number @pages }
|
||||||
@@ -50,7 +68,8 @@ class Entry
|
|||||||
end
|
end
|
||||||
|
|
||||||
def cover_url
|
def cover_url
|
||||||
url = "#{Config.current.base_url}api/page/#{@title_id}/#{@id}/1"
|
return "#{Config.current.base_url}img/icon.png" if @err_msg
|
||||||
|
url = "#{Config.current.base_url}api/page/#{@book.id}/#{@id}/1"
|
||||||
TitleInfo.new @book.dir do |info|
|
TitleInfo.new @book.dir do |info|
|
||||||
info_url = info.entry_cover_url[@title]?
|
info_url = info.entry_cover_url[@title]?
|
||||||
unless info_url.nil? || info_url.empty?
|
unless info_url.nil? || info_url.empty?
|
||||||
@@ -60,10 +79,9 @@ class Entry
|
|||||||
url
|
url
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_page(page_num)
|
private def sorted_archive_entries
|
||||||
img = nil
|
|
||||||
ArchiveFile.open @zip_path do |file|
|
ArchiveFile.open @zip_path do |file|
|
||||||
page = file.entries
|
entries = file.entries
|
||||||
.select { |e|
|
.select { |e|
|
||||||
SUPPORTED_IMG_TYPES.includes? \
|
SUPPORTED_IMG_TYPES.includes? \
|
||||||
MIME.from_filename? e.filename
|
MIME.from_filename? e.filename
|
||||||
@@ -71,7 +89,15 @@ class Entry
|
|||||||
.sort { |a, b|
|
.sort { |a, b|
|
||||||
compare_numerically a.filename, b.filename
|
compare_numerically a.filename, b.filename
|
||||||
}
|
}
|
||||||
.[page_num - 1]
|
yield file, entries
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_page(page_num)
|
||||||
|
raise "Unreadble archive. #{@err_msg}" if @err_msg
|
||||||
|
img = nil
|
||||||
|
sorted_archive_entries do |file, entries|
|
||||||
|
page = entries[page_num - 1]
|
||||||
data = file.read_entry page
|
data = file.read_entry page
|
||||||
if data
|
if data
|
||||||
img = Image.new data, MIME.from_filename(page.filename), page.filename,
|
img = Image.new data, MIME.from_filename(page.filename), page.filename,
|
||||||
@@ -81,6 +107,26 @@ class Entry
|
|||||||
img
|
img
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def page_dimensions
|
||||||
|
sizes = [] of Hash(String, Int32)
|
||||||
|
sorted_archive_entries do |file, entries|
|
||||||
|
entries.each_with_index do |e, i|
|
||||||
|
begin
|
||||||
|
data = file.read_entry(e).not_nil!
|
||||||
|
size = ImageSize.get data
|
||||||
|
sizes << {
|
||||||
|
"width" => size.width,
|
||||||
|
"height" => size.height,
|
||||||
|
}
|
||||||
|
rescue
|
||||||
|
Logger.warn "Failed to read page #{i} of entry #{@id}"
|
||||||
|
sizes << {"width" => 1000_i32, "height" => 1000_i32}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
sizes
|
||||||
|
end
|
||||||
|
|
||||||
def next_entry(username)
|
def next_entry(username)
|
||||||
entries = @book.sorted_entries username
|
entries = @book.sorted_entries username
|
||||||
idx = entries.index self
|
idx = entries.index self
|
||||||
|
|||||||
+52
-43
@@ -2,12 +2,7 @@ class Library
|
|||||||
property dir : String, title_ids : Array(String), scan_interval : Int32,
|
property dir : String, title_ids : Array(String), scan_interval : Int32,
|
||||||
title_hash : Hash(String, Title)
|
title_hash : Hash(String, Title)
|
||||||
|
|
||||||
def self.default : self
|
use_default
|
||||||
unless @@default
|
|
||||||
@@default = new
|
|
||||||
end
|
|
||||||
@@default.not_nil!
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
register_mime_types
|
register_mime_types
|
||||||
@@ -35,6 +30,41 @@ class Library
|
|||||||
@title_ids.map { |tid| self.get_title!(tid) }
|
@title_ids.map { |tid| self.get_title!(tid) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sorted_titles(username, opt : SortOptions? = nil)
|
||||||
|
if opt.nil?
|
||||||
|
opt = SortOptions.from_info_json @dir, username
|
||||||
|
else
|
||||||
|
TitleInfo.new @dir do |info|
|
||||||
|
info.sort_by[username] = opt.to_tuple
|
||||||
|
info.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This is a hack to bypass a compiler bug
|
||||||
|
ary = titles
|
||||||
|
|
||||||
|
case opt.not_nil!.method
|
||||||
|
when .time_modified?
|
||||||
|
ary.sort! { |a, b| (a.mtime <=> b.mtime).or \
|
||||||
|
compare_numerically a.title, b.title }
|
||||||
|
when .progress?
|
||||||
|
ary.sort! do |a, b|
|
||||||
|
(a.load_percentage(username) <=> b.load_percentage(username)).or \
|
||||||
|
compare_numerically a.title, b.title
|
||||||
|
end
|
||||||
|
else
|
||||||
|
unless opt.method.auto?
|
||||||
|
Logger.warn "Unknown sorting method #{opt.not_nil!.method}. Using " \
|
||||||
|
"Auto instead"
|
||||||
|
end
|
||||||
|
ary.sort! { |a, b| compare_numerically a.title, b.title }
|
||||||
|
end
|
||||||
|
|
||||||
|
ary.reverse! unless opt.not_nil!.ascend
|
||||||
|
|
||||||
|
ary
|
||||||
|
end
|
||||||
|
|
||||||
def deep_titles
|
def deep_titles
|
||||||
titles + titles.map { |t| t.deep_titles }.flatten
|
titles + titles.map { |t| t.deep_titles }.flatten
|
||||||
end
|
end
|
||||||
@@ -62,7 +92,6 @@ class Library
|
|||||||
"Attempting to create it"
|
"Attempting to create it"
|
||||||
Dir.mkdir_p @dir
|
Dir.mkdir_p @dir
|
||||||
end
|
end
|
||||||
@title_ids.clear
|
|
||||||
|
|
||||||
storage = Storage.new auto_close: false
|
storage = Storage.new auto_close: false
|
||||||
|
|
||||||
@@ -73,6 +102,7 @@ class Library
|
|||||||
.map { |path| Title.new path, "", storage, self }
|
.map { |path| Title.new path, "", storage, self }
|
||||||
.select { |title| !(title.entries.empty? && title.titles.empty?) }
|
.select { |title| !(title.entries.empty? && title.titles.empty?) }
|
||||||
.sort { |a, b| a.title <=> b.title }
|
.sort { |a, b| a.title <=> b.title }
|
||||||
|
.tap { |_| @title_ids.clear }
|
||||||
.each do |title|
|
.each do |title|
|
||||||
@title_hash[title.id] = title
|
@title_hash[title.id] = title
|
||||||
@title_ids << title.id
|
@title_ids << title.id
|
||||||
@@ -88,7 +118,7 @@ class Library
|
|||||||
cr_entries = deep_titles
|
cr_entries = deep_titles
|
||||||
.map { |t| t.get_last_read_entry username }
|
.map { |t| t.get_last_read_entry username }
|
||||||
# Select elements with type `Entry` from the array and ignore all `Nil`s
|
# Select elements with type `Entry` from the array and ignore all `Nil`s
|
||||||
.select(Entry)[0..11]
|
.select(Entry)[0...ENTRIES_IN_HOME_SECTIONS]
|
||||||
.map { |e|
|
.map { |e|
|
||||||
# Get the last read time of the entry. If it hasn't been started, get
|
# Get the last read time of the entry. If it hasn't been started, get
|
||||||
# the last read time of the previous entry
|
# the last read time of the previous entry
|
||||||
@@ -128,7 +158,7 @@ class Library
|
|||||||
.each do |e|
|
.each do |e|
|
||||||
break if recently_added.size > 12
|
break if recently_added.size > 12
|
||||||
last = recently_added.last?
|
last = recently_added.last?
|
||||||
if last && e[:entry].title_id == last[:entry].title_id &&
|
if last && e[:entry].book.id == last[:entry].book.id &&
|
||||||
(e[:date_added] - last_date_added.not_nil!).duration < 1.day
|
(e[:date_added] - last_date_added.not_nil!).duration < 1.day
|
||||||
# A NamedTuple is immutable, so we have to cast it to a Hash first
|
# A NamedTuple is immutable, so we have to cast it to a Hash first
|
||||||
last_hash = last.to_h
|
last_hash = last.to_h
|
||||||
@@ -148,41 +178,20 @@ class Library
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
recently_added[0..11]
|
recently_added[0...ENTRIES_IN_HOME_SECTIONS]
|
||||||
end
|
end
|
||||||
|
|
||||||
def sorted_titles(username, opt : SortOptions? = nil)
|
def get_start_reading_titles(username)
|
||||||
if opt.nil?
|
# Here we are not using `deep_titles` as it may cause unexpected behaviors
|
||||||
opt = SortOptions.from_info_json @dir, username
|
# For example, consider the following nested titles:
|
||||||
else
|
# - One Puch Man
|
||||||
TitleInfo.new @dir do |info|
|
# - Vol. 1
|
||||||
info.sort_by[username] = opt.to_tuple
|
# - Vol. 2
|
||||||
info.save
|
# If we use `deep_titles`, the start reading section might include `Vol. 2`
|
||||||
end
|
# when the user hasn't started `Vol. 1` yet
|
||||||
end
|
titles
|
||||||
|
.select { |t| t.load_percentage(username) == 0 }
|
||||||
# This is a hack to bypass a compiler bug
|
.sample(ENTRIES_IN_HOME_SECTIONS)
|
||||||
ary = titles
|
.shuffle
|
||||||
|
|
||||||
case opt.not_nil!.method
|
|
||||||
when .time_modified?
|
|
||||||
ary.sort! { |a, b| (a.mtime <=> b.mtime).or \
|
|
||||||
compare_numerically a.title, b.title }
|
|
||||||
when .progress?
|
|
||||||
ary.sort! do |a, b|
|
|
||||||
(a.load_percentage(username) <=> b.load_percentage(username)).or \
|
|
||||||
compare_numerically a.title, b.title
|
|
||||||
end
|
|
||||||
else
|
|
||||||
unless opt.method.auto?
|
|
||||||
Logger.warn "Unknown sorting method #{opt.not_nil!.method}. Using " \
|
|
||||||
"Auto instead"
|
|
||||||
end
|
|
||||||
ary.sort! { |a, b| compare_numerically a.title, b.title }
|
|
||||||
end
|
|
||||||
|
|
||||||
ary.reverse! unless opt.not_nil!.ascend
|
|
||||||
|
|
||||||
ary
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+25
-15
@@ -34,19 +34,8 @@ class Title
|
|||||||
next
|
next
|
||||||
end
|
end
|
||||||
if [".zip", ".cbz", ".rar", ".cbr"].includes? File.extname path
|
if [".zip", ".cbz", ".rar", ".cbr"].includes? File.extname path
|
||||||
unless File.readable? path
|
entry = Entry.new path, self, storage
|
||||||
Logger.warn "File #{path} is not readable. Please make sure the " \
|
@entries << entry if entry.pages > 0 || entry.err_msg
|
||||||
"file permission is configured correctly."
|
|
||||||
next
|
|
||||||
end
|
|
||||||
archive_exception = validate_archive path
|
|
||||||
unless archive_exception.nil?
|
|
||||||
Logger.warn "Unable to extract archive #{path}. Ignoring it. " \
|
|
||||||
"Archive error: #{archive_exception}"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
entry = Entry.new path, self, @id, storage
|
|
||||||
@entries << entry if entry.pages > 0
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -166,8 +155,9 @@ class Title
|
|||||||
|
|
||||||
def cover_url
|
def cover_url
|
||||||
url = "#{Config.current.base_url}img/icon.png"
|
url = "#{Config.current.base_url}img/icon.png"
|
||||||
if @entries.size > 0
|
readable_entries = @entries.select &.err_msg.nil?
|
||||||
url = @entries[0].cover_url
|
if readable_entries.size > 0
|
||||||
|
url = readable_entries[0].cover_url
|
||||||
end
|
end
|
||||||
TitleInfo.new @dir do |info|
|
TitleInfo.new @dir do |info|
|
||||||
info_url = info.cover_url
|
info_url = info.cover_url
|
||||||
@@ -365,4 +355,24 @@ class Title
|
|||||||
return zip if title_ids.empty?
|
return zip if title_ids.empty?
|
||||||
zip + titles.map { |t| t.deep_entries_with_date_added }.flatten
|
zip + titles.map { |t| t.deep_entries_with_date_added }.flatten
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bulk_progress(action, ids : Array(String), username)
|
||||||
|
selected_entries = ids
|
||||||
|
.map { |id|
|
||||||
|
@entries.find { |e| e.id == id }
|
||||||
|
}
|
||||||
|
.select(Entry)
|
||||||
|
|
||||||
|
TitleInfo.new @dir do |info|
|
||||||
|
selected_entries.each do |e|
|
||||||
|
page = action == "read" ? e.pages : 0
|
||||||
|
if info.progress[username]?.nil?
|
||||||
|
info.progress[username] = {e.title => page}
|
||||||
|
else
|
||||||
|
info.progress[username][e.title] = page
|
||||||
|
end
|
||||||
|
end
|
||||||
|
info.save
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-6
@@ -8,12 +8,7 @@ class Logger
|
|||||||
|
|
||||||
@@severity : Log::Severity = :info
|
@@severity : Log::Severity = :info
|
||||||
|
|
||||||
def self.default : self
|
use_default
|
||||||
unless @@default
|
|
||||||
@@default = new
|
|
||||||
end
|
|
||||||
@@default.not_nil!
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
level = Config.current.log_level
|
level = Config.current.log_level
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# On ARM, connecting to the SQLite DB from a spawned fiber would crash
|
||||||
|
# https://github.com/crystal-lang/crystal-sqlite3/issues/30
|
||||||
|
# This is a temporary workaround that forces the relevant code to run in the
|
||||||
|
# main fiber
|
||||||
|
|
||||||
|
class MainFiber
|
||||||
|
@@channel = Channel(-> Nil).new
|
||||||
|
@@done = Channel(Bool).new
|
||||||
|
@@main_fiber = Fiber.current
|
||||||
|
|
||||||
|
def self.start_and_block
|
||||||
|
loop do
|
||||||
|
if proc = @@channel.receive
|
||||||
|
begin
|
||||||
|
proc.call
|
||||||
|
ensure
|
||||||
|
@@done.send true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Fiber.yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.run(&block : -> Nil)
|
||||||
|
if @@main_fiber == Fiber.current
|
||||||
|
block.call
|
||||||
|
else
|
||||||
|
@@channel.send block
|
||||||
|
until @@done.receive
|
||||||
|
Fiber.yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
+1
-7
@@ -1,4 +1,3 @@
|
|||||||
require "http/client"
|
|
||||||
require "json"
|
require "json"
|
||||||
require "csv"
|
require "csv"
|
||||||
require "../rename"
|
require "../rename"
|
||||||
@@ -131,12 +130,7 @@ module MangaDex
|
|||||||
end
|
end
|
||||||
|
|
||||||
class API
|
class API
|
||||||
def self.default : self
|
use_default
|
||||||
unless @@default
|
|
||||||
@@default = new
|
|
||||||
end
|
|
||||||
@@default.not_nil!
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@base_url = Config.current.mangadex["api_url"].to_s ||
|
@base_url = Config.current.mangadex["api_url"].to_s ||
|
||||||
|
|||||||
+21
-276
@@ -1,5 +1,4 @@
|
|||||||
require "./api"
|
require "./api"
|
||||||
require "sqlite3"
|
|
||||||
require "zip"
|
require "zip"
|
||||||
|
|
||||||
module MangaDex
|
module MangaDex
|
||||||
@@ -14,297 +13,43 @@ module MangaDex
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
enum JobStatus
|
class Downloader < Queue::Downloader
|
||||||
Pending # 0
|
|
||||||
Downloading # 1
|
|
||||||
Error # 2
|
|
||||||
Completed # 3
|
|
||||||
MissingPages # 4
|
|
||||||
end
|
|
||||||
|
|
||||||
struct Job
|
|
||||||
property id : String
|
|
||||||
property manga_id : String
|
|
||||||
property title : String
|
|
||||||
property manga_title : String
|
|
||||||
property status : JobStatus
|
|
||||||
property status_message : String = ""
|
|
||||||
property pages : Int32 = 0
|
|
||||||
property success_count : Int32 = 0
|
|
||||||
property fail_count : Int32 = 0
|
|
||||||
property time : Time
|
|
||||||
|
|
||||||
def parse_query_result(res : DB::ResultSet)
|
|
||||||
@id = res.read String
|
|
||||||
@manga_id = res.read String
|
|
||||||
@title = res.read String
|
|
||||||
@manga_title = res.read String
|
|
||||||
status = res.read Int32
|
|
||||||
@status_message = res.read String
|
|
||||||
@pages = res.read Int32
|
|
||||||
@success_count = res.read Int32
|
|
||||||
@fail_count = res.read Int32
|
|
||||||
time = res.read Int64
|
|
||||||
@status = JobStatus.new status
|
|
||||||
@time = Time.unix_ms time
|
|
||||||
end
|
|
||||||
|
|
||||||
# Raises if the result set does not contain the correct set of columns
|
|
||||||
def self.from_query_result(res : DB::ResultSet)
|
|
||||||
job = Job.allocate
|
|
||||||
job.parse_query_result res
|
|
||||||
job
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(@id, @manga_id, @title, @manga_title, @status, @time)
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_json(json)
|
|
||||||
json.object do
|
|
||||||
{% for name in ["id", "manga_id", "title", "manga_title",
|
|
||||||
"status_message"] %}
|
|
||||||
json.field {{name}}, @{{name.id}}
|
|
||||||
{% end %}
|
|
||||||
{% for name in ["pages", "success_count", "fail_count"] %}
|
|
||||||
json.field {{name}} do
|
|
||||||
json.number @{{name.id}}
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
json.field "status", @status.to_s
|
|
||||||
json.field "time" do
|
|
||||||
json.number @time.to_unix_ms
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Queue
|
|
||||||
property downloader : Downloader?
|
|
||||||
@path : String
|
|
||||||
|
|
||||||
def self.default : self
|
|
||||||
unless @@default
|
|
||||||
@@default = new
|
|
||||||
end
|
|
||||||
@@default.not_nil!
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(db_path : String? = nil)
|
|
||||||
@path = db_path || Config.current.mangadex["download_queue_db_path"].to_s
|
|
||||||
dir = File.dirname @path
|
|
||||||
unless Dir.exists? dir
|
|
||||||
Logger.info "The queue DB directory #{dir} does not exist. " \
|
|
||||||
"Attepmting to create it"
|
|
||||||
Dir.mkdir_p dir
|
|
||||||
end
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
begin
|
|
||||||
db.exec "create table if not exists queue " \
|
|
||||||
"(id text, manga_id text, title text, manga_title " \
|
|
||||||
"text, status integer, status_message text, " \
|
|
||||||
"pages integer, success_count integer, " \
|
|
||||||
"fail_count integer, time integer)"
|
|
||||||
db.exec "create unique index if not exists id_idx " \
|
|
||||||
"on queue (id)"
|
|
||||||
db.exec "create index if not exists manga_id_idx " \
|
|
||||||
"on queue (manga_id)"
|
|
||||||
db.exec "create index if not exists status_idx " \
|
|
||||||
"on queue (status)"
|
|
||||||
rescue e
|
|
||||||
Logger.error "Error when checking tables in DB: #{e}"
|
|
||||||
raise e
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the earliest job in queue or nil if the job cannot be parsed.
|
|
||||||
# Returns nil if queue is empty
|
|
||||||
def pop
|
|
||||||
job = nil
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
begin
|
|
||||||
db.query_one "select * from queue where status = 0 " \
|
|
||||||
"or status = 1 order by time limit 1" do |res|
|
|
||||||
job = Job.from_query_result res
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
end
|
|
||||||
end
|
|
||||||
job
|
|
||||||
end
|
|
||||||
|
|
||||||
# Push an array of jobs into the queue, and return the number of jobs
|
|
||||||
# inserted. Any job already exists in the queue will be ignored.
|
|
||||||
def push(jobs : Array(Job))
|
|
||||||
start_count = self.count
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
jobs.each do |job|
|
|
||||||
db.exec "insert or ignore into queue values " \
|
|
||||||
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
job.id, job.manga_id, job.title, job.manga_title,
|
|
||||||
job.status.to_i, job.status_message, job.pages,
|
|
||||||
job.success_count, job.fail_count, job.time.to_unix_ms
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self.count - start_count
|
|
||||||
end
|
|
||||||
|
|
||||||
def reset(id : String)
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
db.exec "update queue set status = 0, status_message = '', " \
|
|
||||||
"pages = 0, success_count = 0, fail_count = 0 " \
|
|
||||||
"where id = (?)", id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def reset(job : Job)
|
|
||||||
self.reset job.id
|
|
||||||
end
|
|
||||||
|
|
||||||
# Reset all failed tasks (missing pages and error)
|
|
||||||
def reset
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
db.exec "update queue set status = 0, status_message = '', " \
|
|
||||||
"pages = 0, success_count = 0, fail_count = 0 " \
|
|
||||||
"where status = 2 or status = 4"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete(id : String)
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
db.exec "delete from queue where id = (?)", id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete(job : Job)
|
|
||||||
self.delete job.id
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete_status(status : JobStatus)
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
db.exec "delete from queue where status = (?)", status.to_i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def count_status(status : JobStatus)
|
|
||||||
num = 0
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
num = db.query_one "select count(*) from queue where " \
|
|
||||||
"status = (?)", status.to_i, as: Int32
|
|
||||||
end
|
|
||||||
num
|
|
||||||
end
|
|
||||||
|
|
||||||
def count
|
|
||||||
num = 0
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
num = db.query_one "select count(*) from queue", as: Int32
|
|
||||||
end
|
|
||||||
num
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_status(status : JobStatus, job : Job)
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
db.exec "update queue set status = (?) where id = (?)",
|
|
||||||
status.to_i, job.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_all
|
|
||||||
jobs = [] of Job
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
jobs = db.query_all "select * from queue order by time" do |rs|
|
|
||||||
Job.from_query_result rs
|
|
||||||
end
|
|
||||||
end
|
|
||||||
jobs
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_success(job : Job)
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
db.exec "update queue set success_count = success_count + 1 " \
|
|
||||||
"where id = (?)", job.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_fail(job : Job)
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
db.exec "update queue set fail_count = fail_count + 1 " \
|
|
||||||
"where id = (?)", job.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_pages(pages : Int32, job : Job)
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
db.exec "update queue set pages = (?), success_count = 0, " \
|
|
||||||
"fail_count = 0 where id = (?)", pages, job.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_message(msg : String, job : Job)
|
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
|
||||||
db.exec "update queue set status_message = " \
|
|
||||||
"status_message || (?) || (?) where id = (?)",
|
|
||||||
"\n", msg, job.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def pause
|
|
||||||
@downloader.not_nil!.stopped = true
|
|
||||||
end
|
|
||||||
|
|
||||||
def resume
|
|
||||||
@downloader.not_nil!.stopped = false
|
|
||||||
end
|
|
||||||
|
|
||||||
def paused?
|
|
||||||
@downloader.not_nil!.stopped
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Downloader
|
|
||||||
property stopped = false
|
|
||||||
@wait_seconds : Int32 = Config.current.mangadex["download_wait_seconds"]
|
@wait_seconds : Int32 = Config.current.mangadex["download_wait_seconds"]
|
||||||
.to_i32
|
.to_i32
|
||||||
@retries : Int32 = Config.current.mangadex["download_retries"].to_i32
|
@retries : Int32 = Config.current.mangadex["download_retries"].to_i32
|
||||||
@library_path : String = Config.current.library_path
|
|
||||||
@downloading = false
|
|
||||||
|
|
||||||
def self.default : self
|
use_default
|
||||||
unless @@default
|
|
||||||
@@default = new
|
|
||||||
end
|
|
||||||
@@default.not_nil!
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@queue = Queue.default
|
|
||||||
@api = API.default
|
@api = API.default
|
||||||
@queue.downloader = self
|
super
|
||||||
|
end
|
||||||
|
|
||||||
spawn do
|
def pop : Queue::Job?
|
||||||
loop do
|
job = nil
|
||||||
sleep 1.second
|
MainFiber.run do
|
||||||
next if @stopped || @downloading
|
DB.open "sqlite3://#{@queue.path}" do |db|
|
||||||
begin
|
begin
|
||||||
job = @queue.pop
|
db.query_one "select * from queue where id not like '%-%' " \
|
||||||
next if job.nil?
|
"and (status = 0 or status = 1) " \
|
||||||
download job
|
"order by time limit 1" do |res|
|
||||||
rescue e
|
job = Queue::Job.from_query_result res
|
||||||
Logger.error e
|
end
|
||||||
|
rescue
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
job
|
||||||
end
|
end
|
||||||
|
|
||||||
private def download(job : Job)
|
private def download(job : Queue::Job)
|
||||||
@downloading = true
|
@downloading = true
|
||||||
@queue.set_status JobStatus::Downloading, job
|
@queue.set_status Queue::JobStatus::Downloading, job
|
||||||
begin
|
begin
|
||||||
chapter = @api.get_chapter(job.id)
|
chapter = @api.get_chapter(job.id)
|
||||||
rescue e
|
rescue e
|
||||||
Logger.error e
|
Logger.error e
|
||||||
@queue.set_status JobStatus::Error, job
|
@queue.set_status Queue::JobStatus::Error, job
|
||||||
unless e.message.nil?
|
unless e.message.nil?
|
||||||
@queue.add_message e.message.not_nil!, job
|
@queue.add_message e.message.not_nil!, job
|
||||||
end
|
end
|
||||||
@@ -378,11 +123,11 @@ module MangaDex
|
|||||||
if !zip_exception.nil?
|
if !zip_exception.nil?
|
||||||
@queue.add_message "The downloaded archive is corrupted. " \
|
@queue.add_message "The downloaded archive is corrupted. " \
|
||||||
"Error: #{zip_exception}", job
|
"Error: #{zip_exception}", job
|
||||||
@queue.set_status JobStatus::Error, job
|
@queue.set_status Queue::JobStatus::Error, job
|
||||||
elsif fail_count > 0
|
elsif fail_count > 0
|
||||||
@queue.set_status JobStatus::MissingPages, job
|
@queue.set_status Queue::JobStatus::MissingPages, job
|
||||||
else
|
else
|
||||||
@queue.set_status JobStatus::Completed, job
|
@queue.set_status Queue::JobStatus::Completed, job
|
||||||
end
|
end
|
||||||
@downloading = false
|
@downloading = false
|
||||||
end
|
end
|
||||||
|
|||||||
+34
-6
@@ -1,10 +1,29 @@
|
|||||||
require "./config"
|
require "./config"
|
||||||
|
require "./queue"
|
||||||
require "./server"
|
require "./server"
|
||||||
|
require "./main_fiber"
|
||||||
require "./mangadex/*"
|
require "./mangadex/*"
|
||||||
require "option_parser"
|
require "option_parser"
|
||||||
require "clim"
|
require "clim"
|
||||||
|
require "./plugin/*"
|
||||||
|
|
||||||
MANGO_VERSION = "0.8.0"
|
MANGO_VERSION = "0.12.1"
|
||||||
|
|
||||||
|
# From http://www.network-science.de/ascii/
|
||||||
|
BANNER = %{
|
||||||
|
|
||||||
|
_| _|
|
||||||
|
_|_| _|_| _|_|_| _|_|_| _|_|_| _|_|
|
||||||
|
_| _| _| _| _| _| _| _| _| _| _|
|
||||||
|
_| _| _| _| _| _| _| _| _| _|
|
||||||
|
_| _| _|_|_| _| _| _|_|_| _|_|
|
||||||
|
_|
|
||||||
|
_|_|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DESCRIPTION = "Mango - Manga Server and Web Reader. Version #{MANGO_VERSION}"
|
||||||
|
|
||||||
macro common_option
|
macro common_option
|
||||||
option "-c PATH", "--config=PATH", type: String,
|
option "-c PATH", "--config=PATH", type: String,
|
||||||
@@ -20,19 +39,28 @@ end
|
|||||||
|
|
||||||
class CLI < Clim
|
class CLI < Clim
|
||||||
main do
|
main do
|
||||||
desc "Mango - Manga Server and Web Reader. Version #{MANGO_VERSION}"
|
desc DESCRIPTION
|
||||||
usage "mango [sub_command] [options]"
|
usage "mango [sub_command] [options]"
|
||||||
help short: "-h"
|
help short: "-h"
|
||||||
version "Version #{MANGO_VERSION}", short: "-v"
|
version "Version #{MANGO_VERSION}", short: "-v"
|
||||||
common_option
|
common_option
|
||||||
run do |opts|
|
run do |opts|
|
||||||
Config.load(opts.config).set_current
|
puts BANNER
|
||||||
MangaDex::Downloader.default
|
puts DESCRIPTION
|
||||||
|
puts
|
||||||
|
|
||||||
# empty ARGV so it won't be passed to Kemal
|
# empty ARGV so it won't be passed to Kemal
|
||||||
ARGV.clear
|
ARGV.clear
|
||||||
server = Server.new
|
|
||||||
server.start
|
Config.load(opts.config).set_current
|
||||||
|
MangaDex::Downloader.default
|
||||||
|
Plugin::Downloader.default
|
||||||
|
|
||||||
|
spawn do
|
||||||
|
Server.new.start
|
||||||
|
end
|
||||||
|
|
||||||
|
MainFiber.start_and_block
|
||||||
end
|
end
|
||||||
|
|
||||||
sub "admin" do
|
sub "admin" do
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
class Plugin
|
||||||
|
class Downloader < Queue::Downloader
|
||||||
|
use_default
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def pop : Queue::Job?
|
||||||
|
job = nil
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@queue.path}" do |db|
|
||||||
|
begin
|
||||||
|
db.query_one "select * from queue where id like '%-%' " \
|
||||||
|
"and (status = 0 or status = 1) " \
|
||||||
|
"order by time limit 1" do |res|
|
||||||
|
job = Queue::Job.from_query_result res
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
job
|
||||||
|
end
|
||||||
|
|
||||||
|
private def process_filename(str)
|
||||||
|
return "_" if str == ".."
|
||||||
|
str.gsub "/", "_"
|
||||||
|
end
|
||||||
|
|
||||||
|
private def download(job : Queue::Job)
|
||||||
|
@downloading = true
|
||||||
|
@queue.set_status Queue::JobStatus::Downloading, job
|
||||||
|
|
||||||
|
begin
|
||||||
|
unless job.plugin_id
|
||||||
|
raise "Job does not have a plugin ID specificed"
|
||||||
|
end
|
||||||
|
|
||||||
|
plugin = Plugin.new job.plugin_id.not_nil!
|
||||||
|
info = plugin.select_chapter job.plugin_chapter_id.not_nil!
|
||||||
|
|
||||||
|
pages = info["pages"].as_i
|
||||||
|
|
||||||
|
manga_title = process_filename job.manga_title
|
||||||
|
chapter_title = process_filename info["title"].as_s
|
||||||
|
|
||||||
|
@queue.set_pages pages, job
|
||||||
|
lib_dir = @library_path
|
||||||
|
manga_dir = File.join lib_dir, manga_title
|
||||||
|
unless File.exists? manga_dir
|
||||||
|
Dir.mkdir_p manga_dir
|
||||||
|
end
|
||||||
|
|
||||||
|
zip_path = File.join manga_dir, "#{chapter_title}.cbz.part"
|
||||||
|
writer = Zip::Writer.new zip_path
|
||||||
|
rescue e
|
||||||
|
@queue.set_status Queue::JobStatus::Error, job
|
||||||
|
unless e.message.nil?
|
||||||
|
@queue.add_message e.message.not_nil!, job
|
||||||
|
end
|
||||||
|
@downloading = false
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
|
||||||
|
fail_count = 0
|
||||||
|
|
||||||
|
while page = plugin.next_page
|
||||||
|
fn = process_filename page["filename"].as_s
|
||||||
|
url = page["url"].as_s
|
||||||
|
headers = HTTP::Headers.new
|
||||||
|
|
||||||
|
if page["headers"]?
|
||||||
|
page["headers"].as_h.each do |k, v|
|
||||||
|
headers.add k, v.as_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
page_success = false
|
||||||
|
tries = 4
|
||||||
|
|
||||||
|
loop do
|
||||||
|
sleep plugin.info.wait_seconds.seconds
|
||||||
|
Logger.debug "downloading #{url}"
|
||||||
|
tries -= 1
|
||||||
|
|
||||||
|
begin
|
||||||
|
HTTP::Client.get url, headers do |res|
|
||||||
|
unless res.success?
|
||||||
|
raise "Failed to download page #{url}. " \
|
||||||
|
"[#{res.status_code}] #{res.status_message}"
|
||||||
|
end
|
||||||
|
writer.add fn, res.body_io
|
||||||
|
end
|
||||||
|
rescue e
|
||||||
|
@queue.add_fail job
|
||||||
|
fail_count += 1
|
||||||
|
msg = "Failed to download page #{url}. Error: #{e}"
|
||||||
|
@queue.add_message msg, job
|
||||||
|
Logger.error msg
|
||||||
|
Logger.debug "[failed] #{url}"
|
||||||
|
else
|
||||||
|
@queue.add_success job
|
||||||
|
Logger.debug "[success] #{url}"
|
||||||
|
page_success = true
|
||||||
|
end
|
||||||
|
|
||||||
|
break if page_success || tries < 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Logger.debug "Download completed. #{fail_count}/#{pages} failed"
|
||||||
|
writer.close
|
||||||
|
filename = File.join File.dirname(zip_path), File.basename(zip_path,
|
||||||
|
".part")
|
||||||
|
File.rename zip_path, filename
|
||||||
|
Logger.debug "cbz File created at #{filename}"
|
||||||
|
|
||||||
|
zip_exception = validate_archive filename
|
||||||
|
if !zip_exception.nil?
|
||||||
|
@queue.add_message "The downloaded archive is corrupted. " \
|
||||||
|
"Error: #{zip_exception}", job
|
||||||
|
@queue.set_status Queue::JobStatus::Error, job
|
||||||
|
elsif fail_count > 0
|
||||||
|
@queue.set_status Queue::JobStatus::MissingPages, job
|
||||||
|
else
|
||||||
|
@queue.set_status Queue::JobStatus::Completed, job
|
||||||
|
end
|
||||||
|
|
||||||
|
@downloading = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
require "duktape/runtime"
|
||||||
|
require "myhtml"
|
||||||
|
require "xml"
|
||||||
|
|
||||||
|
class Plugin
|
||||||
|
class Error < ::Exception
|
||||||
|
end
|
||||||
|
|
||||||
|
class MetadataError < Error
|
||||||
|
end
|
||||||
|
|
||||||
|
class PluginException < Error
|
||||||
|
end
|
||||||
|
|
||||||
|
class SyntaxError < Error
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Info
|
||||||
|
{% for name in ["id", "title", "placeholder"] %}
|
||||||
|
getter {{name.id}} = ""
|
||||||
|
{% end %}
|
||||||
|
getter wait_seconds : UInt64 = 0
|
||||||
|
getter dir : String
|
||||||
|
|
||||||
|
def initialize(@dir)
|
||||||
|
info_path = File.join @dir, "info.json"
|
||||||
|
|
||||||
|
unless File.exists? info_path
|
||||||
|
raise MetadataError.new "File `info.json` not found in the " \
|
||||||
|
"plugin directory #{dir}"
|
||||||
|
end
|
||||||
|
|
||||||
|
@json = JSON.parse File.read info_path
|
||||||
|
|
||||||
|
begin
|
||||||
|
{% for name in ["id", "title", "placeholder"] %}
|
||||||
|
@{{name.id}} = @json[{{name}}].as_s
|
||||||
|
{% end %}
|
||||||
|
@wait_seconds = @json["wait_seconds"].as_i.to_u64
|
||||||
|
|
||||||
|
unless @id.alphanumeric_underscore?
|
||||||
|
raise "Plugin ID can only contain alphanumeric characters and " \
|
||||||
|
"underscores"
|
||||||
|
end
|
||||||
|
rescue e
|
||||||
|
raise MetadataError.new "Failed to retrieve metadata from plugin " \
|
||||||
|
"at #{@dir}. Error: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def each(&block : String, JSON::Any -> _)
|
||||||
|
@json.as_h.each &block
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Storage
|
||||||
|
@hash = {} of String => String
|
||||||
|
|
||||||
|
def initialize(@path : String)
|
||||||
|
unless File.exists? @path
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
|
json = JSON.parse File.read @path
|
||||||
|
json.as_h.each do |k, v|
|
||||||
|
@hash[k] = v.as_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def []?(key)
|
||||||
|
@hash[key]?
|
||||||
|
end
|
||||||
|
|
||||||
|
def []=(key, val : String)
|
||||||
|
@hash[key] = val
|
||||||
|
end
|
||||||
|
|
||||||
|
def save
|
||||||
|
File.write @path, @hash.to_pretty_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@@info_ary = [] of Info
|
||||||
|
@info : Info?
|
||||||
|
|
||||||
|
getter js_path = ""
|
||||||
|
getter storage_path = ""
|
||||||
|
|
||||||
|
def self.build_info_ary
|
||||||
|
@@info_ary.clear
|
||||||
|
dir = Config.current.plugin_path
|
||||||
|
Dir.mkdir_p dir unless Dir.exists? dir
|
||||||
|
|
||||||
|
Dir.each_child dir do |f|
|
||||||
|
path = File.join dir, f
|
||||||
|
next unless File.directory? path
|
||||||
|
|
||||||
|
begin
|
||||||
|
@@info_ary << Info.new path
|
||||||
|
rescue e : MetadataError
|
||||||
|
Logger.warn e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.list
|
||||||
|
self.build_info_ary
|
||||||
|
@@info_ary.map do |m|
|
||||||
|
{id: m.id, title: m.title}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def info
|
||||||
|
@info.not_nil!
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(id : String)
|
||||||
|
Plugin.build_info_ary
|
||||||
|
|
||||||
|
@info = @@info_ary.find { |i| i.id == id }
|
||||||
|
if @info.nil?
|
||||||
|
raise Error.new "Plugin with ID #{id} not found"
|
||||||
|
end
|
||||||
|
|
||||||
|
@js_path = File.join info.dir, "index.js"
|
||||||
|
@storage_path = File.join info.dir, "storage.json"
|
||||||
|
|
||||||
|
unless File.exists? @js_path
|
||||||
|
raise Error.new "Plugin script not found at #{@js_path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
@rt = Duktape::Runtime.new do |sbx|
|
||||||
|
sbx.push_global_object
|
||||||
|
|
||||||
|
sbx.push_pointer @storage_path.as(Void*)
|
||||||
|
path = sbx.require_pointer(-1).as String
|
||||||
|
sbx.pop
|
||||||
|
sbx.push_string path
|
||||||
|
sbx.put_prop_string -2, "storage_path"
|
||||||
|
|
||||||
|
def_helper_functions sbx
|
||||||
|
end
|
||||||
|
|
||||||
|
eval File.read @js_path
|
||||||
|
end
|
||||||
|
|
||||||
|
macro check_fields(ary)
|
||||||
|
{% for field in ary %}
|
||||||
|
unless json[{{field}}]?
|
||||||
|
raise "Field `{{field.id}}` is missing from the function outputs"
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_chapters(query : String)
|
||||||
|
json = eval_json "listChapters('#{query}')"
|
||||||
|
begin
|
||||||
|
check_fields ["title", "chapters"]
|
||||||
|
|
||||||
|
ary = json["chapters"].as_a
|
||||||
|
ary.each do |obj|
|
||||||
|
id = obj["id"]?
|
||||||
|
raise "Field `id` missing from `listChapters` outputs" if id.nil?
|
||||||
|
|
||||||
|
unless id.to_s.alphanumeric_underscore?
|
||||||
|
raise "The `id` field can only contain alphanumeric characters " \
|
||||||
|
"and underscores"
|
||||||
|
end
|
||||||
|
|
||||||
|
title = obj["title"]?
|
||||||
|
raise "Field `title` missing from `listChapters` outputs" if title.nil?
|
||||||
|
end
|
||||||
|
rescue e
|
||||||
|
raise Error.new e.message
|
||||||
|
end
|
||||||
|
json
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_chapter(id : String)
|
||||||
|
json = eval_json "selectChapter('#{id}')"
|
||||||
|
begin
|
||||||
|
check_fields ["title", "pages"]
|
||||||
|
|
||||||
|
if json["title"].to_s.empty?
|
||||||
|
raise "The `title` field of the chapter can not be empty"
|
||||||
|
end
|
||||||
|
rescue e
|
||||||
|
raise Error.new e.message
|
||||||
|
end
|
||||||
|
json
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_page
|
||||||
|
json = eval_json "nextPage()"
|
||||||
|
return if json.size == 0
|
||||||
|
begin
|
||||||
|
check_fields ["filename", "url"]
|
||||||
|
rescue e
|
||||||
|
raise Error.new e.message
|
||||||
|
end
|
||||||
|
json
|
||||||
|
end
|
||||||
|
|
||||||
|
private def eval(str)
|
||||||
|
@rt.eval str
|
||||||
|
rescue e : Duktape::SyntaxError
|
||||||
|
raise SyntaxError.new e.message
|
||||||
|
rescue e : Duktape::Error
|
||||||
|
raise Error.new e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
private def eval_json(str)
|
||||||
|
JSON.parse eval(str).as String
|
||||||
|
end
|
||||||
|
|
||||||
|
private def def_helper_functions(sbx)
|
||||||
|
sbx.push_object
|
||||||
|
|
||||||
|
sbx.push_proc LibDUK::VARARGS do |ptr|
|
||||||
|
env = Duktape::Sandbox.new ptr
|
||||||
|
url = env.require_string 0
|
||||||
|
|
||||||
|
headers = HTTP::Headers.new
|
||||||
|
|
||||||
|
if env.get_top == 2
|
||||||
|
env.enum 1, LibDUK::Enum::OwnPropertiesOnly
|
||||||
|
while env.next -1, true
|
||||||
|
key = env.require_string -2
|
||||||
|
val = env.require_string -1
|
||||||
|
headers.add key, val
|
||||||
|
env.pop_2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
res = HTTP::Client.get url, headers
|
||||||
|
|
||||||
|
env.push_object
|
||||||
|
|
||||||
|
env.push_int res.status_code
|
||||||
|
env.put_prop_string -2, "status_code"
|
||||||
|
|
||||||
|
env.push_string res.body
|
||||||
|
env.put_prop_string -2, "body"
|
||||||
|
|
||||||
|
env.push_object
|
||||||
|
res.headers.each do |k, v|
|
||||||
|
if v.size == 1
|
||||||
|
env.push_string v[0]
|
||||||
|
else
|
||||||
|
env.push_string v.join ","
|
||||||
|
end
|
||||||
|
env.put_prop_string -2, k
|
||||||
|
end
|
||||||
|
env.put_prop_string -2, "headers"
|
||||||
|
|
||||||
|
env.call_success
|
||||||
|
end
|
||||||
|
sbx.put_prop_string -2, "get"
|
||||||
|
|
||||||
|
sbx.push_proc 2 do |ptr|
|
||||||
|
env = Duktape::Sandbox.new ptr
|
||||||
|
html = env.require_string 0
|
||||||
|
selector = env.require_string 1
|
||||||
|
|
||||||
|
myhtml = Myhtml::Parser.new html
|
||||||
|
ary = myhtml.css(selector).map(&.to_html).to_a
|
||||||
|
|
||||||
|
ary_idx = env.push_array
|
||||||
|
ary.each_with_index do |str, i|
|
||||||
|
env.push_string str
|
||||||
|
env.put_prop_index ary_idx, i.to_u32
|
||||||
|
end
|
||||||
|
|
||||||
|
env.call_success
|
||||||
|
end
|
||||||
|
sbx.put_prop_string -2, "css"
|
||||||
|
|
||||||
|
sbx.push_proc 1 do |ptr|
|
||||||
|
env = Duktape::Sandbox.new ptr
|
||||||
|
html = env.require_string 0
|
||||||
|
|
||||||
|
str = XML.parse(html).inner_text
|
||||||
|
|
||||||
|
env.push_string str
|
||||||
|
env.call_success
|
||||||
|
end
|
||||||
|
sbx.put_prop_string -2, "text"
|
||||||
|
|
||||||
|
sbx.push_proc 2 do |ptr|
|
||||||
|
env = Duktape::Sandbox.new ptr
|
||||||
|
html = env.require_string 0
|
||||||
|
name = env.require_string 1
|
||||||
|
|
||||||
|
begin
|
||||||
|
attr = XML.parse(html).first_element_child.not_nil![name]
|
||||||
|
env.push_string attr
|
||||||
|
rescue
|
||||||
|
env.push_undefined
|
||||||
|
end
|
||||||
|
|
||||||
|
env.call_success
|
||||||
|
end
|
||||||
|
sbx.put_prop_string -2, "attribute"
|
||||||
|
|
||||||
|
sbx.push_proc 1 do |ptr|
|
||||||
|
env = Duktape::Sandbox.new ptr
|
||||||
|
msg = env.require_string 0
|
||||||
|
env.call_success
|
||||||
|
|
||||||
|
raise PluginException.new msg
|
||||||
|
end
|
||||||
|
sbx.put_prop_string -2, "raise"
|
||||||
|
|
||||||
|
sbx.push_proc LibDUK::VARARGS do |ptr|
|
||||||
|
env = Duktape::Sandbox.new ptr
|
||||||
|
key = env.require_string 0
|
||||||
|
|
||||||
|
env.get_global_string "storage_path"
|
||||||
|
storage_path = env.require_string -1
|
||||||
|
env.pop
|
||||||
|
storage = Storage.new storage_path
|
||||||
|
|
||||||
|
if env.get_top == 2
|
||||||
|
val = env.require_string 1
|
||||||
|
storage[key] = val
|
||||||
|
storage.save
|
||||||
|
else
|
||||||
|
val = storage[key]?
|
||||||
|
if val
|
||||||
|
env.push_string val
|
||||||
|
else
|
||||||
|
env.push_undefined
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
env.call_success
|
||||||
|
end
|
||||||
|
sbx.put_prop_string -2, "storage"
|
||||||
|
|
||||||
|
sbx.put_prop_string -2, "mango"
|
||||||
|
end
|
||||||
|
end
|
||||||
+303
@@ -0,0 +1,303 @@
|
|||||||
|
require "sqlite3"
|
||||||
|
require "./util/*"
|
||||||
|
|
||||||
|
class Queue
|
||||||
|
abstract class Downloader
|
||||||
|
property stopped = false
|
||||||
|
@library_path : String = Config.current.library_path
|
||||||
|
@downloading = false
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@queue = Queue.default
|
||||||
|
@queue << self
|
||||||
|
|
||||||
|
spawn do
|
||||||
|
loop do
|
||||||
|
sleep 1.second
|
||||||
|
next if @stopped || @downloading
|
||||||
|
begin
|
||||||
|
job = pop
|
||||||
|
next if job.nil?
|
||||||
|
download job
|
||||||
|
rescue e
|
||||||
|
Logger.error e
|
||||||
|
@downloading = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
abstract def pop : Job?
|
||||||
|
private abstract def download(job : Job)
|
||||||
|
end
|
||||||
|
|
||||||
|
enum JobStatus
|
||||||
|
Pending # 0
|
||||||
|
Downloading # 1
|
||||||
|
Error # 2
|
||||||
|
Completed # 3
|
||||||
|
MissingPages # 4
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Job
|
||||||
|
property id : String
|
||||||
|
property manga_id : String
|
||||||
|
property title : String
|
||||||
|
property manga_title : String
|
||||||
|
property status : JobStatus
|
||||||
|
property status_message : String = ""
|
||||||
|
property pages : Int32 = 0
|
||||||
|
property success_count : Int32 = 0
|
||||||
|
property fail_count : Int32 = 0
|
||||||
|
property time : Time
|
||||||
|
property plugin_id : String?
|
||||||
|
property plugin_chapter_id : String?
|
||||||
|
|
||||||
|
def parse_query_result(res : DB::ResultSet)
|
||||||
|
@id = res.read String
|
||||||
|
@manga_id = res.read String
|
||||||
|
@title = res.read String
|
||||||
|
@manga_title = res.read String
|
||||||
|
status = res.read Int32
|
||||||
|
@status_message = res.read String
|
||||||
|
@pages = res.read Int32
|
||||||
|
@success_count = res.read Int32
|
||||||
|
@fail_count = res.read Int32
|
||||||
|
time = res.read Int64
|
||||||
|
@status = JobStatus.new status
|
||||||
|
@time = Time.unix_ms time
|
||||||
|
|
||||||
|
ary = @id.split("-")
|
||||||
|
if ary.size == 2
|
||||||
|
@plugin_id = ary[0]
|
||||||
|
@plugin_chapter_id = ary[1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Raises if the result set does not contain the correct set of columns
|
||||||
|
def self.from_query_result(res : DB::ResultSet)
|
||||||
|
job = Job.allocate
|
||||||
|
job.parse_query_result res
|
||||||
|
job
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(@id, @manga_id, @title, @manga_title, @status, @time,
|
||||||
|
@plugin_id = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(json)
|
||||||
|
json.object do
|
||||||
|
{% for name in ["id", "manga_id", "title", "manga_title",
|
||||||
|
"status_message"] %}
|
||||||
|
json.field {{name}}, @{{name.id}}
|
||||||
|
{% end %}
|
||||||
|
{% for name in ["pages", "success_count", "fail_count"] %}
|
||||||
|
json.field {{name}} do
|
||||||
|
json.number @{{name.id}}
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
json.field "status", @status.to_s
|
||||||
|
json.field "time" do
|
||||||
|
json.number @time.to_unix_ms
|
||||||
|
end
|
||||||
|
json.field "plugin_id", @plugin_id if @plugin_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
getter path : String
|
||||||
|
@downloaders = [] of Downloader
|
||||||
|
@paused = false
|
||||||
|
|
||||||
|
use_default
|
||||||
|
|
||||||
|
def initialize(db_path : String? = nil)
|
||||||
|
@path = db_path || Config.current.mangadex["download_queue_db_path"].to_s
|
||||||
|
dir = File.dirname @path
|
||||||
|
unless Dir.exists? dir
|
||||||
|
Logger.info "The queue DB directory #{dir} does not exist. " \
|
||||||
|
"Attepmting to create it"
|
||||||
|
Dir.mkdir_p dir
|
||||||
|
end
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
begin
|
||||||
|
db.exec "create table if not exists queue " \
|
||||||
|
"(id text, manga_id text, title text, manga_title " \
|
||||||
|
"text, status integer, status_message text, " \
|
||||||
|
"pages integer, success_count integer, " \
|
||||||
|
"fail_count integer, time integer)"
|
||||||
|
db.exec "create unique index if not exists id_idx " \
|
||||||
|
"on queue (id)"
|
||||||
|
db.exec "create index if not exists manga_id_idx " \
|
||||||
|
"on queue (manga_id)"
|
||||||
|
db.exec "create index if not exists status_idx " \
|
||||||
|
"on queue (status)"
|
||||||
|
rescue e
|
||||||
|
Logger.error "Error when checking tables in DB: #{e}"
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Push an array of jobs into the queue, and return the number of jobs
|
||||||
|
# inserted. Any job already exists in the queue will be ignored.
|
||||||
|
def push(jobs : Array(Job))
|
||||||
|
start_count = self.count
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
jobs.each do |job|
|
||||||
|
db.exec "insert or ignore into queue values " \
|
||||||
|
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
job.id, job.manga_id, job.title, job.manga_title,
|
||||||
|
job.status.to_i, job.status_message, job.pages,
|
||||||
|
job.success_count, job.fail_count, job.time.to_unix_ms
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.count - start_count
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset(id : String)
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
db.exec "update queue set status = 0, status_message = '', " \
|
||||||
|
"pages = 0, success_count = 0, fail_count = 0 " \
|
||||||
|
"where id = (?)", id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset(job : Job)
|
||||||
|
self.reset job.id
|
||||||
|
end
|
||||||
|
|
||||||
|
# Reset all failed tasks (missing pages and error)
|
||||||
|
def reset
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
db.exec "update queue set status = 0, status_message = '', " \
|
||||||
|
"pages = 0, success_count = 0, fail_count = 0 " \
|
||||||
|
"where status = 2 or status = 4"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(id : String)
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
db.exec "delete from queue where id = (?)", id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(job : Job)
|
||||||
|
self.delete job.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_status(status : JobStatus)
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
db.exec "delete from queue where status = (?)", status.to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def count_status(status : JobStatus)
|
||||||
|
num = 0
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
num = db.query_one "select count(*) from queue where " \
|
||||||
|
"status = (?)", status.to_i, as: Int32
|
||||||
|
end
|
||||||
|
end
|
||||||
|
num
|
||||||
|
end
|
||||||
|
|
||||||
|
def count
|
||||||
|
num = 0
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
num = db.query_one "select count(*) from queue", as: Int32
|
||||||
|
end
|
||||||
|
end
|
||||||
|
num
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_status(status : JobStatus, job : Job)
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
db.exec "update queue set status = (?) where id = (?)",
|
||||||
|
status.to_i, job.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_all
|
||||||
|
jobs = [] of Job
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
jobs = db.query_all "select * from queue order by time" do |rs|
|
||||||
|
Job.from_query_result rs
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
jobs
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_success(job : Job)
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
db.exec "update queue set success_count = success_count + 1 " \
|
||||||
|
"where id = (?)", job.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_fail(job : Job)
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
db.exec "update queue set fail_count = fail_count + 1 " \
|
||||||
|
"where id = (?)", job.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_pages(pages : Int32, job : Job)
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
db.exec "update queue set pages = (?), success_count = 0, " \
|
||||||
|
"fail_count = 0 where id = (?)", pages, job.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_message(msg : String, job : Job)
|
||||||
|
MainFiber.run do
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
db.exec "update queue set status_message = " \
|
||||||
|
"status_message || (?) || (?) where id = (?)",
|
||||||
|
"\n", msg, job.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def <<(downloader : Downloader)
|
||||||
|
@downloaders << downloader
|
||||||
|
end
|
||||||
|
|
||||||
|
def pause
|
||||||
|
@downloaders.each { |d| d.stopped = true }
|
||||||
|
@paused = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def resume
|
||||||
|
@downloaders.each { |d| d.stopped = false }
|
||||||
|
@paused = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def paused?
|
||||||
|
@paused
|
||||||
|
end
|
||||||
|
end
|
||||||
+99
-3
@@ -97,6 +97,28 @@ class APIRouter < Router
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
post "/api/bulk-progress/:action/:title" do |env|
|
||||||
|
begin
|
||||||
|
username = get_username env
|
||||||
|
title = (@context.library.get_title env.params.url["title"]).not_nil!
|
||||||
|
action = env.params.url["action"]
|
||||||
|
ids = env.params.json["ids"].as(Array).map &.as_s
|
||||||
|
|
||||||
|
unless action.in? ["read", "unread"]
|
||||||
|
raise "Unknow action #{action}"
|
||||||
|
end
|
||||||
|
title.bulk_progress action, ids, username
|
||||||
|
rescue e
|
||||||
|
@context.error e
|
||||||
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message,
|
||||||
|
}.to_json
|
||||||
|
else
|
||||||
|
send_json env, {"success" => true}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
post "/api/admin/display_name/:title/:name" do |env|
|
post "/api/admin/display_name/:title/:name" do |env|
|
||||||
begin
|
begin
|
||||||
title = (@context.library.get_title env.params.url["title"])
|
title = (@context.library.get_title env.params.url["title"])
|
||||||
@@ -136,12 +158,12 @@ class APIRouter < Router
|
|||||||
begin
|
begin
|
||||||
chapters = env.params.json["chapters"].as(Array).map { |c| c.as_h }
|
chapters = env.params.json["chapters"].as(Array).map { |c| c.as_h }
|
||||||
jobs = chapters.map { |chapter|
|
jobs = chapters.map { |chapter|
|
||||||
MangaDex::Job.new(
|
Queue::Job.new(
|
||||||
chapter["id"].as_s,
|
chapter["id"].as_s,
|
||||||
chapter["manga_id"].as_s,
|
chapter["manga_id"].as_s,
|
||||||
chapter["full_title"].as_s,
|
chapter["full_title"].as_s,
|
||||||
chapter["manga_title"].as_s,
|
chapter["manga_title"].as_s,
|
||||||
MangaDex::JobStatus::Pending,
|
Queue::JobStatus::Pending,
|
||||||
Time.unix chapter["time"].as_s.to_i
|
Time.unix chapter["time"].as_s.to_i
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -179,7 +201,7 @@ class APIRouter < Router
|
|||||||
case action
|
case action
|
||||||
when "delete"
|
when "delete"
|
||||||
if id.nil?
|
if id.nil?
|
||||||
@context.queue.delete_status MangaDex::JobStatus::Completed
|
@context.queue.delete_status Queue::JobStatus::Completed
|
||||||
else
|
else
|
||||||
@context.queue.delete id
|
@context.queue.delete id
|
||||||
end
|
end
|
||||||
@@ -259,5 +281,79 @@ class APIRouter < Router
|
|||||||
}.to_json
|
}.to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
post "/api/admin/plugin/list" do |env|
|
||||||
|
begin
|
||||||
|
query = env.params.json["query"].as String
|
||||||
|
plugin = Plugin.new env.params.json["plugin"].as String
|
||||||
|
|
||||||
|
json = plugin.list_chapters query
|
||||||
|
chapters = json["chapters"]
|
||||||
|
title = json["title"]
|
||||||
|
|
||||||
|
send_json env, {
|
||||||
|
"success" => true,
|
||||||
|
"chapters" => chapters,
|
||||||
|
"title" => title,
|
||||||
|
}.to_json
|
||||||
|
rescue e
|
||||||
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/api/admin/plugin/download" do |env|
|
||||||
|
begin
|
||||||
|
plugin = Plugin.new env.params.json["plugin"].as String
|
||||||
|
chapters = env.params.json["chapters"].as Array(JSON::Any)
|
||||||
|
manga_title = env.params.json["title"].as String
|
||||||
|
|
||||||
|
jobs = chapters.map { |ch|
|
||||||
|
Queue::Job.new(
|
||||||
|
"#{plugin.info.id}-#{ch["id"]}",
|
||||||
|
"", # manga_id
|
||||||
|
ch["title"].as_s,
|
||||||
|
manga_title,
|
||||||
|
Queue::JobStatus::Pending,
|
||||||
|
Time.utc
|
||||||
|
)
|
||||||
|
}
|
||||||
|
inserted_count = @context.queue.push jobs
|
||||||
|
send_json env, {
|
||||||
|
"success": inserted_count,
|
||||||
|
"fail": jobs.size - inserted_count,
|
||||||
|
}.to_json
|
||||||
|
rescue e
|
||||||
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/api/dimensions/:tid/:eid" do |env|
|
||||||
|
begin
|
||||||
|
tid = env.params.url["tid"]
|
||||||
|
eid = env.params.url["eid"]
|
||||||
|
|
||||||
|
title = @context.library.get_title tid
|
||||||
|
raise "Title ID `#{tid}` not found" if title.nil?
|
||||||
|
entry = title.get_entry eid
|
||||||
|
raise "Entry ID `#{eid}` of `#{title.title}` not found" if entry.nil?
|
||||||
|
|
||||||
|
sizes = entry.page_dimensions
|
||||||
|
send_json env, {
|
||||||
|
"success" => true,
|
||||||
|
"dimensions" => sizes,
|
||||||
|
}.to_json
|
||||||
|
rescue e
|
||||||
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -78,12 +78,32 @@ class MainRouter < Router
|
|||||||
layout "download"
|
layout "download"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get "/download/plugins" do |env|
|
||||||
|
begin
|
||||||
|
id = env.params.query["plugin"]?
|
||||||
|
plugins = Plugin.list
|
||||||
|
plugin = nil
|
||||||
|
|
||||||
|
if id
|
||||||
|
plugin = Plugin.new id
|
||||||
|
elsif !plugins.empty?
|
||||||
|
plugin = Plugin.new plugins[0][:id]
|
||||||
|
end
|
||||||
|
|
||||||
|
layout "plugin-download"
|
||||||
|
rescue e
|
||||||
|
@context.error e
|
||||||
|
env.response.status_code = 500
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
get "/" do |env|
|
get "/" do |env|
|
||||||
begin
|
begin
|
||||||
username = get_username env
|
username = get_username env
|
||||||
continue_reading = @context
|
continue_reading = @context
|
||||||
.library.get_continue_reading_entries username
|
.library.get_continue_reading_entries username
|
||||||
recently_added = @context.library.get_recently_added_entries username
|
recently_added = @context.library.get_recently_added_entries username
|
||||||
|
start_reading = @context.library.get_start_reading_titles username
|
||||||
titles = @context.library.titles
|
titles = @context.library.titles
|
||||||
new_user = !titles.any? { |t| t.load_percentage(username) > 0 }
|
new_user = !titles.any? { |t| t.load_percentage(username) > 0 }
|
||||||
empty_library = titles.size == 0
|
empty_library = titles.size == 0
|
||||||
|
|||||||
+8
-21
@@ -4,16 +4,15 @@ class ReaderRouter < Router
|
|||||||
def initialize
|
def initialize
|
||||||
get "/reader/:title/:entry" do |env|
|
get "/reader/:title/:entry" do |env|
|
||||||
begin
|
begin
|
||||||
|
username = get_username env
|
||||||
|
|
||||||
title = (@context.library.get_title env.params.url["title"]).not_nil!
|
title = (@context.library.get_title env.params.url["title"]).not_nil!
|
||||||
entry = (title.get_entry env.params.url["entry"]).not_nil!
|
entry = (title.get_entry env.params.url["entry"]).not_nil!
|
||||||
|
|
||||||
|
next layout "reader-error" if entry.err_msg
|
||||||
|
|
||||||
# load progress
|
# load progress
|
||||||
username = get_username env
|
|
||||||
page = entry.load_progress username
|
page = entry.load_progress username
|
||||||
# we go back 2 * `IMGS_PER_PAGE` pages. the infinite scroll
|
|
||||||
# library perloads a few pages in advance, and the user
|
|
||||||
# might not have actually read them
|
|
||||||
page = [page - 2 * IMGS_PER_PAGE, 1].max
|
|
||||||
|
|
||||||
# start from page 1 if the user has finished reading the entry
|
# start from page 1 if the user has finished reading the entry
|
||||||
page = 1 if entry.finished? username
|
page = 1 if entry.finished? username
|
||||||
@@ -29,29 +28,17 @@ class ReaderRouter < Router
|
|||||||
begin
|
begin
|
||||||
base_url = Config.current.base_url
|
base_url = Config.current.base_url
|
||||||
|
|
||||||
|
username = get_username env
|
||||||
|
|
||||||
title = (@context.library.get_title env.params.url["title"]).not_nil!
|
title = (@context.library.get_title env.params.url["title"]).not_nil!
|
||||||
entry = (title.get_entry env.params.url["entry"]).not_nil!
|
entry = (title.get_entry env.params.url["entry"]).not_nil!
|
||||||
page = env.params.url["page"].to_i
|
page = env.params.url["page"].to_i
|
||||||
raise "" if page > entry.pages || page <= 0
|
raise "" if page > entry.pages || page <= 0
|
||||||
|
|
||||||
# save progress
|
|
||||||
username = get_username env
|
|
||||||
entry.save_progress username, page
|
|
||||||
|
|
||||||
pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min)
|
|
||||||
urls = pages.map { |idx|
|
|
||||||
"#{base_url}api/page/#{title.id}/#{entry.id}/#{idx}"
|
|
||||||
}
|
|
||||||
reader_urls = pages.map { |idx|
|
|
||||||
"#{base_url}reader/#{title.id}/#{entry.id}/#{idx}"
|
|
||||||
}
|
|
||||||
next_page = page + IMGS_PER_PAGE
|
|
||||||
next_url = next_entry_url = nil
|
|
||||||
exit_url = "#{base_url}book/#{title.id}"
|
exit_url = "#{base_url}book/#{title.id}"
|
||||||
|
|
||||||
|
next_entry_url = nil
|
||||||
next_entry = entry.next_entry username
|
next_entry = entry.next_entry username
|
||||||
unless next_page > entry.pages
|
|
||||||
next_url = "#{base_url}reader/#{title.id}/#{entry.id}/#{next_page}"
|
|
||||||
end
|
|
||||||
unless next_entry.nil?
|
unless next_entry.nil?
|
||||||
next_entry_url = "#{base_url}reader/#{title.id}/#{next_entry.id}"
|
next_entry_url = "#{base_url}reader/#{title.id}/#{next_entry.id}"
|
||||||
end
|
end
|
||||||
|
|||||||
+3
-8
@@ -8,19 +8,14 @@ require "./routes/*"
|
|||||||
class Context
|
class Context
|
||||||
property library : Library
|
property library : Library
|
||||||
property storage : Storage
|
property storage : Storage
|
||||||
property queue : MangaDex::Queue
|
property queue : Queue
|
||||||
|
|
||||||
def self.default : self
|
use_default
|
||||||
unless @@default
|
|
||||||
@@default = new
|
|
||||||
end
|
|
||||||
@@default.not_nil!
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@storage = Storage.default
|
@storage = Storage.default
|
||||||
@library = Library.default
|
@library = Library.default
|
||||||
@queue = MangaDex::Queue.default
|
@queue = Queue.default
|
||||||
end
|
end
|
||||||
|
|
||||||
{% for lvl in Logger::LEVELS %}
|
{% for lvl in Logger::LEVELS %}
|
||||||
|
|||||||
+123
-100
@@ -21,12 +21,7 @@ class Storage
|
|||||||
id: String,
|
id: String,
|
||||||
is_title: Bool)
|
is_title: Bool)
|
||||||
|
|
||||||
def self.default : self
|
use_default
|
||||||
unless @@default
|
|
||||||
@@default = new
|
|
||||||
end
|
|
||||||
@@default.not_nil!
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(db_path : String? = nil, init_user = true, *,
|
def initialize(db_path : String? = nil, init_user = true, *,
|
||||||
@auto_close = true)
|
@auto_close = true)
|
||||||
@@ -37,38 +32,40 @@ class Storage
|
|||||||
"Attepmting to create it"
|
"Attepmting to create it"
|
||||||
Dir.mkdir_p dir
|
Dir.mkdir_p dir
|
||||||
end
|
end
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
MainFiber.run do
|
||||||
begin
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
# We create the `ids` table first. even if the uses has an
|
begin
|
||||||
# early version installed and has the `user` table only,
|
# We create the `ids` table first. even if the uses has an
|
||||||
# we will still be able to create `ids`
|
# early version installed and has the `user` table only,
|
||||||
db.exec "create table ids" \
|
# we will still be able to create `ids`
|
||||||
"(path text, id text, is_title integer)"
|
db.exec "create table ids" \
|
||||||
db.exec "create unique index path_idx on ids (path)"
|
"(path text, id text, is_title integer)"
|
||||||
db.exec "create unique index id_idx on ids (id)"
|
db.exec "create unique index path_idx on ids (path)"
|
||||||
|
db.exec "create unique index id_idx on ids (id)"
|
||||||
|
|
||||||
db.exec "create table users" \
|
db.exec "create table users" \
|
||||||
"(username text, password text, token text, admin integer)"
|
"(username text, password text, token text, admin integer)"
|
||||||
rescue e
|
rescue e
|
||||||
unless e.message.not_nil!.ends_with? "already exists"
|
unless e.message.not_nil!.ends_with? "already exists"
|
||||||
Logger.fatal "Error when checking tables in DB: #{e}"
|
Logger.fatal "Error when checking tables in DB: #{e}"
|
||||||
raise e
|
raise e
|
||||||
|
end
|
||||||
|
|
||||||
|
# If the DB is initialized through CLI but no user is added, we need
|
||||||
|
# to create the admin user when first starting the app
|
||||||
|
user_count = db.query_one "select count(*) from users", as: Int32
|
||||||
|
init_admin if init_user && user_count == 0
|
||||||
|
else
|
||||||
|
Logger.debug "Creating DB file at #{@path}"
|
||||||
|
db.exec "create unique index username_idx on users (username)"
|
||||||
|
db.exec "create unique index token_idx on users (token)"
|
||||||
|
|
||||||
|
init_admin if init_user
|
||||||
end
|
end
|
||||||
|
|
||||||
# If the DB is initialized through CLI but no user is added, we need
|
|
||||||
# to create the admin user when first starting the app
|
|
||||||
user_count = db.query_one "select count(*) from users", as: Int32
|
|
||||||
init_admin if init_user && user_count == 0
|
|
||||||
else
|
|
||||||
Logger.debug "Creating DB file at #{@path}"
|
|
||||||
db.exec "create unique index username_idx on users (username)"
|
|
||||||
db.exec "create unique index token_idx on users (token)"
|
|
||||||
|
|
||||||
init_admin if init_user
|
|
||||||
end
|
end
|
||||||
end
|
unless @auto_close
|
||||||
unless @auto_close
|
@db = DB.open "sqlite3://#{@path}"
|
||||||
@db = DB.open "sqlite3://#{@path}"
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -92,37 +89,45 @@ class Storage
|
|||||||
end
|
end
|
||||||
|
|
||||||
def verify_user(username, password)
|
def verify_user(username, password)
|
||||||
get_db do |db|
|
out_token = nil
|
||||||
begin
|
MainFiber.run do
|
||||||
hash, token = db.query_one "select password, token from " \
|
get_db do |db|
|
||||||
"users where username = (?)",
|
begin
|
||||||
username, as: {String, String?}
|
hash, token = db.query_one "select password, token from " \
|
||||||
unless verify_password hash, password
|
"users where username = (?)",
|
||||||
Logger.debug "Password does not match the hash"
|
username, as: {String, String?}
|
||||||
return nil
|
unless verify_password hash, password
|
||||||
|
Logger.debug "Password does not match the hash"
|
||||||
|
next
|
||||||
|
end
|
||||||
|
Logger.debug "User #{username} verified"
|
||||||
|
if token
|
||||||
|
out_token = token
|
||||||
|
next
|
||||||
|
end
|
||||||
|
token = random_str
|
||||||
|
Logger.debug "Updating token for #{username}"
|
||||||
|
db.exec "update users set token = (?) where username = (?)",
|
||||||
|
token, username
|
||||||
|
out_token = token
|
||||||
|
rescue e
|
||||||
|
Logger.error "Error when verifying user #{username}: #{e}"
|
||||||
end
|
end
|
||||||
Logger.debug "User #{username} verified"
|
|
||||||
return token if token
|
|
||||||
token = random_str
|
|
||||||
Logger.debug "Updating token for #{username}"
|
|
||||||
db.exec "update users set token = (?) where username = (?)",
|
|
||||||
token, username
|
|
||||||
return token
|
|
||||||
rescue e
|
|
||||||
Logger.error "Error when verifying user #{username}: #{e}"
|
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
out_token
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_token(token)
|
def verify_token(token)
|
||||||
username = nil
|
username = nil
|
||||||
get_db do |db|
|
MainFiber.run do
|
||||||
begin
|
get_db do |db|
|
||||||
username = db.query_one "select username from users where " \
|
begin
|
||||||
"token = (?)", token, as: String
|
username = db.query_one "select username from users where " \
|
||||||
rescue e
|
"token = (?)", token, as: String
|
||||||
Logger.debug "Unable to verify token"
|
rescue e
|
||||||
|
Logger.debug "Unable to verify token"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
username
|
username
|
||||||
@@ -130,12 +135,14 @@ class Storage
|
|||||||
|
|
||||||
def verify_admin(token)
|
def verify_admin(token)
|
||||||
is_admin = false
|
is_admin = false
|
||||||
get_db do |db|
|
MainFiber.run do
|
||||||
begin
|
get_db do |db|
|
||||||
is_admin = db.query_one "select admin from users where " \
|
begin
|
||||||
"token = (?)", token, as: Bool
|
is_admin = db.query_one "select admin from users where " \
|
||||||
rescue e
|
"token = (?)", token, as: Bool
|
||||||
Logger.debug "Unable to verify user as admin"
|
rescue e
|
||||||
|
Logger.debug "Unable to verify user as admin"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
is_admin
|
is_admin
|
||||||
@@ -143,10 +150,12 @@ class Storage
|
|||||||
|
|
||||||
def list_users
|
def list_users
|
||||||
results = Array(Tuple(String, Bool)).new
|
results = Array(Tuple(String, Bool)).new
|
||||||
get_db do |db|
|
MainFiber.run do
|
||||||
db.query "select username, admin from users" do |rs|
|
get_db do |db|
|
||||||
rs.each do
|
db.query "select username, admin from users" do |rs|
|
||||||
results << {rs.read(String), rs.read(Bool)}
|
rs.each do
|
||||||
|
results << {rs.read(String), rs.read(Bool)}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -157,10 +166,12 @@ class Storage
|
|||||||
validate_username username
|
validate_username username
|
||||||
validate_password password
|
validate_password password
|
||||||
admin = (admin ? 1 : 0)
|
admin = (admin ? 1 : 0)
|
||||||
get_db do |db|
|
MainFiber.run do
|
||||||
hash = hash_password password
|
get_db do |db|
|
||||||
db.exec "insert into users values (?, ?, ?, ?)",
|
hash = hash_password password
|
||||||
username, hash, nil, admin
|
db.exec "insert into users values (?, ?, ?, ?)",
|
||||||
|
username, hash, nil, admin
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -168,40 +179,48 @@ class Storage
|
|||||||
admin = (admin ? 1 : 0)
|
admin = (admin ? 1 : 0)
|
||||||
validate_username username
|
validate_username username
|
||||||
validate_password password unless password.empty?
|
validate_password password unless password.empty?
|
||||||
get_db do |db|
|
MainFiber.run do
|
||||||
if password.empty?
|
get_db do |db|
|
||||||
db.exec "update users set username = (?), admin = (?) " \
|
if password.empty?
|
||||||
"where username = (?)",
|
db.exec "update users set username = (?), admin = (?) " \
|
||||||
username, admin, original_username
|
"where username = (?)",
|
||||||
else
|
username, admin, original_username
|
||||||
hash = hash_password password
|
else
|
||||||
db.exec "update users set username = (?), admin = (?)," \
|
hash = hash_password password
|
||||||
"password = (?) where username = (?)",
|
db.exec "update users set username = (?), admin = (?)," \
|
||||||
username, admin, hash, original_username
|
"password = (?) where username = (?)",
|
||||||
|
username, admin, hash, original_username
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_user(username)
|
def delete_user(username)
|
||||||
get_db do |db|
|
MainFiber.run do
|
||||||
db.exec "delete from users where username = (?)", username
|
get_db do |db|
|
||||||
|
db.exec "delete from users where username = (?)", username
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def logout(token)
|
def logout(token)
|
||||||
get_db do |db|
|
MainFiber.run do
|
||||||
begin
|
get_db do |db|
|
||||||
db.exec "update users set token = (?) where token = (?)", nil, token
|
begin
|
||||||
rescue
|
db.exec "update users set token = (?) where token = (?)", nil, token
|
||||||
|
rescue
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_id(path, is_title)
|
def get_id(path, is_title)
|
||||||
id = nil
|
id = nil
|
||||||
get_db do |db|
|
MainFiber.run do
|
||||||
id = db.query_one? "select id from ids where path = (?)", path,
|
get_db do |db|
|
||||||
as: {String}
|
id = db.query_one? "select id from ids where path = (?)", path,
|
||||||
|
as: {String}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
id
|
id
|
||||||
end
|
end
|
||||||
@@ -211,20 +230,24 @@ class Storage
|
|||||||
end
|
end
|
||||||
|
|
||||||
def bulk_insert_ids
|
def bulk_insert_ids
|
||||||
get_db do |db|
|
MainFiber.run do
|
||||||
db.transaction do |tx|
|
get_db do |db|
|
||||||
@insert_ids.each do |tp|
|
db.transaction do |tx|
|
||||||
tx.connection.exec "insert into ids values (?, ?, ?)", tp[:path],
|
@insert_ids.each do |tp|
|
||||||
tp[:id], tp[:is_title] ? 1 : 0
|
tx.connection.exec "insert into ids values (?, ?, ?)", tp[:path],
|
||||||
|
tp[:id], tp[:is_title] ? 1 : 0
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@insert_ids.clear
|
||||||
end
|
end
|
||||||
@insert_ids.clear
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
unless @db.nil?
|
MainFiber.run do
|
||||||
@db.not_nil!.close
|
unless @db.nil?
|
||||||
|
@db.not_nil!.close
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
require "http_proxy"
|
||||||
|
|
||||||
|
# Monkey-patch `HTTP::Client` to make it respect the `*_PROXY`
|
||||||
|
# environment variables
|
||||||
|
module HTTP
|
||||||
|
class Client
|
||||||
|
private def self.exec(uri : URI, tls : TLSContext = nil)
|
||||||
|
Logger.debug "Using monkey-patched HTTP::Client"
|
||||||
|
previous_def uri, tls do |client, path|
|
||||||
|
client.set_proxy get_proxy uri
|
||||||
|
yield client, path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def get_proxy(uri : URI) : HTTP::Proxy::Client?
|
||||||
|
no_proxy = ENV["no_proxy"]? || ENV["NO_PROXY"]?
|
||||||
|
return if no_proxy &&
|
||||||
|
no_proxy.split(",").any? &.== uri.hostname
|
||||||
|
|
||||||
|
case uri.scheme
|
||||||
|
when "http"
|
||||||
|
env_to_proxy "http_proxy"
|
||||||
|
when "https"
|
||||||
|
env_to_proxy "https_proxy"
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def env_to_proxy(key : String) : HTTP::Proxy::Client?
|
||||||
|
val = ENV[key.downcase]? || ENV[key.upcase]?
|
||||||
|
return if val.nil?
|
||||||
|
|
||||||
|
begin
|
||||||
|
uri = URI.parse val
|
||||||
|
HTTP::Proxy::Client.new uri.hostname.not_nil!, uri.port.not_nil!
|
||||||
|
rescue
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
+19
-3
@@ -1,6 +1,7 @@
|
|||||||
IMGS_PER_PAGE = 5
|
IMGS_PER_PAGE = 5
|
||||||
UPLOAD_URL_PREFIX = "/uploads"
|
ENTRIES_IN_HOME_SECTIONS = 8
|
||||||
STATIC_DIRS = ["/css", "/js", "/img", "/favicon.ico"]
|
UPLOAD_URL_PREFIX = "/uploads"
|
||||||
|
STATIC_DIRS = ["/css", "/js", "/img", "/favicon.ico"]
|
||||||
|
|
||||||
def random_str
|
def random_str
|
||||||
UUID.random.to_s.gsub "-", ""
|
UUID.random.to_s.gsub "-", ""
|
||||||
@@ -45,3 +46,18 @@ struct Nil
|
|||||||
other
|
other
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
macro use_default
|
||||||
|
def self.default : self
|
||||||
|
unless @@default
|
||||||
|
@@default = new
|
||||||
|
end
|
||||||
|
@@default.not_nil!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class String
|
||||||
|
def alphanumeric_underscore?
|
||||||
|
self.chars.all? { |c| c.alphanumeric? || c == '_' }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ def validate_username(username)
|
|||||||
if username.size < 3
|
if username.size < 3
|
||||||
raise "Username should contain at least 3 characters"
|
raise "Username should contain at least 3 characters"
|
||||||
end
|
end
|
||||||
if (username =~ /^[A-Za-z0-9_]+$/).nil?
|
if (username =~ /^[a-zA-Z_][a-zA-Z0-9_\-]*$/).nil?
|
||||||
raise "Username should contain alphanumeric characters " \
|
raise "Username can only contain alphanumeric characters, " \
|
||||||
"and underscores only"
|
"underscores, and hyphens"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,14 @@
|
|||||||
<span hidden></span>
|
<span hidden></span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li data-url="<%= base_url %>admin/downloads">Download Manager</li>
|
<li class="nopointer">
|
||||||
|
<span>Theme</span>
|
||||||
|
<select id="theme-select" class="uk-select uk-align-right uk-width-1-3@m uk-width-1-2">
|
||||||
|
<option>Dark</option>
|
||||||
|
<option>Light</option>
|
||||||
|
<option>System</option>
|
||||||
|
</select>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<hr class="uk-divider-icon">
|
<hr class="uk-divider-icon">
|
||||||
|
|||||||
@@ -15,29 +15,44 @@
|
|||||||
<% end %>>
|
<% end %>>
|
||||||
|
|
||||||
<div class="acard
|
<div class="acard
|
||||||
<% if item.is_a? Entry %>
|
<% if item.is_a? Entry && item.err_msg.nil? %>
|
||||||
<%= "is_entry" %>
|
<%= "is_entry" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
"
|
"
|
||||||
<% if item.is_a? Entry %>
|
<% if item.is_a? Entry %>
|
||||||
data-encoded-path="<%= item.encoded_path %>"
|
<% if item.err_msg %>
|
||||||
data-pages="<%= item.pages %>"
|
onclick="location='<%= base_url %>reader/<%= item.book.id %>/<%= item.id %>'"
|
||||||
data-progress="<%= (progress * 100).round(1) %>"
|
<% else %>
|
||||||
data-encoded-book-title="<%= item.book.encoded_display_name %>"
|
data-encoded-path="<%= item.encoded_path %>"
|
||||||
data-encoded-title="<%= item.encoded_display_name %>"
|
data-pages="<%= item.pages %>"
|
||||||
data-book-id="<%= item.book.id %>"
|
data-progress="<%= (progress * 100).round(1) %>"
|
||||||
data-id="<%= item.id %>"
|
data-encoded-book-title="<%= item.book.encoded_display_name %>"
|
||||||
|
data-encoded-title="<%= item.encoded_display_name %>"
|
||||||
|
data-book-id="<%= item.book.id %>"
|
||||||
|
data-id="<%= item.id %>"
|
||||||
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
onclick="location='<%= base_url %>book/<%= item.id %>'"
|
onclick="location='<%= base_url %>book/<%= item.id %>'"
|
||||||
<% end %>>
|
<% end %>>
|
||||||
|
|
||||||
<div class="uk-card uk-card-default">
|
<div class="uk-card uk-card-default" x-data="{selected: false, hover: false, disabled: true}" :class="{selected: selected}"
|
||||||
<div class="uk-card-media-top">
|
<% if page == "title" && item.is_a?(Entry) && item.err_msg.nil? %>
|
||||||
<img data-src="<%= item.cover_url %>" data-width data-height alt="" uk-img>
|
x-init="disabled = false"
|
||||||
|
<% end %>>
|
||||||
|
<div class="uk-card-media-top uk-inline" @mouseenter="hover = true" @mouseleave="hover = false">
|
||||||
|
<img data-src="<%= item.cover_url %>" width="100%" height="100%" alt="" uk-img
|
||||||
|
<% if item.is_a? Entry && item.err_msg %>
|
||||||
|
class="grayscale"
|
||||||
|
<% end %>>
|
||||||
|
<div class="uk-overlay-primary uk-position-cover" x-show="!disabled && (selected || hover)">
|
||||||
|
<div class="uk-position-center">
|
||||||
|
<span class="fas fa-check-circle fa-3x" @click.stop="selected = !selected; $dispatch(selected ? 'add' : 'remove')" :style="`color:${selected && 'orange'};`"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="uk-card-body">
|
<div class="uk-card-body">
|
||||||
<% unless progress < 0 || progress > 100 %>
|
<% unless progress < 0 || progress > 100 || progress.nan? %>
|
||||||
<div class="uk-card-badge label"><%= (progress * 100).round(1) %>%</div>
|
<div class="uk-card-badge label"><%= (progress * 100).round(1) %>%</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
@@ -51,7 +66,12 @@
|
|||||||
<a class="uk-card-title break-word uk-margin-remove-top uk-text-meta uk-display-inline-block no-modal" data-title="<%= HTML.escape(item.book.display_name) %>" href="<%= base_url %>book/<%= item.book.id %>"><%= HTML.escape(item.book.display_name) %></a>
|
<a class="uk-card-title break-word uk-margin-remove-top uk-text-meta uk-display-inline-block no-modal" data-title="<%= HTML.escape(item.book.display_name) %>" href="<%= base_url %>book/<%= item.book.id %>"><%= HTML.escape(item.book.display_name) %></a>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if item.is_a? Entry %>
|
<% if item.is_a? Entry %>
|
||||||
<p class="uk-text-meta"><%= item.pages %> pages</p>
|
<% if item.err_msg %>
|
||||||
|
<p class="uk-text-meta uk-margin-remove-bottom">Error <span uk-icon="info"></span></p>
|
||||||
|
<div uk-dropdown><%= item.err_msg %></div>
|
||||||
|
<% else %>
|
||||||
|
<p class="uk-text-meta"><%= item.pages %> pages</p>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if item.is_a? Title %>
|
<% if item.is_a? Title %>
|
||||||
<% if grouped_count == 1 %>
|
<% if grouped_count == 1 %>
|
||||||
|
|||||||
@@ -10,5 +10,6 @@
|
|||||||
<script defer src="<%= base_url %>js/fontawesome.min.js"></script>
|
<script defer src="<%= base_url %>js/fontawesome.min.js"></script>
|
||||||
<script defer src="<%= base_url %>js/solid.min.js"></script>
|
<script defer src="<%= base_url %>js/solid.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.5.0/dist/alpine.min.js" defer></script>
|
||||||
<script src="<%= base_url %>js/theme.js"></script>
|
<script src="<%= base_url %>js/theme.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<th>Progress</th>
|
<th>Progress</th>
|
||||||
<th>Time</th>
|
<th>Time</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Plugin</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
+13
-2
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
<%- unless continue_reading.empty? -%>
|
<%- unless continue_reading.empty? -%>
|
||||||
<h2 class="uk-title home-headings">Continue Reading</h2>
|
<h2 class="uk-title home-headings">Continue Reading</h2>
|
||||||
<div id="item-container-continue" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
<%- continue_reading.each do |cr| -%>
|
<%- continue_reading.each do |cr| -%>
|
||||||
<% item = cr[:entry] %>
|
<% item = cr[:entry] %>
|
||||||
<% progress = cr[:percentage] %>
|
<% progress = cr[:percentage] %>
|
||||||
@@ -50,9 +50,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
|
|
||||||
|
<%- unless start_reading.empty? -%>
|
||||||
|
<h2 class="uk-title home-headings">Start Reading</h2>
|
||||||
|
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
|
<%- start_reading.each do |t| -%>
|
||||||
|
<% item = t %>
|
||||||
|
<% progress = 0.0 %>
|
||||||
|
<%= render_component "card" %>
|
||||||
|
<%- end -%>
|
||||||
|
</div>
|
||||||
|
<%- end -%>
|
||||||
|
|
||||||
<%- unless recently_added.empty? -%>
|
<%- unless recently_added.empty? -%>
|
||||||
<h2 class="uk-title home-headings">Recently Added</h2>
|
<h2 class="uk-title home-headings">Recently Added</h2>
|
||||||
<div id="item-container-continue" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
<%- recently_added.each do |ra| -%>
|
<%- recently_added.each do |ra| -%>
|
||||||
<% item = ra %>
|
<% item = ra %>
|
||||||
<% progress = ra[:percentage] %>
|
<% progress = ra[:percentage] %>
|
||||||
|
|||||||
@@ -8,12 +8,19 @@
|
|||||||
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
|
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
|
||||||
<div id="mobile-nav" uk-offcanvas="overlay: true">
|
<div id="mobile-nav" uk-offcanvas="overlay: true">
|
||||||
<div class="uk-offcanvas-bar uk-flex uk-flex-column">
|
<div class="uk-offcanvas-bar uk-flex uk-flex-column">
|
||||||
<ul class="uk-nav uk-nav-primary uk-nav-center uk-margin-auto-vertical">
|
<ul class="uk-nav-parent-icon uk-nav-primary uk-nav-center uk-margin-auto-vertical" uk-nav>
|
||||||
<li><a href="<%= base_url %>">Home</a></li>
|
<li><a href="<%= base_url %>">Home</a></li>
|
||||||
<li><a href="<%= base_url %>library">Library</a></li>
|
<li><a href="<%= base_url %>library">Library</a></li>
|
||||||
<% if is_admin %>
|
<% if is_admin %>
|
||||||
<li><a href="<%= base_url %>admin">Admin</a></li>
|
<li><a href="<%= base_url %>admin">Admin</a></li>
|
||||||
<li><a href="<%= base_url %>download">Download</a></li>
|
<li class="uk-parent">
|
||||||
|
<a href="#">Download</a>
|
||||||
|
<ul class="uk-nav-sub">
|
||||||
|
<li><a href="<%= base_url %>download">MangaDex</a></li>
|
||||||
|
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
|
||||||
|
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<hr uk-divider>
|
<hr uk-divider>
|
||||||
<li><a onclick="toggleTheme()"><i class="fas fa-adjust"></i></a></li>
|
<li><a onclick="toggleTheme()"><i class="fas fa-adjust"></i></a></li>
|
||||||
@@ -35,7 +42,18 @@
|
|||||||
<li><a href="<%= base_url %>library">Library</a></li>
|
<li><a href="<%= base_url %>library">Library</a></li>
|
||||||
<% if is_admin %>
|
<% if is_admin %>
|
||||||
<li><a href="<%= base_url %>admin">Admin</a></li>
|
<li><a href="<%= base_url %>admin">Admin</a></li>
|
||||||
<li><a href="<%= base_url %>download">Download</a></li>
|
<li>
|
||||||
|
<a href="#">Download</a>
|
||||||
|
<div class="uk-navbar-dropdown">
|
||||||
|
<ul class="uk-nav uk-navbar-dropdown-nav">
|
||||||
|
<li class="uk-nav-header">Source</li>
|
||||||
|
<li><a href="<%= base_url %>download">MangaDex</a></li>
|
||||||
|
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
|
||||||
|
<li class="uk-nav-divider"></li>
|
||||||
|
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,18 +67,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="uk-section uk-section-small">
|
<div class="uk-section uk-section-small">
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-section uk-section-small">
|
<div class="uk-section uk-section-small" id="main-section">
|
||||||
<div class="uk-container uk-container-small">
|
<div class="uk-container uk-container-small">
|
||||||
<div id="alert"></div>
|
<div id="alert"></div>
|
||||||
<%= content %>
|
<%= content %>
|
||||||
|
<div class="uk-visible@m" id="totop-wrapper" x-data="{}" x-show="$('body').height() > 1.5 * $(window).height()">
|
||||||
|
<a href="#" uk-totop uk-scroll></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
setTheme(getTheme());
|
setTheme();
|
||||||
const base_url = "<%= base_url %>";
|
const base_url = "<%= base_url %>";
|
||||||
</script>
|
</script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>
|
<script src="<%= base_url %>js/uikit.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
|
<script src="<%= base_url %>js/uikit-icons.min.js"></script>
|
||||||
|
|
||||||
<%= yield_content "script" %>
|
<%= yield_content "script" %>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<%= render_component "sort-form" %>
|
<%= render_component "sort-form" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="item-container" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
<% titles.each_with_index do |item, i| %>
|
<% titles.each_with_index do |item, i| %>
|
||||||
<% progress = percentage[i] %>
|
<% progress = percentage[i] %>
|
||||||
<%= render_component "card" %>
|
<%= render_component "card" %>
|
||||||
|
|||||||
@@ -27,10 +27,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
setTheme(getTheme());
|
setTheme();
|
||||||
</script>
|
</script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>
|
<script src="<%= base_url %>js/uikit.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
|
<script src="<%= base_url %>js/uikit-icons.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -21,18 +21,18 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% title.entries.each do |e| %>
|
<% title.entries.each do |e| %>
|
||||||
<entry>
|
<% next if e.err_msg %>
|
||||||
<title><%= HTML.escape(e.display_name) %></title>
|
<entry>
|
||||||
<id>urn:mango:<%= e.id %></id>
|
<title><%= HTML.escape(e.display_name) %></title>
|
||||||
|
<id>urn:mango:<%= e.id %></id>
|
||||||
|
|
||||||
<link rel="http://opds-spec.org/image" href="<%= e.cover_url %>" />
|
<link rel="http://opds-spec.org/image" href="<%= e.cover_url %>" />
|
||||||
<link rel="http://opds-spec.org/image/thumbnail" href="<%= e.cover_url %>" />
|
<link rel="http://opds-spec.org/image/thumbnail" href="<%= e.cover_url %>" />
|
||||||
|
|
||||||
<link rel="http://opds-spec.org/acquisition" href="<%= base_url %>opds/download/<%= e.title_id %>/<%= e.id %>" title="Read" type="<%= MIME.from_filename e.zip_path %>" />
|
<link rel="http://opds-spec.org/acquisition" href="<%= base_url %>opds/download/<%= e.book.id %>/<%= e.id %>" title="Read" type="<%= MIME.from_filename e.zip_path %>" />
|
||||||
|
|
||||||
<link type="text/html" rel="alternate" title="Read in Mango" href="<%= base_url %>reader/<%= e.title_id %>/<%= e.id %>" />
|
<link type="text/html" rel="alternate" title="Read in Mango" href="<%= base_url %>reader/<%= e.book.id %>/<%= e.id %>" />
|
||||||
<link type="text/html" rel="alternate" title="Open in Mango" href="<%= base_url %>book/<%= e.title_id %>" />
|
<link type="text/html" rel="alternate" title="Open in Mango" href="<%= base_url %>book/<%= e.book.id %>" />
|
||||||
</entry>
|
</entry>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
</feed>
|
||||||
</feed>
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<% if plugins.empty? %>
|
||||||
|
<div class="uk-container uk-text-center">
|
||||||
|
<h2>No Plugins Found</h2>
|
||||||
|
<p>We could't find any plugins in the directory <code><%= Config.current.plugin_path %></code>.</p>
|
||||||
|
<p>You can download official plugins from the <a href="https://github.com/hkalexling/mango-plugins">Mango plugins repository</a>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% else %>
|
||||||
|
<h2 class=uk-title>Download with Plugins</h2>
|
||||||
|
|
||||||
|
<div id="controls" class="uk-grid-small" uk-grid hidden>
|
||||||
|
<div class="uk-width-3-4@m uk-child-width-1-1">
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="search-input"> </label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input id="search-input" class="uk-input" type="text" placeholder="<%= plugin.not_nil!.info.placeholder %>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-expand">
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="plugin-select">Choose a plugin</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<select id="plugin-select" class="uk-select">
|
||||||
|
<% plugins.each do |p| %>
|
||||||
|
<option value="<%= p[:id] %>"><%= p[:title] %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-auto">
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="search-input"> </label>
|
||||||
|
<div class="uk-form-controls" style="padding-top: 10px;">
|
||||||
|
<span uk-icon="info" uk-toggle="target: #toggle"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="uk-description-list" id="toggle" hidden>
|
||||||
|
<% plugin.not_nil!.info.each do |k, v| %>
|
||||||
|
<dt><%= k %></dt>
|
||||||
|
<dd><%= v.to_s %></dd>
|
||||||
|
<% end %>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div id="table" class="uk-margin-large-top" hidden>
|
||||||
|
<h3 id="title-text"></h3>
|
||||||
|
|
||||||
|
<div class="uk-margin">
|
||||||
|
<button class="uk-button uk-button-default" onclick="selectAll()">Select All</button>
|
||||||
|
<button class="uk-button uk-button-default" onclick="unselect()">Clear Selections</button>
|
||||||
|
<button class="uk-button uk-button-primary" id="download-btn" onclick="download()">Download Selected</button>
|
||||||
|
<div id="download-spinner" uk-spinner class="uk-margin-left" hidden></div>
|
||||||
|
</div>
|
||||||
|
<p class="uk-text-meta">Click on a table row to select the chapter. Drag your mouse over multiple rows to select them all. Hold Ctrl to make multiple non-adjacent selections.</p>
|
||||||
|
<table class="uk-table uk-table-striped uk-overflow-auto tablesorter">
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
<% content_for "script" do %>
|
||||||
|
<% if plugin %>
|
||||||
|
<script>
|
||||||
|
var pid = "<%= plugin.not_nil!.info.id %>";
|
||||||
|
</script>
|
||||||
|
<% end %>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/js/jquery.tablesorter.combined.min.js"></script>
|
||||||
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
|
<script src="<%= base_url %>js/plugin-download.js"></script>
|
||||||
|
<% end %>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<div id="modal" class="uk-flex-top" uk-modal>
|
||||||
|
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
||||||
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||||
|
<div class="uk-modal-header">
|
||||||
|
<div>
|
||||||
|
<h3 class="uk-modal-title uk-margin-remove-top">Error</h3>
|
||||||
|
</div>
|
||||||
|
<p class="uk-text-meta uk-margin-remove-bottom"><%= entry.zip_path %></p>
|
||||||
|
<p class="uk-text-meta uk-margin-remove-top"><%= entry.err_msg %></p>
|
||||||
|
</div>
|
||||||
|
<div class="uk-modal-body">
|
||||||
|
<p uk-margin>
|
||||||
|
<% if next_entry = entry.next_entry username %>
|
||||||
|
<a class="uk-button uk-button-default" href="<%= base_url %>reader/<%= entry.book.id %>/<%= next_entry.id %>">Next Entry</a>
|
||||||
|
<% end %>
|
||||||
|
<a class="uk-button uk-button-primary" href="<%= base_url %>book/<%= entry.book.id %>">Return to Title</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% content_for "script" do %>
|
||||||
|
<script>
|
||||||
|
UIkit.modal('#modal').show().then(function() {
|
||||||
|
styleModal();
|
||||||
|
});
|
||||||
|
UIkit.util.on('#modal', 'hide', function() {
|
||||||
|
location.href = "<%= base_url %>book/<%= entry.book.id %>";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<% end %>
|
||||||
+45
-18
@@ -6,22 +6,40 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="uk-section uk-section-default uk-section-small reader-bg">
|
<div class="uk-section uk-section-default uk-section-small reader-bg">
|
||||||
<div class="uk-container uk-container-small">
|
<div class="uk-container uk-container-small">
|
||||||
<%- urls.each_with_index do |url, i| -%>
|
<div id="alert"></div>
|
||||||
<img class="uk-align-center" data-src="<%= url %>" data-width data-height uk-img id="<%= reader_urls[i] %>" onclick="showControl(<%= pages.to_a[i] %>);">
|
<div id="root" x-data="{
|
||||||
<%- end -%>
|
loading: true,
|
||||||
<%- if next_url -%>
|
msg: 'Loading the web reader. Please wait...',
|
||||||
<a class="next-url" href="<%= next_url %>"></a>
|
alertClass: 'uk-alert-primary',
|
||||||
<%- end -%>
|
items: []
|
||||||
|
}">
|
||||||
|
<div x-show="loading">
|
||||||
|
<div :class="alertClass" x-show="msg" uk-alert>
|
||||||
|
<p x-text="msg"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div x-show="!loading" x-cloak>
|
||||||
|
<template x-for="item in items">
|
||||||
|
<img
|
||||||
|
uk-img
|
||||||
|
class="uk-align-center"
|
||||||
|
:data-src="item.url"
|
||||||
|
:width="item.width"
|
||||||
|
:height="item.height"
|
||||||
|
:id="item.id"
|
||||||
|
@click="showControl($event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<%- if next_entry_url -%>
|
||||||
|
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="redirect('<%= next_entry_url %>')">Next Entry</button>
|
||||||
|
<%- else -%>
|
||||||
|
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="redirect('<%= exit_url %>')">Exit Reader</button>
|
||||||
|
<%- end -%>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- if next_entry_url -%>
|
|
||||||
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" hidden onclick="redirect('<%= next_entry_url %>')">Next Entry</button>
|
|
||||||
<%- else -%>
|
|
||||||
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" hidden onclick="redirect('<%= exit_url %>')">Exit Reader</button>
|
|
||||||
<%- end -%>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="hidden" hidden></div>
|
|
||||||
|
|
||||||
<div id="modal-sections" class="uk-flex-top" uk-modal>
|
<div id="modal-sections" class="uk-flex-top" uk-modal>
|
||||||
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
||||||
<button class="uk-modal-close-default" type="button" uk-close></button>
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||||
@@ -49,14 +67,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const base_url = "<%= base_url %>"
|
const base_url = "<%= base_url %>";
|
||||||
|
const page = <%= page %>;
|
||||||
|
const tid = "<%= title.id %>";
|
||||||
|
const eid = "<%= entry.id %>";
|
||||||
</script>
|
</script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/protonet-jquery.inview/1.1.2/jquery.inview.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
|
<script src="<%= base_url %>js/uikit.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.min.js"></script>
|
<script src="<%= base_url %>js/uikit-icons.min.js"></script>
|
||||||
<script src="<%= base_url %>js/reader.js"></script>
|
<script src="<%= base_url %>js/reader.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
img[data-src][src*='data:image'] { background: white; }
|
||||||
|
#root img { width: 100%; }
|
||||||
|
</style>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,23 @@
|
|||||||
<div>
|
<div>
|
||||||
|
<div id="select-bar" class="uk-card uk-card-body uk-card-default uk-margin-bottom" uk-sticky="offset:10" x-data="{count: 0}" @add.window="count++" @remove.window="count--" x-show="count > 0" style="border:orange;border-style:solid;" x-cloak data-id="<%= title.id %>">
|
||||||
|
<div class="uk-child-width-1-3" uk-grid>
|
||||||
|
<div>
|
||||||
|
<p x-text="count + ' items selected'" style="color:orange"></p>
|
||||||
|
</div>
|
||||||
|
<div class="uk-text-center" id="select-bar-controls">
|
||||||
|
<a class="uk-icon uk-margin-right" uk-tooltip="title: Mark selected as read" href="" @click.prevent="bulkProgress('read', $el)">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
</a>
|
||||||
|
<a class="uk-icon" uk-tooltip="title: Mark selected as unread" href="" @click.prevent="bulkProgress('unread', $el)">
|
||||||
|
<i class="fas fa-times-circle"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="uk-text-right">
|
||||||
|
<a @click="selectAll()" uk-tooltip="title: Select all"><i class="fas fa-check-double uk-margin-small-right"></i></a>
|
||||||
|
<a @click="deselectAll();" uk-tooltip="title: Deselect all"><i class="fas fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<h2 class=uk-title><span><%= title.display_name %></span>
|
<h2 class=uk-title><span><%= title.display_name %></span>
|
||||||
|
|
||||||
<% if is_admin %>
|
<% if is_admin %>
|
||||||
@@ -32,11 +51,14 @@
|
|||||||
<%= render_component "sort-form" %>
|
<%= render_component "sort-form" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="item-container" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
|
||||||
|
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
<% title.titles.each_with_index do |item, i| %>
|
<% title.titles.each_with_index do |item, i| %>
|
||||||
<% progress = title_percentage[i] %>
|
<% progress = title_percentage[i] %>
|
||||||
<%= render_component "card" %>
|
<%= render_component "card" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
<% entries.each_with_index do |item, i| %>
|
<% entries.each_with_index do |item, i| %>
|
||||||
<% progress = percentage[i] %>
|
<% progress = percentage[i] %>
|
||||||
<%= render_component "card" %>
|
<%= render_component "card" %>
|
||||||
|
|||||||
Reference in New Issue
Block a user