This commit is contained in:
Alex Ling 2020-02-23 18:35:27 +00:00
parent 5b34c05243
commit 650ebc7f9d
8 changed files with 92 additions and 49 deletions

View File

@ -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();
} }

View File

@ -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,11 +15,14 @@ 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
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 @pages = Zip::File.new(path).entries
.select { |e| .select { |e|
@ -26,7 +30,8 @@ class Entry
MIME.from_filename? e.filename MIME.from_filename? e.filename
} }
.size .size
@cover_url = "/api/page/#{@book_title}/#{title}/1" @id = storage.get_id @zip_path, false
@cover_url = "/api/page/#{@title_id}/#{@id}/1"
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 +56,27 @@ 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
def initialize(dir : String) def initialize(dir : String, storage)
@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|
Entry.new File.join(dir, 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 }
end end
def get_entry(name) def get_entry(eid)
@entries.find { |e| e.title == name } @entries.find { |e| e.id == eid }
end 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 +87,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 +97,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 +148,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 +167,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,7 +178,7 @@ 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 }
.select { |title| !title.entries.empty? } .select { |title| !title.entries.empty? }
@logger.debug "Scan completed" @logger.debug "Scan completed"
@logger.debug "Scanned library: \n#{self.to_pretty_json}" @logger.debug "Scanned library: \n#{self.to_pretty_json}"

View File

@ -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

View File

@ -2,19 +2,20 @@ require "./router"
class APIRouter < Router class APIRouter < Router
def setup def setup
get "/api/page/:title/:entry/:page" do |env| get "/api/page/:tid/:eid/:page" do |env|
begin begin
title = env.params.url["title"] tid = env.params.url["tid"]
entry = env.params.url["entry"] eid = env.params.url["eid"]
page = env.params.url["page"].to_i page = env.params.url["page"].to_i
t = @context.library.get_title title title = @context.library.get_title tid
raise "Title `#{title}` not found" if t.nil? raise "Title ID `#{tid}` not found" if title.nil?
e = t.get_entry entry entry = title.get_entry eid
raise "Entry `#{entry}` of `#{title}` not found" if e.nil? raise "Entry ID `#{eid}` of `#{title.title}` not found" if \
img = e.read_page page entry.nil?
raise "Failed to load page #{page} of `#{title}/#{entry}`"\ img = entry.read_page page
if img.nil? raise "Failed to load page #{page} of " \
"`#{title.title}/#{entry.title}`" if img.nil?
send_img env, img send_img env, img
rescue e rescue e
@ -26,12 +27,11 @@ class APIRouter < Router
get "/api/book/:title" do |env| get "/api/book/:title" do |env|
begin begin
title = env.params.url["title"] tid = env.params.url["tid"]
title = @context.library.get_title tid
raise "Title ID `#{tid}` not found" if title.nil?
t = @context.library.get_title title send_json env, title.to_json
raise "Title `#{title}` not found" if t.nil?
send_json env, t.to_json
rescue e rescue e
@context.error e @context.error e
env.response.status_code = 500 env.response.status_code = 500

View File

@ -16,7 +16,7 @@ class ReaderRouter < Router
# 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
env.redirect "/reader/#{title.title}/#{entry.title}/#{page}" env.redirect "/reader/#{title.id}/#{entry.id}/#{page}"
rescue e rescue e
@context.error e @context.error e
env.response.status_code = 404 env.response.status_code = 404
@ -37,16 +37,16 @@ class ReaderRouter < Router
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|
"/api/page/#{title.title}/#{entry.title}/#{idx}" } "/api/page/#{title.id}/#{entry.id}/#{idx}" }
reader_urls = pages.map { |idx| reader_urls = pages.map { |idx|
"/reader/#{title.title}/#{entry.title}/#{idx}" } "/reader/#{title.id}/#{entry.id}/#{idx}" }
next_page = page + IMGS_PER_PAGE next_page = page + IMGS_PER_PAGE
next_url = next_page > entry.pages ? nil : next_url = next_page > entry.pages ? nil :
"/reader/#{title.title}/#{entry.title}/#{next_page}" "/reader/#{title.id}/#{entry.id}/#{next_page}"
exit_url = "/book/#{title.title}" exit_url = "/book/#{title.id}"
next_entry = title.next_entry entry next_entry = title.next_entry entry
next_entry_url = next_entry.nil? ? nil : \ next_entry_url = next_entry.nil? ? nil : \
"/reader/#{title.title}/#{next_entry.title}" "/reader/#{title.id}/#{next_entry.id}"
render "src/views/reader.ecr" render "src/views/reader.ecr"
rescue e rescue e

View File

@ -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

View File

@ -9,7 +9,7 @@
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid> <div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
<%- titles.each_with_index do |t, i| -%> <%- titles.each_with_index do |t, i| -%>
<div class="item"> <div class="item">
<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="">

View File

@ -11,7 +11,7 @@
<%- title.entries.each_with_index do |e, i| -%> <%- title.entries.each_with_index do |e, i| -%>
<div class="item"> <div class="item">
<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(&quot;<%= e.encoded_path %>&quot;, '<%= e.pages %>', <%= (percentage[i] * 100).round(1) %>, &quot;<%= title.encoded_title %>&quot;, &quot;<%= e.encoded_title %>&quot;, '<%= 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>