Compare commits

...

51 Commits

Author SHA1 Message Date
Alex Ling 16a9d7fc2e Merge pull request #110 from XavierSchiller/master
[arm64] Fix Wrong libgc.so location when building Image
2020-09-27 20:07:44 +08:00
Xavier ee2b4abc85 Fix Wrong libgc.so location when building Image.
The Repo Maintainer was using the location of libgc.so from the armhf package, however, according to:
https://debian.pkgs.org/9/debian-main-arm64/libgc-dev_7.4.2-8_arm64.deb.html and
https://packages.ubuntu.com/focal/arm64/libgc-dev/filelist
it exists under /usr/lib/aarch64-linux-gnu/
2020-09-27 14:35:43 +05:30
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/aarch64-linux-gnu/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'));
+3
View File
@@ -6,7 +6,9 @@
"author": "Alex Ling <hkalexling@gmail.com>", "author": "Alex Ling <hkalexling@gmail.com>",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.11.5",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
"gulp-babel-minify": "^0.5.1", "gulp-babel-minify": "^0.5.1",
"gulp-less": "^4.0.1", "gulp-less": "^4.0.1",
"gulp-minify-css": "^1.2.4", "gulp-minify-css": "^1.2.4",
@@ -16,6 +18,7 @@
"uglify": "gulp" "uglify": "gulp"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.14.0",
"uikit": "^3.5.4" "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() {
var nextURL = $('.next-url').attr('href');
$('.next-url').remove();
if (!nextURL) {
console.log('No .next-url found. Reached end of page');
var lastURL = $('img').last().attr('id');
// load the reader URL for the last page to update reading progrss to 100%
$.get(lastURL);
$('#next-btn').removeAttr('hidden');
return;
}
$('#hidden').load(encodeURI(nextURL) + ' .uk-container', function(res, status, xhr) {
if (status === 'error') console.log(xhr.statusText);
if (status === 'success') {
console.log(nextURL + ' loaded');
// new page loaded to #hidden, we now append it
$('.uk-section > .uk-container').append($('#hidden .uk-container').children());
$('#hidden').empty();
bind();
}
}); });
});
}
bind();
});
$('#page-select').change(function() {
jumpTo(parseInt($('#page-select').val()));
}); });
function showControl(idx) { /**
* Set an alpine.js property
*
* @function setProp
* @param {string} key - Key of the data property
* @param {*} prop - The data property
*/
const setProp = (key, prop) => {
$('#root').get(0).__x.$data[key] = prop;
};
/**
* Get dimension of the pages in the entry from the API and update the view
*/
const getPages = () => {
$.get(`${base_url}api/dimensions/${tid}/${eid}`)
.then(data => {
if (!data.success && data.error)
throw new Error(resp.error);
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
};
});
const avgRatio = items.reduce((acc, cur) => {
return acc + cur.height / cur.width
}, 0) / items.length;
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"
+2
View File
@@ -27,6 +27,7 @@ module MangaDex
def pop : Queue::Job? def pop : Queue::Job?
job = nil job = nil
MainFiber.run do
DB.open "sqlite3://#{@queue.path}" do |db| DB.open "sqlite3://#{@queue.path}" do |db|
begin begin
db.query_one "select * from queue where id not like '%-%' " \ db.query_one "select * from queue where id not like '%-%' " \
@@ -37,6 +38,7 @@ module MangaDex
rescue rescue
end end
end end
end
job job
end end
+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
+2
View File
@@ -8,6 +8,7 @@ class Plugin
def pop : Queue::Job? def pop : Queue::Job?
job = nil job = nil
MainFiber.run do
DB.open "sqlite3://#{@queue.path}" do |db| DB.open "sqlite3://#{@queue.path}" do |db|
begin begin
db.query_one "select * from queue where id like '%-%' " \ db.query_one "select * from queue where id like '%-%' " \
@@ -18,6 +19,7 @@ class Plugin
rescue rescue
end end
end end
end
job job
end end
-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
+28
View File
@@ -119,6 +119,7 @@ class Queue
"Attepmting to create it" "Attepmting to create it"
Dir.mkdir_p dir Dir.mkdir_p dir
end end
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
begin begin
db.exec "create table if not exists queue " \ db.exec "create table if not exists queue " \
@@ -138,11 +139,13 @@ class Queue
end end
end end
end end
end
# Push an array of jobs into the queue, and return the number of jobs # 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. # 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
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
jobs.each do |job| jobs.each do |job|
db.exec "insert or ignore into queue values " \ db.exec "insert or ignore into queue values " \
@@ -152,16 +155,19 @@ class Queue
job.success_count, job.fail_count, job.time.to_unix_ms 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)
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
db.exec "update queue set status = 0, status_message = '', " \ db.exec "update queue set status = 0, status_message = '', " \
"pages = 0, success_count = 0, fail_count = 0 " \ "pages = 0, success_count = 0, fail_count = 0 " \
"where id = (?)", id "where id = (?)", id
end end
end end
end
def reset(job : Job) def reset(job : Job)
self.reset job.id self.reset job.id
@@ -169,91 +175,113 @@ class Queue
# Reset all failed tasks (missing pages and error) # Reset all failed tasks (missing pages and error)
def reset def reset
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
db.exec "update queue set status = 0, status_message = '', " \ db.exec "update queue set status = 0, status_message = '', " \
"pages = 0, success_count = 0, fail_count = 0 " \ "pages = 0, success_count = 0, fail_count = 0 " \
"where status = 2 or status = 4" "where status = 2 or status = 4"
end end
end end
end
def delete(id : String) def delete(id : String)
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
db.exec "delete from queue where id = (?)", id db.exec "delete from queue where id = (?)", id
end end
end end
end
def delete(job : Job) def delete(job : Job)
self.delete job.id self.delete job.id
end end
def delete_status(status : JobStatus) def delete_status(status : JobStatus)
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
db.exec "delete from queue where status = (?)", status.to_i 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
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
num = db.query_one "select count(*) from queue where " \ num = db.query_one "select count(*) from queue where " \
"status = (?)", status.to_i, as: Int32 "status = (?)", status.to_i, as: Int32
end end
end
num num
end end
def count def count
num = 0 num = 0
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
num = db.query_one "select count(*) from queue", as: Int32 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)
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
db.exec "update queue set status = (?) where id = (?)", db.exec "update queue set status = (?) where id = (?)",
status.to_i, job.id status.to_i, job.id
end end
end end
end
def get_all def get_all
jobs = [] of Job jobs = [] of Job
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
jobs = db.query_all "select * from queue order by time" do |rs| jobs = db.query_all "select * from queue order by time" do |rs|
Job.from_query_result 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)
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
db.exec "update queue set success_count = success_count + 1 " \ db.exec "update queue set success_count = success_count + 1 " \
"where id = (?)", job.id "where id = (?)", job.id
end end
end end
end
def add_fail(job : Job) def add_fail(job : Job)
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
db.exec "update queue set fail_count = fail_count + 1 " \ db.exec "update queue set fail_count = fail_count + 1 " \
"where id = (?)", job.id "where id = (?)", job.id
end end
end end
end
def set_pages(pages : Int32, job : Job) def set_pages(pages : Int32, job : Job)
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
db.exec "update queue set pages = (?), success_count = 0, " \ db.exec "update queue set pages = (?), success_count = 0, " \
"fail_count = 0 where id = (?)", pages, job.id "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)
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
db.exec "update queue set status_message = " \ db.exec "update queue set status_message = " \
"status_message || (?) || (?) where id = (?)", "status_message || (?) || (?) where id = (?)",
"\n", msg, job.id "\n", msg, job.id
end end
end end
end
def <<(downloader : Downloader) def <<(downloader : Downloader)
@downloaders << downloader @downloaders << downloader
+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
+32 -4
View File
@@ -32,6 +32,7 @@ class Storage
"Attepmting to create it" "Attepmting to create it"
Dir.mkdir_p dir Dir.mkdir_p dir
end end
MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
begin begin
# We create the `ids` table first. even if the uses has an # We create the `ids` table first. even if the uses has an
@@ -66,6 +67,7 @@ class Storage
@db = DB.open "sqlite3://#{@path}" @db = DB.open "sqlite3://#{@path}"
end end
end end
end
macro init_admin macro init_admin
random_pw = random_str random_pw = random_str
@@ -87,6 +89,8 @@ class Storage
end end
def verify_user(username, password) def verify_user(username, password)
out_token = nil
MainFiber.run do
get_db do |db| get_db do |db|
begin begin
hash, token = db.query_one "select password, token from " \ hash, token = db.query_one "select password, token from " \
@@ -94,24 +98,29 @@ class Storage
username, as: {String, String?} username, as: {String, String?}
unless verify_password hash, password unless verify_password hash, password
Logger.debug "Password does not match the hash" Logger.debug "Password does not match the hash"
return nil next
end end
Logger.debug "User #{username} verified" Logger.debug "User #{username} verified"
return token if token if token
out_token = token
next
end
token = random_str token = random_str
Logger.debug "Updating token for #{username}" Logger.debug "Updating token for #{username}"
db.exec "update users set token = (?) where username = (?)", db.exec "update users set token = (?) where username = (?)",
token, username token, username
return token out_token = token
rescue e rescue e
Logger.error "Error when verifying user #{username}: #{e}" Logger.error "Error when verifying user #{username}: #{e}"
return nil
end end
end end
end end
out_token
end
def verify_token(token) def verify_token(token)
username = nil username = nil
MainFiber.run do
get_db do |db| get_db do |db|
begin begin
username = db.query_one "select username from users where " \ username = db.query_one "select username from users where " \
@@ -120,11 +129,13 @@ class Storage
Logger.debug "Unable to verify token" Logger.debug "Unable to verify token"
end end
end end
end
username username
end end
def verify_admin(token) def verify_admin(token)
is_admin = false is_admin = false
MainFiber.run do
get_db do |db| get_db do |db|
begin begin
is_admin = db.query_one "select admin from users where " \ is_admin = db.query_one "select admin from users where " \
@@ -133,11 +144,13 @@ class Storage
Logger.debug "Unable to verify user as admin" Logger.debug "Unable to verify user as admin"
end end
end end
end
is_admin is_admin
end end
def list_users def list_users
results = Array(Tuple(String, Bool)).new results = Array(Tuple(String, Bool)).new
MainFiber.run do
get_db do |db| get_db do |db|
db.query "select username, admin from users" do |rs| db.query "select username, admin from users" do |rs|
rs.each do rs.each do
@@ -145,6 +158,7 @@ class Storage
end end
end end
end end
end
results results
end end
@@ -152,17 +166,20 @@ class Storage
validate_username username validate_username username
validate_password password validate_password password
admin = (admin ? 1 : 0) admin = (admin ? 1 : 0)
MainFiber.run do
get_db do |db| get_db do |db|
hash = hash_password password hash = hash_password password
db.exec "insert into users values (?, ?, ?, ?)", db.exec "insert into users values (?, ?, ?, ?)",
username, hash, nil, admin username, hash, nil, admin
end end
end end
end
def update_user(original_username, username, password, admin) def update_user(original_username, username, password, admin)
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?
MainFiber.run do
get_db do |db| get_db do |db|
if password.empty? if password.empty?
db.exec "update users set username = (?), admin = (?) " \ db.exec "update users set username = (?), admin = (?) " \
@@ -176,14 +193,18 @@ class Storage
end end
end end
end end
end
def delete_user(username) def delete_user(username)
MainFiber.run do
get_db do |db| get_db do |db|
db.exec "delete from users where username = (?)", username db.exec "delete from users where username = (?)", username
end end
end end
end
def logout(token) def logout(token)
MainFiber.run do
get_db do |db| get_db do |db|
begin begin
db.exec "update users set token = (?) where token = (?)", nil, token db.exec "update users set token = (?) where token = (?)", nil, token
@@ -191,13 +212,16 @@ class Storage
end end
end end
end end
end
def get_id(path, is_title) def get_id(path, is_title)
id = nil id = nil
MainFiber.run do
get_db do |db| get_db do |db|
id = db.query_one? "select id from ids where path = (?)", path, id = db.query_one? "select id from ids where path = (?)", path,
as: {String} as: {String}
end end
end
id id
end end
@@ -206,6 +230,7 @@ class Storage
end end
def bulk_insert_ids def bulk_insert_ids
MainFiber.run do
get_db do |db| get_db do |db|
db.transaction do |tx| db.transaction do |tx|
@insert_ids.each do |tp| @insert_ids.each do |tp|
@@ -216,12 +241,15 @@ class Storage
end end
@insert_ids.clear @insert_ids.clear
end end
end
def close def close
MainFiber.run do
unless @db.nil? unless @db.nil?
@db.not_nil!.close @db.not_nil!.close
end end
end end
end
def to_json(json : JSON::Builder) def to_json(json : JSON::Builder)
json.string self json.string self
+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>
+2 -2
View File
@@ -21,6 +21,7 @@
<% end %> <% end %>
<% title.entries.each do |e| %> <% title.entries.each do |e| %>
<% next if e.err_msg %>
<entry> <entry>
<title><%= HTML.escape(e.display_name) %></title> <title><%= HTML.escape(e.display_name) %></title>
<id>urn:mango:<%= e.id %></id> <id>urn:mango:<%= e.id %></id>
@@ -34,5 +35,4 @@
<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>
+40 -13
View File
@@ -6,21 +6,39 @@
<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>
<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 -%> <%- 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> <button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="nextEntry('<%= next_entry_url %>')">Next Entry</button>
<%- else -%> <%- else -%>
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" hidden onclick="redirect('<%= exit_url %>')">Exit Reader</button> <button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="redirect('<%= exit_url %>')">Exit Reader</button>
<%- end -%> <%- end -%>
</div> </div>
</div>
<div id="hidden" hidden></div> </div>
</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">
@@ -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>