mirror of
https://github.com/hkalexling/Mango.git
synced 2026-04-25 00:00:52 -04: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
|
- 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:
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
name: mango
|
||||||
version: 0.9.0
|
version: 0.10.0
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Alex Ling <hkalexling@gmail.com>
|
- Alex Ling <hkalexling@gmail.com>
|
||||||
|
|||||||
+49
-35
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -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,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"]
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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] %>
|
||||||
|
|||||||
@@ -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" %>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
<% 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" %>
|
||||||
|
|||||||
Reference in New Issue
Block a user