mirror of
https://github.com/hkalexling/Mango.git
synced 2026-04-25 00:00:52 -04:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07100121ef | |||
| a0e550569e | |||
| bbbe2e0588 | |||
| 9d31b24e8c | |||
| 38ba324fa9 | |||
| c00016fa19 | |||
| 4d5a305d1b | |||
| f9ca52ee2f | |||
| f6c393545c | |||
| 466aee62fe | |||
| eab0800376 | |||
| 1725f42698 | |||
| f5cdf8b7b6 | |||
| fe082e7537 | |||
| c87b96dd0b | |||
| 9d76ca8c24 |
@@ -50,7 +50,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
|
|||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
```
|
```
|
||||||
Mango - Manga Server and Web Reader. Version 0.7.1
|
Mango - Manga Server and Web Reader. Version 0.7.3
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ gulp.task('minify-js', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('less', () => {
|
gulp.task('less', () => {
|
||||||
return gulp.src('src/assets/*.less')
|
return gulp.src('public/css/*.less')
|
||||||
.pipe(less())
|
.pipe(less())
|
||||||
.pipe(gulp.dest('public/css'));
|
.pipe(gulp.dest('public/css'));
|
||||||
});
|
});
|
||||||
|
|||||||
+59
-34
@@ -1,74 +1,99 @@
|
|||||||
.uk-alert-close {
|
.uk-alert-close {
|
||||||
color: black !important;
|
color: black !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uk-card-body {
|
.uk-card-body {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uk-card-media-top {
|
.uk-card-media-top {
|
||||||
height: 250px;
|
height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 600px) {
|
@media (min-width: 600px) {
|
||||||
.uk-card-media-top {
|
.uk-card-media-top {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uk-card-media-top>img {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.uk-card-media-top > img {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
.uk-card-title {
|
.uk-card-title {
|
||||||
height: 3em;
|
max-height: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.acard:hover {
|
.acard:hover {
|
||||||
text-decoration: none;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uk-list li {
|
.uk-list li {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reader-bg {
|
.reader-bg {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
#scan-status {
|
#scan-status {
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.break-word {
|
.break-word {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
.uk-logo > img {
|
|
||||||
height: 90px;
|
.uk-logo>img {
|
||||||
width: 90px;
|
height: 90px;
|
||||||
|
width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uk-search {
|
.uk-search {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#selectable .ui-selecting {
|
#selectable .ui-selecting {
|
||||||
background: #EEE6B9;
|
background: #EEE6B9;
|
||||||
}
|
}
|
||||||
|
|
||||||
#selectable .ui-selected {
|
#selectable .ui-selected {
|
||||||
background: #F4E487;
|
background: #F4E487;
|
||||||
}
|
}
|
||||||
|
|
||||||
#selectable .ui-selecting.dark {
|
#selectable .ui-selecting.dark {
|
||||||
background: #5E5731;
|
background: #5E5731;
|
||||||
}
|
}
|
||||||
|
|
||||||
#selectable .ui-selected.dark {
|
#selectable .ui-selected.dark {
|
||||||
background: #9D9252;
|
background: #9D9252;
|
||||||
}
|
}
|
||||||
td > .uk-dropdown {
|
|
||||||
white-space: pre-line;
|
td>.uk-dropdown {
|
||||||
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
#edit-modal .uk-grid > div {
|
|
||||||
height: 300px;
|
#edit-modal .uk-grid>div {
|
||||||
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edit-modal #cover {
|
#edit-modal #cover {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edit-modal #cover-upload {
|
#edit-modal #cover-upload {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edit-modal .uk-modal-body .uk-inline {
|
#edit-modal .uk-modal-body .uk-inline {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item .uk-card-title {
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -1,13 +1,12 @@
|
|||||||
const truncate = () => {
|
const truncate = () => {
|
||||||
$('.acard .uk-card-title').each((i, e) => {
|
$('.uk-card-title').each((i, e) => {
|
||||||
$(e).dotdotdot({
|
$(e).dotdotdot({
|
||||||
truncate: 'letter',
|
truncate: 'letter',
|
||||||
watch: true,
|
watch: true,
|
||||||
callback: (truncated) => {
|
callback: (truncated) => {
|
||||||
if (truncated) {
|
if (truncated) {
|
||||||
$(e).attr('uk-tooltip', $(e).attr('data-title'));
|
$(e).attr('uk-tooltip', $(e).attr('data-title'));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$(e).removeAttr('uk-tooltip');
|
$(e).removeAttr('uk-tooltip');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-8
@@ -1,6 +1,10 @@
|
|||||||
$(() => {
|
$(() => {
|
||||||
|
const titleID = $('.data').attr('data-title-id') || 'library';
|
||||||
|
|
||||||
const sortItems = () => {
|
const sortItems = () => {
|
||||||
const sort = $('#sort-select').find(':selected').attr('id');
|
const sort = $('#sort-select').find(':selected').attr('id');
|
||||||
|
localStorage.setItem(`sort-${titleID}`, sort);
|
||||||
|
|
||||||
const ary = sort.split('-');
|
const ary = sort.split('-');
|
||||||
const by = ary[0];
|
const by = ary[0];
|
||||||
const dir = ary[1];
|
const dir = ary[1];
|
||||||
@@ -25,20 +29,21 @@ $(() => {
|
|||||||
|
|
||||||
if (!keyRange[key]) {
|
if (!keyRange[key]) {
|
||||||
keyRange[key] = [num, num, 1];
|
keyRange[key] = [num, num, 1];
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
keyRange[key][2] += 1;
|
keyRange[key][2] += 1;
|
||||||
if (num < keyRange[key][0]) {
|
if (num < keyRange[key][0]) {
|
||||||
keyRange[key][0] = num;
|
keyRange[key][0] = num;
|
||||||
}
|
} else if (num > keyRange[key][1]) {
|
||||||
else if (num > keyRange[key][1]) {
|
|
||||||
keyRange[key][1] = num;
|
keyRange[key][1] = num;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match = regex.exec(name);
|
match = regex.exec(name);
|
||||||
}
|
}
|
||||||
ctxAry.push({index: i, numbers: numbers});
|
ctxAry.push({
|
||||||
|
index: i,
|
||||||
|
numbers: numbers
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(keyRange);
|
console.log(keyRange);
|
||||||
@@ -84,8 +89,7 @@ $(() => {
|
|||||||
if (dir === 'down') {
|
if (dir === 'down') {
|
||||||
items.reverse();
|
items.reverse();
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
items.sort((a, b) => {
|
items.sort((a, b) => {
|
||||||
var res;
|
var res;
|
||||||
if (by === 'name')
|
if (by === 'name')
|
||||||
@@ -108,13 +112,17 @@ $(() => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
$('#item-container').append(items);
|
$('#item-container').append(items);
|
||||||
|
setupAcard();
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#sort-select').change(() => {
|
$('#sort-select').change(() => {
|
||||||
sortItems();
|
sortItems();
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($('option#auto-up').length > 0)
|
const sortID = localStorage.getItem(`sort-${titleID}`);
|
||||||
|
if (sortID)
|
||||||
|
$(`option#${sortID}`).attr('selected', '');
|
||||||
|
else if ($('option#auto-up').length > 0)
|
||||||
$('option#auto-up').attr('selected', '');
|
$('option#auto-up').attr('selected', '');
|
||||||
else
|
else
|
||||||
$('option#name-up').attr('selected', '');
|
$('option#name-up').attr('selected', '');
|
||||||
|
|||||||
+21
-3
@@ -1,3 +1,24 @@
|
|||||||
|
$(() => {
|
||||||
|
setupAcard();
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupAcard = () => {
|
||||||
|
$('.acard.is_entry').click((e) => {
|
||||||
|
if ($(e.target).hasClass('no-modal')) return;
|
||||||
|
const card = $(e.target).closest('.acard');
|
||||||
|
|
||||||
|
showModal(
|
||||||
|
$(card).attr('data-encoded-path'),
|
||||||
|
parseInt($(card).attr('data-pages')),
|
||||||
|
parseFloat($(card).attr('data-progress')),
|
||||||
|
$(card).attr('data-encoded-book-title'),
|
||||||
|
$(card).attr('data-encoded-title'),
|
||||||
|
$(card).attr('data-book-id'),
|
||||||
|
$(card).attr('data-id')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTitle, titleID, entryID) {
|
function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTitle, titleID, entryID) {
|
||||||
const zipPath = decodeURIComponent(encodedPath);
|
const zipPath = decodeURIComponent(encodedPath);
|
||||||
const title = decodeURIComponent(encodedeTitle);
|
const title = decodeURIComponent(encodedeTitle);
|
||||||
@@ -15,9 +36,6 @@ function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTi
|
|||||||
$('#continue-btn').text('Continue from ' + percentage + '%');
|
$('#continue-btn').text('Continue from ' + percentage + '%');
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#modal-title-link').text(title);
|
|
||||||
$('#modal-title-link').attr('href', `${base_url}book/${titleID}`);
|
|
||||||
|
|
||||||
$('#modal-entry-title').find('span').text(entry);
|
$('#modal-entry-title').find('span').text(entry);
|
||||||
$('#modal-entry-title').next().attr('data-id', titleID);
|
$('#modal-entry-title').next().attr('data-id', titleID);
|
||||||
$('#modal-entry-title').next().attr('data-entry-id', entryID);
|
$('#modal-entry-title').next().attr('data-entry-id', entryID);
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ shards:
|
|||||||
|
|
||||||
archive:
|
archive:
|
||||||
github: hkalexling/archive.cr
|
github: hkalexling/archive.cr
|
||||||
version: 0.2.0
|
version: 0.3.0
|
||||||
|
|
||||||
baked_file_system:
|
baked_file_system:
|
||||||
github: schovi/baked_file_system
|
github: schovi/baked_file_system
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: mango
|
name: mango
|
||||||
version: 0.7.1
|
version: 0.7.3
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Alex Ling <hkalexling@gmail.com>
|
- Alex Ling <hkalexling@gmail.com>
|
||||||
|
|||||||
+124
-27
@@ -33,7 +33,16 @@ class Entry
|
|||||||
MIME.from_filename? e.filename
|
MIME.from_filename? e.filename
|
||||||
end
|
end
|
||||||
file.close
|
file.close
|
||||||
@id = storage.get_id @zip_path, false
|
id = storage.get_id @zip_path, false
|
||||||
|
if id.nil?
|
||||||
|
id = random_str
|
||||||
|
storage.insert_id({
|
||||||
|
path: @zip_path,
|
||||||
|
id: id,
|
||||||
|
is_title: false,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
@id = id
|
||||||
@mtime = File.info(@zip_path).modification_time
|
@mtime = File.info(@zip_path).modification_time
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -178,7 +187,16 @@ class Title
|
|||||||
|
|
||||||
def initialize(@dir : String, @parent_id, storage,
|
def initialize(@dir : String, @parent_id, storage,
|
||||||
@library : Library)
|
@library : Library)
|
||||||
@id = storage.get_id @dir, true
|
id = storage.get_id @dir, true
|
||||||
|
if id.nil?
|
||||||
|
id = random_str
|
||||||
|
storage.insert_id({
|
||||||
|
path: @dir,
|
||||||
|
id: id,
|
||||||
|
is_title: true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
@id = id
|
||||||
@title = File.basename dir
|
@title = File.basename dir
|
||||||
@encoded_title = URI.encode @title
|
@encoded_title = URI.encode @title
|
||||||
@title_ids = [] of String
|
@title_ids = [] of String
|
||||||
@@ -374,7 +392,7 @@ class Title
|
|||||||
end
|
end
|
||||||
|
|
||||||
def deep_read_page_count(username) : Int32
|
def deep_read_page_count(username) : Int32
|
||||||
entries.map { |e| e.load_progress username }.sum +
|
load_progress_for_all_entries(username).sum +
|
||||||
titles.map { |t| t.deep_read_page_count username }.flatten.sum
|
titles.map { |t| t.deep_read_page_count username }.flatten.sum
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -401,6 +419,85 @@ class Title
|
|||||||
latest_read_entry
|
latest_read_entry
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_progress_for_all_entries(username)
|
||||||
|
progress = {} of String => Int32
|
||||||
|
TitleInfo.new @dir do |info|
|
||||||
|
progress = info.progress[username]?
|
||||||
|
end
|
||||||
|
|
||||||
|
@entries.map do |e|
|
||||||
|
info_progress = 0
|
||||||
|
if progress && progress.has_key? e.title
|
||||||
|
info_progress = [progress[e.title], e.pages].min
|
||||||
|
end
|
||||||
|
info_progress
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_percentage_for_all_entries(username)
|
||||||
|
progress = load_progress_for_all_entries username
|
||||||
|
@entries.map_with_index do |e, i|
|
||||||
|
progress[i] / e.pages
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# === helper methods ===
|
||||||
|
|
||||||
|
# Gets the last read entry in the title. If the entry has been completed,
|
||||||
|
# returns the next entry. Returns nil when no entry has been read yet,
|
||||||
|
# or when all entries are completed
|
||||||
|
def get_last_read_entry(username) : Entry?
|
||||||
|
progress = {} of String => Int32
|
||||||
|
TitleInfo.new @dir do |info|
|
||||||
|
progress = info.progress[username]?
|
||||||
|
end
|
||||||
|
return if progress.nil?
|
||||||
|
|
||||||
|
last_read_entry = nil
|
||||||
|
|
||||||
|
@entries.reverse_each do |e|
|
||||||
|
if progress.has_key?(e.title) && progress[e.title] > 0
|
||||||
|
last_read_entry = e
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if last_read_entry && last_read_entry.finished? username
|
||||||
|
last_read_entry = last_read_entry.next_entry
|
||||||
|
end
|
||||||
|
|
||||||
|
last_read_entry
|
||||||
|
end
|
||||||
|
|
||||||
|
# Equivalent to `@entries.map &. date_added`, but much more efficient
|
||||||
|
def get_date_added_for_all_entries
|
||||||
|
da = {} of String => Time
|
||||||
|
TitleInfo.new @dir do |info|
|
||||||
|
da = info.date_added
|
||||||
|
end
|
||||||
|
|
||||||
|
@entries.each do |e|
|
||||||
|
next if da.has_key? e.title
|
||||||
|
da[e.title] = ctime e.zip_path
|
||||||
|
end
|
||||||
|
|
||||||
|
TitleInfo.new @dir do |info|
|
||||||
|
info.date_added = da
|
||||||
|
info.save
|
||||||
|
end
|
||||||
|
|
||||||
|
@entries.map { |e| da[e.title] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def deep_entries_with_date_added
|
||||||
|
da_ary = get_date_added_for_all_entries
|
||||||
|
zip = @entries.map_with_index do |e, i|
|
||||||
|
{entry: e, date_added: da_ary[i]}
|
||||||
|
end
|
||||||
|
return zip if title_ids.empty?
|
||||||
|
zip + titles.map { |t| t.deep_entries_with_date_added }.flatten
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class TitleInfo
|
class TitleInfo
|
||||||
@@ -446,7 +543,7 @@ end
|
|||||||
|
|
||||||
class Library
|
class Library
|
||||||
property dir : String, title_ids : Array(String), scan_interval : Int32,
|
property dir : String, title_ids : Array(String), scan_interval : Int32,
|
||||||
storage : Storage, title_hash : Hash(String, Title)
|
title_hash : Hash(String, Title)
|
||||||
|
|
||||||
def self.default : self
|
def self.default : self
|
||||||
unless @@default
|
unless @@default
|
||||||
@@ -456,7 +553,8 @@ class Library
|
|||||||
end
|
end
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@storage = Storage.default
|
register_mime_types
|
||||||
|
|
||||||
@dir = Config.current.library_path
|
@dir = Config.current.library_path
|
||||||
@scan_interval = Config.current.scan_interval
|
@scan_interval = Config.current.scan_interval
|
||||||
# explicitly initialize @titles to bypass the compiler check. it will
|
# explicitly initialize @titles to bypass the compiler check. it will
|
||||||
@@ -508,35 +606,32 @@ class Library
|
|||||||
Dir.mkdir_p @dir
|
Dir.mkdir_p @dir
|
||||||
end
|
end
|
||||||
@title_ids.clear
|
@title_ids.clear
|
||||||
|
|
||||||
|
storage = Storage.new auto_close: false
|
||||||
|
|
||||||
(Dir.entries @dir)
|
(Dir.entries @dir)
|
||||||
.select { |fn| !fn.starts_with? "." }
|
.select { |fn| !fn.starts_with? "." }
|
||||||
.map { |fn| File.join @dir, fn }
|
.map { |fn| File.join @dir, fn }
|
||||||
.select { |path| File.directory? path }
|
.select { |path| File.directory? path }
|
||||||
.map { |path| Title.new path, "", @storage, self }
|
.map { |path| Title.new path, "", storage, self }
|
||||||
.select { |title| !(title.entries.empty? && title.titles.empty?) }
|
.select { |title| !(title.entries.empty? && title.titles.empty?) }
|
||||||
.sort { |a, b| a.title <=> b.title }
|
.sort { |a, b| a.title <=> b.title }
|
||||||
.each do |title|
|
.each do |title|
|
||||||
@title_hash[title.id] = title
|
@title_hash[title.id] = title
|
||||||
@title_ids << title.id
|
@title_ids << title.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
storage.bulk_insert_ids
|
||||||
|
storage.close
|
||||||
|
|
||||||
Logger.debug "Scan completed"
|
Logger.debug "Scan completed"
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_continue_reading_entries(username)
|
def get_continue_reading_entries(username)
|
||||||
cr_entries = deep_titles
|
cr_entries = deep_titles
|
||||||
# For each Title, get the last read entry. If the user has finished
|
.map { |t| t.get_last_read_entry username }
|
||||||
# reading this entry, get the next entry
|
|
||||||
.map { |t|
|
|
||||||
last_read_entry = t.entries.reverse_each.find do |e|
|
|
||||||
e.started? username
|
|
||||||
end
|
|
||||||
if last_read_entry && last_read_entry.finished? username
|
|
||||||
last_read_entry = last_read_entry.next_entry
|
|
||||||
end
|
|
||||||
last_read_entry
|
|
||||||
}
|
|
||||||
# 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)
|
.select(Entry)[0..11]
|
||||||
.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
|
||||||
@@ -558,7 +653,7 @@ class Library
|
|||||||
next 1 if a[:last_read].nil?
|
next 1 if a[:last_read].nil?
|
||||||
next -1 if b[:last_read].nil?
|
next -1 if b[:last_read].nil?
|
||||||
b[:last_read].not_nil! <=> a[:last_read].not_nil!
|
b[:last_read].not_nil! <=> a[:last_read].not_nil!
|
||||||
}[0..11]
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
alias RA = NamedTuple(
|
alias RA = NamedTuple(
|
||||||
@@ -568,15 +663,16 @@ class Library
|
|||||||
|
|
||||||
def get_recently_added_entries(username)
|
def get_recently_added_entries(username)
|
||||||
recently_added = [] of RA
|
recently_added = [] of RA
|
||||||
|
last_date_added = nil
|
||||||
|
|
||||||
titles.map { |t| t.deep_entries }
|
titles.map { |t| t.deep_entries_with_date_added }.flatten
|
||||||
.flatten
|
.select { |e| e[:date_added] > 1.month.ago }
|
||||||
.select { |e| e.date_added > 1.month.ago }
|
.sort { |a, b| b[:date_added] <=> a[:date_added] }
|
||||||
.sort { |a, b| b.date_added <=> a.date_added }
|
|
||||||
.each do |e|
|
.each do |e|
|
||||||
|
break if recently_added.size > 12
|
||||||
last = recently_added.last?
|
last = recently_added.last?
|
||||||
if last && e.title_id == last[:entry].title_id &&
|
if last && e[:entry].title_id == last[:entry].title_id &&
|
||||||
(e.date_added - last[:entry].date_added).duration < 1.day
|
(e[:date_added] - last_date_added.not_nil!).duration < 1.day
|
||||||
# A NamedTuple is immutable, so we have to cast it to a Hash first
|
# A NamedTuple is immutable, so we have to cast it to a Hash first
|
||||||
last_hash = last.to_h
|
last_hash = last.to_h
|
||||||
count = last_hash[:grouped_count].as(Int32)
|
count = last_hash[:grouped_count].as(Int32)
|
||||||
@@ -586,9 +682,10 @@ class Library
|
|||||||
last_hash[:percentage] = -1.0
|
last_hash[:percentage] = -1.0
|
||||||
recently_added[recently_added.size - 1] = RA.from last_hash
|
recently_added[recently_added.size - 1] = RA.from last_hash
|
||||||
else
|
else
|
||||||
|
last_date_added = e[:date_added]
|
||||||
recently_added << {
|
recently_added << {
|
||||||
entry: e,
|
entry: e[:entry],
|
||||||
percentage: e.load_percentage(username),
|
percentage: e[:entry].load_percentage(username),
|
||||||
grouped_count: 1,
|
grouped_count: 1,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ require "./mangadex/*"
|
|||||||
require "option_parser"
|
require "option_parser"
|
||||||
require "clim"
|
require "clim"
|
||||||
|
|
||||||
MANGO_VERSION = "0.7.1"
|
MANGO_VERSION = "0.7.3"
|
||||||
|
|
||||||
macro common_option
|
macro common_option
|
||||||
option "-c PATH", "--config=PATH", type: String,
|
option "-c PATH", "--config=PATH", type: String,
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ class MainRouter < Router
|
|||||||
begin
|
begin
|
||||||
title = (@context.library.get_title env.params.url["title"]).not_nil!
|
title = (@context.library.get_title env.params.url["title"]).not_nil!
|
||||||
username = get_username env
|
username = get_username env
|
||||||
percentage = title.entries.map &.load_percentage username
|
percentage = title.load_percentage_for_all_entries username
|
||||||
title_percentage = title.titles.map &.load_percentage username
|
title_percentage = title.titles.map &.load_percentage username
|
||||||
layout "title"
|
layout "title"
|
||||||
rescue e
|
rescue e
|
||||||
|
|||||||
+55
-17
@@ -14,6 +14,12 @@ end
|
|||||||
|
|
||||||
class Storage
|
class Storage
|
||||||
@path : String
|
@path : String
|
||||||
|
@db : DB::Database?
|
||||||
|
@insert_ids = [] of IDTuple
|
||||||
|
|
||||||
|
alias IDTuple = NamedTuple(path: String,
|
||||||
|
id: String,
|
||||||
|
is_title: Bool)
|
||||||
|
|
||||||
def self.default : self
|
def self.default : self
|
||||||
unless @@default
|
unless @@default
|
||||||
@@ -22,7 +28,8 @@ class Storage
|
|||||||
@@default.not_nil!
|
@@default.not_nil!
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(db_path : String? = nil, init_user = true)
|
def initialize(db_path : String? = nil, init_user = true, *,
|
||||||
|
@auto_close = true)
|
||||||
@path = db_path || Config.current.db_path
|
@path = db_path || Config.current.db_path
|
||||||
dir = File.dirname @path
|
dir = File.dirname @path
|
||||||
unless Dir.exists? dir
|
unless Dir.exists? dir
|
||||||
@@ -60,6 +67,9 @@ class Storage
|
|||||||
init_admin if init_user
|
init_admin if init_user
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
unless @auto_close
|
||||||
|
@db = DB.open "sqlite3://#{@path}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
macro init_admin
|
macro init_admin
|
||||||
@@ -71,8 +81,18 @@ class Storage
|
|||||||
"#{{"username" => "admin", "password" => random_pw}}"
|
"#{{"username" => "admin", "password" => random_pw}}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private def get_db(&block : DB::Database ->)
|
||||||
|
if @db.nil?
|
||||||
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
yield db
|
||||||
|
end
|
||||||
|
else
|
||||||
|
yield @db.not_nil!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def verify_user(username, password)
|
def verify_user(username, password)
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
get_db do |db|
|
||||||
begin
|
begin
|
||||||
hash, token = db.query_one "select password, token from " \
|
hash, token = db.query_one "select password, token from " \
|
||||||
"users where username = (?)",
|
"users where username = (?)",
|
||||||
@@ -97,7 +117,7 @@ class Storage
|
|||||||
|
|
||||||
def verify_token(token)
|
def verify_token(token)
|
||||||
username = nil
|
username = nil
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
get_db do |db|
|
||||||
begin
|
begin
|
||||||
username = db.query_one "select username from users where " \
|
username = db.query_one "select username from users where " \
|
||||||
"token = (?)", token, as: String
|
"token = (?)", token, as: String
|
||||||
@@ -110,7 +130,7 @@ class Storage
|
|||||||
|
|
||||||
def verify_admin(token)
|
def verify_admin(token)
|
||||||
is_admin = false
|
is_admin = false
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
get_db do |db|
|
||||||
begin
|
begin
|
||||||
is_admin = db.query_one "select admin from users where " \
|
is_admin = db.query_one "select admin from users where " \
|
||||||
"token = (?)", token, as: Bool
|
"token = (?)", token, as: Bool
|
||||||
@@ -123,7 +143,7 @@ class Storage
|
|||||||
|
|
||||||
def list_users
|
def list_users
|
||||||
results = Array(Tuple(String, Bool)).new
|
results = Array(Tuple(String, Bool)).new
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
get_db do |db|
|
||||||
db.query "select username, admin from users" do |rs|
|
db.query "select username, admin from users" do |rs|
|
||||||
rs.each do
|
rs.each do
|
||||||
results << {rs.read(String), rs.read(Bool)}
|
results << {rs.read(String), rs.read(Bool)}
|
||||||
@@ -137,7 +157,7 @@ class Storage
|
|||||||
validate_username username
|
validate_username username
|
||||||
validate_password password
|
validate_password password
|
||||||
admin = (admin ? 1 : 0)
|
admin = (admin ? 1 : 0)
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
get_db do |db|
|
||||||
hash = hash_password password
|
hash = hash_password password
|
||||||
db.exec "insert into users values (?, ?, ?, ?)",
|
db.exec "insert into users values (?, ?, ?, ?)",
|
||||||
username, hash, nil, admin
|
username, hash, nil, admin
|
||||||
@@ -148,7 +168,7 @@ class Storage
|
|||||||
admin = (admin ? 1 : 0)
|
admin = (admin ? 1 : 0)
|
||||||
validate_username username
|
validate_username username
|
||||||
validate_password password unless password.empty?
|
validate_password password unless password.empty?
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
get_db do |db|
|
||||||
if password.empty?
|
if password.empty?
|
||||||
db.exec "update users set username = (?), admin = (?) " \
|
db.exec "update users set username = (?), admin = (?) " \
|
||||||
"where username = (?)",
|
"where username = (?)",
|
||||||
@@ -163,13 +183,13 @@ class Storage
|
|||||||
end
|
end
|
||||||
|
|
||||||
def delete_user(username)
|
def delete_user(username)
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
get_db do |db|
|
||||||
db.exec "delete from users where username = (?)", username
|
db.exec "delete from users where username = (?)", username
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def logout(token)
|
def logout(token)
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
get_db do |db|
|
||||||
begin
|
begin
|
||||||
db.exec "update users set token = (?) where token = (?)", nil, token
|
db.exec "update users set token = (?) where token = (?)", nil, token
|
||||||
rescue
|
rescue
|
||||||
@@ -178,18 +198,36 @@ class Storage
|
|||||||
end
|
end
|
||||||
|
|
||||||
def get_id(path, is_title)
|
def get_id(path, is_title)
|
||||||
id = random_str
|
id = nil
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
get_db do |db|
|
||||||
begin
|
id = db.query_one? "select id from ids where path = (?)", path,
|
||||||
id = db.query_one "select id from ids where path = (?)", path,
|
as: {String}
|
||||||
as: {String}
|
|
||||||
rescue
|
|
||||||
db.exec "insert into ids values (?, ?, ?)", path, id, is_title ? 1 : 0
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
id
|
id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def insert_id(tp : IDTuple)
|
||||||
|
@insert_ids << tp
|
||||||
|
end
|
||||||
|
|
||||||
|
def bulk_insert_ids
|
||||||
|
get_db do |db|
|
||||||
|
db.transaction do |tx|
|
||||||
|
@insert_ids.each do |tp|
|
||||||
|
tx.connection.exec "insert into ids values (?, ?, ?)", tp[:path],
|
||||||
|
tp[:id], tp[:is_title] ? 1 : 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@insert_ids.clear
|
||||||
|
end
|
||||||
|
|
||||||
|
def close
|
||||||
|
unless @db.nil?
|
||||||
|
@db.not_nil!.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def to_json(json : JSON::Builder)
|
def to_json(json : JSON::Builder)
|
||||||
json.string self
|
json.string self
|
||||||
end
|
end
|
||||||
|
|||||||
+11
-2
@@ -41,8 +41,6 @@ def send_json(env, json)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def send_attachment(env, path)
|
def send_attachment(env, path)
|
||||||
MIME.register ".cbz", "application/vnd.comicbook+zip"
|
|
||||||
MIME.register ".cbr", "application/vnd.comicbook-rar"
|
|
||||||
send_file env, path, filename: File.basename(path), disposition: "attachment"
|
send_file env, path, filename: File.basename(path), disposition: "attachment"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -154,3 +152,14 @@ def ctime(file_path : String) : Time
|
|||||||
Time.new stat.st_ctim, Time::Location::UTC
|
Time.new stat.st_ctim, Time::Location::UTC
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def register_mime_types
|
||||||
|
{
|
||||||
|
".zip" => "application/zip",
|
||||||
|
".rar" => "application/x-rar-compressed",
|
||||||
|
".cbz" => "application/vnd.comicbook+zip",
|
||||||
|
".cbr" => "application/vnd.comicbook-rar",
|
||||||
|
}.each do |k, v|
|
||||||
|
MIME.register k, v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -14,37 +14,53 @@
|
|||||||
id="<%= item.id %>"
|
id="<%= item.id %>"
|
||||||
<% end %>>
|
<% end %>>
|
||||||
|
|
||||||
<a class="acard"
|
<div class="acard
|
||||||
<% unless item.is_a? Entry %>
|
<% if item.is_a? Entry %>
|
||||||
href="<%= base_url %>book/<%= item.id %>"
|
<%= "is_entry" %>
|
||||||
|
<% end %>
|
||||||
|
"
|
||||||
|
<% if item.is_a? Entry %>
|
||||||
|
data-encoded-path="<%= item.encoded_path %>"
|
||||||
|
data-pages="<%= item.pages %>"
|
||||||
|
data-progress="<%= (progress * 100).round(1) %>"
|
||||||
|
data-encoded-book-title="<%= item.book.encoded_display_name %>"
|
||||||
|
data-encoded-title="<%= item.encoded_display_name %>"
|
||||||
|
data-book-id="<%= item.book.id %>"
|
||||||
|
data-id="<%= item.id %>"
|
||||||
|
<% else %>
|
||||||
|
onclick="location='<%= base_url %>book/<%= item.id %>'"
|
||||||
<% end %>>
|
<% end %>>
|
||||||
|
|
||||||
<div class="uk-card uk-card-default"
|
<div class="uk-card uk-card-default">
|
||||||
<% if item.is_a? Entry %>
|
|
||||||
onclick="showModal("<%= item.encoded_path %>", '<%= item.pages %>', <%= (progress * 100).round(1) %>, "<%= item.book.encoded_display_name %>", "<%= item.encoded_display_name %>", '<%= item.title_id %>', '<%= item.id %>')"
|
|
||||||
<% end %>>
|
|
||||||
|
|
||||||
<div class="uk-card-media-top">
|
<div class="uk-card-media-top">
|
||||||
<img data-src="<%= item.cover_url %>" data-width data-height alt="" uk-img>
|
<img data-src="<%= item.cover_url %>" data-width data-height alt="" uk-img>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="uk-card-body">
|
<div class="uk-card-body">
|
||||||
<% unless progress < 0 || progress > 100 %>
|
<% unless progress < 0 || progress > 100 %>
|
||||||
<div class="uk-card-badge uk-label"><%= (progress * 100).round(1) %>%</div>
|
<div class="uk-card-badge label"><%= (progress * 100).round(1) %>%</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<h3 class="uk-card-title break-word" data-title="<%= item.display_name.gsub("\"", """) %>"><%= item.display_name %></h3>
|
<h3 class="uk-card-title break-word
|
||||||
|
<% if page == "home" && item.is_a? Entry %>
|
||||||
|
<%= "uk-margin-remove-bottom" %>
|
||||||
|
<% end %>
|
||||||
|
" data-title="<%= HTML.escape(item.display_name) %>"><%= item.display_name %>
|
||||||
|
</h3>
|
||||||
|
<% if page == "home" && item.is_a? Entry %>
|
||||||
|
<a class="uk-card-title break-word uk-margin-remove-top uk-text-meta uk-display-inline-block no-modal" data-title="<%= HTML.escape(item.book.display_name) %>" href="<%= base_url %>book/<%= item.book.id %>"><%= HTML.escape(item.book.display_name) %></a>
|
||||||
|
<% end %>
|
||||||
<% if item.is_a? Entry %>
|
<% if item.is_a? Entry %>
|
||||||
<p><%= item.pages %> pages</p>
|
<p class="uk-text-meta"><%= item.pages %> pages</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if item.is_a? Title %>
|
<% if item.is_a? Title %>
|
||||||
<% if grouped_count == 1 %>
|
<% if grouped_count == 1 %>
|
||||||
<p><%= item.size %> entries</p>
|
<p class="uk-text-meta"><%= item.size %> entries</p>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p><%= grouped_count %> new entries</p>
|
<p class="uk-text-meta"><%= grouped_count %> new entries</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
<button class="uk-modal-close-default" type="button" uk-close></button>
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||||
<div class="uk-modal-header">
|
<div class="uk-modal-header">
|
||||||
<div>
|
<div>
|
||||||
<% if page == "home" %>
|
|
||||||
<h4 class="uk-margin-remove-bottom"><a id="modal-title-link"></a></h4>
|
|
||||||
<% end %>
|
|
||||||
<h3 class="uk-modal-title break-word uk-margin-remove-top" id="modal-entry-title"><span></span>
|
<h3 class="uk-modal-title break-word uk-margin-remove-top" id="modal-entry-title"><span></span>
|
||||||
|
|
||||||
<% unless page == "home" %>
|
<% unless page == "home" %>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<% titles.each do |t| %>
|
<% titles.each do |t| %>
|
||||||
<entry>
|
<entry>
|
||||||
<title><%= t.display_name %></title>
|
<title><%= HTML.escape(t.display_name) %></title>
|
||||||
<id>urn:mango:<%= t.id %></id>
|
<id>urn:mango:<%= t.id %></id>
|
||||||
<link type="application/atom+xml;profile=opds-catalog;kind=navigation" rel="subsection" href="<%= base_url %>opds/book/<%= t.id %>" />
|
<link type="application/atom+xml;profile=opds-catalog;kind=navigation" rel="subsection" href="<%= base_url %>opds/book/<%= t.id %>" />
|
||||||
</entry>
|
</entry>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<link rel="self" href="<%= base_url %>opds/book/<%= title.id %>" type="application/atom+xml;profile=opds-catalog;kind=navigation" />
|
<link rel="self" href="<%= base_url %>opds/book/<%= title.id %>" type="application/atom+xml;profile=opds-catalog;kind=navigation" />
|
||||||
<link rel="start" href="<%= base_url %>opds/" type="application/atom+xml;profile=opds-catalog;kind=navigation" />
|
<link rel="start" href="<%= base_url %>opds/" type="application/atom+xml;profile=opds-catalog;kind=navigation" />
|
||||||
|
|
||||||
<title><%= title.display_name %></title>
|
<title><%= HTML.escape(title.display_name) %></title>
|
||||||
|
|
||||||
<author>
|
<author>
|
||||||
<name>Mango</name>
|
<name>Mango</name>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<% title.titles.each do |t| %>
|
<% title.titles.each do |t| %>
|
||||||
<entry>
|
<entry>
|
||||||
<title><%= t.display_name %></title>
|
<title><%= HTML.escape(t.display_name) %></title>
|
||||||
<id>urn:mango:<%= t.id %></id>
|
<id>urn:mango:<%= t.id %></id>
|
||||||
<link type="application/atom+xml;profile=opds-catalog;kind=navigation" rel="subsection" href="<%= base_url %>opds/book/<%= t.id %>" />
|
<link type="application/atom+xml;profile=opds-catalog;kind=navigation" rel="subsection" href="<%= base_url %>opds/book/<%= t.id %>" />
|
||||||
</entry>
|
</entry>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
<% title.entries.each do |e| %>
|
<% title.entries.each do |e| %>
|
||||||
<entry>
|
<entry>
|
||||||
<title><%= e.display_name %></title>
|
<title><%= HTML.escape(e.display_name) %></title>
|
||||||
<id>urn:mango:<%= e.id %></id>
|
<id>urn:mango:<%= e.id %></id>
|
||||||
|
|
||||||
<link rel="http://opds-spec.org/image" href="<%= e.cover_url %>" />
|
<link rel="http://opds-spec.org/image" href="<%= e.cover_url %>" />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<div class="data" data-title-id="<%= title.id %>"></div>
|
||||||
<div>
|
<div>
|
||||||
<h2 class=uk-title><span><%= title.display_name %></span>
|
<h2 class=uk-title><span><%= title.display_name %></span>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user