mirror of
https://github.com/hkalexling/Mango.git
synced 2026-04-25 00:00:52 -04:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef1ab940f5 | |||
| 97a1c408d8 | |||
| abbf77df13 | |||
| 3b4021f680 | |||
| 68b1923cb6 | |||
| 3cdd4b29a5 | |||
| af84c0f6de | |||
| 85a65f84d0 | |||
| 5027a911cd | |||
| ac63bf7599 | |||
| 30b0e0b8fb | |||
| ddda058d8d | |||
| 46db25e8e0 | |||
| c07f421322 | |||
| 99a77966ad | |||
| d00b917575 | |||
| 4fd8334c37 | |||
| 3aa4630558 | |||
| cde5af7066 | |||
| eb528e1726 | |||
| 5e01cc38fe | |||
| 9a787ccbc3 | |||
| 8a83c0df4e | |||
| 87dea01917 | |||
| 586ee4f0ba | |||
| 53f3387e1a | |||
| be5d1918aa | |||
| df2cc0ffa9 | |||
| b8cfc3a201 | |||
| 8dc60ac2ea | |||
| 1719335d02 | |||
| 0cd46abc66 | |||
| e4fd7c58ee | |||
| d4abee52db | |||
| d29c94e898 |
@@ -19,13 +19,22 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: apk add --no-cache yarn yaml sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static
|
run: apk add --no-cache yarn yaml sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make static
|
run: make static || make static
|
||||||
- name: Linter
|
- name: Linter
|
||||||
run: make check
|
run: make check
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test
|
run: make test
|
||||||
- name: Upload artifact
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: mango
|
name: mango
|
||||||
path: mango
|
path: mango
|
||||||
|
- name: build arm32v7 object file
|
||||||
|
run: make arm32v7 || make arm32v7
|
||||||
|
- name: build arm64v8 object file
|
||||||
|
run: make arm64v8 || make arm64v8
|
||||||
|
- name: Upload object files
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: object files
|
||||||
|
path: ./*.o
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
FROM arm32v7/ubuntu:18.04
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y wget git make llvm-8 llvm-8-dev g++ libsqlite3-dev libyaml-dev libgc-dev libssl-dev libcrypto++-dev libevent-dev libgmp-dev zlib1g-dev libpcre++-dev pkg-config libarchive-dev libxml2-dev libacl1-dev nettle-dev liblzo2-dev liblzma-dev libbz2-dev
|
||||||
|
|
||||||
|
RUN git clone https://github.com/crystal-lang/crystal && cd crystal && git checkout 0.34.0 && make deps && cd ..
|
||||||
|
RUN git clone https://github.com/kostya/myhtml && cd myhtml/src/ext && git checkout v1.5.0 && make && cd ..
|
||||||
|
RUN git clone https://github.com/jessedoyle/duktape.cr && cd duktape.cr/ext && git checkout v0.20.0 && make && cd ..
|
||||||
|
|
||||||
|
COPY mango-arm32v7.o .
|
||||||
|
|
||||||
|
RUN cc 'mango-arm32v7.o' -o 'mango' -rdynamic -lxml2 /myhtml/src/ext/modest-c/lib/libmodest_static.a -L/duktape.cr/src/.build/lib -L/duktape.cr/src/.build/include -lduktape -lm `pkg-config libarchive --libs` -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'` -lgmp -lsqlite3 -lyaml -lpcre -lm /usr/lib/arm-linux-gnueabihf/libgc.so -lpthread /crystal/src/ext/libcrystal.a -levent -lrt -ldl -L/usr/bin/../lib/crystal/lib -L/usr/bin/../lib/crystal/lib
|
||||||
|
|
||||||
|
CMD ["./mango"]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
FROM arm64v8/ubuntu:18.04
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y wget git make llvm-8 llvm-8-dev g++ libsqlite3-dev libyaml-dev libgc-dev libssl-dev libcrypto++-dev libevent-dev libgmp-dev zlib1g-dev libpcre++-dev pkg-config libarchive-dev libxml2-dev libacl1-dev nettle-dev liblzo2-dev liblzma-dev libbz2-dev
|
||||||
|
|
||||||
|
RUN git clone https://github.com/crystal-lang/crystal && cd crystal && git checkout 0.34.0 && make deps && cd ..
|
||||||
|
RUN git clone https://github.com/kostya/myhtml && cd myhtml/src/ext && git checkout v1.5.0 && make && cd ..
|
||||||
|
RUN git clone https://github.com/jessedoyle/duktape.cr && cd duktape.cr/ext && git checkout v0.20.0 && make && cd ..
|
||||||
|
|
||||||
|
COPY mango-arm64v8.o .
|
||||||
|
|
||||||
|
RUN cc 'mango-arm64v8.o' -o 'mango' -rdynamic -lxml2 /myhtml/src/ext/modest-c/lib/libmodest_static.a -L/duktape.cr/src/.build/lib -L/duktape.cr/src/.build/include -lduktape -lm `pkg-config libarchive --libs` -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'` -lgmp -lsqlite3 -lyaml -lpcre -lm /usr/lib/aarch64-linux-gnu/libgc.so -lpthread /crystal/src/ext/libcrystal.a -levent -lrt -ldl -L/usr/bin/../lib/crystal/lib -L/usr/bin/../lib/crystal/lib
|
||||||
|
|
||||||
|
CMD ["./mango"]
|
||||||
@@ -12,10 +12,10 @@ setup: libs
|
|||||||
yarn gulp dev
|
yarn gulp dev
|
||||||
|
|
||||||
build: libs
|
build: libs
|
||||||
crystal build src/mango.cr --release --progress
|
crystal build src/mango.cr --release --progress --error-trace
|
||||||
|
|
||||||
static: uglify | libs
|
static: uglify | libs
|
||||||
crystal build src/mango.cr --release --progress --static
|
crystal build src/mango.cr --release --progress --static --error-trace
|
||||||
|
|
||||||
libs:
|
libs:
|
||||||
shards install --production
|
shards install --production
|
||||||
@@ -31,6 +31,12 @@ check:
|
|||||||
./bin/ameba
|
./bin/ameba
|
||||||
./dev/linewidth.sh
|
./dev/linewidth.sh
|
||||||
|
|
||||||
|
arm32v7:
|
||||||
|
crystal build src/mango.cr --release --progress --error-trace --cross-compile --target='arm-linux-gnueabihf' -o mango-arm32v7
|
||||||
|
|
||||||
|
arm64v8:
|
||||||
|
crystal build src/mango.cr --release --progress --error-trace --cross-compile --target='aarch64-linux-gnu' -o mango-arm64v8
|
||||||
|
|
||||||
install:
|
install:
|
||||||
cp mango $(INSTALL_DIR)/mango
|
cp mango $(INSTALL_DIR)/mango
|
||||||
|
|
||||||
|
|||||||
@@ -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.11.0
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
@@ -139,8 +139,12 @@ Mobile UI:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
<a href="https://casinoshunter.com/online-casinos/"><img src="https://i.imgur.com/EJb3wBo.png" width="150" height="auto"></a>
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
Please check the [development guideline](https://github.com/hkalexling/Mango/wiki/Development) if you are interest in code contributions.
|
Please check the [development guideline](https://github.com/hkalexling/Mango/wiki/Development) if you are interested in code contributions.
|
||||||
|
|
||||||
[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/0)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/1)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/2)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/3)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/4)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/5)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/6)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/7)
|
[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/0)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/1)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/2)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/3)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/4)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/5)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/6)[](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/7)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.uk-card-media-top {
|
.uk-card-media-top {
|
||||||
|
width: 100%;
|
||||||
height: 250px;
|
height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,3 +123,32 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-section {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#totop-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 100vh;
|
||||||
|
right: 2em;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#totop-wrapper a {
|
||||||
|
position: fixed;
|
||||||
|
position: sticky;
|
||||||
|
top: calc(100vh - 5em);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ shards:
|
|||||||
github: crystal-loot/exception_page
|
github: crystal-loot/exception_page
|
||||||
version: 0.1.4
|
version: 0.1.4
|
||||||
|
|
||||||
|
http_proxy:
|
||||||
|
github: mamantoha/http_proxy
|
||||||
|
version: 0.7.1
|
||||||
|
|
||||||
kemal:
|
kemal:
|
||||||
github: kemalcr/kemal
|
github: kemalcr/kemal
|
||||||
version: 0.26.1
|
version: 0.26.1
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: mango
|
name: mango
|
||||||
version: 0.9.0
|
version: 0.11.0
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Alex Ling <hkalexling@gmail.com>
|
- Alex Ling <hkalexling@gmail.com>
|
||||||
@@ -32,3 +32,5 @@ dependencies:
|
|||||||
version: ~> 0.20.0
|
version: ~> 0.20.0
|
||||||
myhtml:
|
myhtml:
|
||||||
github: kostya/myhtml
|
github: kostya/myhtml
|
||||||
|
http_proxy:
|
||||||
|
github: mamantoha/http_proxy
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ require "spec"
|
|||||||
require "../src/queue"
|
require "../src/queue"
|
||||||
require "../src/server"
|
require "../src/server"
|
||||||
require "../src/config"
|
require "../src/config"
|
||||||
|
require "../src/main_fiber"
|
||||||
|
|
||||||
class State
|
class State
|
||||||
@@hash = {} of String => String
|
@@hash = {} of String => String
|
||||||
|
|||||||
+2
-6
@@ -52,12 +52,8 @@ class Config
|
|||||||
config.fill_defaults
|
config.fill_defaults
|
||||||
return config
|
return config
|
||||||
end
|
end
|
||||||
puts "The config file #{cfg_path} does not exist." \
|
puts "The config file #{cfg_path} does not exist. " \
|
||||||
" Do you want mango to dump the default config there? [Y/n]"
|
"Dumping the default config there."
|
||||||
input = gets
|
|
||||||
if input && input.downcase == "n"
|
|
||||||
abort "Aborting..."
|
|
||||||
end
|
|
||||||
default = self.allocate
|
default = self.allocate
|
||||||
default.path = path
|
default.path = path
|
||||||
default.fill_defaults
|
default.fill_defaults
|
||||||
|
|||||||
+50
-36
@@ -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
|
||||||
@@ -57,7 +92,6 @@ class Library
|
|||||||
"Attempting to create it"
|
"Attempting to create it"
|
||||||
Dir.mkdir_p @dir
|
Dir.mkdir_p @dir
|
||||||
end
|
end
|
||||||
@title_ids.clear
|
|
||||||
|
|
||||||
storage = Storage.new auto_close: false
|
storage = Storage.new auto_close: false
|
||||||
|
|
||||||
@@ -68,6 +102,7 @@ class Library
|
|||||||
.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 }
|
||||||
|
.tap { |_| @title_ids.clear }
|
||||||
.each do |title|
|
.each do |title|
|
||||||
@title_hash[title.id] = title
|
@title_hash[title.id] = title
|
||||||
@title_ids << title.id
|
@title_ids << title.id
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# On ARM, connecting to the SQLite DB from a spawned fiber would crash
|
||||||
|
# https://github.com/crystal-lang/crystal-sqlite3/issues/30
|
||||||
|
# This is a temporary workaround that forces the relevant code to run in the
|
||||||
|
# main fiber
|
||||||
|
|
||||||
|
class MainFiber
|
||||||
|
@@channel = Channel(-> Nil).new
|
||||||
|
@@done = Channel(Bool).new
|
||||||
|
@@main_fiber = Fiber.current
|
||||||
|
|
||||||
|
def self.start_and_block
|
||||||
|
loop do
|
||||||
|
if proc = @@channel.receive
|
||||||
|
begin
|
||||||
|
proc.call
|
||||||
|
ensure
|
||||||
|
@@done.send true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Fiber.yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.run(&block : -> Nil)
|
||||||
|
if @@main_fiber == Fiber.current
|
||||||
|
block.call
|
||||||
|
else
|
||||||
|
@@channel.send block
|
||||||
|
until @@done.receive
|
||||||
|
Fiber.yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
require "http/client"
|
|
||||||
require "json"
|
require "json"
|
||||||
require "csv"
|
require "csv"
|
||||||
require "../rename"
|
require "../rename"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ module MangaDex
|
|||||||
|
|
||||||
def pop : Queue::Job?
|
def pop : Queue::Job?
|
||||||
job = nil
|
job = nil
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@queue.path}" do |db|
|
DB.open "sqlite3://#{@queue.path}" do |db|
|
||||||
begin
|
begin
|
||||||
db.query_one "select * from queue where id not like '%-%' " \
|
db.query_one "select * from queue where id not like '%-%' " \
|
||||||
@@ -37,6 +38,7 @@ module MangaDex
|
|||||||
rescue
|
rescue
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
job
|
job
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+31
-6
@@ -1,12 +1,29 @@
|
|||||||
require "./config"
|
require "./config"
|
||||||
require "./queue"
|
require "./queue"
|
||||||
require "./server"
|
require "./server"
|
||||||
|
require "./main_fiber"
|
||||||
require "./mangadex/*"
|
require "./mangadex/*"
|
||||||
require "option_parser"
|
require "option_parser"
|
||||||
require "clim"
|
require "clim"
|
||||||
require "./plugin/*"
|
require "./plugin/*"
|
||||||
|
|
||||||
MANGO_VERSION = "0.9.0"
|
MANGO_VERSION = "0.11.0"
|
||||||
|
|
||||||
|
# From http://www.network-science.de/ascii/
|
||||||
|
BANNER = %{
|
||||||
|
|
||||||
|
_| _|
|
||||||
|
_|_| _|_| _|_|_| _|_|_| _|_|_| _|_|
|
||||||
|
_| _| _| _| _| _| _| _| _| _| _|
|
||||||
|
_| _| _| _| _| _| _| _| _| _|
|
||||||
|
_| _| _|_|_| _| _| _|_|_| _|_|
|
||||||
|
_|
|
||||||
|
_|_|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DESCRIPTION = "Mango - Manga Server and Web Reader. Version #{MANGO_VERSION}"
|
||||||
|
|
||||||
macro common_option
|
macro common_option
|
||||||
option "-c PATH", "--config=PATH", type: String,
|
option "-c PATH", "--config=PATH", type: String,
|
||||||
@@ -22,20 +39,28 @@ end
|
|||||||
|
|
||||||
class CLI < Clim
|
class CLI < Clim
|
||||||
main do
|
main do
|
||||||
desc "Mango - Manga Server and Web Reader. Version #{MANGO_VERSION}"
|
desc DESCRIPTION
|
||||||
usage "mango [sub_command] [options]"
|
usage "mango [sub_command] [options]"
|
||||||
help short: "-h"
|
help short: "-h"
|
||||||
version "Version #{MANGO_VERSION}", short: "-v"
|
version "Version #{MANGO_VERSION}", short: "-v"
|
||||||
common_option
|
common_option
|
||||||
run do |opts|
|
run do |opts|
|
||||||
|
puts BANNER
|
||||||
|
puts DESCRIPTION
|
||||||
|
puts
|
||||||
|
|
||||||
|
# empty ARGV so it won't be passed to Kemal
|
||||||
|
ARGV.clear
|
||||||
|
|
||||||
Config.load(opts.config).set_current
|
Config.load(opts.config).set_current
|
||||||
MangaDex::Downloader.default
|
MangaDex::Downloader.default
|
||||||
Plugin::Downloader.default
|
Plugin::Downloader.default
|
||||||
|
|
||||||
# empty ARGV so it won't be passed to Kemal
|
spawn do
|
||||||
ARGV.clear
|
Server.new.start
|
||||||
server = Server.new
|
end
|
||||||
server.start
|
|
||||||
|
MainFiber.start_and_block
|
||||||
end
|
end
|
||||||
|
|
||||||
sub "admin" do
|
sub "admin" do
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class Plugin
|
|||||||
|
|
||||||
def pop : Queue::Job?
|
def pop : Queue::Job?
|
||||||
job = nil
|
job = nil
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@queue.path}" do |db|
|
DB.open "sqlite3://#{@queue.path}" do |db|
|
||||||
begin
|
begin
|
||||||
db.query_one "select * from queue where id like '%-%' " \
|
db.query_one "select * from queue where id like '%-%' " \
|
||||||
@@ -18,6 +19,7 @@ class Plugin
|
|||||||
rescue
|
rescue
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
job
|
job
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
require "duktape/runtime"
|
require "duktape/runtime"
|
||||||
require "myhtml"
|
require "myhtml"
|
||||||
require "http"
|
|
||||||
require "xml"
|
require "xml"
|
||||||
|
|
||||||
class Plugin
|
class Plugin
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ class Queue
|
|||||||
"Attepmting to create it"
|
"Attepmting to create it"
|
||||||
Dir.mkdir_p dir
|
Dir.mkdir_p dir
|
||||||
end
|
end
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
begin
|
begin
|
||||||
db.exec "create table if not exists queue " \
|
db.exec "create table if not exists queue " \
|
||||||
@@ -138,11 +139,13 @@ class Queue
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Push an array of jobs into the queue, and return the number of jobs
|
# Push an array of jobs into the queue, and return the number of jobs
|
||||||
# inserted. Any job already exists in the queue will be ignored.
|
# inserted. Any job already exists in the queue will be ignored.
|
||||||
def push(jobs : Array(Job))
|
def push(jobs : Array(Job))
|
||||||
start_count = self.count
|
start_count = self.count
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
jobs.each do |job|
|
jobs.each do |job|
|
||||||
db.exec "insert or ignore into queue values " \
|
db.exec "insert or ignore into queue values " \
|
||||||
@@ -152,16 +155,19 @@ class Queue
|
|||||||
job.success_count, job.fail_count, job.time.to_unix_ms
|
job.success_count, job.fail_count, job.time.to_unix_ms
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
self.count - start_count
|
self.count - start_count
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset(id : String)
|
def reset(id : String)
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
db.exec "update queue set status = 0, status_message = '', " \
|
db.exec "update queue set status = 0, status_message = '', " \
|
||||||
"pages = 0, success_count = 0, fail_count = 0 " \
|
"pages = 0, success_count = 0, fail_count = 0 " \
|
||||||
"where id = (?)", id
|
"where id = (?)", id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def reset(job : Job)
|
def reset(job : Job)
|
||||||
self.reset job.id
|
self.reset job.id
|
||||||
@@ -169,91 +175,113 @@ class Queue
|
|||||||
|
|
||||||
# Reset all failed tasks (missing pages and error)
|
# Reset all failed tasks (missing pages and error)
|
||||||
def reset
|
def reset
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
db.exec "update queue set status = 0, status_message = '', " \
|
db.exec "update queue set status = 0, status_message = '', " \
|
||||||
"pages = 0, success_count = 0, fail_count = 0 " \
|
"pages = 0, success_count = 0, fail_count = 0 " \
|
||||||
"where status = 2 or status = 4"
|
"where status = 2 or status = 4"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def delete(id : String)
|
def delete(id : String)
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
db.exec "delete from queue where id = (?)", id
|
db.exec "delete from queue where id = (?)", id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def delete(job : Job)
|
def delete(job : Job)
|
||||||
self.delete job.id
|
self.delete job.id
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_status(status : JobStatus)
|
def delete_status(status : JobStatus)
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
db.exec "delete from queue where status = (?)", status.to_i
|
db.exec "delete from queue where status = (?)", status.to_i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def count_status(status : JobStatus)
|
def count_status(status : JobStatus)
|
||||||
num = 0
|
num = 0
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
num = db.query_one "select count(*) from queue where " \
|
num = db.query_one "select count(*) from queue where " \
|
||||||
"status = (?)", status.to_i, as: Int32
|
"status = (?)", status.to_i, as: Int32
|
||||||
end
|
end
|
||||||
|
end
|
||||||
num
|
num
|
||||||
end
|
end
|
||||||
|
|
||||||
def count
|
def count
|
||||||
num = 0
|
num = 0
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
num = db.query_one "select count(*) from queue", as: Int32
|
num = db.query_one "select count(*) from queue", as: Int32
|
||||||
end
|
end
|
||||||
|
end
|
||||||
num
|
num
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_status(status : JobStatus, job : Job)
|
def set_status(status : JobStatus, job : Job)
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
db.exec "update queue set status = (?) where id = (?)",
|
db.exec "update queue set status = (?) where id = (?)",
|
||||||
status.to_i, job.id
|
status.to_i, job.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def get_all
|
def get_all
|
||||||
jobs = [] of Job
|
jobs = [] of Job
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
jobs = db.query_all "select * from queue order by time" do |rs|
|
jobs = db.query_all "select * from queue order by time" do |rs|
|
||||||
Job.from_query_result rs
|
Job.from_query_result rs
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
jobs
|
jobs
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_success(job : Job)
|
def add_success(job : Job)
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
db.exec "update queue set success_count = success_count + 1 " \
|
db.exec "update queue set success_count = success_count + 1 " \
|
||||||
"where id = (?)", job.id
|
"where id = (?)", job.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def add_fail(job : Job)
|
def add_fail(job : Job)
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
db.exec "update queue set fail_count = fail_count + 1 " \
|
db.exec "update queue set fail_count = fail_count + 1 " \
|
||||||
"where id = (?)", job.id
|
"where id = (?)", job.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_pages(pages : Int32, job : Job)
|
def set_pages(pages : Int32, job : Job)
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
db.exec "update queue set pages = (?), success_count = 0, " \
|
db.exec "update queue set pages = (?), success_count = 0, " \
|
||||||
"fail_count = 0 where id = (?)", pages, job.id
|
"fail_count = 0 where id = (?)", pages, job.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def add_message(msg : String, job : Job)
|
def add_message(msg : String, job : Job)
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
db.exec "update queue set status_message = " \
|
db.exec "update queue set status_message = " \
|
||||||
"status_message || (?) || (?) where id = (?)",
|
"status_message || (?) || (?) where id = (?)",
|
||||||
"\n", msg, job.id
|
"\n", msg, job.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def <<(downloader : Downloader)
|
def <<(downloader : Downloader)
|
||||||
@downloaders << downloader
|
@downloaders << downloader
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+32
-4
@@ -32,6 +32,7 @@ class Storage
|
|||||||
"Attepmting to create it"
|
"Attepmting to create it"
|
||||||
Dir.mkdir_p dir
|
Dir.mkdir_p dir
|
||||||
end
|
end
|
||||||
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
begin
|
begin
|
||||||
# We create the `ids` table first. even if the uses has an
|
# We create the `ids` table first. even if the uses has an
|
||||||
@@ -66,6 +67,7 @@ class Storage
|
|||||||
@db = DB.open "sqlite3://#{@path}"
|
@db = DB.open "sqlite3://#{@path}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
macro init_admin
|
macro init_admin
|
||||||
random_pw = random_str
|
random_pw = random_str
|
||||||
@@ -87,6 +89,8 @@ class Storage
|
|||||||
end
|
end
|
||||||
|
|
||||||
def verify_user(username, password)
|
def verify_user(username, password)
|
||||||
|
out_token = nil
|
||||||
|
MainFiber.run do
|
||||||
get_db 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 " \
|
||||||
@@ -94,24 +98,29 @@ class Storage
|
|||||||
username, as: {String, String?}
|
username, as: {String, String?}
|
||||||
unless verify_password hash, password
|
unless verify_password hash, password
|
||||||
Logger.debug "Password does not match the hash"
|
Logger.debug "Password does not match the hash"
|
||||||
return nil
|
next
|
||||||
end
|
end
|
||||||
Logger.debug "User #{username} verified"
|
Logger.debug "User #{username} verified"
|
||||||
return token if token
|
if token
|
||||||
|
out_token = token
|
||||||
|
next
|
||||||
|
end
|
||||||
token = random_str
|
token = random_str
|
||||||
Logger.debug "Updating token for #{username}"
|
Logger.debug "Updating token for #{username}"
|
||||||
db.exec "update users set token = (?) where username = (?)",
|
db.exec "update users set token = (?) where username = (?)",
|
||||||
token, username
|
token, username
|
||||||
return token
|
out_token = token
|
||||||
rescue e
|
rescue e
|
||||||
Logger.error "Error when verifying user #{username}: #{e}"
|
Logger.error "Error when verifying user #{username}: #{e}"
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
out_token
|
||||||
|
end
|
||||||
|
|
||||||
def verify_token(token)
|
def verify_token(token)
|
||||||
username = nil
|
username = nil
|
||||||
|
MainFiber.run do
|
||||||
get_db 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 " \
|
||||||
@@ -120,11 +129,13 @@ class Storage
|
|||||||
Logger.debug "Unable to verify token"
|
Logger.debug "Unable to verify token"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
username
|
username
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_admin(token)
|
def verify_admin(token)
|
||||||
is_admin = false
|
is_admin = false
|
||||||
|
MainFiber.run do
|
||||||
get_db 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 " \
|
||||||
@@ -133,11 +144,13 @@ class Storage
|
|||||||
Logger.debug "Unable to verify user as admin"
|
Logger.debug "Unable to verify user as admin"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
is_admin
|
is_admin
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_users
|
def list_users
|
||||||
results = Array(Tuple(String, Bool)).new
|
results = Array(Tuple(String, Bool)).new
|
||||||
|
MainFiber.run do
|
||||||
get_db 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
|
||||||
@@ -145,6 +158,7 @@ class Storage
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
results
|
results
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -152,17 +166,20 @@ class Storage
|
|||||||
validate_username username
|
validate_username username
|
||||||
validate_password password
|
validate_password password
|
||||||
admin = (admin ? 1 : 0)
|
admin = (admin ? 1 : 0)
|
||||||
|
MainFiber.run do
|
||||||
get_db 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
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def update_user(original_username, username, password, admin)
|
def update_user(original_username, username, password, admin)
|
||||||
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?
|
||||||
|
MainFiber.run do
|
||||||
get_db 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 = (?) " \
|
||||||
@@ -176,14 +193,18 @@ class Storage
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def delete_user(username)
|
def delete_user(username)
|
||||||
|
MainFiber.run do
|
||||||
get_db 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
|
||||||
|
end
|
||||||
|
|
||||||
def logout(token)
|
def logout(token)
|
||||||
|
MainFiber.run do
|
||||||
get_db 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
|
||||||
@@ -191,13 +212,16 @@ class Storage
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def get_id(path, is_title)
|
def get_id(path, is_title)
|
||||||
id = nil
|
id = nil
|
||||||
|
MainFiber.run do
|
||||||
get_db do |db|
|
get_db do |db|
|
||||||
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}
|
||||||
end
|
end
|
||||||
|
end
|
||||||
id
|
id
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -206,6 +230,7 @@ class Storage
|
|||||||
end
|
end
|
||||||
|
|
||||||
def bulk_insert_ids
|
def bulk_insert_ids
|
||||||
|
MainFiber.run do
|
||||||
get_db do |db|
|
get_db do |db|
|
||||||
db.transaction do |tx|
|
db.transaction do |tx|
|
||||||
@insert_ids.each do |tp|
|
@insert_ids.each do |tp|
|
||||||
@@ -216,12 +241,15 @@ class Storage
|
|||||||
end
|
end
|
||||||
@insert_ids.clear
|
@insert_ids.clear
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
|
MainFiber.run do
|
||||||
unless @db.nil?
|
unless @db.nil?
|
||||||
@db.not_nil!.close
|
@db.not_nil!.close
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def to_json(json : JSON::Builder)
|
def to_json(json : JSON::Builder)
|
||||||
json.string self
|
json.string self
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
require "http_proxy"
|
||||||
|
|
||||||
|
# Monkey-patch `HTTP::Client` to make it respect the `*_PROXY`
|
||||||
|
# environment variables
|
||||||
|
module HTTP
|
||||||
|
class Client
|
||||||
|
private def self.exec(uri : URI, tls : TLSContext = nil)
|
||||||
|
Logger.debug "Using monkey-patched HTTP::Client"
|
||||||
|
previous_def uri, tls do |client, path|
|
||||||
|
client.set_proxy get_proxy uri
|
||||||
|
yield client, path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def get_proxy(uri : URI) : HTTP::Proxy::Client?
|
||||||
|
no_proxy = ENV["no_proxy"]? || ENV["NO_PROXY"]?
|
||||||
|
return if no_proxy &&
|
||||||
|
no_proxy.split(",").any? &.== uri.hostname
|
||||||
|
|
||||||
|
case uri.scheme
|
||||||
|
when "http"
|
||||||
|
env_to_proxy "http_proxy"
|
||||||
|
when "https"
|
||||||
|
env_to_proxy "https_proxy"
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def env_to_proxy(key : String) : HTTP::Proxy::Client?
|
||||||
|
val = ENV[key.downcase]? || ENV[key.upcase]?
|
||||||
|
return if val.nil?
|
||||||
|
|
||||||
|
begin
|
||||||
|
uri = URI.parse val
|
||||||
|
HTTP::Proxy::Client.new uri.hostname.not_nil!, uri.port.not_nil!
|
||||||
|
rescue
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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] %>
|
||||||
|
|||||||
@@ -67,10 +67,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="uk-section uk-section-small">
|
<div class="uk-section uk-section-small">
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-section uk-section-small">
|
<div class="uk-section uk-section-small" id="main-section">
|
||||||
<div class="uk-container uk-container-small">
|
<div class="uk-container uk-container-small">
|
||||||
<div id="alert"></div>
|
<div id="alert"></div>
|
||||||
<%= content %>
|
<%= content %>
|
||||||
|
<div class="uk-visible@m" id="totop-wrapper" x-data="{}" x-show="$('body').height() > 1.5 * $(window).height()">
|
||||||
|
<a href="#" uk-totop uk-scroll></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -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