mirror of
https://github.com/hkalexling/Mango.git
synced 2026-04-25 00:00:52 -04:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07100121ef | |||
| a0e550569e | |||
| bbbe2e0588 | |||
| 9d31b24e8c | |||
| 38ba324fa9 | |||
| c00016fa19 | |||
| 4d5a305d1b | |||
| f9ca52ee2f | |||
| f6c393545c | |||
| 466aee62fe | |||
| eab0800376 | |||
| 1725f42698 | |||
| f5cdf8b7b6 | |||
| fe082e7537 | |||
| c87b96dd0b | |||
| 9d76ca8c24 | |||
| 5f21653e07 | |||
| 0035cd9177 | |||
| 899b221842 | |||
| a317086f81 | |||
| b83313b231 | |||
| 62af879bfa | |||
| 96f98f6c78 | |||
| 841d5051cb | |||
| 0768e2177b | |||
| 0e4d67cf29 | |||
| 00fcc881ee | |||
| ca8d9efcfd | |||
| 0e7be6392d | |||
| 4af5258602 | |||
| 23c6256552 | |||
| ef0e3fd346 | |||
| b70fad13a7 | |||
| d2f9735250 | |||
| 06d6311080 | |||
| 674da55bde | |||
| dc084aff7c | |||
| 4c2cf64f53 | |||
| f4c4bb536c | |||
| 47edb3008b | |||
| e28dadc94e | |||
| 3dc9bd2264 | |||
| 9302601307 | |||
| 650ba98039 | |||
| bb2173788b | |||
| c8be2849b9 | |||
| aa269f26ee | |||
| 5c26b0d6dc | |||
| c9d3c35bdd | |||
| 9255de710f | |||
| 39b251774f | |||
| 156e511d4a | |||
| 5cd6f3eacb |
@@ -8,10 +8,13 @@ assignees: ''
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
**Is your feature request related to a problem? Please describe.**
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
A clear and concise description of what the problem is. E.g. I'm always frustrated when [...]
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
**Describe the solution you'd like**
|
||||||
A clear and concise description of what you want to happen.
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe a small use-case for this feature request**
|
||||||
|
How would you imagine this to be used? What would be the advantage of this for the users of the application?
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context or screenshots about the feature request here.
|
Add any other context or screenshots about the feature request here.
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ dist
|
|||||||
mango
|
mango
|
||||||
.env
|
.env
|
||||||
*.md
|
*.md
|
||||||
|
public/css/uikit.css
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ Mango is a self-hosted manga server and reader. Its features include
|
|||||||
- The web reader is responsive and works well on mobile, so there is no need for a mobile app
|
- The web reader is responsive and works well on mobile, so there is no need for a mobile app
|
||||||
- All the static files are embedded in the binary, so the deployment process is easy and painless
|
- All the static files are embedded in the binary, so the deployment process is easy and painless
|
||||||
|
|
||||||
|
Please check the [Wiki](https://github.com/hkalexling/Mango/wiki) for more information.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Pre-built Binary
|
### Pre-built Binary
|
||||||
@@ -48,7 +50,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
|
|||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
```
|
```
|
||||||
Mango - Manga Server and Web Reader. Version 0.6.0
|
Mango - Manga Server and Web Reader. Version 0.7.3
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -1,6 +1,7 @@
|
|||||||
const gulp = require('gulp');
|
const gulp = require('gulp');
|
||||||
const minify = require("gulp-babel-minify");
|
const minify = require("gulp-babel-minify");
|
||||||
const minifyCss = require('gulp-minify-css');
|
const minifyCss = require('gulp-minify-css');
|
||||||
|
const less = require('gulp-less');
|
||||||
|
|
||||||
gulp.task('minify-js', () => {
|
gulp.task('minify-js', () => {
|
||||||
return gulp.src('public/js/*.js')
|
return gulp.src('public/js/*.js')
|
||||||
@@ -10,6 +11,12 @@ gulp.task('minify-js', () => {
|
|||||||
.pipe(gulp.dest('dist/js'));
|
.pipe(gulp.dest('dist/js'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task('less', () => {
|
||||||
|
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())
|
||||||
@@ -21,9 +28,9 @@ gulp.task('img', () => {
|
|||||||
.pipe(gulp.dest('dist/img'));
|
.pipe(gulp.dest('dist/img'));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('favicon', () => {
|
gulp.task('copy-files', () => {
|
||||||
return gulp.src('public/favicon.ico')
|
return gulp.src('public/*.*')
|
||||||
.pipe(gulp.dest('dist'));
|
.pipe(gulp.dest('dist'));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('default', gulp.parallel('minify-js', 'minify-css', 'img', 'favicon'));
|
gulp.task('default', gulp.parallel('minify-js', gulp.series('less', 'minify-css'), 'img', 'copy-files'));
|
||||||
|
|||||||
+6
-1
@@ -8,9 +8,14 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"gulp-babel-minify": "^0.5.1",
|
"gulp-babel-minify": "^0.5.1",
|
||||||
"gulp-minify-css": "^1.2.4"
|
"gulp-less": "^4.0.1",
|
||||||
|
"gulp-minify-css": "^1.2.4",
|
||||||
|
"less": "^3.11.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"uglify": "gulp"
|
"uglify": "gulp"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"uikit": "^3.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-34
@@ -1,74 +1,99 @@
|
|||||||
.uk-alert-close {
|
.uk-alert-close {
|
||||||
color: black !important;
|
color: black !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uk-card-body {
|
.uk-card-body {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uk-card-media-top {
|
.uk-card-media-top {
|
||||||
height: 250px;
|
height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 600px) {
|
@media (min-width: 600px) {
|
||||||
.uk-card-media-top {
|
.uk-card-media-top {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uk-card-media-top>img {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.uk-card-media-top > img {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
.uk-card-title {
|
.uk-card-title {
|
||||||
height: 3em;
|
max-height: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.acard:hover {
|
.acard:hover {
|
||||||
text-decoration: none;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uk-list li {
|
.uk-list li {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reader-bg {
|
.reader-bg {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
#scan-status {
|
#scan-status {
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.break-word {
|
.break-word {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
.uk-logo > img {
|
|
||||||
height: 90px;
|
.uk-logo>img {
|
||||||
width: 90px;
|
height: 90px;
|
||||||
|
width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uk-search {
|
.uk-search {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#selectable .ui-selecting {
|
#selectable .ui-selecting {
|
||||||
background: #EEE6B9;
|
background: #EEE6B9;
|
||||||
}
|
}
|
||||||
|
|
||||||
#selectable .ui-selected {
|
#selectable .ui-selected {
|
||||||
background: #F4E487;
|
background: #F4E487;
|
||||||
}
|
}
|
||||||
|
|
||||||
#selectable .ui-selecting.dark {
|
#selectable .ui-selecting.dark {
|
||||||
background: #5E5731;
|
background: #5E5731;
|
||||||
}
|
}
|
||||||
|
|
||||||
#selectable .ui-selected.dark {
|
#selectable .ui-selected.dark {
|
||||||
background: #9D9252;
|
background: #9D9252;
|
||||||
}
|
}
|
||||||
td > .uk-dropdown {
|
|
||||||
white-space: pre-line;
|
td>.uk-dropdown {
|
||||||
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
#edit-modal .uk-grid > div {
|
|
||||||
height: 300px;
|
#edit-modal .uk-grid>div {
|
||||||
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edit-modal #cover {
|
#edit-modal #cover {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edit-modal #cover-upload {
|
#edit-modal #cover-upload {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edit-modal .uk-modal-body .uk-inline {
|
#edit-modal .uk-modal-body .uk-inline {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item .uk-card-title {
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
@import "node_modules/uikit/src/less/uikit.theme.less";
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: @label-padding-vertical @label-padding-horizontal;
|
||||||
|
background: @label-background;
|
||||||
|
line-height: @label-line-height;
|
||||||
|
font-size: @label-font-size;
|
||||||
|
color: @label-color;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
.hook-label;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-success {
|
||||||
|
background-color: @label-success-background;
|
||||||
|
color: @label-success-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-warning {
|
||||||
|
background-color: @label-warning-background;
|
||||||
|
color: @label-warning-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-danger {
|
||||||
|
background-color: @label-danger-background;
|
||||||
|
color: @label-danger-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-pending {
|
||||||
|
background-color: @global-secondary-background;
|
||||||
|
color: @global-inverse-color;
|
||||||
|
}
|
||||||
+2
-3
@@ -1,13 +1,12 @@
|
|||||||
const truncate = () => {
|
const truncate = () => {
|
||||||
$('.acard .uk-card-title').each((i, e) => {
|
$('.uk-card-title').each((i, e) => {
|
||||||
$(e).dotdotdot({
|
$(e).dotdotdot({
|
||||||
truncate: 'letter',
|
truncate: 'letter',
|
||||||
watch: true,
|
watch: true,
|
||||||
callback: (truncated) => {
|
callback: (truncated) => {
|
||||||
if (truncated) {
|
if (truncated) {
|
||||||
$(e).attr('uk-tooltip', $(e).attr('data-title'));
|
$(e).attr('uk-tooltip', $(e).attr('data-title'));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$(e).removeAttr('uk-tooltip');
|
$(e).removeAttr('uk-tooltip');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,44 +24,48 @@ const loadConfig = () => {
|
|||||||
const remove = (id) => {
|
const remove = (id) => {
|
||||||
var url = base_url + 'api/admin/mangadex/queue/delete';
|
var url = base_url + 'api/admin/mangadex/queue/delete';
|
||||||
if (id !== undefined)
|
if (id !== undefined)
|
||||||
url += '?' + $.param({id: id});
|
url += '?' + $.param({
|
||||||
|
id: id
|
||||||
|
});
|
||||||
console.log(url);
|
console.log(url);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
url: url,
|
url: url,
|
||||||
dataType: 'json'
|
dataType: 'json'
|
||||||
})
|
})
|
||||||
.done(data => {
|
.done(data => {
|
||||||
if (!data.success && data.error) {
|
if (!data.success && data.error) {
|
||||||
alert('danger', `Failed to remove job from download queue. Error: ${data.error}`);
|
alert('danger', `Failed to remove job from download queue. Error: ${data.error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
load();
|
load();
|
||||||
})
|
})
|
||||||
.fail((jqXHR, status) => {
|
.fail((jqXHR, status) => {
|
||||||
alert('danger', `Failed to remove job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
alert('danger', `Failed to remove job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const refresh = (id) => {
|
const refresh = (id) => {
|
||||||
var url = base_url + 'api/admin/mangadex/queue/retry';
|
var url = base_url + 'api/admin/mangadex/queue/retry';
|
||||||
if (id !== undefined)
|
if (id !== undefined)
|
||||||
url += '?' + $.param({id: id});
|
url += '?' + $.param({
|
||||||
|
id: id
|
||||||
|
});
|
||||||
console.log(url);
|
console.log(url);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
url: url,
|
url: url,
|
||||||
dataType: 'json'
|
dataType: 'json'
|
||||||
})
|
})
|
||||||
.done(data => {
|
.done(data => {
|
||||||
if (!data.success && data.error) {
|
if (!data.success && data.error) {
|
||||||
alert('danger', `Failed to restart download job. Error: ${data.error}`);
|
alert('danger', `Failed to restart download job. Error: ${data.error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
load();
|
load();
|
||||||
})
|
})
|
||||||
.fail((jqXHR, status) => {
|
.fail((jqXHR, status) => {
|
||||||
alert('danger', `Failed to restart download job. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
alert('danger', `Failed to restart download job. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
$('#pause-resume-btn').attr('disabled', '');
|
$('#pause-resume-btn').attr('disabled', '');
|
||||||
@@ -69,50 +73,52 @@ const toggle = () => {
|
|||||||
const action = paused ? 'resume' : 'pause';
|
const action = paused ? 'resume' : 'pause';
|
||||||
const url = `${base_url}api/admin/mangadex/queue/${action}`;
|
const url = `${base_url}api/admin/mangadex/queue/${action}`;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
url: url,
|
url: url,
|
||||||
dataType: 'json'
|
dataType: 'json'
|
||||||
})
|
})
|
||||||
.fail((jqXHR, status) => {
|
.fail((jqXHR, status) => {
|
||||||
alert('danger', `Failed to ${action} download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
alert('danger', `Failed to ${action} download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
})
|
})
|
||||||
.always(() => {
|
.always(() => {
|
||||||
load();
|
load();
|
||||||
$('#pause-resume-btn').removeAttr('disabled');
|
$('#pause-resume-btn').removeAttr('disabled');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const load = () => {
|
const load = () => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
console.log('fetching');
|
console.log('fetching');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
url: base_url + 'api/admin/mangadex/queue',
|
url: base_url + 'api/admin/mangadex/queue',
|
||||||
dataType: 'json'
|
dataType: 'json'
|
||||||
})
|
})
|
||||||
.done(data => {
|
.done(data => {
|
||||||
if (!data.success && data.error) {
|
if (!data.success && data.error) {
|
||||||
alert('danger', `Failed to fetch download queue. Error: ${data.error}`);
|
alert('danger', `Failed to fetch download queue. Error: ${data.error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(data);
|
console.log(data);
|
||||||
const btnText = data.paused ? "Resume download" : "Pause download";
|
const btnText = data.paused ? "Resume download" : "Pause download";
|
||||||
$('#pause-resume-btn').text(btnText);
|
$('#pause-resume-btn').text(btnText);
|
||||||
$('#pause-resume-btn').removeAttr('hidden');
|
$('#pause-resume-btn').removeAttr('hidden');
|
||||||
const rows = data.jobs.map(obj => {
|
const rows = data.jobs.map(obj => {
|
||||||
var cls = 'uk-label ';
|
var cls = 'label ';
|
||||||
if (obj.status === 'Completed')
|
if (obj.status === 'Pending')
|
||||||
cls += 'uk-label-success';
|
cls += 'label-pending';
|
||||||
if (obj.status === 'Error')
|
if (obj.status === 'Completed')
|
||||||
cls += 'uk-label-danger';
|
cls += 'label-success';
|
||||||
if (obj.status === 'MissingPages')
|
if (obj.status === 'Error')
|
||||||
cls += 'uk-label-warning';
|
cls += 'label-danger';
|
||||||
|
if (obj.status === 'MissingPages')
|
||||||
|
cls += 'label-warning';
|
||||||
|
|
||||||
const info = obj.status_message.length > 0 ? '<span uk-icon="info"></span>' : '';
|
const info = obj.status_message.length > 0 ? '<span uk-icon="info"></span>' : '';
|
||||||
const statusSpan = `<span class="${cls}">${obj.status} ${info}</span>`;
|
const statusSpan = `<span class="${cls}">${obj.status} ${info}</span>`;
|
||||||
const dropdown = obj.status_message.length > 0 ? `<div uk-dropdown>${obj.status_message}</div>` : '';
|
const dropdown = obj.status_message.length > 0 ? `<div uk-dropdown>${obj.status_message}</div>` : '';
|
||||||
const retryBtn = obj.status_message.length > 0 ? `<a onclick="refresh('${obj.id}')" uk-icon="refresh"></a>` : '';
|
const retryBtn = obj.status_message.length > 0 ? `<a onclick="refresh('${obj.id}')" uk-icon="refresh"></a>` : '';
|
||||||
return `<tr id="chapter-${obj.id}">
|
return `<tr id="chapter-${obj.id}">
|
||||||
<td><a href="${baseURL}/chapter/${obj.id}">${obj.title}</a></td>
|
<td><a href="${baseURL}/chapter/${obj.id}">${obj.title}</a></td>
|
||||||
<td><a href="${baseURL}/manga/${obj.manga_id}">${obj.manga_title}</a></td>
|
<td><a href="${baseURL}/manga/${obj.manga_id}">${obj.manga_title}</a></td>
|
||||||
<td>${obj.success_count}/${obj.pages}</td>
|
<td>${obj.success_count}/${obj.pages}</td>
|
||||||
@@ -123,16 +129,16 @@ const load = () => {
|
|||||||
${retryBtn}
|
${retryBtn}
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const tbody = `<tbody>${rows.join('')}</tbody>`;
|
const tbody = `<tbody>${rows.join('')}</tbody>`;
|
||||||
$('tbody').remove();
|
$('tbody').remove();
|
||||||
$('table').append(tbody);
|
$('table').append(tbody);
|
||||||
})
|
})
|
||||||
.fail((jqXHR, status) => {
|
.fail((jqXHR, status) => {
|
||||||
alert('danger', `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
alert('danger', `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
})
|
})
|
||||||
.always(() => {
|
.always(() => {
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+16
-8
@@ -1,6 +1,10 @@
|
|||||||
$(() => {
|
$(() => {
|
||||||
|
const titleID = $('.data').attr('data-title-id') || 'library';
|
||||||
|
|
||||||
const sortItems = () => {
|
const sortItems = () => {
|
||||||
const sort = $('#sort-select').find(':selected').attr('id');
|
const sort = $('#sort-select').find(':selected').attr('id');
|
||||||
|
localStorage.setItem(`sort-${titleID}`, sort);
|
||||||
|
|
||||||
const ary = sort.split('-');
|
const ary = sort.split('-');
|
||||||
const by = ary[0];
|
const by = ary[0];
|
||||||
const dir = ary[1];
|
const dir = ary[1];
|
||||||
@@ -25,20 +29,21 @@ $(() => {
|
|||||||
|
|
||||||
if (!keyRange[key]) {
|
if (!keyRange[key]) {
|
||||||
keyRange[key] = [num, num, 1];
|
keyRange[key] = [num, num, 1];
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
keyRange[key][2] += 1;
|
keyRange[key][2] += 1;
|
||||||
if (num < keyRange[key][0]) {
|
if (num < keyRange[key][0]) {
|
||||||
keyRange[key][0] = num;
|
keyRange[key][0] = num;
|
||||||
}
|
} else if (num > keyRange[key][1]) {
|
||||||
else if (num > keyRange[key][1]) {
|
|
||||||
keyRange[key][1] = num;
|
keyRange[key][1] = num;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match = regex.exec(name);
|
match = regex.exec(name);
|
||||||
}
|
}
|
||||||
ctxAry.push({index: i, numbers: numbers});
|
ctxAry.push({
|
||||||
|
index: i,
|
||||||
|
numbers: numbers
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(keyRange);
|
console.log(keyRange);
|
||||||
@@ -84,8 +89,7 @@ $(() => {
|
|||||||
if (dir === 'down') {
|
if (dir === 'down') {
|
||||||
items.reverse();
|
items.reverse();
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
items.sort((a, b) => {
|
items.sort((a, b) => {
|
||||||
var res;
|
var res;
|
||||||
if (by === 'name')
|
if (by === 'name')
|
||||||
@@ -108,13 +112,17 @@ $(() => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
$('#item-container').append(items);
|
$('#item-container').append(items);
|
||||||
|
setupAcard();
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#sort-select').change(() => {
|
$('#sort-select').change(() => {
|
||||||
sortItems();
|
sortItems();
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($('option#auto-up').length > 0)
|
const sortID = localStorage.getItem(`sort-${titleID}`);
|
||||||
|
if (sortID)
|
||||||
|
$(`option#${sortID}`).attr('selected', '');
|
||||||
|
else if ($('option#auto-up').length > 0)
|
||||||
$('option#auto-up').attr('selected', '');
|
$('option#auto-up').attr('selected', '');
|
||||||
else
|
else
|
||||||
$('option#name-up').attr('selected', '');
|
$('option#name-up').attr('selected', '');
|
||||||
|
|||||||
+8
-3
@@ -22,8 +22,7 @@ const setTheme = themeStr => {
|
|||||||
$('.uk-card').addClass('uk-card-secondary');
|
$('.uk-card').addClass('uk-card-secondary');
|
||||||
$('.uk-card').removeClass('uk-card-default');
|
$('.uk-card').removeClass('uk-card-default');
|
||||||
$('.ui-widget-content').addClass('dark');
|
$('.ui-widget-content').addClass('dark');
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$('html').css('background', '');
|
$('html').css('background', '');
|
||||||
$('body').removeClass('uk-light');
|
$('body').removeClass('uk-light');
|
||||||
$('.uk-card').removeClass('uk-card-secondary');
|
$('.uk-card').removeClass('uk-card-secondary');
|
||||||
@@ -39,5 +38,11 @@ const styleModal = () => {
|
|||||||
$('.uk-modal-footer').css('background', color);
|
$('.uk-modal-footer').css('background', color);
|
||||||
};
|
};
|
||||||
|
|
||||||
// do it before document is ready to prevent the initial flash of white
|
// do it before document is ready to prevent the initial flash of white on
|
||||||
|
// most pages
|
||||||
setTheme(getTheme());
|
setTheme(getTheme());
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
// hack for the reader page
|
||||||
|
setTheme(getTheme());
|
||||||
|
});
|
||||||
|
|||||||
+27
-7
@@ -1,3 +1,24 @@
|
|||||||
|
$(() => {
|
||||||
|
setupAcard();
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupAcard = () => {
|
||||||
|
$('.acard.is_entry').click((e) => {
|
||||||
|
if ($(e.target).hasClass('no-modal')) return;
|
||||||
|
const card = $(e.target).closest('.acard');
|
||||||
|
|
||||||
|
showModal(
|
||||||
|
$(card).attr('data-encoded-path'),
|
||||||
|
parseInt($(card).attr('data-pages')),
|
||||||
|
parseFloat($(card).attr('data-progress')),
|
||||||
|
$(card).attr('data-encoded-book-title'),
|
||||||
|
$(card).attr('data-encoded-title'),
|
||||||
|
$(card).attr('data-book-id'),
|
||||||
|
$(card).attr('data-id')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTitle, titleID, entryID) {
|
function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTitle, titleID, entryID) {
|
||||||
const zipPath = decodeURIComponent(encodedPath);
|
const zipPath = decodeURIComponent(encodedPath);
|
||||||
const title = decodeURIComponent(encodedeTitle);
|
const title = decodeURIComponent(encodedeTitle);
|
||||||
@@ -8,15 +29,12 @@ function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTi
|
|||||||
if (percentage === 0) {
|
if (percentage === 0) {
|
||||||
$('#continue-btn').attr('hidden', '');
|
$('#continue-btn').attr('hidden', '');
|
||||||
$('#unread-btn').attr('hidden', '');
|
$('#unread-btn').attr('hidden', '');
|
||||||
|
} else if (percentage === 100) {
|
||||||
|
$('#read-btn').attr('hidden', '');
|
||||||
|
$('#continue-btn').attr('hidden', '');
|
||||||
} else {
|
} else {
|
||||||
$('#continue-btn').text('Continue from ' + percentage + '%');
|
$('#continue-btn').text('Continue from ' + percentage + '%');
|
||||||
}
|
}
|
||||||
if (percentage === 100) {
|
|
||||||
$('#read-btn').attr('hidden', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#modal-title-link').text(title);
|
|
||||||
$('#modal-title-link').attr('href', `${base_url}book/${titleID}`);
|
|
||||||
|
|
||||||
$('#modal-entry-title').find('span').text(entry);
|
$('#modal-entry-title').find('span').text(entry);
|
||||||
$('#modal-entry-title').next().attr('data-id', titleID);
|
$('#modal-entry-title').next().attr('data-id', titleID);
|
||||||
@@ -35,7 +53,9 @@ function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTi
|
|||||||
updateProgress(titleID, entryID, 0);
|
updateProgress(titleID, entryID, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.uk-modal-title.break-word > a').attr('onclick', `edit("${entryID}")`);
|
$('#modal-edit-btn').attr('onclick', `edit("${entryID}")`);
|
||||||
|
|
||||||
|
$('#modal-download-btn').attr('href', `/opds/download/${titleID}/${entryID}`);
|
||||||
|
|
||||||
UIkit.modal($('#modal')).show();
|
UIkit.modal($('#modal')).show();
|
||||||
styleModal();
|
styleModal();
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
+1
-1
@@ -6,7 +6,7 @@ shards:
|
|||||||
|
|
||||||
archive:
|
archive:
|
||||||
github: hkalexling/archive.cr
|
github: hkalexling/archive.cr
|
||||||
version: 0.2.0
|
version: 0.3.0
|
||||||
|
|
||||||
baked_file_system:
|
baked_file_system:
|
||||||
github: schovi/baked_file_system
|
github: schovi/baked_file_system
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: mango
|
name: mango
|
||||||
version: 0.6.0
|
version: 0.7.3
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Alex Ling <hkalexling@gmail.com>
|
- Alex Ling <hkalexling@gmail.com>
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ class AuthHandler < Kemal::Handler
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_auth(env)
|
def handle_auth(env)
|
||||||
return call_next(env) if request_path_startswith env, ["/login", "/logout"]
|
if request_path_startswith(env, ["/login", "/logout"]) ||
|
||||||
|
requesting_static_file env
|
||||||
|
return call_next(env)
|
||||||
|
end
|
||||||
|
|
||||||
unless validate_token env
|
unless validate_token env
|
||||||
env.session.string "callback", env.request.path
|
env.session.string "callback", env.request.path
|
||||||
|
|||||||
@@ -16,10 +16,8 @@ class FS
|
|||||||
end
|
end
|
||||||
|
|
||||||
class StaticHandler < Kemal::Handler
|
class StaticHandler < Kemal::Handler
|
||||||
@dirs = ["/css", "/js", "/img", "/favicon.ico"]
|
|
||||||
|
|
||||||
def call(env)
|
def call(env)
|
||||||
if request_path_startswith env, @dirs
|
if requesting_static_file env
|
||||||
file = FS.get? env.request.path
|
file = FS.get? env.request.path
|
||||||
return call_next env if file.nil?
|
return call_next env if file.nil?
|
||||||
|
|
||||||
|
|||||||
+254
-155
@@ -4,6 +4,8 @@ require "uri"
|
|||||||
require "./util"
|
require "./util"
|
||||||
require "./archive"
|
require "./archive"
|
||||||
|
|
||||||
|
SUPPORTED_IMG_TYPES = ["image/jpeg", "image/png", "image/webp"]
|
||||||
|
|
||||||
struct Image
|
struct Image
|
||||||
property data : Bytes
|
property data : Bytes
|
||||||
property mime : String
|
property mime : String
|
||||||
@@ -17,8 +19,7 @@ end
|
|||||||
class Entry
|
class Entry
|
||||||
property zip_path : String, book : Title, title : String,
|
property zip_path : String, book : Title, title : String,
|
||||||
size : String, pages : Int32, id : String, title_id : String,
|
size : String, pages : Int32, id : String, title_id : String,
|
||||||
encoded_path : String, encoded_title : String, mtime : Time,
|
encoded_path : String, encoded_title : String, mtime : Time
|
||||||
date_added : Time
|
|
||||||
|
|
||||||
def initialize(path, @book, @title_id, storage)
|
def initialize(path, @book, @title_id, storage)
|
||||||
@zip_path = path
|
@zip_path = path
|
||||||
@@ -28,13 +29,21 @@ class Entry
|
|||||||
@size = (File.size path).humanize_bytes
|
@size = (File.size path).humanize_bytes
|
||||||
file = ArchiveFile.new path
|
file = ArchiveFile.new path
|
||||||
@pages = file.entries.count do |e|
|
@pages = file.entries.count do |e|
|
||||||
["image/jpeg", "image/png"].includes? \
|
SUPPORTED_IMG_TYPES.includes? \
|
||||||
MIME.from_filename? e.filename
|
MIME.from_filename? e.filename
|
||||||
end
|
end
|
||||||
file.close
|
file.close
|
||||||
@id = storage.get_id @zip_path, false
|
id = storage.get_id @zip_path, false
|
||||||
|
if id.nil?
|
||||||
|
id = random_str
|
||||||
|
storage.insert_id({
|
||||||
|
path: @zip_path,
|
||||||
|
id: id,
|
||||||
|
is_title: false,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
@id = id
|
||||||
@mtime = File.info(@zip_path).modification_time
|
@mtime = File.info(@zip_path).modification_time
|
||||||
@date_added = load_date_added
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_json(json : JSON::Builder)
|
def to_json(json : JSON::Builder)
|
||||||
@@ -74,7 +83,7 @@ class Entry
|
|||||||
ArchiveFile.open @zip_path do |file|
|
ArchiveFile.open @zip_path do |file|
|
||||||
page = file.entries
|
page = file.entries
|
||||||
.select { |e|
|
.select { |e|
|
||||||
["image/jpeg", "image/png"].includes? \
|
SUPPORTED_IMG_TYPES.includes? \
|
||||||
MIME.from_filename? e.filename
|
MIME.from_filename? e.filename
|
||||||
}
|
}
|
||||||
.sort { |a, b|
|
.sort { |a, b|
|
||||||
@@ -90,7 +99,19 @@ class Entry
|
|||||||
img
|
img
|
||||||
end
|
end
|
||||||
|
|
||||||
private def load_date_added
|
def next_entry
|
||||||
|
idx = @book.entries.index self
|
||||||
|
return nil if idx.nil? || idx == @book.entries.size - 1
|
||||||
|
@book.entries[idx + 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def previous_entry
|
||||||
|
idx = @book.entries.index self
|
||||||
|
return nil if idx.nil? || idx == 0
|
||||||
|
@book.entries[idx - 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def date_added
|
||||||
date_added = nil
|
date_added = nil
|
||||||
TitleInfo.new @book.dir do |info|
|
TitleInfo.new @book.dir do |info|
|
||||||
info_da = info.date_added[@title]?
|
info_da = info.date_added[@title]?
|
||||||
@@ -103,6 +124,60 @@ class Entry
|
|||||||
end
|
end
|
||||||
date_added.not_nil! # is it ok to set not_nil! here?
|
date_added.not_nil! # is it ok to set not_nil! here?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# For backward backward compatibility with v0.1.0, we save entry titles
|
||||||
|
# instead of IDs in info.json
|
||||||
|
def save_progress(username, page)
|
||||||
|
TitleInfo.new @book.dir do |info|
|
||||||
|
if info.progress[username]?.nil?
|
||||||
|
info.progress[username] = {@title => page}
|
||||||
|
else
|
||||||
|
info.progress[username][@title] = page
|
||||||
|
end
|
||||||
|
# save last_read timestamp
|
||||||
|
if info.last_read[username]?.nil?
|
||||||
|
info.last_read[username] = {@title => Time.utc}
|
||||||
|
else
|
||||||
|
info.last_read[username][@title] = Time.utc
|
||||||
|
end
|
||||||
|
info.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_progress(username)
|
||||||
|
progress = 0
|
||||||
|
TitleInfo.new @book.dir do |info|
|
||||||
|
unless info.progress[username]?.nil? ||
|
||||||
|
info.progress[username][@title]?.nil?
|
||||||
|
progress = info.progress[username][@title]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
[progress, @pages].min
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_percentage(username)
|
||||||
|
page = load_progress username
|
||||||
|
page / @pages
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_last_read(username)
|
||||||
|
last_read = nil
|
||||||
|
TitleInfo.new @book.dir do |info|
|
||||||
|
unless info.last_read[username]?.nil? ||
|
||||||
|
info.last_read[username][@title]?.nil?
|
||||||
|
last_read = info.last_read[username][@title]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
last_read
|
||||||
|
end
|
||||||
|
|
||||||
|
def finished?(username)
|
||||||
|
load_progress(username) == @pages
|
||||||
|
end
|
||||||
|
|
||||||
|
def started?(username)
|
||||||
|
load_progress(username) > 0
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Title
|
class Title
|
||||||
@@ -112,7 +187,16 @@ class Title
|
|||||||
|
|
||||||
def initialize(@dir : String, @parent_id, storage,
|
def initialize(@dir : String, @parent_id, storage,
|
||||||
@library : Library)
|
@library : Library)
|
||||||
@id = storage.get_id @dir, true
|
id = storage.get_id @dir, true
|
||||||
|
if id.nil?
|
||||||
|
id = random_str
|
||||||
|
storage.insert_id({
|
||||||
|
path: @dir,
|
||||||
|
id: id,
|
||||||
|
is_title: true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
@id = id
|
||||||
@title = File.basename dir
|
@title = File.basename dir
|
||||||
@encoded_title = URI.encode @title
|
@encoded_title = URI.encode @title
|
||||||
@title_ids = [] of String
|
@title_ids = [] of String
|
||||||
@@ -191,6 +275,17 @@ class Title
|
|||||||
@title_ids.map { |tid| @library.get_title! tid }
|
@title_ids.map { |tid| @library.get_title! tid }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Get all entries, including entries in nested titles
|
||||||
|
def deep_entries
|
||||||
|
return @entries if title_ids.empty?
|
||||||
|
@entries + titles.map { |t| t.deep_entries }.flatten
|
||||||
|
end
|
||||||
|
|
||||||
|
def deep_titles
|
||||||
|
return [] of Title if titles.empty?
|
||||||
|
titles + titles.map { |t| t.deep_titles }.flatten
|
||||||
|
end
|
||||||
|
|
||||||
def parents
|
def parents
|
||||||
ary = [] of Title
|
ary = [] of Title
|
||||||
tid = @parent_id
|
tid = @parent_id
|
||||||
@@ -199,7 +294,7 @@ class Title
|
|||||||
ary << title
|
ary << title
|
||||||
tid = title.parent_id
|
tid = title.parent_id
|
||||||
end
|
end
|
||||||
ary
|
ary.reverse
|
||||||
end
|
end
|
||||||
|
|
||||||
def size
|
def size
|
||||||
@@ -279,7 +374,7 @@ class Title
|
|||||||
# Set the reading progress of all entries and nested libraries to 100%
|
# Set the reading progress of all entries and nested libraries to 100%
|
||||||
def read_all(username)
|
def read_all(username)
|
||||||
@entries.each do |e|
|
@entries.each do |e|
|
||||||
save_progress username, e.title, e.pages
|
e.save_progress username, e.pages
|
||||||
end
|
end
|
||||||
titles.each do |t|
|
titles.each do |t|
|
||||||
t.read_all username
|
t.read_all username
|
||||||
@@ -289,81 +384,25 @@ class Title
|
|||||||
# Set the reading progress of all entries and nested libraries to 0%
|
# Set the reading progress of all entries and nested libraries to 0%
|
||||||
def unread_all(username)
|
def unread_all(username)
|
||||||
@entries.each do |e|
|
@entries.each do |e|
|
||||||
save_progress username, e.title, 0
|
e.save_progress username, 0
|
||||||
end
|
end
|
||||||
titles.each do |t|
|
titles.each do |t|
|
||||||
t.unread_all username
|
t.unread_all username
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# For backward backward compatibility with v0.1.0, we save entry titles
|
def deep_read_page_count(username) : Int32
|
||||||
# instead of IDs in info.json
|
load_progress_for_all_entries(username).sum +
|
||||||
def save_progress(username, entry, page)
|
titles.map { |t| t.deep_read_page_count username }.flatten.sum
|
||||||
TitleInfo.new @dir do |info|
|
|
||||||
if info.progress[username]?.nil?
|
|
||||||
info.progress[username] = {entry => page}
|
|
||||||
else
|
|
||||||
info.progress[username][entry] = page
|
|
||||||
end
|
|
||||||
# save last_read timestamp
|
|
||||||
if info.last_read[username]?.nil?
|
|
||||||
info.last_read[username] = {entry => Time.utc}
|
|
||||||
else
|
|
||||||
info.last_read[username][entry] = Time.utc
|
|
||||||
end
|
|
||||||
info.save
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_progress(username, entry)
|
def deep_total_page_count : Int32
|
||||||
progress = 0
|
entries.map { |e| e.pages }.sum +
|
||||||
TitleInfo.new @dir do |info|
|
titles.map { |t| t.deep_total_page_count }.flatten.sum
|
||||||
unless info.progress[username]?.nil? ||
|
|
||||||
info.progress[username][entry]?.nil?
|
|
||||||
progress = info.progress[username][entry]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
progress
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_percentage(username, entry)
|
|
||||||
page = load_progress username, entry
|
|
||||||
entry_obj = @entries.find { |e| e.title == entry }
|
|
||||||
return 0.0 if entry_obj.nil?
|
|
||||||
page / entry_obj.pages
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_percentage(username)
|
def load_percentage(username)
|
||||||
return 0.0 if @entries.empty?
|
deep_read_page_count(username) / deep_total_page_count
|
||||||
read_pages = total_pages = 0
|
|
||||||
@entries.each do |e|
|
|
||||||
read_pages += load_progress username, e.title
|
|
||||||
total_pages += e.pages
|
|
||||||
end
|
|
||||||
read_pages / total_pages
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_last_read(username, entry)
|
|
||||||
last_read = nil
|
|
||||||
TitleInfo.new @dir do |info|
|
|
||||||
unless info.last_read[username]?.nil? ||
|
|
||||||
info.last_read[username][entry]?.nil?
|
|
||||||
last_read = info.last_read[username][entry]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
last_read
|
|
||||||
end
|
|
||||||
|
|
||||||
def next_entry(current_entry_obj)
|
|
||||||
idx = @entries.index current_entry_obj
|
|
||||||
return nil if idx.nil? || idx == @entries.size - 1
|
|
||||||
@entries[idx + 1]
|
|
||||||
end
|
|
||||||
|
|
||||||
def previous_entry(current_entry_obj)
|
|
||||||
idx = @entries.index current_entry_obj
|
|
||||||
return nil if idx.nil? || idx == 0
|
|
||||||
@entries[idx - 1]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_continue_reading_entry(username)
|
def get_continue_reading_entry(username)
|
||||||
@@ -381,15 +420,83 @@ class Title
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: More concise title?
|
def load_progress_for_all_entries(username)
|
||||||
def get_last_read_for_continue_reading(username, entry_obj)
|
progress = {} of String => Int32
|
||||||
last_read = load_last_read username, entry_obj.title
|
TitleInfo.new @dir do |info|
|
||||||
# grab from previous entry if current entry hasn't been started yet
|
progress = info.progress[username]?
|
||||||
if last_read.nil?
|
|
||||||
previous_entry = previous_entry(entry_obj)
|
|
||||||
return load_last_read username, previous_entry.title if previous_entry
|
|
||||||
end
|
end
|
||||||
last_read
|
|
||||||
|
@entries.map do |e|
|
||||||
|
info_progress = 0
|
||||||
|
if progress && progress.has_key? e.title
|
||||||
|
info_progress = [progress[e.title], e.pages].min
|
||||||
|
end
|
||||||
|
info_progress
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_percentage_for_all_entries(username)
|
||||||
|
progress = load_progress_for_all_entries username
|
||||||
|
@entries.map_with_index do |e, i|
|
||||||
|
progress[i] / e.pages
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# === helper methods ===
|
||||||
|
|
||||||
|
# Gets the last read entry in the title. If the entry has been completed,
|
||||||
|
# returns the next entry. Returns nil when no entry has been read yet,
|
||||||
|
# or when all entries are completed
|
||||||
|
def get_last_read_entry(username) : Entry?
|
||||||
|
progress = {} of String => Int32
|
||||||
|
TitleInfo.new @dir do |info|
|
||||||
|
progress = info.progress[username]?
|
||||||
|
end
|
||||||
|
return if progress.nil?
|
||||||
|
|
||||||
|
last_read_entry = nil
|
||||||
|
|
||||||
|
@entries.reverse_each do |e|
|
||||||
|
if progress.has_key?(e.title) && progress[e.title] > 0
|
||||||
|
last_read_entry = e
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if last_read_entry && last_read_entry.finished? username
|
||||||
|
last_read_entry = last_read_entry.next_entry
|
||||||
|
end
|
||||||
|
|
||||||
|
last_read_entry
|
||||||
|
end
|
||||||
|
|
||||||
|
# Equivalent to `@entries.map &. date_added`, but much more efficient
|
||||||
|
def get_date_added_for_all_entries
|
||||||
|
da = {} of String => Time
|
||||||
|
TitleInfo.new @dir do |info|
|
||||||
|
da = info.date_added
|
||||||
|
end
|
||||||
|
|
||||||
|
@entries.each do |e|
|
||||||
|
next if da.has_key? e.title
|
||||||
|
da[e.title] = ctime e.zip_path
|
||||||
|
end
|
||||||
|
|
||||||
|
TitleInfo.new @dir do |info|
|
||||||
|
info.date_added = da
|
||||||
|
info.save
|
||||||
|
end
|
||||||
|
|
||||||
|
@entries.map { |e| da[e.title] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def deep_entries_with_date_added
|
||||||
|
da_ary = get_date_added_for_all_entries
|
||||||
|
zip = @entries.map_with_index do |e, i|
|
||||||
|
{entry: e, date_added: da_ary[i]}
|
||||||
|
end
|
||||||
|
return zip if title_ids.empty?
|
||||||
|
zip + titles.map { |t| t.deep_entries_with_date_added }.flatten
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -436,7 +543,7 @@ end
|
|||||||
|
|
||||||
class Library
|
class Library
|
||||||
property dir : String, title_ids : Array(String), scan_interval : Int32,
|
property dir : String, title_ids : Array(String), scan_interval : Int32,
|
||||||
storage : Storage, title_hash : Hash(String, Title)
|
title_hash : Hash(String, Title)
|
||||||
|
|
||||||
def self.default : self
|
def self.default : self
|
||||||
unless @@default
|
unless @@default
|
||||||
@@ -446,7 +553,8 @@ class Library
|
|||||||
end
|
end
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@storage = Storage.default
|
register_mime_types
|
||||||
|
|
||||||
@dir = Config.current.library_path
|
@dir = Config.current.library_path
|
||||||
@scan_interval = Config.current.scan_interval
|
@scan_interval = Config.current.scan_interval
|
||||||
# explicitly initialize @titles to bypass the compiler check. it will
|
# explicitly initialize @titles to bypass the compiler check. it will
|
||||||
@@ -470,6 +578,10 @@ class Library
|
|||||||
@title_ids.map { |tid| self.get_title!(tid) }
|
@title_ids.map { |tid| self.get_title!(tid) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def deep_titles
|
||||||
|
titles + titles.map { |t| t.deep_titles }.flatten
|
||||||
|
end
|
||||||
|
|
||||||
def to_json(json : JSON::Builder)
|
def to_json(json : JSON::Builder)
|
||||||
json.object do
|
json.object do
|
||||||
json.field "dir", @dir
|
json.field "dir", @dir
|
||||||
@@ -494,104 +606,91 @@ class Library
|
|||||||
Dir.mkdir_p @dir
|
Dir.mkdir_p @dir
|
||||||
end
|
end
|
||||||
@title_ids.clear
|
@title_ids.clear
|
||||||
|
|
||||||
|
storage = Storage.new auto_close: false
|
||||||
|
|
||||||
(Dir.entries @dir)
|
(Dir.entries @dir)
|
||||||
.select { |fn| !fn.starts_with? "." }
|
.select { |fn| !fn.starts_with? "." }
|
||||||
.map { |fn| File.join @dir, fn }
|
.map { |fn| File.join @dir, fn }
|
||||||
.select { |path| File.directory? path }
|
.select { |path| File.directory? path }
|
||||||
.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 }
|
||||||
.each do |title|
|
.each do |title|
|
||||||
@title_hash[title.id] = title
|
@title_hash[title.id] = title
|
||||||
@title_ids << title.id
|
@title_ids << title.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
storage.bulk_insert_ids
|
||||||
|
storage.close
|
||||||
|
|
||||||
Logger.debug "Scan completed"
|
Logger.debug "Scan completed"
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_continue_reading_entries(username)
|
def get_continue_reading_entries(username)
|
||||||
# map: get the continue-reading entry or nil for each Title
|
cr_entries = deep_titles
|
||||||
# select: select only entries (and ignore Nil's) from the array
|
.map { |t| t.get_last_read_entry username }
|
||||||
# produced by map
|
# Select elements with type `Entry` from the array and ignore all `Nil`s
|
||||||
continue_reading_entries = titles.map { |t|
|
.select(Entry)[0..11]
|
||||||
get_continue_reading_entry username, t
|
.map { |e|
|
||||||
}.select Entry
|
# Get the last read time of the entry. If it hasn't been started, get
|
||||||
|
# the last read time of the previous entry
|
||||||
continue_reading = continue_reading_entries.map { |e|
|
last_read = e.load_last_read username
|
||||||
{
|
pe = e.previous_entry
|
||||||
entry: e,
|
if last_read.nil? && pe
|
||||||
percentage: e.book.load_percentage(username, e.title),
|
last_read = pe.load_last_read username
|
||||||
last_read: get_relevant_last_read(username, e),
|
end
|
||||||
|
{
|
||||||
|
entry: e,
|
||||||
|
percentage: e.load_percentage(username),
|
||||||
|
last_read: last_read,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
# Sort by by last_read, most recent first (nils at the end)
|
# Sort by by last_read, most recent first (nils at the end)
|
||||||
continue_reading.sort! { |a, b|
|
cr_entries.sort { |a, b|
|
||||||
next 0 if a[:last_read].nil? && b[:last_read].nil?
|
next 0 if a[:last_read].nil? && b[:last_read].nil?
|
||||||
next 1 if a[:last_read].nil?
|
next 1 if a[:last_read].nil?
|
||||||
next -1 if b[:last_read].nil?
|
next -1 if b[:last_read].nil?
|
||||||
b[:last_read].not_nil! <=> a[:last_read].not_nil!
|
b[:last_read].not_nil! <=> a[:last_read].not_nil!
|
||||||
}[0..11]
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
alias RA = NamedTuple(
|
||||||
|
entry: Entry,
|
||||||
|
percentage: Float64,
|
||||||
|
grouped_count: Int32)
|
||||||
|
|
||||||
def get_recently_added_entries(username)
|
def get_recently_added_entries(username)
|
||||||
# Get all entries added within the last three months
|
recently_added = [] of RA
|
||||||
entries = titles.map { |t| t.entries }
|
last_date_added = nil
|
||||||
.flatten
|
|
||||||
.select { |e| e.date_added > 3.months.ago }
|
|
||||||
|
|
||||||
# Group entries in a Hash by title ID
|
titles.map { |t| t.deep_entries_with_date_added }.flatten
|
||||||
grouped_entries = {} of String => Array(Entry)
|
.select { |e| e[:date_added] > 1.month.ago }
|
||||||
entries.each do |e|
|
.sort { |a, b| b[:date_added] <=> a[:date_added] }
|
||||||
if grouped_entries.has_key? e.title_id
|
.each do |e|
|
||||||
grouped_entries[e.title_id].push e
|
break if recently_added.size > 12
|
||||||
else
|
last = recently_added.last?
|
||||||
grouped_entries[e.title_id] = [e]
|
if last && e[:entry].title_id == last[:entry].title_id &&
|
||||||
|
(e[:date_added] - last_date_added.not_nil!).duration < 1.day
|
||||||
|
# A NamedTuple is immutable, so we have to cast it to a Hash first
|
||||||
|
last_hash = last.to_h
|
||||||
|
count = last_hash[:grouped_count].as(Int32)
|
||||||
|
last_hash[:grouped_count] = count + 1
|
||||||
|
# Setting the percentage to a negative value will hide the
|
||||||
|
# percentage badge on the card
|
||||||
|
last_hash[:percentage] = -1.0
|
||||||
|
recently_added[recently_added.size - 1] = RA.from last_hash
|
||||||
|
else
|
||||||
|
last_date_added = e[:date_added]
|
||||||
|
recently_added << {
|
||||||
|
entry: e[:entry],
|
||||||
|
percentage: e[:entry].load_percentage(username),
|
||||||
|
grouped_count: 1,
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
# Cast the Hash to an Array of Tuples and sort it by date_added
|
|
||||||
grouped_ary = grouped_entries.to_a.sort do |a, b|
|
|
||||||
date_added_a = a[1].map { |e| e.date_added }.max
|
|
||||||
date_added_b = b[1].map { |e| e.date_added }.max
|
|
||||||
date_added_b <=> date_added_a
|
|
||||||
end
|
|
||||||
|
|
||||||
recently_added = grouped_ary.map do |_, ary|
|
|
||||||
# Get the most recently added entry in the group
|
|
||||||
entry = ary.sort { |a, b| a.date_added <=> b.date_added }.last
|
|
||||||
{
|
|
||||||
entry: entry,
|
|
||||||
percentage: entry.book.load_percentage(username, entry.title),
|
|
||||||
grouped_count: ary.size,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
recently_added[0..11]
|
recently_added[0..11]
|
||||||
end
|
end
|
||||||
|
|
||||||
private def get_continue_reading_entry(username, title)
|
|
||||||
in_progress_entries = title.entries.select do |e|
|
|
||||||
title.load_progress(username, e.title) > 0
|
|
||||||
end
|
|
||||||
return nil if in_progress_entries.empty?
|
|
||||||
|
|
||||||
latest_read_entry = in_progress_entries[-1]
|
|
||||||
if title.load_progress(username, latest_read_entry.title) ==
|
|
||||||
latest_read_entry.pages
|
|
||||||
title.next_entry latest_read_entry
|
|
||||||
else
|
|
||||||
latest_read_entry
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private def get_relevant_last_read(username, entry_obj)
|
|
||||||
last_read = entry_obj.book.load_last_read username, entry_obj.title
|
|
||||||
# grab from previous entry if current entry hasn't been started yet
|
|
||||||
if last_read.nil?
|
|
||||||
previous_entry = entry_obj.book.previous_entry(entry_obj)
|
|
||||||
return entry_obj.book.load_last_read username, previous_entry.title \
|
|
||||||
if previous_entry
|
|
||||||
end
|
|
||||||
last_read
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ require "./mangadex/*"
|
|||||||
require "option_parser"
|
require "option_parser"
|
||||||
require "clim"
|
require "clim"
|
||||||
|
|
||||||
MANGO_VERSION = "0.6.0"
|
MANGO_VERSION = "0.7.3"
|
||||||
|
|
||||||
macro common_option
|
macro common_option
|
||||||
option "-c PATH", "--config=PATH", type: String,
|
option "-c PATH", "--config=PATH", type: String,
|
||||||
|
|||||||
+2
-2
@@ -80,7 +80,7 @@ class APIRouter < Router
|
|||||||
if !entry_id.nil?
|
if !entry_id.nil?
|
||||||
entry = title.get_entry(entry_id).not_nil!
|
entry = title.get_entry(entry_id).not_nil!
|
||||||
raise "incorrect page value" if page < 0 || page > entry.pages
|
raise "incorrect page value" if page < 0 || page > entry.pages
|
||||||
title.save_progress username, entry.title, page
|
entry.save_progress username, page
|
||||||
elsif page == 0
|
elsif page == 0
|
||||||
title.unread_all username
|
title.unread_all username
|
||||||
else
|
else
|
||||||
@@ -224,7 +224,7 @@ class APIRouter < Router
|
|||||||
entry_id = env.params.query["entry"]?
|
entry_id = env.params.query["entry"]?
|
||||||
title = @context.library.get_title(title_id).not_nil!
|
title = @context.library.get_title(title_id).not_nil!
|
||||||
|
|
||||||
unless ["image/jpeg", "image/png"].includes? \
|
unless SUPPORTED_IMG_TYPES.includes? \
|
||||||
MIME.from_filename? filename
|
MIME.from_filename? filename
|
||||||
raise "The uploaded image must be either JPEG or PNG"
|
raise "The uploaded image must be either JPEG or PNG"
|
||||||
end
|
end
|
||||||
|
|||||||
+3
-4
@@ -4,7 +4,7 @@ class MainRouter < Router
|
|||||||
def initialize
|
def initialize
|
||||||
get "/login" do |env|
|
get "/login" do |env|
|
||||||
base_url = Config.current.base_url
|
base_url = Config.current.base_url
|
||||||
render "src/views/login.ecr"
|
render "src/views/login.html.ecr"
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/logout" do |env|
|
get "/logout" do |env|
|
||||||
@@ -53,9 +53,8 @@ class MainRouter < Router
|
|||||||
begin
|
begin
|
||||||
title = (@context.library.get_title env.params.url["title"]).not_nil!
|
title = (@context.library.get_title env.params.url["title"]).not_nil!
|
||||||
username = get_username env
|
username = get_username env
|
||||||
percentage = title.entries.map { |e|
|
percentage = title.load_percentage_for_all_entries username
|
||||||
title.load_percentage username, e.title
|
title_percentage = title.titles.map &.load_percentage username
|
||||||
}
|
|
||||||
layout "title"
|
layout "title"
|
||||||
rescue e
|
rescue e
|
||||||
@context.error e
|
@context.error e
|
||||||
|
|||||||
+2
-2
@@ -4,13 +4,13 @@ class OPDSRouter < Router
|
|||||||
def initialize
|
def initialize
|
||||||
get "/opds" do |env|
|
get "/opds" do |env|
|
||||||
titles = @context.library.titles
|
titles = @context.library.titles
|
||||||
render_xml "src/views/opds/index.ecr"
|
render_xml "src/views/opds/index.xml.ecr"
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/opds/book/:title_id" do |env|
|
get "/opds/book/:title_id" do |env|
|
||||||
begin
|
begin
|
||||||
title = @context.library.get_title(env.params.url["title_id"]).not_nil!
|
title = @context.library.get_title(env.params.url["title_id"]).not_nil!
|
||||||
render_xml "src/views/opds/title.ecr"
|
render_xml "src/views/opds/title.xml.ecr"
|
||||||
rescue e
|
rescue e
|
||||||
@context.error e
|
@context.error e
|
||||||
env.response.status_code = 404
|
env.response.status_code = 404
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ class ReaderRouter < Router
|
|||||||
|
|
||||||
# load progress
|
# load progress
|
||||||
username = get_username env
|
username = get_username env
|
||||||
page = title.load_progress username, entry.title
|
page = entry.load_progress username
|
||||||
# we go back 2 * `IMGS_PER_PAGE` pages. the infinite scroll
|
# we go back 2 * `IMGS_PER_PAGE` pages. the infinite scroll
|
||||||
# library perloads a few pages in advance, and the user
|
# library perloads a few pages in advance, and the user
|
||||||
# might not have actually read them
|
# might not have actually read them
|
||||||
page = [page - 2 * IMGS_PER_PAGE, 1].max
|
page = [page - 2 * IMGS_PER_PAGE, 1].max
|
||||||
|
|
||||||
|
# start from page 1 if the user has finished reading the entry
|
||||||
|
page = 1 if entry.finished? username
|
||||||
|
|
||||||
redirect env, "/reader/#{title.id}/#{entry.id}/#{page}"
|
redirect env, "/reader/#{title.id}/#{entry.id}/#{page}"
|
||||||
rescue e
|
rescue e
|
||||||
@context.error e
|
@context.error e
|
||||||
@@ -33,7 +36,7 @@ class ReaderRouter < Router
|
|||||||
|
|
||||||
# save progress
|
# save progress
|
||||||
username = get_username env
|
username = get_username env
|
||||||
title.save_progress username, entry.title, page
|
entry.save_progress username, page
|
||||||
|
|
||||||
pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min)
|
pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min)
|
||||||
urls = pages.map { |idx|
|
urls = pages.map { |idx|
|
||||||
@@ -45,7 +48,7 @@ class ReaderRouter < Router
|
|||||||
next_page = page + IMGS_PER_PAGE
|
next_page = page + IMGS_PER_PAGE
|
||||||
next_url = next_entry_url = nil
|
next_url = next_entry_url = nil
|
||||||
exit_url = "#{base_url}book/#{title.id}"
|
exit_url = "#{base_url}book/#{title.id}"
|
||||||
next_entry = title.next_entry entry
|
next_entry = entry.next_entry
|
||||||
unless next_page > entry.pages
|
unless next_page > entry.pages
|
||||||
next_url = "#{base_url}reader/#{title.id}/#{entry.id}/#{next_page}"
|
next_url = "#{base_url}reader/#{title.id}/#{entry.id}/#{next_page}"
|
||||||
end
|
end
|
||||||
@@ -53,7 +56,7 @@ class ReaderRouter < Router
|
|||||||
next_entry_url = "#{base_url}reader/#{title.id}/#{next_entry.id}"
|
next_entry_url = "#{base_url}reader/#{title.id}/#{next_entry.id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
render "src/views/reader.ecr"
|
render "src/views/reader.html.ecr"
|
||||||
rescue e
|
rescue e
|
||||||
@context.error e
|
@context.error e
|
||||||
env.response.status_code = 404
|
env.response.status_code = 404
|
||||||
|
|||||||
+55
-17
@@ -14,6 +14,12 @@ end
|
|||||||
|
|
||||||
class Storage
|
class Storage
|
||||||
@path : String
|
@path : String
|
||||||
|
@db : DB::Database?
|
||||||
|
@insert_ids = [] of IDTuple
|
||||||
|
|
||||||
|
alias IDTuple = NamedTuple(path: String,
|
||||||
|
id: String,
|
||||||
|
is_title: Bool)
|
||||||
|
|
||||||
def self.default : self
|
def self.default : self
|
||||||
unless @@default
|
unless @@default
|
||||||
@@ -22,7 +28,8 @@ class Storage
|
|||||||
@@default.not_nil!
|
@@default.not_nil!
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(db_path : String? = nil, init_user = true)
|
def initialize(db_path : String? = nil, init_user = true, *,
|
||||||
|
@auto_close = true)
|
||||||
@path = db_path || Config.current.db_path
|
@path = db_path || Config.current.db_path
|
||||||
dir = File.dirname @path
|
dir = File.dirname @path
|
||||||
unless Dir.exists? dir
|
unless Dir.exists? dir
|
||||||
@@ -60,6 +67,9 @@ class Storage
|
|||||||
init_admin if init_user
|
init_admin if init_user
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
unless @auto_close
|
||||||
|
@db = DB.open "sqlite3://#{@path}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
macro init_admin
|
macro init_admin
|
||||||
@@ -71,8 +81,18 @@ class Storage
|
|||||||
"#{{"username" => "admin", "password" => random_pw}}"
|
"#{{"username" => "admin", "password" => random_pw}}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private def get_db(&block : DB::Database ->)
|
||||||
|
if @db.nil?
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
yield db
|
||||||
|
end
|
||||||
|
else
|
||||||
|
yield @db.not_nil!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def verify_user(username, password)
|
def verify_user(username, password)
|
||||||
DB.open "sqlite3://#{@path}" 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 " \
|
||||||
"users where username = (?)",
|
"users where username = (?)",
|
||||||
@@ -97,7 +117,7 @@ class Storage
|
|||||||
|
|
||||||
def verify_token(token)
|
def verify_token(token)
|
||||||
username = nil
|
username = nil
|
||||||
DB.open "sqlite3://#{@path}" 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 " \
|
||||||
"token = (?)", token, as: String
|
"token = (?)", token, as: String
|
||||||
@@ -110,7 +130,7 @@ class Storage
|
|||||||
|
|
||||||
def verify_admin(token)
|
def verify_admin(token)
|
||||||
is_admin = false
|
is_admin = false
|
||||||
DB.open "sqlite3://#{@path}" 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 " \
|
||||||
"token = (?)", token, as: Bool
|
"token = (?)", token, as: Bool
|
||||||
@@ -123,7 +143,7 @@ class Storage
|
|||||||
|
|
||||||
def list_users
|
def list_users
|
||||||
results = Array(Tuple(String, Bool)).new
|
results = Array(Tuple(String, Bool)).new
|
||||||
DB.open "sqlite3://#{@path}" 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
|
||||||
results << {rs.read(String), rs.read(Bool)}
|
results << {rs.read(String), rs.read(Bool)}
|
||||||
@@ -137,7 +157,7 @@ class Storage
|
|||||||
validate_username username
|
validate_username username
|
||||||
validate_password password
|
validate_password password
|
||||||
admin = (admin ? 1 : 0)
|
admin = (admin ? 1 : 0)
|
||||||
DB.open "sqlite3://#{@path}" 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
|
||||||
@@ -148,7 +168,7 @@ 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?
|
||||||
DB.open "sqlite3://#{@path}" 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 = (?) " \
|
||||||
"where username = (?)",
|
"where username = (?)",
|
||||||
@@ -163,13 +183,13 @@ class Storage
|
|||||||
end
|
end
|
||||||
|
|
||||||
def delete_user(username)
|
def delete_user(username)
|
||||||
DB.open "sqlite3://#{@path}" 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
|
||||||
|
|
||||||
def logout(token)
|
def logout(token)
|
||||||
DB.open "sqlite3://#{@path}" 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
|
||||||
rescue
|
rescue
|
||||||
@@ -178,18 +198,36 @@ class Storage
|
|||||||
end
|
end
|
||||||
|
|
||||||
def get_id(path, is_title)
|
def get_id(path, is_title)
|
||||||
id = random_str
|
id = nil
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
get_db do |db|
|
||||||
begin
|
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}
|
|
||||||
rescue
|
|
||||||
db.exec "insert into ids values (?, ?, ?)", path, id, is_title ? 1 : 0
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
id
|
id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def insert_id(tp : IDTuple)
|
||||||
|
@insert_ids << tp
|
||||||
|
end
|
||||||
|
|
||||||
|
def bulk_insert_ids
|
||||||
|
get_db do |db|
|
||||||
|
db.transaction do |tx|
|
||||||
|
@insert_ids.each do |tp|
|
||||||
|
tx.connection.exec "insert into ids values (?, ?, ?)", tp[:path],
|
||||||
|
tp[:id], tp[:is_title] ? 1 : 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@insert_ids.clear
|
||||||
|
end
|
||||||
|
|
||||||
|
def close
|
||||||
|
unless @db.nil?
|
||||||
|
@db.not_nil!.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def to_json(json : JSON::Builder)
|
def to_json(json : JSON::Builder)
|
||||||
json.string self
|
json.string self
|
||||||
end
|
end
|
||||||
|
|||||||
+19
-5
@@ -2,6 +2,11 @@ require "big"
|
|||||||
|
|
||||||
IMGS_PER_PAGE = 5
|
IMGS_PER_PAGE = 5
|
||||||
UPLOAD_URL_PREFIX = "/uploads"
|
UPLOAD_URL_PREFIX = "/uploads"
|
||||||
|
STATIC_DIRS = ["/css", "/js", "/img", "/favicon.ico"]
|
||||||
|
|
||||||
|
def requesting_static_file(env)
|
||||||
|
request_path_startswith env, STATIC_DIRS
|
||||||
|
end
|
||||||
|
|
||||||
macro layout(name)
|
macro layout(name)
|
||||||
base_url = Config.current.base_url
|
base_url = Config.current.base_url
|
||||||
@@ -11,11 +16,11 @@ macro layout(name)
|
|||||||
is_admin = @context.storage.verify_admin token
|
is_admin = @context.storage.verify_admin token
|
||||||
end
|
end
|
||||||
page = {{name}}
|
page = {{name}}
|
||||||
render "src/views/#{{{name}}}.ecr", "src/views/layout.ecr"
|
render "src/views/#{{{name}}}.html.ecr", "src/views/layout.html.ecr"
|
||||||
rescue e
|
rescue e
|
||||||
message = e.to_s
|
message = e.to_s
|
||||||
@context.error message
|
@context.error message
|
||||||
render "src/views/message.ecr", "src/views/layout.ecr"
|
render "src/views/message.html.ecr", "src/views/layout.html.ecr"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -36,8 +41,6 @@ def send_json(env, json)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def send_attachment(env, path)
|
def send_attachment(env, path)
|
||||||
MIME.register ".cbz", "application/vnd.comicbook+zip"
|
|
||||||
MIME.register ".cbr", "application/vnd.comicbook-rar"
|
|
||||||
send_file env, path, filename: File.basename(path), disposition: "attachment"
|
send_file env, path, filename: File.basename(path), disposition: "attachment"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -134,7 +137,7 @@ macro render_xml(path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
macro render_component(filename)
|
macro render_component(filename)
|
||||||
render "src/views/components/#{{{filename}}}.ecr"
|
render "src/views/components/#{{{filename}}}.html.ecr"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Works in all Unix systems. Follows https://github.com/crystal-lang/crystal/
|
# Works in all Unix systems. Follows https://github.com/crystal-lang/crystal/
|
||||||
@@ -149,3 +152,14 @@ def ctime(file_path : String) : Time
|
|||||||
Time.new stat.st_ctim, Time::Location::UTC
|
Time.new stat.st_ctim, Time::Location::UTC
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def register_mime_types
|
||||||
|
{
|
||||||
|
".zip" => "application/zip",
|
||||||
|
".rar" => "application/x-rar-compressed",
|
||||||
|
".cbz" => "application/vnd.comicbook+zip",
|
||||||
|
".cbr" => "application/vnd.comicbook-rar",
|
||||||
|
}.each do |k, v|
|
||||||
|
MIME.register k, v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -11,8 +11,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<hr class="uk-divider-icon">
|
<hr class="uk-divider-icon">
|
||||||
|
<p class="uk-text-meta">Version: v<%= MANGO_VERSION %></p>
|
||||||
<a class="uk-button uk-button-danger" href="<%= base_url %>logout">Log Out</a>
|
<a class="uk-button uk-button-danger" href="<%= base_url %>logout">Log Out</a>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<script src="<%= base_url %>js/admin.js"></script>
|
<script src="<%= base_url %>js/admin.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<% if item.is_a? NamedTuple(entry: Entry, percentage: Float64, grouped_count: Int32) %>
|
|
||||||
<% grouped_count = item[:grouped_count] %>
|
|
||||||
<% if grouped_count == 1 %>
|
|
||||||
<% item = item[:entry] %>
|
|
||||||
<% else %>
|
|
||||||
<% item = item[:entry].book %>
|
|
||||||
<% end %>
|
|
||||||
<% else %>
|
|
||||||
<% grouped_count = 1 %>
|
|
||||||
<% end %>
|
|
||||||
<div class="item" data-mtime="<%= item.mtime.to_unix %>" data-progress="<%= progress || 0.0 %>"
|
|
||||||
<% if item.is_a? Entry %>
|
|
||||||
id="<%= item.id %>"
|
|
||||||
<% end %>>
|
|
||||||
|
|
||||||
<a class="acard"
|
|
||||||
<% unless item.is_a? Entry %>
|
|
||||||
href="<%= base_url %>book/<%= item.id %>"
|
|
||||||
<% end %>>
|
|
||||||
|
|
||||||
<div class="uk-card uk-card-default"
|
|
||||||
<% if item.is_a? Entry %>
|
|
||||||
onclick="showModal("<%= item.encoded_path %>", '<%= item.pages %>', <%= (progress.not_nil! * 100).round(1) %>, "<%= item.book.encoded_display_name %>", "<%= item.encoded_display_name %>", '<%= item.title_id %>', '<%= item.id %>')"
|
|
||||||
<% end %>>
|
|
||||||
|
|
||||||
<div class="uk-card-media-top">
|
|
||||||
<img data-src="<%= item.cover_url %>" data-width data-height alt="" uk-img>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="uk-card-body">
|
|
||||||
<% unless (item.is_a? Title && item.entries.size == 0) || progress.nil? %>
|
|
||||||
<div class="uk-card-badge uk-label"><%= (progress * 100).round(1) %>%</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<h3 class="uk-card-title break-word" data-title="<%= item.display_name.gsub("\"", """) %>"><%= item.display_name %></h3>
|
|
||||||
<% if item.is_a? Entry %>
|
|
||||||
<p><%= item.pages %> pages</p>
|
|
||||||
<% end %>
|
|
||||||
<% if item.is_a? Title %>
|
|
||||||
<% if grouped_count == 1 %>
|
|
||||||
<p><%= item.size %> entries</p>
|
|
||||||
<% else %>
|
|
||||||
<p><%= grouped_count %> new entries</p>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<% if item.is_a? NamedTuple(entry: Entry, percentage: Float64, grouped_count: Int32) %>
|
||||||
|
<% grouped_count = item[:grouped_count] %>
|
||||||
|
<% if grouped_count == 1 %>
|
||||||
|
<% item = item[:entry] %>
|
||||||
|
<% else %>
|
||||||
|
<% item = item[:entry].book %>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<% grouped_count = 1 %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="item" data-mtime="<%= item.mtime.to_unix %>" data-progress="<%= progress %>"
|
||||||
|
<% if item.is_a? Entry %>
|
||||||
|
id="<%= item.id %>"
|
||||||
|
<% end %>>
|
||||||
|
|
||||||
|
<div class="acard
|
||||||
|
<% if item.is_a? Entry %>
|
||||||
|
<%= "is_entry" %>
|
||||||
|
<% end %>
|
||||||
|
"
|
||||||
|
<% if item.is_a? Entry %>
|
||||||
|
data-encoded-path="<%= item.encoded_path %>"
|
||||||
|
data-pages="<%= item.pages %>"
|
||||||
|
data-progress="<%= (progress * 100).round(1) %>"
|
||||||
|
data-encoded-book-title="<%= item.book.encoded_display_name %>"
|
||||||
|
data-encoded-title="<%= item.encoded_display_name %>"
|
||||||
|
data-book-id="<%= item.book.id %>"
|
||||||
|
data-id="<%= item.id %>"
|
||||||
|
<% else %>
|
||||||
|
onclick="location='<%= base_url %>book/<%= item.id %>'"
|
||||||
|
<% end %>>
|
||||||
|
|
||||||
|
<div class="uk-card uk-card-default">
|
||||||
|
<div class="uk-card-media-top">
|
||||||
|
<img data-src="<%= item.cover_url %>" data-width data-height alt="" uk-img>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uk-card-body">
|
||||||
|
<% unless progress < 0 || progress > 100 %>
|
||||||
|
<div class="uk-card-badge label"><%= (progress * 100).round(1) %>%</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<h3 class="uk-card-title break-word
|
||||||
|
<% if page == "home" && item.is_a? Entry %>
|
||||||
|
<%= "uk-margin-remove-bottom" %>
|
||||||
|
<% end %>
|
||||||
|
" data-title="<%= HTML.escape(item.display_name) %>"><%= item.display_name %>
|
||||||
|
</h3>
|
||||||
|
<% if page == "home" && item.is_a? Entry %>
|
||||||
|
<a class="uk-card-title break-word uk-margin-remove-top uk-text-meta uk-display-inline-block no-modal" data-title="<%= HTML.escape(item.book.display_name) %>" href="<%= base_url %>book/<%= item.book.id %>"><%= HTML.escape(item.book.display_name) %></a>
|
||||||
|
<% end %>
|
||||||
|
<% if item.is_a? Entry %>
|
||||||
|
<p class="uk-text-meta"><%= item.pages %> pages</p>
|
||||||
|
<% end %>
|
||||||
|
<% if item.is_a? Title %>
|
||||||
|
<% if grouped_count == 1 %>
|
||||||
|
<p class="uk-text-meta"><%= item.size %> entries</p>
|
||||||
|
<% else %>
|
||||||
|
<p class="uk-text-meta"><%= grouped_count %> new entries</p>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -3,16 +3,14 @@
|
|||||||
<button class="uk-modal-close-default" type="button" uk-close></button>
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||||
<div class="uk-modal-header">
|
<div class="uk-modal-header">
|
||||||
<div>
|
<div>
|
||||||
<% if page == "home" %>
|
|
||||||
<h4 class="uk-margin-remove-bottom"><a id="modal-title-link"></a></h4>
|
|
||||||
<% end %>
|
|
||||||
<h3 class="uk-modal-title break-word uk-margin-remove-top" id="modal-entry-title"><span></span>
|
<h3 class="uk-modal-title break-word uk-margin-remove-top" id="modal-entry-title"><span></span>
|
||||||
<% unless page == "home" %>
|
|
||||||
|
|
||||||
<% if is_admin %>
|
<% unless page == "home" %>
|
||||||
<a class="uk-icon-button" uk-icon="icon:pencil"></a>
|
<% if is_admin %>
|
||||||
<% end %>
|
<a id="modal-edit-btn" class="uk-icon-button" uk-icon="icon:pencil"></a>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<a id="modal-download-btn" class="uk-icon-button" uk-icon="icon:download"></a>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="uk-text-meta uk-margin-remove-bottom break-word" id="path-text"></p>
|
<p class="uk-text-meta uk-margin-remove-bottom break-word" id="path-text"></p>
|
||||||
@@ -21,13 +19,13 @@
|
|||||||
<div class="uk-modal-body">
|
<div class="uk-modal-body">
|
||||||
<p>Read</p>
|
<p>Read</p>
|
||||||
<p uk-margin>
|
<p uk-margin>
|
||||||
<a id="beginning-btn" class="uk-button uk-button-default">From beginning</a>
|
<a id="beginning-btn" class="uk-button uk-button-default">From beginning</a>
|
||||||
<a id="continue-btn" class="uk-button uk-button-primary"></a>
|
<a id="continue-btn" class="uk-button uk-button-primary"></a>
|
||||||
</p>
|
</p>
|
||||||
<p>Progress</p>
|
<p>Progress</p>
|
||||||
<p uk-margin>
|
<p uk-margin>
|
||||||
<button id="read-btn" class="uk-button uk-button-default">Mark as read (100%)</button>
|
<button id="read-btn" class="uk-button uk-button-default">Mark as read (100%)</button>
|
||||||
<button id="unread-btn" class="uk-button uk-button-default">Mark as unread (0%)</button>
|
<button id="unread-btn" class="uk-button uk-button-default">Mark as unread (0%)</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<title>Mango</title>
|
<title>Mango</title>
|
||||||
<meta name="description" content="Mango - Manga Server and Web Reader">
|
<meta name="description" content="Mango - Manga Server and Web Reader">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/css/uikit.min.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 defer src="<%= base_url %>js/fontawesome.min.js"></script>
|
<script defer src="<%= base_url %>js/fontawesome.min.js"></script>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<div class="uk-form-horizontal">
|
<div class="uk-form-horizontal">
|
||||||
<select class="uk-select" id="sort-select">
|
<select class="uk-select" id="sort-select">
|
||||||
<% hash.each do |k, v| %>
|
<% hash.each do |k, v| %>
|
||||||
<option id="<%= k %>-up">â–˛ <%= v %></option>
|
<option id="<%= k %>-up">â–˛ <%= v %></option>
|
||||||
<option id="<%= k %>-down">â–Ľ <%= v %></option>
|
<option id="<%= k %>-down">â–Ľ <%= v %></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,10 +23,10 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<script>
|
<script>
|
||||||
var baseURL = "<%= mangadex_base_url %>".replace(/\/$/, "");
|
var baseURL = "<%= mangadex_base_url %>".replace(/\/$/, "");
|
||||||
</script>
|
</script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
<script src="<%= base_url %>js/download-manager.js"></script>
|
<script src="<%= base_url %>js/download-manager.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -73,11 +73,11 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<script>
|
<script>
|
||||||
var baseURL = "<%= mangadex_base_url %>".replace(/\/$/, "");
|
var baseURL = "<%= mangadex_base_url %>".replace(/\/$/, "");
|
||||||
</script>
|
</script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
<script src="<%= base_url %>js/download.js"></script>
|
<script src="<%= base_url %>js/download.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<%- if new_user && empty_library -%>
|
|
||||||
|
|
||||||
<div class="uk-container uk-text-center">
|
|
||||||
<i class="fas fa-plus" style="font-size: 80px;"></i>
|
|
||||||
<h2>Add your first manga</h2>
|
|
||||||
<p style="margin-bottom: 40px;">We can't find any files yet. Add some to your library and they'll appear here.</p>
|
|
||||||
<dl class="uk-description-list">
|
|
||||||
<dt style="font-weight: 500;">Current library path</dt>
|
|
||||||
<dd><code><%= Config.current.library_path %></code></dd>
|
|
||||||
<dt style="font-weight: 500;">Want to change your library path?</dt>
|
|
||||||
<dd>Update <code>config.yml</code> located at: <code><%= Config.current.path %></code></dd>
|
|
||||||
<dt style="font-weight: 500;">Can't see your files yet?</dt>
|
|
||||||
<dd>You must wait <%= Config.current.scan_interval %> minutes for the library scan to complete
|
|
||||||
<% if is_admin %>, or manually re-scan from <a href="<%= base_url %>admin">Admin</a><% end %>.</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%- elsif new_user && empty_library == false -%>
|
|
||||||
|
|
||||||
<div class="uk-container uk-text-center">
|
|
||||||
<i class="fas fa-book-open" style="font-size: 80px;"></i>
|
|
||||||
<h2>Read your first manga</h2>
|
|
||||||
<p>Once you start reading, Mango will remember where you left off
|
|
||||||
and show your entries here.</p>
|
|
||||||
<a href="<%= base_url %>library" class="uk-button uk-button-default">View library</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%- elsif new_user == false && empty_library == false -%>
|
|
||||||
|
|
||||||
<%- if continue_reading.empty? && recently_added.empty? -%>
|
|
||||||
<div class="uk-container uk-text-center">
|
|
||||||
<img src="<%= base_url %>img/banner.png" style="max-width: 400px; padding: 0 20px;">
|
|
||||||
<p>A self-hosted manga server and reader</p>
|
|
||||||
<a href="<%= base_url %>library" class="uk-button uk-button-default">View library</a>
|
|
||||||
</div>
|
|
||||||
<%- end -%>
|
|
||||||
|
|
||||||
<%- unless continue_reading.empty? -%>
|
|
||||||
<h2 class="uk-title home-headings">Continue Reading</h2>
|
|
||||||
<div id="item-container-continue" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
|
||||||
<%- continue_reading.each do |cr| -%>
|
|
||||||
<% item = cr[:entry] %>
|
|
||||||
<% progress = cr[:percentage] %>
|
|
||||||
<%= render_component "card" %>
|
|
||||||
<%- end -%>
|
|
||||||
</div>
|
|
||||||
<%- end -%>
|
|
||||||
|
|
||||||
<%- unless recently_added.empty? -%>
|
|
||||||
<h2 class="uk-title home-headings">Recently Added</h2>
|
|
||||||
<div id="item-container-continue" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
|
||||||
<%- recently_added.each do |ra| -%>
|
|
||||||
<% item = ra %>
|
|
||||||
<% progress = ra[:percentage] %>
|
|
||||||
<%= render_component "card" %>
|
|
||||||
<%- end -%>
|
|
||||||
</div>
|
|
||||||
<%- end -%>
|
|
||||||
|
|
||||||
<%= render_component "entry-modal" %>
|
|
||||||
|
|
||||||
<%- end -%>
|
|
||||||
|
|
||||||
<% content_for "script" do %>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.dotdotdot/4.0.11/dotdotdot.js"></script>
|
|
||||||
<script src="<%= base_url %>js/dots.js"></script>
|
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
|
||||||
<script src="<%= base_url %>js/title.js"></script>
|
|
||||||
<% end %>
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<%- if new_user && empty_library -%>
|
||||||
|
|
||||||
|
<div class="uk-container uk-text-center">
|
||||||
|
<i class="fas fa-plus" style="font-size: 80px;"></i>
|
||||||
|
<h2>Add your first manga</h2>
|
||||||
|
<p style="margin-bottom: 40px;">We can't find any files yet. Add some to your library and they'll appear here.</p>
|
||||||
|
<dl class="uk-description-list">
|
||||||
|
<dt style="font-weight: 500;">Current library path</dt>
|
||||||
|
<dd><code><%= Config.current.library_path %></code></dd>
|
||||||
|
<dt style="font-weight: 500;">Want to change your library path?</dt>
|
||||||
|
<dd>Update <code>config.yml</code> located at: <code><%= Config.current.path %></code></dd>
|
||||||
|
<dt style="font-weight: 500;">Can't see your files yet?</dt>
|
||||||
|
<dd>
|
||||||
|
You must wait <%= Config.current.scan_interval %> minutes for the library scan to complete
|
||||||
|
<% if is_admin %>
|
||||||
|
, or manually re-scan from <a href="<%= base_url %>admin">Admin</a>
|
||||||
|
<% end %>.
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%- elsif new_user && empty_library == false -%>
|
||||||
|
|
||||||
|
<div class="uk-container uk-text-center">
|
||||||
|
<i class="fas fa-book-open" style="font-size: 80px;"></i>
|
||||||
|
<h2>Read your first manga</h2>
|
||||||
|
<p>Once you start reading, Mango will remember where you left off
|
||||||
|
and show your entries here.</p>
|
||||||
|
<a href="<%= base_url %>library" class="uk-button uk-button-default">View library</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%- elsif new_user == false && empty_library == false -%>
|
||||||
|
|
||||||
|
<%- if continue_reading.empty? && recently_added.empty? -%>
|
||||||
|
<div class="uk-container uk-text-center">
|
||||||
|
<img src="<%= base_url %>img/banner.png" style="max-width: 400px; padding: 0 20px;">
|
||||||
|
<p>A self-hosted manga server and reader</p>
|
||||||
|
<a href="<%= base_url %>library" class="uk-button uk-button-default">View library</a>
|
||||||
|
</div>
|
||||||
|
<%- end -%>
|
||||||
|
|
||||||
|
<%- unless continue_reading.empty? -%>
|
||||||
|
<h2 class="uk-title home-headings">Continue Reading</h2>
|
||||||
|
<div id="item-container-continue" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
|
<%- continue_reading.each do |cr| -%>
|
||||||
|
<% item = cr[:entry] %>
|
||||||
|
<% progress = cr[:percentage] %>
|
||||||
|
<%= render_component "card" %>
|
||||||
|
<%- end -%>
|
||||||
|
</div>
|
||||||
|
<%- end -%>
|
||||||
|
|
||||||
|
<%- unless recently_added.empty? -%>
|
||||||
|
<h2 class="uk-title home-headings">Recently Added</h2>
|
||||||
|
<div id="item-container-continue" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
|
<%- recently_added.each do |ra| -%>
|
||||||
|
<% item = ra %>
|
||||||
|
<% progress = ra[:percentage] %>
|
||||||
|
<%= render_component "card" %>
|
||||||
|
<%- end -%>
|
||||||
|
</div>
|
||||||
|
<%- end -%>
|
||||||
|
|
||||||
|
<%= render_component "entry-modal" %>
|
||||||
|
|
||||||
|
<%- end -%>
|
||||||
|
|
||||||
|
<% content_for "script" do %>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.dotdotdot/4.0.11/dotdotdot.js"></script>
|
||||||
|
<script src="<%= base_url %>js/dots.js"></script>
|
||||||
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
|
<script src="<%= base_url %>js/title.js"></script>
|
||||||
|
<% end %>
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<%= render_component "head" %>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="uk-offcanvas-content">
|
|
||||||
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
|
|
||||||
<div id="mobile-nav" uk-offcanvas="overlay: true">
|
|
||||||
<div class="uk-offcanvas-bar uk-flex uk-flex-column">
|
|
||||||
<ul class="uk-nav uk-nav-primary uk-nav-center uk-margin-auto-vertical">
|
|
||||||
<li><a href="<%= base_url %>">Home</a></li>
|
|
||||||
<li><a href="<%= base_url %>library">Library</a></li>
|
|
||||||
<% if is_admin %>
|
|
||||||
<li><a href="<%= base_url %>admin">Admin</a></li>
|
|
||||||
<li><a href="<%= base_url %>download">Download</a></li>
|
|
||||||
<% end %>
|
|
||||||
<hr uk-divider>
|
|
||||||
<li><a onclick="toggleTheme()"><i class="fas fa-adjust"></i></a></li>
|
|
||||||
<li><a href="<%= base_url %>logout">Logout</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="uk-position-top">
|
|
||||||
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
|
|
||||||
<div class="uk-navbar-left uk-hidden@s">
|
|
||||||
<div class="uk-navbar-toggle" uk-navbar-toggle-icon="uk-navbar-toggle-icon" uk-toggle="target: #mobile-nav"></div>
|
|
||||||
</div>
|
|
||||||
<div class="uk-navbar-left uk-visible@s">
|
|
||||||
<a class="uk-navbar-item uk-logo" href="<%= base_url %>"><img src="<%= base_url %>img/icon.png"></a>
|
|
||||||
<ul class="uk-navbar-nav">
|
|
||||||
<li><a href="<%= base_url %>">Home</a></li>
|
|
||||||
<li><a href="<%= base_url %>library">Library</a></li>
|
|
||||||
<% if is_admin %>
|
|
||||||
<li><a href="<%= base_url %>admin">Admin</a></li>
|
|
||||||
<li><a href="<%= base_url %>download">Download</a></li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="uk-navbar-right uk-visible@s">
|
|
||||||
<ul class="uk-navbar-nav">
|
|
||||||
<li><a onclick="toggleTheme()"><i class="fas fa-adjust"></i></a></li>
|
|
||||||
<li><a href="<%= base_url %>logout">Logout</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="uk-section uk-section-small">
|
|
||||||
</div>
|
|
||||||
<div class="uk-section uk-section-small">
|
|
||||||
<div class="uk-container uk-container-small">
|
|
||||||
<div id="alert"></div>
|
|
||||||
<%= content %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
setTheme(getTheme());
|
|
||||||
const base_url = "<%= base_url %>";
|
|
||||||
</script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
|
|
||||||
|
|
||||||
<%= yield_content "script" %>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<%= render_component "head" %>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="uk-offcanvas-content">
|
||||||
|
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
|
||||||
|
<div id="mobile-nav" uk-offcanvas="overlay: true">
|
||||||
|
<div class="uk-offcanvas-bar uk-flex uk-flex-column">
|
||||||
|
<ul class="uk-nav uk-nav-primary uk-nav-center uk-margin-auto-vertical">
|
||||||
|
<li><a href="<%= base_url %>">Home</a></li>
|
||||||
|
<li><a href="<%= base_url %>library">Library</a></li>
|
||||||
|
<% if is_admin %>
|
||||||
|
<li><a href="<%= base_url %>admin">Admin</a></li>
|
||||||
|
<li><a href="<%= base_url %>download">Download</a></li>
|
||||||
|
<% end %>
|
||||||
|
<hr uk-divider>
|
||||||
|
<li><a onclick="toggleTheme()"><i class="fas fa-adjust"></i></a></li>
|
||||||
|
<li><a href="<%= base_url %>logout">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-position-top">
|
||||||
|
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
|
||||||
|
<div class="uk-navbar-left uk-hidden@s">
|
||||||
|
<div class="uk-navbar-toggle" uk-navbar-toggle-icon="uk-navbar-toggle-icon" uk-toggle="target: #mobile-nav"></div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-navbar-left uk-visible@s">
|
||||||
|
<a class="uk-navbar-item uk-logo" href="<%= base_url %>"><img src="<%= base_url %>img/icon.png"></a>
|
||||||
|
<ul class="uk-navbar-nav">
|
||||||
|
<li><a href="<%= base_url %>">Home</a></li>
|
||||||
|
<li><a href="<%= base_url %>library">Library</a></li>
|
||||||
|
<% if is_admin %>
|
||||||
|
<li><a href="<%= base_url %>admin">Admin</a></li>
|
||||||
|
<li><a href="<%= base_url %>download">Download</a></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="uk-navbar-right uk-visible@s">
|
||||||
|
<ul class="uk-navbar-nav">
|
||||||
|
<li><a onclick="toggleTheme()"><i class="fas fa-adjust"></i></a></li>
|
||||||
|
<li><a href="<%= base_url %>logout">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-section uk-section-small">
|
||||||
|
</div>
|
||||||
|
<div class="uk-section uk-section-small">
|
||||||
|
<div class="uk-container uk-container-small">
|
||||||
|
<div id="alert"></div>
|
||||||
|
<%= content %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
setTheme(getTheme());
|
||||||
|
const base_url = "<%= base_url %>";
|
||||||
|
</script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
|
||||||
|
|
||||||
|
<%= yield_content "script" %>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -18,14 +18,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="item-container" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
<div id="item-container" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
<% titles.each_with_index do |item, i| %>
|
<% titles.each_with_index do |item, i| %>
|
||||||
<% progress = percentage[i] %>
|
<% progress = percentage[i] %>
|
||||||
<%= render_component "card" %>
|
<%= render_component "card" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.dotdotdot/4.0.11/dotdotdot.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.dotdotdot/4.0.11/dotdotdot.js"></script>
|
||||||
<script src="<%= base_url %>js/dots.js"></script>
|
<script src="<%= base_url %>js/dots.js"></script>
|
||||||
<script src="<%= base_url %>js/search.js"></script>
|
<script src="<%= base_url %>js/search.js"></script>
|
||||||
<script src="<%= base_url %>js/sort-items.js"></script>
|
<script src="<%= base_url %>js/sort-items.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<%= render_component "head" %>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="uk-section uk-flex uk-flex-middle uk-animation-fade" uk-height-viewport="">
|
|
||||||
<div class="uk-width-1-1">
|
|
||||||
<div class="uk-container">
|
|
||||||
<div class="uk-grid-margin uk-grid uk-grid-stack" uk-grid="">
|
|
||||||
<div class="uk-width-1-1@m">
|
|
||||||
<div class="uk-margin uk-width-large uk-margin-auto uk-card uk-card-default uk-card-body uk-box-shadow-large">
|
|
||||||
<h3 class="uk-card-title uk-text-center">Log In</h3>
|
|
||||||
<form action="<%= base_url %>login" method="post">
|
|
||||||
<div class="uk-margin">
|
|
||||||
<div class="uk-inline uk-width-1-1"><span class="uk-form-icon" uk-icon="icon:user"></span><input class="uk-input uk-form-large" type="text" name="username"></div>
|
|
||||||
</div>
|
|
||||||
<div class="uk-margin">
|
|
||||||
<div class="uk-inline uk-width-1-1"><span class="uk-form-icon" uk-icon="icon:lock"></span><input class="uk-input uk-form-large" type="password" name="password"></div>
|
|
||||||
</div>
|
|
||||||
<div class="uk-margin"><button class="uk-button uk-button-primary uk-button-large uk-width-1-1">Login</button></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
setTheme(getTheme());
|
|
||||||
</script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<%= render_component "head" %>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="uk-section uk-flex uk-flex-middle uk-animation-fade" uk-height-viewport="">
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<div class="uk-container">
|
||||||
|
<div class="uk-grid-margin uk-grid uk-grid-stack" uk-grid="">
|
||||||
|
<div class="uk-width-1-1@m">
|
||||||
|
<div class="uk-margin uk-width-large uk-margin-auto uk-card uk-card-default uk-card-body uk-box-shadow-large">
|
||||||
|
<h3 class="uk-card-title uk-text-center">Log In</h3>
|
||||||
|
<form action="<%= base_url %>login" method="post">
|
||||||
|
<div class="uk-margin">
|
||||||
|
<div class="uk-inline uk-width-1-1"><span class="uk-form-icon" uk-icon="icon:user"></span><input class="uk-input uk-form-large" type="text" name="username"></div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-margin">
|
||||||
|
<div class="uk-inline uk-width-1-1"><span class="uk-form-icon" uk-icon="icon:lock"></span><input class="uk-input uk-form-large" type="password" name="password"></div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-margin"><button class="uk-button uk-button-primary uk-button-large uk-width-1-1">Login</button></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
setTheme(getTheme());
|
||||||
|
</script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1 +0,0 @@
|
|||||||
<p class="uk-text-lead uk-text-center"><%= message %></p>
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<p class="uk-text-lead uk-text-center"><%= message %></p>
|
||||||
@@ -13,10 +13,10 @@
|
|||||||
</author>
|
</author>
|
||||||
|
|
||||||
<% titles.each do |t| %>
|
<% titles.each do |t| %>
|
||||||
<entry>
|
<entry>
|
||||||
<title><%= t.display_name %></title>
|
<title><%= HTML.escape(t.display_name) %></title>
|
||||||
<id>urn:mango:<%= t.id %></id>
|
<id>urn:mango:<%= t.id %></id>
|
||||||
<link type="application/atom+xml;profile=opds-catalog;kind=navigation" rel="subsection" href="<%= base_url %>opds/book/<%= t.id %>" />
|
<link type="application/atom+xml;profile=opds-catalog;kind=navigation" rel="subsection" href="<%= base_url %>opds/book/<%= t.id %>" />
|
||||||
</entry>
|
</entry>
|
||||||
<% end %>
|
<% end %>
|
||||||
</feed>
|
</feed>
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
||||||
<id>urn:mango:<%= title.id %></id>
|
|
||||||
|
|
||||||
<link rel="self" href="<%= base_url %>opds/book/<%= title.id %>" type="application/atom+xml;profile=opds-catalog;kind=navigation" />
|
|
||||||
<link rel="start" href="<%= base_url %>opds/" type="application/atom+xml;profile=opds-catalog;kind=navigation" />
|
|
||||||
|
|
||||||
<title><%= title.display_name %></title>
|
|
||||||
|
|
||||||
<author>
|
|
||||||
<name>Mango</name>
|
|
||||||
<uri>https://github.com/hkalexling/Mango</uri>
|
|
||||||
</author>
|
|
||||||
|
|
||||||
<% title.titles.each do |t| %>
|
|
||||||
<entry>
|
|
||||||
<title><%= t.display_name %></title>
|
|
||||||
<id>urn:mango:<%= t.id %></id>
|
|
||||||
<link type="application/atom+xml;profile=opds-catalog;kind=navigation" rel="subsection" href="<%= base_url %>opds/book/<%= t.id %>" />
|
|
||||||
</entry>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% title.entries.each do |e| %>
|
|
||||||
<entry>
|
|
||||||
<title><%= 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/thumbnail" href="<%= e.cover_url %>" />
|
|
||||||
|
|
||||||
<link rel="http://opds-spec.org/acquisition" href="<%= base_url %>opds/download/<%= e.title_id %>/<%= e.id %>" title="Read" type="<%= MIME.from_filename e.zip_path %>" />
|
|
||||||
|
|
||||||
<link type="text/html" rel="alternate" title="Read in Mango" href="<%= base_url %>reader/<%= e.title_id %>/<%= e.id %>" />
|
|
||||||
<link type="text/html" rel="alternate" title="Open in Mango" href="<%= base_url %>book/<%= e.title_id %>" />
|
|
||||||
</entry>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
</feed>
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<id>urn:mango:<%= title.id %></id>
|
||||||
|
|
||||||
|
<link rel="self" href="<%= base_url %>opds/book/<%= title.id %>" type="application/atom+xml;profile=opds-catalog;kind=navigation" />
|
||||||
|
<link rel="start" href="<%= base_url %>opds/" type="application/atom+xml;profile=opds-catalog;kind=navigation" />
|
||||||
|
|
||||||
|
<title><%= HTML.escape(title.display_name) %></title>
|
||||||
|
|
||||||
|
<author>
|
||||||
|
<name>Mango</name>
|
||||||
|
<uri>https://github.com/hkalexling/Mango</uri>
|
||||||
|
</author>
|
||||||
|
|
||||||
|
<% title.titles.each do |t| %>
|
||||||
|
<entry>
|
||||||
|
<title><%= HTML.escape(t.display_name) %></title>
|
||||||
|
<id>urn:mango:<%= t.id %></id>
|
||||||
|
<link type="application/atom+xml;profile=opds-catalog;kind=navigation" rel="subsection" href="<%= base_url %>opds/book/<%= t.id %>" />
|
||||||
|
</entry>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% title.entries.each do |e| %>
|
||||||
|
<entry>
|
||||||
|
<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/thumbnail" href="<%= e.cover_url %>" />
|
||||||
|
|
||||||
|
<link rel="http://opds-spec.org/acquisition" href="<%= base_url %>opds/download/<%= e.title_id %>/<%= e.id %>" title="Read" type="<%= MIME.from_filename e.zip_path %>" />
|
||||||
|
|
||||||
|
<link type="text/html" rel="alternate" title="Read in Mango" href="<%= base_url %>reader/<%= e.title_id %>/<%= e.id %>" />
|
||||||
|
<link type="text/html" rel="alternate" title="Open in Mango" href="<%= base_url %>book/<%= e.title_id %>" />
|
||||||
|
</entry>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</feed>
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html class="reader-bg">
|
|
||||||
|
|
||||||
<%= render_component "head" %>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<script src="<%= base_url %>js/theme.js"></script>
|
|
||||||
<div class="uk-section uk-section-default uk-section-small reader-bg">
|
|
||||||
<div class="uk-container uk-container-small">
|
|
||||||
<%- urls.each_with_index do |url, i| -%>
|
|
||||||
<img class="uk-align-center" data-src="<%= url %>" data-width data-height uk-img id="<%= reader_urls[i] %>" onclick="showControl(<%= pages.to_a[i] %>);">
|
|
||||||
<%- end -%>
|
|
||||||
<%- if next_url -%>
|
|
||||||
<a class="next-url" href="<%= next_url %>"></a>
|
|
||||||
<%- end -%>
|
|
||||||
</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 id="hidden" hidden></div>
|
|
||||||
|
|
||||||
<div id="modal-sections" class="uk-flex-top" uk-modal>
|
|
||||||
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
|
||||||
<button class="uk-modal-close-default" type="button" uk-close></button>
|
|
||||||
<div class="uk-modal-header">
|
|
||||||
<h3 class="uk-modal-title">Options</h3>
|
|
||||||
</div>
|
|
||||||
<div class="uk-modal-body">
|
|
||||||
<div class="uk-margin">
|
|
||||||
<p id="progress-label"></p>
|
|
||||||
</div>
|
|
||||||
<div class="uk-margin">
|
|
||||||
<label class="uk-form-label" for="form-stacked-select">Jump to page</label>
|
|
||||||
<div class="uk-form-controls">
|
|
||||||
<select id="page-select" class="uk-select">
|
|
||||||
<%- (1..entry.pages).each do |p| -%>
|
|
||||||
<option value="<%= p %>"><%= p %></option>
|
|
||||||
<%- end -%>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="uk-modal-footer uk-text-right">
|
|
||||||
<button class="uk-button uk-button-danger" type="button" onclick="redirect('<%= exit_url %>')">Exit Reader</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const base_url = "<%= base_url %>"
|
|
||||||
</script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.min.js"></script>
|
|
||||||
<script src="<%= base_url %>js/reader.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html class="reader-bg">
|
||||||
|
|
||||||
|
<%= render_component "head" %>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="uk-section uk-section-default uk-section-small reader-bg">
|
||||||
|
<div class="uk-container uk-container-small">
|
||||||
|
<%- urls.each_with_index do |url, i| -%>
|
||||||
|
<img class="uk-align-center" data-src="<%= url %>" data-width data-height uk-img id="<%= reader_urls[i] %>" onclick="showControl(<%= pages.to_a[i] %>);">
|
||||||
|
<%- end -%>
|
||||||
|
<%- if next_url -%>
|
||||||
|
<a class="next-url" href="<%= next_url %>"></a>
|
||||||
|
<%- end -%>
|
||||||
|
</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 id="hidden" hidden></div>
|
||||||
|
|
||||||
|
<div id="modal-sections" class="uk-flex-top" uk-modal>
|
||||||
|
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
||||||
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||||
|
<div class="uk-modal-header">
|
||||||
|
<h3 class="uk-modal-title break-word"><%= entry.display_name %></h3>
|
||||||
|
<p class="uk-text-meta uk-margin-remove-bottom break-word"><%= entry.zip_path %></p>
|
||||||
|
</div>
|
||||||
|
<div class="uk-modal-body">
|
||||||
|
<div class="uk-margin">
|
||||||
|
<p id="progress-label"></p>
|
||||||
|
</div>
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="form-stacked-select">Jump to page</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<select id="page-select" class="uk-select">
|
||||||
|
<%- (1..entry.pages).each do |p| -%>
|
||||||
|
<option value="<%= p %>"><%= p %></option>
|
||||||
|
<%- end -%>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-modal-footer uk-text-right">
|
||||||
|
<button class="uk-button uk-button-danger" type="button" onclick="redirect('<%= exit_url %>')">Exit Reader</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const base_url = "<%= base_url %>"
|
||||||
|
</script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.min.js"></script>
|
||||||
|
<script src="<%= base_url %>js/reader.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
|
<div class="data" data-title-id="<%= title.id %>"></div>
|
||||||
<div>
|
<div>
|
||||||
<h2 class=uk-title><span><%= title.display_name %></span>
|
<h2 class=uk-title><span><%= title.display_name %></span>
|
||||||
|
|
||||||
<% if is_admin %>
|
<% if is_admin %>
|
||||||
<a onclick="edit()" class="uk-icon-button" uk-icon="icon:pencil"></a>
|
<a onclick="edit()" class="uk-icon-button" uk-icon="icon:pencil"></a>
|
||||||
<% end %>
|
<% end %>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<ul class="uk-breadcrumb">
|
<ul class="uk-breadcrumb">
|
||||||
<li><a href="<%= base_url %>library">Library</a></li>
|
<li><a href="<%= base_url %>library">Library</a></li>
|
||||||
<%- title.parents.each do |t| -%>
|
<%- title.parents.each do |t| -%>
|
||||||
<li><a href="<%= base_url %>book/<%= t.id %>"><%= t.display_name %></a></li>
|
<li><a href="<%= base_url %>book/<%= t.id %>"><%= t.display_name %></a></li>
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
<li class="uk-disabled"><a><%= title.display_name %></a></li>
|
<li class="uk-disabled"><a><%= title.display_name %></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -33,12 +34,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="item-container" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
<div id="item-container" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
<% title.titles.each_with_index do |item, i| %>
|
<% title.titles.each_with_index do |item, i| %>
|
||||||
<% progress = nil %>
|
<% progress = title_percentage[i] %>
|
||||||
<%= render_component "card" %>
|
<%= render_component "card" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% title.entries.each_with_index do |item, i| %>
|
<% title.entries.each_with_index do |item, i| %>
|
||||||
<% progress = percentage[i] %>
|
<% progress = percentage[i] %>
|
||||||
<%= render_component "card" %>
|
<%= render_component "card" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@
|
|||||||
<span uk-icon="icon: cloud-upload"></span>
|
<span uk-icon="icon: cloud-upload"></span>
|
||||||
<span class="uk-text-middle">Upload a cover image by dropping it here or</span>
|
<span class="uk-text-middle">Upload a cover image by dropping it here or</span>
|
||||||
<div uk-form-custom>
|
<div uk-form-custom>
|
||||||
<input type="file" accept="image/jpeg, image/png">
|
<input type="file" accept="<%= SUPPORTED_IMG_TYPES.join ", " %>">
|
||||||
<span class="uk-link">selecting one</span>
|
<span class="uk-link">selecting one</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,8 +86,8 @@
|
|||||||
<div id="title-progress-control" hidden>
|
<div id="title-progress-control" hidden>
|
||||||
<label class="uk-form-label">Progress</label>
|
<label class="uk-form-label">Progress</label>
|
||||||
<p class="uk-margin-remove-vertical">
|
<p class="uk-margin-remove-vertical">
|
||||||
<button id="read-btn" class="uk-button uk-button-default" onclick="updateProgress('<%= title.id %>', null, 1)">Mark all as read (100%)</button>
|
<button id="read-btn" class="uk-button uk-button-default" onclick="updateProgress('<%= title.id %>', null, 1)">Mark all as read (100%)</button>
|
||||||
<button id="unread-btn" class="uk-button uk-button-default" onclick="updateProgress('<%= title.id %>', null, 0)">Mark all as unread (0%)</button>
|
<button id="unread-btn" class="uk-button uk-button-default" onclick="updateProgress('<%= title.id %>', null, 0)">Mark all as unread (0%)</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,10 +95,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.dotdotdot/4.0.11/dotdotdot.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.dotdotdot/4.0.11/dotdotdot.js"></script>
|
||||||
<script src="<%= base_url %>js/dots.js"></script>
|
<script src="<%= base_url %>js/dots.js"></script>
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
<script src="<%= base_url %>js/title.js"></script>
|
<script src="<%= base_url %>js/title.js"></script>
|
||||||
<script src="<%= base_url %>js/search.js"></script>
|
<script src="<%= base_url %>js/search.js"></script>
|
||||||
<script src="<%= base_url %>js/sort-items.js"></script>
|
<script src="<%= base_url %>js/sort-items.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<form action="<%= base_url %>admin/user/edit" method="post" accept-charset="utf-8">
|
|
||||||
|
|
||||||
<div class="uk-margin">
|
|
||||||
<label class="uk-form-label" for="form-stacked-text">Username</label>
|
|
||||||
<input class="uk-input" type="text" name="username" <%- if username -%> value=<%= username %> <%- end -%>>
|
|
||||||
</div>
|
|
||||||
<%- if new_user -%>
|
|
||||||
<div class="uk-margin">
|
|
||||||
<label class="uk-form-label" for="form-stacked-text">Password</label>
|
|
||||||
<input class="uk-input" type="password" name="password">
|
|
||||||
</div>
|
|
||||||
<%- end -%>
|
|
||||||
<div class="uk-margin">
|
|
||||||
<label class="uk-form-label" for="form-stacked-text">Admin Access</label>
|
|
||||||
<input class="uk-checkbox" type="checkbox" name="admin" <%- if admin == true -%> checked <%- end -%>>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%- if !new_user -%>
|
|
||||||
<div>
|
|
||||||
<button class="uk-button uk-button-default" type="button" uk-toggle="target: #change-password">Change Password</button>
|
|
||||||
<div id="change-password" class="uk-margin" hidden>
|
|
||||||
<label class="uk-form-label" for="form-stacked-text">New Password</label>
|
|
||||||
<input class="uk-input" type="password" name="password">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<%- end -%>
|
|
||||||
|
|
||||||
<hr class="uk-divider-icon">
|
|
||||||
|
|
||||||
<input type="submit" value="Save" class="uk-button uk-button-primary">
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<% content_for "script" do %>
|
|
||||||
<script>
|
|
||||||
var username;
|
|
||||||
var error;
|
|
||||||
<%- if !new_user -%>
|
|
||||||
username = '/<%= username %>';
|
|
||||||
<%- end -%>
|
|
||||||
<%- if error -%>
|
|
||||||
error = '<%= error %>';
|
|
||||||
<%- end -%>
|
|
||||||
</script>
|
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
|
||||||
<script src="<%= base_url %>js/user-edit.js"></script>
|
|
||||||
<% end %>
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<form action="<%= base_url %>admin/user/edit" method="post" accept-charset="utf-8">
|
||||||
|
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="form-stacked-text">Username</label>
|
||||||
|
<input class="uk-input" type="text" name="username" <%- if username -%> value=<%= username %> <%- end -%>>
|
||||||
|
</div>
|
||||||
|
<%- if new_user -%>
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="form-stacked-text">Password</label>
|
||||||
|
<input class="uk-input" type="password" name="password">
|
||||||
|
</div>
|
||||||
|
<%- end -%>
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="form-stacked-text">Admin Access</label>
|
||||||
|
<input class="uk-checkbox" type="checkbox" name="admin" <%- if admin == true -%> checked <%- end -%>>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%- unless new_user -%>
|
||||||
|
<div>
|
||||||
|
<button class="uk-button uk-button-default" type="button" uk-toggle="target: #change-password">Change Password</button>
|
||||||
|
<div id="change-password" class="uk-margin" hidden>
|
||||||
|
<label class="uk-form-label" for="form-stacked-text">New Password</label>
|
||||||
|
<input class="uk-input" type="password" name="password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%- end -%>
|
||||||
|
|
||||||
|
<hr class="uk-divider-icon">
|
||||||
|
|
||||||
|
<input type="submit" value="Save" class="uk-button uk-button-primary">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<% content_for "script" do %>
|
||||||
|
<script>
|
||||||
|
var username;
|
||||||
|
var error;
|
||||||
|
<%- if !new_user -%>
|
||||||
|
username = '/<%= username %>';
|
||||||
|
<%- end -%>
|
||||||
|
<%- if error -%>
|
||||||
|
error = '<%= error %>';
|
||||||
|
<%- end -%>
|
||||||
|
</script>
|
||||||
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
|
<script src="<%= base_url %>js/user-edit.js"></script>
|
||||||
|
<% end %>
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<table class="uk-table uk-table-divider">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Admin Access</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<%- users.each do |u| -%>
|
|
||||||
<tr>
|
|
||||||
<td><%= u[0] %></td>
|
|
||||||
<td><%= u[1] %></td>
|
|
||||||
<td>
|
|
||||||
<a href="<%= base_url %>admin/user/edit?username=<%= u[0] %>&admin=<%= u[1] %>" uk-icon="file-edit"></a>
|
|
||||||
<%- if u[0] != username %>
|
|
||||||
<a href="#" onclick="remove('<%= u[0] %>');return false;" uk-icon="trash"></a>
|
|
||||||
<%- end %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<%- end -%>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<a href="<%= base_url %>admin/user/edit" class="uk-button uk-button-primary">New User</a>
|
|
||||||
|
|
||||||
|
|
||||||
<% content_for "script" do %>
|
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
|
||||||
<script src="<%= base_url %>js/user.js"></script>
|
|
||||||
<% end %>
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<table class="uk-table uk-table-divider">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Admin Access</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<%- users.each do |u| -%>
|
||||||
|
<tr>
|
||||||
|
<td><%= u[0] %></td>
|
||||||
|
<td><%= u[1] %></td>
|
||||||
|
<td>
|
||||||
|
<a href="<%= base_url %>admin/user/edit?username=<%= u[0] %>&admin=<%= u[1] %>" uk-icon="file-edit"></a>
|
||||||
|
<%- if u[0] != username %>
|
||||||
|
<a href="#" onclick="remove('<%= u[0] %>');return false;" uk-icon="trash"></a>
|
||||||
|
<%- end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<%- end -%>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<a href="<%= base_url %>admin/user/edit" class="uk-button uk-button-primary">New User</a>
|
||||||
|
|
||||||
|
|
||||||
|
<% content_for "script" do %>
|
||||||
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
|
<script src="<%= base_url %>js/user.js"></script>
|
||||||
|
<% end %>
|
||||||
Reference in New Issue
Block a user