mirror of
https://github.com/hkalexling/Mango.git
synced 2026-04-25 00:00:52 -04:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5645f272df | |||
| dc3bbd10d6 | |||
| c89c74c71b | |||
| cb76a96126 | |||
| 73b38492ba | |||
| bf37c4aa10 | |||
| f837be0718 | |||
| 98baf63b0c | |||
| bf0f5270f0 | |||
| ac620e1f2a | |||
| a7519a791e | |||
| 7a21f4dc9b | |||
| 650ebc7f9d | |||
| 5b34c05243 | |||
| 803fc8c44b | |||
| dd49f75079 | |||
| 6be9c3eac6 | |||
| 5a2f80b5e1 | |||
| 5b4d79220c |
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: I found a bug in Mango!
|
||||||
|
title: "[Bug Report]"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Environment (please complete the following information):**
|
||||||
|
- OS: [e.g. Ubuntu 18.04]
|
||||||
|
- Browser [e.g. chrome, safari, if applicable]
|
||||||
|
- Mango Version [e.g. v0.1.0]
|
||||||
|
|
||||||
|
**Docker (if you are running Mango in a Docker container)**
|
||||||
|
- The `docker-compose.yml` file you are using
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here. Add screenshots if applicable.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest a feature for Mango
|
||||||
|
title: "[Feature Request]"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**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 [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
name: General Question
|
||||||
|
about: I have a question about Mango
|
||||||
|
title: "[Question]"
|
||||||
|
labels: general question
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
# Mango
|
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
# Mango
|
||||||
|
|
||||||
|
[](https://gitter.im/mango-cr/mango?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||||
|
|
||||||
Mango is a self-hosted manga server and reader. Its features include
|
Mango is a self-hosted manga server and reader. Its features include
|
||||||
|
|
||||||
- Multi-user support
|
- Multi-user support
|
||||||
@@ -12,6 +17,10 @@ Mango is a self-hosted manga server and reader. Its features include
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
### Pre-built Binary
|
||||||
|
|
||||||
|
1. Simply download the pre-built binary file `mango` for the latest [release](https://github.com/hkalexling/Mango/releases). All the dependencies are statically linked, and it should work with most Linux systems on amd64.
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
1. Make sure you have docker installed and running. You will also need `docker-compose`
|
1. Make sure you have docker installed and running. You will also need `docker-compose`
|
||||||
@@ -21,10 +30,9 @@ Mango is a self-hosted manga server and reader. Its features include
|
|||||||
5. Run `docker-compose up`. This should build the docker image and start the container with Mango running inside
|
5. Run `docker-compose up`. This should build the docker image and start the container with Mango running inside
|
||||||
6. Head over to `localhost:9000` to log in
|
6. Head over to `localhost:9000` to log in
|
||||||
|
|
||||||
|
|
||||||
### Build from source
|
### Build from source
|
||||||
|
|
||||||
1. Make sure you have Crystal, Node and Yarn installed
|
1. Make sure you have Crystal, Node and Yarn installed. You might also need to install the development headers for `libsqlite3` and `libyaml`.
|
||||||
2. Clone the repository
|
2. Clone the repository
|
||||||
3. `make && sudo make install`
|
3. `make && sudo make install`
|
||||||
4. Start Mango by running the command `mango`
|
4. Start Mango by running the command `mango`
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 9000:9000
|
- 9000:9000
|
||||||
volumes:
|
volumes:
|
||||||
- ./mango:/root/mango
|
- ~/mango:/root/mango
|
||||||
- ./config:/root/.config/mango
|
- ~/.config/mango:/root/.config/mango
|
||||||
|
|||||||
+4
-2
@@ -1,10 +1,12 @@
|
|||||||
const gulp = require('gulp');
|
const gulp = require('gulp');
|
||||||
const uglify = require('gulp-uglify');
|
const minify = require("gulp-babel-minify");
|
||||||
const minifyCss = require('gulp-minify-css');
|
const minifyCss = require('gulp-minify-css');
|
||||||
|
|
||||||
gulp.task('minify-js', () => {
|
gulp.task('minify-js', () => {
|
||||||
return gulp.src('public/js/*.js')
|
return gulp.src('public/js/*.js')
|
||||||
.pipe(uglify())
|
.pipe(minify({
|
||||||
|
removeConsole: true
|
||||||
|
}))
|
||||||
.pipe(gulp.dest('dist/js'));
|
.pipe(gulp.dest('dist/js'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"gulp-minify-css": "^1.2.4",
|
"gulp-babel-minify": "^0.5.1",
|
||||||
"gulp-uglify": "^3.0.2"
|
"gulp-minify-css": "^1.2.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"uglify": "gulp"
|
"uglify": "gulp"
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
$(() => {
|
||||||
|
$('option#name-up').attr('selected', '');
|
||||||
|
$('#sort-select').change(() => {
|
||||||
|
const sort = $('#sort-select').find(':selected').attr('id');
|
||||||
|
const ary = sort.split('-');
|
||||||
|
const by = ary[0];
|
||||||
|
const dir = ary[1];
|
||||||
|
|
||||||
|
const items = $('.item');
|
||||||
|
items.remove();
|
||||||
|
|
||||||
|
items.sort((a, b) => {
|
||||||
|
var res;
|
||||||
|
if (by === 'name')
|
||||||
|
res = $(a).find('.uk-card-title').text() > $(b).find('.uk-card-title').text();
|
||||||
|
else if (by === 'date')
|
||||||
|
res = $(a).attr('data-mtime') > $(b).attr('data-mtime');
|
||||||
|
else {
|
||||||
|
const ap = $(a).attr('data-progress');
|
||||||
|
const bp = $(b).attr('data-progress');
|
||||||
|
if (ap === bp)
|
||||||
|
// if progress is the same, we compare by name
|
||||||
|
res = $(a).find('.uk-card-title').text() > $(b).find('.uk-card-title').text();
|
||||||
|
else
|
||||||
|
res = ap > bp;
|
||||||
|
}
|
||||||
|
if (dir === 'up')
|
||||||
|
return res;
|
||||||
|
else
|
||||||
|
return !res;
|
||||||
|
});
|
||||||
|
var html = '';
|
||||||
|
$('#item-container').append(items);
|
||||||
|
});
|
||||||
|
});
|
||||||
+10
-7
@@ -1,4 +1,7 @@
|
|||||||
function showModal(title, zipPath, pages, percentage, title, entry) {
|
function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTitle, titleID, entryID) {
|
||||||
|
const zipPath = decodeURIComponent(encodedPath);
|
||||||
|
const title = decodeURIComponent(encodedeTitle);
|
||||||
|
const entry = decodeURIComponent(encodedEntryTitle);
|
||||||
$('#modal button, #modal a').each(function(){
|
$('#modal button, #modal a').each(function(){
|
||||||
$(this).removeAttr('hidden');
|
$(this).removeAttr('hidden');
|
||||||
});
|
});
|
||||||
@@ -16,20 +19,20 @@ function showModal(title, zipPath, pages, percentage, title, entry) {
|
|||||||
$('#path-text').text(zipPath);
|
$('#path-text').text(zipPath);
|
||||||
$('#pages-text').text(pages + ' pages');
|
$('#pages-text').text(pages + ' pages');
|
||||||
|
|
||||||
$('#beginning-btn').attr('href', '/reader/' + title + '/' + entry + '/1');
|
$('#beginning-btn').attr('href', '/reader/' + titleID + '/' + entryID + '/1');
|
||||||
$('#continue-btn').attr('href', '/reader/' + title + '/' + entry);
|
$('#continue-btn').attr('href', '/reader/' + titleID + '/' + entryID);
|
||||||
|
|
||||||
$('#read-btn').click(function(){
|
$('#read-btn').click(function(){
|
||||||
updateProgress(title, entry, pages);
|
updateProgress(titleID, entryID, pages);
|
||||||
});
|
});
|
||||||
$('#unread-btn').click(function(){
|
$('#unread-btn').click(function(){
|
||||||
updateProgress(title, entry, 0);
|
updateProgress(titleID, entryID, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
UIkit.modal($('#modal')).show();
|
UIkit.modal($('#modal')).show();
|
||||||
}
|
}
|
||||||
function updateProgress(title, entry, page) {
|
function updateProgress(titleID, entryID, page) {
|
||||||
$.post('/api/progress/' + title + '/' + entry + '/' + page, function(data) {
|
$.post('/api/progress/' + titleID + '/' + entryID + '/' + page, function(data) {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-18
@@ -1,6 +1,7 @@
|
|||||||
require "zip"
|
require "zip"
|
||||||
require "mime"
|
require "mime"
|
||||||
require "json"
|
require "json"
|
||||||
|
require "uri"
|
||||||
|
|
||||||
struct Image
|
struct Image
|
||||||
property data : Bytes
|
property data : Bytes
|
||||||
@@ -14,19 +15,27 @@ end
|
|||||||
|
|
||||||
class Entry
|
class Entry
|
||||||
JSON.mapping zip_path: String, book_title: String, title: String, \
|
JSON.mapping zip_path: String, book_title: String, title: String, \
|
||||||
size: String, pages: Int32, cover_url: String
|
size: String, pages: Int32, cover_url: String, id: String, \
|
||||||
|
title_id: String, encoded_path: String, encoded_title: String,
|
||||||
|
mtime: Time
|
||||||
|
|
||||||
def initialize(path, @book_title)
|
def initialize(path, @book_title, @title_id, storage)
|
||||||
@zip_path = path
|
@zip_path = path
|
||||||
|
@encoded_path = URI.encode path
|
||||||
@title = File.basename path, File.extname path
|
@title = File.basename path, File.extname path
|
||||||
|
@encoded_title = URI.encode @title
|
||||||
@size = (File.size path).humanize_bytes
|
@size = (File.size path).humanize_bytes
|
||||||
@pages = Zip::File.new(path).entries
|
file = Zip::File.new path
|
||||||
|
@pages = file.entries
|
||||||
.select { |e|
|
.select { |e|
|
||||||
["image/jpeg", "image/png"].includes? \
|
["image/jpeg", "image/png"].includes? \
|
||||||
MIME.from_filename? e.filename
|
MIME.from_filename? e.filename
|
||||||
}
|
}
|
||||||
.size
|
.size
|
||||||
@cover_url = "/api/page/#{@book_title}/#{title}/1"
|
file.close
|
||||||
|
@id = storage.get_id @zip_path, false
|
||||||
|
@cover_url = "/api/page/#{@title_id}/#{@id}/1"
|
||||||
|
@mtime = File.info(@zip_path).modification_time
|
||||||
end
|
end
|
||||||
def read_page(page_num)
|
def read_page(page_num)
|
||||||
Zip::File.open @zip_path do |file|
|
Zip::File.open @zip_path do |file|
|
||||||
@@ -51,20 +60,47 @@ class Entry
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Title
|
class Title
|
||||||
JSON.mapping dir: String, entries: Array(Entry), title: String
|
JSON.mapping dir: String, entries: Array(Entry), title: String,
|
||||||
|
id: String, encoded_title: String, mtime: Time, logger: MLogger
|
||||||
|
|
||||||
def initialize(dir : String)
|
def initialize(dir : String, storage, @logger : MLogger)
|
||||||
@dir = dir
|
@dir = dir
|
||||||
|
@id = storage.get_id @dir, true
|
||||||
@title = File.basename dir
|
@title = File.basename dir
|
||||||
|
@encoded_title = URI.encode @title
|
||||||
@entries = (Dir.entries dir)
|
@entries = (Dir.entries dir)
|
||||||
.select { |path| [".zip", ".cbz"].includes? File.extname path }
|
.select { |path| [".zip", ".cbz"].includes? File.extname path }
|
||||||
.map { |path| Entry.new File.join(dir, path), @title }
|
.map { |path| File.join dir, path }
|
||||||
|
.select { |path| valid_zip path }
|
||||||
|
.map { |path|
|
||||||
|
Entry.new path, @title, @id, storage
|
||||||
|
}
|
||||||
.select { |e| e.pages > 0 }
|
.select { |e| e.pages > 0 }
|
||||||
.sort { |a, b| a.title <=> b.title }
|
.sort { |a, b| a.title <=> b.title }
|
||||||
|
mtimes = [File.info(dir).modification_time]
|
||||||
|
mtimes += @entries.map{|e| e.mtime}
|
||||||
|
@mtime = mtimes.max
|
||||||
end
|
end
|
||||||
def get_entry(name)
|
# When downloading from MangaDex, the zip/cbz file would not be valid
|
||||||
@entries.find { |e| e.title == name }
|
# before the download is completed. If we scan the zip file,
|
||||||
|
# Entry.new would throw, so we use this method to check before
|
||||||
|
# constructing Entry
|
||||||
|
private def valid_zip(path : String)
|
||||||
|
begin
|
||||||
|
file = Zip::File.new path
|
||||||
|
file.close
|
||||||
|
return true
|
||||||
|
rescue
|
||||||
|
@logger.warn "File #{path} is corrupted or is not a valid zip "\
|
||||||
|
"archive. Ignoring it."
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
def get_entry(eid)
|
||||||
|
@entries.find { |e| e.id == eid }
|
||||||
|
end
|
||||||
|
# For backward backward compatibility with v0.1.0, we save entry titles
|
||||||
|
# instead of IDs in info.json
|
||||||
def save_progress(username, entry, page)
|
def save_progress(username, entry, page)
|
||||||
info = TitleInfo.new @dir
|
info = TitleInfo.new @dir
|
||||||
if info.progress[username]?.nil?
|
if info.progress[username]?.nil?
|
||||||
@@ -75,7 +111,7 @@ class Title
|
|||||||
info.progress[username][entry] = page
|
info.progress[username][entry] = page
|
||||||
info.save @dir
|
info.save @dir
|
||||||
end
|
end
|
||||||
def load_progress(username, entry : String)
|
def load_progress(username, entry)
|
||||||
info = TitleInfo.new @dir
|
info = TitleInfo.new @dir
|
||||||
if info.progress[username]?.nil?
|
if info.progress[username]?.nil?
|
||||||
return 0
|
return 0
|
||||||
@@ -85,10 +121,10 @@ class Title
|
|||||||
end
|
end
|
||||||
info.progress[username][entry]
|
info.progress[username][entry]
|
||||||
end
|
end
|
||||||
def load_percetage(username, entry : String)
|
def load_percetage(username, entry)
|
||||||
info = TitleInfo.new @dir
|
info = TitleInfo.new @dir
|
||||||
page = load_progress username, entry
|
page = load_progress username, entry
|
||||||
entry_obj = get_entry entry
|
entry_obj = @entries.find{|e| e.title == entry}
|
||||||
return 0 if entry_obj.nil?
|
return 0 if entry_obj.nil?
|
||||||
page / entry_obj.pages
|
page / entry_obj.pages
|
||||||
end
|
end
|
||||||
@@ -136,9 +172,10 @@ class TitleInfo
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Library
|
class Library
|
||||||
JSON.mapping dir: String, titles: Array(Title), scan_interval: Int32, logger: MLogger
|
JSON.mapping dir: String, titles: Array(Title), scan_interval: Int32,
|
||||||
|
logger: MLogger, storage: Storage
|
||||||
|
|
||||||
def initialize(@dir, @scan_interval, @logger)
|
def initialize(@dir, @scan_interval, @logger, @storage)
|
||||||
# explicitly initialize @titles to bypass the compiler check. it will
|
# explicitly initialize @titles to bypass the compiler check. it will
|
||||||
# be filled with actual Titles in the `scan` call below
|
# be filled with actual Titles in the `scan` call below
|
||||||
@titles = [] of Title
|
@titles = [] of Title
|
||||||
@@ -154,8 +191,8 @@ class Library
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
def get_title(name)
|
def get_title(tid)
|
||||||
@titles.find { |t| t.title == name }
|
@titles.find { |t| t.id == tid }
|
||||||
end
|
end
|
||||||
def scan
|
def scan
|
||||||
unless Dir.exists? @dir
|
unless Dir.exists? @dir
|
||||||
@@ -165,9 +202,9 @@ class Library
|
|||||||
end
|
end
|
||||||
@titles = (Dir.entries @dir)
|
@titles = (Dir.entries @dir)
|
||||||
.select { |path| File.directory? File.join @dir, path }
|
.select { |path| File.directory? File.join @dir, path }
|
||||||
.map { |path| Title.new File.join @dir, path }
|
.map { |path| Title.new File.join(@dir, path), @storage, @logger }
|
||||||
.select { |title| !title.entries.empty? }
|
.select { |title| !title.entries.empty? }
|
||||||
|
.sort { |a, b| a.title <=> b.title }
|
||||||
@logger.debug "Scan completed"
|
@logger.debug "Scan completed"
|
||||||
@logger.debug "Scanned library: \n#{self.to_pretty_json}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ require "./server"
|
|||||||
require "./context"
|
require "./context"
|
||||||
require "option_parser"
|
require "option_parser"
|
||||||
|
|
||||||
VERSION = "0.1.0"
|
VERSION = "0.1.2"
|
||||||
|
|
||||||
config_path = nil
|
config_path = nil
|
||||||
|
|
||||||
@@ -25,8 +25,8 @@ end
|
|||||||
|
|
||||||
config = Config.load config_path
|
config = Config.load config_path
|
||||||
logger = MLogger.new config
|
logger = MLogger.new config
|
||||||
library = Library.new config.library_path, config.scan_interval, logger
|
|
||||||
storage = Storage.new config.db_path, logger
|
storage = Storage.new config.db_path, logger
|
||||||
|
library = Library.new config.library_path, config.scan_interval, logger, storage
|
||||||
|
|
||||||
context = Context.new config, logger, library, storage
|
context = Context.new config, logger, library, storage
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
require "./router"
|
||||||
|
|
||||||
|
class AdminRouter < Router
|
||||||
|
def setup
|
||||||
|
get "/admin" do |env|
|
||||||
|
layout "admin"
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/admin/user" do |env|
|
||||||
|
users = @context.storage.list_users
|
||||||
|
username = get_username env
|
||||||
|
layout "user"
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/admin/user/edit" do |env|
|
||||||
|
username = env.params.query["username"]?
|
||||||
|
admin = env.params.query["admin"]?
|
||||||
|
if admin
|
||||||
|
admin = admin == "true"
|
||||||
|
end
|
||||||
|
error = env.params.query["error"]?
|
||||||
|
current_user = get_username env
|
||||||
|
new_user = username.nil? && admin.nil?
|
||||||
|
layout "user-edit"
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/admin/user/edit" do |env|
|
||||||
|
# creating new user
|
||||||
|
begin
|
||||||
|
username = env.params.body["username"]
|
||||||
|
password = env.params.body["password"]
|
||||||
|
# if `admin` is unchecked, the body hash
|
||||||
|
# would not contain `admin`
|
||||||
|
admin = !env.params.body["admin"]?.nil?
|
||||||
|
|
||||||
|
if username.size < 3
|
||||||
|
raise "Username should contain at least 3 characters"
|
||||||
|
end
|
||||||
|
if (username =~ /^[A-Za-z0-9_]+$/).nil?
|
||||||
|
raise "Username should contain alphanumeric characters "\
|
||||||
|
"and underscores only"
|
||||||
|
end
|
||||||
|
if password.size < 6
|
||||||
|
raise "Password should contain at least 6 characters"
|
||||||
|
end
|
||||||
|
if (password =~ /^[[:ascii:]]+$/).nil?
|
||||||
|
raise "password should contain ASCII characters only"
|
||||||
|
end
|
||||||
|
|
||||||
|
@context.storage.new_user username, password, admin
|
||||||
|
|
||||||
|
env.redirect "/admin/user"
|
||||||
|
rescue e
|
||||||
|
@context.error e
|
||||||
|
redirect_url = URI.new \
|
||||||
|
path: "/admin/user/edit",\
|
||||||
|
query: hash_to_query({"error" => e.message})
|
||||||
|
env.redirect redirect_url.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/admin/user/edit/:original_username" do |env|
|
||||||
|
# editing existing user
|
||||||
|
begin
|
||||||
|
username = env.params.body["username"]
|
||||||
|
password = env.params.body["password"]
|
||||||
|
# if `admin` is unchecked, the body
|
||||||
|
# hash would not contain `admin`
|
||||||
|
admin = !env.params.body["admin"]?.nil?
|
||||||
|
original_username = env.params.url["original_username"]
|
||||||
|
|
||||||
|
if username.size < 3
|
||||||
|
raise "Username should contain at least 3 characters"
|
||||||
|
end
|
||||||
|
if (username =~ /^[A-Za-z0-9_]+$/).nil?
|
||||||
|
raise "Username should contain alphanumeric characters "\
|
||||||
|
"and underscores only"
|
||||||
|
end
|
||||||
|
|
||||||
|
if password.size != 0
|
||||||
|
if password.size < 6
|
||||||
|
raise "Password should contain at least 6 characters"
|
||||||
|
end
|
||||||
|
if (password =~ /^[[:ascii:]]+$/).nil?
|
||||||
|
raise "password should contain ASCII characters only"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@context.storage.update_user \
|
||||||
|
original_username, username, password, admin
|
||||||
|
|
||||||
|
env.redirect "/admin/user"
|
||||||
|
rescue e
|
||||||
|
@context.error e
|
||||||
|
redirect_url = URI.new \
|
||||||
|
path: "/admin/user/edit",\
|
||||||
|
query: hash_to_query({"username" => original_username, \
|
||||||
|
"admin" => admin, "error" => e.message})
|
||||||
|
env.redirect redirect_url.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
require "./router"
|
||||||
|
|
||||||
|
class APIRouter < Router
|
||||||
|
def setup
|
||||||
|
get "/api/page/:tid/:eid/:page" do |env|
|
||||||
|
begin
|
||||||
|
tid = env.params.url["tid"]
|
||||||
|
eid = env.params.url["eid"]
|
||||||
|
page = env.params.url["page"].to_i
|
||||||
|
|
||||||
|
title = @context.library.get_title tid
|
||||||
|
raise "Title ID `#{tid}` not found" if title.nil?
|
||||||
|
entry = title.get_entry eid
|
||||||
|
raise "Entry ID `#{eid}` of `#{title.title}` not found" if \
|
||||||
|
entry.nil?
|
||||||
|
img = entry.read_page page
|
||||||
|
raise "Failed to load page #{page} of " \
|
||||||
|
"`#{title.title}/#{entry.title}`" if img.nil?
|
||||||
|
|
||||||
|
send_img env, img
|
||||||
|
rescue e
|
||||||
|
@context.error e
|
||||||
|
env.response.status_code = 500
|
||||||
|
e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/api/book/:title" do |env|
|
||||||
|
begin
|
||||||
|
tid = env.params.url["tid"]
|
||||||
|
title = @context.library.get_title tid
|
||||||
|
raise "Title ID `#{tid}` not found" if title.nil?
|
||||||
|
|
||||||
|
send_json env, title.to_json
|
||||||
|
rescue e
|
||||||
|
@context.error e
|
||||||
|
env.response.status_code = 500
|
||||||
|
e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/api/book" do |env|
|
||||||
|
send_json env, @context.library.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/api/admin/scan" do |env|
|
||||||
|
start = Time.utc
|
||||||
|
@context.library.scan
|
||||||
|
ms = (Time.utc - start).total_milliseconds
|
||||||
|
send_json env, {
|
||||||
|
"milliseconds" => ms,
|
||||||
|
"titles" => @context.library.titles.size
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/api/admin/user/delete/:username" do |env|
|
||||||
|
begin
|
||||||
|
username = env.params.url["username"]
|
||||||
|
@context.storage.delete_user username
|
||||||
|
rescue e
|
||||||
|
@context.error e
|
||||||
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message
|
||||||
|
}.to_json
|
||||||
|
else
|
||||||
|
send_json env, {"success" => true}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/api/progress/:title/:entry/:page" do |env|
|
||||||
|
begin
|
||||||
|
username = get_username env
|
||||||
|
title = (@context.library.get_title env.params.url["title"])
|
||||||
|
.not_nil!
|
||||||
|
entry = (title.get_entry env.params.url["entry"]).not_nil!
|
||||||
|
page = env.params.url["page"].to_i
|
||||||
|
|
||||||
|
raise "incorrect page value" if page < 0 || page > entry.pages
|
||||||
|
title.save_progress username, entry.title, page
|
||||||
|
rescue e
|
||||||
|
@context.error e
|
||||||
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message
|
||||||
|
}.to_json
|
||||||
|
else
|
||||||
|
send_json env, {"success" => true}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
require "./router"
|
||||||
|
|
||||||
|
class MainRouter < Router
|
||||||
|
def setup
|
||||||
|
get "/login" do |env|
|
||||||
|
render "src/views/login.ecr"
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/logout" do |env|
|
||||||
|
begin
|
||||||
|
cookie = env.request.cookies
|
||||||
|
.find { |c| c.name == "token" }.not_nil!
|
||||||
|
@context.storage.logout cookie.value
|
||||||
|
rescue e
|
||||||
|
@context.error "Error when attempting to log out: #{e}"
|
||||||
|
ensure
|
||||||
|
env.redirect "/login"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/login" do |env|
|
||||||
|
begin
|
||||||
|
username = env.params.body["username"]
|
||||||
|
password = env.params.body["password"]
|
||||||
|
token = @context.storage.verify_user(username, password)
|
||||||
|
.not_nil!
|
||||||
|
|
||||||
|
cookie = HTTP::Cookie.new "token", token
|
||||||
|
env.response.cookies << cookie
|
||||||
|
env.redirect "/"
|
||||||
|
rescue
|
||||||
|
env.redirect "/login"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
get "/" do |env|
|
||||||
|
titles = @context.library.titles
|
||||||
|
username = get_username env
|
||||||
|
percentage = titles.map &.load_percetage username
|
||||||
|
layout "index"
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/book/:title" do |env|
|
||||||
|
begin
|
||||||
|
title = (@context.library.get_title env.params.url["title"])
|
||||||
|
.not_nil!
|
||||||
|
username = get_username env
|
||||||
|
percentage = title.entries.map { |e|
|
||||||
|
title.load_percetage username, e.title }
|
||||||
|
layout "title"
|
||||||
|
rescue e
|
||||||
|
@context.error e
|
||||||
|
env.response.status_code = 404
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
require "./router"
|
||||||
|
|
||||||
|
class ReaderRouter < Router
|
||||||
|
def setup
|
||||||
|
get "/reader/:title/:entry" do |env|
|
||||||
|
begin
|
||||||
|
title = (@context.library.get_title env.params.url["title"])
|
||||||
|
.not_nil!
|
||||||
|
entry = (title.get_entry env.params.url["entry"]).not_nil!
|
||||||
|
|
||||||
|
# load progress
|
||||||
|
username = get_username env
|
||||||
|
page = title.load_progress username, entry.title
|
||||||
|
# we go back 2 * `IMGS_PER_PAGE` pages. the infinite scroll
|
||||||
|
# library perloads a few pages in advance, and the user
|
||||||
|
# might not have actually read them
|
||||||
|
page = [page - 2 * IMGS_PER_PAGE, 1].max
|
||||||
|
|
||||||
|
env.redirect "/reader/#{title.id}/#{entry.id}/#{page}"
|
||||||
|
rescue e
|
||||||
|
@context.error e
|
||||||
|
env.response.status_code = 404
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/reader/:title/:entry/:page" do |env|
|
||||||
|
begin
|
||||||
|
title = (@context.library.get_title env.params.url["title"])
|
||||||
|
.not_nil!
|
||||||
|
entry = (title.get_entry env.params.url["entry"]).not_nil!
|
||||||
|
page = env.params.url["page"].to_i
|
||||||
|
raise "" if page > entry.pages || page <= 0
|
||||||
|
|
||||||
|
# save progress
|
||||||
|
username = get_username env
|
||||||
|
title.save_progress username, entry.title, page
|
||||||
|
|
||||||
|
pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min)
|
||||||
|
urls = pages.map { |idx|
|
||||||
|
"/api/page/#{title.id}/#{entry.id}/#{idx}" }
|
||||||
|
reader_urls = pages.map { |idx|
|
||||||
|
"/reader/#{title.id}/#{entry.id}/#{idx}" }
|
||||||
|
next_page = page + IMGS_PER_PAGE
|
||||||
|
next_url = next_page > entry.pages ? nil :
|
||||||
|
"/reader/#{title.id}/#{entry.id}/#{next_page}"
|
||||||
|
exit_url = "/book/#{title.id}"
|
||||||
|
next_entry = title.next_entry entry
|
||||||
|
next_entry_url = next_entry.nil? ? nil : \
|
||||||
|
"/reader/#{title.id}/#{next_entry.id}"
|
||||||
|
|
||||||
|
render "src/views/reader.ecr"
|
||||||
|
rescue e
|
||||||
|
@context.error e
|
||||||
|
env.response.status_code = 404
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
require "../context"
|
||||||
|
|
||||||
|
class Router
|
||||||
|
def initialize(@context : Context)
|
||||||
|
end
|
||||||
|
end
|
||||||
+5
-290
@@ -4,6 +4,7 @@ require "./auth_handler"
|
|||||||
require "./static_handler"
|
require "./static_handler"
|
||||||
require "./log_handler"
|
require "./log_handler"
|
||||||
require "./util"
|
require "./util"
|
||||||
|
require "./routes/*"
|
||||||
|
|
||||||
class Server
|
class Server
|
||||||
def initialize(@context : Context)
|
def initialize(@context : Context)
|
||||||
@@ -13,296 +14,10 @@ class Server
|
|||||||
layout "message"
|
layout "message"
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/" do |env|
|
MainRouter.new(@context).setup
|
||||||
titles = @context.library.titles
|
AdminRouter.new(@context).setup
|
||||||
username = get_username env
|
ReaderRouter.new(@context).setup
|
||||||
percentage = titles.map &.load_percetage username
|
APIRouter.new(@context).setup
|
||||||
layout "index"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/book/:title" do |env|
|
|
||||||
begin
|
|
||||||
title = (@context.library.get_title env.params.url["title"])
|
|
||||||
.not_nil!
|
|
||||||
username = get_username env
|
|
||||||
percentage = title.entries.map { |e|
|
|
||||||
title.load_percetage username, e.title }
|
|
||||||
layout "title"
|
|
||||||
rescue e
|
|
||||||
@context.error e
|
|
||||||
env.response.status_code = 404
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/admin" do |env|
|
|
||||||
layout "admin"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/admin/user" do |env|
|
|
||||||
users = @context.storage.list_users
|
|
||||||
username = get_username env
|
|
||||||
layout "user"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/admin/user/edit" do |env|
|
|
||||||
username = env.params.query["username"]?
|
|
||||||
admin = env.params.query["admin"]?
|
|
||||||
if admin
|
|
||||||
admin = admin == "true"
|
|
||||||
end
|
|
||||||
error = env.params.query["error"]?
|
|
||||||
current_user = get_username env
|
|
||||||
new_user = username.nil? && admin.nil?
|
|
||||||
layout "user-edit"
|
|
||||||
end
|
|
||||||
|
|
||||||
post "/admin/user/edit" do |env|
|
|
||||||
# creating new user
|
|
||||||
begin
|
|
||||||
username = env.params.body["username"]
|
|
||||||
password = env.params.body["password"]
|
|
||||||
# if `admin` is unchecked, the body hash
|
|
||||||
# would not contain `admin`
|
|
||||||
admin = !env.params.body["admin"]?.nil?
|
|
||||||
|
|
||||||
if username.size < 3
|
|
||||||
raise "Username should contain at least 3 characters"
|
|
||||||
end
|
|
||||||
if (username =~ /^[A-Za-z0-9_]+$/).nil?
|
|
||||||
raise "Username should contain alphanumeric characters "\
|
|
||||||
"and underscores only"
|
|
||||||
end
|
|
||||||
if password.size < 6
|
|
||||||
raise "Password should contain at least 6 characters"
|
|
||||||
end
|
|
||||||
if (password =~ /^[[:ascii:]]+$/).nil?
|
|
||||||
raise "password should contain ASCII characters only"
|
|
||||||
end
|
|
||||||
|
|
||||||
@context.storage.new_user username, password, admin
|
|
||||||
|
|
||||||
env.redirect "/admin/user"
|
|
||||||
rescue e
|
|
||||||
@context.error e
|
|
||||||
redirect_url = URI.new \
|
|
||||||
path: "/admin/user/edit",\
|
|
||||||
query: hash_to_query({"error" => e.message})
|
|
||||||
env.redirect redirect_url.to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
post "/admin/user/edit/:original_username" do |env|
|
|
||||||
# editing existing user
|
|
||||||
begin
|
|
||||||
username = env.params.body["username"]
|
|
||||||
password = env.params.body["password"]
|
|
||||||
# if `admin` is unchecked, the body
|
|
||||||
# hash would not contain `admin`
|
|
||||||
admin = !env.params.body["admin"]?.nil?
|
|
||||||
original_username = env.params.url["original_username"]
|
|
||||||
|
|
||||||
if username.size < 3
|
|
||||||
raise "Username should contain at least 3 characters"
|
|
||||||
end
|
|
||||||
if (username =~ /^[A-Za-z0-9_]+$/).nil?
|
|
||||||
raise "Username should contain alphanumeric characters "\
|
|
||||||
"and underscores only"
|
|
||||||
end
|
|
||||||
|
|
||||||
if password.size != 0
|
|
||||||
if password.size < 6
|
|
||||||
raise "Password should contain at least 6 characters"
|
|
||||||
end
|
|
||||||
if (password =~ /^[[:ascii:]]+$/).nil?
|
|
||||||
raise "password should contain ASCII characters only"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@context.storage.update_user \
|
|
||||||
original_username, username, password, admin
|
|
||||||
|
|
||||||
env.redirect "/admin/user"
|
|
||||||
rescue e
|
|
||||||
@context.error e
|
|
||||||
redirect_url = URI.new \
|
|
||||||
path: "/admin/user/edit",\
|
|
||||||
query: hash_to_query({"username" => original_username, \
|
|
||||||
"admin" => admin, "error" => e.message})
|
|
||||||
env.redirect redirect_url.to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
get "/reader/:title/:entry" do |env|
|
|
||||||
begin
|
|
||||||
title = (@context.library.get_title env.params.url["title"])
|
|
||||||
.not_nil!
|
|
||||||
entry = (title.get_entry env.params.url["entry"]).not_nil!
|
|
||||||
|
|
||||||
# load progress
|
|
||||||
username = get_username env
|
|
||||||
page = title.load_progress username, entry.title
|
|
||||||
# we go back 2 * `IMGS_PER_PAGE` pages. the infinite scroll
|
|
||||||
# library perloads a few pages in advance, and the user
|
|
||||||
# might not have actually read them
|
|
||||||
page = [page - 2 * IMGS_PER_PAGE, 1].max
|
|
||||||
|
|
||||||
env.redirect "/reader/#{title.title}/#{entry.title}/#{page}"
|
|
||||||
rescue e
|
|
||||||
@context.error e
|
|
||||||
env.response.status_code = 404
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/reader/:title/:entry/:page" do |env|
|
|
||||||
begin
|
|
||||||
title = (@context.library.get_title env.params.url["title"])
|
|
||||||
.not_nil!
|
|
||||||
entry = (title.get_entry env.params.url["entry"]).not_nil!
|
|
||||||
page = env.params.url["page"].to_i
|
|
||||||
raise "" if page > entry.pages || page <= 0
|
|
||||||
|
|
||||||
# save progress
|
|
||||||
username = get_username env
|
|
||||||
title.save_progress username, entry.title, page
|
|
||||||
|
|
||||||
pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min)
|
|
||||||
urls = pages.map { |idx|
|
|
||||||
"/api/page/#{title.title}/#{entry.title}/#{idx}" }
|
|
||||||
reader_urls = pages.map { |idx|
|
|
||||||
"/reader/#{title.title}/#{entry.title}/#{idx}" }
|
|
||||||
next_page = page + IMGS_PER_PAGE
|
|
||||||
next_url = next_page > entry.pages ? nil :
|
|
||||||
"/reader/#{title.title}/#{entry.title}/#{next_page}"
|
|
||||||
exit_url = "/book/#{title.title}"
|
|
||||||
next_entry = title.next_entry entry
|
|
||||||
next_entry_url = next_entry.nil? ? nil : \
|
|
||||||
"/reader/#{title.title}/#{next_entry.title}"
|
|
||||||
|
|
||||||
render "src/views/reader.ecr"
|
|
||||||
rescue e
|
|
||||||
@context.error e
|
|
||||||
env.response.status_code = 404
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/login" do |env|
|
|
||||||
render "src/views/login.ecr"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/logout" do |env|
|
|
||||||
begin
|
|
||||||
cookie = env.request.cookies
|
|
||||||
.find { |c| c.name == "token" }.not_nil!
|
|
||||||
@context.storage.logout cookie.value
|
|
||||||
rescue e
|
|
||||||
@context.error "Error when attempting to log out: #{e}"
|
|
||||||
ensure
|
|
||||||
env.redirect "/login"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
post "/login" do |env|
|
|
||||||
begin
|
|
||||||
username = env.params.body["username"]
|
|
||||||
password = env.params.body["password"]
|
|
||||||
token = @context.storage.verify_user(username, password)
|
|
||||||
.not_nil!
|
|
||||||
|
|
||||||
cookie = HTTP::Cookie.new "token", token
|
|
||||||
env.response.cookies << cookie
|
|
||||||
env.redirect "/"
|
|
||||||
rescue
|
|
||||||
env.redirect "/login"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/api/page/:title/:entry/:page" do |env|
|
|
||||||
begin
|
|
||||||
title = env.params.url["title"]
|
|
||||||
entry = env.params.url["entry"]
|
|
||||||
page = env.params.url["page"].to_i
|
|
||||||
|
|
||||||
t = @context.library.get_title title
|
|
||||||
raise "Title `#{title}` not found" if t.nil?
|
|
||||||
e = t.get_entry entry
|
|
||||||
raise "Entry `#{entry}` of `#{title}` not found" if e.nil?
|
|
||||||
img = e.read_page page
|
|
||||||
raise "Failed to load page #{page} of `#{title}/#{entry}`"\
|
|
||||||
if img.nil?
|
|
||||||
|
|
||||||
send_img env, img
|
|
||||||
rescue e
|
|
||||||
@context.error e
|
|
||||||
env.response.status_code = 500
|
|
||||||
e.message
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/api/book/:title" do |env|
|
|
||||||
begin
|
|
||||||
title = env.params.url["title"]
|
|
||||||
|
|
||||||
t = @context.library.get_title title
|
|
||||||
raise "Title `#{title}` not found" if t.nil?
|
|
||||||
|
|
||||||
send_json env, t.to_json
|
|
||||||
rescue e
|
|
||||||
@context.error e
|
|
||||||
env.response.status_code = 500
|
|
||||||
e.message
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/api/book" do |env|
|
|
||||||
send_json env, @context.library.to_json
|
|
||||||
end
|
|
||||||
|
|
||||||
post "/api/admin/scan" do |env|
|
|
||||||
start = Time.utc
|
|
||||||
@context.library.scan
|
|
||||||
ms = (Time.utc - start).total_milliseconds
|
|
||||||
send_json env, {
|
|
||||||
"milliseconds" => ms,
|
|
||||||
"titles" => @context.library.titles.size
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
|
|
||||||
post "/api/admin/user/delete/:username" do |env|
|
|
||||||
begin
|
|
||||||
username = env.params.url["username"]
|
|
||||||
@context.storage.delete_user username
|
|
||||||
rescue e
|
|
||||||
@context.error e
|
|
||||||
send_json env, {
|
|
||||||
"success" => false,
|
|
||||||
"error" => e.message
|
|
||||||
}.to_json
|
|
||||||
else
|
|
||||||
send_json env, {"success" => true}.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
post "/api/progress/:title/:entry/:page" do |env|
|
|
||||||
begin
|
|
||||||
username = get_username env
|
|
||||||
title = (@context.library.get_title env.params.url["title"])
|
|
||||||
.not_nil!
|
|
||||||
entry = (title.get_entry env.params.url["entry"]).not_nil!
|
|
||||||
page = env.params.url["page"].to_i
|
|
||||||
|
|
||||||
raise "incorrect page value" if page < 0 || page > entry.pages
|
|
||||||
title.save_progress username, entry.title, page
|
|
||||||
rescue e
|
|
||||||
@context.error e
|
|
||||||
send_json env, {
|
|
||||||
"success" => false,
|
|
||||||
"error" => e.message
|
|
||||||
}.to_json
|
|
||||||
else
|
|
||||||
send_json env, {"success" => true}.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Kemal.config.logging = false
|
Kemal.config.logging = false
|
||||||
add_handler LogHandler.new @context.logger
|
add_handler LogHandler.new @context.logger
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ require "./util"
|
|||||||
|
|
||||||
class FS
|
class FS
|
||||||
extend BakedFileSystem
|
extend BakedFileSystem
|
||||||
{% if read_file? "./dist" %}
|
{% if read_file? "#{__DIR__}/../dist/favicon.ico" %}
|
||||||
|
{% puts "baking ../dist" %}
|
||||||
bake_folder "../dist"
|
bake_folder "../dist"
|
||||||
{% else %}
|
{% else %}
|
||||||
|
{% puts "baking ../public" %}
|
||||||
bake_folder "../public"
|
bake_folder "../public"
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|||||||
+29
-2
@@ -12,7 +12,7 @@ def verify_password(hash, pw)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def random_str
|
def random_str
|
||||||
Base64.strict_encode UUID.random().to_s
|
UUID.random.to_s.gsub "-", ""
|
||||||
end
|
end
|
||||||
|
|
||||||
class Storage
|
class Storage
|
||||||
@@ -25,10 +25,18 @@ class Storage
|
|||||||
end
|
end
|
||||||
DB.open "sqlite3://#{path}" do |db|
|
DB.open "sqlite3://#{path}" do |db|
|
||||||
begin
|
begin
|
||||||
|
# We create the `ids` table first. even if the uses has an
|
||||||
|
# early version installed and has the `user` table only,
|
||||||
|
# we will still be able to create `ids`
|
||||||
|
db.exec "create table ids" \
|
||||||
|
"(path text, id text, is_title integer)"
|
||||||
|
db.exec "create unique index path_idx on ids (path)"
|
||||||
|
db.exec "create unique index id_idx on ids (id)"
|
||||||
|
|
||||||
db.exec "create table users" \
|
db.exec "create table users" \
|
||||||
"(username text, password text, token text, admin integer)"
|
"(username text, password text, token text, admin integer)"
|
||||||
rescue e
|
rescue e
|
||||||
unless e.message == "table users already exists"
|
unless e.message.not_nil!.ends_with? "already exists"
|
||||||
@logger.fatal "Error when checking tables in DB: #{e}"
|
@logger.fatal "Error when checking tables in DB: #{e}"
|
||||||
raise e
|
raise e
|
||||||
end
|
end
|
||||||
@@ -147,4 +155,23 @@ class Storage
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_id(path, is_title)
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
begin
|
||||||
|
id = db.query_one "select id from ids where path = (?)",
|
||||||
|
path, as: {String}
|
||||||
|
return id
|
||||||
|
rescue
|
||||||
|
id = random_str
|
||||||
|
db.exec "insert into ids values (?, ?, ?)", path, id,
|
||||||
|
is_title ? 1 : 0
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(json : JSON::Builder)
|
||||||
|
json.string self
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+19
-4
@@ -1,15 +1,29 @@
|
|||||||
<h2 class=uk-title>Library</h2>
|
<h2 class=uk-title>Library</h2>
|
||||||
<p class="uk-text-meta"><%= titles.size %> titles found</p>
|
<p class="uk-text-meta"><%= titles.size %> titles found</p>
|
||||||
<div class="uk-margin">
|
<div class="uk-grid-small" uk-grid>
|
||||||
|
<div class="uk-margin-bottom uk-width-3-4@s">
|
||||||
<form class="uk-search uk-search-default">
|
<form class="uk-search uk-search-default">
|
||||||
<span uk-search-icon></span>
|
<span uk-search-icon></span>
|
||||||
<input class="uk-search-input" type="search" placeholder="Search">
|
<input class="uk-search-input" type="search" placeholder="Search">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
<div class="uk-margin-bottom uk-width-1-4@s">
|
||||||
|
<div class="uk-form-horizontal">
|
||||||
|
<select class="uk-select" id="sort-select">
|
||||||
|
<option id="name-up">▲ Name</option>
|
||||||
|
<option id="name-down">▼ Name</option>
|
||||||
|
<option id="date-up">▲ Date Modified</option>
|
||||||
|
<option id="date-down">▼ Date Modified</option>
|
||||||
|
<option id="progress-up">▲ Progress</option>
|
||||||
|
<option id="progress-down">▼ Progress</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="item-container" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
<%- titles.each_with_index do |t, i| -%>
|
<%- titles.each_with_index do |t, i| -%>
|
||||||
<div class="item">
|
<div class="item" data-mtime="<%= t.mtime.to_unix %>" data-progress="<%= percentage[i] %>">
|
||||||
<a class="acard" href="/book/<%= t.title %>">
|
<a class="acard" href="/book/<%= t.id %>">
|
||||||
<div class="uk-card uk-card-default">
|
<div class="uk-card uk-card-default">
|
||||||
<div class="uk-card-media-top">
|
<div class="uk-card-media-top">
|
||||||
<img src="<%= t.entries[0].cover_url %>" alt="">
|
<img src="<%= t.entries[0].cover_url %>" alt="">
|
||||||
@@ -27,4 +41,5 @@
|
|||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<script src="/js/search.js"></script>
|
<script src="/js/search.js"></script>
|
||||||
|
<script src="/js/sort-items.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
+19
-4
@@ -1,17 +1,31 @@
|
|||||||
<div id="alert"></div>
|
<div id="alert"></div>
|
||||||
<h2 class=uk-title><%= title.title %></h2>
|
<h2 class=uk-title><%= title.title %></h2>
|
||||||
<p class="uk-text-meta"><%= title.entries.size %> entries found</p>
|
<p class="uk-text-meta"><%= title.entries.size %> entries found</p>
|
||||||
<div class="uk-margin">
|
<div class="uk-grid-small" uk-grid>
|
||||||
|
<div class="uk-margin-bottom uk-width-3-4@s">
|
||||||
<form class="uk-search uk-search-default">
|
<form class="uk-search uk-search-default">
|
||||||
<span uk-search-icon></span>
|
<span uk-search-icon></span>
|
||||||
<input class="uk-search-input" type="search" placeholder="Search">
|
<input class="uk-search-input" type="search" placeholder="Search">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
<div class="uk-margin-bottom uk-width-1-4@s">
|
||||||
|
<div class="uk-form-horizontal">
|
||||||
|
<select class="uk-select" id="sort-select">
|
||||||
|
<option id="name-up">▲ Name</option>
|
||||||
|
<option id="name-down">▼ Name</option>
|
||||||
|
<option id="date-up">▲ Date Modified</option>
|
||||||
|
<option id="date-down">▼ Date Modified</option>
|
||||||
|
<option id="progress-up">▲ Progress</option>
|
||||||
|
<option id="progress-down">▼ Progress</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="item-container" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
<%- title.entries.each_with_index do |e, i| -%>
|
<%- title.entries.each_with_index do |e, i| -%>
|
||||||
<div class="item">
|
<div class="item" data-mtime="<%= e.mtime.to_unix %>" data-progress="<%= percentage[i] %>">
|
||||||
<a class="acard">
|
<a class="acard">
|
||||||
<div class="uk-card uk-card-default" onclick="showModal('<%= e.title %>', '<%= e.zip_path %>', '<%= e.pages %>', <%= (percentage[i] * 100).round(1) %>, '<%= title.title %>', '<%= e.title %>')">
|
<div class="uk-card uk-card-default" onclick="showModal("<%= e.encoded_path %>", '<%= e.pages %>', <%= (percentage[i] * 100).round(1) %>, "<%= title.encoded_title %>", "<%= e.encoded_title %>", '<%= e.title_id %>', '<%= e.id %>')">
|
||||||
<div class="uk-card-media-top">
|
<div class="uk-card-media-top">
|
||||||
<img src="<%= e.cover_url %>" alt="">
|
<img src="<%= e.cover_url %>" alt="">
|
||||||
</div>
|
</div>
|
||||||
@@ -51,4 +65,5 @@
|
|||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<script src="/js/title.js"></script>
|
<script src="/js/title.js"></script>
|
||||||
<script src="/js/search.js"></script>
|
<script src="/js/search.js"></script>
|
||||||
|
<script src="/js/sort-items.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
Reference in New Issue
Block a user