Compare commits

..

11 Commits

Author SHA1 Message Date
Alex Ling
586ee4f0ba Bump version to v0.10.0 2020-08-02 12:33:31 +00:00
Alex Ling
53f3387e1a Rephrase the plugin part in README 2020-08-02 12:32:14 +00:00
Alex Ling
be5d1918aa Add offset to the sticky bar 2020-08-02 12:29:49 +00:00
Alex Ling
df2cc0ffa9 Display nested titles and entries separately 2020-08-02 10:43:46 +00:00
Alex Ling
b8cfc3a201 Remove unnecessary ids from HTML 2020-08-02 10:43:24 +00:00
Alex Ling
8dc60ac2ea Add select all button to the selection bar 2020-08-02 09:28:31 +00:00
Alex Ling
1719335d02 Add "Start Reading" section to home page (#92) 2020-08-01 15:17:18 +00:00
Alex Ling
0cd46abc66 Finish batch marking (#75) 2020-07-30 11:39:23 +00:00
Alex Ling
e4fd7c58ee Add multi-select for cards in web interface 2020-07-30 08:32:00 +00:00
Alex Ling
d4abee52db Fix .uk-card-media-top width 2020-07-30 08:29:41 +00:00
Alex Ling
d29c94e898 Use Alpine.js 2020-07-30 08:28:54 +00:00
15 changed files with 222 additions and 49 deletions

View File

@@ -13,7 +13,7 @@ Mango is a self-hosted manga server and reader. Its features include
- Supports nested folders in library
- Automatically stores reading progress
- Built-in [MangaDex](https://mangadex.org/) downloader
- [Plugins](https://github.com/hkalexling/mango-plugins) support
- Supports [plugins](https://github.com/hkalexling/mango-plugins) to download from thrid-party sites
- The web reader is responsive and works well on mobile, so there is no need for a mobile app
- All the static files are embedded in the binary, so the deployment process is easy and painless
@@ -51,7 +51,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
### CLI
```
Mango - Manga Server and Web Reader. Version 0.9.0
Mango - Manga Server and Web Reader. Version 0.10.0
Usage:

View File

@@ -7,6 +7,7 @@
}
.uk-card-media-top {
width: 100%;
height: 250px;
}
@@ -122,3 +123,15 @@ td>.uk-dropdown {
.uk-light .uk-description-list>dt {
color: #555;
}
[x-cloak] {
display: none;
}
#select-bar-controls a {
transform: scale(1.5, 1.5);
}
#select-bar-controls a:hover {
color: orange;
}

View File

@@ -182,3 +182,63 @@ const setupUpload = (eid) => {
}
});
};
const deselectAll = () => {
$('.item .uk-card').each((i, e) => {
const data = e.__x.$data;
data['selected'] = false;
});
$('#select-bar')[0].__x.$data['count'] = 0;
};
const selectAll = () => {
let count = 0;
$('.item .uk-card').each((i, e) => {
const data = e.__x.$data;
if (!data['disabled']) {
data['selected'] = true;
count++;
}
});
$('#select-bar')[0].__x.$data['count'] = count;
};
const selectedIDs = () => {
const ary = [];
$('.item .uk-card').each((i, e) => {
const data = e.__x.$data;
if (!data['disabled'] && data['selected']) {
const item = $(e).closest('.item');
ary.push($(item).attr('id'));
}
});
return ary;
};
const bulkProgress = (action, el) => {
const tid = $(el).attr('data-id');
const ids = selectedIDs();
const url = `${base_url}api/bulk-progress/${action}/${tid}`;
$.ajax({
type: 'POST',
url: url,
contentType: "application/json",
dataType: 'json',
data: JSON.stringify({
ids: ids
})
})
.done(data => {
if (data.error) {
alert('danger', `Failed to mark entries as ${action}. Error: ${data.error}`);
return;
}
location.reload();
})
.fail((jqXHR, status) => {
alert('danger', `Failed to mark entries as ${action}. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
.always(() => {
deselectAll();
});
};

View File

@@ -1,5 +1,5 @@
name: mango
version: 0.9.0
version: 0.10.0
authors:
- Alex Ling <hkalexling@gmail.com>

View File

@@ -30,6 +30,41 @@ class Library
@title_ids.map { |tid| self.get_title!(tid) }
end
def sorted_titles(username, opt : SortOptions? = nil)
if opt.nil?
opt = SortOptions.from_info_json @dir, 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 .time_modified?
ary.sort! { |a, b| (a.mtime <=> b.mtime).or \
compare_numerically a.title, b.title }
when .progress?
ary.sort! do |a, b|
(a.load_percentage(username) <=> b.load_percentage(username)).or \
compare_numerically a.title, b.title
end
else
unless opt.method.auto?
Logger.warn "Unknown sorting method #{opt.not_nil!.method}. Using " \
"Auto instead"
end
ary.sort! { |a, b| compare_numerically a.title, b.title }
end
ary.reverse! unless opt.not_nil!.ascend
ary
end
def deep_titles
titles + titles.map { |t| t.deep_titles }.flatten
end
@@ -83,7 +118,7 @@ class Library
cr_entries = deep_titles
.map { |t| t.get_last_read_entry username }
# Select elements with type `Entry` from the array and ignore all `Nil`s
.select(Entry)[0..11]
.select(Entry)[0...ENTRIES_IN_HOME_SECTIONS]
.map { |e|
# Get the last read time of the entry. If it hasn't been started, get
# the last read time of the previous entry
@@ -143,41 +178,20 @@ class Library
end
end
recently_added[0..11]
recently_added[0...ENTRIES_IN_HOME_SECTIONS]
end
def sorted_titles(username, opt : SortOptions? = nil)
if opt.nil?
opt = SortOptions.from_info_json @dir, 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 .time_modified?
ary.sort! { |a, b| (a.mtime <=> b.mtime).or \
compare_numerically a.title, b.title }
when .progress?
ary.sort! do |a, b|
(a.load_percentage(username) <=> b.load_percentage(username)).or \
compare_numerically a.title, b.title
end
else
unless opt.method.auto?
Logger.warn "Unknown sorting method #{opt.not_nil!.method}. Using " \
"Auto instead"
end
ary.sort! { |a, b| compare_numerically a.title, b.title }
end
ary.reverse! unless opt.not_nil!.ascend
ary
def get_start_reading_titles(username)
# Here we are not using `deep_titles` as it may cause unexpected behaviors
# For example, consider the following nested titles:
# - One Puch Man
# - Vol. 1
# - Vol. 2
# If we use `deep_titles`, the start reading section might include `Vol. 2`
# when the user hasn't started `Vol. 1` yet
titles
.select { |t| t.load_percentage(username) == 0 }
.sample(ENTRIES_IN_HOME_SECTIONS)
.shuffle
end
end

View File

@@ -355,4 +355,24 @@ class Title
return zip if title_ids.empty?
zip + titles.map { |t| t.deep_entries_with_date_added }.flatten
end
def bulk_progress(action, ids : Array(String), username)
selected_entries = ids
.map { |id|
@entries.find { |e| e.id == id }
}
.select(Entry)
TitleInfo.new @dir do |info|
selected_entries.each do |e|
page = action == "read" ? e.pages : 0
if info.progress[username]?.nil?
info.progress[username] = {e.title => page}
else
info.progress[username][e.title] = page
end
end
info.save
end
end
end

View File

@@ -6,7 +6,7 @@ require "option_parser"
require "clim"
require "./plugin/*"
MANGO_VERSION = "0.9.0"
MANGO_VERSION = "0.10.0"
macro common_option
option "-c PATH", "--config=PATH", type: String,

View File

@@ -97,6 +97,28 @@ class APIRouter < Router
end
end
post "/api/bulk-progress/:action/:title" do |env|
begin
username = get_username env
title = (@context.library.get_title env.params.url["title"]).not_nil!
action = env.params.url["action"]
ids = env.params.json["ids"].as(Array).map &.as_s
unless action.in? ["read", "unread"]
raise "Unknow action #{action}"
end
title.bulk_progress action, ids, 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/admin/display_name/:title/:name" do |env|
begin
title = (@context.library.get_title env.params.url["title"])

View File

@@ -103,6 +103,7 @@ class MainRouter < Router
continue_reading = @context
.library.get_continue_reading_entries username
recently_added = @context.library.get_recently_added_entries username
start_reading = @context.library.get_start_reading_titles username
titles = @context.library.titles
new_user = !titles.any? { |t| t.load_percentage(username) > 0 }
empty_library = titles.size == 0

View File

@@ -1,6 +1,7 @@
IMGS_PER_PAGE = 5
UPLOAD_URL_PREFIX = "/uploads"
STATIC_DIRS = ["/css", "/js", "/img", "/favicon.ico"]
IMGS_PER_PAGE = 5
ENTRIES_IN_HOME_SECTIONS = 8
UPLOAD_URL_PREFIX = "/uploads"
STATIC_DIRS = ["/css", "/js", "/img", "/favicon.ico"]
def random_str
UUID.random.to_s.gsub "-", ""

View File

@@ -35,12 +35,20 @@
onclick="location='<%= base_url %>book/<%= item.id %>'"
<% end %>>
<div class="uk-card uk-card-default">
<div class="uk-card-media-top">
<img data-src="<%= item.cover_url %>" data-width data-height alt="" uk-img
<div class="uk-card uk-card-default" x-data="{selected: false, hover: false, disabled: true}" :class="{selected: selected}"
<% if page == "title" && item.is_a?(Entry) && item.err_msg.nil? %>
x-init="disabled = false"
<% end %>>
<div class="uk-card-media-top uk-inline" @mouseenter="hover = true" @mouseleave="hover = false">
<img data-src="<%= item.cover_url %>" width="100%" height="100%" alt="" uk-img
<% if item.is_a? Entry && item.err_msg %>
class="grayscale"
<% end %>>
<div class="uk-overlay-primary uk-position-cover" x-show="!disabled && (selected || hover)">
<div class="uk-position-center">
<span class="fas fa-check-circle fa-3x" @click.stop="selected = !selected; $dispatch(selected ? 'add' : 'remove')" :style="`color:${selected && 'orange'};`"></span>
</div>
</div>
</div>
<div class="uk-card-body">

View File

@@ -10,5 +10,6 @@
<script defer src="<%= base_url %>js/fontawesome.min.js"></script>
<script defer src="<%= base_url %>js/solid.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.5.0/dist/alpine.min.js" defer></script>
<script src="<%= base_url %>js/theme.js"></script>
</head>

View File

@@ -41,7 +41,7 @@
<%- 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>
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
<%- continue_reading.each do |cr| -%>
<% item = cr[:entry] %>
<% progress = cr[:percentage] %>
@@ -50,9 +50,20 @@
</div>
<%- end -%>
<%- unless start_reading.empty? -%>
<h2 class="uk-title home-headings">Start Reading</h2>
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
<%- start_reading.each do |t| -%>
<% item = t %>
<% progress = 0.0 %>
<%= render_component "card" %>
<%- 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>
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
<%- recently_added.each do |ra| -%>
<% item = ra %>
<% progress = ra[:percentage] %>

View File

@@ -16,7 +16,7 @@
<%= render_component "sort-form" %>
</div>
</div>
<div id="item-container" 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 |item, i| %>
<% progress = percentage[i] %>
<%= render_component "card" %>

View File

@@ -1,4 +1,23 @@
<div>
<div id="select-bar" class="uk-card uk-card-body uk-card-default uk-margin-bottom" uk-sticky="offset:10" x-data="{count: 0}" @add.window="count++" @remove.window="count--" x-show="count > 0" style="border:orange;border-style:solid;" x-cloak data-id="<%= title.id %>">
<div class="uk-child-width-1-3" uk-grid>
<div>
<p x-text="count + ' items selected'" style="color:orange"></p>
</div>
<div class="uk-text-center" id="select-bar-controls">
<a class="uk-icon uk-margin-right" uk-tooltip="title: Mark selected as read" href="" @click.prevent="bulkProgress('read', $el)">
<i class="fas fa-check-circle"></i>
</a>
<a class="uk-icon" uk-tooltip="title: Mark selected as unread" href="" @click.prevent="bulkProgress('unread', $el)">
<i class="fas fa-times-circle"></i>
</a>
</div>
<div class="uk-text-right">
<a @click="selectAll()" uk-tooltip="title: Select all"><i class="fas fa-check-double uk-margin-small-right"></i></a>
<a @click="deselectAll();" uk-tooltip="title: Deselect all"><i class="fas fa-times"></i></a>
</div>
</div>
</div>
<h2 class=uk-title><span><%= title.display_name %></span>
&nbsp;
<% if is_admin %>
@@ -32,11 +51,14 @@
<%= render_component "sort-form" %>
</div>
</div>
<div id="item-container" 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>
<% title.titles.each_with_index do |item, i| %>
<% progress = title_percentage[i] %>
<%= render_component "card" %>
<% end %>
</div>
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
<% entries.each_with_index do |item, i| %>
<% progress = percentage[i] %>
<%= render_component "card" %>