mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-02 10:55:30 -04:00
Merge pull request #173 from hkalexling/rc/0.21.0
This commit is contained in:
commit
8829d2e237
@ -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
|
||||||
|
20
migration/md_account.11.cr
Normal file
20
migration/md_account.11.cr
Normal 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
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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
61
public/js/mangadex.js
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
@ -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}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 = %{
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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?
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
1
src/views/components/jquery-ui.html.ecr
Normal file
1
src/views/components/jquery-ui.html.ecr
Normal file
@ -0,0 +1 @@
|
|||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
1
src/views/components/moment.html.ecr
Normal file
1
src/views/components/moment.html.ecr
Normal file
@ -0,0 +1 @@
|
|||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
@ -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 %>
|
||||||
|
@ -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 %>
|
||||||
|
@ -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 %>
|
||||||
|
@ -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 %>
|
||||||
|
39
src/views/mangadex.html.ecr
Normal file
39
src/views/mangadex.html.ecr
Normal 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 %>
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 %>
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user