mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-02 10:55:30 -04:00
Handle library/title sorting on backend (#86)
This commit is contained in:
parent
360913ee78
commit
94a1e63963
@ -1,131 +1,15 @@
|
||||
$(() => {
|
||||
const titleID = $('.data').attr('data-title-id') || 'library';
|
||||
|
||||
const sortItems = () => {
|
||||
$('#sort-select').change(() => {
|
||||
const sort = $('#sort-select').find(':selected').attr('id');
|
||||
localStorage.setItem(`sort-${titleID}`, sort);
|
||||
|
||||
const ary = sort.split('-');
|
||||
const by = ary[0];
|
||||
const dir = ary[1];
|
||||
|
||||
let items = $('.item');
|
||||
items.remove();
|
||||
|
||||
const ctxAry = [];
|
||||
const keyRange = {};
|
||||
if (by === 'auto') {
|
||||
// intelligent sorting
|
||||
items.each((i, item) => {
|
||||
const name = $(item).find('.uk-card-title').text();
|
||||
const regex = /([^0-9\n\r\ ]*)[ ]*([0-9]*\.*[0-9]+)/g;
|
||||
|
||||
const numbers = {};
|
||||
let match = regex.exec(name);
|
||||
while (match) {
|
||||
const key = match[1];
|
||||
const num = parseFloat(match[2]);
|
||||
numbers[key] = num;
|
||||
|
||||
if (!keyRange[key]) {
|
||||
keyRange[key] = [num, num, 1];
|
||||
} else {
|
||||
keyRange[key][2] += 1;
|
||||
if (num < keyRange[key][0]) {
|
||||
keyRange[key][0] = num;
|
||||
} else if (num > keyRange[key][1]) {
|
||||
keyRange[key][1] = num;
|
||||
}
|
||||
}
|
||||
|
||||
match = regex.exec(name);
|
||||
}
|
||||
ctxAry.push({
|
||||
index: i,
|
||||
numbers: numbers
|
||||
});
|
||||
});
|
||||
|
||||
console.log(keyRange);
|
||||
|
||||
const sortedKeys = Object.keys(keyRange).filter(k => {
|
||||
return keyRange[k][2] >= items.length / 2;
|
||||
});
|
||||
|
||||
sortedKeys.sort((a, b) => {
|
||||
// sort by frequency of the key first
|
||||
if (keyRange[a][2] !== keyRange[b][2]) {
|
||||
return (keyRange[a][2] < keyRange[b][2]) ? 1 : -1;
|
||||
}
|
||||
// then sort by range of the key
|
||||
return ((keyRange[a][1] - keyRange[a][0]) < (keyRange[b][1] - keyRange[b][0])) ? 1 : -1;
|
||||
});
|
||||
|
||||
console.log(sortedKeys);
|
||||
|
||||
ctxAry.sort((a, b) => {
|
||||
for (let i = 0; i < sortedKeys.length; i++) {
|
||||
const key = sortedKeys[i];
|
||||
|
||||
if (a.numbers[key] === undefined && b.numbers[key] === undefined)
|
||||
continue;
|
||||
if (a.numbers[key] === undefined)
|
||||
return 1;
|
||||
if (b.numbers[key] === undefined)
|
||||
return -1;
|
||||
if (a.numbers[key] === b.numbers[key])
|
||||
continue;
|
||||
return (a.numbers[key] > b.numbers[key]) ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const sortedItems = [];
|
||||
ctxAry.forEach(ctx => {
|
||||
sortedItems.push(items[ctx.index]);
|
||||
});
|
||||
items = sortedItems;
|
||||
|
||||
if (dir === 'down') {
|
||||
items.reverse();
|
||||
}
|
||||
} else {
|
||||
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 if (by === 'progress') {
|
||||
const ap = parseFloat($(a).attr('data-progress'));
|
||||
const bp = parseFloat($(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 ? 1 : -1;
|
||||
else
|
||||
return !res ? 1 : -1;
|
||||
});
|
||||
}
|
||||
$('#item-container').append(items);
|
||||
setupAcard();
|
||||
};
|
||||
|
||||
$('#sort-select').change(() => {
|
||||
sortItems();
|
||||
const url = `${location.protocol}//${location.host}${location.pathname}`;
|
||||
const newURL = `${url}?${$.param({
|
||||
sort: by,
|
||||
ascend: dir === 'up' ? 1 : 0
|
||||
})}`;
|
||||
window.location.href = newURL;
|
||||
});
|
||||
|
||||
const sortID = localStorage.getItem(`sort-${titleID}`);
|
||||
if (sortID)
|
||||
$(`option#${sortID}`).attr('selected', '');
|
||||
else if ($('option#auto-up').length > 0)
|
||||
$('option#auto-up').attr('selected', '');
|
||||
else
|
||||
$('option#name-up').attr('selected', '');
|
||||
|
||||
sortItems();
|
||||
});
|
||||
|
@ -38,6 +38,9 @@ end
|
||||
describe "chapter_sort" do
|
||||
it "sorts correctly" do
|
||||
ary = ["Vol.1 Ch.01", "Vol.1 Ch.02", "Vol.2 Ch. 2.5", "Ch. 3", "Ch.04"]
|
||||
chapter_sort(ary.reverse).should eq ary
|
||||
sorter = ChapterSorter.new ary
|
||||
ary.reverse.sort do |a, b|
|
||||
sorter.compare a, b
|
||||
end.should eq ary
|
||||
end
|
||||
end
|
||||
|
185
src/library.cr
185
src/library.cr
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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" %>
|
||||
|
@ -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 %>
|
||||
|
Loading…
x
Reference in New Issue
Block a user