mirror of
https://github.com/hkalexling/Mango.git
synced 2026-05-01 00:00:55 -04:00
Compare commits
3 Commits
59bcb4db3b
...
3b19883dde
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b19883dde | |||
| 6844860065 | |||
| 9eb699ea3b |
@@ -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
|
||||
@@ -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
@@ -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
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user