Compare commits

...

3 Commits

Author SHA1 Message Date
Alex Ling 3b19883dde Use auto overflow tables
cherry-picked from a612500b0f
2021-06-07 07:35:44 +00:00
Alex Ling 6844860065 Revert "Subscription manager"
This reverts commit a612500b0f.
2021-06-07 07:32:32 +00:00
Alex Ling 9eb699ea3b Add plugin subscription types 2021-06-07 07:04:49 +00:00
15 changed files with 343 additions and 645 deletions
-31
View File
@@ -1,31 +0,0 @@
class CreateSubscription < MG::Base
def up : String
# We allow multiple subscriptions for the same manga.
# This can be useful for example when you want to download from multiple
# groups.
<<-SQL
CREATE TABLE subscription (
id INTEGER PRIMARY KEY,
manga_id INTEGER NOT NULL,
language TEXT,
group_id INTEGER,
min_volume INTEGER,
max_volume INTEGER,
min_chapter INTEGER,
max_chapter INTEGER,
last_checked INTEGER NOT NULL,
created_at INTEGER NOT NULL,
username TEXT NOT NULL,
FOREIGN KEY (username) REFERENCES users (username)
ON UPDATE CASCADE
ON DELETE CASCADE
);
SQL
end
def down : String
<<-SQL
DROP TABLE subscription;
SQL
end
end
-94
View File
@@ -280,100 +280,6 @@ const downloadComponent = () => {
UIkit.modal($('#modal').get(0)).hide();
this.searchInput = id;
this.search();
},
subscribe(langConfirmed = false, groupConfirmed = false) {
const filters = {
manga: this.data.id,
language: this.langChoice === 'All' ? null : this.langChoice,
group: this.groupChoice === 'All' ? null : this.groupChoice,
volume: this.volumeRange === '' ? null : this.volumeRange,
chapter: this.chapterRange === '' ? null : this.chapterRange
};
// Get group ID
if (filters.group) {
this.data.chapters.forEach(chp => {
const gid = chp.groups[filters.group];
if (gid) {
filters.groupId = gid;
return;
}
});
}
// Parse range values
if (filters.volume) {
[filters.volumeMin, filters.volumeMax] = this.parseRange(filters.volume);
}
if (filters.chapter) {
[filters.chapterMin, filters.chapterMax] = this.parseRange(filters.chapter);
}
if (!filters.language && !langConfirmed) {
UIkit.modal.confirm('You didn\'t specify a language in the filtering rules. This might cause Mango to download chapters that are not in your preferred language. Are you sure you want to continue?', {
labels: {
ok: 'Yes',
cancel: 'Cancel'
}
}).then(() => {
this.subscribe(true, groupConfirmed);
});
return;
}
if (!filters.group && !groupConfirmed) {
UIkit.modal.confirm('You didn\'t specify a group in the filtering rules. This might cause Mango to download multiple versions of the same chapter. Are you sure you want to continue?', {
labels: {
ok: 'Yes',
cancel: 'Cancel'
}
}).then(() => {
this.subscribe(langConfirmed, true);
});
return;
}
const mangaURL = `${mangadex_base_url}/manga/${filters.manga}`;
console.log(filters);
UIkit.modal.confirm(`All <strong>FUTURE</strong> chapters matching the following filters will be downloaded:<br>
<ul>
<li>Manga ID: ${filters.manga}</li>
<li>Language: ${filters.language || 'all'}</li>
<li>Group: ${filters.group || 'all'}</li>
<li>Volume: ${filters.volume || 'all'}</li>
<li>Chapter: ${filters.chapter || 'all'}</li>
</ul>
<strong>IMPORTANT:</strong> Please make sure you are following the manga on MangaDex, otherwise Mango won't be able to receive any updates. To follow it, visit <a href="${mangaURL}">${mangaURL}</a> and click "Follow".
`, {
labels: {
ok: 'Confirm',
cancel: 'Cancel'
}
}).then(() => {
$.ajax({
type: 'POST',
url: `${base_url}api/admin/mangadex/subscriptions`,
data: JSON.stringify({
subscription: filters
}),
contentType: "application/json",
dataType: 'json'
})
.done(data => {
console.log(data);
if (data.error) {
alert('danger', `Failed to subscribe. Error: ${data.error}`);
return;
}
alert('success', `You've successfully subscribed to this manga! You can view and manage your subscriptions on the <a href="${base_url}download/subscription">subscription manager page</a>.`);
})
.fail((jqXHR, status) => {
alert('danger', `Failed to subscribe. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
});
});
}
};
};
+1 -1
View File
@@ -54,7 +54,7 @@ shards:
mangadex:
git: https://github.com/hkalexling/mangadex.git
version: 0.11.0+git.commit.f5b0d64fbb138879fb9228b6e9ff34ec97c3e824
version: 0.9.0+git.commit.a8e5deb3e6f882f5bc0f4de66e0f6c20aa98a8a6
mg:
git: https://github.com/hkalexling/mg.git
+2 -4
View File
@@ -33,10 +33,8 @@ class Config
"download_retries" => 4,
"download_queue_db_path" => File.expand_path("~/mango/queue.db",
home: true),
"chapter_rename_rule" => "[Vol.{volume} ]" \
"[Ch.{chapter} ]{title|id}",
"manga_rename_rule" => "{title}",
"subscription_update_interval_hours" => 24,
"chapter_rename_rule" => "[Vol.{volume} ][Ch.{chapter} ]{title|id}",
"manga_rename_rule" => "{title}",
}
@@singlet : Config?
-19
View File
@@ -42,25 +42,6 @@ class Library
end
end
end
subscription_interval = Config.current
.mangadex["subscription_update_interval_hours"].as Int32
unless subscription_interval < 1
spawn do
loop do
subscriptions = Storage.default.subscriptions
Logger.info "Checking MangaDex for updates on " \
"#{subscriptions.size} subscriptions"
added_count = 0
subscriptions.each do |sub|
added_count += sub.check_for_updates
end
Logger.info "Subscription update completed. Added #{added_count} " \
"chapters to the download queue"
sleep subscription_interval.hours
end
end
end
end
def titles
-34
View File
@@ -56,39 +56,5 @@ module MangaDex
hash["full_title"] = JSON::Any.new full_title
hash.to_json
end
# We don't need to rename the manga title here. It will be renamed in
# src/mangadex/downloader.cr
def to_job : Queue::Job
Queue::Job.new(
id.to_s,
manga_id.to_s,
full_title,
manga_title,
Queue::JobStatus::Pending,
Time.unix timestamp
)
end
end
struct User
def updates_after(time : Time, &block : Chapter ->)
page = 1
stopped = false
until stopped
chapters = followed_updates(page: page).chapters
return if chapters.empty?
chapters.each do |c|
if time > Time.unix c.timestamp
stopped = true
break
end
yield c
end
page += 1
# Let's not DDOS MangaDex :)
sleep 5.seconds
end
end
end
end
+18
View File
@@ -2,6 +2,8 @@ require "duktape/runtime"
require "myhtml"
require "xml"
require "./subscriptions"
class Plugin
class Error < ::Exception
end
@@ -131,6 +133,22 @@ class Plugin
@info.not_nil!
end
def subscribe(subscription : Subscription)
list = SubscriptionList.new info.dir
list << subscription
list.save
end
def list_subscriptions
SubscriptionList.new(info.dir).ary
end
def unsubscribe(id : String)
list = SubscriptionList.new info.dir
list.reject &.id.== id
list.save
end
def initialize(id : String)
Plugin.build_info_ary
+87
View File
@@ -0,0 +1,87 @@
require "uuid"
enum FilterType
String
NumMin
NumMax
DateMin
DateMax
Array
def self.from_string(str)
case str
when "string"
String
when "number-min"
NumMin
when "number-max"
NumMax
when "date-min"
DateMin
when "date-max"
DateMax
when "array"
Array
else
raise "Unknown filter type with string #{str}"
end
end
end
struct Filter
include JSON::Serializable
property key : String
property value : String | Int32 | Int64 | Float32 | Nil
property type : FilterType
def initialize(@key, @value, @type)
end
def self.from_json(str) : Filter
json = JSON.parse str
key = json["key"].as_s
type = FilterType.from_string json["type"].as_s
_value = json["value"]
value = _value.as_s? || _value.as_i32? || _value.as_i64? ||
_value.as_f32? || nil
self.new key, value, type
end
end
struct Subscription
include JSON::Serializable
property id : String
property plugin_id : String
property name : String
property created_at : Int64
property last_checked : Int64
property filters = [] of Filter
def initialize(@plugin_id, @name)
@id = UUID.random.to_s
@created_at = Time.utc.to_unix
@last_checked = Time.utc.to_unix
end
end
struct SubscriptionList
@dir : String
@path : String
getter ary = [] of Subscription
forward_missing_to @ary
def initialize(@dir)
@path = Path[@dir, "subscriptions.json"]
if File.exists? @path
@ary = Array(Subscription).from_json File.read @path
end
end
def save
File.write @path, @ary.to_json
end
end
+12 -136
View File
@@ -1083,147 +1083,23 @@ struct APIRouter
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" => get_client(env).partial_search query,
}.to_json
rescue e
Logger.error e
send_json env, {
"success" => false,
"error" => e.message,
}.to_json
end
end
Koa.describe "Lists all MangaDex subscriptions"
Koa.response 200, schema: {
"success" => Bool,
"error" => String?,
"subscriptions?" => [{
"id" => Int64,
"username" => String,
"manga_id" => Int64,
"language" => String?,
"group_id" => Int64?,
"min_volume" => Int64?,
"max_volume" => Int64?,
"min_chapter" => Int64?,
"max_chapter" => Int64?,
"last_checked" => Int64,
"created_at" => Int64,
}],
}
Koa.tags ["admin", "mangadex", "subscriptions"]
get "/api/admin/mangadex/subscriptions" do |env|
begin
send_json env, {
"success" => true,
"error" => nil,
"subscriptions" => Storage.default.subscriptions,
}.to_json
rescue e
Logger.error e
send_json env, {
"success" => false,
"error" => e.message,
}.to_json
end
end
Koa.describe "Creates a new MangaDex subscription"
Koa.body schema: {
"subscription" => {
"manga" => Int64,
"language" => String?,
"groupId" => Int64?,
"volumeMin" => Int64?,
"volumeMax" => Int64?,
"chapterMin" => Int64?,
"chapterMax" => Int64?,
},
}
Koa.response 200, schema: {
"success" => Bool,
"error" => String?,
}
Koa.tags ["admin", "mangadex", "subscriptions"]
post "/api/admin/mangadex/subscriptions" do |env|
begin
json = env.params.json["subscription"].as Hash(String, JSON::Any)
sub = Subscription.new json["manga"].as_i64, get_username env
sub.language = json["language"]?.try &.as_s?
sub.group_id = json["groupId"]?.try &.as_i64?
sub.min_volume = json["volumeMin"]?.try &.as_i64?
sub.max_volume = json["volumeMax"]?.try &.as_i64?
sub.min_chapter = json["chapterMin"]?.try &.as_i64?
sub.max_chapter = json["chapterMax"]?.try &.as_i64?
Storage.default.save_subscription sub
send_json env, {
"success" => true,
"error" => nil,
}.to_json
rescue e
Logger.error e
send_json env, {
"success" => false,
"error" => e.message,
}.to_json
end
end
Koa.describe "Deletes a MangaDex subscription identified by `id`", <<-MD
Does nothing if the subscription was not created by the current user.
MD
Koa.response 200, schema: {
"success" => Bool,
"error" => String?,
}
Koa.tags ["admin", "mangadex", "subscriptions"]
delete "/api/admin/mangadex/subscriptions/:id" do |env|
begin
id = env.params.url["id"].to_i64
Storage.default.delete_subscription id, get_username env
send_json env, {
"success" => true,
"error" => nil,
}.to_json
rescue e
Logger.error e
send_json env, {
"success" => false,
"error" => e.message,
}.to_json
end
end
Koa.describe "Triggers an update for a MangaDex subscription identified by `id`", <<-MD
Does nothing if the subscription was not created by the current user.
MD
Koa.response 200, schema: {
"success" => Bool,
"error" => String?,
}
Koa.tags ["admin", "mangadex", "subscriptions"]
post "/api/admin/mangadex/subscriptions/check/:id" do |env|
begin
id = env.params.url["id"].to_i64
username = get_username env
sub = Storage.default.get_subscription id, username
unless sub
raise "Subscription with id #{id} not found under user #{username}"
end
spawn do
sub.check_for_updates
end
send_json env, {
"success" => true,
"error" => nil,
"manga" => client.partial_search query,
}.to_json
rescue e
Logger.error e
-6
View File
@@ -86,12 +86,6 @@ struct MainRouter
end
end
get "/download/subscription" do |env|
mangadex_base_url = Config.current.mangadex["base_url"]
username = get_username env
layout "subscription"
end
get "/" do |env|
begin
username = get_username env
-68
View File
@@ -5,7 +5,6 @@ require "base64"
require "./util/*"
require "mg"
require "../migration/*"
require "./subscription"
def hash_password(pw)
Crypto::Bcrypt::Password.create(pw).to_s
@@ -15,9 +14,6 @@ def verify_password(hash, pw)
(Crypto::Bcrypt::Password.new hash).verify pw
end
SUB_ATTR = %w(manga_id language group_id min_volume max_volume min_chapter
max_chapter username)
class Storage
@@insert_entry_ids = [] of IDTuple
@@insert_title_ids = [] of IDTuple
@@ -549,70 +545,6 @@ class Storage
{token, expires}
end
def save_subscription(sub : Subscription)
MainFiber.run do
get_db do |db|
{% begin %}
db.exec "insert into subscription (#{SUB_ATTR.join ","}, " \
"last_checked, created_at) values " \
"(#{Array.new(SUB_ATTR.size + 2, "?").join ","})",
{% for type in SUB_ATTR %}
sub.{{type.id}},
{% end %}
sub.last_checked.to_unix, sub.created_at.to_unix
{% end %}
end
end
end
def subscriptions : Array(Subscription)
subs = [] of Subscription
MainFiber.run do
get_db do |db|
db.query "select * from subscription" do |rs|
subs += Subscription.from_rs rs
end
end
end
subs
end
def delete_subscription(id : Int64, username : String)
MainFiber.run do
get_db do |db|
db.exec "delete from subscription where id = (?) and username = (?)",
id, username
end
end
end
def get_subscription(id : Int64, username : String) : Subscription?
sub = nil
MainFiber.run do
get_db do |db|
db.query "select * from subscription where id = (?) and " \
"username = (?) limit 1", id, username do |rs|
sub = Subscription.from_rs(rs).first?
end
end
end
sub
end
def update_subscription_last_checked(id : Int64? = nil)
MainFiber.run do
get_db do |db|
if id
db.exec "update subscription set last_checked = (?) where id = (?)",
Time.utc.to_unix, id
else
db.exec "update subscription set last_checked = (?)",
Time.utc.to_unix
end
end
end
end
def close
MainFiber.run do
unless @db.nil?
-19
View File
@@ -107,25 +107,6 @@ macro get_sort_opt
end
end
# Returns an authorized client
def get_client(username : String) : MangaDex::Client
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
client
end
def get_client(env) : MangaDex::Client
get_client get_username env
end
module HTTP
class Client
private def self.exec(uri : URI, tls : TLSContext = nil)
+2 -2
View File
@@ -52,9 +52,9 @@
<td x-text="`${job.plugin_id || ''}`"></td>
<td>
<a @click="jobAction('delete', $event)" uk-icon="trash" uk-tooltip="Delete"></a>
<a @click="jobAction('delete', $event)" uk-icon="trash"></a>
<template x-if="job.status_message.length > 0">
<a @click="jobAction('retry', $event)" uk-icon="refresh" uk-tooltip="Retry"></a>
<a @click="jobAction('retry', $event)" uk-icon="refresh"></a>
</template>
</td>
</tr>
+142 -150
View File
@@ -1,170 +1,162 @@
<h2 class=uk-title>Download from MangaDex</h2>
<div x-data="downloadComponent()" x-init="init()">
<div class="uk-grid-small" uk-grid style="margin-bottom:40px;">
<div class="uk-width-expand">
<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 class="uk-width-auto">
<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>
</div>
<div class="uk-grid-small" uk-grid style="margin-bottom:40px;">
<div class="uk-width-expand">
<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 class="uk-width-auto">
<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>
</div>
</div>
<template x-if="mangaAry">
<div>
<p x-show="mangaAry.length === 0">No matching manga found.</p>
<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 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>
</template>
</div>
</div>
</template>
<div x-show="data && data.chapters" x-cloak>
<div class"uk-grid-small" uk-grid>
<div class="uk-width-1-4@s">
<img :src="data.mainCover">
</div>
<div class="uk-width-1-4@s">
<p>Title: <a :href="`<%= mangadex_base_url %>/manga/${data.id}`" x-text="data.title"></a></p>
<p x-text="`Artist: ${data.artist}`"></p>
<p x-text="`Author: ${data.author}`"></p>
</div>
<div class="uk-form-stacked uk-width-1-2@s" id="filters">
<p class="uk-text-lead uk-margin-remove-bottom">
<span>Filter Chapters</span>
<button class="uk-icon-button uk-align-right" uk-icon="rss" uk-tooltip="Subscribe" x-show="searchAvailable" @click="subscribe()"></button>
</p>
<p class="uk-text-meta uk-margin-remove-top" x-text="`${chapters.length} chapters found`"></p>
<div class="uk-margin">
<label class="uk-form-label">Language</label>
<div class="uk-form-controls">
<select class="uk-select filter-field" x-model="langChoice" @change="filtersUpdated()">
<template x-for="lang in languages" :key="lang">
<option x-text="lang"></option>
</template>
</select>
</div>
</div>
<div class="uk-margin">
<label class="uk-form-label">Group</label>
<div class="uk-form-controls">
<select class="uk-select filter-field" x-model="groupChoice" @change="filtersUpdated()">
<template x-for="group in groups" :key="group">
<option x-text="group"></option>
</template>
</select>
</div>
</div>
<div class="uk-margin">
<label class="uk-form-label">Volume</label>
<div class="uk-form-controls">
<input class="uk-input filter-field" type="text" placeholder="e.g., 127, 10-14, >30, <=212, or leave it empty." x-model="volumeRange" @keydown.enter="filtersUpdated()">
</div>
</div>
<div class="uk-margin">
<label class="uk-form-label">Chapter</label>
<div class="uk-form-controls">
<input class="uk-input filter-field" type="text" placeholder="e.g., 127, 10-14, >30, <=212, or leave it empty." x-model="chapterRange" @keydown.enter="filtersUpdated()">
</div>
</div>
</div>
<div x-show="data && data.chapters" x-cloak>
<div class"uk-grid-small" uk-grid>
<div class="uk-width-1-4@s">
<img :src="data.mainCover">
</div>
<div class="uk-width-1-4@s">
<p>Title: <a :href="`<%= mangadex_base_url %>/manga/${data.id}`" x-text="data.title"></a></p>
<p x-text="`Artist: ${data.artist}`"></p>
<p x-text="`Author: ${data.author}`"></p>
</div>
<div class="uk-form-stacked uk-width-1-2@s" id="filters">
<p class="uk-text-lead uk-margin-remove-bottom">Filter Chapters</p>
<p class="uk-text-meta uk-margin-remove-top" x-text="`${chapters.length} chapters found`"></p>
<div class="uk-margin">
<label class="uk-form-label">Language</label>
<div class="uk-form-controls">
<select class="uk-select filter-field" x-model="langChoice" @change="filtersUpdated()">
<template x-for="lang in languages" :key="lang">
<option x-text="lang"></option>
</template>
</select>
</div>
</div>
<div class="uk-margin">
<div class="uk-margin">
<button class="uk-button uk-button-default" @click="selectAll()">Select All</button>
<button class="uk-button uk-button-default" @click="clearSelection()">Clear Selections</button>
<button class="uk-button uk-button-primary" @click="download()" x-show="!addingToDownload">Download Selected</button>
<div uk-spinner class="uk-margin-left" x-show="addingToDownload"></div>
</div>
<p class="uk-text-meta">Click on a table row to select the chapter. Drag your mouse over multiple rows to select them all. Hold Ctrl to make multiple non-adjacent selections.</p>
<label class="uk-form-label">Group</label>
<div class="uk-form-controls">
<select class="uk-select filter-field" x-model="groupChoice" @change="filtersUpdated()">
<template x-for="group in groups" :key="group">
<option x-text="group"></option>
</template>
</select>
</div>
</div>
<p x-text="`Mango can only list ${chaptersLimit} chapters, but we found ${chapters.length} chapters. Please use the filter options above to narrow down your search.`" x-show="chapters.length > chaptersLimit"></p>
<div class="uk-overflow-auto">
<table class="uk-table uk-table-striped" x-show="chapters.length <= chaptersLimit">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Language</th>
<th>Group</th>
<th>Volume</th>
<th>Chapter</th>
<th>Timestamp</th>
</tr>
</thead>
<template x-if="chapters.length <= chaptersLimit">
<tbody id="selectable">
<template x-for="chp in chapters" :key="chp">
<tr class="ui-widget-content">
<td><a :href="`<%= mangadex_base_url %>/chapter/${chp.id}`" x-text="chp.id"></a></td>
<td x-text="chp.title"></td>
<td x-text="chp.language"></td>
<td>
<template x-for="grp in Object.entries(chp.groups)">
<div>
<a :href="`<%= mangadex_base_url %>/group/${grp[1]}`" x-text="grp[0]"></a>
</div>
</template>
</td>
<td x-text="chp.volume"></td>
<td x-text="chp.chapter"></td>
<td x-text="`${moment.unix(chp.timestamp).fromNow()}`"></td>
</tr>
</template>
</tbody>
<div class="uk-margin">
<label class="uk-form-label">Volume</label>
<div class="uk-form-controls">
<input class="uk-input filter-field" type="text" placeholder="e.g., 127, 10-14, >30, <=212, or leave it empty." x-model="volumeRange" @keydown.enter="filtersUpdated()">
</div>
</div>
<div class="uk-margin">
<label class="uk-form-label">Chapter</label>
<div class="uk-form-controls">
<input class="uk-input filter-field" type="text" placeholder="e.g., 127, 10-14, >30, <=212, or leave it empty." x-model="chapterRange" @keydown.enter="filtersUpdated()">
</div>
</div>
</div>
</div>
<div class="uk-margin">
<div class="uk-margin">
<button class="uk-button uk-button-default" @click="selectAll()">Select All</button>
<button class="uk-button uk-button-default" @click="clearSelection()">Clear Selections</button>
<button class="uk-button uk-button-primary" @click="download()" x-show="!addingToDownload">Download Selected</button>
<div uk-spinner class="uk-margin-left" x-show="addingToDownload"></div>
</div>
<p class="uk-text-meta">Click on a table row to select the chapter. Drag your mouse over multiple rows to select them all. Hold Ctrl to make multiple non-adjacent selections.</p>
</div>
<p x-text="`Mango can only list ${chaptersLimit} chapters, but we found ${chapters.length} chapters. Please use the filter options above to narrow down your search.`" x-show="chapters.length > chaptersLimit"></p>
<table class="uk-table uk-table-striped uk-overflow-auto" x-show="chapters.length <= chaptersLimit">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Language</th>
<th>Group</th>
<th>Volume</th>
<th>Chapter</th>
<th>Timestamp</th>
</tr>
</thead>
<template x-if="chapters.length <= chaptersLimit">
<tbody id="selectable">
<template x-for="chp in chapters" :key="chp">
<tr class="ui-widget-content">
<td><a :href="`<%= mangadex_base_url %>/chapter/${chp.id}`" x-text="chp.id"></a></td>
<td x-text="chp.title"></td>
<td x-text="chp.language"></td>
<td>
<template x-for="grp in Object.entries(chp.groups)">
<div>
<a :href="`<%= mangadex_base_url %>/group/${grp[1]}`" x-text="grp[0]"></a>
</div>
</template>
</table>
</div>
</div>
</td>
<td x-text="chp.volume"></td>
<td x-text="chp.chapter"></td>
<td x-text="`${moment.unix(chp.timestamp).fromNow()}`"></td>
</tr>
</template>
</tbody>
</template>
</table>
</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">
<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 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>
<% content_for "script" do %>
<%= render_component "moment" %>
<%= render_component "jquery-ui" %>
<script>
const mangadex_base_url = "<%= mangadex_base_url %>";
</script>
<script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/download.js"></script>
<%= render_component "moment" %>
<%= render_component "jquery-ui" %>
<script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/download.js"></script>
<% end %>
+79 -81
View File
@@ -1,91 +1,89 @@
<!DOCTYPE html>
<html>
<%= render_component "head" %>
<%= render_component "head" %>
<body>
<div class="uk-offcanvas-content">
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
<div id="mobile-nav" uk-offcanvas="overlay: true">
<div class="uk-offcanvas-bar uk-flex uk-flex-column">
<ul class="uk-nav-parent-icon uk-nav-primary uk-nav-center uk-margin-auto-vertical" uk-nav>
<li><a href="<%= base_url %>">Home</a></li>
<li><a href="<%= base_url %>library">Library</a></li>
<li><a href="<%= base_url %>tags">Tags</a></li>
<% if is_admin %>
<li><a href="<%= base_url %>admin">Admin</a></li>
<li class="uk-parent">
<a href="#">Download</a>
<ul class="uk-nav-sub">
<li><a href="<%= base_url %>download">MangaDex</a></li>
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
<li><a href="<%= base_url %>download/subscription">Subscription Manager</a></li>
</ul>
</li>
<% end %>
<hr uk-divider>
<li><a onclick="toggleTheme()"><i class="fas fa-adjust"></i></a></li>
<li><a href="<%= base_url %>logout">Logout</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="uk-position-top">
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
<div class="uk-navbar-left uk-hidden@s">
<div class="uk-navbar-toggle" uk-navbar-toggle-icon="uk-navbar-toggle-icon" uk-toggle="target: #mobile-nav"></div>
</div>
<div class="uk-navbar-left uk-visible@s">
<a class="uk-navbar-item uk-logo" href="<%= base_url %>"><img src="<%= base_url %>img/icon.png" style="width:90px;height:90px;"></a>
<ul class="uk-navbar-nav">
<li><a href="<%= base_url %>">Home</a></li>
<li><a href="<%= base_url %>library">Library</a></li>
<li><a href="<%= base_url %>tags">Tags</a></li>
<% if is_admin %>
<li><a href="<%= base_url %>admin">Admin</a></li>
<li>
<a href="#">Download</a>
<div class="uk-navbar-dropdown">
<ul class="uk-nav uk-navbar-dropdown-nav">
<li class="uk-nav-header">Source</li>
<li><a href="<%= base_url %>download">MangaDex</a></li>
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
<li class="uk-nav-divider"></li>
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
<li><a href="<%= base_url %>download/subscription">Subscription Manager</a></li>
</ul>
<body>
<div class="uk-offcanvas-content">
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
<div id="mobile-nav" uk-offcanvas="overlay: true">
<div class="uk-offcanvas-bar uk-flex uk-flex-column">
<ul class="uk-nav-parent-icon uk-nav-primary uk-nav-center uk-margin-auto-vertical" uk-nav>
<li><a href="<%= base_url %>">Home</a></li>
<li><a href="<%= base_url %>library">Library</a></li>
<li><a href="<%= base_url %>tags">Tags</a></li>
<% if is_admin %>
<li><a href="<%= base_url %>admin">Admin</a></li>
<li class="uk-parent">
<a href="#">Download</a>
<ul class="uk-nav-sub">
<li><a href="<%= base_url %>download">MangaDex</a></li>
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
</ul>
</li>
<% end %>
<hr uk-divider>
<li><a onclick="toggleTheme()"><i class="fas fa-adjust"></i></a></li>
<li><a href="<%= base_url %>logout">Logout</a></li>
</ul>
</div>
</div>
</li>
<% end %>
</ul>
</div>
</div>
<div class="uk-navbar-right uk-visible@s">
<ul class="uk-navbar-nav">
<li><a onclick="toggleTheme()"><i class="fas fa-adjust"></i></a></li>
<li><a href="<%= base_url %>logout">Logout</a></li>
</ul>
<div class="uk-position-top">
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
<div class="uk-navbar-left uk-hidden@s">
<div class="uk-navbar-toggle" uk-navbar-toggle-icon="uk-navbar-toggle-icon" uk-toggle="target: #mobile-nav"></div>
</div>
<div class="uk-navbar-left uk-visible@s">
<a class="uk-navbar-item uk-logo" href="<%= base_url %>"><img src="<%= base_url %>img/icon.png" style="width:90px;height:90px;"></a>
<ul class="uk-navbar-nav">
<li><a href="<%= base_url %>">Home</a></li>
<li><a href="<%= base_url %>library">Library</a></li>
<li><a href="<%= base_url %>tags">Tags</a></li>
<% if is_admin %>
<li><a href="<%= base_url %>admin">Admin</a></li>
<li>
<a href="#">Download</a>
<div class="uk-navbar-dropdown">
<ul class="uk-nav uk-navbar-dropdown-nav">
<li class="uk-nav-header">Source</li>
<li><a href="<%= base_url %>download">MangaDex</a></li>
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
<li class="uk-nav-divider"></li>
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
</ul>
</div>
</li>
<% end %>
</ul>
</div>
<div class="uk-navbar-right uk-visible@s">
<ul class="uk-navbar-nav">
<li><a onclick="toggleTheme()"><i class="fas fa-adjust"></i></a></li>
<li><a href="<%= base_url %>logout">Logout</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="uk-section uk-section-small">
</div>
<div class="uk-section uk-section-small" style="position:relative;">
<div class="uk-container uk-container-small">
<div id="alert"></div>
<%= content %>
<div class="uk-visible@m" id="totop-wrapper" x-data="{}" x-show="$('body').height() > 1.5 * $(window).height()">
<a href="#" uk-totop uk-scroll></a>
<div class="uk-section uk-section-small">
</div>
</div>
</div>
<script>
setTheme();
const base_url = "<%= base_url %>";
</script>
<%= render_component "uikit" %>
<%= yield_content "script" %>
</body>
<div class="uk-section uk-section-small" style="position:relative;">
<div class="uk-container uk-container-small">
<div id="alert"></div>
<%= content %>
<div class="uk-visible@m" id="totop-wrapper" x-data="{}" x-show="$('body').height() > 1.5 * $(window).height()">
<a href="#" uk-totop uk-scroll></a>
</div>
</div>
</div>
<script>
setTheme();
const base_url = "<%= base_url %>";
</script>
<%= render_component "uikit" %>
<%= yield_content "script" %>
</body>
</html>