Merge pull request #173 from hkalexling/rc/0.21.0

This commit is contained in:
Alex Ling 2021-03-11 00:44:49 +08:00 committed by GitHub
commit 8829d2e237
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 673 additions and 273 deletions

View File

@ -52,7 +52,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
### CLI ### CLI
``` ```
Mango - Manga Server and Web Reader. Version 0.20.2 Mango - Manga Server and Web Reader. Version 0.21.0
Usage: Usage:
@ -93,7 +93,7 @@ default_username: ""
auth_proxy_header_name: "" auth_proxy_header_name: ""
mangadex: mangadex:
base_url: https://mangadex.org base_url: https://mangadex.org
api_url: https://mangadex.org/api/v2 api_url: https://api.mangadex.org/v2
download_wait_seconds: 5 download_wait_seconds: 5
download_retries: 4 download_retries: 4
download_queue_db_path: ~/mango/queue.db download_queue_db_path: ~/mango/queue.db

View File

@ -0,0 +1,20 @@
class CreateMangaDexAccount < MG::Base
def up : String
<<-SQL
CREATE TABLE md_account (
username TEXT NOT NULL PRIMARY KEY,
token TEXT NOT NULL,
expire INTEGER NOT NULL,
FOREIGN KEY (username) REFERENCES users (username)
ON UPDATE CASCADE
ON DELETE CASCADE
);
SQL
end
def down : String
<<-SQL
DROP TABLE md_account;
SQL
end
end

View File

@ -34,9 +34,11 @@
.uk-card-body { .uk-card-body {
padding: 20px; padding: 20px;
.uk-card-title { .uk-card-title {
max-height: 3em;
font-size: 1rem; font-size: 1rem;
} }
.uk-card-title:not(.free-height) {
max-height: 3em;
}
} }
} }

View File

@ -43,3 +43,22 @@
@internal-list-bullet-image: "../img/list-bullet.svg"; @internal-list-bullet-image: "../img/list-bullet.svg";
@internal-accordion-open-image: "../img/accordion-open.svg"; @internal-accordion-open-image: "../img/accordion-open.svg";
@internal-accordion-close-image: "../img/accordion-close.svg"; @internal-accordion-close-image: "../img/accordion-close.svg";
.hook-card-default() {
.uk-light & {
background: @card-secondary-background;
color: @card-secondary-color;
}
}
.hook-card-default-title() {
.uk-light & {
color: @card-secondary-title-color;
}
}
.hook-card-default-hover() {
.uk-light & {
background-color: @card-secondary-hover-background;
}
}

View File

@ -117,14 +117,10 @@ const setTheme = (theme) => {
if (theme === 'dark') { if (theme === 'dark') {
$('html').css('background', 'rgb(20, 20, 20)'); $('html').css('background', 'rgb(20, 20, 20)');
$('body').addClass('uk-light'); $('body').addClass('uk-light');
$('.uk-card').addClass('uk-card-secondary');
$('.uk-card').removeClass('uk-card-default');
$('.ui-widget-content').addClass('dark'); $('.ui-widget-content').addClass('dark');
} else { } else {
$('html').css('background', ''); $('html').css('background', '');
$('body').removeClass('uk-light'); $('body').removeClass('uk-light');
$('.uk-card').removeClass('uk-card-secondary');
$('.uk-card').addClass('uk-card-default');
$('.ui-widget-content').removeClass('dark'); $('.ui-widget-content').removeClass('dark');
} }
}; };

View File

@ -3,9 +3,12 @@ const downloadComponent = () => {
chaptersLimit: 1000, chaptersLimit: 1000,
loading: false, loading: false,
addingToDownload: false, addingToDownload: false,
searchAvailable: false,
searchInput: '', searchInput: '',
data: {}, data: {},
chapters: [], chapters: [],
mangaAry: undefined, // undefined: not searching; []: searched but no result
candidateManga: {},
langChoice: 'All', langChoice: 'All',
groupChoice: 'All', groupChoice: 'All',
chapterRange: '', chapterRange: '',
@ -48,7 +51,21 @@ const downloadComponent = () => {
childList: true, childList: true,
subtree: true subtree: true
}); });
$.getJSON(`${base_url}api/admin/mangadex/expires`)
.done((data) => {
if (data.error) {
alert('danger', 'Failed to check MangaDex integration status. Error: ' + data.error);
return;
}
if (data.expires && data.expires > Math.floor(Date.now() / 1000))
this.searchAvailable = true;
})
.fail((jqXHR, status) => {
alert('danger', `Failed to check MangaDex integration status. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
}, },
filtersUpdated() { filtersUpdated() {
if (!this.data.chapters) if (!this.data.chapters)
this.chapters = []; this.chapters = [];
@ -90,10 +107,11 @@ const downloadComponent = () => {
console.log('filtered chapters:', _chapters); console.log('filtered chapters:', _chapters);
this.chapters = _chapters; this.chapters = _chapters;
}, },
search() { search() {
if (this.loading || this.searchInput === '') return; if (this.loading || this.searchInput === '') return;
this.loading = true;
this.data = {}; this.data = {};
this.mangaAry = undefined;
var int_id = -1; var int_id = -1;
try { try {
@ -103,29 +121,54 @@ const downloadComponent = () => {
} catch (e) { } catch (e) {
int_id = parseInt(this.searchInput); int_id = parseInt(this.searchInput);
} }
if (int_id <= 0 || isNaN(int_id)) {
alert('danger', 'Please make sure you are using a valid manga ID or manga URL from Mangadex.'); if (!isNaN(int_id) && int_id > 0) {
this.loading = false; // The input is a positive integer. We treat it as an ID.
return; this.loading = true;
$.getJSON(`${base_url}api/admin/mangadex/manga/${int_id}`)
.done((data) => {
if (data.error) {
alert('danger', 'Failed to get manga info. Error: ' + data.error);
return;
}
this.data = data;
this.chapters = data.chapters;
this.mangaAry = undefined;
})
.fail((jqXHR, status) => {
alert('danger', `Failed to get manga info. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
.always(() => {
this.loading = false;
});
} else {
if (!this.searchAvailable) {
alert('danger', 'Please make sure you are using a valid manga ID or manga URL from Mangadex. If you are trying to search MangaDex with a search term, please log in to MangaDex first by going to "Admin -> Connect to MangaDex".');
return;
}
// Search as a search term
this.loading = true;
$.getJSON(`${base_url}api/admin/mangadex/search?${$.param({
query: this.searchInput
})}`)
.done((data) => {
if (data.error) {
alert('danger', `Failed to search MangaDex. Error: ${data.error}`);
return;
}
this.mangaAry = data.manga;
this.data = {};
})
.fail((jqXHR, status) => {
alert('danger', `Failed to search MangaDex. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
.always(() => {
this.loading = false;
});
} }
$.getJSON(`${base_url}api/admin/mangadex/manga/${int_id}`)
.done((data) => {
if (data.error) {
alert('danger', 'Failed to get manga info. Error: ' + data.error);
return;
}
this.data = data;
this.chapters = data.chapters;
})
.fail((jqXHR, status) => {
alert('danger', `Failed to get manga info. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
.always(() => {
this.loading = false;
});
}, },
parseRange(str) { parseRange(str) {
@ -228,6 +271,17 @@ const downloadComponent = () => {
this.addingToDownload = false; this.addingToDownload = false;
}); });
}); });
},
chooseManga(manga) {
this.candidateManga = manga;
UIkit.modal($('#modal').get(0)).show();
},
confirmManga(id) {
UIkit.modal($('#modal').get(0)).hide();
this.searchInput = id;
this.search();
} }
}; };
}; };

61
public/js/mangadex.js Normal file
View File

@ -0,0 +1,61 @@
const component = () => {
return {
username: '',
password: '',
expires: undefined,
loading: true,
loggingIn: false,
init() {
this.loading = true;
$.ajax({
type: 'GET',
url: `${base_url}api/admin/mangadex/expires`,
contentType: "application/json",
})
.done(data => {
console.log(data);
if (data.error) {
alert('danger', `Failed to retrieve MangaDex token status. Error: ${data.error}`);
return;
}
this.expires = data.expires;
this.loading = false;
})
.fail((jqXHR, status) => {
alert('danger', `Failed to retrieve MangaDex token status. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
});
},
login() {
if (!(this.username && this.password)) return;
this.loggingIn = true;
$.ajax({
type: 'POST',
url: `${base_url}api/admin/mangadex/login`,
contentType: "application/json",
dataType: 'json',
data: JSON.stringify({
username: this.username,
password: this.password
})
})
.done(data => {
console.log(data);
if (data.error) {
alert('danger', `Failed to log in. Error: ${data.error}`);
return;
}
this.expires = data.expires;
})
.fail((jqXHR, status) => {
alert('danger', `Failed to log in. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
.always(() => {
this.loggingIn = false;
});
},
get expired() {
return this.expires && moment().diff(moment.unix(this.expires)) > 0;
}
};
};

View File

@ -9,6 +9,7 @@ const readerComponent = () => {
flipAnimation: null, flipAnimation: null,
longPages: false, longPages: false,
lastSavedPage: page, lastSavedPage: page,
selectedIndex: 0, // 0: not selected; 1: the first page
/** /**
* Initialize the component by fetching the page dimensions * Initialize the component by fetching the page dimensions
@ -221,10 +222,7 @@ const readerComponent = () => {
*/ */
showControl(event) { showControl(event) {
const idx = event.currentTarget.id; const idx = event.currentTarget.id;
const pageCount = this.items.length; this.selectedIndex = idx;
const progressText = `Progress: ${idx}/${pageCount} (${(idx/pageCount * 100).toFixed(1)}%)`;
$('#progress-label').text(progressText);
$('#page-select').val(idx);
UIkit.modal($('#modal-sections')).show(); UIkit.modal($('#modal-sections')).show();
}, },
/** /**
@ -263,19 +261,22 @@ const readerComponent = () => {
}); });
}, },
/** /**
* Exits the reader, and optionally sets the reading progress tp 100% * Exits the reader, and sets the reading progress tp 100%
* *
* @param {string} exitUrl - The Exit URL * @param {string} exitUrl - The Exit URL
* @param {boolean} [markCompleted] - Whether we should mark the
* reading progress to 100%
*/ */
exitReader(exitUrl, markCompleted = false) { exitReader(exitUrl) {
if (!markCompleted) {
return this.redirect(exitUrl);
}
this.saveProgress(this.items.length, () => { this.saveProgress(this.items.length, () => {
this.redirect(exitUrl); this.redirect(exitUrl);
}); });
},
/**
* Handles the `change` event for the entry selector
*/
entryChanged() {
const id = $('#entry-select').val();
this.redirect(`${base_url}reader/${tid}/${id}`);
} }
}; };
} }

View File

@ -50,11 +50,11 @@ shards:
koa: koa:
git: https://github.com/hkalexling/koa.git git: https://github.com/hkalexling/koa.git
version: 0.5.0 version: 0.7.0
mangadex: mangadex:
git: https://github.com/hkalexling/mangadex.git git: https://github.com/hkalexling/mangadex.git
version: 0.5.0+git.commit.323110c56c2d5134ce4162b27a9b24ec34137fcb version: 0.8.0+git.commit.24e6fb51afd043721139355854e305b43bf98c43
mg: mg:
git: https://github.com/hkalexling/mg.git git: https://github.com/hkalexling/mg.git

View File

@ -1,5 +1,5 @@
name: mango name: mango
version: 0.20.2 version: 0.21.0
authors: authors:
- Alex Ling <hkalexling@gmail.com> - Alex Ling <hkalexling@gmail.com>

View File

@ -29,7 +29,7 @@ class Config
@[YAML::Field(ignore: true)] @[YAML::Field(ignore: true)]
@mangadex_defaults = { @mangadex_defaults = {
"base_url" => "https://mangadex.org", "base_url" => "https://mangadex.org",
"api_url" => "https://mangadex.org/api/v2", "api_url" => "https://api.mangadex.org/v2",
"download_wait_seconds" => 5, "download_wait_seconds" => 5,
"download_retries" => 4, "download_retries" => 4,
"download_queue_db_path" => File.expand_path("~/mango/queue.db", "download_queue_db_path" => File.expand_path("~/mango/queue.db",
@ -93,15 +93,23 @@ class Config
raise "Login is disabled, but default username is not set. " \ raise "Login is disabled, but default username is not set. " \
"Please set a default username" "Please set a default username"
end end
# `Logger.default` is not available yet
Log.setup :debug
unless mangadex["api_url"] =~ /\/v2/ unless mangadex["api_url"] =~ /\/v2/
# `Logger.default` is not available yet
Log.setup :debug
Log.warn { "It looks like you are using the deprecated MangaDex API " \ Log.warn { "It looks like you are using the deprecated MangaDex API " \
"v1 in your config file. Please update it to either " \ "v1 in your config file. Please update it to " \
"https://mangadex.org/api/v2 or " \
"https://api.mangadex.org/v2 to suppress this warning." } "https://api.mangadex.org/v2 to suppress this warning." }
mangadex["api_url"] = "https://mangadex.org/api/v2" mangadex["api_url"] = "https://api.mangadex.org/v2"
end end
if mangadex["api_url"] =~ /\/api\/v2/
Log.warn { "It looks like you are using the outdated MangaDex API " \
"url (mangadex.org/api/v2) in your config file. Please " \
"update it to https://api.mangadex.org/v2 to suppress this " \
"warning." }
mangadex["api_url"] = "https://api.mangadex.org/v2"
end
mangadex["api_url"] = mangadex["api_url"].to_s.rstrip "/" mangadex["api_url"] = mangadex["api_url"].to_s.rstrip "/"
mangadex["base_url"] = mangadex["base_url"].to_s.rstrip "/" mangadex["base_url"] = mangadex["base_url"].to_s.rstrip "/"
end end

View File

@ -134,10 +134,11 @@ class Entry
entries[idx + 1] entries[idx + 1]
end end
def previous_entry def previous_entry(username)
idx = @book.entries.index self entries = @book.sorted_entries username
idx = entries.index self
return nil if idx.nil? || idx == 0 return nil if idx.nil? || idx == 0
@book.entries[idx - 1] entries[idx - 1]
end end
def date_added def date_added

View File

@ -121,7 +121,7 @@ class Library
# 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
last_read = e.load_last_read username last_read = e.load_last_read username
pe = e.previous_entry pe = e.previous_entry username
if last_read.nil? && pe if last_read.nil? && pe
last_read = pe.load_last_read username last_read = pe.load_last_read username
end end

View File

@ -8,7 +8,7 @@ require "option_parser"
require "clim" require "clim"
require "tallboy" require "tallboy"
MANGO_VERSION = "0.20.2" MANGO_VERSION = "0.21.0"
# From http://www.network-science.de/ascii/ # From http://www.network-science.de/ascii/
BANNER = %{ BANNER = %{

View File

@ -73,5 +73,9 @@ struct AdminRouter
get "/admin/missing" do |env| get "/admin/missing" do |env|
layout "missing-items" layout "missing-items"
end end
get "/admin/mangadex" do |env|
layout "mangadex"
end
end end
end end

View File

@ -10,7 +10,7 @@ struct APIRouter
macro s(fields) macro s(fields)
{ {
{% for field in fields %} {% for field in fields %}
{{field}} => "string", {{field}} => String,
{% end %} {% end %}
} }
end end
@ -33,160 +33,49 @@ struct APIRouter
MD MD
Koa.cookie_auth "cookie", "mango-sessid-#{Config.current.port}" Koa.cookie_auth "cookie", "mango-sessid-#{Config.current.port}"
Koa.global_tag "admin", desc: <<-MD Koa.define_tag "admin", desc: <<-MD
These are the admin endpoints only accessible for users with admin access. A non-admin user will get HTTP 403 when calling the endpoints. These are the admin endpoints only accessible for users with admin access. A non-admin user will get HTTP 403 when calling the endpoints.
MD MD
Koa.binary "binary", desc: "A binary file" Koa.schema "entry", {
Koa.array "entryAry", "$entry", desc: "An array of entries" "pages" => Int32,
Koa.array "titleAry", "$title", desc: "An array of titles" "mtime" => Int64,
Koa.array "strAry", "string", desc: "An array of strings" }.merge(s %w(zip_path title size id title_id display_name cover_url)),
desc: "An entry in a book"
entry_schema = { Koa.schema "title", {
"pages" => "integer", "mtime" => Int64,
"mtime" => "integer", "entries" => ["entry"],
}.merge s %w(zip_path title size id title_id display_name cover_url) "titles" => ["title"],
Koa.object "entry", entry_schema, desc: "An entry in a book" "parents" => [String],
}.merge(s %w(dir title id display_name cover_url)),
title_schema = {
"mtime" => "integer",
"entries" => "$entryAry",
"titles" => "$titleAry",
"parents" => "$strAry",
}.merge s %w(dir title id display_name cover_url)
Koa.object "title", title_schema,
desc: "A manga title (a collection of entries and sub-titles)" desc: "A manga title (a collection of entries and sub-titles)"
Koa.object "library", { Koa.schema "result", {
"dir" => "string", "success" => Bool,
"titles" => "$titleAry", "error" => String?,
}, desc: "A library containing a list of top-level titles"
Koa.object "scanResult", {
"milliseconds" => "integer",
"titles" => "integer",
} }
Koa.object "progressResult", { Koa.schema("mdChapter", {
"progress" => "number", "id" => Int64,
} "group" => {} of String => String,
}.merge(s %w(title volume chapter language full_title time
manga_title manga_id)),
desc: "A MangaDex chapter")
Koa.object "result", { Koa.schema "mdManga", {
"success" => "boolean", "id" => Int64,
"error" => "string?", "chapters" => ["mdChapter"],
} }.merge(s %w(title description author artist cover_url)),
desc: "A MangaDex manga"
mc_schema = {
"groups" => "object",
}.merge s %w(id title volume chapter language full_title time manga_title manga_id)
Koa.object "mangadexChapter", mc_schema, desc: "A MangaDex chapter"
Koa.array "chapterAry", "$mangadexChapter"
mm_schema = {
"chapers" => "$chapterAry",
}.merge s %w(id title description author artist cover_url)
Koa.object "mangadexManga", mm_schema, desc: "A MangaDex manga"
Koa.object "chaptersObj", {
"chapters" => "$chapterAry",
}
Koa.object "successFailCount", {
"success" => "integer",
"fail" => "integer",
}
job_schema = {
"pages" => "integer",
"success_count" => "integer",
"fail_count" => "integer",
"time" => "integer",
}.merge s %w(id manga_id title manga_title status_message status)
Koa.object "job", job_schema, desc: "A download job in the queue"
Koa.array "jobAry", "$job"
Koa.object "jobs", {
"success" => "boolean",
"paused" => "boolean",
"jobs" => "$jobAry",
}
Koa.object "binaryUpload", {
"file" => "$binary",
}
Koa.object "pluginListBody", {
"plugin" => "string",
"query" => "string",
}
Koa.object "pluginChapter", {
"id" => "string",
"title" => "string",
}
Koa.array "pluginChapterAry", "$pluginChapter"
Koa.object "pluginList", {
"success" => "boolean",
"chapters" => "$pluginChapterAry?",
"title" => "string?",
"error" => "string?",
}
Koa.object "pluginDownload", {
"plugin" => "string",
"title" => "string",
"chapters" => "$pluginChapterAry",
}
Koa.object "dimension", {
"width" => "integer",
"height" => "integer",
}
Koa.array "dimensionAry", "$dimension"
Koa.object "dimensionResult", {
"success" => "boolean",
"dimensions" => "$dimensionAry?",
"margin" => "number",
"error" => "string?",
}
Koa.object "ids", {
"ids" => "$strAry",
}
Koa.object "tagsResult", {
"success" => "boolean",
"tags" => "$strAry?",
"error" => "string?",
}
Koa.object "missing", {
"path" => "string",
"id" => "string",
"signature" => "string",
}
Koa.array "missingAry", "$missing"
Koa.object "missingResult", {
"success" => "boolean",
"error" => "string?",
"entries" => "$missingAry?",
"titles" => "$missingAry?",
}
Koa.describe "Returns a page in a manga entry" Koa.describe "Returns a page in a manga entry"
Koa.path "tid", desc: "Title ID" Koa.path "tid", desc: "Title ID"
Koa.path "eid", desc: "Entry ID" Koa.path "eid", desc: "Entry ID"
Koa.path "page", type: "integer", desc: "The page number to return (starts from 1)" Koa.path "page", schema: Int32, desc: "The page number to return (starts from 1)"
Koa.response 200, ref: "$binary", media_type: "image/*" Koa.response 200, schema: Bytes, media_type: "image/*"
Koa.response 500, "Page not found or not readable" Koa.response 500, "Page not found or not readable"
Koa.tag "reader"
get "/api/page/:tid/:eid/:page" do |env| get "/api/page/:tid/:eid/:page" do |env|
begin begin
tid = env.params.url["tid"] tid = env.params.url["tid"]
@ -212,8 +101,9 @@ struct APIRouter
Koa.describe "Returns the cover image of a manga entry" Koa.describe "Returns the cover image of a manga entry"
Koa.path "tid", desc: "Title ID" Koa.path "tid", desc: "Title ID"
Koa.path "eid", desc: "Entry ID" Koa.path "eid", desc: "Entry ID"
Koa.response 200, ref: "$binary", media_type: "image/*" Koa.response 200, schema: Bytes, media_type: "image/*"
Koa.response 500, "Page not found or not readable" Koa.response 500, "Page not found or not readable"
Koa.tag "library"
get "/api/cover/:tid/:eid" do |env| get "/api/cover/:tid/:eid" do |env|
begin begin
tid = env.params.url["tid"] tid = env.params.url["tid"]
@ -238,8 +128,9 @@ struct APIRouter
Koa.describe "Returns the book with title `tid`" Koa.describe "Returns the book with title `tid`"
Koa.path "tid", desc: "Title ID" Koa.path "tid", desc: "Title ID"
Koa.response 200, ref: "$title" Koa.response 200, schema: "title"
Koa.response 404, "Title not found" Koa.response 404, "Title not found"
Koa.tag "library"
get "/api/book/:tid" do |env| get "/api/book/:tid" do |env|
begin begin
tid = env.params.url["tid"] tid = env.params.url["tid"]
@ -255,14 +146,21 @@ struct APIRouter
end end
Koa.describe "Returns the entire library with all titles and entries" Koa.describe "Returns the entire library with all titles and entries"
Koa.response 200, ref: "$library" Koa.response 200, schema: {
"dir" => String,
"titles" => ["title"],
}
Koa.tag "library"
get "/api/library" do |env| get "/api/library" do |env|
send_json env, Library.default.to_json send_json env, Library.default.to_json
end end
Koa.describe "Triggers a library scan" Koa.describe "Triggers a library scan"
Koa.tag "admin" Koa.tags ["admin", "library"]
Koa.response 200, ref: "$scanResult" Koa.response 200, schema: {
"milliseconds" => Float64,
"titles" => Int32,
}
post "/api/admin/scan" do |env| post "/api/admin/scan" do |env|
start = Time.utc start = Time.utc
Library.default.scan Library.default.scan
@ -274,8 +172,10 @@ struct APIRouter
end end
Koa.describe "Returns the thumbnail generation progress between 0 and 1" Koa.describe "Returns the thumbnail generation progress between 0 and 1"
Koa.tag "admin" Koa.tags ["admin", "library"]
Koa.response 200, ref: "$progressResult" Koa.response 200, schema: {
"progress" => Float64,
}
get "/api/admin/thumbnail_progress" do |env| get "/api/admin/thumbnail_progress" do |env|
send_json env, { send_json env, {
"progress" => Library.default.thumbnail_generation_progress, "progress" => Library.default.thumbnail_generation_progress,
@ -283,7 +183,7 @@ struct APIRouter
end end
Koa.describe "Triggers a thumbnail generation" Koa.describe "Triggers a thumbnail generation"
Koa.tag "admin" Koa.tags ["admin", "library"]
post "/api/admin/generate_thumbnails" do |env| post "/api/admin/generate_thumbnails" do |env|
spawn do spawn do
Library.default.generate_thumbnails Library.default.generate_thumbnails
@ -291,8 +191,8 @@ struct APIRouter
end end
Koa.describe "Deletes a user with `username`" Koa.describe "Deletes a user with `username`"
Koa.tag "admin" Koa.tags ["admin", "users"]
Koa.response 200, ref: "$result" Koa.response 200, schema: "result"
delete "/api/admin/user/delete/:username" do |env| delete "/api/admin/user/delete/:username" do |env|
begin begin
username = env.params.url["username"] username = env.params.url["username"]
@ -319,7 +219,8 @@ struct APIRouter
Koa.path "tid", desc: "Title ID" Koa.path "tid", desc: "Title ID"
Koa.query "eid", desc: "Entry ID", required: false Koa.query "eid", desc: "Entry ID", required: false
Koa.path "page", desc: "The new page number indicating the progress" Koa.path "page", desc: "The new page number indicating the progress"
Koa.response 200, ref: "$result" Koa.response 200, schema: "result"
Koa.tag "progress"
put "/api/progress/:tid/:page" do |env| put "/api/progress/:tid/:page" do |env|
begin begin
username = get_username env username = get_username env
@ -350,8 +251,11 @@ struct APIRouter
Koa.describe "Updates the reading progress of multiple entries in a title" Koa.describe "Updates the reading progress of multiple entries in a title"
Koa.path "action", desc: "The action to perform. Can be either `read` or `unread`" Koa.path "action", desc: "The action to perform. Can be either `read` or `unread`"
Koa.path "tid", desc: "Title ID" Koa.path "tid", desc: "Title ID"
Koa.body ref: "$ids", desc: "An array of entry IDs" Koa.body schema: {
Koa.response 200, ref: "$result" "ids" => [String],
}, desc: "An array of entry IDs"
Koa.response 200, schema: "result"
Koa.tag "progress"
put "/api/bulk_progress/:action/:tid" do |env| put "/api/bulk_progress/:action/:tid" do |env|
begin begin
username = get_username env username = get_username env
@ -377,11 +281,11 @@ struct APIRouter
Koa.describe "Sets the display name of a title or an entry", <<-MD Koa.describe "Sets the display name of a title or an entry", <<-MD
When `eid` is provided, apply the display name to the entry. Otherwise, apply the display name to the title identified by `tid`. When `eid` is provided, apply the display name to the entry. Otherwise, apply the display name to the title identified by `tid`.
MD MD
Koa.tag "admin" Koa.tags ["admin", "library"]
Koa.path "tid", desc: "Title ID" Koa.path "tid", desc: "Title ID"
Koa.query "eid", desc: "Entry ID", required: false Koa.query "eid", desc: "Entry ID", required: false
Koa.path "name", desc: "The new display name" Koa.path "name", desc: "The new display name"
Koa.response 200, ref: "$result" Koa.response 200, schema: "result"
put "/api/admin/display_name/:tid/:name" do |env| put "/api/admin/display_name/:tid/:name" do |env|
begin begin
title = (Library.default.get_title env.params.url["tid"]) title = (Library.default.get_title env.params.url["tid"])
@ -408,9 +312,9 @@ struct APIRouter
Koa.describe "Returns a MangaDex manga identified by `id`", <<-MD Koa.describe "Returns a MangaDex manga identified by `id`", <<-MD
On error, returns a JSON that contains the error message in the `error` field. On error, returns a JSON that contains the error message in the `error` field.
MD MD
Koa.tag "admin" Koa.tags ["admin", "mangadex"]
Koa.path "id", desc: "A MangaDex manga ID" Koa.path "id", desc: "A MangaDex manga ID"
Koa.response 200, ref: "$mangadexManga" Koa.response 200, schema: "mdManga"
get "/api/admin/mangadex/manga/:id" do |env| get "/api/admin/mangadex/manga/:id" do |env|
begin begin
id = env.params.url["id"] id = env.params.url["id"]
@ -425,9 +329,14 @@ struct APIRouter
Koa.describe "Adds a list of MangaDex chapters to the download queue", <<-MD Koa.describe "Adds a list of MangaDex chapters to the download queue", <<-MD
On error, returns a JSON that contains the error message in the `error` field. On error, returns a JSON that contains the error message in the `error` field.
MD MD
Koa.tag "admin" Koa.tags ["admin", "mangadex", "downloader"]
Koa.body ref: "$chaptersObj" Koa.body schema: {
Koa.response 200, ref: "$successFailCount" "chapters" => ["mdChapter"],
}
Koa.response 200, schema: {
"success" => Int32,
"fail" => Int32,
}
post "/api/admin/mangadex/download" do |env| post "/api/admin/mangadex/download" do |env|
begin begin
chapters = env.params.json["chapters"].as(Array).map { |c| c.as_h } chapters = env.params.json["chapters"].as(Array).map { |c| c.as_h }
@ -467,8 +376,18 @@ struct APIRouter
Koa.describe "Returns the current download queue", <<-MD Koa.describe "Returns the current download queue", <<-MD
On error, returns a JSON that contains the error message in the `error` field. On error, returns a JSON that contains the error message in the `error` field.
MD MD
Koa.tag "admin" Koa.tags ["admin", "downloader"]
Koa.response 200, ref: "$jobs" Koa.response 200, schema: {
"success" => Bool,
"error" => String?,
"paused" => Bool?,
"jobs?" => [{
"pages" => Int32,
"success_count" => Int32,
"fail_count" => Int32,
"time" => Int64,
}.merge(s %w(id manga_id title manga_title status_message status))],
}
get "/api/admin/mangadex/queue" do |env| get "/api/admin/mangadex/queue" do |env|
begin begin
jobs = Queue.default.get_all jobs = Queue.default.get_all
@ -494,10 +413,10 @@ struct APIRouter
When `action` is set to `retry`, the behavior depends on `id`. If `id` is provided, restarts the job identified by the ID. Otherwise, retries all jobs in the `Error` or `MissingPages` status in the queue. When `action` is set to `retry`, the behavior depends on `id`. If `id` is provided, restarts the job identified by the ID. Otherwise, retries all jobs in the `Error` or `MissingPages` status in the queue.
MD MD
Koa.tag "admin" Koa.tags ["admin", "downloader"]
Koa.path "action", desc: "The action to perform. It should be one of the followins: `delete`, `retry`, `pause` and `resume`." Koa.path "action", desc: "The action to perform. It should be one of the followins: `delete`, `retry`, `pause` and `resume`."
Koa.query "id", required: false, desc: "A job ID" Koa.query "id", required: false, desc: "A job ID"
Koa.response 200, ref: "$result" Koa.response 200, schema: "result"
post "/api/admin/mangadex/queue/:action" do |env| post "/api/admin/mangadex/queue/:action" do |env|
begin begin
action = env.params.url["action"] action = env.params.url["action"]
@ -546,8 +465,10 @@ struct APIRouter
When `eid` is omitted, the new cover image will be applied to the title. Otherwise, applies the image to the specified entry. When `eid` is omitted, the new cover image will be applied to the title. Otherwise, applies the image to the specified entry.
MD MD
Koa.tag "admin" Koa.tag "admin"
Koa.body type: "multipart/form-data", ref: "$binaryUpload" Koa.body media_type: "multipart/form-data", schema: {
Koa.response 200, ref: "$result" "file" => Bytes,
}
Koa.response 200, schema: "result"
post "/api/admin/upload/:target" do |env| post "/api/admin/upload/:target" do |env|
begin begin
target = env.params.url["target"] target = env.params.url["target"]
@ -603,9 +524,18 @@ struct APIRouter
end end
Koa.describe "Lists the chapters in a title from a plugin" Koa.describe "Lists the chapters in a title from a plugin"
Koa.tag "admin" Koa.tags ["admin", "downloader"]
Koa.body ref: "$pluginListBody" Koa.query "plugin", schema: String
Koa.response 200, ref: "$pluginList" Koa.query "query", schema: String
Koa.response 200, schema: {
"success" => Bool,
"error" => String?,
"chapters?" => [{
"id" => String,
"title" => String,
}],
"title" => String?,
}
get "/api/admin/plugin/list" do |env| get "/api/admin/plugin/list" do |env|
begin begin
query = env.params.query["query"].as String query = env.params.query["query"].as String
@ -629,9 +559,19 @@ struct APIRouter
end end
Koa.describe "Adds a list of chapters from a plugin to the download queue" Koa.describe "Adds a list of chapters from a plugin to the download queue"
Koa.tag "admin" Koa.tags ["admin", "downloader"]
Koa.body ref: "$pluginDownload" Koa.body schema: {
Koa.response 200, ref: "$successFailCount" "plugin" => String,
"title" => String,
"chapters" => [{
"id" => String,
"title" => String,
}],
}
Koa.response 200, schema: {
"success" => Int32,
"fail" => Int32,
}
post "/api/admin/plugin/download" do |env| post "/api/admin/plugin/download" do |env|
begin begin
plugin = Plugin.new env.params.json["plugin"].as String plugin = Plugin.new env.params.json["plugin"].as String
@ -664,7 +604,16 @@ struct APIRouter
Koa.describe "Returns the image dimensions of all pages in an entry" Koa.describe "Returns the image dimensions of all pages in an entry"
Koa.path "tid", desc: "A title ID" Koa.path "tid", desc: "A title ID"
Koa.path "eid", desc: "An entry ID" Koa.path "eid", desc: "An entry ID"
Koa.response 200, ref: "$dimensionResult" Koa.tag "reader"
Koa.response 200, schema: {
"success" => Bool,
"error" => String?,
"dimensions?" => [{
"width" => Int32,
"height" => Int32,
}],
"margin" => Int32?,
}
get "/api/dimensions/:tid/:eid" do |env| get "/api/dimensions/:tid/:eid" do |env|
begin begin
tid = env.params.url["tid"] tid = env.params.url["tid"]
@ -692,8 +641,9 @@ struct APIRouter
Koa.describe "Downloads an entry" Koa.describe "Downloads an entry"
Koa.path "tid", desc: "A title ID" Koa.path "tid", desc: "A title ID"
Koa.path "eid", desc: "An entry ID" Koa.path "eid", desc: "An entry ID"
Koa.response 200, ref: "$binary" Koa.response 200, schema: Bytes
Koa.response 404, "Entry not found" Koa.response 404, "Entry not found"
Koa.tags ["library", "reader"]
get "/api/download/:tid/:eid" do |env| get "/api/download/:tid/:eid" do |env|
begin begin
title = (Library.default.get_title env.params.url["tid"]).not_nil! title = (Library.default.get_title env.params.url["tid"]).not_nil!
@ -708,7 +658,12 @@ struct APIRouter
Koa.describe "Gets the tags of a title" Koa.describe "Gets the tags of a title"
Koa.path "tid", desc: "A title ID" Koa.path "tid", desc: "A title ID"
Koa.response 200, ref: "$tagsResult" Koa.response 200, schema: {
"success" => Bool,
"error" => String?,
"tags" => [String?],
}
Koa.tags ["library", "tags"]
get "/api/tags/:tid" do |env| get "/api/tags/:tid" do |env|
begin begin
title = (Library.default.get_title env.params.url["tid"]).not_nil! title = (Library.default.get_title env.params.url["tid"]).not_nil!
@ -728,7 +683,12 @@ struct APIRouter
end end
Koa.describe "Returns all tags" Koa.describe "Returns all tags"
Koa.response 200, ref: "$tagsResult" Koa.response 200, schema: {
"success" => Bool,
"error" => String?,
"tags" => [String?],
}
Koa.tags ["library", "tags"]
get "/api/tags" do |env| get "/api/tags" do |env|
begin begin
tags = Storage.default.list_tags tags = Storage.default.list_tags
@ -747,8 +707,8 @@ struct APIRouter
Koa.describe "Adds a new tag to a title" Koa.describe "Adds a new tag to a title"
Koa.path "tid", desc: "A title ID" Koa.path "tid", desc: "A title ID"
Koa.response 200, ref: "$result" Koa.response 200, schema: "result"
Koa.tag "admin" Koa.tags ["admin", "library", "tags"]
put "/api/admin/tags/:tid/:tag" do |env| put "/api/admin/tags/:tid/:tag" do |env|
begin begin
title = (Library.default.get_title env.params.url["tid"]).not_nil! title = (Library.default.get_title env.params.url["tid"]).not_nil!
@ -770,8 +730,8 @@ struct APIRouter
Koa.describe "Deletes a tag from a title" Koa.describe "Deletes a tag from a title"
Koa.path "tid", desc: "A title ID" Koa.path "tid", desc: "A title ID"
Koa.response 200, ref: "$result" Koa.response 200, schema: "result"
Koa.tag "admin" Koa.tags ["admin", "library", "tags"]
delete "/api/admin/tags/:tid/:tag" do |env| delete "/api/admin/tags/:tid/:tag" do |env|
begin begin
title = (Library.default.get_title env.params.url["tid"]).not_nil! title = (Library.default.get_title env.params.url["tid"]).not_nil!
@ -792,8 +752,16 @@ struct APIRouter
end end
Koa.describe "Lists all missing titles" Koa.describe "Lists all missing titles"
Koa.response 200, ref: "$missingResult" Koa.response 200, schema: {
Koa.tag "admin" "success" => Bool,
"error" => String?,
"titles?" => [{
"path" => String,
"id" => String,
"signature" => String,
}],
}
Koa.tags ["admin", "library"]
get "/api/admin/titles/missing" do |env| get "/api/admin/titles/missing" do |env|
begin begin
send_json env, { send_json env, {
@ -810,8 +778,16 @@ struct APIRouter
end end
Koa.describe "Lists all missing entries" Koa.describe "Lists all missing entries"
Koa.response 200, ref: "$missingResult" Koa.response 200, schema: {
Koa.tag "admin" "success" => Bool,
"error" => String?,
"entries?" => [{
"path" => String,
"id" => String,
"signature" => String,
}],
}
Koa.tags ["admin", "library"]
get "/api/admin/entries/missing" do |env| get "/api/admin/entries/missing" do |env|
begin begin
send_json env, { send_json env, {
@ -828,8 +804,8 @@ struct APIRouter
end end
Koa.describe "Deletes all missing titles" Koa.describe "Deletes all missing titles"
Koa.response 200, ref: "$result" Koa.response 200, schema: "result"
Koa.tag "admin" Koa.tags ["admin", "library"]
delete "/api/admin/titles/missing" do |env| delete "/api/admin/titles/missing" do |env|
begin begin
Storage.default.delete_missing_title Storage.default.delete_missing_title
@ -846,8 +822,8 @@ struct APIRouter
end end
Koa.describe "Deletes all missing entries" Koa.describe "Deletes all missing entries"
Koa.response 200, ref: "$result" Koa.response 200, schema: "result"
Koa.tag "admin" Koa.tags ["admin", "library"]
delete "/api/admin/entries/missing" do |env| delete "/api/admin/entries/missing" do |env|
begin begin
Storage.default.delete_missing_entry Storage.default.delete_missing_entry
@ -866,8 +842,8 @@ struct APIRouter
Koa.describe "Deletes a missing title identified by `tid`", <<-MD Koa.describe "Deletes a missing title identified by `tid`", <<-MD
Does nothing if the given `tid` is not found or if the title is not missing. Does nothing if the given `tid` is not found or if the title is not missing.
MD MD
Koa.response 200, ref: "$result" Koa.response 200, schema: "result"
Koa.tag "admin" Koa.tags ["admin", "library"]
delete "/api/admin/titles/missing/:tid" do |env| delete "/api/admin/titles/missing/:tid" do |env|
begin begin
tid = env.params.url["tid"] tid = env.params.url["tid"]
@ -887,8 +863,8 @@ struct APIRouter
Koa.describe "Deletes a missing entry identified by `eid`", <<-MD Koa.describe "Deletes a missing entry identified by `eid`", <<-MD
Does nothing if the given `eid` is not found or if the entry is not missing. Does nothing if the given `eid` is not found or if the entry is not missing.
MD MD
Koa.response 200, ref: "$result" Koa.response 200, schema: "result"
Koa.tag "admin" Koa.tags ["admin", "library"]
delete "/api/admin/entries/missing/:eid" do |env| delete "/api/admin/entries/missing/:eid" do |env|
begin begin
eid = env.params.url["eid"] eid = env.params.url["eid"]
@ -905,6 +881,115 @@ struct APIRouter
end end
end end
Koa.describe "Logs the current user into their MangaDex account", <<-MD
If successful, returns the expiration date (as a unix timestamp) of the newly created token.
MD
Koa.body schema: {
"username" => String,
"password" => String,
}
Koa.response 200, schema: {
"success" => Bool,
"error" => String?,
"expires" => Int64?,
}
Koa.tags ["admin", "mangadex", "users"]
post "/api/admin/mangadex/login" do |env|
begin
username = env.params.json["username"].as String
password = env.params.json["password"].as String
mango_username = get_username env
client = MangaDex::Client.from_config
client.auth username, password
Storage.default.save_md_token mango_username, client.token.not_nil!,
client.token_expires
send_json env, {
"success" => true,
"error" => nil,
"expires" => client.token_expires.to_unix,
}.to_json
rescue e
Logger.error e
send_json env, {
"success" => false,
"error" => e.message,
}.to_json
end
end
Koa.describe "Returns the expiration date (as a unix timestamp) of the mangadex token if it exists"
Koa.response 200, schema: {
"success" => Bool,
"error" => String?,
"expires" => Int64?,
}
Koa.tags ["admin", "mangadex", "users"]
get "/api/admin/mangadex/expires" do |env|
begin
username = get_username env
_, expires = Storage.default.get_md_token username
send_json env, {
"success" => true,
"error" => nil,
"expires" => expires.try &.to_unix,
}.to_json
rescue e
Logger.error e
send_json env, {
"success" => false,
"error" => e.message,
}.to_json
end
end
Koa.describe "Searches MangaDex for manga matching `query`", <<-MD
Returns an empty list if the current user hasn't logged in to MangaDex.
MD
Koa.query "query"
Koa.response 200, schema: {
"success" => Bool,
"error" => String?,
"manga?" => [{
"id" => Int64,
"title" => String,
"description" => String,
"mainCover" => String,
}],
}
Koa.tags ["admin", "mangadex"]
get "/api/admin/mangadex/search" do |env|
begin
username = get_username env
token, expires = Storage.default.get_md_token username
unless expires && token
raise "No token found for user #{username}"
end
client = MangaDex::Client.from_config
client.token = token
client.token_expires = expires
query = env.params.query["query"]
send_json env, {
"success" => true,
"error" => nil,
"manga" => client.partial_search query,
}.to_json
rescue e
Logger.error e
send_json env, {
"success" => false,
"error" => e.message,
}.to_json
end
end
doc = Koa.generate doc = Koa.generate
@@api_json = doc.to_json if doc @@api_json = doc.to_json if doc

View File

@ -30,6 +30,11 @@ struct ReaderRouter
title = (Library.default.get_title env.params.url["title"]).not_nil! title = (Library.default.get_title env.params.url["title"]).not_nil!
entry = (title.get_entry env.params.url["entry"]).not_nil! entry = (title.get_entry env.params.url["entry"]).not_nil!
sort_opt = SortOptions.from_info_json title.dir, username
get_sort_opt
entries = title.sorted_entries username, sort_opt
page_idx = env.params.url["page"].to_i page_idx = env.params.url["page"].to_i
if page_idx > entry.pages || page_idx <= 0 if page_idx > entry.pages || page_idx <= 0
raise "Page #{page_idx} not found." raise "Page #{page_idx} not found."
@ -37,10 +42,12 @@ struct ReaderRouter
exit_url = "#{base_url}book/#{title.id}" exit_url = "#{base_url}book/#{title.id}"
next_entry_url = nil next_entry_url = entry.next_entry(username).try do |e|
next_entry = entry.next_entry username "#{base_url}reader/#{title.id}/#{e.id}"
unless next_entry.nil? end
next_entry_url = "#{base_url}reader/#{title.id}/#{next_entry.id}"
previous_entry_url = entry.previous_entry(username).try do |e|
"#{base_url}reader/#{title.id}/#{e.id}"
end end
render "src/views/reader.html.ecr" render "src/views/reader.html.ecr"

View File

@ -34,7 +34,7 @@ class Storage
dir = File.dirname @path dir = File.dirname @path
unless Dir.exists? dir unless Dir.exists? dir
Logger.info "The DB directory #{dir} does not exist. " \ Logger.info "The DB directory #{dir} does not exist. " \
"Attepmting to create it" "Attempting to create it"
Dir.mkdir_p dir Dir.mkdir_p dir
end end
MainFiber.run do MainFiber.run do
@ -514,6 +514,37 @@ class Storage
delete_missing "titles", id delete_missing "titles", id
end end
def save_md_token(username : String, token : String, expire : Time)
MainFiber.run do
get_db do |db|
count = db.query_one "select count(*) from md_account where " \
"username = (?)", username, as: Int64
if count == 0
db.exec "insert into md_account values (?, ?, ?)", username, token,
expire.to_unix
else
db.exec "update md_account set token = (?), expire = (?) " \
"where username = (?)", token, expire.to_unix, username
end
end
end
end
def get_md_token(username) : Tuple(String?, Time?)
token = nil
expires = nil
MainFiber.run do
get_db do |db|
db.query_one? "select token, expire from md_account where " \
"username = (?)", username do |res|
token = res.read String
expires = Time.unix res.read Int64
end
end
end
{token, expires}
end
def close def close
MainFiber.run do MainFiber.run do
unless @db.nil? unless @db.nil?

View File

@ -33,6 +33,7 @@
<option>System</option> <option>System</option>
</select> </select>
</li> </li>
<li><a class="uk-link-reset" href="<%= base_url %>admin/mangadex">Connect to MangaDex</a></li>
</ul> </ul>
<hr class="uk-divider-icon"> <hr class="uk-divider-icon">

View File

@ -1,3 +1,3 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.dotdotdot/4.0.11/dotdotdot.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.dotdotdot/4.0.11/dotdotdot.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/protonet-jquery.inview/1.1.2/jquery.inview.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/protonet-jquery.inview/1.1.2/jquery.inview.min.js"></script>
<script src="<%= base_url %>js/dots.js"></script> <script src="<%= base_url %>js/dots.js"></script>

View File

@ -0,0 +1 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>

View File

@ -0,0 +1 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>

View File

@ -63,7 +63,7 @@
</div> </div>
<% content_for "script" do %> <% content_for "script" do %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script> <%= render_component "moment" %>
<script src="<%= base_url %>js/alert.js"></script> <script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/download-manager.js"></script> <script src="<%= base_url %>js/download-manager.js"></script>
<% end %> <% end %>

View File

@ -1,17 +1,39 @@
<h2 class=uk-title>Download from MangaDex</h2> <h2 class=uk-title>Download from MangaDex</h2>
<div x-data="downloadComponent()" x-init="init()"> <div x-data="downloadComponent()" x-init="init()">
<div class="uk-grid-small" uk-grid> <div class="uk-grid-small" uk-grid style="margin-bottom:40px;">
<div class="uk-width-3-4"> <div class="uk-width-expand">
<input class="uk-input" type="text" placeholder="MangaDex manga ID or URL" x-model="searchInput" @keydown.enter.debounce="search()"> <input class="uk-input" type="text" :placeholder="searchAvailable ? 'Search MangaDex or enter a manga ID/URL' : 'MangaDex manga ID or URL'" x-model="searchInput" @keydown.enter.debounce="search()">
</div> </div>
<div class="uk-width-1-4"> <div class="uk-width-auto">
<div uk-spinner class="uk-align-center" x-show="loading" x-cloak></div> <div uk-spinner class="uk-align-center" x-show="loading" x-cloak></div>
<button class="uk-button uk-button-default" x-show="!loading" @click="search()">Search</button> <button class="uk-button uk-button-default" x-show="!loading" @click="search()">Search</button>
</div> </div>
</div> </div>
<template x-if="mangaAry">
<div>
<p x-show="mangaAry.length === 0">No matching manga found.</p>
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
<template x-for="manga in mangaAry" :key="manga.id">
<div class="item" :data-id="manga.id" @click="chooseManga(manga)">
<div class="uk-card uk-card-default">
<div class="uk-card-media-top uk-inline">
<img uk-img :data-src="manga.mainCover">
</div>
<div class="uk-card-body">
<h3 class="uk-card-title break-word uk-margin-remove-bottom free-height" x-text="manga.title"></h3>
<p class="uk-text-meta" x-text="`ID: ${manga.id}`"></p>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<div x-show="data && data.chapters" x-cloak> <div x-show="data && data.chapters" x-cloak>
<div class"uk-grid-small" uk-grid style="margin-top:40px"> <div class"uk-grid-small" uk-grid>
<div class="uk-width-1-4@s"> <div class="uk-width-1-4@s">
<img :src="data.mainCover"> <img :src="data.mainCover">
</div> </div>
@ -107,11 +129,34 @@
</template> </template>
</table> </table>
</div> </div>
<div id="modal" class="uk-flex-top" uk-modal="container: false">
<div class="uk-modal-dialog uk-margin-auto-vertical">
<button class="uk-modal-close-default" type="button" uk-close></button>
<div class="uk-modal-header">
<h3 class="uk-modal-title break-word" x-text="candidateManga.title"></h3>
</div>
<div class="uk-modal-body">
<div class="uk-grid">
<div class="uk-width-1-3@s">
<img uk-img data-width data-height :src="candidateManga.mainCover" style="width:100%;margin-bottom:10px;">
<a :href="`<%= mangadex_base_url %>/manga/${candidateManga.id}`" x-text="`ID: ${candidateManga.id}`" class="uk-link-muted"></a>
</div>
<div class="uk-width-2-3@s" uk-overflow-auto>
<p x-text="candidateManga.description"></p>
</div>
</div>
</div>
<div class="uk-modal-footer">
<button class="uk-button uk-button-primary" type="button" @click="confirmManga(candidateManga.id)">Choose</button>
</div>
</div>
</div>
</div> </div>
<% content_for "script" do %> <% content_for "script" do %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script> <%= render_component "moment" %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> <%= render_component "jquery-ui" %>
<script src="<%= base_url %>js/alert.js"></script> <script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/download.js"></script> <script src="<%= base_url %>js/download.js"></script>
<% end %> <% end %>

View File

@ -77,7 +77,7 @@
<%- end -%> <%- end -%>
<% content_for "script" do %> <% content_for "script" do %>
<%= render_component "dots-scripts" %> <%= render_component "dots" %>
<script src="<%= base_url %>js/alert.js"></script> <script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/title.js"></script> <script src="<%= base_url %>js/title.js"></script>
<% end %> <% end %>

View File

@ -24,7 +24,7 @@
</div> </div>
<% content_for "script" do %> <% content_for "script" do %>
<%= render_component "dots-scripts" %> <%= render_component "dots" %>
<script src="<%= base_url %>js/search.js"></script> <script src="<%= base_url %>js/search.js"></script>
<script src="<%= base_url %>js/sort-items.js"></script> <script src="<%= base_url %>js/sort-items.js"></script>
<% end %> <% end %>

View File

@ -0,0 +1,39 @@
<div x-data="component()" x-init="init()">
<h2 class="uk-title">Connect to MangaDex</h2>
<div class"uk-grid-small" uk-grid x-show="!loading" x-cloak>
<div class="uk-width-1-2@s" x-show="!expires">
<p>This step is optional but highly recommended if you are using the MangaDex downloader. Connecting to MangaDex allows you to:</p>
<ul>
<li>Search MangaDex by search terms in addition to manga IDs</li>
<li>Automatically download new chapters when they are available (coming soon)</li>
</ul>
</div>
<div class="uk-width-1-2@s" x-show="expires">
<p>
<span x-show="!expired">You have logged in to MangaDex!</span>
<span x-show="expired">You have logged in to MangaDex but the token has expired.</span>
The expiration date of your token is <code x-text="moment.unix(expires).format('MMMM Do YYYY, HH:mm:ss')"></code>.
<span x-show="!expired">If the integration is not working, you</span>
<span x-show="expired">You</span>
can log in again and the token will be updated.
</p>
</div>
<div class="uk-width-1-2@s">
<div class="uk-margin">
<div class="uk-inline uk-width-1-1"><span class="uk-form-icon" uk-icon="icon:user"></span><input class="uk-input uk-form-large" type="text" x-model="username" @keydown.enter.debounce="login()"></div>
</div>
<div class="uk-margin">
<div class="uk-inline uk-width-1-1"><span class="uk-form-icon" uk-icon="icon:lock"></span><input class="uk-input uk-form-large" type="password" x-model="password" @keydown.enter.debounce="login()"></div>
</div>
<div class="uk-margin"><button class="uk-button uk-button-primary uk-button-large uk-width-1-1" @click="login()" :disabled="loggingIn">Login to MangaDex</button></div>
</div>
</div>
</div>
<% content_for "script" do %>
<%= render_component "moment" %>
<script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/mangadex.js"></script>
<% end %>

View File

@ -68,7 +68,7 @@
var pid = "<%= plugin.not_nil!.info.id %>"; var pid = "<%= plugin.not_nil!.info.id %>";
</script> </script>
<% end %> <% end %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> <%= render_component "jquery-ui" %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/js/jquery.tablesorter.combined.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/js/jquery.tablesorter.combined.min.js"></script>
<script src="<%= base_url %>js/alert.js"></script> <script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/plugin-download.js"></script> <script src="<%= base_url %>js/plugin-download.js"></script>

View File

@ -36,7 +36,7 @@
<%- if next_entry_url -%> <%- if next_entry_url -%>
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="nextEntry('<%= next_entry_url %>')">Next Entry</button> <button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="nextEntry('<%= next_entry_url %>')">Next Entry</button>
<%- else -%> <%- else -%>
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="exitReader('<%= exit_url %>', true)">Exit Reader</button> <button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="exitReader('<%= exit_url %>')">Exit Reader</button>
<%- end -%> <%- end -%>
</div> </div>
@ -68,12 +68,12 @@
</div> </div>
<div class="uk-modal-body"> <div class="uk-modal-body">
<div class="uk-margin"> <div class="uk-margin">
<p id="progress-label"></p> <p x-text="`Progress: ${selectedIndex}/${items.length} (${(selectedIndex/items.length * 100).toFixed(1)}%)`"></p>
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
<label class="uk-form-label" for="page-select">Jump to page</label> <label class="uk-form-label" for="page-select">Jump to Page</label>
<div class="uk-form-controls"> <div class="uk-form-controls">
<select id="page-select" class="uk-select" @change="pageChanged()"> <select id="page-select" class="uk-select" @change="pageChanged()" x-model="selectedIndex">
<%- (1..entry.pages).each do |p| -%> <%- (1..entry.pages).each do |p| -%>
<option value="<%= p %>"><%= p %></option> <option value="<%= p %>"><%= p %></option>
<%- end -%> <%- end -%>
@ -89,9 +89,33 @@
</select> </select>
</div> </div>
</div> </div>
<hr class="uk-divider-icon">
<div class="uk-margin">
<label class="uk-form-label" for="entry-select">Jump to Entry</label>
<div class="uk-form-controls">
<select id="entry-select" class="uk-select" @change="entryChanged()">
<% entries.each do |e| %>
<option value="<%= e.id %>"
<% if e.id == entry.id %>
selected
<% end %>>
<%= e.title %>
</option>
<% end %>
</select>
</div>
</div>
</div> </div>
<div class="uk-modal-footer uk-text-right"> <div class="uk-modal-footer uk-text-right">
<button class="uk-button uk-button-danger" type="button" @click="exitReader('<%= exit_url %>')">Exit Reader</button> <% if previous_entry_url %>
<a class="uk-button uk-button-default uk-margin-small-right" href="<%= previous_entry_url %>">Previous Entry</a>
<% end %>
<% if next_entry_url %>
<a class="uk-button uk-button-default uk-margin-small-right" href="<%= next_entry_url %>">Next Entry</a>
<% end %>
<a class="uk-button uk-button-danger" href="<%= exit_url %>">Exit Reader</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -24,7 +24,7 @@
</div> </div>
<% content_for "script" do %> <% content_for "script" do %>
<%= render_component "dots-scripts" %> <%= render_component "dots" %>
<script src="<%= base_url %>js/search.js"></script> <script src="<%= base_url %>js/search.js"></script>
<script src="<%= base_url %>js/sort-items.js"></script> <script src="<%= base_url %>js/sort-items.js"></script>
<% end %> <% end %>

View File

@ -123,7 +123,7 @@
</div> </div>
<% content_for "script" do %> <% content_for "script" do %>
<%= render_component "dots-scripts" %> <%= render_component "dots" %>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/css/select2.min.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/css/select2.min.css" rel="stylesheet" />
<link href="<%= base_url %>css/tags.css" rel="stylesheet" /> <link href="<%= base_url %>css/tags.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/js/select2.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/js/select2.min.js"></script>