Handle library/title sorting on backend (#86)

This commit is contained in:
Alex Ling
2020-07-15 10:34:53 +00:00
parent 360913ee78
commit 94a1e63963
10 changed files with 263 additions and 205 deletions

View File

@@ -6,6 +6,45 @@ require "./archive"
SUPPORTED_IMG_TYPES = ["image/jpeg", "image/png", "image/webp"]
enum SortMethod
Auto
Title
Progress
TimeModified
TimeAdded
end
class SortOptions
property method : SortMethod, ascend : Bool
def initialize(in_method : String? = nil, @ascend = true)
@method = SortMethod::Auto
SortMethod.each do |m, _|
if in_method && m.to_s.underscore == in_method
@method = m
return
end
end
end
def initialize(in_method : SortMethod? = nil, @ascend = true)
if in_method
@method = in_method
else
@method = SortMethod::Auto
end
end
def self.from_tuple(tp : Tuple(String, Bool))
method, ascend = tp
self.new method, ascend
end
def to_tuple
{@method.to_s.underscore, ascend}
end
end
struct Image
property data : Bytes
property mime : String
@@ -99,10 +138,11 @@ class Entry
img
end
def next_entry
idx = @book.entries.index self
return nil if idx.nil? || idx == @book.entries.size - 1
@book.entries[idx + 1]
def next_entry(username)
entries = @book.sorted_entries username
idx = entries.index self
return nil if idx.nil? || idx == entries.size - 1
entries[idx + 1]
end
def previous_entry
@@ -239,8 +279,9 @@ class Title
compare_numerically @library.title_hash[a].title,
@library.title_hash[b].title
end
sorter = ChapterSorter.new @entries.map { |e| e.title }
@entries.sort! do |a, b|
compare_numerically a.title, b.title
sorter.compare a.title, b.title
end
end
@@ -405,28 +446,20 @@ class Title
deep_read_page_count(username) / deep_total_page_count
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
def load_progress_for_all_entries(username)
def load_progress_for_all_entries(username, opt : SortOptions? = nil,
unsorted = false)
progress = {} of String => Int32
TitleInfo.new @dir do |info|
progress = info.progress[username]?
end
@entries.map do |e|
if unsorted
ary = @entries
else
ary = sorted_entries username, opt
end
ary.map do |e|
info_progress = 0
if progress && progress.has_key? e.title
info_progress = [progress[e.title], e.pages].min
@@ -435,13 +468,71 @@ class Title
end
end
def load_percentage_for_all_entries(username)
progress = load_progress_for_all_entries username
@entries.map_with_index do |e, i|
def load_percentage_for_all_entries(username, opt : SortOptions? = nil,
unsorted = false)
if unsorted
ary = @entries
else
ary = sorted_entries username, opt
end
progress = load_progress_for_all_entries username, opt, unsorted
ary.map_with_index do |e, i|
progress[i] / e.pages
end
end
# Returns the sorted entries array
#
# When `opt` is nil, it uses the preferred sorting options in info.json, or
# use the default (auto, ascending)
# When `opt` is not nil, it saves the options to info.json
def sorted_entries(username, opt : SortOptions? = nil)
if opt.nil?
opt = load_sort_options username
else
TitleInfo.new @dir do |info|
info.sort_by[username] = opt.to_tuple
info.save
end
end
case opt.not_nil!.method
when .title?
ary = @entries.sort { |a, b| compare_numerically a.title, b.title }
when .time_modified?
ary = @entries.sort { |a, b| a.mtime <=> b.mtime }
when .time_added?
ary = @entries.sort { |a, b| a.date_added <=> b.date_added }
when .progress?
percentage_ary = load_percentage_for_all_entries username, opt, true
ary = @entries.zip(percentage_ary)
.sort { |a_tp, b_tp| a_tp[1] <=> b_tp[1] }
.map { |tp| tp[0] }
when .auto?
sorter = ChapterSorter.new @entries.map { |e| e.title }
ary = @entries.sort do |a, b|
sorter.compare a.title, b.title
end
else
raise "Unknown sorting method #{opt.not_nil!.method}"
end
ary.reverse! unless opt.not_nil!.ascend
ary
end
def load_sort_options(username)
opt = SortOptions.new
TitleInfo.new @dir do |info|
if info.sort_by.has_key? username
opt = SortOptions.from_tuple info.sort_by[username]
end
end
opt
end
# === helper methods ===
# Gets the last read entry in the title. If the entry has been completed,
@@ -464,7 +555,7 @@ class Title
end
if last_read_entry && last_read_entry.finished? username
last_read_entry = last_read_entry.next_entry
last_read_entry = last_read_entry.next_entry username
end
last_read_entry
@@ -511,6 +602,7 @@ class TitleInfo
property entry_cover_url = {} of String => String
property last_read = {} of String => Hash(String, Time)
property date_added = {} of String => Time
property sort_by = {} of String => Tuple(String, Bool)
@[JSON::Field(ignore: true)]
property dir : String = ""
@@ -693,4 +785,45 @@ class Library
recently_added[0..11]
end
def sorted_titles(username, opt : SortOptions? = nil)
if opt.nil?
opt = load_sort_options username
else
TitleInfo.new @dir do |info|
info.sort_by[username] = opt.to_tuple
info.save
end
end
# This is a hack to bypass a compiler bug
ary = titles
case opt.not_nil!.method
when .auto?
ary.sort! { |a, b| compare_numerically a.title, b.title }
when .time_modified?
ary.sort! { |a, b| a.mtime <=> b.mtime }
when .progress?
ary.sort! do |a, b|
a.load_percentage(username) <=> b.load_percentage(username)
end
else
raise "Unknown sorting method #{opt.not_nil!.method}"
end
ary.reverse! unless opt.not_nil!.ascend
ary
end
def load_sort_options(username)
opt = SortOptions.new
TitleInfo.new @dir do |info|
if info.sort_by.has_key? username
opt = SortOptions.from_tuple info.sort_by[username]
end
end
opt
end
end

View File

@@ -39,9 +39,14 @@ class MainRouter < Router
get "/library" do |env|
begin
titles = @context.library.titles
username = get_username env
sort_opt = @context.library.load_sort_options username
get_sort_opt
titles = @context.library.sorted_titles username, sort_opt
percentage = titles.map &.load_percentage username
layout "library"
rescue e
@context.error e
@@ -53,12 +58,18 @@ class MainRouter < Router
begin
title = (@context.library.get_title env.params.url["title"]).not_nil!
username = get_username env
percentage = title.load_percentage_for_all_entries username
sort_opt = title.load_sort_options username
get_sort_opt
entries = title.sorted_entries username, sort_opt
percentage = title.load_percentage_for_all_entries username, sort_opt
title_percentage = title.titles.map &.load_percentage username
layout "title"
rescue e
@context.error e
env.response.status_code = 404
env.response.status_code = 500
end
end

View File

@@ -48,7 +48,7 @@ class ReaderRouter < Router
next_page = page + IMGS_PER_PAGE
next_url = next_entry_url = nil
exit_url = "#{base_url}book/#{title.id}"
next_entry = entry.next_entry
next_entry = entry.next_entry username
unless next_page > entry.pages
next_url = "#{base_url}reader/#{title.id}/#{entry.id}/#{next_page}"
end

View File

@@ -7,9 +7,9 @@
require "big"
private class Item
getter index : Int32, numbers : Hash(String, BigDecimal)
getter numbers : Hash(String, BigDecimal)
def initialize(@index, @numbers)
def initialize(@numbers)
end
# Compare with another Item using keys
@@ -51,57 +51,62 @@ private class KeyRange
end
end
def chapter_sort(in_ary : Array(String)) : Array(String)
ary = in_ary.sort do |a, b|
compare_numerically a, b
class ChapterSorter
@sorted_keys = [] of String
def initialize(str_ary : Array(String))
keys = {} of String => KeyRange
str_ary.each do |str|
scan str do |k, v|
if keys.has_key? k
keys[k].update v
else
keys[k] = KeyRange.new v
end
end
end
# Get the array of keys string and sort them
@sorted_keys = keys.keys
# Only use keys that are present in over half of the strings
.select do |key|
keys[key].count >= str_ary.size / 2
end
.sort do |a_key, b_key|
a = keys[a_key]
b = keys[b_key]
# Sort keys by the number of times they appear
count_compare = b.count <=> a.count
if count_compare == 0
# Then sort by value range
b.range <=> a.range
else
count_compare
end
end
end
items = [] of Item
keys = {} of String => KeyRange
ary.each_with_index do |str, i|
numbers = {} of String => BigDecimal
def compare(a : String, b : String)
item_a = str_to_item a
item_b = str_to_item b
item_a.<=>(item_b, @sorted_keys)
end
private def scan(str, &)
str.scan /([^0-9\n\r\ ]*)[ ]*([0-9]*\.*[0-9]+)/ do |match|
key = match[1]
num = match[2].to_big_d
numbers[key] = num
if keys.has_key? key
keys[key].update num
else
keys[key] = KeyRange.new num
end
yield key, num
end
items << Item.new(i, numbers)
end
# Get the array of keys string and sort them
sorted_keys = keys.keys
# Only use keys that are present in over half of the strings
.select do |key|
keys[key].count >= ary.size / 2
end
.sort do |a_key, b_key|
a = keys[a_key]
b = keys[b_key]
# Sort keys by the number of times they appear
count_compare = b.count <=> a.count
if count_compare == 0
# Then sort by value range
b.range <=> a.range
else
count_compare
end
end
items
.sort do |a, b|
a.<=>(b, sorted_keys)
end
.map do |item|
ary[item.index]
private def str_to_item(str)
numbers = {} of String => BigDecimal
scan str do |k, v|
numbers[k] = v
end
Item.new numbers
end
end

View File

@@ -66,3 +66,18 @@ end
macro render_component(filename)
render "src/views/components/#{{{filename}}}.html.ecr"
end
macro get_sort_opt
sort_method = env.params.query["sort"]?
if sort_method
is_ascending = true
ascend = env.params.query["ascend"]?
if ascend && ascend.to_i? == 0
is_ascending = false
end
sort_opt = SortOptions.new sort_method, is_ascending
end
end

View File

@@ -1,8 +1,14 @@
<div class="uk-form-horizontal">
<select class="uk-select" id="sort-select">
<% hash.each do |k, v| %>
<option id="<%= k %>-up">▲ <%= v %></option>
<option id="<%= k %>-down">▼ <%= v %></option>
<option id="<%= k %>-up"
<% if sort_opt && k == sort_opt.method.to_s.underscore && sort_opt.ascend %>
<%= "selected" %>
<% end %>>▲ <%= v %></option>
<option id="<%= k %>-down"
<% if sort_opt && k == sort_opt.method.to_s.underscore && !sort_opt.ascend %>
<%= "selected" %>
<% end %>>▼ <%= v %></option>
<% end %>
</select>
</div>

View File

@@ -9,8 +9,8 @@
</div>
<div class="uk-margin-bottom uk-width-1-4@s">
<% hash = {
"name" => "Name",
"date" => "Date Modified",
"auto" => "Auto",
"time_modified" => "Date Modified",
"progress" => "Progress"
} %>
<%= render_component "sort-form" %>

View File

@@ -25,8 +25,9 @@
<div class="uk-margin-bottom uk-width-1-4@s">
<% hash = {
"auto" => "Auto",
"name" => "Name",
"date" => "Date Modified",
"title" => "Name",
"time_modified" => "Date Modified",
"time_added" => "Date Added",
"progress" => "Progress"
} %>
<%= render_component "sort-form" %>
@@ -37,7 +38,7 @@
<% progress = title_percentage[i] %>
<%= render_component "card" %>
<% end %>
<% title.entries.each_with_index do |item, i| %>
<% entries.each_with_index do |item, i| %>
<% progress = percentage[i] %>
<%= render_component "card" %>
<% end %>