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
+2 -2
View File
@@ -13,7 +13,7 @@ Mango is a self-hosted manga server and reader. Its features include
- Supports nested folders in library - Supports nested folders in library
- Automatically stores reading progress - Automatically stores reading progress
- Built-in [MangaDex](https://mangadex.org/) downloader - 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 - 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 - 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 ### CLI
``` ```
Mango - Manga Server and Web Reader. Version 0.9.0 Mango - Manga Server and Web Reader. Version 0.10.0
Usage: Usage:
+13
View File
@@ -7,6 +7,7 @@
} }
.uk-card-media-top { .uk-card-media-top {
width: 100%;
height: 250px; height: 250px;
} }
@@ -122,3 +123,15 @@ td>.uk-dropdown {
.uk-light .uk-description-list>dt { .uk-light .uk-description-list>dt {
color: #555; color: #555;
} }
[x-cloak] {
display: none;
}
#select-bar-controls a {
transform: scale(1.5, 1.5);
}
#select-bar-controls a:hover {
color: orange;
}
+60
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();
});
};
+1 -1
View File
@@ -1,5 +1,5 @@
name: mango name: mango
version: 0.9.0 version: 0.10.0
authors: authors:
- Alex Ling <hkalexling@gmail.com> - Alex Ling <hkalexling@gmail.com>
+49 -35
View File
@@ -30,6 +30,41 @@ class Library
@title_ids.map { |tid| self.get_title!(tid) } @title_ids.map { |tid| self.get_title!(tid) }
end 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 def deep_titles
titles + titles.map { |t| t.deep_titles }.flatten titles + titles.map { |t| t.deep_titles }.flatten
end end
@@ -83,7 +118,7 @@ class Library
cr_entries = deep_titles cr_entries = deep_titles
.map { |t| t.get_last_read_entry username } .map { |t| t.get_last_read_entry username }
# Select elements with type `Entry` from the array and ignore all `Nil`s # 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| .map { |e|
# Get the last read time of the entry. If it hasn't been started, get # Get the last read time of the entry. If it hasn't been started, get
# the last read time of the previous entry # the last read time of the previous entry
@@ -143,41 +178,20 @@ class Library
end end
end end
recently_added[0..11] recently_added[0...ENTRIES_IN_HOME_SECTIONS]
end end
def sorted_titles(username, opt : SortOptions? = nil) def get_start_reading_titles(username)
if opt.nil? # Here we are not using `deep_titles` as it may cause unexpected behaviors
opt = SortOptions.from_info_json @dir, username # For example, consider the following nested titles:
else # - One Puch Man
TitleInfo.new @dir do |info| # - Vol. 1
info.sort_by[username] = opt.to_tuple # - Vol. 2
info.save # If we use `deep_titles`, the start reading section might include `Vol. 2`
end # when the user hasn't started `Vol. 1` yet
end titles
.select { |t| t.load_percentage(username) == 0 }
# This is a hack to bypass a compiler bug .sample(ENTRIES_IN_HOME_SECTIONS)
ary = titles .shuffle
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 end
end end
+20
View File
@@ -355,4 +355,24 @@ class Title
return zip if title_ids.empty? return zip if title_ids.empty?
zip + titles.map { |t| t.deep_entries_with_date_added }.flatten zip + titles.map { |t| t.deep_entries_with_date_added }.flatten
end 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 end
+1 -1
View File
@@ -6,7 +6,7 @@ require "option_parser"
require "clim" require "clim"
require "./plugin/*" require "./plugin/*"
MANGO_VERSION = "0.9.0" MANGO_VERSION = "0.10.0"
macro common_option macro common_option
option "-c PATH", "--config=PATH", type: String, option "-c PATH", "--config=PATH", type: String,
+22
View File
@@ -97,6 +97,28 @@ class APIRouter < Router
end end
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| post "/api/admin/display_name/:title/:name" do |env|
begin begin
title = (@context.library.get_title env.params.url["title"]) title = (@context.library.get_title env.params.url["title"])
+1
View File
@@ -103,6 +103,7 @@ class MainRouter < Router
continue_reading = @context continue_reading = @context
.library.get_continue_reading_entries username .library.get_continue_reading_entries username
recently_added = @context.library.get_recently_added_entries username recently_added = @context.library.get_recently_added_entries username
start_reading = @context.library.get_start_reading_titles username
titles = @context.library.titles titles = @context.library.titles
new_user = !titles.any? { |t| t.load_percentage(username) > 0 } new_user = !titles.any? { |t| t.load_percentage(username) > 0 }
empty_library = titles.size == 0 empty_library = titles.size == 0
+1
View File
@@ -1,4 +1,5 @@
IMGS_PER_PAGE = 5 IMGS_PER_PAGE = 5
ENTRIES_IN_HOME_SECTIONS = 8
UPLOAD_URL_PREFIX = "/uploads" UPLOAD_URL_PREFIX = "/uploads"
STATIC_DIRS = ["/css", "/js", "/img", "/favicon.ico"] STATIC_DIRS = ["/css", "/js", "/img", "/favicon.ico"]
+11 -3
View File
@@ -35,12 +35,20 @@
onclick="location='<%= base_url %>book/<%= item.id %>'" onclick="location='<%= base_url %>book/<%= item.id %>'"
<% end %>> <% end %>>
<div class="uk-card uk-card-default"> <div class="uk-card uk-card-default" x-data="{selected: false, hover: false, disabled: true}" :class="{selected: selected}"
<div class="uk-card-media-top"> <% if page == "title" && item.is_a?(Entry) && item.err_msg.nil? %>
<img data-src="<%= item.cover_url %>" data-width data-height alt="" uk-img 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 %> <% if item.is_a? Entry && item.err_msg %>
class="grayscale" class="grayscale"
<% end %>> <% 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>
<div class="uk-card-body"> <div class="uk-card-body">
+1
View File
@@ -10,5 +10,6 @@
<script defer src="<%= base_url %>js/fontawesome.min.js"></script> <script defer src="<%= base_url %>js/fontawesome.min.js"></script>
<script defer src="<%= base_url %>js/solid.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://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> <script src="<%= base_url %>js/theme.js"></script>
</head> </head>
+13 -2
View File
@@ -41,7 +41,7 @@
<%- unless continue_reading.empty? -%> <%- unless continue_reading.empty? -%>
<h2 class="uk-title home-headings">Continue Reading</h2> <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| -%> <%- continue_reading.each do |cr| -%>
<% item = cr[:entry] %> <% item = cr[:entry] %>
<% progress = cr[:percentage] %> <% progress = cr[:percentage] %>
@@ -50,9 +50,20 @@
</div> </div>
<%- end -%> <%- 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? -%> <%- unless recently_added.empty? -%>
<h2 class="uk-title home-headings">Recently Added</h2> <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| -%> <%- recently_added.each do |ra| -%>
<% item = ra %> <% item = ra %>
<% progress = ra[:percentage] %> <% progress = ra[:percentage] %>
+1 -1
View File
@@ -16,7 +16,7 @@
<%= render_component "sort-form" %> <%= render_component "sort-form" %>
</div> </div>
</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| %> <% titles.each_with_index do |item, i| %>
<% progress = percentage[i] %> <% progress = percentage[i] %>
<%= render_component "card" %> <%= render_component "card" %>
+23 -1
View File
@@ -1,4 +1,23 @@
<div> <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> <h2 class=uk-title><span><%= title.display_name %></span>
&nbsp; &nbsp;
<% if is_admin %> <% if is_admin %>
@@ -32,11 +51,14 @@
<%= render_component "sort-form" %> <%= render_component "sort-form" %>
</div> </div>
</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| %> <% title.titles.each_with_index do |item, i| %>
<% progress = title_percentage[i] %> <% progress = title_percentage[i] %>
<%= render_component "card" %> <%= render_component "card" %>
<% end %> <% end %>
</div>
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
<% entries.each_with_index do |item, i| %> <% entries.each_with_index do |item, i| %>
<% progress = percentage[i] %> <% progress = percentage[i] %>
<%= render_component "card" %> <%= render_component "card" %>