Compare commits

...

49 Commits

Author SHA1 Message Date
Alex Ling a6c2799521 Bump version to v0.12.3 2020-09-22 08:55:29 +00:00
Alex Ling 2370e4d2c6 Add browserstack as a sponsor 2020-09-22 08:54:20 +00:00
Alex Ling 32b0384ea0 Clearer gulpfile 2020-09-22 08:46:53 +00:00
Alex Ling 50d4ffdb7b Use babel and polyfill.io 2020-09-22 07:40:47 +00:00
Alex Ling 96463641f9 Update progress on last page (#105) 2020-09-21 04:35:23 +00:00
Alex Ling ddbba5d596 Bump version to v0.12.2 2020-09-17 16:08:52 +00:00
Alex Ling 2a04f4531e Bound the page number in the reader route
fixes #104
2020-09-17 16:06:01 +00:00
Alex Ling a5b6fb781f Bump version to v0.12.1 2020-09-17 13:32:00 +00:00
Alex Ling 8dfdab9d73 Respect the base URL in direct download link (#103) 2020-09-17 13:29:52 +00:00
Alex Ling 3a95270dfb Don't copy unused UIKit files 2020-09-17 13:25:35 +00:00
Alex Ling 2960ca54df Move fontawesome to NPM 2020-09-17 13:20:24 +00:00
Alex Ling f5fe3c6b1c Use image_size.cr v0.2.0 2020-09-16 15:40:01 +00:00
Alex Ling a612cc15fb Bump version to v0.12.0 2020-09-12 14:35:56 +00:00
Alex Ling c9c0818069 Add inline documentation to reader.js 2020-09-12 14:32:29 +00:00
Alex Ling 2f8efc382f Clean up 2020-09-12 14:05:15 +00:00
Alex Ling a0fb1880bd Update Dockerfile [skip ci] 2020-09-12 13:59:32 +00:00
Alex Ling a408f14425 Add .dockerignore 2020-09-12 13:59:14 +00:00
Alex Ling 243b6c8927 Typo fix [skip ci] 2020-09-12 07:13:15 +00:00
Alex Ling ff3a44d017 Update ARM dockerfiles to use image_size.cr 2020-09-12 07:03:30 +00:00
Alex Ling 67ef1f7112 DRY when listing archive entries 2020-09-12 06:58:03 +00:00
Alex Ling 5d7b8a1ef9 Skip error entries in OPDS feed 2020-09-12 06:57:42 +00:00
Alex Ling a68f3eea95 Allow hyphens in username (#99) 2020-09-12 05:29:25 +00:00
Alex Ling 220fc42bf2 Add system dependencies for image_size.cr 2020-09-11 17:02:45 +00:00
Alex Ling a45e6ea3da Rewrite web reader 2020-09-11 17:00:42 +00:00
Alex Ling 88394d4636 Expose page ratios through API 2020-09-11 17:00:28 +00:00
Alex Ling ef1ab940f5 Fix GitHub tags of dependencies in the Dockerfiles
[skip ci]
2020-08-16 16:45:57 +00:00
Alex Ling 97a1c408d8 Bump version to v0.11.0 2020-08-16 12:44:02 +00:00
Alex Ling abbf77df13 Merge branch 'master' into dev 2020-08-10 14:32:53 +00:00
Alex Ling 3b4021f680 Workflow retry hack
I got random "Invalid memory access" when running `crystal build`.
This is probably a compiler or LLVM bug.
We use this temporary hack to retry until they fix it.
2020-08-10 13:14:05 +00:00
Alex Ling 68b1923cb6 Clear title ID at the end of scans
This minimizes the chance of getting an unexpected empty home page
2020-08-10 11:45:50 +00:00
Alex Ling 3cdd4b29a5 Add back to top button to all pages (#95) 2020-08-10 11:42:23 +00:00
Alex Ling af84c0f6de Fix typo 2020-08-08 17:04:42 +08:00
Alex Ling 85a65f84d0 Remove unnecessary "require" statements 2020-08-06 18:10:13 +00:00
Alex Ling 5027a911cd Respect the *_PROXY environment variables (#94) 2020-08-06 17:01:53 +00:00
Alex Ling ac63bf7599 Add sponsors [skip ci] 2020-08-06 12:49:43 +08:00
Alex Ling 30b0e0b8fb Pin down mythml and duktape versions in Dockerfile
[skip ci]
2020-08-05 12:00:49 +00:00
Alex Ling ddda058d8d Fix spec 2020-08-05 09:59:52 +00:00
Alex Ling 46db25e8e0 Fix wildcard in workflow 2020-08-05 09:50:46 +00:00
Alex Ling c07f421322 Fix CLI tool not exiting 2020-08-05 09:48:31 +00:00
Alex Ling 99a77966ad Add arm64v8 to Makefile and rename object files 2020-08-05 09:48:03 +00:00
Alex Ling d00b917575 Build the object file in Action 2020-08-04 17:24:36 +00:00
Alex Ling 4fd8334c37 Name the object file 2020-08-04 17:24:13 +00:00
Alex Ling 3aa4630558 Use Crystal 0.34.0 2020-08-04 17:23:19 +00:00
Alex Ling cde5af7066 Remove interactive prompt for easier use in docker 2020-08-04 12:57:40 +00:00
Alex Ling eb528e1726 Add the arm32v7 target to Makefile 2020-08-04 11:50:07 +00:00
Alex Ling 5e01cc38fe Fix downloaders 2020-08-04 11:36:36 +00:00
Alex Ling 9a787ccbc3 Formatting 2020-08-04 11:36:24 +00:00
Alex Ling 8a83c0df4e ARM support (#25, #78) 2020-08-04 11:00:33 +00:00
Alex Ling 87dea01917 Add ASCII banner, because we can :) 2020-08-02 17:52:52 +00:00
36 changed files with 803 additions and 383 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules
lib
+12 -3
View File
@@ -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
+2 -3
View File
@@ -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
+14
View File
@@ -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"]
+14
View File
@@ -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"]
+8 -2
View File
@@ -12,10 +12,10 @@ setup: libs
yarn gulp dev 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
@@ -31,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
+7 -2
View File
@@ -51,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.10.0 Mango - Manga Server and Web Reader. Version 0.12.3
Usage: Usage:
@@ -139,8 +139,13 @@ Mobile UI:
![mobile screenshot](./.github/screenshots/mobile.png) ![mobile screenshot](./.github/screenshots/mobile.png)
## Sponsors
<a href="https://casinoshunter.com/online-casinos/"><img src="https://i.imgur.com/EJb3wBo.png" width="150" height="auto"></a>
<a href="https://www.browserstack.com/open-source"><img src="https://i.imgur.com/hGJUJXD.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/images/0)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/0)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/1)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/1)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/2)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/2)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/3)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/3)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/4)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/4)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/5)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/5)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/6)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/6)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/7)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/7) [![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/0)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/0)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/1)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/1)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/2)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/2)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/3)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/3)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/4)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/4)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/5)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/5)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/6)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/6)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/7)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/7)
+45 -31
View File
@@ -1,15 +1,43 @@
const gulp = require('gulp'); const gulp = require('gulp');
const minify = require("gulp-babel-minify"); const babel = require('gulp-babel');
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', () => { // Copy libraries from node_moduels to public/js
return gulp.src('node_modules/uikit/dist/js/*.min.js') gulp.task('copy-js', () => {
return gulp.src([
'node_modules/@fortawesome/fontawesome-free/js/fontawesome.min.js',
'node_modules/@fortawesome/fontawesome-free/js/solid.min.js',
'node_modules/uikit/dist/js/uikit.min.js',
'node_modules/uikit/dist/js/uikit-icons.min.js'
])
.pipe(gulp.dest('public/js')); .pipe(gulp.dest('public/js'));
}); });
gulp.task('minify-js', () => { // Copy UIKit SVG icons to public/img
return gulp.src('public/js/*.js') gulp.task('copy-uikit-icons', () => {
return gulp.src('node_modules/uikit/src/images/backgrounds/*.svg')
.pipe(gulp.dest('public/img'));
});
// Compile less
gulp.task('less', () => {
return gulp.src('public/css/*.less')
.pipe(less())
.pipe(gulp.dest('public/css'));
});
// Transpile and minify JS files and output to dist
gulp.task('babel', () => {
return gulp.src(['public/js/*.js', '!public/js/*.min.js'])
.pipe(babel({
presets: [
['@babel/preset-env', {
targets: '>0.25%, not dead, ios>=9'
}]
],
}))
.pipe(minify({ .pipe(minify({
removeConsole: true, removeConsole: true,
builtIns: false builtIns: false
@@ -17,40 +45,26 @@ gulp.task('minify-js', () => {
.pipe(gulp.dest('dist/js')); .pipe(gulp.dest('dist/js'));
}); });
gulp.task('less', () => { // Minify CSS and output to dist
return gulp.src('public/css/*.less')
.pipe(less())
.pipe(gulp.dest('public/css'));
});
gulp.task('minify-css', () => { gulp.task('minify-css', () => {
return gulp.src('public/css/*.css') return gulp.src('public/css/*.css')
.pipe(minifyCss()) .pipe(minifyCss())
.pipe(gulp.dest('dist/css')); .pipe(gulp.dest('dist/css'));
}); });
gulp.task('copy-uikit-icons', () => { // Copy static files (includeing images) to dist
return gulp.src('node_modules/uikit/src/images/backgrounds/*.svg')
.pipe(gulp.dest('public/img'));
});
gulp.task('img', () => {
return gulp.src('public/img/*')
.pipe(gulp.dest('dist/img'));
});
gulp.task('copy-files', () => { gulp.task('copy-files', () => {
return gulp.src('public/*.*') return gulp.src(['public/img/*', 'public/*.*', 'public/js/*.min.js'], {
base: 'public'
})
.pipe(gulp.dest('dist')); .pipe(gulp.dest('dist'));
}); });
gulp.task('default', gulp.parallel( // Set up the public folder for development
gulp.series('copy-uikit-js', 'minify-js'), gulp.task('dev', gulp.parallel('copy-js', 'copy-uikit-icons', 'less'));
gulp.series('less', 'minify-css'),
gulp.series('copy-uikit-icons', 'img'),
'copy-files'
));
gulp.task('dev', gulp.parallel( // Set up the dist folder for deployment
'copy-uikit-js', 'less', 'copy-uikit-icons' gulp.task('deploy', gulp.parallel('babel', 'minify-css', 'copy-files'));
));
// Default task
gulp.task('default', gulp.series('dev', 'deploy'));
+22 -19
View File
@@ -1,21 +1,24 @@
{ {
"name": "mango", "name": "mango",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"repository": "https://github.com/hkalexling/Mango.git", "repository": "https://github.com/hkalexling/Mango.git",
"author": "Alex Ling <hkalexling@gmail.com>", "author": "Alex Ling <hkalexling@gmail.com>",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"gulp": "^4.0.2", "@babel/preset-env": "^7.11.5",
"gulp-babel-minify": "^0.5.1", "gulp": "^4.0.2",
"gulp-less": "^4.0.1", "gulp-babel": "^8.0.0",
"gulp-minify-css": "^1.2.4", "gulp-babel-minify": "^0.5.1",
"less": "^3.11.3" "gulp-less": "^4.0.1",
}, "gulp-minify-css": "^1.2.4",
"scripts": { "less": "^3.11.3"
"uglify": "gulp" },
}, "scripts": {
"dependencies": { "uglify": "gulp"
"uikit": "^3.5.4" },
} "dependencies": {
"@fortawesome/fontawesome-free": "^5.14.0",
"uikit": "^3.5.4"
}
} }
+17
View File
@@ -135,3 +135,20 @@ td>.uk-dropdown {
#select-bar-controls a:hover { #select-bar-controls a:hover {
color: orange; 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);
}
-5
View File
File diff suppressed because one or more lines are too long
+181 -70
View File
@@ -1,64 +1,102 @@
$(function() { let lastSavedPage = page;
function bind() { let items = [];
var controller = new ScrollMagic.Controller(); let longPages = false;
// replace history on scroll $(() => {
$('img').each(function(idx) { getPages();
var scene = new ScrollMagic.Scene({
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 $('#page-select').change(() => {
var scene = new ScrollMagic.Scene({ const p = parseInt($('#page-select').val());
triggerElement: $('.next-url').get(), toPage(p);
triggerHook: 'onEnter', });
offset: -500 });
})
.addTo(controller) /**
.on('enter', function() { * Set an alpine.js property
var nextURL = $('.next-url').attr('href'); *
$('.next-url').remove(); * @function setProp
if (!nextURL) { * @param {string} key - Key of the data property
console.log('No .next-url found. Reached end of page'); * @param {*} prop - The data property
var lastURL = $('img').last().attr('id'); */
// load the reader URL for the last page to update reading progrss to 100% const setProp = (key, prop) => {
$.get(lastURL); $('#root').get(0).__x.$data[key] = prop;
$('#next-btn').removeAttr('hidden'); };
return;
} /**
$('#hidden').load(encodeURI(nextURL) + ' .uk-container', function(res, status, xhr) { * Get dimension of the pages in the entry from the API and update the view
if (status === 'error') console.log(xhr.statusText); */
if (status === 'success') { const getPages = () => {
console.log(nextURL + ' loaded'); $.get(`${base_url}api/dimensions/${tid}/${eid}`)
// new page loaded to #hidden, we now append it .then(data => {
$('.uk-section > .uk-container').append($('#hidden .uk-container').children()); if (!data.success && data.error)
$('#hidden').empty(); throw new Error(resp.error);
bind(); const dimensions = data.dimensions;
}
}); items = dimensions.map((d, i) => {
return {
id: i + 1,
url: `${base_url}api/page/${tid}/${eid}/${i+1}`,
width: d.width,
height: d.height
};
}); });
}
bind(); const avgRatio = items.reduce((acc, cur) => {
}); return acc + cur.height / cur.width
$('#page-select').change(function() { }, 0) / items.length;
jumpTo(parseInt($('#page-select').val()));
});
function showControl(idx) { console.log(avgRatio);
longPages = avgRatio > 2;
setProp('items', items);
setProp('loading', false);
waitForPage(items.length, () => {
toPage(page);
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);
@@ -66,19 +104,92 @@ function showControl(idx) {
UIkit.modal($('#modal-sections')).show(); UIkit.modal($('#modal-sections')).show();
} }
function jumpTo(page) { /**
var ary = window.location.pathname.split('/'); * Redirect to a URL
ary[ary.length - 1] = page; *
ary.shift(); // remove leading `/` * @function redirect
ary.unshift(window.location.origin); * @param {string} url - The target URL
window.location.replace(ary.join('/')); */
} const redirect = (url) => {
function replaceHistory(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);
}
});
});
};
/**
* Update the backend reading progress if:
* 1) the current page is more than five pages away from the last
* saved page, or
* 2) the average height/width ratio of the pages is over 2, or
* 3) the current page is the first page, or
* 4) the current page is the last page
*
* @function saveProgress
* @param {number} idx - One-based index of the page
* @param {function} cb - Callback
*/
const saveProgress = (idx, cb) => {
idx = parseInt(idx);
if (Math.abs(idx - lastSavedPage) >= 5 ||
longPages ||
idx === 1 || idx === items.length
) {
lastSavedPage = idx;
console.log('saving progress', idx);
const url = `${base_url}api/progress/${tid}/${idx}?${$.param({entry: eid})}`;
$.post(url)
.then(data => {
if (data.error) throw new Error(data.error);
if (cb) cb();
})
.catch(e => {
console.error(e);
alert('danger', e);
});
}
};
/**
* Mark progress to 100% and redirect to the next entry
* Used as the onclick handler for the "Next Entry" button
*
* @function nextEntry
* @param {string} nextUrl - URL of the next entry
*/
const nextEntry = (nextUrl) => {
saveProgress(items.length, () => {
redirect(nextUrl);
});
};
-5
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -55,7 +55,7 @@ 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();
} }
+8
View File
@@ -28,6 +28,14 @@ shards:
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
+5 -1
View File
@@ -1,5 +1,5 @@
name: mango name: mango
version: 0.10.0 version: 0.12.3
authors: authors:
- Alex Ling <hkalexling@gmail.com> - Alex Ling <hkalexling@gmail.com>
@@ -32,3 +32,7 @@ dependencies:
version: ~> 0.20.0 version: ~> 0.20.0
myhtml: myhtml:
github: kostya/myhtml github: kostya/myhtml
http_proxy:
github: mamantoha/http_proxy
image_size:
github: hkalexling/image_size.cr
+1
View File
@@ -2,6 +2,7 @@ require "spec"
require "../src/queue" 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
+2 -6
View File
@@ -52,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
+33 -5
View File
@@ -1,3 +1,5 @@
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, encoded_path : String, size : String, pages : Int32, id : String, encoded_path : String,
@@ -77,11 +79,9 @@ class Entry
url url
end end
def read_page(page_num) private def sorted_archive_entries
raise "Unreadble archive. #{@err_msg}" if @err_msg
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
@@ -89,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,
@@ -99,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
+1 -1
View File
@@ -92,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
@@ -103,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
+34
View File
@@ -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
View File
@@ -1,4 +1,3 @@
require "http/client"
require "json" require "json"
require "csv" require "csv"
require "../rename" require "../rename"
+9 -7
View File
@@ -27,14 +27,16 @@ module MangaDex
def pop : Queue::Job? def pop : Queue::Job?
job = nil job = nil
DB.open "sqlite3://#{@queue.path}" do |db| MainFiber.run do
begin DB.open "sqlite3://#{@queue.path}" do |db|
db.query_one "select * from queue where id not like '%-%' " \ begin
"and (status = 0 or status = 1) " \ db.query_one "select * from queue where id not like '%-%' " \
"order by time limit 1" do |res| "and (status = 0 or status = 1) " \
job = Queue::Job.from_query_result res "order by time limit 1" do |res|
job = Queue::Job.from_query_result res
end
rescue
end end
rescue
end end
end end
job job
+31 -6
View File
@@ -1,12 +1,29 @@
require "./config" require "./config"
require "./queue" 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/*" require "./plugin/*"
MANGO_VERSION = "0.10.0" MANGO_VERSION = "0.12.3"
# 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,
@@ -22,20 +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|
puts BANNER
puts DESCRIPTION
puts
# empty ARGV so it won't be passed to Kemal
ARGV.clear
Config.load(opts.config).set_current Config.load(opts.config).set_current
MangaDex::Downloader.default MangaDex::Downloader.default
Plugin::Downloader.default Plugin::Downloader.default
# empty ARGV so it won't be passed to Kemal spawn do
ARGV.clear Server.new.start
server = Server.new end
server.start
MainFiber.start_and_block
end end
sub "admin" do sub "admin" do
+9 -7
View File
@@ -8,14 +8,16 @@ class Plugin
def pop : Queue::Job? def pop : Queue::Job?
job = nil job = nil
DB.open "sqlite3://#{@queue.path}" do |db| MainFiber.run do
begin DB.open "sqlite3://#{@queue.path}" do |db|
db.query_one "select * from queue where id like '%-%' " \ begin
"and (status = 0 or status = 1) " \ db.query_one "select * from queue where id like '%-%' " \
"order by time limit 1" do |res| "and (status = 0 or status = 1) " \
job = Queue::Job.from_query_result res "order by time limit 1" do |res|
job = Queue::Job.from_query_result res
end
rescue
end end
rescue
end end
end end
job job
-1
View File
@@ -1,6 +1,5 @@
require "duktape/runtime" require "duktape/runtime"
require "myhtml" require "myhtml"
require "http"
require "xml" require "xml"
class Plugin class Plugin
+87 -59
View File
@@ -119,22 +119,24 @@ class Queue
"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|
db.exec "create table if not exists queue " \ begin
"(id text, manga_id text, title text, manga_title " \ db.exec "create table if not exists queue " \
"text, status integer, status_message text, " \ "(id text, manga_id text, title text, manga_title " \
"pages integer, success_count integer, " \ "text, status integer, status_message text, " \
"fail_count integer, time integer)" "pages integer, success_count integer, " \
db.exec "create unique index if not exists id_idx " \ "fail_count integer, time integer)"
"on queue (id)" db.exec "create unique index if not exists id_idx " \
db.exec "create index if not exists manga_id_idx " \ "on queue (id)"
"on queue (manga_id)" db.exec "create index if not exists manga_id_idx " \
db.exec "create index if not exists status_idx " \ "on queue (manga_id)"
"on queue (status)" db.exec "create index if not exists status_idx " \
rescue e "on queue (status)"
Logger.error "Error when checking tables in DB: #{e}" rescue e
raise e Logger.error "Error when checking tables in DB: #{e}"
raise e
end
end end
end end
end end
@@ -143,23 +145,27 @@ class Queue
# inserted. Any job already exists in the queue will be ignored. # inserted. Any job already exists in the queue will be ignored.
def push(jobs : Array(Job)) def push(jobs : Array(Job))
start_count = self.count start_count = self.count
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
jobs.each do |job| DB.open "sqlite3://#{@path}" do |db|
db.exec "insert or ignore into queue values " \ 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.id, job.manga_id, job.title, job.manga_title,
job.success_count, job.fail_count, job.time.to_unix_ms job.status.to_i, job.status_message, job.pages,
job.success_count, job.fail_count, job.time.to_unix_ms
end
end end
end end
self.count - start_count self.count - start_count
end end
def reset(id : String) def reset(id : String)
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
db.exec "update queue set status = 0, status_message = '', " \ DB.open "sqlite3://#{@path}" do |db|
"pages = 0, success_count = 0, fail_count = 0 " \ db.exec "update queue set status = 0, status_message = '', " \
"where id = (?)", id "pages = 0, success_count = 0, fail_count = 0 " \
"where id = (?)", id
end
end end
end end
@@ -169,16 +175,20 @@ class Queue
# Reset all failed tasks (missing pages and error) # Reset all failed tasks (missing pages and error)
def reset def reset
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
db.exec "update queue set status = 0, status_message = '', " \ DB.open "sqlite3://#{@path}" do |db|
"pages = 0, success_count = 0, fail_count = 0 " \ db.exec "update queue set status = 0, status_message = '', " \
"where status = 2 or status = 4" "pages = 0, success_count = 0, fail_count = 0 " \
"where status = 2 or status = 4"
end
end end
end end
def delete(id : String) def delete(id : String)
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
db.exec "delete from queue where id = (?)", id DB.open "sqlite3://#{@path}" do |db|
db.exec "delete from queue where id = (?)", id
end
end end
end end
@@ -187,71 +197,89 @@ class Queue
end end
def delete_status(status : JobStatus) def delete_status(status : JobStatus)
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
db.exec "delete from queue where status = (?)", status.to_i DB.open "sqlite3://#{@path}" do |db|
db.exec "delete from queue where status = (?)", status.to_i
end
end end
end end
def count_status(status : JobStatus) def count_status(status : JobStatus)
num = 0 num = 0
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
num = db.query_one "select count(*) from queue where " \ DB.open "sqlite3://#{@path}" do |db|
"status = (?)", status.to_i, as: Int32 num = db.query_one "select count(*) from queue where " \
"status = (?)", status.to_i, as: Int32
end
end end
num num
end end
def count def count
num = 0 num = 0
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
num = db.query_one "select count(*) from queue", as: Int32 DB.open "sqlite3://#{@path}" do |db|
num = db.query_one "select count(*) from queue", as: Int32
end
end end
num num
end end
def set_status(status : JobStatus, job : Job) def set_status(status : JobStatus, job : Job)
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
db.exec "update queue set status = (?) where id = (?)", DB.open "sqlite3://#{@path}" do |db|
status.to_i, job.id db.exec "update queue set status = (?) where id = (?)",
status.to_i, job.id
end
end end
end end
def get_all def get_all
jobs = [] of Job jobs = [] of Job
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
jobs = db.query_all "select * from queue order by time" do |rs| DB.open "sqlite3://#{@path}" do |db|
Job.from_query_result rs jobs = db.query_all "select * from queue order by time" do |rs|
Job.from_query_result rs
end
end end
end end
jobs jobs
end end
def add_success(job : Job) def add_success(job : Job)
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
db.exec "update queue set success_count = success_count + 1 " \ DB.open "sqlite3://#{@path}" do |db|
"where id = (?)", job.id db.exec "update queue set success_count = success_count + 1 " \
"where id = (?)", job.id
end
end end
end end
def add_fail(job : Job) def add_fail(job : Job)
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
db.exec "update queue set fail_count = fail_count + 1 " \ DB.open "sqlite3://#{@path}" do |db|
"where id = (?)", job.id db.exec "update queue set fail_count = fail_count + 1 " \
"where id = (?)", job.id
end
end end
end end
def set_pages(pages : Int32, job : Job) def set_pages(pages : Int32, job : Job)
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
db.exec "update queue set pages = (?), success_count = 0, " \ DB.open "sqlite3://#{@path}" do |db|
"fail_count = 0 where id = (?)", pages, job.id db.exec "update queue set pages = (?), success_count = 0, " \
"fail_count = 0 where id = (?)", pages, job.id
end
end end
end end
def add_message(msg : String, job : Job) def add_message(msg : String, job : Job)
DB.open "sqlite3://#{@path}" do |db| MainFiber.run do
db.exec "update queue set status_message = " \ DB.open "sqlite3://#{@path}" do |db|
"status_message || (?) || (?) where id = (?)", db.exec "update queue set status_message = " \
"\n", msg, job.id "status_message || (?) || (?) where id = (?)",
"\n", msg, job.id
end
end end
end end
+23
View File
@@ -332,5 +332,28 @@ class APIRouter < Router
}.to_json }.to_json
end end
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
+5 -21
View File
@@ -12,11 +12,7 @@ class ReaderRouter < Router
next layout "reader-error" if entry.err_msg next layout "reader-error" if entry.err_msg
# load progress # load progress
page = entry.load_progress username page = [1, entry.load_progress username].max
# 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
@@ -32,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
+122 -94
View File
@@ -32,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
@@ -87,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
@@ -125,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
@@ -138,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
@@ -152,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
@@ -163,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
@@ -206,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
+42
View File
@@ -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
+3 -3
View File
@@ -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
+4 -1
View File
@@ -7,9 +7,12 @@
<link rel="stylesheet" href="<%= base_url %>css/uikit.css" /> <link rel="stylesheet" href="<%= base_url %>css/uikit.css" />
<link rel="stylesheet" href="<%= base_url %>css/mango.css" /> <link rel="stylesheet" href="<%= base_url %>css/mango.css" />
<link rel="icon" href="<%= base_url %>favicon.ico"> <link rel="icon" href="<%= base_url %>favicon.ico">
<script src="https://polyfill.io/v3/polyfill.min.js?features=matchMedia%2Cdefault&flags=gated"></script>
<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 type="module" src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.5.0/dist/alpine.min.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.5.0/dist/alpine-ie11.min.js" defer></script>
<script src="<%= base_url %>js/theme.js"></script> <script src="<%= base_url %>js/theme.js"></script>
</head> </head>
+4 -1
View File
@@ -67,10 +67,13 @@
</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>
+12 -12
View File
@@ -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.book.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.book.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.book.id %>" /> <link type="text/html" rel="alternate" title="Open in Mango" href="<%= base_url %>book/<%= e.book.id %>" />
</entry> </entry>
<% end %> <% end %>
</feed>
</feed>
+43 -16
View File
@@ -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 %>" src="<%= base_url %>img/loading.gif" 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="nextEntry('<%= 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://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.min.js"></script> <script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/uikit.min.js"></script> <script src="<%= base_url %>js/uikit.min.js"></script>
<script src="<%= base_url %>js/uikit-icons.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>