mirror of
https://github.com/hkalexling/Mango.git
synced 2026-01-24 00:03:14 -05:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
586ee4f0ba | ||
|
|
53f3387e1a | ||
|
|
be5d1918aa | ||
|
|
df2cc0ffa9 | ||
|
|
b8cfc3a201 | ||
|
|
8dc60ac2ea | ||
|
|
1719335d02 | ||
|
|
0cd46abc66 | ||
|
|
e4fd7c58ee | ||
|
|
d4abee52db | ||
|
|
d29c94e898 |
@@ -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:
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
name: mango
|
||||
version: 0.9.0
|
||||
version: 0.10.0
|
||||
|
||||
authors:
|
||||
- Alex Ling <hkalexling@gmail.com>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "-", ""
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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] %>
|
||||
|
||||
@@ -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" %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
<% 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" %>
|
||||
|
||||
Reference in New Issue
Block a user