Merge branch 'feature/home' into dev

This commit is contained in:
Alex Ling 2020-06-08 15:11:09 +00:00
commit 53c3798691
9 changed files with 331 additions and 14 deletions

View File

@ -15,10 +15,14 @@ function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTi
if (percentage === 100) {
$('#read-btn').attr('hidden', '');
}
$('#modal-title').find('span').text(entry);
$('#modal-title').next().attr('data-id', titleID);
$('#modal-title').next().attr('data-entry-id', entryID);
$('#modal-title').next().find('.title-rename-field').val(entry);
$('#modal-title-link').text(title);
$('#modal-title-link').attr('href', '/book/' + titleID);
$('#modal-entry-title').find('span').text(entry);
$('#modal-entry-title').next().attr('data-id', titleID);
$('#modal-entry-title').next().attr('data-entry-id', entryID);
$('#modal-entry-title').next().find('.title-rename-field').val(entry);
$('#path-text').text(zipPath);
$('#pages-text').text(pages + ' pages');

View File

@ -3,6 +3,8 @@ require "yaml"
class Config
include YAML::Serializable
@[YAML::Field(ignore: true)]
property path : String = ""
property port : Int32 = 9000
property base_url : String = "/"
property session_secret : String = "mango-session-secret"
@ -44,6 +46,7 @@ class Config
if File.exists? cfg_path
config = self.from_yaml File.read cfg_path
config.preprocess
config.path = path
config.fill_defaults
return config
end
@ -54,6 +57,7 @@ class Config
abort "Aborting..."
end
default = self.allocate
default.path = path
default.fill_defaults
cfg_dir = File.dirname cfg_path
unless Dir.exists? cfg_dir

View File

@ -17,7 +17,8 @@ end
class Entry
property zip_path : String, book : Title, title : 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)
@zip_path = path
@ -33,6 +34,7 @@ class Entry
file.close
@id = storage.get_id @zip_path, false
@mtime = File.info(@zip_path).modification_time
@date_added = load_date_added
end
def to_json(json : JSON::Builder)
@ -87,6 +89,20 @@ class Entry
end
img
end
private def load_date_added
date_added = nil
TitleInfo.new @book.dir do |info|
info_da = info.date_added[@title]?
if info_da.nil?
date_added = info.date_added[@title] = ctime @zip_path
info.save
else
date_added = info_da
end
end
date_added.not_nil! # is it ok to set not_nil! here?
end
end
class Title
@ -289,6 +305,12 @@ class Title
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
@ -304,14 +326,14 @@ class Title
progress
end
def load_percetage(username, entry)
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
def load_percetage(username)
def load_percentage(username)
return 0.0 if @entries.empty?
read_pages = total_pages = 0
@entries.each do |e|
@ -321,11 +343,53 @@ class Title
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
def get_continue_reading_entry(username)
in_progress_entries = @entries.select do |e|
load_progress(username, e.title) > 0
end
return nil if in_progress_entries.empty?
latest_read_entry = in_progress_entries[-1]
if load_progress(username, latest_read_entry.title) ==
latest_read_entry.pages
next_entry latest_read_entry
else
latest_read_entry
end
end
# TODO: More concise title?
def get_last_read_for_continue_reading(username, entry_obj)
last_read = load_last_read username, entry_obj.title
if last_read.nil? # grab from previous entry if current entry hasn't been started yet
previous_entry = previous_entry(entry_obj)
return load_last_read username, previous_entry.title if previous_entry
end
last_read
end
end
class TitleInfo
@ -337,6 +401,8 @@ class TitleInfo
property entry_display_name = {} of String => String
property cover_url = ""
property entry_cover_url = {} of String => String
property last_read = {} of String => Hash(String, Time)
property date_added = {} of String => Time
@[JSON::Field(ignore: true)]
property dir : String = ""
@ -440,4 +506,85 @@ class Library
end
Logger.debug "Scan completed"
end
def get_continue_reading_entries(username)
# map: get the continue-reading entry or nil for each Title
# select: select only entries (and ignore Nil's) from the array
# produced by map
continue_reading_entries = titles.map { |t|
get_continue_reading_entry username, t
}.select Entry
continue_reading = continue_reading_entries.map { |e|
{
entry: e,
percentage: e.book.load_percentage(username, e.title),
last_read: get_relevant_last_read(username, e)
}
}
# Sort by by last_read, most recent first (nils at the end)
continue_reading.sort! { |a, b|
next 0 if a[:last_read].nil? && b[:last_read].nil?
next 1 if a[:last_read].nil?
next -1 if b[:last_read].nil?
b[:last_read].not_nil! <=> a[:last_read].not_nil!
}[0..11]
end
alias RA = NamedTuple(entry: Entry, percentage: Float64, grouped_count: Int32)
def get_recently_added_entries(username)
entries = [] of Entry
titles.each do |t|
t.entries.each { |e| entries << e }
end
entries.sort! { |a, b| b.date_added <=> a.date_added }
entries.select! { |e| e.date_added > 3.months.ago }
# Group Entries if neighbour is same Title
recently_added = [] of RA
entries.each do |e|
last = recently_added.last?
if last && e.title_id == last[:entry].title_id
# 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
recently_added[recently_added.size - 1] = RA.from last_hash
else
recently_added << {
entry: e,
percentage: e.book.load_percentage(username, e.title),
grouped_count: 1,
}
end
end
recently_added[0..11]
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
if last_read.nil? # grab from previous entry if current entry hasn't been started yet
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

View File

@ -37,12 +37,12 @@ class MainRouter < Router
end
end
get "/" do |env|
get "/library" do |env|
begin
titles = @context.library.titles
username = get_username env
percentage = titles.map &.load_percetage username
layout "index"
percentage = titles.map &.load_percentage username
layout "library"
rescue e
@context.error e
env.response.status_code = 500
@ -54,7 +54,7 @@ class MainRouter < Router
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
title.load_percentage username, e.title
}
layout "title"
rescue e
@ -67,5 +67,20 @@ class MainRouter < Router
mangadex_base_url = Config.current.mangadex["base_url"]
layout "download"
end
get "/" do |env|
begin
username = get_username env
continue_reading = @context.library.get_continue_reading_entries username
recently_added = @context.library.get_recently_added_entries username
titles = @context.library.titles
new_user = ! titles.any? { |t| t.load_percentage(username) > 0 }
empty_library = titles.size == 0
layout "home"
rescue e
@context.error e
env.response.status_code = 500
end
end
end
end

View File

@ -135,3 +135,16 @@ end
macro render_component(filename)
render "src/views/components/#{{{filename}}}.ecr"
end
# Works in all Unix systems. Follows https://github.com/crystal-lang/crystal/
# blob/master/src/crystal/system/unix/file_info.cr#L42-L48
def ctime(file_path : String) : Time
res = LibC.stat(file_path, out stat)
raise "Unable to get ctime of file #{file_path}" if res != 0
{% if flag?(:darwin) %}
Time.new stat.st_ctimespec, Time::Location::UTC
{% else %}
Time.new stat.st_ctim, Time::Location::UTC
{% end %}
end

132
src/views/home.ecr Normal file
View File

@ -0,0 +1,132 @@
<%- 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="/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="/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="/img/banner.png" style="max-width: 400px; padding: 0 20px;">
<p>A self-hosted manga server and reader</p>
<a href="/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| -%>
<div class="item" data-mtime="<%= cr[:entry].mtime.to_unix %>" data-progress="<%= cr[:percentage] %>" id="<%= cr[:entry].id %>">
<a class="acard">
<div class="uk-card uk-card-default" onclick="showModal(&quot;<%= cr[:entry].encoded_path %>&quot;, '<%= cr[:entry].pages %>', <%= (cr[:percentage] * 100).round(1) %>, &quot;<%= cr[:entry].book.encoded_display_name %>&quot;, &quot;<%= cr[:entry].encoded_display_name %>&quot;, '<%= cr[:entry].title_id %>', '<%= cr[:entry].id %>')">
<div class="uk-card-media-top">
<img data-src="<%= cr[:entry].cover_url %>" alt="" data-width data-height uk-img>
</div>
<div class="uk-card-body">
<div class="uk-card-badge uk-label"><%= (cr[:percentage] * 100).round(1) %>%</div>
<h3 class="uk-card-title break-word" data-title="<%= cr[:entry].display_name.gsub("\"", "&quot;") %>"><%= cr[:entry].display_name %></h3>
<p><%= cr[:entry].pages %> pages</p>
</div>
</div>
</a>
</div>
<%- 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| -%>
<%- if ra[:grouped_count] == 1 -%>
<div class="item" data-progress="<%= ra[:percentage] %>" id="<%= ra[:entry].id %>">
<a class="acard">
<div class="uk-card uk-card-default" onclick="showModal(&quot;<%= ra[:entry].encoded_path %>&quot;, '<%= ra[:entry].pages %>', <%= (ra[:percentage] * 100).round(1) %>, &quot;<%= ra[:entry].book.encoded_display_name %>&quot;, &quot;<%= ra[:entry].encoded_display_name %>&quot;, '<%= ra[:entry].title_id %>', '<%= ra[:entry].id %>')">
<div class="uk-card-media-top">
<img data-src="<%= ra[:entry].cover_url %>" alt="" data-width data-height uk-img>
</div>
<div class="uk-card-body">
<div class="uk-card-badge uk-label"><%= (ra[:percentage] * 100).round(1) %>%</div>
<h3 class="uk-card-title break-word" data-title="<%= ra[:entry].display_name.gsub("\"", "&quot;") %>"><%= ra[:entry].display_name %></h3>
<p><%= ra[:entry].pages %> pages</p>
</div>
</div>
</a>
</div>
<%- else -%>
<div class="item">
<a class="acard" href="/book/<%= ra[:entry].book.id %>">
<div class="uk-card uk-card-default">
<div class="uk-card-media-top">
<img data-src="<%= ra[:entry].cover_url %>" data-width data-height alt="" uk-img>
</div>
<div class="uk-card-body">
<h3 class="uk-card-title break-word" data-title="<%= ra[:entry].book.display_name.gsub("\"", "&quot;") %>"><%= ra[:entry].book.display_name %></h3>
<p><%= ra[:grouped_count] %> new entries</p>
</div>
</div>
</a>
</div>
<%- end -%>
<%- end -%>
</div>
<%- end -%>
<!-- TODO: DRY this code with calls in other ecr files? eg. title.ecr -->
<div id="modal" 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">
<div>
<h4 class="uk-margin-remove-bottom"><a id="modal-title-link"></a></h4>
<h3 class="uk-modal-title break-word uk-margin-remove-top" id="modal-entry-title"><span></span></h3>
</div>
<p class="uk-text-meta uk-margin-remove-bottom break-word" id="path-text"></p>
<p class="uk-text-meta uk-margin-remove-top" id="pages-text"></p>
</div>
<div class="uk-modal-body">
<p>Read</p>
<p uk-margin>
<a id="beginning-btn" class="uk-button uk-button-default">From beginning</a>
<a id="continue-btn" class="uk-button uk-button-primary"></a>
</p>
<p>Progress</p>
<p uk-margin>
<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>
</p>
</div>
</div>
</div>
<%- end -%>
<% content_for "script" do %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.dotdotdot/4.0.11/dotdotdot.js"></script>
<script src="/js/dots.js"></script>
<script src="/js/alert.js"></script>
<script src="/js/title.js"></script>
<% end %>

View File

@ -23,6 +23,7 @@
<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>
@ -44,6 +45,7 @@
<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>

View File

@ -7,7 +7,7 @@
</h2>
</div>
<ul class="uk-breadcrumb">
<li><a href="<%= base_url %>">Library</a></li>
<li><a href="<%= base_url %>library">Library</a></li>
<%- title.parents.each do |t| -%>
<li><a href="<%= base_url %>book/<%= t.id %>"><%= t.display_name %></a></li>
<%- end -%>
@ -47,7 +47,7 @@
<button class="uk-modal-close-default" type="button" uk-close></button>
<div class="uk-modal-header">
<div>
<h3 class="uk-modal-title break-word" id="modal-title"><span></span>
<h3 class="uk-modal-title break-word" id="modal-entry-title"><span></span>
&nbsp;
<% if is_admin %>
<a class="uk-icon-button" uk-icon="icon:pencil"></a>
@ -77,7 +77,7 @@
<button class="uk-modal-close-default" type="button" uk-close></button>
<div class="uk-modal-header">
<div>
<h3 class="uk-modal-title break-word">Edit</h3>
<h3 class="uk-modal-title break-word" id="modal-entry-title">Edit</h3>
</div>
</div>
<div class="uk-modal-body">