Update code
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto
|
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
config.req.php
|
||||||
|
|
||||||
|
/images/manga
|
||||||
|
/images/avatars
|
||||||
|
/images/lists
|
||||||
|
/images/groups
|
||||||
|
/images/covers
|
||||||
|
/images/banners
|
||||||
|
|
||||||
|
/data
|
||||||
|
/data-drama
|
||||||
|
/data-saver
|
||||||
|
/delete
|
||||||
|
/dl/*
|
||||||
|
/transferred
|
||||||
|
|
||||||
|
/vendor
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
sitemap.xml
|
||||||
|
|
||||||
|
/db/
|
||||||
|
/dist/*
|
||||||
|
phpunit.xml
|
||||||
|
/.idea
|
||||||
|
.env*.local
|
||||||
|
composer.phar
|
46
README.md
@ -1,3 +1,45 @@
|
|||||||
Code vulenerable to a PHP RCE.
|
**Edit a file, create a new file, and clone from Bitbucket in under 2 minutes**
|
||||||
|
|
||||||
IP Address for Site: 152.228.156.26
|
When you're done, you can delete the content in this README and update the file with details for others getting started with your repository.
|
||||||
|
|
||||||
|
*We recommend that you open this README in another tab as you perform the tasks below. You can [watch our video](https://youtu.be/0ocf7u76WSo) for a full demo of all the steps in this tutorial. Open the video in a new tab to avoid leaving Bitbucket.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Edit a file
|
||||||
|
|
||||||
|
You’ll start by editing this README file to learn how to edit a file in Bitbucket.
|
||||||
|
|
||||||
|
1. Click **Source** on the left side.
|
||||||
|
2. Click the README.md link from the list of files.
|
||||||
|
3. Click the **Edit** button.
|
||||||
|
4. Delete the following text: *Delete this line to make a change to the README from Bitbucket.*
|
||||||
|
5. After making your change, click **Commit** and then **Commit** again in the dialog. The commit page will open and you’ll see the change you just made.
|
||||||
|
6. Go back to the **Source** page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Create a file
|
||||||
|
|
||||||
|
Next, you’ll add a new file to this repository.
|
||||||
|
|
||||||
|
1. Click the **New file** button at the top of the **Source** page.
|
||||||
|
2. Give the file a filename of **contributors.txt**.
|
||||||
|
3. Enter your name in the empty file space.
|
||||||
|
4. Click **Commit** and then **Commit** again in the dialog.
|
||||||
|
5. Go back to the **Source** page.
|
||||||
|
|
||||||
|
Before you move on, go ahead and explore the repository. You've already seen the **Source** page, but check out the **Commits**, **Branches**, and **Settings** pages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Clone a repository
|
||||||
|
|
||||||
|
Use these steps to clone from SourceTree, our client for using the repository command-line free. Cloning allows you to work on your files locally. If you don't yet have SourceTree, [download and install first](https://www.sourcetreeapp.com/). If you prefer to clone from the command line, see [Clone a repository](https://confluence.atlassian.com/x/4whODQ).
|
||||||
|
|
||||||
|
1. You’ll see the clone button under the **Source** heading. Click that button.
|
||||||
|
2. Now click **Check out in SourceTree**. You may need to create a SourceTree account or log in.
|
||||||
|
3. When you see the **Clone New** dialog in SourceTree, update the destination path and name if you’d like to and then click **Clone**.
|
||||||
|
4. Open the directory you just created to see your repository’s files.
|
||||||
|
|
||||||
|
Now that you're more familiar with your Bitbucket repository, go ahead and add a new file locally. You can [push your change back to Bitbucket with SourceTree](https://confluence.atlassian.com/x/iqyBMg), or you can [add, commit,](https://confluence.atlassian.com/x/8QhODQ) and [push from the command line](https://confluence.atlassian.com/x/NQ0zDQ).
|
@ -1,10 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
//if (isset($_GET['_'])) {
|
|
||||||
// http_response_code(666);
|
|
||||||
// die();
|
|
||||||
//}
|
|
||||||
|
|
||||||
require_once ('../bootstrap.php');
|
require_once ('../bootstrap.php');
|
||||||
|
|
||||||
define('IS_NOJS', (isset($_GET['nojs']) && $_GET['nojs']));
|
define('IS_NOJS', (isset($_GET['nojs']) && $_GET['nojs']));
|
||||||
@ -174,13 +169,34 @@ switch ($function) {
|
|||||||
$recipient_user = new User($thread->recipient_id, 'user_id');
|
$recipient_user = new User($thread->recipient_id, 'user_id');
|
||||||
$sender_user = new User($thread->sender_id, 'user_id');
|
$sender_user = new User($thread->sender_id, 'user_id');
|
||||||
|
|
||||||
|
// in the context of a pm thread, "sender" is the op rather than necessarily whoever is currently sending the message
|
||||||
|
// so in case the current replying user is the "recipient", flip the variables around to match the correct meaning
|
||||||
|
if ($thread->recipient_id == $user->user_id) {
|
||||||
|
$recipient_user = $sender_user;
|
||||||
|
$sender_user = $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*$canReceiveDms = \validate_level($user, 'pr') // Staff can always send dms
|
||||||
|
|| ($recipient_user->dm_privacy ?? 0) < 1 // User has no dm restriction set
|
||||||
|
|| \in_array( // sender is a friend of recipient
|
||||||
|
$user->user_id,
|
||||||
|
\array_map(static function ($u) {
|
||||||
|
return $u['user_id'];
|
||||||
|
},
|
||||||
|
\array_filter($recipient_user->get_friends_user_ids(), static function ($u) {
|
||||||
|
return $u['accepted'] === 1;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);*/
|
||||||
|
|
||||||
$sender_blocked = $sender_user->get_blocked_user_ids();
|
$sender_blocked = $sender_user->get_blocked_user_ids();
|
||||||
$recipient_blocked = $recipient_user->get_blocked_user_ids();
|
$recipient_blocked = $recipient_user->get_blocked_user_ids();
|
||||||
|
|
||||||
// DM restriction if there is an active restriction and the sender isnt staff. restricted users can always message staff
|
// DM restriction if there is an active restriction and the sender isnt staff. restricted users can always message staff
|
||||||
$dm_restriction = $user->has_active_restriction(USER_RESTRICTION_CREATE_DM) && !validate_level($recipient_user, 'mod');
|
$dm_restriction = $user->has_active_restriction(USER_RESTRICTION_CREATE_DM) && !validate_level($recipient_user, 'mod');
|
||||||
|
|
||||||
if (($user->user_id == $thread->sender_id || $user->user_id == $thread->recipient_id) && !isset($sender_blocked[$thread->recipient_id]) && !isset($recipient_blocked[$thread->sender_id]) && !$dm_restriction) {
|
if (/*$canReceiveDms &&*/($user->user_id == $thread->sender_id || $user->user_id == $thread->recipient_id) && !isset($sender_blocked[$thread->recipient_id]) && !isset($recipient_blocked[$thread->sender_id]) && !$dm_restriction) {
|
||||||
$sql->modify('msg_reply', ' INSERT INTO mangadex_pm_msgs (msg_id, thread_id, user_id, timestamp, text) VALUES (NULL, ?, ?, UNIX_TIMESTAMP(), ?) ', [$id, $user->user_id, $reply]);
|
$sql->modify('msg_reply', ' INSERT INTO mangadex_pm_msgs (msg_id, thread_id, user_id, timestamp, text) VALUES (NULL, ?, ?, UNIX_TIMESTAMP(), ?) ', [$id, $user->user_id, $reply]);
|
||||||
|
|
||||||
if ($thread->sender_id == $user->user_id)
|
if ($thread->sender_id == $user->user_id)
|
||||||
@ -199,6 +215,8 @@ switch ($function) {
|
|||||||
$details = "You can't reply to the message because they are blocked.";
|
$details = "You can't reply to the message because they are blocked.";
|
||||||
elseif (isset($recipient_blocked[$thread->sender_id]))
|
elseif (isset($recipient_blocked[$thread->sender_id]))
|
||||||
$details = "You can't reply to the message because they are blocked.";
|
$details = "You can't reply to the message because they are blocked.";
|
||||||
|
/*elseif (!$canReceiveDms)
|
||||||
|
$details = "You can't send messages to this user until you have accepted each other as friends.";*/
|
||||||
elseif ($dm_restriction)
|
elseif ($dm_restriction)
|
||||||
$details = $user->get_restriction_message(USER_RESTRICTION_CREATE_DM) ?? "You can't reply to this dm.";
|
$details = $user->get_restriction_message(USER_RESTRICTION_CREATE_DM) ?? "You can't reply to this dm.";
|
||||||
else
|
else
|
||||||
@ -248,6 +266,20 @@ switch ($function) {
|
|||||||
$recipient_id = $sql->prep('recipient_id', ' SELECT user_id FROM mangadex_users WHERE username = ?', [$recipient], 'fetchColumn', '', -1);
|
$recipient_id = $sql->prep('recipient_id', ' SELECT user_id FROM mangadex_users WHERE username = ?', [$recipient], 'fetchColumn', '', -1);
|
||||||
$recipient_user = new User($recipient_id, 'user_id');
|
$recipient_user = new User($recipient_id, 'user_id');
|
||||||
|
|
||||||
|
$canReceiveDms = \validate_level($user, 'pr') // Staff can always send dms
|
||||||
|
|| ($recipient_user->dm_privacy ?? 0) < 1 // User has no dm restriction set
|
||||||
|
|| \in_array( // sender is a friend of recipient
|
||||||
|
$user->user_id,
|
||||||
|
\array_map(static function ($u) {
|
||||||
|
return $u['user_id'];
|
||||||
|
},
|
||||||
|
\array_filter($recipient_user->get_friends_user_ids(), static function ($u) {
|
||||||
|
return $u['accepted'] === 1;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
$user_blocked = $user->get_blocked_user_ids();
|
$user_blocked = $user->get_blocked_user_ids();
|
||||||
$recipient_blocked = $recipient_user->get_blocked_user_ids();
|
$recipient_blocked = $recipient_user->get_blocked_user_ids();
|
||||||
|
|
||||||
@ -257,7 +289,7 @@ switch ($function) {
|
|||||||
$has_banned_word = !validate_level($user, "pr") && (strpos_arr($message, SPAM_WORDS) !== FALSE || strpos_arr($subject, SPAM_WORDS) !== FALSE);
|
$has_banned_word = !validate_level($user, "pr") && (strpos_arr($message, SPAM_WORDS) !== FALSE || strpos_arr($subject, SPAM_WORDS) !== FALSE);
|
||||||
$has_dmed_recently = !validate_level($user, "pr") && ($timestamp - $last_message_timestamp < 30);
|
$has_dmed_recently = !validate_level($user, "pr") && ($timestamp - $last_message_timestamp < 30);
|
||||||
|
|
||||||
$is_valid_recipient = $recipient_id && $recipient_id != $user->user_id;
|
$is_valid_recipient = $canReceiveDms && $recipient_id && $recipient_id != $user->user_id;
|
||||||
$is_blocked = isset($user_blocked[$recipient_id]) || isset($recipient_blocked[$user->user_id]);
|
$is_blocked = isset($user_blocked[$recipient_id]) || isset($recipient_blocked[$user->user_id]);
|
||||||
|
|
||||||
if(!validate_level($user, 'member') || $dm_restriction){
|
if(!validate_level($user, 'member') || $dm_restriction){
|
||||||
@ -273,20 +305,17 @@ switch ($function) {
|
|||||||
$memcached->delete("user_{$recipient_id}_unread_msgs");
|
$memcached->delete("user_{$recipient_id}_unread_msgs");
|
||||||
|
|
||||||
$details = $thread_id;
|
$details = $thread_id;
|
||||||
}
|
} else if ($has_dmed_recently) {
|
||||||
else if($has_dmed_recently) {
|
|
||||||
$details = "Please wait before sending another message.";
|
$details = "Please wait before sending another message.";
|
||||||
}
|
} else if (!$canReceiveDms) {
|
||||||
else if(!$is_valid_recipient) {
|
$details = "You can't send messages to this user until you have accepted each other as friends.";
|
||||||
|
} else if (!$is_valid_recipient) {
|
||||||
$details = "$recipient is an invalid recipient.";
|
$details = "$recipient is an invalid recipient.";
|
||||||
}
|
} else if ($is_blocked) {
|
||||||
else if($is_blocked) {
|
|
||||||
$details = "$recipient has blocked you or you have blocked them.";
|
$details = "$recipient has blocked you or you have blocked them.";
|
||||||
}
|
} else if (!$captcha_validate['success']) {
|
||||||
else if(!$captcha_validate['success']) {
|
|
||||||
$details = 'You need to solve the captcha to send messages.';
|
$details = 'You need to solve the captcha to send messages.';
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$thread_id = $sql->modify('msg_send', ' INSERT INTO mangadex_pm_threads (thread_id, thread_subject, sender_id, recipient_id, thread_timestamp, sender_read, recipient_read, sender_deleted, recipient_deleted)
|
$thread_id = $sql->modify('msg_send', ' INSERT INTO mangadex_pm_threads (thread_id, thread_subject, sender_id, recipient_id, thread_timestamp, sender_read, recipient_read, sender_deleted, recipient_deleted)
|
||||||
VALUES (NULL, ?, ?, ?, UNIX_TIMESTAMP(), 1, 0, 0, 0) ', [$subject, $user->user_id, $recipient_id]);
|
VALUES (NULL, ?, ?, ?, UNIX_TIMESTAMP(), 1, 0, 0, 0) ', [$subject, $user->user_id, $recipient_id]);
|
||||||
|
|
||||||
@ -545,6 +574,89 @@ switch ($function) {
|
|||||||
|
|
||||||
$result = ($details) ? 0 : 1;
|
$result = ($details) ? 0 : 1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "banner_upload":
|
||||||
|
$file = $_FILES["file"];
|
||||||
|
$user_id = prepare_numeric($_POST["user_id"]);
|
||||||
|
$is_anonymous = isset($_POST["is_anonymous"]) ? 1 : 0;
|
||||||
|
$is_enabled = isset($_POST["is_enabled"]) ? 1 : 0;
|
||||||
|
$file_extension = strtolower(end(explode(".", $file["name"])));
|
||||||
|
|
||||||
|
if($file["error"] != UPLOAD_ERR_OK){
|
||||||
|
$error .= display_alert('danger', 'Failed', "File upload error.");
|
||||||
|
}
|
||||||
|
if(!validate_level($user, 'pr')) {
|
||||||
|
$error .= display_alert('danger', 'Failed', "You can't upload banners.");
|
||||||
|
}
|
||||||
|
if(!in_array($file_extension, ALLOWED_IMG_EXT)){
|
||||||
|
$error .= display_alert('danger', 'Failed', "Illegal file extension.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$error){
|
||||||
|
try {
|
||||||
|
$banner_id = $sql->modify('banner_upload',
|
||||||
|
"INSERT INTO mangadex_banners (user_id, is_anonymous, is_enabled, ext) VALUES (?, ?, ?, ?)",
|
||||||
|
[$user_id, $is_anonymous, $is_enabled, $file_extension]);
|
||||||
|
move_uploaded_file($file["tmp_name"], ABS_DATA_BASEPATH . "/banners/affiliatebanner$banner_id.$file_extension");
|
||||||
|
$memcached->delete("banners_all");
|
||||||
|
$memcached->delete("banners_enabled");
|
||||||
|
}
|
||||||
|
catch(Exception $e){
|
||||||
|
$error .= display_alert('danger', 'Failed', "Database error.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($error){
|
||||||
|
$details = $error;
|
||||||
|
print $error;
|
||||||
|
}
|
||||||
|
$result = $details ? 0 : 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "banner_edit":
|
||||||
|
$file = $_FILES["file"];
|
||||||
|
$banner_id = prepare_numeric($_GET["banner_id"]);
|
||||||
|
$user_id = prepare_numeric($_POST["user_id"]);
|
||||||
|
$is_anonymous = isset($_POST["is_anonymous"]) ? 1 : 0;
|
||||||
|
$is_enabled = isset($_POST["is_enabled"]) ? 1 : 0;
|
||||||
|
|
||||||
|
if($file["error"] == UPLOAD_ERR_NO_FILE){
|
||||||
|
$file_extension = $sql->prep("banner_ext", "SELECT ext FROM mangadex_banners WHERE banner_id = ?", [$banner_id], "fetch", PDO::FETCH_ASSOC, -1)["ext"];
|
||||||
|
}
|
||||||
|
else if($file["error"] == UPLOAD_ERR_OK){
|
||||||
|
$file_extension = strtolower(end(explode(".", $file["name"])));
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$error .= display_alert('danger', 'Failed', "File upload error.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!validate_level($user, 'pr')) {
|
||||||
|
$error .= display_alert('danger', 'Failed', "You can't edit banners.");
|
||||||
|
}
|
||||||
|
if(!in_array($file_extension, ALLOWED_IMG_EXT)){
|
||||||
|
$error .= display_alert('danger', 'Failed', "Illegal file extension.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$error){
|
||||||
|
try {
|
||||||
|
$sql->modify('banner_edit',
|
||||||
|
"UPDATE mangadex_banners SET user_id = ?, is_anonymous = ?, is_enabled = ?, ext = ? WHERE banner_id = ?",
|
||||||
|
[$user_id, $is_anonymous, $is_enabled, $file_extension, $banner_id]);
|
||||||
|
if($file["error"] == UPLOAD_ERR_OK){
|
||||||
|
move_uploaded_file($file["tmp_name"], ABS_DATA_BASEPATH . "/banners/affiliatebanner$banner_id.$file_extension");
|
||||||
|
}
|
||||||
|
$memcached->delete("banners_all");
|
||||||
|
$memcached->delete("banners_enabled");
|
||||||
|
}
|
||||||
|
catch(Exception $e){
|
||||||
|
$error .= display_alert('danger', 'Failed', "Database error.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($error){
|
||||||
|
$details = $error;
|
||||||
|
print $error;
|
||||||
|
}
|
||||||
|
$result = $details ? 0 : 1;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
if (!in_array($function, ['manga_follow', 'manga_unfollow']))
|
if (!in_array($function, ['manga_follow', 'manga_unfollow']))
|
||||||
|
@ -236,7 +236,7 @@ switch ($function) {
|
|||||||
|
|
||||||
$to = $email1;
|
$to = $email1;
|
||||||
$subject = "MangaDex: Account Creation - $username";
|
$subject = "MangaDex: Account Creation - $username";
|
||||||
$body = "Thank you for creating an account on MangaDex. \n\nUsername: $username \nPassword: (your chosen password) \n\nActivation code: $activation_key \n\nPlease visit " . URL . "activation/$activation_key to activate your account.";
|
$body = "Thank you for creating an account on MangaDex. \n\nUsername: $username \nPassword: (your chosen password) \n\nActivation code: $activation_key \n\nPlease visit " . URL . "activation/$activation_key to activate your account. \n\n If the above link doesn't work, try logging in and entering the activation code manually here " . URL . "activation instead.";
|
||||||
//$body = "Thank you for creating an account on MangaDex. \n\nUsername: $username \nPassword: (your chosen password) Due to problem with a spammer, activation codes are temporarily not being sent in this email. Please reply to this email to request an activation code. Apologies for the inconvenience!";
|
//$body = "Thank you for creating an account on MangaDex. \n\nUsername: $username \nPassword: (your chosen password) Due to problem with a spammer, activation codes are temporarily not being sent in this email. Please reply to this email to request an activation code. Apologies for the inconvenience!";
|
||||||
|
|
||||||
send_email($to, $subject, $body);
|
send_email($to, $subject, $body);
|
||||||
@ -427,7 +427,7 @@ switch ($function) {
|
|||||||
FROM mangadex_users u
|
FROM mangadex_users u
|
||||||
JOIN mangadex_ip_bans b
|
JOIN mangadex_ip_bans b
|
||||||
ON u.creation_ip = b.ip OR u.last_ip = b.ip
|
ON u.creation_ip = b.ip OR u.last_ip = b.ip
|
||||||
WHERE user_id = ? LIMIT 1', [$user->user_id], "fetchAll", PDO::FETCH_UNIQUE, -1);
|
WHERE user_id = ? LIMIT 1', [$user->user_id], "fetchColumn", '', -1);
|
||||||
if($user_banned){
|
if($user_banned){
|
||||||
$sql->modify('activate', ' UPDATE mangadex_users SET level_id = 0, activated = 1 WHERE user_id = ? AND activated = 0 LIMIT 1 ', [$user->user_id]);
|
$sql->modify('activate', ' UPDATE mangadex_users SET level_id = 0, activated = 1 WHERE user_id = ? AND activated = 0 LIMIT 1 ', [$user->user_id]);
|
||||||
}
|
}
|
||||||
@ -465,6 +465,39 @@ switch ($function) {
|
|||||||
$result = 1;
|
$result = 1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "change_activation_email":
|
||||||
|
$email = $_POST['email'];
|
||||||
|
|
||||||
|
if($email != $user->email){
|
||||||
|
// check for another account with this email
|
||||||
|
$count_email = $sql->prep('count_email', ' SELECT count(*) FROM mangadex_users WHERE email = ? ', [$email], 'fetchColumn', '', -1);
|
||||||
|
|
||||||
|
//check for banned hosts
|
||||||
|
$banned_hosts = $sql->query_read('tempmail', "SELECT host FROM mangadex_tempmail ORDER BY host ASC ", 'fetchAll', PDO::FETCH_COLUMN);
|
||||||
|
$email_parts = explode('@', $email);
|
||||||
|
$banned_email = in_array($email_parts[1], $banned_hosts);
|
||||||
|
|
||||||
|
if($count_email || $banned_email){
|
||||||
|
$details = 'This email cannot be used.';
|
||||||
|
print display_alert('danger', 'Failed', $details); // wrong code
|
||||||
|
$result = 0;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$sql->modify('change_email', ' UPDATE mangadex_users SET email = ? WHERE user_id = ? LIMIT 1 ', [$email, $user->user_id]);
|
||||||
|
$memcached->delete("user_$user->user_id");
|
||||||
|
|
||||||
|
$to = $email;
|
||||||
|
$subject = "MangaDex: Resend Activation Code - $user->username";
|
||||||
|
$body = "Here's your activation code. \n\nUsername: $user->username \n\nActivation code: $user->activation_key \n\nPlease visit " . URL . "activation/$user->activation_key to activate your account. ";
|
||||||
|
|
||||||
|
send_email($to, $subject, $body, 3);
|
||||||
|
|
||||||
|
$result = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
case "2fa_setup":
|
case "2fa_setup":
|
||||||
|
|
||||||
if ($user === null || $user->user_id < 2 || !validate_level($user, 'member')) {
|
if ($user === null || $user->user_id < 2 || !validate_level($user, 'member')) {
|
||||||
|
@ -738,6 +738,25 @@ switch ($function) {
|
|||||||
$result = (!is_numeric($details)) ? 0 : 1;
|
$result = (!is_numeric($details)) ? 0 : 1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "manga_regenerate_thumb":
|
||||||
|
$id = prepare_numeric($_GET['id']);
|
||||||
|
|
||||||
|
if (validate_level($user, 'mod')) {
|
||||||
|
$manga = new Manga($id);
|
||||||
|
$ext = strtolower($manga->manga_image);
|
||||||
|
|
||||||
|
generate_thumbnail(ABS_DATA_BASEPATH . "/manga/$manga->manga_id.$ext", 1);
|
||||||
|
|
||||||
|
$details = $id;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$details = "You can't regenerate this thumbnail.";
|
||||||
|
print display_alert('danger', 'Failed', $details); //fail
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = (!is_numeric($details)) ? 0 : 1;
|
||||||
|
break;
|
||||||
|
|
||||||
case "manga_report":
|
case "manga_report":
|
||||||
$id = prepare_numeric($_GET['id']);
|
$id = prepare_numeric($_GET['id']);
|
||||||
$report_text = htmlentities($_POST["report_text"]);
|
$report_text = htmlentities($_POST["report_text"]);
|
||||||
|
@ -25,10 +25,10 @@ switch ($function) {
|
|||||||
|
|
||||||
if (!$user->user_id)
|
if (!$user->user_id)
|
||||||
$error .= display_alert('danger', 'Failed', "Your session has timed out. Please log in again.");
|
$error .= display_alert('danger', 'Failed', "Your session has timed out. Please log in again.");
|
||||||
elseif ($upload < 80)
|
elseif ($upload < 40)
|
||||||
$error .= display_alert('danger', 'Failed', "Your upload speed must be at least 80 Mbps.");
|
$error .= display_alert('danger', 'Failed', "Your upload speed must be at least 40 Mbps.");
|
||||||
elseif ($download < 80)
|
elseif ($download < 40)
|
||||||
$error .= display_alert('danger', 'Failed', "Your download speed must be at least 80 Mbps.");
|
$error .= display_alert('danger', 'Failed', "Your download speed must be at least 40 Mbps.");
|
||||||
elseif ($upload > 65535)
|
elseif ($upload > 65535)
|
||||||
$error .= display_alert('danger', 'Failed', "Your upload speed is too high.");
|
$error .= display_alert('danger', 'Failed', "Your upload speed is too high.");
|
||||||
elseif ($download > 65535)
|
elseif ($download > 65535)
|
||||||
|
@ -298,6 +298,7 @@ switch ($function) {
|
|||||||
$post_sensitivity = prepare_numeric($_POST['swipe_sensitivity']);
|
$post_sensitivity = prepare_numeric($_POST['swipe_sensitivity']);
|
||||||
$reader_mode = prepare_numeric($_POST['reader_mode']) ?? 0;
|
$reader_mode = prepare_numeric($_POST['reader_mode']) ?? 0;
|
||||||
$image_fit = prepare_numeric($_POST['image_fit']) ?? 0;
|
$image_fit = prepare_numeric($_POST['image_fit']) ?? 0;
|
||||||
|
$data_saver = prepare_numeric($_POST['data_saver']) ?? 0;
|
||||||
$img_server = prepare_numeric($_POST['img_server']);
|
$img_server = prepare_numeric($_POST['img_server']);
|
||||||
if ($reader_mode && $image_fit == 2)
|
if ($reader_mode && $image_fit == 2)
|
||||||
$image_fit = 0;
|
$image_fit = 0;
|
||||||
@ -312,6 +313,7 @@ switch ($function) {
|
|||||||
$sql->modify('reader_settings', '
|
$sql->modify('reader_settings', '
|
||||||
UPDATE mangadex_users SET reader = ?, swipe_direction = ?, swipe_sensitivity = ?, reader_mode = ?, reader_click = ?, image_fit = ?, img_server = ? WHERE user_id = ? LIMIT 1
|
UPDATE mangadex_users SET reader = ?, swipe_direction = ?, swipe_sensitivity = ?, reader_mode = ?, reader_click = ?, image_fit = ?, img_server = ? WHERE user_id = ? LIMIT 1
|
||||||
', [$reader, $swipe_direction, $swipe_sensitivity, $reader_mode, $reader_click, $image_fit, $img_server, $user->user_id]);
|
', [$reader, $swipe_direction, $swipe_sensitivity, $reader_mode, $reader_click, $image_fit, $img_server, $user->user_id]);
|
||||||
|
$sql->modify('reader_settings', ' UPDATE mangadex_user_options SET data_saver = ? WHERE user_id = ? LIMIT 1 ', [(int) $data_saver, $user->user_id]);
|
||||||
|
|
||||||
$memcached->delete("user_$user->user_id");
|
$memcached->delete("user_$user->user_id");
|
||||||
}
|
}
|
||||||
@ -328,6 +330,7 @@ switch ($function) {
|
|||||||
$website = str_replace(['javascript:'], '', htmlentities($_POST['website']));
|
$website = str_replace(['javascript:'], '', htmlentities($_POST['website']));
|
||||||
$user_bio = str_replace(['javascript:'], '', htmlentities($_POST['user_bio']));
|
$user_bio = str_replace(['javascript:'], '', htmlentities($_POST['user_bio']));
|
||||||
$old_file = $_FILES['file']['name'];
|
$old_file = $_FILES['file']['name'];
|
||||||
|
$email = $_POST['email'];
|
||||||
|
|
||||||
// Make sure website has http://
|
// Make sure website has http://
|
||||||
if (!empty($website) && stripos($website, 'http://') === false && stripos($website, 'https://') === false)
|
if (!empty($website) && stripos($website, 'http://') === false && stripos($website, 'https://') === false)
|
||||||
@ -351,6 +354,21 @@ switch ($function) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($email != $user->email){
|
||||||
|
// check for another account with this email
|
||||||
|
$count_email = $sql->prep('count_email', ' SELECT count(*) FROM mangadex_users WHERE email = ? ', [$email], 'fetchColumn', '', -1);
|
||||||
|
|
||||||
|
//check for banned hosts
|
||||||
|
$banned_hosts = $sql->query_read('tempmail', "SELECT host FROM mangadex_tempmail ORDER BY host ASC ", 'fetchAll', PDO::FETCH_COLUMN);
|
||||||
|
$email_parts = explode('@', $email);
|
||||||
|
$banned_email = in_array($email_parts[1], $banned_hosts);
|
||||||
|
|
||||||
|
if($count_email || $banned_email){
|
||||||
|
$fail_reason = "This email cannot be used.";
|
||||||
|
$error .= display_alert("danger", "Failed", $fail_reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!$user->user_id)
|
if (!$user->user_id)
|
||||||
$error .= display_alert('danger', 'Failed', 'Your session has timed out. Please log in again.'); //success
|
$error .= display_alert('danger', 'Failed', 'Your session has timed out. Please log in again.'); //success
|
||||||
|
|
||||||
@ -358,7 +376,7 @@ switch ($function) {
|
|||||||
$error .= display_alert('danger', 'Failed', 'You need to be at least a member.'); //success
|
$error .= display_alert('danger', 'Failed', 'You need to be at least a member.'); //success
|
||||||
|
|
||||||
if (!$error) {
|
if (!$error) {
|
||||||
$sql->modify('change_profile', ' UPDATE mangadex_users SET language = ?, user_website = ?, user_bio = ? WHERE user_id = ? LIMIT 1 ', [$lang_id, $website, $user_bio, $user->user_id]);
|
$sql->modify('change_profile', ' UPDATE mangadex_users SET language = ?, user_website = ?, user_bio = ?, email = ? WHERE user_id = ? LIMIT 1 ', [$lang_id, $website, $user_bio, $email, $user->user_id]);
|
||||||
|
|
||||||
if ($old_file) {
|
if ($old_file) {
|
||||||
$arr = explode('.', $_FILES['file']['name']);
|
$arr = explode('.', $_FILES['file']['name']);
|
||||||
@ -399,8 +417,9 @@ switch ($function) {
|
|||||||
$theme_id = prepare_numeric($_POST['theme_id']);
|
$theme_id = prepare_numeric($_POST['theme_id']);
|
||||||
$navigation = prepare_numeric($_POST['navigation']);
|
$navigation = prepare_numeric($_POST['navigation']);
|
||||||
$list_privacy = prepare_numeric($_POST['list_privacy']);
|
$list_privacy = prepare_numeric($_POST['list_privacy']);
|
||||||
|
$dm_privacy = prepare_numeric($_POST['dm_privacy']);
|
||||||
$reader = $_POST['reader'] ?? 0;
|
$reader = $_POST['reader'] ?? 0;
|
||||||
$data_saver = $_POST['data_saver'] ?? 0;
|
$port_limit = prepare_numeric($_POST['mdh_portlimit'] ?? 0);
|
||||||
$display_lang_id = prepare_numeric($_POST['display_lang_id']);
|
$display_lang_id = prepare_numeric($_POST['display_lang_id']);
|
||||||
$old_file = $_FILES['file']['name'];
|
$old_file = $_FILES['file']['name'];
|
||||||
$hentai_mode = prepare_numeric($_POST["hentai_mode"]);
|
$hentai_mode = prepare_numeric($_POST["hentai_mode"]);
|
||||||
@ -423,10 +442,10 @@ switch ($function) {
|
|||||||
|
|
||||||
if (!$error) {
|
if (!$error) {
|
||||||
$sql->modify('site_settings', '
|
$sql->modify('site_settings', '
|
||||||
UPDATE mangadex_users SET hentai_mode = ?, display_moderated = ?, latest_updates = ?, reader = ?, default_lang_ids = ?, style = ?, display_lang_id = ?, list_privacy = ?, excluded_genres = ?, navigation = ?, show_unavailable = ? WHERE user_id = ? LIMIT 1
|
UPDATE mangadex_users SET hentai_mode = ?, display_moderated = ?, latest_updates = ?, reader = ?, default_lang_ids = ?, style = ?, display_lang_id = ?, list_privacy = ?, excluded_genres = ?, navigation = ?, dm_privacy = ?, show_unavailable = ? WHERE user_id = ? LIMIT 1
|
||||||
', [$hentai_mode, $display_moderated, $latest_updates, (int) $reader, $default_lang_ids, $theme_id, $display_lang_id, $list_privacy, implode(',', $excluded_genres), $navigation, $show_unavailable, $user->user_id]);
|
', [$hentai_mode, $display_moderated, $latest_updates, (int) $reader, $default_lang_ids, $theme_id, $display_lang_id, $list_privacy, implode(',', $excluded_genres), $navigation, $dm_privacy, $show_unavailable, $user->user_id]);
|
||||||
|
|
||||||
$sql->modify('site_settings', ' UPDATE mangadex_user_options SET data_saver = ? WHERE user_id = ? LIMIT 1 ', [(int) $data_saver, $user->user_id]);
|
$sql->modify('site_settings', ' UPDATE mangadex_user_options SET mdh_portlimit = ? WHERE user_id = ? LIMIT 1 ', [$port_limit, $user->user_id]);
|
||||||
|
|
||||||
if ($old_file && !$reset_list_banner) {
|
if ($old_file && !$reset_list_banner) {
|
||||||
$arr = explode(".", $_FILES["file"]["name"]);
|
$arr = explode(".", $_FILES["file"]["name"]);
|
||||||
|
@ -280,36 +280,18 @@ switch ($type) {
|
|||||||
$page_array = array_combine(range(1, count($arr)), array_values($arr));
|
$page_array = array_combine(range(1, count($arr)), array_values($arr));
|
||||||
}
|
}
|
||||||
|
|
||||||
$server_fallback = LOCAL_SERVER_URL;
|
$server_fallback = IMG_SERVER_URL;
|
||||||
$server_network = null;
|
$server_network = null;
|
||||||
|
|
||||||
// when a chapter does not exist on the local webserver, it gets an id. since all imageservers share the same data, we can assign any imageserver
|
// use md@h for all images
|
||||||
// with the best location to the user.
|
|
||||||
if ($chapter->server > 0) {
|
|
||||||
if (isset($user->md_at_home) && $user->md_at_home && stripos($chapter->page_order, 'http') === false) {
|
|
||||||
try {
|
try {
|
||||||
$subsubdomain = $mdAtHomeClient->getServerUrl($chapter->chapter_hash, explode(',', $chapter->page_order), _IP);
|
$subsubdomain = $mdAtHomeClient->getServerUrl($chapter->chapter_hash, explode(',', $chapter->page_order), _IP, $user->mdh_portlimit ?? false);
|
||||||
if (!empty($subsubdomain)) {
|
if (!empty($subsubdomain)) {
|
||||||
$server_network = $subsubdomain;
|
$server_network = $subsubdomain;
|
||||||
}
|
}
|
||||||
} catch (Throwable $t) {
|
} catch (\Throwable $t) {
|
||||||
trigger_error($t->getMessage(), E_USER_WARNING);
|
trigger_error($t->getMessage(), E_USER_WARNING);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
$server_id = -1;
|
|
||||||
// If a usersetting overwrites it, take this
|
|
||||||
if (isset($_GET['server'])) {
|
|
||||||
// if the parameter was trash, this returns -1
|
|
||||||
$server_id = get_server_id_by_code($_GET['server']);
|
|
||||||
}
|
|
||||||
if ($server_id < 1) {
|
|
||||||
// Try to select a region based server if we havent set one already
|
|
||||||
$server_id = get_server_id_by_geography();
|
|
||||||
}
|
|
||||||
if ($server_id > 0) {
|
|
||||||
$server_fallback = "https://s$server_id.mangadex.org";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$server = $server_network ?: $server_fallback;
|
$server = $server_network ?: $server_fallback;
|
||||||
|
|
||||||
@ -341,11 +323,16 @@ switch ($type) {
|
|||||||
if (!empty($server_network)) {
|
if (!empty($server_network)) {
|
||||||
$array['server_fallback'] = $server_fallback.$data_dir;
|
$array['server_fallback'] = $server_fallback.$data_dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$isRestricted = in_array($chapter->manga_id, RESTRICTED_MANGA_IDS) && !validate_level($user, 'contributor') && $user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA;
|
||||||
|
$countryCode = strtoupper(get_country_code($user->last_ip));
|
||||||
|
$isRegionBlocked = isset(REGION_BLOCKED_MANGA[$countryCode]) && in_array($manga->manga_id, REGION_BLOCKED_MANGA[$countryCode]) && !validate_level($user, 'pr');
|
||||||
|
|
||||||
if ($status === 'external') {
|
if ($status === 'external') {
|
||||||
$array['external'] = $chapter->page_order;
|
$array['external'] = $chapter->page_order;
|
||||||
}
|
}
|
||||||
|
|
||||||
elseif (in_array($chapter->manga_id, RESTRICTED_MANGA_IDS) && !validate_level($user, 'contributor') && $user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA) {
|
elseif ($isRestricted || $isRegionBlocked) {
|
||||||
$array = [
|
$array = [
|
||||||
'id' => $chapter->chapter_id,
|
'id' => $chapter->chapter_id,
|
||||||
'status' => 'restricted',
|
'status' => 'restricted',
|
||||||
|
@ -11,6 +11,7 @@ use Mangadex\Controller\API\MangaController;
|
|||||||
use Mangadex\Controller\API\RelationTypeController;
|
use Mangadex\Controller\API\RelationTypeController;
|
||||||
use Mangadex\Controller\API\TagController;
|
use Mangadex\Controller\API\TagController;
|
||||||
use Mangadex\Controller\API\UserController;
|
use Mangadex\Controller\API\UserController;
|
||||||
|
use Mangadex\Controller\API\HighestChapterIDController;
|
||||||
use Mangadex\Exception\Http\HttpException;
|
use Mangadex\Exception\Http\HttpException;
|
||||||
use Mangadex\Exception\Http\NotFoundHttpException;
|
use Mangadex\Exception\Http\NotFoundHttpException;
|
||||||
use Mangadex\Exception\Http\TooManyRequestsHttpException;
|
use Mangadex\Exception\Http\TooManyRequestsHttpException;
|
||||||
@ -67,6 +68,9 @@ try {
|
|||||||
case 'index':
|
case 'index':
|
||||||
$controller = new IndexController();
|
$controller = new IndexController();
|
||||||
break;
|
break;
|
||||||
|
case 'highest_chapter_id':
|
||||||
|
$controller = new HighestChapterIDController();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new NotFoundHttpException("Invalid endpoint");
|
throw new NotFoundHttpException("Invalid endpoint");
|
||||||
break;
|
break;
|
||||||
|
@ -8,6 +8,7 @@ define('DB_USER', 'mangadex');
|
|||||||
define('DB_PASSWORD', '');
|
define('DB_PASSWORD', '');
|
||||||
define('DB_NAME', 'mangadex');
|
define('DB_NAME', 'mangadex');
|
||||||
define('DB_HOST', 'localhost');
|
define('DB_HOST', 'localhost');
|
||||||
|
define('DB_PERSISTENT', false);
|
||||||
|
|
||||||
define('DB_READ_HOSTS', ['127.0.0.1']);
|
define('DB_READ_HOSTS', ['127.0.0.1']);
|
||||||
define('DB_READ_NAME', DB_NAME);
|
define('DB_READ_NAME', DB_NAME);
|
||||||
@ -29,6 +30,8 @@ define('URL', 'https://mangadex.org/');
|
|||||||
define('TITLE', 'MangaDex');
|
define('TITLE', 'MangaDex');
|
||||||
define('DESCRIPTION', 'Read manga online for free at MangaDex with no ads, high quality images and support scanlation groups!');
|
define('DESCRIPTION', 'Read manga online for free at MangaDex with no ads, high quality images and support scanlation groups!');
|
||||||
define('MEMCACHED_HOST', '127.0.0.1');
|
define('MEMCACHED_HOST', '127.0.0.1');
|
||||||
|
define('MEMCACHED_SYNC_HOST', null);
|
||||||
|
define('MEMCACHED_SYNC_PORT', null);
|
||||||
|
|
||||||
define('GOOGLE_CAPTCHA_SITEKEY', 'xxx');
|
define('GOOGLE_CAPTCHA_SITEKEY', 'xxx');
|
||||||
define('GOOGLE_CAPTCHA_SECRET', 'xxx');
|
define('GOOGLE_CAPTCHA_SECRET', 'xxx');
|
||||||
@ -60,6 +63,7 @@ define('GOOGLE_SERVICE_ACCOUNT_PATH', '/var/www/google_service_credentials.json'
|
|||||||
define('MAX_CHAPTER_FILESIZE', 104857600); //100*1024*1024
|
define('MAX_CHAPTER_FILESIZE', 104857600); //100*1024*1024
|
||||||
|
|
||||||
define('DMS_DISPLAY_LIMIT', 25);
|
define('DMS_DISPLAY_LIMIT', 25);
|
||||||
|
define('PRIVATE_API_KEY', sha1('secretpass_changeme'));
|
||||||
|
|
||||||
define('REQUIRE_LOGIN_PAGES', ['users', 'follows', 'followed_manga', 'followed_groups', 'follows_import', 'upload', 'settings', 'messages', 'message', 'send_message', 'activation', 'admin', 'mod', 'group_new', 'manga_new', 'stats', 'social']);
|
define('REQUIRE_LOGIN_PAGES', ['users', 'follows', 'followed_manga', 'followed_groups', 'follows_import', 'upload', 'settings', 'messages', 'message', 'send_message', 'activation', 'admin', 'mod', 'group_new', 'manga_new', 'stats', 'social']);
|
||||||
|
|
||||||
@ -90,8 +94,9 @@ define('MAX_IMAGE_FILESIZE', 1048576);
|
|||||||
define('ALLOWED_IMG_EXT', ['jpg', 'jpeg', 'png', 'gif']);
|
define('ALLOWED_IMG_EXT', ['jpg', 'jpeg', 'png', 'gif']);
|
||||||
define('ALLOWED_MIME_TYPES', ['image/png', 'image/jpeg', 'image/gif']);
|
define('ALLOWED_MIME_TYPES', ['image/png', 'image/jpeg', 'image/gif']);
|
||||||
define('IMAGE_SERVER', 0);
|
define('IMAGE_SERVER', 0);
|
||||||
define('IMG_SERVER_URL', 'https://s1.mangadex.org');
|
define('IMG_SERVER_URL', 'https://s2.mangadex.org');
|
||||||
define('LOCAL_SERVER_URL', 'https://cdndex.com/data/');
|
define('LOCAL_SERVER_URL', 'https://cdndex.com/data/');
|
||||||
|
define('API_V2_URL', 'https://api.mangadex.org/v2/');
|
||||||
|
|
||||||
//$server_array = ['eu2' => 1, 'na' => 2, 'eu' => 3, 'na2' => 4, 'na3' => 5];
|
//$server_array = ['eu2' => 1, 'na' => 2, 'eu' => 3, 'na2' => 4, 'na3' => 5];
|
||||||
define('IMAGE_SERVER_INFO', [
|
define('IMAGE_SERVER_INFO', [
|
||||||
|
@ -9,6 +9,9 @@ require_once (__DIR__.'/../bootstrap.php');
|
|||||||
|
|
||||||
require_once (ABSPATH . "/scripts/header.req.php");
|
require_once (ABSPATH . "/scripts/header.req.php");
|
||||||
|
|
||||||
|
echo "START @ ".date("F j, Y, g:i a")."\n";
|
||||||
|
|
||||||
|
echo "prune remote files ...\n";
|
||||||
// prune remote file upload tmpfiles
|
// prune remote file upload tmpfiles
|
||||||
$dirh = opendir(sys_get_temp_dir());
|
$dirh = opendir(sys_get_temp_dir());
|
||||||
$nameFormat = 'remote_file_dl_';
|
$nameFormat = 'remote_file_dl_';
|
||||||
@ -23,13 +26,14 @@ while (false !== ($entry = readdir($dirh))) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//updated featured
|
//updated featured
|
||||||
|
echo "featured ...\n";
|
||||||
$memcached->delete('featured');
|
$memcached->delete('featured');
|
||||||
$manga_lists = new Manga_Lists();
|
$manga_lists = new Manga_Lists();
|
||||||
$array_of_featured_manga_ids = $manga_lists->get_manga_list(11);
|
$array_of_featured_manga_ids = $manga_lists->get_manga_list(12);
|
||||||
if (!empty($array_of_featured_manga_ids)) {
|
if (!empty($array_of_featured_manga_ids)) {
|
||||||
$manga_ids_in = prepare_in($array_of_featured_manga_ids);
|
$manga_ids_in = prepare_in($array_of_featured_manga_ids);
|
||||||
$featured = $sql->prep('featured', "
|
$featured = $sql->prep('featured', "
|
||||||
SELECT chapters.manga_id, chapters.chapter_id, chapters.chapter_views, chapters.chapter, chapters.upload_timestamp,
|
SELECT /*+ MAX_EXECUTION_TIME(1800000) */ chapters.manga_id, chapters.chapter_id, chapters.chapter_views, chapters.chapter, chapters.upload_timestamp,
|
||||||
mangas.manga_name, mangas.manga_image, mangas.manga_hentai, mangas.manga_bayesian,
|
mangas.manga_name, mangas.manga_image, mangas.manga_hentai, mangas.manga_bayesian,
|
||||||
(SELECT count(*) FROM mangadex_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows
|
(SELECT count(*) FROM mangadex_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows
|
||||||
FROM mangadex_chapters AS chapters
|
FROM mangadex_chapters AS chapters
|
||||||
@ -40,51 +44,55 @@ if (!empty($array_of_featured_manga_ids)) {
|
|||||||
AND mangas.manga_id IN ($manga_ids_in)
|
AND mangas.manga_id IN ($manga_ids_in)
|
||||||
GROUP BY chapters.manga_id
|
GROUP BY chapters.manga_id
|
||||||
ORDER BY chapters.chapter_views DESC
|
ORDER BY chapters.chapter_views DESC
|
||||||
", $array_of_featured_manga_ids , 'fetchAll', PDO::FETCH_ASSOC, 3600);
|
", $array_of_featured_manga_ids , 'fetchAll', PDO::FETCH_ASSOC, 3600, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
//update new manga
|
//update new manga
|
||||||
|
echo "new_manga ...\n";
|
||||||
$memcached->delete('new_manga');
|
$memcached->delete('new_manga');
|
||||||
$new_manga = $sql->query_read('new_manga', "
|
$new_manga = $sql->prep('new_manga', "
|
||||||
SELECT mangas.manga_id, mangas.manga_name, mangas.manga_image, mangas.manga_hentai, chapters.chapter_id, chapters.chapter_views, chapters.chapter, chapters.upload_timestamp
|
SELECT /*+ MAX_EXECUTION_TIME(1800000) */ mangas.manga_id, mangas.manga_name, mangas.manga_image, mangas.manga_hentai, chapters.chapter_id, chapters.chapter_views, chapters.chapter, chapters.upload_timestamp
|
||||||
FROM mangadex_mangas AS mangas
|
FROM mangadex_mangas AS mangas
|
||||||
LEFT JOIN mangadex_chapters AS chapters
|
LEFT JOIN mangadex_chapters AS chapters
|
||||||
ON mangas.manga_id = chapters.manga_id
|
ON mangas.manga_id = chapters.manga_id
|
||||||
WHERE mangas.manga_hentai = 0 AND chapters.chapter_id IS NOT NULL
|
WHERE mangas.manga_hentai = 0 AND chapters.chapter_id IS NOT NULL
|
||||||
GROUP BY mangas.manga_id
|
GROUP BY mangas.manga_id
|
||||||
ORDER BY mangas.manga_id DESC LIMIT 10
|
ORDER BY mangas.manga_id DESC LIMIT 10
|
||||||
", 'fetchAll', PDO::FETCH_ASSOC, 3600);
|
", [], 'fetchAll', PDO::FETCH_ASSOC, 3600, true);
|
||||||
|
|
||||||
//update top follows
|
//update top follows
|
||||||
|
echo "top_follows ...\n";
|
||||||
$memcached->delete('top_follows');
|
$memcached->delete('top_follows');
|
||||||
$top_follows = $sql->query_read('top_follows', "
|
$top_follows = $sql->prep('top_follows', "
|
||||||
SELECT mangas.manga_id, mangas.manga_image, mangas.manga_name, mangas.manga_hentai, mangas.manga_bayesian,
|
SELECT /*+ MAX_EXECUTION_TIME(1800000) */ mangas.manga_id, mangas.manga_image, mangas.manga_name, mangas.manga_hentai, mangas.manga_bayesian,
|
||||||
(SELECT count(*) FROM mangadex_manga_ratings WHERE mangadex_manga_ratings.manga_id = mangas.manga_id) AS count_pop,
|
(SELECT count(*) FROM mangadex_manga_ratings WHERE mangadex_manga_ratings.manga_id = mangas.manga_id) AS count_pop,
|
||||||
(SELECT count(*) FROM mangadex_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows
|
(SELECT count(*) FROM mangadex_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows
|
||||||
FROM mangadex_mangas AS mangas
|
FROM mangadex_mangas AS mangas
|
||||||
WHERE mangas.manga_hentai = 0
|
WHERE mangas.manga_hentai = 0
|
||||||
ORDER BY count_follows DESC LIMIT 10
|
ORDER BY count_follows DESC LIMIT 10
|
||||||
", 'fetchAll', PDO::FETCH_ASSOC, 3600);
|
", [], 'fetchAll', PDO::FETCH_ASSOC, 3600, true);
|
||||||
|
|
||||||
//update top rating
|
//update top rating
|
||||||
|
echo "top_rating ...\n";
|
||||||
$memcached->delete('top_rating');
|
$memcached->delete('top_rating');
|
||||||
$top_rating = $sql->query_read('top_rating', "
|
$top_rating = $sql->prep('top_rating', "
|
||||||
SELECT mangas.manga_id, mangas.manga_image, mangas.manga_name, mangas.manga_hentai, mangas.manga_bayesian,
|
SELECT /*+ MAX_EXECUTION_TIME(1800000) */ mangas.manga_id, mangas.manga_image, mangas.manga_name, mangas.manga_hentai, mangas.manga_bayesian,
|
||||||
(SELECT count(*) FROM mangadex_manga_ratings WHERE mangadex_manga_ratings.manga_id = mangas.manga_id) AS count_pop,
|
(SELECT count(*) FROM mangadex_manga_ratings WHERE mangadex_manga_ratings.manga_id = mangas.manga_id) AS count_pop,
|
||||||
(SELECT count(*) FROM mangadex_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows
|
(SELECT count(*) FROM mangadex_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows
|
||||||
FROM mangadex_mangas AS mangas
|
FROM mangadex_mangas AS mangas
|
||||||
WHERE mangas.manga_hentai = 0
|
WHERE mangas.manga_hentai = 0
|
||||||
ORDER BY manga_bayesian DESC LIMIT 10
|
ORDER BY manga_bayesian DESC LIMIT 10
|
||||||
", 'fetchAll', PDO::FETCH_ASSOC, 3600);
|
", [], 'fetchAll', PDO::FETCH_ASSOC, 3600, true);
|
||||||
|
|
||||||
//process logs
|
//process logs
|
||||||
$last_timestamp = $sql->query_read('last_timestamp', " SELECT visit_timestamp FROM mangadex_logs_visits ORDER BY visit_timestamp ASC LIMIT 1 ", 'fetchColumn', '', -1) + 3600;
|
echo "last_timestamp ...\n";
|
||||||
|
$last_timestamp = $sql->prep('last_timestamp', " SELECT /*+ MAX_EXECUTION_TIME(1800000) */ visit_timestamp FROM mangadex_logs_visits ORDER BY visit_timestamp ASC LIMIT 1 ", [], 'fetchColumn', '', -1, true) + 3600;
|
||||||
for($i = $last_timestamp; $i < ($last_timestamp + 3600); $i+=3600) {
|
for($i = $last_timestamp; $i < ($last_timestamp + 3600); $i+=3600) {
|
||||||
$views_guests = $sql->query_read('views_guests', " SELECT count(*) FROM mangadex_logs_visits WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id = 0 ", 'fetchColumn', '', -1);
|
$views_guests = $sql->prep('views_guests', " SELECT count(*) FROM mangadex_logs_visits WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id = 0 ", [], 'fetchColumn', '', -1, true);
|
||||||
$views_logged_in = $sql->query_read('views_logged_in', " SELECT count(*) FROM mangadex_logs_visits WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id > 0 ", 'fetchColumn', '', -1);
|
$views_logged_in = $sql->prep('views_logged_in', " SELECT count(*) FROM mangadex_logs_visits WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id > 0 ", [], 'fetchColumn', '', -1, true);
|
||||||
|
|
||||||
$users_guests = $sql->query_read('users_guests', " SELECT COUNT(*) FROM (SELECT `visit_user_id` FROM `mangadex_logs_visits` WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id = 0 GROUP BY `visit_ip`) AS `TABLE` ", 'fetchColumn', '', -1);
|
$users_guests = $sql->prep('users_guests', " SELECT COUNT(*) FROM (SELECT `visit_user_id` FROM `mangadex_logs_visits` WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id = 0 GROUP BY `visit_ip`) AS `TABLE` ", [], 'fetchColumn', '', -1, true);
|
||||||
$users_logged_in = $sql->query_read('users_logged_in', " SELECT COUNT(*) FROM (SELECT `visit_user_id` FROM `mangadex_logs_visits` WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id > 0 GROUP BY `visit_user_id`) AS `TABLE` ", 'fetchColumn', '', -1);
|
$users_logged_in = $sql->prep('users_logged_in', " SELECT COUNT(*) FROM (SELECT `visit_user_id` FROM `mangadex_logs_visits` WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id > 0 GROUP BY `visit_user_id`) AS `TABLE` ", [], 'fetchColumn', '', -1, true);
|
||||||
|
|
||||||
$sql->modify('insert', ' INSERT INTO `mangadex_logs_visits_summary` (`id`, `timestamp`, `users_guests`, `users_logged_in`, `views_guests`, `views_logged_in`) VALUES (NULL, ?, ?, ?, ?, ?) ', [$i, $users_guests, $users_logged_in, $views_guests, $views_logged_in]);
|
$sql->modify('insert', ' INSERT INTO `mangadex_logs_visits_summary` (`id`, `timestamp`, `users_guests`, `users_logged_in`, `views_guests`, `views_logged_in`) VALUES (NULL, ?, ?, ?, ?, ?) ', [$i, $users_guests, $users_logged_in, $views_guests, $views_logged_in]);
|
||||||
$sql->modify('delete', ' DELETE FROM `mangadex_logs_visits` WHERE visit_timestamp >= (? - 3600) AND visit_timestamp < ? ', [$i, $i]);
|
$sql->modify('delete', ' DELETE FROM `mangadex_logs_visits` WHERE visit_timestamp >= (? - 3600) AND visit_timestamp < ? ', [$i, $i]);
|
||||||
@ -92,16 +100,22 @@ for($i = $last_timestamp; $i < ($last_timestamp + 3600); $i+=3600) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prune old chapter_history data
|
// Prune old chapter_history data
|
||||||
|
echo "prune_manga_history ...\n";
|
||||||
$cutoff = time() - (60 * 60 * 24 * 90); // 90 days
|
$cutoff = time() - (60 * 60 * 24 * 90); // 90 days
|
||||||
$sql->modify('prune_manga_history', 'DELETE FROM mangadex_manga_history WHERE `timestamp` < ?', [$cutoff]);
|
$sql->modify('prune_manga_history', 'DELETE FROM mangadex_manga_history WHERE `timestamp` < ?', [$cutoff]);
|
||||||
|
|
||||||
// Prune expired ip bans
|
// Prune expired ip bans
|
||||||
|
echo "prune_ip_bans ...\n";
|
||||||
$sql->modify('prune_ip_bans', 'DELETE FROM mangadex_ip_bans WHERE expires < UNIX_TIMESTAMP()', []);
|
$sql->modify('prune_ip_bans', 'DELETE FROM mangadex_ip_bans WHERE expires < UNIX_TIMESTAMP()', []);
|
||||||
|
|
||||||
// Prune expired sessions each month on the 1st
|
// Prune expired sessions each month on the 1st
|
||||||
if (date('j') == 1 && date('G') < 1) {
|
if (date('j') == 1 && date('G') < 1) {
|
||||||
|
echo "prune_sessions ...\n";
|
||||||
$sql->modify('prune_sessions', 'DELETE FROM mangadex_sessions WHERE (created + ?) < UNIX_TIMESTAMP()', [60*60*24*365]);
|
$sql->modify('prune_sessions', 'DELETE FROM mangadex_sessions WHERE (created + ?) < UNIX_TIMESTAMP()', [60*60*24*365]);
|
||||||
}
|
}
|
||||||
|
|
||||||
//prune old chapter_live_views for trending data
|
//prune old chapter_live_views for trending data
|
||||||
|
echo "prune_trending ...\n";
|
||||||
$sql->modify('prune_trending', 'DELETE FROM `mangadex_chapter_live_views` WHERE (timestamp + ?) < UNIX_TIMESTAMP()', [60*60*25]);
|
$sql->modify('prune_trending', 'DELETE FROM `mangadex_chapter_live_views` WHERE (timestamp + ?) < UNIX_TIMESTAMP()', [60*60*25]);
|
||||||
|
|
||||||
|
echo "END @ ".date("F j, Y, g:i a")."\n";
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
if (PHP_SAPI !== 'cli')
|
if (PHP_SAPI !== 'cli') {
|
||||||
die();
|
die();
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "START @ ".date("F j, Y, g:i a")."\n";
|
||||||
|
|
||||||
require_once (__DIR__.'/../bootstrap.php');
|
require_once (__DIR__.'/../bootstrap.php');
|
||||||
|
|
||||||
require_once (ABSPATH . "/scripts/header.req.php");
|
require_once (ABSPATH . "/scripts/header.req.php");
|
||||||
|
|
||||||
|
echo "latest_manga_comments ...\n";
|
||||||
$memcached->delete("latest_manga_comments");
|
$memcached->delete("latest_manga_comments");
|
||||||
$latest_manga_comments = $sql->query_read('latest_manga_comments', "
|
$latest_manga_comments = $sql->prep('latest_manga_comments', "
|
||||||
SELECT posts.post_id, posts.text, posts.timestamp, posts.thread_id, mangas.manga_name, mangas.manga_id,
|
SELECT /*+ MAX_EXECUTION_TIME(600000) */ posts.post_id, posts.text, posts.timestamp, posts.thread_id, mangas.manga_name, mangas.manga_id,
|
||||||
(SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts
|
(SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts
|
||||||
WHERE mangadex_forum_posts.post_id <= posts.post_id
|
WHERE mangadex_forum_posts.post_id <= posts.post_id
|
||||||
AND mangadex_forum_posts.thread_id = posts.thread_id
|
AND mangadex_forum_posts.thread_id = posts.thread_id
|
||||||
@ -21,11 +25,12 @@ $latest_manga_comments = $sql->query_read('latest_manga_comments', "
|
|||||||
ON threads.thread_name = mangas.manga_id
|
ON threads.thread_name = mangas.manga_id
|
||||||
WHERE threads.forum_id = 11 AND threads.thread_deleted = 0
|
WHERE threads.forum_id = 11 AND threads.thread_deleted = 0
|
||||||
ORDER BY timestamp DESC LIMIT 10
|
ORDER BY timestamp DESC LIMIT 10
|
||||||
", 'fetchAll', PDO::FETCH_ASSOC, 600);
|
", [], 'fetchAll', PDO::FETCH_ASSOC, 600, true);
|
||||||
|
|
||||||
|
echo "latest_forum_posts ...\n";
|
||||||
$memcached->delete("latest_forum_posts");
|
$memcached->delete("latest_forum_posts");
|
||||||
$latest_forum_posts = $sql->query_read('latest_forum_posts', "
|
$latest_forum_posts = $sql->prep('latest_forum_posts', "
|
||||||
SELECT posts.post_id, posts.text, posts.timestamp, posts.thread_id, threads.thread_name, forums.forum_name,
|
SELECT /*+ MAX_EXECUTION_TIME(600000) */ posts.post_id, posts.text, posts.timestamp, posts.thread_id, threads.thread_name, forums.forum_name,
|
||||||
(SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts
|
(SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts
|
||||||
WHERE mangadex_forum_posts.post_id <= posts.post_id
|
WHERE mangadex_forum_posts.post_id <= posts.post_id
|
||||||
AND mangadex_forum_posts.thread_id = posts.thread_id
|
AND mangadex_forum_posts.thread_id = posts.thread_id
|
||||||
@ -37,11 +42,12 @@ $latest_forum_posts = $sql->query_read('latest_forum_posts', "
|
|||||||
ON threads.forum_id = forums.forum_id
|
ON threads.forum_id = forums.forum_id
|
||||||
WHERE threads.forum_id NOT IN (11, 12, 14, 17, 18, 20) AND threads.thread_deleted = 0
|
WHERE threads.forum_id NOT IN (11, 12, 14, 17, 18, 20) AND threads.thread_deleted = 0
|
||||||
ORDER BY timestamp DESC LIMIT 10
|
ORDER BY timestamp DESC LIMIT 10
|
||||||
", 'fetchAll', PDO::FETCH_ASSOC, 600);
|
", [], 'fetchAll', PDO::FETCH_ASSOC, 600, true);
|
||||||
|
|
||||||
|
echo "latest_news_posts ...\n";
|
||||||
$memcached->delete("latest_news_posts");
|
$memcached->delete("latest_news_posts");
|
||||||
$latest_forum_posts = $sql->query_read('latest_news_posts', "
|
$latest_forum_posts = $sql->prep('latest_news_posts', "
|
||||||
SELECT posts.post_id, posts.text, posts.timestamp, posts.thread_id, threads.thread_name, forums.forum_name,
|
SELECT /*+ MAX_EXECUTION_TIME(600000) */ posts.post_id, posts.text, posts.timestamp, posts.thread_id, threads.thread_name, forums.forum_name,
|
||||||
(SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts
|
(SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts
|
||||||
WHERE mangadex_forum_posts.post_id <= posts.post_id
|
WHERE mangadex_forum_posts.post_id <= posts.post_id
|
||||||
AND mangadex_forum_posts.thread_id = posts.thread_id
|
AND mangadex_forum_posts.thread_id = posts.thread_id
|
||||||
@ -53,16 +59,17 @@ $latest_forum_posts = $sql->query_read('latest_news_posts', "
|
|||||||
ON threads.forum_id = forums.forum_id
|
ON threads.forum_id = forums.forum_id
|
||||||
WHERE threads.forum_id = 26 AND threads.thread_sticky = 1
|
WHERE threads.forum_id = 26 AND threads.thread_sticky = 1
|
||||||
ORDER BY timestamp ASC LIMIT 1
|
ORDER BY timestamp ASC LIMIT 1
|
||||||
", 'fetchAll', PDO::FETCH_ASSOC, 600);
|
", [], 'fetchAll', PDO::FETCH_ASSOC, 600, true);
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Put delayed chapters that just expired into the last_updated table
|
/// Put delayed chapters that just expired into the last_updated table
|
||||||
///
|
///
|
||||||
|
|
||||||
|
echo "expired_delayed_chapters ... \n";
|
||||||
// Collect all chapters that have been uploaded as delayed, but where the delay is expired
|
// Collect all chapters that have been uploaded as delayed, but where the delay is expired
|
||||||
$expired_delayed_chapters = $sql->query_read('expired_delayed_chapters', '
|
$expired_delayed_chapters = $sql->prep('expired_delayed_chapters', '
|
||||||
SELECT c.chapter_id, c.manga_id, c.volume, c.chapter, c.title, c.upload_timestamp, c.user_id, c.lang_id, c.group_id, c.group_id_2, c.group_id_3, c.available FROM mangadex_chapters c, mangadex_delayed_chapters d WHERE d.upload_timestamp < UNIX_TIMESTAMP() AND c.chapter_id = d.chapter_id
|
SELECT /*+ MAX_EXECUTION_TIME(600000) */ c.chapter_id, c.manga_id, c.volume, c.chapter, c.title, c.upload_timestamp, c.user_id, c.lang_id, c.group_id, c.group_id_2, c.group_id_3, c.available FROM mangadex_chapters c, mangadex_delayed_chapters d WHERE d.upload_timestamp < UNIX_TIMESTAMP() AND c.chapter_id = d.chapter_id
|
||||||
', 'fetchAll', PDO::FETCH_ASSOC, -1);
|
', [], 'fetchAll', PDO::FETCH_ASSOC, -1, true);
|
||||||
// Only process if we found any
|
// Only process if we found any
|
||||||
if (!empty($expired_delayed_chapters)) {
|
if (!empty($expired_delayed_chapters)) {
|
||||||
// Collect all chapter ids in this array, so we can unset them as delayed after this
|
// Collect all chapter ids in this array, so we can unset them as delayed after this
|
||||||
@ -94,6 +101,8 @@ INSERT INTO mangadex_last_updated (`chapter_id`, `manga_id`, `volume`, `chapter`
|
|||||||
$sql->modify('unexpire_delayed_chapters', 'DELETE FROM mangadex_delayed_chapters WHERE chapter_id IN ('.$unexpire_in.')', []);
|
$sql->modify('unexpire_delayed_chapters', 'DELETE FROM mangadex_delayed_chapters WHERE chapter_id IN ('.$unexpire_in.')', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
echo "mdAtHomeClient->getStatus(); ... ";
|
||||||
|
try {
|
||||||
$stats = $mdAtHomeClient->getStatus();
|
$stats = $mdAtHomeClient->getStatus();
|
||||||
|
|
||||||
foreach ($stats as $client) {
|
foreach ($stats as $client) {
|
||||||
@ -113,3 +122,10 @@ foreach ($stats as $client) {
|
|||||||
$bytes_served = (int) $client['bytes_served'];
|
$bytes_served = (int) $client['bytes_served'];
|
||||||
$sql->modify('x', " update mangadex_clients set upload_speed = ?, client_ip = ?, client_subsubdomain = ?, client_port = ?, client_available = ?, shard_count = ?, images_served = ?, images_failed = ?, bytes_served = ?, update_timestamp = UNIX_TIMESTAMP() WHERE client_id = ? AND user_id = ? LIMIT 1 ", [$speed, $client_ip, $subsubdomain, $port, $available, $shard_count, $images_served, $images_failed, $bytes_served, $client_id, $user_id]);
|
$sql->modify('x', " update mangadex_clients set upload_speed = ?, client_ip = ?, client_subsubdomain = ?, client_port = ?, client_available = ?, shard_count = ?, images_served = ?, images_failed = ?, bytes_served = ?, update_timestamp = UNIX_TIMESTAMP() WHERE client_id = ? AND user_id = ? LIMIT 1 ", [$speed, $client_ip, $subsubdomain, $port, $available, $shard_count, $images_served, $images_failed, $bytes_served, $client_id, $user_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
echo "OK\n";
|
||||||
|
} catch (\Throwable $t) {
|
||||||
|
echo "FAIL: ".$t->getMessage()."\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "END @ ".date("F j, Y, g:i a")."\n";
|
||||||
|
BIN
images/affiliates/ddosguard_logo.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
images/affiliates/onramper_logo.png
Normal file
After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 7.6 KiB |
BIN
images/affiliates/path_logo.png
Normal file
After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 628 B |
BIN
images/affiliates/saucenao_logo.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
images/affiliates/sdbx_logo.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
images/affiliates/taoistbanner1.gif
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
images/affiliates/taoistbanner2.gif
Normal file
After Width: | Height: | Size: 251 KiB |
BIN
images/affiliates/taoistbanner3.gif
Normal file
After Width: | Height: | Size: 796 KiB |
BIN
images/agg.jpg
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
images/flags-flat-24-20201130.png
Normal file
After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
1
images/forums/v5-Development.svg
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
images/misc/dj.png
Normal file
After Width: | Height: | Size: 833 B |
BIN
images/rock.png
Normal file
After Width: | Height: | Size: 41 KiB |
4334
package-lock.json
generated
39
package.json
@ -5,33 +5,34 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build": "webpack --config=webpack.config.js --mode=production",
|
"build": "webpack --config=webpack.config.js --mode=production && webpack --config=webpack-reader.config.js --mode=production",
|
||||||
"build-watch": "webpack --config=webpack.config.js --mode=production --watch",
|
"build-watch": "webpack --config=webpack.config.js --mode=production --watch",
|
||||||
"build-dev": "webpack --config=webpack.config.js --mode=development"
|
"build-dev": "webpack --config=webpack.config.js --mode=development",
|
||||||
|
"build-reader": "webpack --config=webpack-reader.config.js --mode=development"
|
||||||
},
|
},
|
||||||
"author": "MangaDex",
|
"author": "MangaDex",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.3.3",
|
"@babel/core": "^7.12.10",
|
||||||
"@babel/plugin-transform-runtime": "^7.2.0",
|
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||||
"@babel/preset-env": "^7.3.1",
|
"@babel/preset-env": "^7.12.11",
|
||||||
"babel-loader": "^8.0.5",
|
"babel-loader": "^8.2.2",
|
||||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
"core-js": "^3.8.2",
|
||||||
"babel-plugin-transform-decorators": "^6.24.1",
|
"webpack": "^4.46.0",
|
||||||
"webpack": "^4.29.5",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-cli": "^3.2.3",
|
"webpack-merge": "^4.2.2"
|
||||||
"webpack-merge": "^4.2.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/polyfill": "^7.2.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@babel/runtime": "^7.3.1",
|
"abortcontroller-polyfill": "^1.7.1",
|
||||||
"date-fns": "^2.0.0-alpha.27",
|
"date-fns": "^2.16.1",
|
||||||
"dotenv-webpack": "^1.7.0",
|
"dotenv-webpack": "^1.8.0",
|
||||||
"eligrey-classlist-js-polyfill": "^1.2.20180112",
|
"eligrey-classlist-js-polyfill": "^1.2.20180112",
|
||||||
"formdata-polyfill": "^3.0.18",
|
"formdata-polyfill": "^3.0.20",
|
||||||
"polyfill-queryselector": "^1.0.2",
|
"js-cookie": "^2.2.1",
|
||||||
"url-polyfill": "^1.1.3",
|
"natsort": "^2.0.2",
|
||||||
"vtt.js": "^0.13.0",
|
"vtt.js": "^0.13.0",
|
||||||
"whatwg-fetch": "^3.0.0"
|
"whatwg-fetch": "^3.5.0",
|
||||||
|
"wolfy87-eventemitter": "^5.2.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
pages/affiliates.req.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$page_html = parse_template('static_pages/affiliates');
|
3
pages/change_activation_email.req.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$page_html = parse_template('user/change_activation_email', ['user' => $user]);
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
$manga_lists = new Manga_Lists();
|
$manga_lists = new Manga_Lists();
|
||||||
$array_of_manga_ids = $manga_lists->get_manga_list(11);
|
$array_of_manga_ids = $manga_lists->get_manga_list(12);
|
||||||
|
|
||||||
$search = [];
|
$search = [];
|
||||||
|
|
||||||
|
@ -441,6 +441,7 @@ else {
|
|||||||
", array_keys($blocked_group_ids), 'fetchAll', PDO::FETCH_ASSOC, 60);
|
", array_keys($blocked_group_ids), 'fetchAll', PDO::FETCH_ASSOC, 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$banners = get_banners();
|
||||||
|
|
||||||
$featured = $memcached->get('featured');
|
$featured = $memcached->get('featured');
|
||||||
|
|
||||||
@ -471,6 +472,7 @@ $templateVars = [
|
|||||||
'latest_news_posts' => $latest_news_posts,
|
'latest_news_posts' => $latest_news_posts,
|
||||||
'featured' => $featured,
|
'featured' => $featured,
|
||||||
'new_manga' => $new_manga,
|
'new_manga' => $new_manga,
|
||||||
|
'banners' => $banners,
|
||||||
];
|
];
|
||||||
|
|
||||||
$page_html = parse_template('home', $templateVars);
|
$page_html = parse_template('home', $templateVars);
|
||||||
|
@ -35,12 +35,17 @@ $manga = new Manga($id);
|
|||||||
|
|
||||||
$relation_types = new Relation_Types(); // This is needed, otherwise it breaks manga.req.js
|
$relation_types = new Relation_Types(); // This is needed, otherwise it breaks manga.req.js
|
||||||
|
|
||||||
|
$countryCode = strtoupper(get_country_code($user->last_ip));
|
||||||
|
|
||||||
if (!isset($manga->manga_id)) {
|
if (!isset($manga->manga_id)) {
|
||||||
$page_html = parse_template('partials/alert', ['type' => 'danger', 'strong' => 'Warning', 'text' => "Manga #$id does not exist."]);
|
$page_html = parse_template('partials/alert', ['type' => 'danger', 'strong' => 'Warning', 'text' => "Manga #$id does not exist."]);
|
||||||
}
|
}
|
||||||
elseif (in_array($manga->manga_id, RESTRICTED_MANGA_IDS) && !validate_level($user, 'contributor') && $user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA) {
|
elseif (in_array($manga->manga_id, RESTRICTED_MANGA_IDS) && !validate_level($user, 'contributor') && $user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA) {
|
||||||
$page_html = parse_template('partials/alert', ['type' => 'danger', 'strong' => 'Warning', 'text' => "Manga #$id is not available. Contact staff on discord for more information."]);
|
$page_html = parse_template('partials/alert', ['type' => 'danger', 'strong' => 'Warning', 'text' => "Manga #$id is not available. Contact staff on discord for more information."]);
|
||||||
}
|
}
|
||||||
|
elseif (isset(REGION_BLOCKED_MANGA[$countryCode]) && in_array($manga->manga_id, REGION_BLOCKED_MANGA[$countryCode]) && !validate_level($user, 'pr')) {
|
||||||
|
$page_html = parse_template('partials/alert', ['type' => 'danger', 'strong' => 'Warning', 'text' => "Manga #$id is not available."]);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
|
|
||||||
update_views_v2($page, $manga->manga_id, $ip);
|
update_views_v2($page, $manga->manga_id, $ip);
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Mangadex\Model\MdexAtHomeClient;
|
||||||
|
|
||||||
$section = $_GET['section'] ?? 'info';
|
$section = $_GET['section'] ?? 'info';
|
||||||
|
|
||||||
$approvaltime = $user->get_client_approval_time();
|
$approvaltime = $user->get_client_approval_time();
|
||||||
@ -40,6 +43,8 @@ switch ($section) {
|
|||||||
'section' => $section,
|
'section' => $section,
|
||||||
'user_clients' => $user->get_clients(),
|
'user_clients' => $user->get_clients(),
|
||||||
'approvaltime' => $approvaltime,
|
'approvaltime' => $approvaltime,
|
||||||
|
'ip' => _IP,
|
||||||
|
'backend' => new MdexAtHomeClient(),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (validate_level($user, 'member')) {
|
if (validate_level($user, 'member')) {
|
||||||
@ -54,6 +59,8 @@ switch ($section) {
|
|||||||
'user' => $user,
|
'user' => $user,
|
||||||
'section' => $section,
|
'section' => $section,
|
||||||
'clients' => $clients,
|
'clients' => $clients,
|
||||||
|
'ip' => _IP,
|
||||||
|
'backend' => new MdexAtHomeClient(),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (validate_level($user, 'admin')) {
|
if (validate_level($user, 'admin')) {
|
||||||
|
@ -34,13 +34,20 @@ switch ($mode) {
|
|||||||
default:
|
default:
|
||||||
$deleted = ($mode == 'bin') ? 1 : 0;
|
$deleted = ($mode == 'bin') ? 1 : 0;
|
||||||
|
|
||||||
|
$current_page = (isset($_GET['p']) && $_GET['p'] > 0) ? $_GET['p'] : 1;
|
||||||
|
$limit = 100;
|
||||||
|
|
||||||
$threads = new PM_Threads($user->user_id, $deleted);
|
$threads = new PM_Threads($user->user_id, $deleted);
|
||||||
$threads_obj = $threads->query_read();
|
$threads_obj = $threads->query_read($current_page, $limit);
|
||||||
|
|
||||||
if ($threads->num_rows < 1) {
|
if ($threads->num_rows < 1) {
|
||||||
$messages_tab_html = parse_template('partials/alert', ['type' => 'info', 'strong' => 'Notice', 'text' => 'You have no messages']);
|
$messages_tab_html = parse_template('partials/alert', ['type' => 'info', 'strong' => 'Notice', 'text' => 'There are no messages.']);
|
||||||
} else {
|
} else {
|
||||||
$templateVars = [
|
$templateVars = [
|
||||||
|
'thread_count' => $threads->num_rows,
|
||||||
|
'current_page' => $current_page,
|
||||||
|
'mode' => $mode,
|
||||||
|
'limit' => $limit,
|
||||||
'threads' => $threads_obj,
|
'threads' => $threads_obj,
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'deleted' => $deleted,
|
'deleted' => $deleted,
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
if (!validate_level($user, 'pr')) die('No access');
|
if (!validate_level($user, 'pr')) die('No access');
|
||||||
|
|
||||||
|
//pages
|
||||||
|
$mode = $_GET['mode'] ?? 'banners';
|
||||||
|
|
||||||
|
$templateVars = [
|
||||||
|
'mode' => $mode,
|
||||||
|
];
|
||||||
|
|
||||||
|
$page_html = parse_template('pr/partials/pr_navtabs', $templateVars);
|
||||||
|
|
||||||
|
switch ($mode) {
|
||||||
|
case 'email_search':
|
||||||
$search = [];
|
$search = [];
|
||||||
|
|
||||||
if (isset($_GET['username']) && !empty($_GET['username']))
|
if (isset($_GET['username']) && !empty($_GET['username']))
|
||||||
@ -27,12 +38,8 @@ else {
|
|||||||
$users_obj = new stdClass();
|
$users_obj = new stdClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
$page_html = "";
|
|
||||||
|
|
||||||
$templateVars = ['search' => $search];
|
$templateVars = ['search' => $search];
|
||||||
|
|
||||||
$page_html .= parse_template('pr/partials/user_list_searchbox', $templateVars);
|
$page_html .= parse_template('pr/partials/user_list_searchbox', $templateVars);
|
||||||
|
|
||||||
$templateVars = [
|
$templateVars = [
|
||||||
'search' => $search,
|
'search' => $search,
|
||||||
'sort' => $sort,
|
'sort' => $sort,
|
||||||
@ -50,3 +57,12 @@ if (!$search['username'] && !$search['email']) {
|
|||||||
} else {
|
} else {
|
||||||
$page_html .= parse_template('user/user_list', $templateVars);
|
$page_html .= parse_template('user/user_list', $templateVars);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "banners":
|
||||||
|
default:
|
||||||
|
$templateVars['banners'] = get_banners(false);
|
||||||
|
|
||||||
|
$page_html .= parse_template('pr/banners', $templateVars);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,28 @@
|
|||||||
<?php
|
<?php
|
||||||
|
$mode = $_GET['mode'] ?? 'home';
|
||||||
|
|
||||||
|
$templateVars = [
|
||||||
|
'mode' => $mode
|
||||||
|
];
|
||||||
|
|
||||||
|
$page_html = parse_template('support/partials/support_navtabs', $templateVars);
|
||||||
|
|
||||||
|
switch ($mode) {
|
||||||
|
case 'home':
|
||||||
|
$page_html .= parse_template('support/home', [
|
||||||
|
'user' => $user
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
case 'donate':
|
||||||
|
$wallet_no = substr($user->user_id, -1);
|
||||||
|
$wallet_no_2 = floor(substr($user->user_id, -1) / 2);
|
||||||
|
|
||||||
|
$page_html .= parse_template('support/donate', [
|
||||||
|
'wallet_no' => $wallet_no,
|
||||||
|
'wallet_no_2' => $wallet_no_2
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
case 'history':
|
||||||
$transactions = $user->get_transactions();
|
$transactions = $user->get_transactions();
|
||||||
|
|
||||||
if ($transactions) {
|
if ($transactions) {
|
||||||
@ -7,11 +31,12 @@ if ($transactions) {
|
|||||||
$memcached->delete("user_{$user->user_id}_transactions");
|
$memcached->delete("user_{$user->user_id}_transactions");
|
||||||
}
|
}
|
||||||
|
|
||||||
$wallet_no = substr($user->user_id, -1);
|
$page_html .= parse_template('support/history', [
|
||||||
$wallet_no_2 = floor(substr($user->user_id, -1) / 2);
|
'user' => $user
|
||||||
|
|
||||||
$page_html = parse_template('user/support', [
|
|
||||||
'user' => $user,
|
|
||||||
'wallet_no' => $wallet_no,
|
|
||||||
'wallet_no_2' => $wallet_no_2,
|
|
||||||
]);
|
]);
|
||||||
|
break;
|
||||||
|
case 'affiliates':
|
||||||
|
$page_html .= parse_template('support/affiliates', $templateVars);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
9
scripts/axios.min.js
vendored
@ -1,6 +1,41 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class Cache extends Memcached
|
class Synced_Memcached extends Memcached
|
||||||
|
{
|
||||||
|
|
||||||
|
private $memcachedSync = null;
|
||||||
|
|
||||||
|
public function __construct($persistent_id = '', $on_new_object_cb = null, $connection_str = '')
|
||||||
|
{
|
||||||
|
parent::__construct($persistent_id, $on_new_object_cb, $connection_str);
|
||||||
|
|
||||||
|
if (defined('MEMCACHED_SYNC_HOST') && !empty(MEMCACHED_SYNC_HOST)) {
|
||||||
|
$this->memcachedSync = new Memcached('sync_host');
|
||||||
|
if (!$this->memcachedSync->getServerList()) {
|
||||||
|
// Persistent servers remember the serverlist, so only add if its empty after a php-fpm restart
|
||||||
|
$this->memcachedSync->addServer(MEMCACHED_SYNC_HOST, defined('MEMCACHED_SYNC_PORT') ? MEMCACHED_SYNC_PORT : 11211);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSynced($key, $value, $expiration = 0, $udf_flags = 0)
|
||||||
|
{
|
||||||
|
parent::set($key, $value, $expiration);
|
||||||
|
if ($this->memcachedSync !== null) {
|
||||||
|
$this->memcachedSync->set($key, $value, $expiration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteSynced($key, $time = 0)
|
||||||
|
{
|
||||||
|
parent::delete($key, $time);
|
||||||
|
if ($this->memcachedSync !== null) {
|
||||||
|
$this->memcachedSync->delete($key, $time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Cache extends Synced_Memcached
|
||||||
{
|
{
|
||||||
|
|
||||||
const RESULT_CODES = [
|
const RESULT_CODES = [
|
||||||
@ -76,7 +111,7 @@ class Cache extends Memcached
|
|||||||
|
|
||||||
public function get($key, $cache_cb = null, $flags = null)
|
public function get($key, $cache_cb = null, $flags = null)
|
||||||
{
|
{
|
||||||
$res = parent::get($key, $cache_cb, $flags); // TODO: Change the autogenerated stub
|
$res = parent::get($key, $cache_cb, $flags);
|
||||||
$this->stats[$res === false ? 'miss' : 'hit']++;
|
$this->stats[$res === false ? 'miss' : 'hit']++;
|
||||||
$this->log[] = [
|
$this->log[] = [
|
||||||
'method' => "GET",
|
'method' => "GET",
|
||||||
@ -88,7 +123,7 @@ class Cache extends Memcached
|
|||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function set($key, $value, $expiration = 0)
|
public function set($key, $value, $expiration = 0, $udf_flags = 0)
|
||||||
{
|
{
|
||||||
parent::set($key, $value, $expiration);
|
parent::set($key, $value, $expiration);
|
||||||
$this->stats['set']++;
|
$this->stats['set']++;
|
||||||
@ -103,8 +138,7 @@ class Cache extends Memcached
|
|||||||
|
|
||||||
public function delete($key, $time = 0)
|
public function delete($key, $time = 0)
|
||||||
{
|
{
|
||||||
|
parent::delete($key, $time);
|
||||||
parent::delete($key, $time); // TODO: Change the autogenerated stub
|
|
||||||
$this->stats['delete']++;
|
$this->stats['delete']++;
|
||||||
$this->log[] = [
|
$this->log[] = [
|
||||||
'method' => "DEL",
|
'method' => "DEL",
|
||||||
|
@ -102,7 +102,7 @@ class Chapters {
|
|||||||
$limit = prepare_numeric($limit);
|
$limit = prepare_numeric($limit);
|
||||||
$offset = prepare_numeric($limit * ($current_page - 1));
|
$offset = prepare_numeric($limit * ($current_page - 1));
|
||||||
|
|
||||||
$results = $this->sql->prep("chapters_query_" . hash_array($this->pdo_bind) . "_orderby_".md5($orderby)."_offset_$offset", "
|
$results = $this->sql->prep("chapters_query_" . hash_array($this->pdo_bind) . "_orderby_" . md5($orderby) . "_offset_$offset" . "_limit_$limit", "
|
||||||
SELECT chapters.*,
|
SELECT chapters.*,
|
||||||
lang.*,
|
lang.*,
|
||||||
users.username,
|
users.username,
|
||||||
|
@ -182,7 +182,7 @@ class User {
|
|||||||
SELECT count(*)
|
SELECT count(*)
|
||||||
FROM mangadex_pm_threads
|
FROM mangadex_pm_threads
|
||||||
WHERE (sender_id = ? AND sender_read = 0) OR (recipient_id = ? AND recipient_read = 0)
|
WHERE (sender_id = ? AND sender_read = 0) OR (recipient_id = ? AND recipient_read = 0)
|
||||||
", [$this->user_id, $this->user_id], 'fetchColumn', '', -1);
|
", [$this->user_id, $this->user_id], 'fetchColumn', '', 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_unread_notifications() {
|
public function get_unread_notifications() {
|
||||||
@ -190,7 +190,7 @@ class User {
|
|||||||
SELECT count(*)
|
SELECT count(*)
|
||||||
FROM mangadex_notifications
|
FROM mangadex_notifications
|
||||||
WHERE mentionee_user_id = ? AND is_read = 0
|
WHERE mentionee_user_id = ? AND is_read = 0
|
||||||
", [$this->user_id], 'fetchColumn', '');
|
", [$this->user_id], 'fetchColumn', '', 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_groups() {
|
public function get_groups() {
|
||||||
@ -237,20 +237,18 @@ class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function get_manga_userdata($manga_id) { //contains progress data, title, and rating
|
public function get_manga_userdata($manga_id) { //contains progress data, title, and rating
|
||||||
return $this->sql->prep("user_{$this->user_id}_manga_{$manga_id}_api", "
|
$follows = $this->get_followed_manga_ids_api();
|
||||||
SELECT m.manga_id, m.manga_name AS title, f.follow_type, f.volume, f.chapter, COALESCE(r.rating, 0) as rating
|
foreach ($follows as $manga) {
|
||||||
FROM mangadex_mangas m
|
if ($manga['manga_id'] == $manga_id) {
|
||||||
LEFT JOIN mangadex_follow_user_manga f
|
return $manga;
|
||||||
ON m.manga_id = f.manga_id AND f.user_id = ?
|
}
|
||||||
LEFT JOIN mangadex_manga_ratings r
|
}
|
||||||
ON m.manga_id = r.manga_id AND r.user_id = ?
|
return null;
|
||||||
WHERE m.manga_id = ?
|
|
||||||
", [$this->user_id, $this->user_id, $manga_id], 'fetchAll', PDO::FETCH_ASSOC);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_followed_manga_ids_api() { //contains progress data, title, and rating for all followed manga
|
public function get_followed_manga_ids_api() { //contains progress data, title, and rating for all followed manga
|
||||||
return $this->sql->prep("user_{$this->user_id}_followed_manga_ids_api", "
|
return $this->sql->prep("user_{$this->user_id}_followed_manga_ids_api", "
|
||||||
SELECT f.manga_id, m.manga_name AS title, f.follow_type, f.volume, f.chapter, COALESCE(r.rating, 0) as rating
|
SELECT f.manga_id, m.manga_name AS title, m.manga_hentai, m.manga_image, f.follow_type, f.volume, f.chapter, COALESCE(r.rating, 0) as rating
|
||||||
FROM mangadex_follow_user_manga f
|
FROM mangadex_follow_user_manga f
|
||||||
JOIN mangadex_mangas m
|
JOIN mangadex_mangas m
|
||||||
ON m.manga_id = f.manga_id
|
ON m.manga_id = f.manga_id
|
||||||
@ -330,7 +328,7 @@ class User {
|
|||||||
return $this->sql->prep("user_{$this->user_id}_friends_user_ids", "
|
return $this->sql->prep("user_{$this->user_id}_friends_user_ids", "
|
||||||
SELECT relations.target_user_id, relations.accepted, user.user_id, user.username, user.last_seen_timestamp, user.list_privacy, user_level.level_colour
|
SELECT relations.target_user_id, relations.accepted, user.user_id, user.username, user.last_seen_timestamp, user.list_privacy, user_level.level_colour
|
||||||
FROM mangadex_user_relations AS relations
|
FROM mangadex_user_relations AS relations
|
||||||
LEFT JOIN mangadex_users AS user
|
JOIN mangadex_users AS user
|
||||||
ON relations.target_user_id = user.user_id
|
ON relations.target_user_id = user.user_id
|
||||||
LEFT JOIN mangadex_user_levels AS user_level
|
LEFT JOIN mangadex_user_levels AS user_level
|
||||||
ON user.level_id = user_level.level_id
|
ON user.level_id = user_level.level_id
|
||||||
@ -343,26 +341,26 @@ class User {
|
|||||||
return $this->sql->prep("user_{$this->user_id}_pending_friends_user_ids", "
|
return $this->sql->prep("user_{$this->user_id}_pending_friends_user_ids", "
|
||||||
SELECT relations.user_id, user.user_id, user.username, user.last_seen_timestamp, user_level.level_colour
|
SELECT relations.user_id, user.user_id, user.username, user.last_seen_timestamp, user_level.level_colour
|
||||||
FROM mangadex_user_relations AS relations
|
FROM mangadex_user_relations AS relations
|
||||||
LEFT JOIN mangadex_users AS user
|
JOIN mangadex_users AS user
|
||||||
ON relations.user_id = user.user_id
|
ON relations.user_id = user.user_id
|
||||||
LEFT JOIN mangadex_user_levels AS user_level
|
LEFT JOIN mangadex_user_levels AS user_level
|
||||||
ON user.level_id = user_level.level_id
|
ON user.level_id = user_level.level_id
|
||||||
WHERE relations.relation_id = 1 AND relations.accepted = 0 AND relations.target_user_id = ?
|
WHERE relations.relation_id = 1 AND relations.accepted = 0 AND relations.target_user_id = ?
|
||||||
ORDER BY user.username ASC
|
ORDER BY user.username ASC
|
||||||
", [$this->user_id], 'fetchAll', PDO::FETCH_UNIQUE);
|
", [$this->user_id], 'fetchAll', PDO::FETCH_UNIQUE, 60*60*24);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_blocked_user_ids() {
|
public function get_blocked_user_ids() {
|
||||||
return $this->sql->prep("user_{$this->user_id}_blocked_user_ids", "
|
return $this->sql->prep("user_{$this->user_id}_blocked_user_ids", "
|
||||||
SELECT relations.target_user_id, user.user_id, user.username, user_level.level_colour
|
SELECT relations.target_user_id, user.user_id, user.username, user_level.level_colour
|
||||||
FROM mangadex_user_relations AS relations
|
FROM mangadex_user_relations AS relations
|
||||||
LEFT JOIN mangadex_users AS user
|
JOIN mangadex_users AS user
|
||||||
ON relations.target_user_id = user.user_id
|
ON relations.target_user_id = user.user_id
|
||||||
LEFT JOIN mangadex_user_levels AS user_level
|
LEFT JOIN mangadex_user_levels AS user_level
|
||||||
ON user.level_id = user_level.level_id
|
ON user.level_id = user_level.level_id
|
||||||
WHERE relations.relation_id = 0 AND relations.user_id = ? AND user.level_id < ?
|
WHERE relations.relation_id = 0 AND relations.user_id = ? AND user.level_id < ?
|
||||||
ORDER BY user.username ASC
|
ORDER BY user.username ASC
|
||||||
", [$this->user_id, 10 /** staff level: PR **/], 'fetchAll', PDO::FETCH_UNIQUE);
|
", [$this->user_id, 10 /** staff level: PR **/], 'fetchAll', PDO::FETCH_UNIQUE, 60*60*24);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_active_restrictions() {
|
public function get_active_restrictions() {
|
||||||
@ -515,15 +513,18 @@ class PM_Threads {
|
|||||||
FROM mangadex_pm_threads
|
FROM mangadex_pm_threads
|
||||||
WHERE (sender_id = ? AND sender_deleted = ?)
|
WHERE (sender_id = ? AND sender_deleted = ?)
|
||||||
OR (recipient_id = ? AND recipient_deleted = ?)
|
OR (recipient_id = ? AND recipient_deleted = ?)
|
||||||
ORDER BY thread_timestamp DESC LIMIT 20
|
|
||||||
", [$user_id, $deleted, $user_id, $deleted], 'fetchColumn', '', -1);
|
", [$user_id, $deleted, $user_id, $deleted], 'fetchColumn', '', -1);
|
||||||
|
|
||||||
$this->user_id = $user_id;
|
$this->user_id = $user_id;
|
||||||
$this->deleted = $deleted;
|
$this->deleted = $deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function query_read() {
|
public function query_read($page = 1, $limit = 100)
|
||||||
$results = $this->sql->prep("user_{$this->user_id}_PMs", "
|
{
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$results = $this->sql->prep(
|
||||||
|
"user_{$this->user_id}_PMs",
|
||||||
|
"
|
||||||
SELECT threads.*,
|
SELECT threads.*,
|
||||||
sender.username AS sender_username,
|
sender.username AS sender_username,
|
||||||
recipient.username AS recipient_username,
|
recipient.username AS recipient_username,
|
||||||
@ -541,8 +542,13 @@ class PM_Threads {
|
|||||||
WHERE (threads.sender_id = ? AND threads.sender_deleted = ?)
|
WHERE (threads.sender_id = ? AND threads.sender_deleted = ?)
|
||||||
OR (threads.recipient_id = ? AND threads.recipient_deleted = ?)
|
OR (threads.recipient_id = ? AND threads.recipient_deleted = ?)
|
||||||
ORDER BY threads.thread_timestamp DESC
|
ORDER BY threads.thread_timestamp DESC
|
||||||
LIMIT 100
|
LIMIT ? OFFSET ?
|
||||||
", [$this->user_id, $this->deleted, $this->user_id, $this->deleted], 'fetchAll', PDO::FETCH_ASSOC, -1);
|
",
|
||||||
|
[$this->user_id, $this->deleted, $this->user_id, $this->deleted, $limit, $offset],
|
||||||
|
'fetchAll',
|
||||||
|
PDO::FETCH_ASSOC,
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
|
||||||
//return get_results_as_object($results, 'thread_id');
|
//return get_results_as_object($results, 'thread_id');
|
||||||
return $results;
|
return $results;
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
.noselect {
|
.noselect {
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-epub-user-select: none;
|
-epub-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
@ -29,11 +28,6 @@
|
|||||||
}
|
}
|
||||||
.nodrag {
|
.nodrag {
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
-epub-user-drag: none;
|
|
||||||
-moz-user-drag: none;
|
|
||||||
-ms-user-drag: none;
|
|
||||||
-o-user-drag: none;
|
|
||||||
user-drag: none;
|
|
||||||
}
|
}
|
||||||
.noevents {
|
.noevents {
|
||||||
-webkit-pointer-events: none;
|
-webkit-pointer-events: none;
|
||||||
@ -70,12 +64,14 @@ body {
|
|||||||
.reader-page-bar,
|
.reader-page-bar,
|
||||||
.reader-page-bar .trail,
|
.reader-page-bar .trail,
|
||||||
.reader-page-bar .thumb,
|
.reader-page-bar .thumb,
|
||||||
.reader-page-bar .track {
|
.reader-page-bar .track,
|
||||||
|
.reader-controls-collapser:before,
|
||||||
|
.reader-controls-collapser span {
|
||||||
transition-property: all;
|
transition-property: all;
|
||||||
transition-duration: 0.2s;
|
transition-duration: 0.2s;
|
||||||
}
|
}
|
||||||
#reader-controls-collapser span {
|
#reader-controls-collapser-bar {
|
||||||
transition-property: all;
|
transition-property: opacity;
|
||||||
transition-duration: 0.4s;
|
transition-duration: 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,18 +84,12 @@ nav.navbar {
|
|||||||
.footer {
|
.footer {
|
||||||
border-top: 1px solid rgba(128, 128, 128, 0.5);
|
border-top: 1px solid rgba(128, 128, 128, 0.5);
|
||||||
}
|
}
|
||||||
#reader-controls-collapser {
|
|
||||||
border-left: 1px solid rgba(128, 128, 128, 0.5);
|
|
||||||
border-right: 1px solid rgba(128, 128, 128, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* settings and controls */
|
/* settings and controls */
|
||||||
|
|
||||||
#modal-settings:not(.show-advanced) .advanced {
|
#modal-settings:not(.show-advanced) .advanced {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#modal-settings .advanced label {
|
|
||||||
}
|
|
||||||
#modal-settings .advanced label:before {
|
#modal-settings .advanced label:before {
|
||||||
content: '* ';
|
content: '* ';
|
||||||
}
|
}
|
||||||
@ -110,12 +100,70 @@ nav.navbar {
|
|||||||
left: 0;
|
left: 0;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
}
|
}
|
||||||
#reader-controls-collapser {
|
.reader-controls-collapser {
|
||||||
width: 34px;
|
display: none;
|
||||||
|
color: #eee;
|
||||||
|
text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
.reader.hide-sidebar #reader-controls-collapser .fa-caret-right {
|
.reader-controls-collapser:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#reader-controls-collapser-button {
|
||||||
|
position: absolute;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
}
|
||||||
|
#reader-controls-collapser-button > span {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
margin-right: 1rem;
|
||||||
|
margin-bottom: 0.7rem;
|
||||||
|
}
|
||||||
|
#reader-controls-collapser-button:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 2.75rem 2.75rem 0 0;
|
||||||
|
border-color: rgba(128, 128, 128, 0.2) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
#reader-controls-collapser-button:hover:before {
|
||||||
|
border-color: rgba(128, 128, 128, 0.4) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
.reader.hide-sidebar #reader-controls-collapser-button {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 99;
|
||||||
|
top: 3.5rem;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.reader.hide-sidebar.hide-header #reader-controls-collapser-button {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.reader.hide-sidebar #reader-controls-collapser-button {
|
||||||
transform: rotateY(180deg);
|
transform: rotateY(180deg);
|
||||||
}
|
}
|
||||||
|
#reader-controls-collapser-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin-left: -40px;
|
||||||
|
width: 40px;
|
||||||
|
background: linear-gradient(to right, rgba(64, 64, 64, 0), rgba(64, 64, 64, 0.4));
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
#reader-controls-collapser-bar:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.reader.hide-sidebar #reader-controls-collapser-bar span {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
#reader-controls-collapser-bar span {
|
||||||
|
font-size: 2.75rem;
|
||||||
|
margin-left: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.reader-controls-mode span:not(.fas) {
|
.reader-controls-mode span:not(.fas) {
|
||||||
display: none
|
display: none
|
||||||
}
|
}
|
||||||
@ -333,9 +381,6 @@ body {
|
|||||||
.reader-page-bar:hover .thumb {
|
.reader-page-bar:hover .thumb {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
}
|
}
|
||||||
.reader-page-bar:hover .track {
|
|
||||||
/*border: 2px solid #ccc;*/
|
|
||||||
}
|
|
||||||
.reader-page-bar .notch:not(.loaded) {
|
.reader-page-bar .notch:not(.loaded) {
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
-45deg,
|
-45deg,
|
||||||
@ -431,6 +476,11 @@ body {
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* cursor hiding */
|
||||||
|
.hide-cursor .reader-images img {
|
||||||
|
cursor: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Modernizr */
|
/* Modernizr */
|
||||||
|
|
||||||
.no-localstorage #alert-storage-warning {
|
.no-localstorage #alert-storage-warning {
|
||||||
@ -454,6 +504,15 @@ body {
|
|||||||
/* desktop definitions */
|
/* desktop definitions */
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
|
.reader-controls {
|
||||||
|
border-left: 1px solid rgba(128, 128, 128, 0.5);
|
||||||
|
}
|
||||||
|
.reader-controls-title {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
.reader-controls-title .manga-title-col {
|
||||||
|
padding: 0 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.reader.layout-horizontal .reader-controls-wrapper {
|
.reader.layout-horizontal .reader-controls-wrapper {
|
||||||
order: 2;
|
order: 2;
|
||||||
@ -462,6 +521,10 @@ body {
|
|||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#right_swipe_area {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* controls */
|
/* controls */
|
||||||
|
|
||||||
.reader:not(.layout-horizontal) .d-lg-none {
|
.reader:not(.layout-horizontal) .d-lg-none {
|
||||||
@ -469,7 +532,7 @@ body {
|
|||||||
}
|
}
|
||||||
.reader:not(.layout-horizontal) .reader-controls-pages,
|
.reader:not(.layout-horizontal) .reader-controls-pages,
|
||||||
.reader:not(.layout-horizontal) .reader-controls-footer,
|
.reader:not(.layout-horizontal) .reader-controls-footer,
|
||||||
.reader:not(.layout-horizontal) #reader-controls-collapser
|
.reader:not(.layout-horizontal) .reader-controls-collapser
|
||||||
{
|
{
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@ -486,12 +549,17 @@ body {
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
.reader.layout-horizontal.hide-sidebar .reader-controls-wrapper {
|
.reader.layout-horizontal.hide-sidebar .reader-controls-wrapper {
|
||||||
width: 34px;
|
width: 0;
|
||||||
}
|
}
|
||||||
.reader.layout-horizontal.hide-sidebar .reader-controls {
|
.reader.layout-horizontal.hide-sidebar .reader-controls {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reader[data-collapser="bar"] #reader-controls-collapser-bar,
|
||||||
|
.reader[data-collapser="button"] #reader-controls-collapser-button {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
/* load icon */
|
/* load icon */
|
||||||
|
|
||||||
.reader.layout-horizontal .reader-load-icon {
|
.reader.layout-horizontal .reader-load-icon {
|
||||||
@ -511,7 +579,7 @@ body {
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
.reader.layout-horizontal.hide-sidebar .reader-page-bar {
|
.reader.layout-horizontal.hide-sidebar .reader-page-bar {
|
||||||
right: 34px;
|
right: 0;
|
||||||
}
|
}
|
||||||
.reader.layout-horizontal .reader-page-bar:hover .track {
|
.reader.layout-horizontal .reader-page-bar:hover .track {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
@ -532,7 +600,8 @@ body {
|
|||||||
padding-right: 20vw !important;
|
padding-right: 20vw !important;
|
||||||
}
|
}
|
||||||
.reader.layout-horizontal.hide-sidebar .reader-images {
|
.reader.layout-horizontal.hide-sidebar .reader-images {
|
||||||
padding-right: 34px !important;
|
padding-right: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -11,9 +11,9 @@ body {
|
|||||||
font-family: "Ubuntu", sans-serif;
|
font-family: "Ubuntu", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flag{display:inline-block;background:url(/images/flags-flat-24.png) no-repeat;background-size:100%;width:24px;min-width:24px;height:24px;vertical-align:bottom;opacity:0.75;}
|
.flag{display:inline-block;background:url(/images/flags-flat-24-20201130.png) no-repeat;background-size:100%;width:24px;min-width:24px;height:24px;vertical-align:bottom;opacity:0.75;}
|
||||||
|
|
||||||
.flag-_unknown{background-position:0 -0px}.flag-bd{background-position:0 -24px}.flag-bg{background-position:0 -48px}.flag-br{background-position:0 -72px}.flag-cn{background-position:0 -96px}.flag-ct{background-position:0 -120px}.flag-cz{background-position:0 -144px}.flag-de{background-position:0 -168px}.flag-dk{background-position:0 -192px}.flag-es{background-position:0 -216px}.flag-fi{background-position:0 -240px}.flag-fr{background-position:0 -264px}.flag-gb{background-position:0 -288px}.flag-gr{background-position:0 -312px}.flag-hk{background-position:0 -336px}.flag-hu{background-position:0 -360px}.flag-id{background-position:0 -384px}.flag-il{background-position:0 -408px}.flag-in{background-position:0 -432px}.flag-ir{background-position:0 -456px}.flag-it{background-position:0 -480px}.flag-jp{background-position:0 -504px}.flag-kr{background-position:0 -528px}.flag-lt{background-position:0 -552px}.flag-mm{background-position:0 -576px}.flag-mn{background-position:0 -600px}.flag-mx{background-position:0 -624px}.flag-my{background-position:0 -648px}.flag-nl{background-position:0 -672px}.flag-no{background-position:0 -696px}.flag-ph{background-position:0 -720px}.flag-pl{background-position:0 -744px}.flag-pt{background-position:0 -768px}.flag-ro{background-position:0 -792px}.flag-rs{background-position:0 -816px}.flag-ru{background-position:0 -840px}.flag-sa{background-position:0 -864px}.flag-se{background-position:0 -888px}.flag-th{background-position:0 -912px}.flag-tr{background-position:0 -936px}.flag-ua{background-position:0 -960px}.flag-vn{background-position:0 -984px}
|
.flag-_unknown{background-position:0 -0px}.flag-bd{background-position:0 -24px}.flag-bg{background-position:0 -48px}.flag-br{background-position:0 -72px}.flag-cn{background-position:0 -96px}.flag-ct{background-position:0 -120px}.flag-cz{background-position:0 -144px}.flag-de{background-position:0 -168px}.flag-dk{background-position:0 -192px}.flag-es{background-position:0 -216px}.flag-fi{background-position:0 -240px}.flag-fr{background-position:0 -264px}.flag-gb{background-position:0 -288px}.flag-gr{background-position:0 -312px}.flag-hk{background-position:0 -336px}.flag-hu{background-position:0 -360px}.flag-id{background-position:0 -384px}.flag-il{background-position:0 -408px}.flag-in{background-position:0 -432px}.flag-ir{background-position:0 -456px}.flag-it{background-position:0 -480px}.flag-jp{background-position:0 -504px}.flag-kr{background-position:0 -528px}.flag-kz{background-position:0 -552px}.flag-lt{background-position:0 -576px}.flag-mm{background-position:0 -600px}.flag-mn{background-position:0 -624px}.flag-mx{background-position:0 -648px}.flag-my{background-position:0 -672px}.flag-nl{background-position:0 -696px}.flag-no{background-position:0 -720px}.flag-ph{background-position:0 -744px}.flag-pl{background-position:0 -768px}.flag-pt{background-position:0 -792px}.flag-ro{background-position:0 -816px}.flag-rs{background-position:0 -840px}.flag-ru{background-position:0 -864px}.flag-sa{background-position:0 -888px}.flag-se{background-position:0 -912px}.flag-th{background-position:0 -936px}.flag-tr{background-position:0 -960px}.flag-ua{background-position:0 -984px}.flag-vn{background-position:0 -1008px}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
@ -529,12 +529,6 @@ function display_manga_ext_links($links_array) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function display_manga_logo_link($manga) {
|
|
||||||
return "<a alt='Manga $manga->manga_id' title='$manga->manga_name' href='/title/$manga->manga_id/" . slugify($manga->manga_name) . "'>
|
|
||||||
<img class='rounded' src='" . IMG_SERVER_URL . "/images/manga/$manga->manga_id.thumb.jpg' alt='Manga image' />
|
|
||||||
</a>";
|
|
||||||
}
|
|
||||||
|
|
||||||
function display_js_posting() {
|
function display_js_posting() {
|
||||||
return "
|
return "
|
||||||
$('.bbcode').click(function(){
|
$('.bbcode').click(function(){
|
||||||
@ -640,7 +634,7 @@ function display_forum($forum, $user) {
|
|||||||
$return = "
|
$return = "
|
||||||
<div class='d-flex row m-0 py-1 border-bottom align-items-center'>
|
<div class='d-flex row m-0 py-1 border-bottom align-items-center'>
|
||||||
<div class='col-auto px-2 ' >
|
<div class='col-auto px-2 ' >
|
||||||
<a href='/forum/$forum->forum_id'><img src='" . LOCAL_SERVER_URL . "/images/forums/$forum->forum_name.svg' width='70px' ></a>
|
<a href='/forum/$forum->forum_id'><img src='" . LOCAL_SERVER_URL . "/images/forums/" . str_replace(' ', '-', $forum->forum_name) . ".svg' width='70px' ></a>
|
||||||
</div>
|
</div>
|
||||||
<div class='col p-0 text-truncate'>
|
<div class='col p-0 text-truncate'>
|
||||||
<div class='row m-2'>
|
<div class='row m-2'>
|
||||||
@ -1520,6 +1514,12 @@ function display_lock_manga($user, $manga) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function display_regenerate_manga_thumb($user) {
|
||||||
|
if (validate_level($user, 'mod')) {
|
||||||
|
return "<button class='btn btn-info' id='manga_regenerate_thumb_button'>" . display_fa_icon('sync', 'Regenerate thumb') . " <span class='d-none d-xl-inline'>Regenerate thumb</span></button>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function display_delete_manga($user) {
|
function display_delete_manga($user) {
|
||||||
if (validate_level($user, 'admin'))
|
if (validate_level($user, 'admin'))
|
||||||
return "<button class='btn btn-danger float-right' id='delete_button'>" . display_fa_icon('trash', 'Delete') . " <span class='d-none d-xl-inline'>Delete</span></button>";
|
return "<button class='btn btn-danger float-right' id='delete_button'>" . display_fa_icon('trash', 'Delete') . " <span class='d-none d-xl-inline'>Delete</span></button>";
|
||||||
|
@ -998,6 +998,24 @@ function get_zip_originalsize($filename) {
|
|||||||
return $size;
|
return $size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get_banners($enabledOnly = true){
|
||||||
|
global $sql;
|
||||||
|
|
||||||
|
$query = "
|
||||||
|
SELECT banner_id, banners.user_id, username, ext, is_enabled, is_anonymous, levels.level_name, levels.level_colour
|
||||||
|
FROM mangadex_banners banners
|
||||||
|
JOIN mangadex_users users
|
||||||
|
ON banners.user_id = users.user_id
|
||||||
|
JOIN mangadex_user_levels levels
|
||||||
|
ON users.level_id = levels.level_id
|
||||||
|
";
|
||||||
|
if($enabledOnly){
|
||||||
|
$query .= " WHERE is_enabled = 1";
|
||||||
|
}
|
||||||
|
$banners = $sql->prep("banners_" . ($enabledOnly ? "enabled" : "all"), $query, [], 'fetchAll', PDO::FETCH_ASSOC, 600);
|
||||||
|
return $banners;
|
||||||
|
}
|
||||||
|
|
||||||
/*************************************
|
/*************************************
|
||||||
* Discord webhook
|
* Discord webhook
|
||||||
*************************************/
|
*************************************/
|
||||||
|
@ -36,6 +36,7 @@ $opt = [
|
|||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
PDO::ATTR_PERSISTENT => defined('DB_PERSISTENT') ? (bool)DB_PERSISTENT : false,
|
||||||
];
|
];
|
||||||
|
|
||||||
class SQL extends PDO {
|
class SQL extends PDO {
|
||||||
@ -45,11 +46,35 @@ class SQL extends PDO {
|
|||||||
/** @var \PDO */
|
/** @var \PDO */
|
||||||
private $slave_sql;
|
private $slave_sql;
|
||||||
|
|
||||||
|
private $credentials = [];
|
||||||
|
private $isConnected = false;
|
||||||
|
|
||||||
public function __construct(string $dsn_master, array $dsn_slaves, $username = null, $passwd = null, $options = null)
|
public function __construct(string $dsn_master, array $dsn_slaves, $username = null, $passwd = null, $options = null)
|
||||||
{
|
{
|
||||||
|
$this->credentials = [
|
||||||
|
'dsn_master' => $dsn_master,
|
||||||
|
'dsn_slaves' => $dsn_slaves,
|
||||||
|
'username' => $username,
|
||||||
|
'passwd' => $passwd,
|
||||||
|
'options' => $options,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureConnected(): void
|
||||||
|
{
|
||||||
|
if (!$this->isConnected) {
|
||||||
|
$dsn_master = $this->credentials['dsn_master'];
|
||||||
|
$dsn_slaves = $this->credentials['dsn_slaves'];
|
||||||
|
$username = $this->credentials['username'];
|
||||||
|
$passwd = $this->credentials['passwd'];
|
||||||
|
$options = $this->credentials['options'];
|
||||||
|
$this->credentials = [];
|
||||||
|
|
||||||
// Establish connection with master
|
// Establish connection with master
|
||||||
parent::__construct($dsn_master, $username, $passwd, $options);
|
parent::__construct($dsn_master, $username, $passwd, $options);
|
||||||
|
|
||||||
|
$this->isConnected = true;
|
||||||
|
|
||||||
// Randomize pick order
|
// Randomize pick order
|
||||||
shuffle($dsn_slaves);
|
shuffle($dsn_slaves);
|
||||||
|
|
||||||
@ -75,14 +100,16 @@ class SQL extends PDO {
|
|||||||
// Fall back to master
|
// Fall back to master
|
||||||
$this->slave_sql = $this;
|
$this->slave_sql = $this;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function query_read($name, $query, $fetch, $pdo_mode, $expiry = 0) {
|
public function query_read($name, $query, $fetch, $pdo_mode, $expiry = 0) {
|
||||||
global $memcached;
|
global $memcached;
|
||||||
|
|
||||||
$name = str_replace(' ', '_', $name);
|
$name = str_replace(' ', '_', $name);
|
||||||
|
|
||||||
if ($expiry < 0) //delete from cache and update
|
if ($expiry < 0) {
|
||||||
$memcached->delete($name);
|
$memcached->delete($name);
|
||||||
|
}
|
||||||
|
|
||||||
$start = microtime(true);
|
$start = microtime(true);
|
||||||
|
|
||||||
@ -90,15 +117,20 @@ class SQL extends PDO {
|
|||||||
$from_cache = 'Y';
|
$from_cache = 'Y';
|
||||||
|
|
||||||
if ($cache === FALSE) {
|
if ($cache === FALSE) {
|
||||||
if ($fetch == 'fetchAll')
|
$this->ensureConnected();
|
||||||
|
if ($fetch === 'fetchAll') {
|
||||||
$cache = $this->slave_sql->query($query)->fetchAll($pdo_mode);
|
$cache = $this->slave_sql->query($query)->fetchAll($pdo_mode);
|
||||||
elseif ($fetch == 'fetchColumn')
|
}
|
||||||
|
elseif ($fetch === 'fetchColumn') {
|
||||||
$cache = $this->slave_sql->query($query)->fetchColumn();
|
$cache = $this->slave_sql->query($query)->fetchColumn();
|
||||||
else
|
}
|
||||||
|
else {
|
||||||
$cache = $this->slave_sql->query($query)->fetch($pdo_mode);
|
$cache = $this->slave_sql->query($query)->fetch($pdo_mode);
|
||||||
|
}
|
||||||
|
|
||||||
if ($expiry >= 0)
|
if ($expiry >= 0) {
|
||||||
$memcached->set($name, $cache, $expiry);
|
$memcached->set($name, $cache, $expiry);
|
||||||
|
}
|
||||||
$from_cache = 'N';
|
$from_cache = 'N';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,13 +142,14 @@ class SQL extends PDO {
|
|||||||
return $cache;
|
return $cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prep($name, $query, $bind, $fetch, $pdo_mode = '', $expiry = 0) {
|
public function prep($name, $query, $bind, $fetch, $pdo_mode = '', $expiry = 0, $force_master = false) {
|
||||||
global $memcached;
|
global $memcached;
|
||||||
|
|
||||||
$name = str_replace(' ', '_', $name);
|
$name = str_replace(' ', '_', $name);
|
||||||
|
|
||||||
if ($expiry < 0) //delete from cache and update
|
if ($expiry < 0) {
|
||||||
$memcached->delete($name);
|
$memcached->delete($name);
|
||||||
|
}
|
||||||
|
|
||||||
$start = microtime(true);
|
$start = microtime(true);
|
||||||
|
|
||||||
@ -124,7 +157,8 @@ class SQL extends PDO {
|
|||||||
$from_cache = 'Y';
|
$from_cache = 'Y';
|
||||||
|
|
||||||
if ($cache === FALSE) {
|
if ($cache === FALSE) {
|
||||||
$stmt = $this->slave_sql->prepare($query);
|
$this->ensureConnected();
|
||||||
|
$stmt = $force_master ? $this->prepare($query) : $this->slave_sql->prepare($query);
|
||||||
$stmt->execute($bind);
|
$stmt->execute($bind);
|
||||||
|
|
||||||
switch ($fetch) {
|
switch ($fetch) {
|
||||||
@ -141,8 +175,9 @@ class SQL extends PDO {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($expiry >= 0)
|
if ($expiry >= 0) {
|
||||||
$memcached->set($name, $cache, $expiry);
|
$memcached->set($name, $cache, $expiry);
|
||||||
|
}
|
||||||
$from_cache = 'N';
|
$from_cache = 'N';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,6 +199,7 @@ class SQL extends PDO {
|
|||||||
|
|
||||||
$from_cache = '/';
|
$from_cache = '/';
|
||||||
|
|
||||||
|
$this->ensureConnected();
|
||||||
$stmt = $this->prepare($query);
|
$stmt = $this->prepare($query);
|
||||||
$stmt->execute($bind);
|
$stmt->execute($bind);
|
||||||
|
|
||||||
@ -178,10 +214,6 @@ class SQL extends PDO {
|
|||||||
return $this->lastInsertId();
|
return $this->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function modify_deferred($name, $query, $bind) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function debug() {
|
public function debug() {
|
||||||
global $memcached;
|
global $memcached;
|
||||||
|
|
||||||
@ -305,7 +337,7 @@ require_once ABSPATH . '/scripts/classes/cache.class.req.php';
|
|||||||
if (defined('CAPTURE_CACHE_STATS') && CAPTURE_CACHE_STATS) {
|
if (defined('CAPTURE_CACHE_STATS') && CAPTURE_CACHE_STATS) {
|
||||||
$memcached = new Cache();
|
$memcached = new Cache();
|
||||||
} else {
|
} else {
|
||||||
$memcached = new Memcached();
|
$memcached = new Synced_Memcached();
|
||||||
}
|
}
|
||||||
$memcached->addServer(MEMCACHED_HOST, 11211);
|
$memcached->addServer(MEMCACHED_HOST, 11211);
|
||||||
|
|
||||||
|
5
scripts/js/change_activation_email.req.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
if ($user->user_id){
|
||||||
|
print jquery_post("change_activation_email", 0, "check", "Confirm", "Confirming", "Your email has been updated.", "");
|
||||||
|
}
|
||||||
|
?>
|
@ -397,6 +397,8 @@ $(".mass_edit_delete_button").click(function(event){
|
|||||||
|
|
||||||
<?= jquery_get("manga_unlock", $id, '', "Unlock", "Unlocking", "You have unlocked this manga.", "location.reload();") ?>
|
<?= jquery_get("manga_unlock", $id, '', "Unlock", "Unlocking", "You have unlocked this manga.", "location.reload();") ?>
|
||||||
|
|
||||||
|
<?= jquery_get("manga_regenerate_thumb", $id, '', "Regenerate thumb", "Regenerating thumb", "You have regenerated the thumbnail.", "location.reload();") ?>
|
||||||
|
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<?php if (validate_level($user, 'gmod')) { ?>
|
<?php if (validate_level($user, 'gmod')) { ?>
|
||||||
|
@ -1,7 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
switch ($mode) {
|
||||||
|
case 'email_search':
|
||||||
|
?>
|
||||||
$("#user_search_form").submit(function(event) {
|
$("#user_search_form").submit(function(event) {
|
||||||
var email = encodeURIComponent($("#email").val());
|
var email = encodeURIComponent($("#email").val());
|
||||||
var username = encodeURIComponent($("#username").val());
|
var username = encodeURIComponent($("#username").val());
|
||||||
$("#search_button").html("<?= display_fa_icon('spinner', '', 'fa-pulse') ?> Searching...").attr("disabled", true);
|
$("#search_button").html("<?= display_fa_icon('spinner', '', 'fa-pulse') ?> Searching...").attr("disabled", true);
|
||||||
location.href = "/pr/?username="+username+"&email="+email;
|
location.href = "/pr/email_search?username="+username+"&email="+email;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
<?php
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'banners':
|
||||||
|
default:
|
||||||
|
?>
|
||||||
|
$("#banner_upload_form").submit(function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
$("#banner_upload_button").html("<span class='fas fa-spinner fa-pulse' aria-hidden='true' title=''></span> Uploading...").attr("disabled", true);
|
||||||
|
|
||||||
|
const success_msg = "<div class='alert alert-success text-center' role='alert'><strong>Success:</strong> Your banner has been uploaded.</div>";
|
||||||
|
const error_msg = "<div class='alert alert-warning text-center' role='alert'><strong>Warning:</strong> Something went wrong with your upload.</div>";
|
||||||
|
const form = this;
|
||||||
|
const formdata = new FormData(form);
|
||||||
|
$.ajax({
|
||||||
|
url: "/ajax/actions.ajax.php?function=banner_upload",
|
||||||
|
type: 'POST',
|
||||||
|
data: formdata,
|
||||||
|
cache: false,
|
||||||
|
contentType: false,
|
||||||
|
processData: false,
|
||||||
|
success: function (data) {
|
||||||
|
if (!data) {
|
||||||
|
$("#message_container").html(success_msg).show().delay(3000).fadeOut();
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$("#banner_upload_button").html("<?= display_fa_icon('upload') ?> Upload").attr("disabled", false);
|
||||||
|
$("#message_container").html(data).show().delay(5000).fadeOut();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
console.error(err);
|
||||||
|
$("#banner_upload_button").html("<?= display_fa_icon('upload') ?> Upload").attr("disabled", false);
|
||||||
|
$("#message_container").html(error_msg).show().delay(5000).fadeOut();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".toggle_banner_edit_button, .cancel_banner_edit_button").click(function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
let id = $(this).attr("data-toggle");
|
||||||
|
$("#banner_edit_" + id).toggle();
|
||||||
|
$("#banner_" + id).toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".banner_edit_form").submit(function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
const id = $(this).attr("data-banner-id");
|
||||||
|
const success_msg = "<div class='alert alert-success text-center' role='alert'><strong>Success:</strong> Your banner has been edited.</div>";
|
||||||
|
const error_msg = "<div class='alert alert-warning text-center' role='alert'><strong>Warning:</strong> Something went wrong with your edit.</div>";
|
||||||
|
const formData = new FormData($(this)[0]);
|
||||||
|
$("#banner_edit_button_"+id).html("<?= display_fa_icon('spinner', '', 'fa-pulse') ?>").attr("disabled", true);
|
||||||
|
$.ajax({
|
||||||
|
url: "/ajax/actions.ajax.php?function=banner_edit&banner_id=" + id,
|
||||||
|
type: 'POST',
|
||||||
|
data: formData,
|
||||||
|
cache: false,
|
||||||
|
contentType: false,
|
||||||
|
processData: false,
|
||||||
|
success: function(data) {
|
||||||
|
if (!data) {
|
||||||
|
$("#message_container").html(success_msg).show().delay(3000).fadeOut();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$("#message_container").html(data).show().delay(10000).fadeOut();
|
||||||
|
}
|
||||||
|
$("#banner_edit_button_" + id).html("<?= display_fa_icon('pencil-alt', '', 'fa-fw') ?>").attr("disabled", false);
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
console.error(err);
|
||||||
|
$("#banner_edit_button_" + id).html("<?= display_fa_icon('pencil-alt', '', 'fa-fw') ?>").attr("disabled", false);
|
||||||
|
$("#message_container").html(error_msg).show().delay(10000).fadeOut();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
<?php
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
?>
|
@ -1,16 +0,0 @@
|
|||||||
/* closest */
|
|
||||||
if (!Element.prototype.matches) {
|
|
||||||
Element.prototype.matches = Element.prototype.msMatchesSelector ||
|
|
||||||
Element.prototype.webkitMatchesSelector;
|
|
||||||
}
|
|
||||||
if (!Element.prototype.closest) {
|
|
||||||
Element.prototype.closest = function(s) {
|
|
||||||
var el = this;
|
|
||||||
|
|
||||||
do {
|
|
||||||
if (el.matches(s)) return el;
|
|
||||||
el = el.parentElement || el.parentNode;
|
|
||||||
} while (el !== null && el.nodeType === 1);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
}
|
|
8
scripts/reader.min.js
vendored
213
scripts/reader/ReaderPageModel.js
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import EventEmitter from 'wolfy87-eventemitter'
|
||||||
|
|
||||||
|
export default class ReaderPageModel extends EventEmitter {
|
||||||
|
constructor(number, chapterId, url, fallbackURL) {
|
||||||
|
super()
|
||||||
|
this._number = number
|
||||||
|
this._chapter = chapterId
|
||||||
|
this._state = ReaderPageModel.STATE_WAIT
|
||||||
|
this._error = null
|
||||||
|
this._url = url
|
||||||
|
this._fallbackURL = fallbackURL
|
||||||
|
this._image = new Image()
|
||||||
|
this.addImageListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
get number() { return this._number }
|
||||||
|
get chapter() { return this._chapter }
|
||||||
|
get image() { return this._image }
|
||||||
|
get waiting() { return this.state === ReaderPageModel.STATE_WAIT }
|
||||||
|
get loading() { return this.state === ReaderPageModel.STATE_LOADING }
|
||||||
|
get loaded() { return this.state === ReaderPageModel.STATE_LOADED }
|
||||||
|
get hasError() { return this.state === ReaderPageModel.STATE_ERROR }
|
||||||
|
get isDone() { return this.loaded || this.hasError }
|
||||||
|
get error() { return this._error }
|
||||||
|
get state() { return this._state }
|
||||||
|
set state(v) {
|
||||||
|
this._state = v
|
||||||
|
this.trigger('statechange', [this])
|
||||||
|
}
|
||||||
|
get stateName() {
|
||||||
|
switch (this.state) {
|
||||||
|
case 0: return 'wait'
|
||||||
|
case 1: return 'loading'
|
||||||
|
case 2: return 'loaded'
|
||||||
|
case 3: return 'error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load(breakCache = false) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!breakCache && this.isDone) {
|
||||||
|
return resolve(this)
|
||||||
|
} else {
|
||||||
|
if (!this.loading) {
|
||||||
|
this._error = null
|
||||||
|
|
||||||
|
this.unload()
|
||||||
|
loadImage(this._url, this._fallbackURL).then(url => {
|
||||||
|
this._image.src = url
|
||||||
|
}).catch(e => {
|
||||||
|
this._error = e
|
||||||
|
this.state = ReaderPageModel.STATE_ERROR
|
||||||
|
reject(this)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.state = ReaderPageModel.STATE_LOADING
|
||||||
|
}
|
||||||
|
this.once('statechange', () => {
|
||||||
|
switch (this.state) {
|
||||||
|
case ReaderPageModel.STATE_LOADED: return resolve(this)
|
||||||
|
case ReaderPageModel.STATE_ERROR: return reject(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unload() {
|
||||||
|
try {
|
||||||
|
if (this._image.src && this._image.src.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(this._image.src)
|
||||||
|
this._image.src = ''
|
||||||
|
}
|
||||||
|
} catch () {}
|
||||||
|
}
|
||||||
|
|
||||||
|
addImageListeners() {
|
||||||
|
const _errorHandler = () => {
|
||||||
|
this._error = new Error(`Image #${this.number} failed to load.`)
|
||||||
|
this.state = ReaderPageModel.STATE_ERROR
|
||||||
|
this.unload()
|
||||||
|
}
|
||||||
|
const _loadHandler = () => {
|
||||||
|
this.state = ReaderPageModel.STATE_LOADED
|
||||||
|
try { this._image.decode() } catch (e) { }
|
||||||
|
}
|
||||||
|
this._image.addEventListener('error', _errorHandler)
|
||||||
|
this._image.addEventListener('load', _loadHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
reload(breakCache = false) {
|
||||||
|
return this.load(this.hasError || breakCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
static get STATE_WAIT() { return 0 }
|
||||||
|
static get STATE_LOADING() { return 1 }
|
||||||
|
static get STATE_LOADED() { return 2 }
|
||||||
|
static get STATE_ERROR() { return 3 }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadImage(primaryURL, fallbackURL) {
|
||||||
|
try {
|
||||||
|
await caches.delete('mangadex_images')
|
||||||
|
} catch () {}
|
||||||
|
|
||||||
|
// Otherwise fetch the image and time how long it takes
|
||||||
|
let resp, timing, body
|
||||||
|
if (!fallbackURL || primaryURL === fallbackURL || !window.AbortController) {
|
||||||
|
// If we only have one URL, load it normally
|
||||||
|
;[resp, timing, body] = await fetchWithTiming(primaryURL, null)
|
||||||
|
} else {
|
||||||
|
// Otherwise things get weird. We want to race the requests
|
||||||
|
// but also get a working response if possible.
|
||||||
|
const primaryA = new AbortController()
|
||||||
|
const fallbackA = new AbortController()
|
||||||
|
// Make a promise for each URL, starting the fallback 5s after the primary
|
||||||
|
const primaryP = fetchWithTiming(primaryURL, primaryA.signal).catch(e => {
|
||||||
|
return [new Response(null, { status: 599 }), 0, 0]
|
||||||
|
})
|
||||||
|
const fallbackP = sleep(5000, fallbackA.signal)
|
||||||
|
.then(() => {
|
||||||
|
return fetchWithTiming(fallbackURL, fallbackA.signal)
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
return [new Response(null, { status: 599 }), 0, 0]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the first response
|
||||||
|
;[resp, timing, body] = await Promise.race([primaryP, fallbackP])
|
||||||
|
|
||||||
|
// If the first response failed, wait for the second
|
||||||
|
if (!resp.ok) {
|
||||||
|
const results = await Promise.all([primaryP, fallbackP])
|
||||||
|
// Find the first good result, if it exists
|
||||||
|
;[resp, timing, body] = results.find(([r]) => r.ok) || results[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel the other request
|
||||||
|
// We need seperate AbortControllers & abort calls since abort also cancels the *response*
|
||||||
|
// normalize the URLs so we can actually compare
|
||||||
|
if (new URL(resp.url).href === new URL(primaryURL).href) {
|
||||||
|
fallbackA.abort()
|
||||||
|
} else {
|
||||||
|
primaryA.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async report to MD@H server how it went, if applicable
|
||||||
|
if (primaryURL && /mangadex\.network/.test(primaryURL)) {
|
||||||
|
const success = (new URL(resp.url).href === new URL(primaryURL).href) && resp.ok
|
||||||
|
fetch('https://api.mangadex.network/report', {
|
||||||
|
method: 'post',
|
||||||
|
mode: 'cors',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: primaryURL,
|
||||||
|
success: success,
|
||||||
|
bytes: success ? body.size : 0,
|
||||||
|
duration: success ? timing : 0,
|
||||||
|
cached: success && resp.headers.get('X-Cache') === 'HIT',
|
||||||
|
}),
|
||||||
|
keepalive: true, // Keep sending the report even if the user leaves the page as we send it
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw res
|
||||||
|
})
|
||||||
|
.catch(e => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return URL.createObjectURL(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms, signal) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (signal.aborted) return resolve()
|
||||||
|
|
||||||
|
const t = setTimeout(resolve, ms)
|
||||||
|
|
||||||
|
signal.addEventListener('abort', () => {
|
||||||
|
clearTimeout(t)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithTiming(url, signal) {
|
||||||
|
let resp, body
|
||||||
|
const networkImage = /mangadex\.network/.test(url)
|
||||||
|
const start = 'performance' in self ? performance.now() : +new Date()
|
||||||
|
const hashM = /\/(?:data)\/.*-([0-9a-f]{64})\.[a-z]{3,4}$/.exec(url)
|
||||||
|
|
||||||
|
resp = await fetch(url, {
|
||||||
|
mode: 'cors',
|
||||||
|
cache: networkImage ? 'no-store' : 'default',
|
||||||
|
referrer: 'no-referrer',
|
||||||
|
redirect: 'error', // Cross-origin redirects strip required headers, so explicitly error instead
|
||||||
|
integrity: hashM ? `sha256-${hex2b64(hashM[1])}` : undefined, // if the image has a hash in it, check that it matches what we got
|
||||||
|
signal, // Cancel the fetch if we already got a response
|
||||||
|
})
|
||||||
|
body = await resp.blob()
|
||||||
|
|
||||||
|
const end = 'performance' in self ? performance.now() : +new Date()
|
||||||
|
return [resp, end - start, body]
|
||||||
|
}
|
||||||
|
|
||||||
|
function hex2b64(s) {
|
||||||
|
let b = ''
|
||||||
|
const n = Math.ceil(s.length / 2)
|
||||||
|
for (let i = 0, o = 0; i < n; i++, o += 2) {
|
||||||
|
b += String.fromCharCode(parseInt(s.substr(o, 2), 16))
|
||||||
|
}
|
||||||
|
return btoa(b)
|
||||||
|
}
|
61
scripts/reader/ReaderSetting.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { StoreCookie, StoreLocalStorage } from './Store'
|
||||||
|
|
||||||
|
const SettingStore = Modernizr.localstorage ? StoreLocalStorage : StoreCookie
|
||||||
|
|
||||||
|
export default class ReaderSetting {
|
||||||
|
constructor(name, def, test, parser) {
|
||||||
|
this._name = name
|
||||||
|
this._default = def
|
||||||
|
this._value = def
|
||||||
|
this.test = test || ReaderSetting.getTest(def)
|
||||||
|
this.parser = parser || ReaderSetting.getParser(def)
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() { return this._name }
|
||||||
|
|
||||||
|
get default() { return this._default }
|
||||||
|
|
||||||
|
get value() { return this._value }
|
||||||
|
set value(val) {
|
||||||
|
const parsedVal = this.parser(val)
|
||||||
|
if (this.test(parsedVal)) {
|
||||||
|
this._value = parsedVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
this.value = SettingStore.load(this.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
save(val) {
|
||||||
|
this.value = val
|
||||||
|
if (this.value === this.default) {
|
||||||
|
SettingStore.clear(this.name)
|
||||||
|
} else {
|
||||||
|
SettingStore.save(this.name, this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
SettingStore.clear(this.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getTest(val) {
|
||||||
|
switch (typeof val) {
|
||||||
|
case 'number': return (val) => !isNaN(val)
|
||||||
|
case 'string': return (val) => typeof val === 'string'
|
||||||
|
default: return (val) => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getParser(val) {
|
||||||
|
switch (typeof val) {
|
||||||
|
case 'number': return (val) => parseFloat(val)
|
||||||
|
default: return (val) => val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static clearAllExcept(retainedKeys) {
|
||||||
|
SettingStore.clearAllExcept(retainedKeys)
|
||||||
|
}
|
||||||
|
}
|
54
scripts/reader/Store.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
|
class Store {
|
||||||
|
static save(key, val) { }
|
||||||
|
static load(key) { }
|
||||||
|
static clear(key) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StoreLocalStorage extends Store {
|
||||||
|
static save(key, val) {
|
||||||
|
localStorage.setItem(`reader.${key}`, val)
|
||||||
|
}
|
||||||
|
static load(key) {
|
||||||
|
return localStorage.getItem(`reader.${key}`)
|
||||||
|
}
|
||||||
|
static clear(key) {
|
||||||
|
localStorage.removeItem(`reader.${key}`)
|
||||||
|
}
|
||||||
|
static clearAllExcept(retainedKeys) {
|
||||||
|
retainedKeys = retainedKeys.map(key => `reader.${key}`)
|
||||||
|
for (let [key, value] of Object.entries(localStorage)) {
|
||||||
|
if (key.startsWith('reader.') && retainedKeys.indexOf(key) === -1) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StoreCookie extends Store {
|
||||||
|
static get cookies() {
|
||||||
|
return document.cookie.split(';').reduce((a, c) => {
|
||||||
|
const [k, v] = c.split('=')
|
||||||
|
a[k.trim()] = v.trim()
|
||||||
|
return a
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
static save(key, val) {
|
||||||
|
Cookies.set(`r-${key}`, val)
|
||||||
|
}
|
||||||
|
static load(key) {
|
||||||
|
return Coookies.get(`r-${key}`)
|
||||||
|
}
|
||||||
|
static clear(key) {
|
||||||
|
Coookies.remove(`r-${key}`)
|
||||||
|
}
|
||||||
|
static clearAllExcept(retainedKeys) {
|
||||||
|
retainedKeys = retainedKeys.map(key => `r-${key}`)
|
||||||
|
for (let [key, value] of Object.entries(Cookies.get())) {
|
||||||
|
if (key.startsWith('reader.') && retainedKeys.indexOf(key) === -1) {
|
||||||
|
Coookies.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
418
scripts/reader/api.js
Normal file
@ -0,0 +1,418 @@
|
|||||||
|
import Utils from './utils.js'
|
||||||
|
import natsort from 'natsort'
|
||||||
|
|
||||||
|
export default class Resource {
|
||||||
|
get resourceType() { return 'resource' }
|
||||||
|
get resourceFormat() { return 'json' }
|
||||||
|
|
||||||
|
constructor(data = {}) {
|
||||||
|
this.initialize(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(data = {}) {
|
||||||
|
this._data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
get status() { return this._data.status }
|
||||||
|
|
||||||
|
load(opts = {}, force = false) {
|
||||||
|
const id = opts.id != null ? opts.id : ''
|
||||||
|
const type = this.resourceType
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
opts.type = type
|
||||||
|
|
||||||
|
if (!(type in Resource.cache)) {
|
||||||
|
Resource.cache[type] = {}
|
||||||
|
}
|
||||||
|
if (!force && id in Resource.cache[type]) {
|
||||||
|
return resolve(Resource.cache[type][id])
|
||||||
|
}
|
||||||
|
const baseURL = opts.baseURL || window.location
|
||||||
|
delete opts.baseURL
|
||||||
|
const url = new URL(this.constructor.API_URL(), baseURL)
|
||||||
|
for (let key in opts) {
|
||||||
|
url.searchParams.append(key, opts[key])
|
||||||
|
}
|
||||||
|
return resolve(fetch(url, {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
this.response = err
|
||||||
|
this._data.message = err.message
|
||||||
|
throw err
|
||||||
|
}).then(res => {
|
||||||
|
this.response = res
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error('Fetch not ok:', type, id, res)
|
||||||
|
return { id }
|
||||||
|
}
|
||||||
|
if (type === 'follows') {
|
||||||
|
return res.text()
|
||||||
|
} else {
|
||||||
|
return res.json().catch(err => {
|
||||||
|
console.error('JSON parsing error:', err)
|
||||||
|
return { id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).then(json => {
|
||||||
|
Resource.cache[type][id] = this
|
||||||
|
this.initialize(json)
|
||||||
|
if (!this.response.ok) {
|
||||||
|
console.error('Response status:', this.response.status, this.response.statusText)
|
||||||
|
console.error('Resource status:', json.status)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}))
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Error:', err)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(opts, force) {
|
||||||
|
const resource = new this()
|
||||||
|
return resource.load(opts, force)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Resource.cache = {}
|
||||||
|
|
||||||
|
var xx = 0
|
||||||
|
export class Manga extends Resource {
|
||||||
|
get resourceType() {
|
||||||
|
return xx++ ? 'manga' : 'mangaa'
|
||||||
|
}
|
||||||
|
// static API_URL(id = '') { return `/api/manga/${id}` }
|
||||||
|
static API_URL() { return `/api/` }
|
||||||
|
|
||||||
|
initialize(data) {
|
||||||
|
super.initialize(data.manga)
|
||||||
|
this.chapters = Object.entries(data.chapter || {}).map(([id, ch]) => { ch.id = parseInt(id); return ch })
|
||||||
|
this.chapterList = []
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() { return this._data.id }
|
||||||
|
get title() { return this._data.title || '' }
|
||||||
|
get langCode() { return this._data.lang_flag }
|
||||||
|
get langName() { return this._data.lang_name }
|
||||||
|
get lastChapter() { return this._data.last_chapter }
|
||||||
|
get isLongStrip() { return this._data.genres && this._data.genres.includes(36) }
|
||||||
|
get isDoujinshi() { return this._data.genres && this._data.genres.includes(7) }
|
||||||
|
get isHentai() { return !!this._data.hentai }
|
||||||
|
get url() {
|
||||||
|
const title = this.title.toLowerCase().replace(/&[0-9a-z]+;/gi, '').replace(/[^0-9a-z]/gi, ' ').split(' ').filter(t => t).join('-')
|
||||||
|
return `/title/${this.id}/${title}`
|
||||||
|
}
|
||||||
|
get coverUrl() { return `/images/manga/${this.id}.jpg` }
|
||||||
|
get coverThumbUrl() { return `/images/manga/${this.id}.thumb.jpg` }
|
||||||
|
|
||||||
|
getChapterData(id) {
|
||||||
|
return this.chapters.find(c => c.id === id)
|
||||||
|
}
|
||||||
|
|
||||||
|
getChapterTitle(id, noTitle = false) {
|
||||||
|
const ch = this.getChapterData(id)
|
||||||
|
if (!ch) {
|
||||||
|
return ''
|
||||||
|
} else {
|
||||||
|
let title = ''
|
||||||
|
if (ch.volume)
|
||||||
|
title += `Vol. ${ch.volume} `
|
||||||
|
if (ch.chapter)
|
||||||
|
title += `Ch. ${ch.chapter} `
|
||||||
|
if (ch.title && !noTitle)
|
||||||
|
title += `${ch.title}`
|
||||||
|
if (!title)
|
||||||
|
title = 'Oneshot'
|
||||||
|
return title.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getChapterName(id) {
|
||||||
|
const ch = this.getChapterData(id)
|
||||||
|
if (!ch) {
|
||||||
|
return ''
|
||||||
|
} else {
|
||||||
|
if (ch.title)
|
||||||
|
return ch.title
|
||||||
|
if (ch.volume && ch.chapter)
|
||||||
|
return `Vol. ${ch.volume} Ch. ${ch.chapter}`
|
||||||
|
if (ch.chapter)
|
||||||
|
return `Ch. ${ch.chapter}`
|
||||||
|
if (ch.volume)
|
||||||
|
return `Vol. ${ch.volume}`
|
||||||
|
return 'Oneshot'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeChapterList(lang, [g1 = 0, g2 = 0, g3 = 0]) {
|
||||||
|
this.chapterList = []
|
||||||
|
const sameLang = this.chapters.filter(c => c.lang_code === lang)
|
||||||
|
Manga.sortChapters(sameLang)
|
||||||
|
let best = null
|
||||||
|
for (let ch of sameLang) {
|
||||||
|
if (!best) {
|
||||||
|
best = ch
|
||||||
|
} else {
|
||||||
|
if (!ch.chapter && (!ch.volume || ch.volume === "0") || (best.chapter !== ch.chapter || best.volume !== ch.volume)) {
|
||||||
|
this.chapterList.push(best)
|
||||||
|
best = ch
|
||||||
|
} else if (ch.group_id === g1 && ch.group_id_2 === g2 && ch.group_id_3 === g3) {
|
||||||
|
best = ch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best) {
|
||||||
|
this.chapterList.push(best)
|
||||||
|
}
|
||||||
|
return this.chapterList
|
||||||
|
}
|
||||||
|
|
||||||
|
getAltChapters(id) {
|
||||||
|
const cur = this.getChapterData(id)
|
||||||
|
if (!cur) {
|
||||||
|
return []
|
||||||
|
} else {
|
||||||
|
const isNonNumbered = (cur.volume === "" || cur.volume === "0") && cur.chapter === ""
|
||||||
|
return this.chapters
|
||||||
|
.filter(c =>
|
||||||
|
c.lang_code === cur.lang_code
|
||||||
|
&& c.volume === cur.volume && c.chapter === cur.chapter
|
||||||
|
&& (!isNonNumbered || cur.title === c.title)
|
||||||
|
).map(c => new Chapter(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrevChapterId(id) {
|
||||||
|
const index = this.chapterList.findIndex(c => c.id === id)
|
||||||
|
if (index <= 0) {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return this.chapterList[index - 1].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextChapterId(id) {
|
||||||
|
const index = this.chapterList.findIndex(c => c.id === id)
|
||||||
|
if (index === -1 || index === this.chapterList.length - 1) {
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
return this.chapterList[index + 1].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
areChaptersSequential(id1, id2) {
|
||||||
|
const c1 = this.getChapterData(id1)
|
||||||
|
const c2 = this.getChapterData(id2)
|
||||||
|
if (!c1 || !c2) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const c1Chapter = parseFloat(c1.chapter)
|
||||||
|
const c2Chapter = parseFloat(c2.chapter)
|
||||||
|
const c1Volume = parseFloat(c1.volume)
|
||||||
|
const c2Volume = parseFloat(c2.volume)
|
||||||
|
if (isNaN(c1Chapter) || isNaN(c2Chapter)) {
|
||||||
|
return true
|
||||||
|
} else if (c1Chapter === c2Chapter && c1Volume === c2Volume) {
|
||||||
|
return true
|
||||||
|
} else if (Math.abs(c1Chapter - c2Chapter).toFixed(1) <= 1.1) {
|
||||||
|
return true
|
||||||
|
} else if ((c1Chapter <= 1 && Math.floor(c1Volume - c2Volume) <= 1) || (c2Chapter <= 1 && Math.floor(c2Volume - c1Volume) <= 1)) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*areChaptersSequential(c1Chapter, c1Volume, c2Chapter, c2Volume) {
|
||||||
|
c1Chapter = parseFloat(c1Chapter)
|
||||||
|
c2Chapter = parseFloat(c2Chapter)
|
||||||
|
c1Volume = parseFloat(c1Volume)
|
||||||
|
c2Volume = parseFloat(c2Volume)
|
||||||
|
if (isNaN(c1Chapter) || isNaN(c2Chapter)) {
|
||||||
|
return true
|
||||||
|
} else if (Math.abs(c1Chapter - c2Chapter).toFixed(1) <= 1.1) {
|
||||||
|
return true
|
||||||
|
} else if ((c1Chapter === 1 || c2Chapter === 1) && Math.abs(c1Volume - c2Volume) <= 1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}*/
|
||||||
|
|
||||||
|
static sortChapters(chapters) {
|
||||||
|
const sorter = natsort({ desc: false, insensitive: true })
|
||||||
|
// sort by volume desc, so that vol null > vol number where ch are equal
|
||||||
|
Utils.stableSort(chapters, (a, b) => sorter(b.volume, a.volume))
|
||||||
|
// sort by first group
|
||||||
|
Utils.stableSort(chapters, (a, b) => sorter(a.group_id, b.group_id))
|
||||||
|
// sort by chapter number
|
||||||
|
Utils.stableSort(chapters, (a, b) => sorter(a.chapter, b.chapter))
|
||||||
|
// add ghost prev vol numbers
|
||||||
|
let pv = '0'
|
||||||
|
chapters.forEach(c => {
|
||||||
|
c.__prev_vol = pv
|
||||||
|
if (c.volume) {
|
||||||
|
pv = c.volume
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// sort by vol or prev vol
|
||||||
|
Utils.stableSort(chapters, (a, b) => sorter(a.volume || a.__prev_vol, b.volume || b.__prev_vol))
|
||||||
|
// remove ghost vols
|
||||||
|
chapters.forEach(c => { delete c.__prev_vol })
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(opts, force) {
|
||||||
|
return super.create(opts, force).then(manga => {
|
||||||
|
manga._data.id = opts.id
|
||||||
|
return manga
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Chapter extends Resource {
|
||||||
|
get resourceType() { return 'chapter' }
|
||||||
|
// static API_URL(id = '') { return `/api/chapter/${id}` }
|
||||||
|
static API_URL() { return `/api/` }
|
||||||
|
|
||||||
|
get id() { return this._data.id }
|
||||||
|
get mangaId() { return this._data.manga_id }
|
||||||
|
get title() { return this._data.title }
|
||||||
|
get chapter() { return this._data.chapter }
|
||||||
|
get volume() { return this._data.volume }
|
||||||
|
get comments() { return this._data.comments }
|
||||||
|
get isLastChapter() { return this.manga && this.manga.lastChapter && this.manga.lastChapter !== "0" && this.manga.lastChapter === this.chapter }
|
||||||
|
get langCode() { return this._data.lang_code }
|
||||||
|
get langName() { return this._data.lang_name }
|
||||||
|
get totalPages() { return this._data.page_array ? this._data.page_array.length : 0 }
|
||||||
|
get groupIds() { return [this._data.group_id, this._data.group_id_2, this._data.group_id_3].filter(n => n) }
|
||||||
|
get groupNames() { return [this._data.group_name, this._data.group_name_2, this._data.group_name_3].filter(n => n) }
|
||||||
|
get groupWebsite() { return this._data.group_website }
|
||||||
|
get timestamp() { return this._data.timestamp }
|
||||||
|
get prevChapterId() { return this.manga.getPrevChapterId(this.id) }
|
||||||
|
get nextChapterId() { return this.manga.getNextChapterId(this.id) }
|
||||||
|
get url() { return `/chapter/${this.id}` }
|
||||||
|
get externalUrl() { return this._data.external || '' }
|
||||||
|
get fullTitle() {
|
||||||
|
let title = ''
|
||||||
|
if (this.volume) title += `Vol. ${this.volume} `
|
||||||
|
if (this.chapter) title += `Ch. ${this.chapter} `
|
||||||
|
if (this.title) title += `${this.title}`
|
||||||
|
if (!title) title = 'Oneshot'
|
||||||
|
return title.trim()
|
||||||
|
}
|
||||||
|
get isMangaFailed() { try { return !this.manga.response.ok } catch (err) { return true } }
|
||||||
|
get isNotFound() { try { return this.response.status == 404 } catch (err) { return false } }
|
||||||
|
get isDelayed() { try { return this.response.status == 409 } catch (err) { return false } }
|
||||||
|
get isDeleted() { try { return this.response.status == 410 } catch (err) { return false } }
|
||||||
|
get isRestricted() { try { return this.response.status == 451 } catch (err) { return false } }
|
||||||
|
get isExternal() { return this._data.status === 'external' }
|
||||||
|
get message() { return this._data.message }
|
||||||
|
get isNetworkServer() {
|
||||||
|
if (!this._isNetworkServer) {
|
||||||
|
this._isNetworkServer = /mangadex\.network/.test(this._data.server || '')
|
||||||
|
}
|
||||||
|
return this._isNetworkServer
|
||||||
|
}
|
||||||
|
|
||||||
|
get pageArray() { return this._data.page_array || [] }
|
||||||
|
getPage(pg) {
|
||||||
|
return pg >= 1 && pg <= this.totalPages ? this._data.page_array[pg - 1] : ''
|
||||||
|
}
|
||||||
|
get pagesFullURL() { return this.pageArray.map((pg, i) => this.imageURL(i + 1)) }
|
||||||
|
|
||||||
|
imageURL(pg) {
|
||||||
|
return this._data.server + this._data.hash + '/' + this.getPage(pg)
|
||||||
|
}
|
||||||
|
|
||||||
|
makeMangaChapterList() {
|
||||||
|
this.manga.makeChapterList(this.langCode, this.groupIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadManga(force) {
|
||||||
|
if (!this._data.manga_id) {
|
||||||
|
console.warn('No manga id for chapter', this.id)
|
||||||
|
return Promise.resolve(this)
|
||||||
|
}
|
||||||
|
return Manga.create({ id: this._data.manga_id }, force).then(manga => {
|
||||||
|
this.manga = manga
|
||||||
|
if (!manga.response.ok) {
|
||||||
|
return Promise.reject(this)
|
||||||
|
} else {
|
||||||
|
this.makeMangaChapterList()
|
||||||
|
return Promise.resolve(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(opts, force) {
|
||||||
|
return super.create(opts, force)
|
||||||
|
.catch(ch => ch.mangaId ? Promise.resolve(ch) : Promise.reject(ch))
|
||||||
|
.then(ch => {
|
||||||
|
if (ch._data.page_array) {
|
||||||
|
ch._data.page_array = ch._data.page_array.filter(p => !!p)
|
||||||
|
}
|
||||||
|
return Promise.resolve(ch)
|
||||||
|
})
|
||||||
|
.then(ch => ch.loadManga(force))
|
||||||
|
.catch(ch => ch.manga && !ch.isMangaFailed ? Promise.resolve(ch) : Promise.reject(ch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Follows extends Resource {
|
||||||
|
get resourceType() { return 'follows' }
|
||||||
|
get resourceFormat() { return 'text' }
|
||||||
|
static API_URL() { return '/follows/' }
|
||||||
|
|
||||||
|
get unreadChapters() {
|
||||||
|
return this.chapters.filter(c => c.id && !c._data.read)
|
||||||
|
}
|
||||||
|
|
||||||
|
get unreadManga() {
|
||||||
|
return this.unreadChapters.reduce((acc, cur) => {
|
||||||
|
acc[cur.manga.id] = cur.manga
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(opts, force) {
|
||||||
|
return super.create(opts, force).then(follows => {
|
||||||
|
const rows = follows._data.match(/col-md-3 [\s\S]*?chapter-list-group/gim)
|
||||||
|
if (!rows) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const mangaCache = {}
|
||||||
|
let mangaTitle = ''
|
||||||
|
follows.chapters = rows.map(row => {
|
||||||
|
const none = ['', '']
|
||||||
|
mangaTitle = (row.match(/manga_title[\s\S]*?title='([\s\S]*?)'/) || none)[1].trim() || ''
|
||||||
|
if (mangaTitle) { console.log(mangaTitle) }
|
||||||
|
const mangaId = parseInt((row.match(/data-manga-id="(\d*?)"/) || none)[1])
|
||||||
|
if (!(mangaId in mangaCache)) {
|
||||||
|
mangaCache[mangaId] = new Manga()
|
||||||
|
mangaCache[mangaId].initialize({
|
||||||
|
manga: {
|
||||||
|
id: mangaId || 0,
|
||||||
|
title: mangaTitle,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const manga = mangaCache[mangaId]
|
||||||
|
const chapter = new Chapter()
|
||||||
|
chapter.initialize({
|
||||||
|
id: parseInt((row.match(/data-id="(\d*?)"/) || none)[1]) || null,
|
||||||
|
title: (row.match(/data-title="([\s\S]*?)"/) || none)[1],
|
||||||
|
chapter: parseFloat((row.match(/data-chapter="([\d\.]*?)"/) || none)[1]) || null,
|
||||||
|
volume: parseFloat((row.match(/data-volume="([\d\.]*?)"/) || none)[1]) || null,
|
||||||
|
timestamp: parseInt((row.match(/data-timestamp="(.*?)"/) || none)[1]) * 1000 || null,
|
||||||
|
lang_code: (row.match(/flag-(..)/) || none)[1],
|
||||||
|
read: /chapter_mark_unread_button/.test(row),
|
||||||
|
})
|
||||||
|
chapter.manga = manga
|
||||||
|
manga.chapters.push(chapter)
|
||||||
|
return chapter
|
||||||
|
})
|
||||||
|
return follows
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// export default { Manga, Chapter, Follows }
|
6
scripts/reader/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Reader from './reader-controller.js'
|
||||||
|
|
||||||
|
const reader = new Reader()
|
||||||
|
reader.initialize()
|
||||||
|
|
||||||
|
window.reader = reader
|
138
scripts/reader/keyboard-shortcuts.js
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import ReaderView from './reader-view.js'
|
||||||
|
|
||||||
|
export default class KeyboardShortcuts {
|
||||||
|
static registerDefaults() {
|
||||||
|
const defaultScroll = 50
|
||||||
|
// kbd shortcuts
|
||||||
|
// ^ only when shift is pressed
|
||||||
|
// ! only when shift is not pressed
|
||||||
|
this.register('turnPageLeft', ['arrowleft', 'left', 'a'], function (evt, view) {
|
||||||
|
if (ReaderView.isScrolledToLeft) {
|
||||||
|
view.turnPageLeft(evt.shiftKey ? 1 : undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.register('turnPageRight', ['arrowright', 'right', 'd'], function (evt, view) {
|
||||||
|
if (ReaderView.isScrolledToRight) {
|
||||||
|
view.turnPageRight(evt.shiftKey ? 1 : undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.register('turnPageUp', ['arrowup', 'up', 'w'], function (evt, view) {
|
||||||
|
if (view.model.settings.pageWheelTurn == 1 && ReaderView.isScrolledToTop) {
|
||||||
|
view.turnPageBackward(evt.shiftKey ? 1 : undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.register('turnPageDown', ['arrowdown', 'down', 's'], function (evt, view) {
|
||||||
|
if (view.model.settings.pageWheelTurn == 1 && ReaderView.isScrolledToBottom) {
|
||||||
|
view.turnPageForward(evt.shiftKey ? 1 : undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.register('scrollLeft', ['arrowleft', 'left', 'a'], function (evt, view) {
|
||||||
|
if (view.model.settings.scrollingMethod == 1) {
|
||||||
|
ReaderView.scroll(-Math.floor(view.el.clientWidth * 0.9), 0, 'smooth')
|
||||||
|
} else if (view.model.settings.scrollingMethod == 0) {
|
||||||
|
const key = evt.key.toLowerCase()
|
||||||
|
if (key !== 'arrowleft' && key !== 'left') {
|
||||||
|
ReaderView.scroll(-defaultScroll, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.register('scrollRight', ['arrowright', 'right', 'd'], function (evt, view) {
|
||||||
|
if (view.model.settings.scrollingMethod == 1) {
|
||||||
|
ReaderView.scroll(Math.floor(view.el.clientWidth * 0.9), 0, 'smooth')
|
||||||
|
} else if (view.model.settings.scrollingMethod == 0) {
|
||||||
|
const key = evt.key.toLowerCase()
|
||||||
|
if (key !== 'arrowright' && key !== 'right') {
|
||||||
|
ReaderView.scroll(defaultScroll, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.register('scrollUp', ['arrowup', 'up', 'w'], function (evt, view) {
|
||||||
|
if (view.model.settings.scrollingMethod == 1) {
|
||||||
|
ReaderView.scroll(0, -Math.floor(view.el.clientHeight * 0.9), 'smooth')
|
||||||
|
} else if (view.model.settings.scrollingMethod == 0) {
|
||||||
|
const key = evt.key.toLowerCase()
|
||||||
|
if (key !== 'arrowup' && key !== 'up') {
|
||||||
|
ReaderView.scroll(0, -defaultScroll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.register('scrollDown', ['arrowdown', 'down', 's'], function (evt, view) {
|
||||||
|
if (view.model.settings.scrollingMethod == 1) {
|
||||||
|
ReaderView.scroll(0, Math.floor(view.el.clientHeight * 0.9), 'smooth')
|
||||||
|
} else if (view.model.settings.scrollingMethod == 0) {
|
||||||
|
const key = evt.key.toLowerCase()
|
||||||
|
if (key !== 'arrowdown' && key !== 'down') {
|
||||||
|
ReaderView.scroll(0, defaultScroll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.register('turnChapterLeft', ['^q'], function (evt, view) {
|
||||||
|
view.moveToChapter(view.model.isDirectionLTR ? view.model.chapter.prevChapterId : view.model.chapter.nextChapterId, 1)
|
||||||
|
})
|
||||||
|
this.register('turnChapterRight', ['^e'], function (evt, view) {
|
||||||
|
view.moveToChapter(view.model.isDirectionRTL ? view.model.chapter.prevChapterId : view.model.chapter.nextChapterId, 1)
|
||||||
|
})
|
||||||
|
this.register('toggleDisplayFit', ['f'], function (evt, view) {
|
||||||
|
view.model.saveSetting('displayFit', view.model.displayFit % 2 + (evt.shiftKey ? 3 : 1))
|
||||||
|
})
|
||||||
|
this.register('toggleRenderingMode', ['g'], function (evt, view) {
|
||||||
|
view.model.saveSetting('renderingMode', ((view.model.renderingMode) + (evt.shiftKey ? -1 : 1)) % 3 || 3)
|
||||||
|
})
|
||||||
|
this.register('toggleDirection', ['h'], function (evt, view) {
|
||||||
|
view.model.saveSetting('direction', view.model.direction % 2 + 1)
|
||||||
|
})
|
||||||
|
this.register('toggleHeader', ['!r'], function (evt, view) {
|
||||||
|
view.model.saveSetting('hideHeader', view.model.settings.hideHeader ? 0 : 1)
|
||||||
|
})
|
||||||
|
this.register('toggleSidebar', ['!t'], function (evt, view) {
|
||||||
|
view.model.saveSetting('hideSidebar', view.model.settings.hideSidebar ? 0 : 1)
|
||||||
|
})
|
||||||
|
this.register('togglePagebar', ['!y'], function (evt, view) {
|
||||||
|
view.model.saveSetting('hidePagebar', view.model.settings.hidePagebar ? 0 : 1)
|
||||||
|
})
|
||||||
|
this.register('toggleAllBars', ['^r', '^t', '^y'], function (evt, view) {
|
||||||
|
let any = view.model.settings.hideSidebar || view.model.settings.hideHeader || view.model.settings.hidePagebar
|
||||||
|
view.model.saveSetting('hideSidebar', any ? 0 : 1)
|
||||||
|
view.model.saveSetting('hideHeader', any ? 0 : 1)
|
||||||
|
view.model.saveSetting('hidePagebar', any ? 0 : 1)
|
||||||
|
})
|
||||||
|
this.register('exitToManga', ['^m'], function (evt, view) {
|
||||||
|
view.exitToURL(view.model.manga.url)
|
||||||
|
})
|
||||||
|
this.register('exitToComments', ['^k'], function (evt, view) {
|
||||||
|
view.exitToURL(`${view.pageURL(view.model.chapter.id)}/comments`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static register(action, keys, handler) {
|
||||||
|
this._kbdInput = this._kbdInput || {}
|
||||||
|
this._kbdHandlers = this._kbdHandlers || {}
|
||||||
|
this._kbdHandlers[action] = handler
|
||||||
|
for (let key of keys) {
|
||||||
|
this._kbdInput[key] = this._kbdInput[key] || []
|
||||||
|
this._kbdInput[key].push(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static fire(key, evt, view) {
|
||||||
|
if (key in this._kbdInput) {
|
||||||
|
for (let action of this._kbdInput[key]) {
|
||||||
|
if (action in this._kbdHandlers) {
|
||||||
|
this._kbdHandlers[action](evt, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static keydownHandler(evt, view) {
|
||||||
|
if (!(evt.altKey || evt.ctrlKey || evt.metaKey || evt.key === 'OS')) {
|
||||||
|
const tag = (evt.target || evt.srcElement).tagName
|
||||||
|
const key = evt.key.toLowerCase()
|
||||||
|
if (!['INPUT','SELECT','TEXTAREA'].includes(tag)) {
|
||||||
|
evt.stopPropagation()
|
||||||
|
this.fire(key, evt, view)
|
||||||
|
this.fire(evt.shiftKey ? '^'+key : '!'+key, evt, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
scripts/reader/reader-component.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
export default class ReaderComponent {
|
||||||
|
static create(type, props) {
|
||||||
|
const el = typeof type === 'string' ? document.createElement(type) : type
|
||||||
|
for (let i in props) {
|
||||||
|
el[i] = props[i]
|
||||||
|
}
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
static empty(node) {
|
||||||
|
while (node && node.firstChild) {
|
||||||
|
node.removeChild(node.firstChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Option extends ReaderComponent {
|
||||||
|
static render(data) {
|
||||||
|
return this.create('option', {
|
||||||
|
value: data.value,
|
||||||
|
selected: data.selected || false,
|
||||||
|
textContent: data.text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Flag extends ReaderComponent {
|
||||||
|
static render(data, el) {
|
||||||
|
const langCode = (data.language || '_unknown').replace(/\W/g, '')
|
||||||
|
el = this.create(el || 'span', {
|
||||||
|
title: langCode,
|
||||||
|
})
|
||||||
|
el.className = ''
|
||||||
|
el.classList.add('rounded', 'flag', `flag-${langCode}`.trim())
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Link extends ReaderComponent {
|
||||||
|
static render(data, el) {
|
||||||
|
return this.create(el || 'a', {
|
||||||
|
href: data.url,
|
||||||
|
title: data.title,
|
||||||
|
innerHTML: data.title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChapterDropdown extends ReaderComponent {
|
||||||
|
static render(model, el) {
|
||||||
|
this.empty(el)
|
||||||
|
for (let ch of model.manga.uniqueChapterList.slice().reverse()) {
|
||||||
|
el.appendChild(Option.render({
|
||||||
|
value: ch.id,
|
||||||
|
selected: ch.id === model.chapter.id,
|
||||||
|
text: model.settings.showDropdownTitles ? ch.fullTitle : ch.numberTitle,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PageDropdown extends ReaderComponent {
|
||||||
|
static render(model, el) {
|
||||||
|
this.empty(el)
|
||||||
|
for (let i = 1; i <= model.chapter.totalPages; ++i) {
|
||||||
|
el.appendChild(Option.render({
|
||||||
|
value: i,
|
||||||
|
selected: i === model.currentPage,
|
||||||
|
text: i
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GroupItem extends ReaderComponent {
|
||||||
|
static render(chapter) {
|
||||||
|
const li = this.create('li')
|
||||||
|
const flag = li.appendChild(Flag.render(chapter))
|
||||||
|
flag.classList.add('mr-1')
|
||||||
|
const link = li.appendChild(this.create(!chapter.isCurrentChapter ? 'a' : 'strong', {
|
||||||
|
innerHTML: chapter.groups.map(g => g.name).join(' | '),
|
||||||
|
href: chapter.url
|
||||||
|
}))
|
||||||
|
link.dataset.action = "chapter"
|
||||||
|
link.dataset.chapter = chapter.id
|
||||||
|
return li
|
||||||
|
}
|
||||||
|
}
|
55
scripts/reader/reader-controller.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import ReaderModel from './reader-model'
|
||||||
|
import ReaderView from './reader-view'
|
||||||
|
|
||||||
|
export default class Reader {
|
||||||
|
constructor() {
|
||||||
|
this.model = new ReaderModel()
|
||||||
|
this.view = new ReaderView(this.model)
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const meta = document.querySelector('meta[name="app"]')
|
||||||
|
this.model.appMeta = meta ? meta.dataset : {}
|
||||||
|
this.model.loadSettings()
|
||||||
|
for (let mode of ['renderingMode', 'displayFit', 'direction']) {
|
||||||
|
this.model[mode] = this.model.settings[mode]
|
||||||
|
}
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return reject()
|
||||||
|
}
|
||||||
|
return resolve()
|
||||||
|
}).then(() => {
|
||||||
|
this.view.initialize(document.querySelector('div[role="main"]'))
|
||||||
|
this.view.addListeners()
|
||||||
|
if (this.model.appMeta.page === 'recs') {
|
||||||
|
return this.model.moveToRecommendations()
|
||||||
|
.then(() => {
|
||||||
|
this.view.renderer.render()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let page = parseInt(this.model.appMeta.page) || 1
|
||||||
|
if (page === -1 && this.model.chapter) {
|
||||||
|
page = this.model.chapter.totalPages
|
||||||
|
}
|
||||||
|
return this.model.setChapter(parseInt(this.model.appMeta.chapterId), page)
|
||||||
|
.then((chapter) => {
|
||||||
|
if (this.model.appMeta.page === 'recs') {
|
||||||
|
this.view.moveToRecommendations()
|
||||||
|
} else if (!chapter.error) {
|
||||||
|
this.model.preload(page)
|
||||||
|
if (this.model.isStateReading) {
|
||||||
|
return this.view.moveToPage(page, false).then(() => {
|
||||||
|
this.view.replaceHistory()
|
||||||
|
this.view.updatePage()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.view.replaceHistory(chapter.id, null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
445
scripts/reader/reader-model.js
Normal file
@ -0,0 +1,445 @@
|
|||||||
|
import EventEmitter from 'wolfy87-eventemitter'
|
||||||
|
import Chapter from './resource/Chapter'
|
||||||
|
import Manga from './resource/Manga'
|
||||||
|
import Follows from './resource/Follows'
|
||||||
|
import Utils from './utils.js'
|
||||||
|
import ReaderPageModel from './ReaderPageModel'
|
||||||
|
import ReaderSetting from './ReaderSetting'
|
||||||
|
|
||||||
|
export default class ReaderModel extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this._state = ReaderModel.readerState.READING
|
||||||
|
this._isLoading = false
|
||||||
|
this._currentPage = 0
|
||||||
|
this._chapter = null
|
||||||
|
this._renderingMode = 0
|
||||||
|
this._displayFit = 0
|
||||||
|
this._direction = 0
|
||||||
|
this._appMeta = {}
|
||||||
|
this._settings = {}
|
||||||
|
this._settingsShortcut = {}
|
||||||
|
this._pageCache = new Map()
|
||||||
|
this._preloadSet = new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
get appMeta() { return this._appMeta }
|
||||||
|
set appMeta(val) {
|
||||||
|
this._appMeta = val
|
||||||
|
}
|
||||||
|
|
||||||
|
get isUserGuest() { return this.appMeta.guest !== '0' }
|
||||||
|
|
||||||
|
get isLoading() { return this._isLoading }
|
||||||
|
set isLoading(val) {
|
||||||
|
val = !!val
|
||||||
|
if (this._isLoading !== val && !this.exiting) {
|
||||||
|
this._isLoading = val
|
||||||
|
this.trigger('loadingchange', [val])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentPage() { return this._currentPage }
|
||||||
|
setCurrentPage(val) {
|
||||||
|
if (this._currentPage !== val && !isNaN(val) && !this.exiting) {
|
||||||
|
this._currentPage = val
|
||||||
|
this.trigger('currentpagechange', [val])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalPages() { return this.chapter ? this.chapter.totalPages : 0 }
|
||||||
|
|
||||||
|
get state() { return this._state }
|
||||||
|
set state(val) {
|
||||||
|
if (this._state !== val && !this.exiting) {
|
||||||
|
this._state = val
|
||||||
|
this.trigger('statechange', [val])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isStateReading() { return this._state === ReaderModel.readerState.READING }
|
||||||
|
get isStateRecommendations() { return this._state === ReaderModel.readerState.RECS }
|
||||||
|
get isStateGap() { return this._state === ReaderModel.readerState.GAP }
|
||||||
|
|
||||||
|
get exiting() { return this._state === ReaderModel.readerState.EXITING }
|
||||||
|
|
||||||
|
exitReader() {
|
||||||
|
this.state = ReaderModel.readerState.EXITING
|
||||||
|
// bfcache
|
||||||
|
window.onunload = () => { this.state = ReaderModel.readerState.READING }
|
||||||
|
}
|
||||||
|
|
||||||
|
get renderingMode() { return this._renderingMode }
|
||||||
|
set renderingMode(val) {
|
||||||
|
if (this._renderingMode !== val && !this.exiting) {
|
||||||
|
this._renderingMode = val
|
||||||
|
this.trigger('renderingmodechange', [val])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get displayFit() { return this._displayFit }
|
||||||
|
set displayFit(val) {
|
||||||
|
if (this._displayFit !== val && !this.exiting) {
|
||||||
|
this._displayFit = val
|
||||||
|
this.trigger('displayfitchange', [val])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get direction() { return this._direction }
|
||||||
|
set direction(val) {
|
||||||
|
if (this._direction !== val && !this.exiting) {
|
||||||
|
this._direction = val
|
||||||
|
this.trigger('directionchange', [val])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSinglePage() { return this.renderingMode === ReaderModel.renderingModeState.SINGLE }
|
||||||
|
get isDoublePage() { return this.renderingMode === ReaderModel.renderingModeState.DOUBLE }
|
||||||
|
get isLongStrip() { return this.renderingMode === ReaderModel.renderingModeState.LONG }
|
||||||
|
get isNoResize() { return this.displayFit === ReaderModel.displayFitState.NO_RESIZE }
|
||||||
|
get isFitHeight() { return this.displayFit === ReaderModel.displayFitState.FIT_HEIGHT }
|
||||||
|
get isFitWidth() { return this.displayFit === ReaderModel.displayFitState.FIT_WIDTH }
|
||||||
|
get isFitBoth() { return this.displayFit === ReaderModel.displayFitState.FIT_BOTH }
|
||||||
|
get isDirectionLTR() { return this.direction === ReaderModel.directionState.LTR }
|
||||||
|
get isDirectionRTL() { return this.direction === ReaderModel.directionState.RTL }
|
||||||
|
|
||||||
|
get settings() { return this._settingsShortcut }
|
||||||
|
get settingDefaults() {
|
||||||
|
return Array.from(Object.values(this._settings)).reduce((acc, setting) => {
|
||||||
|
acc[setting.name] = setting.default
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSetting(key, value) {
|
||||||
|
const setting = this._settings[key]
|
||||||
|
setting.save(value)
|
||||||
|
this._settingsShortcut[setting.name] = setting.value
|
||||||
|
if (['renderingMode', 'displayFit', 'direction'].includes(key)) {
|
||||||
|
this[key] = setting.value
|
||||||
|
}
|
||||||
|
this.trigger('settingchange', [setting.name, setting.value])
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettings() {
|
||||||
|
const defaults = [
|
||||||
|
new ReaderSetting('displayFit', ReaderModel.displayFitState.FIT_WIDTH),
|
||||||
|
new ReaderSetting('direction', ReaderModel.directionState.LTR),
|
||||||
|
new ReaderSetting('renderingMode', ReaderModel.renderingModeState.SINGLE),
|
||||||
|
new ReaderSetting('showAdvancedSettings', 0),
|
||||||
|
new ReaderSetting('scrollingMethod', 0),
|
||||||
|
new ReaderSetting('swipeDirection', 0),
|
||||||
|
new ReaderSetting('swipeSensitivity', 3),
|
||||||
|
new ReaderSetting('pageTapTurn', 1),
|
||||||
|
new ReaderSetting('pageTurnLongStrip', 1),
|
||||||
|
new ReaderSetting('pageWheelTurn', 0),
|
||||||
|
new ReaderSetting('showDropdownTitles', 1),
|
||||||
|
new ReaderSetting('tapTargetArea', 1),
|
||||||
|
new ReaderSetting('hideHeader', 0),
|
||||||
|
new ReaderSetting('hideSidebar', 0),
|
||||||
|
new ReaderSetting('hidePagebar', 0),
|
||||||
|
new ReaderSetting('collapserStyle', 0),
|
||||||
|
new ReaderSetting('hideCursor', 0),
|
||||||
|
new ReaderSetting('betaRecommendations', 0),
|
||||||
|
new ReaderSetting('imageServer', '0'),
|
||||||
|
new ReaderSetting('dataSaverV2', 0),
|
||||||
|
new ReaderSetting('gapWarning', 1),
|
||||||
|
new ReaderSetting('restrictChLang', 1, null, (val) => {
|
||||||
|
val = parseInt(val)
|
||||||
|
if (this.chapter && this.manga) {
|
||||||
|
this.manga.updateChapterList(this.chapter, val)
|
||||||
|
this.trigger('chapterlistchange', [])
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}),
|
||||||
|
new ReaderSetting('preloadPages', 10, () => true, (val) => {
|
||||||
|
if (isNaN(parseInt(val)))
|
||||||
|
return 10
|
||||||
|
const clamped = Utils.clamp(parseInt(val), 0, this.preloadMax)
|
||||||
|
return !isNaN(clamped) ? clamped : 0
|
||||||
|
}),
|
||||||
|
new ReaderSetting('containerWidth', null, (val) => {
|
||||||
|
return !val || !isNaN(parseInt(val))
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
for (let setting of defaults) {
|
||||||
|
setting.load()
|
||||||
|
this._settings[setting.name] = setting
|
||||||
|
this._settingsShortcut[setting.name] = setting.value
|
||||||
|
this.trigger('settingchange', [setting.name, setting.value])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveSetting('gapWarning', 1)
|
||||||
|
|
||||||
|
ReaderSetting.clearAllExcept(defaults.map(s => s.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
get chapter() { return this._chapter }
|
||||||
|
|
||||||
|
get manga() { return this._chapter ? this._chapter.manga : null }
|
||||||
|
|
||||||
|
getChapterParams() {
|
||||||
|
return {
|
||||||
|
server: this.settings.imageServer !== '0' ? this.settings.imageServer : null,
|
||||||
|
saver: this.settings.dataSaverV2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setChapter(id, pg = 1) {
|
||||||
|
if (this.exiting || this.isLoading) {
|
||||||
|
throw new Error(`Trying to set chapter while ${this.exiting ? 'exiting' : 'loading'}`)
|
||||||
|
} else if (isNaN(id) || id <= 0) {
|
||||||
|
throw new Error("Trying to set invalid chapter: " + id)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isLoading = true
|
||||||
|
let chapter = Chapter.getResource(id)
|
||||||
|
if (chapter && !chapter.isFullyLoaded) {
|
||||||
|
chapter = await Chapter.load(id, this.getChapterParams())
|
||||||
|
} else if (!chapter) {
|
||||||
|
chapter = await Chapter.loadWithManga(id, this.getChapterParams())
|
||||||
|
await Manga.loadChapterList(chapter.manga.id)
|
||||||
|
}
|
||||||
|
const oldManga = this.manga
|
||||||
|
this._chapter = chapter
|
||||||
|
this._currentPage = pg
|
||||||
|
this.isLoading = false
|
||||||
|
chapter.manga.updateChapterList(chapter, this.settings.restrictChLang)
|
||||||
|
this._createPageCache(chapter)
|
||||||
|
this.state = ReaderModel.readerState.READING
|
||||||
|
this.trigger('chapterchange', [chapter])
|
||||||
|
if (!oldManga || oldManga.id !== chapter.manga.id) {
|
||||||
|
this.trigger('mangachange', [chapter.manga])
|
||||||
|
}
|
||||||
|
if (chapter.error || chapter.isExternal) {
|
||||||
|
throw chapter
|
||||||
|
} else if (chapter.totalPages === 0) {
|
||||||
|
chapter.error = new Error("Chapter has no pages.")
|
||||||
|
throw chapter
|
||||||
|
}
|
||||||
|
if (chapter.isNetworkServer) {
|
||||||
|
this.preload(this.currentPage, Infinity)
|
||||||
|
}
|
||||||
|
return chapter
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.isLoading = false
|
||||||
|
this.state = ReaderModel.readerState.ERROR
|
||||||
|
this.trigger('readererror', [error])
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadChapterList() {
|
||||||
|
if (this.manga.isChapterListOutdated) {
|
||||||
|
this.isLoading = true
|
||||||
|
try {
|
||||||
|
await Manga.loadChapterList(this.manga.id)
|
||||||
|
} catch (error) { }
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_createPageCache(chapter) {
|
||||||
|
for (let [i, page] of this._pageCache) {
|
||||||
|
page.unload()
|
||||||
|
page.off()
|
||||||
|
}
|
||||||
|
this._pageCache.clear()
|
||||||
|
this._preloadSet.clear()
|
||||||
|
this._preloading = false
|
||||||
|
let pgNum = 1
|
||||||
|
for (let [url, fallbackURL] of chapter.getAllPageUrls()) {
|
||||||
|
const page = new ReaderPageModel(pgNum, chapter.id, url, fallbackURL)
|
||||||
|
this._pageCache.set(pgNum, page)
|
||||||
|
page.on('statechange', (page) => {
|
||||||
|
switch (page.state) {
|
||||||
|
case ReaderPageModel.STATE_LOADING: return this.trigger('pageloading', [page])
|
||||||
|
case ReaderPageModel.STATE_LOADED: return this.trigger('pageload', [page])
|
||||||
|
case ReaderPageModel.STATE_ERROR: return this.trigger('pageerror', [page])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
++pgNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadPage(pg, skipCache = false) {
|
||||||
|
const page = this._pageCache.get(pg)
|
||||||
|
if (!page) {
|
||||||
|
return Promise.reject(new Error(`Page ${pg} not in cache`))
|
||||||
|
} else {
|
||||||
|
return page.load(skipCache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPage(pg) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.chapter == null || this._pageCache.size === 0) {
|
||||||
|
return reject(new Error("Tried to get a page before chapter has loaded"))
|
||||||
|
} else if (isNaN(pg) || pg < 1 || pg > this.totalPages) {
|
||||||
|
return resolve(null)
|
||||||
|
// return reject(new Error("Page not a number or out of bounds"))
|
||||||
|
} else if (this._pageCache.get(pg).loaded) {
|
||||||
|
return resolve(this._pageCache.get(pg))
|
||||||
|
} else {
|
||||||
|
return resolve(this._loadPage(pg))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadErrorPages() {
|
||||||
|
Array.from(this._pageCache.values()).filter(i => i.hasError).forEach(i => i.reload())
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageWithoutLoading(pg) {
|
||||||
|
if (!this._pageCache.has(pg)) {
|
||||||
|
//throw new Error(`No page ${pg} set.`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this._pageCache.get(pg)
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentPageObject() { return this.getPageWithoutLoading(this.currentPage) }
|
||||||
|
|
||||||
|
get isPageCacheEmpty() { return this._pageCache.size === 0 }
|
||||||
|
|
||||||
|
getAllPages() {
|
||||||
|
return Array.from(this._pageCache.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
getLoadedPages() {
|
||||||
|
return this.getAllPages().filter(i => i.loaded || i.hasError)
|
||||||
|
}
|
||||||
|
|
||||||
|
_preloadNextInSet() {
|
||||||
|
if (this._preloadSet.size > 0) {
|
||||||
|
this._preloading = true
|
||||||
|
const pg = [...this._preloadSet][0]
|
||||||
|
this._preloadSet.delete(pg)
|
||||||
|
this._loadPage(pg)
|
||||||
|
.catch((page) => { /*console.warn(`Preload failed for page ${pg}`)*/ })
|
||||||
|
.then(() => this._preloadNextInSet())
|
||||||
|
} else {
|
||||||
|
this._preloading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_preloadArray(pages) {
|
||||||
|
if (pages.length > 0) {
|
||||||
|
pages
|
||||||
|
.filter(pg => !this.getPageWithoutLoading(pg).loaded)
|
||||||
|
.forEach(pg => this._preloadSet.add(pg))
|
||||||
|
if (!this._preloading) {
|
||||||
|
return this._preloadNextInSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get preloadMax() {
|
||||||
|
return this.isUserGuest ? PRELOAD_MAX_GUEST : PRELOAD_MAX_USER
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: preload backwards iff [current, end] already loaded
|
||||||
|
preload(start = this.currentPage + 1, amount = this.settings.preloadPages) {
|
||||||
|
if (this.isPageCacheEmpty) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (amount == null) {
|
||||||
|
amount = 10
|
||||||
|
}
|
||||||
|
if (amount !== Infinity) {
|
||||||
|
amount = Utils.clamp(amount, 0, this.chapter.isNetworkServer ? this.totalPages : this.preloadMax)
|
||||||
|
}
|
||||||
|
start = Utils.clamp(start, 1, this.totalPages + 1)
|
||||||
|
const end = Utils.clamp(start + amount, 1, this.totalPages + 1)
|
||||||
|
return this._preloadArray(Utils.range(start, end))
|
||||||
|
}
|
||||||
|
|
||||||
|
preloadEverything() {
|
||||||
|
return this.preload(1, Infinity) // lol
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToPage(pg) {
|
||||||
|
if (!this.chapter || isNaN(pg)) {
|
||||||
|
return Promise.resolve()
|
||||||
|
} else if (pg <= -1) {
|
||||||
|
return this.moveToPage(this.totalPages)
|
||||||
|
} else if (pg === 0) {
|
||||||
|
return Promise.reject({ chapter: this.chapter.prevChapterId, page: -1 })
|
||||||
|
// return this.moveToChapter(this.chapter.prevChapterId, -1)
|
||||||
|
} else if (pg > this.totalPages) {
|
||||||
|
return Promise.reject({ chapter: this.chapter.nextChapterId, page: 1 })
|
||||||
|
// return this.moveToChapter(this.chapter.nextChapterId, 1)
|
||||||
|
} else {
|
||||||
|
if (this.currentPage === pg) {
|
||||||
|
this.trigger('currentpagechange', [pg])
|
||||||
|
} else {
|
||||||
|
this.setCurrentPage(pg)
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToChapter(id, pg = 1) {
|
||||||
|
if (id <= 0) {
|
||||||
|
return Promise.reject({ chapter: id })
|
||||||
|
} else {
|
||||||
|
return this.setChapter(id, pg).then(() => {
|
||||||
|
if (this.chapter && this.totalPages > 0 && this.isStateReading) {
|
||||||
|
return this.moveToPage(pg)
|
||||||
|
} else {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToRecommendations() {
|
||||||
|
this.isLoading = true
|
||||||
|
return Follows.load({}, false)
|
||||||
|
.then(recs => {
|
||||||
|
this.recommendations = recs
|
||||||
|
this.isLoading = false
|
||||||
|
this.state = ReaderModel.readerState.RECS
|
||||||
|
return Promise.resolve(recs)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.recommendations = null
|
||||||
|
this.isLoading = false
|
||||||
|
this.state = ReaderModel.readerState.ERROR
|
||||||
|
this.trigger('readererror', [err])
|
||||||
|
return Promise.reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRELOAD_MAX_USER = 20
|
||||||
|
const PRELOAD_MAX_GUEST = 5
|
||||||
|
|
||||||
|
ReaderModel.renderingModeState = {
|
||||||
|
SINGLE: 1,
|
||||||
|
DOUBLE: 2,
|
||||||
|
LONG: 3,
|
||||||
|
ALERT: 4,
|
||||||
|
RECS: 5,
|
||||||
|
}
|
||||||
|
ReaderModel.directionState = {
|
||||||
|
LTR: 1,
|
||||||
|
RTL: 2,
|
||||||
|
}
|
||||||
|
ReaderModel.displayFitState = {
|
||||||
|
FIT_BOTH: 1,
|
||||||
|
FIT_WIDTH: 2,
|
||||||
|
FIT_HEIGHT: 3,
|
||||||
|
NO_RESIZE: 4,
|
||||||
|
}
|
||||||
|
ReaderModel.readerState = {
|
||||||
|
ERROR: 0,
|
||||||
|
READING: 1,
|
||||||
|
RECS: 2,
|
||||||
|
EXITING: 3,
|
||||||
|
GAP: 4,
|
||||||
|
}
|
1067
scripts/reader/reader-view.js
Normal file
652
scripts/reader/renderer.js
Normal file
@ -0,0 +1,652 @@
|
|||||||
|
import { formatDistance } from 'date-fns'
|
||||||
|
import Utils from './utils'
|
||||||
|
import ReaderView from './reader-view'
|
||||||
|
import Chapter from './resource/Chapter'
|
||||||
|
import Manga from './resource/Manga'
|
||||||
|
|
||||||
|
export default class AbstractRenderer {
|
||||||
|
constructor(container, model, view) {
|
||||||
|
this.el = container
|
||||||
|
this.model = model
|
||||||
|
this.view = view
|
||||||
|
this._initialized = false
|
||||||
|
}
|
||||||
|
|
||||||
|
get chapter() { return this.model.chapter }
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
// console.log('initialize',this.name)
|
||||||
|
this._initialized = true
|
||||||
|
this.clearImageContainer()
|
||||||
|
this.renderedPages = 0
|
||||||
|
this._pageStateHandler = (page) => {
|
||||||
|
//console.info('pagestatehandler', this.name, page)
|
||||||
|
this.pageStateHandler(page)
|
||||||
|
}
|
||||||
|
this.model.on('pageloading', this._pageStateHandler)
|
||||||
|
this.model.on('pageload', this._pageStateHandler)
|
||||||
|
this.model.on('pageerror', this._pageStateHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (!this._initialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// console.log('destroy',this.name)
|
||||||
|
this._initialized = false
|
||||||
|
this.clearImageContainer()
|
||||||
|
this.model.off('pageloading', this._pageStateHandler)
|
||||||
|
this.model.off('pageload', this._pageStateHandler)
|
||||||
|
this.model.off('pageerror', this._pageStateHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
reinitialize() {
|
||||||
|
if (this._initialized) {
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
createAndAppendWrapper(page) {
|
||||||
|
return this.el.appendChild(this.updateWrapper(this.createWrapper(), page))
|
||||||
|
}
|
||||||
|
|
||||||
|
createWrapper() {
|
||||||
|
const wrapper = document.createElement('div')
|
||||||
|
const classes = [
|
||||||
|
'reader-image-wrapper',
|
||||||
|
'col-auto',
|
||||||
|
'my-auto',
|
||||||
|
'justify-content-center',
|
||||||
|
'align-items-center',
|
||||||
|
'noselect', 'nodrag',
|
||||||
|
'row', 'no-gutters',
|
||||||
|
]
|
||||||
|
wrapper.classList.add(...classes)
|
||||||
|
wrapper.dataset.state = 0
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWrapper(wrapper, page = {}) {
|
||||||
|
//console.log('update wrapper to', page.number, page)
|
||||||
|
if (page.state !== parseInt(wrapper.dataset.state)) {
|
||||||
|
while (wrapper.firstChild) {
|
||||||
|
wrapper.removeChild(wrapper.firstChild)
|
||||||
|
}
|
||||||
|
switch (page.state) {
|
||||||
|
case 1: wrapper.appendChild(this.createPageLoading()); break;
|
||||||
|
case 2: wrapper.appendChild(this.createPageLoaded()); break;
|
||||||
|
case 3: wrapper.appendChild(this.createPageError()); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wrapper.style.order = page.number || 0
|
||||||
|
wrapper.dataset.page = page.number || 0
|
||||||
|
wrapper.dataset.state = page.state || 0
|
||||||
|
switch (page.state) {
|
||||||
|
case 1: wrapper.querySelector('.loading-page-number').textContent = page.number; break;
|
||||||
|
case 2: wrapper.firstChild.src = page.image.src; break;
|
||||||
|
case 3: wrapper.querySelector('.alert .message').textContent = page.error.message; break;
|
||||||
|
}
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
createPageLoading() {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.classList.add('m-5', 'd-flex', 'align-items-center', 'justify-content-center')
|
||||||
|
container.style.color = '#fff'
|
||||||
|
container.style.textShadow = '0 0 7px rgba(0,0,0,0.5)'
|
||||||
|
const spinner = container.appendChild(document.createElement('span'))
|
||||||
|
spinner.classList.add('fas', 'fa-circle-notch', 'fa-spin', 'position-absolute')
|
||||||
|
spinner.style.opacity = '0.5'
|
||||||
|
spinner.style.fontSize = '7em'
|
||||||
|
const pgNum = container.appendChild(document.createElement('span'))
|
||||||
|
pgNum.classList.add('loading-page-number')
|
||||||
|
pgNum.style.fontSize = '2em'
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
createPageLoaded() {
|
||||||
|
const container = document.createElement('img')
|
||||||
|
container.draggable = false
|
||||||
|
container.classList.add('noselect', 'nodrag', 'cursor-pointer')
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
createPageError() {
|
||||||
|
const container = Alert.container('', 'danger')
|
||||||
|
const tapMsg = container.appendChild(document.createElement('div'))
|
||||||
|
tapMsg.innerHTML = "Tap to reload."
|
||||||
|
container.addEventListener('click', evt => {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
const page = this.model.getPageWithoutLoading(parseInt(container.parentElement.dataset.page))
|
||||||
|
page.reload(true).catch(console.error)
|
||||||
|
})
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
createMangaError(chapter) {
|
||||||
|
const container = Alert.container('', 'danger')
|
||||||
|
const tapMsg = container.appendChild(document.createElement('div'))
|
||||||
|
tapMsg.innerHTML = "Tap to reload."
|
||||||
|
container.addEventListener('click', evt => {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
container.parentElement.removeChild(container)
|
||||||
|
this.model.isLoading = true
|
||||||
|
chapter.loadManga(true)
|
||||||
|
.then(chapter => {
|
||||||
|
this.model.isLoading = false
|
||||||
|
this.model.setChapter(chapter.id)
|
||||||
|
.then(() => { this.view.moveToPage(1, false) })
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.model.isLoading = false
|
||||||
|
this.model.trigger('readererror', [err])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
clearImageContainer() {
|
||||||
|
while (this.el && this.el.firstChild) {
|
||||||
|
this.el.removeChild(this.el.firstChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
throw new Error("Not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
pageStateHandler() {
|
||||||
|
throw new Error("Not implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SinglePage extends AbstractRenderer {
|
||||||
|
get name() { return 'single-page' }
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
super.initialize()
|
||||||
|
this.renderedPages = 1
|
||||||
|
this.pageToRender = null
|
||||||
|
this.pageWrapper = this.createAndAppendWrapper()
|
||||||
|
}
|
||||||
|
|
||||||
|
pageStateHandler(page) {
|
||||||
|
if (this.pageToRender === page) {
|
||||||
|
this.updateWrapper(this.pageWrapper, page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(pg) {
|
||||||
|
const page = this.model.getPageWithoutLoading(pg)
|
||||||
|
this.pageToRender = page
|
||||||
|
this.updateWrapper(this.pageWrapper, page)
|
||||||
|
return page.load().catch(p => Promise.resolve(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DoublePage extends AbstractRenderer {
|
||||||
|
get name() { return 'double-page' }
|
||||||
|
|
||||||
|
get renderedPages() { return this.pagesToRender.length }
|
||||||
|
set renderedPages(v) { }
|
||||||
|
|
||||||
|
isPageTurnForwards() { return this.previousPage < this.model.currentPage }
|
||||||
|
isSinglePageBackwards() { return this.previousPage === this.model.currentPage + 1 }
|
||||||
|
isImageTooWide(img) {
|
||||||
|
return img && img.naturalWidth > img.naturalHeight && img.naturalWidth > this.el.offsetWidth / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
super.initialize()
|
||||||
|
this.pageWrapperLoading = this.createAndAppendWrapper({ state: 1, number: '', })
|
||||||
|
this.previousPage = 0
|
||||||
|
this.pageWrappers = [this.createAndAppendWrapper(), this.createAndAppendWrapper()]
|
||||||
|
this.pagesToRender = []
|
||||||
|
this.setLoading(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pageStateHandler(page) {
|
||||||
|
if (this.pagesToRender.includes(page)) {
|
||||||
|
this.checkRender()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(pg) {
|
||||||
|
this.pagesToRender = [pg, pg + 1]
|
||||||
|
.map(p => this.model.getPageWithoutLoading(p))
|
||||||
|
.filter(p => p)
|
||||||
|
|
||||||
|
this.checkRender()
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
this.pagesToRender.map(page =>
|
||||||
|
page.load().catch(p => Promise.resolve(p))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkRender() {
|
||||||
|
const pagesDone = this.pagesToRender.every(p => p.isDone)
|
||||||
|
if (pagesDone) {
|
||||||
|
if (this.pagesToRender.length > 1 && this.pagesToRender.some(p => this.isImageTooWide(p.image))) {
|
||||||
|
if (this.isPageTurnForwards() || this.isSinglePageBackwards()) {
|
||||||
|
this.pagesToRender.pop()
|
||||||
|
} else {
|
||||||
|
this.pagesToRender.shift()
|
||||||
|
this.model.setCurrentPage(this.pagesToRender[0].number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.updateWrapper(this.pageWrappers[0], this.pagesToRender[0])
|
||||||
|
this.updateWrapper(this.pageWrappers[1], this.pagesToRender[1])
|
||||||
|
this.previousPage = this.model.currentPage
|
||||||
|
}
|
||||||
|
this.setLoading(!pagesDone)
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(state) {
|
||||||
|
this.pageWrapperLoading.classList.toggle('d-none', !state)
|
||||||
|
for (let wrapper of this.pageWrappers) {
|
||||||
|
wrapper.classList.toggle('d-none', state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LongStrip extends AbstractRenderer {
|
||||||
|
get name() { return 'long-strip' }
|
||||||
|
get renderedPages() { return this._renderedPageSet.length }
|
||||||
|
set renderedPages(v) { }
|
||||||
|
get lastRenderedPage() { return this._renderedPageSet[this._renderedPageSet.length - 1] }
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
super.initialize()
|
||||||
|
this._pageWrapperMap = new Map()
|
||||||
|
this._renderedPageSet = []
|
||||||
|
this._scrollY = -1
|
||||||
|
|
||||||
|
this.observer = new MutationObserver((mutationsList) => {
|
||||||
|
// this is horrible
|
||||||
|
if (this._scrollY === -1 && this.model.currentPage !== 1) {
|
||||||
|
this._scrollY = -2
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.getPageWrapper(this.model.currentPage).scrollIntoView(true)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this._scrollY = window.pageYOffset || -1
|
||||||
|
// console.log('did it',this._scrollY)
|
||||||
|
if (this._scrollY !== -1) {
|
||||||
|
ReaderView.scroll(0, -document.querySelector('nav.navbar').offsetHeight + 1)
|
||||||
|
this.observer.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
//this.scrollToPage(this.model.currentPage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.observer.observe(this.el, { childList: true, subtree: true })
|
||||||
|
|
||||||
|
for (let page of this.model.getAllPages()) {
|
||||||
|
this._pageWrapperMap.set(page.number, this.createAndAppendWrapper(page))
|
||||||
|
if (page.isDone) {
|
||||||
|
this._renderedPageSet.push(page.number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Utils.stableSort(this._renderedPageSet)
|
||||||
|
this.renderEndBlock()
|
||||||
|
|
||||||
|
this.render(this.model.currentPage)
|
||||||
|
|
||||||
|
this.addScrollHandler()
|
||||||
|
this._currentPageHandler = (pg) => {
|
||||||
|
this.render(pg + 1)
|
||||||
|
.then(() => {
|
||||||
|
this.render(pg - 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.model.on('currentpagechange', this._currentPageHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (!this._initialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
super.destroy()
|
||||||
|
this.observer.disconnect()
|
||||||
|
//this.el.scrollIntoView(true)
|
||||||
|
this._pageWrapperMap.clear()
|
||||||
|
this.removeScrollHandler()
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
this.model.off('currentpagechange', this._currentPageHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
pageStateHandler(page) {
|
||||||
|
this.updateWrapper(this.getPageWrapper(page.number), page)
|
||||||
|
if (page.isDone || page.loading) {
|
||||||
|
if (!this.isRendered(page.number)) {
|
||||||
|
this._renderedPageSet.push(page.number)
|
||||||
|
Utils.stableSort(this._renderedPageSet)
|
||||||
|
}
|
||||||
|
if (this.isChapterFullyRendered) {
|
||||||
|
this.showEndBlock()
|
||||||
|
}
|
||||||
|
if (this._scrollY >= 0) {
|
||||||
|
this.updateCurrentPage()
|
||||||
|
} //else {
|
||||||
|
// this._scrollY = -2
|
||||||
|
// requestAnimationFrame(() => {
|
||||||
|
// this.getPageWrapper(this.model.currentPage).scrollIntoView(true)
|
||||||
|
// requestAnimationFrame(() => {
|
||||||
|
// ReaderView.scroll(0, -document.querySelector('nav.navbar').offsetHeight + 1)
|
||||||
|
// requestAnimationFrame(() => {
|
||||||
|
// this._scrollY = window.pageYOffset || -1
|
||||||
|
// console.log('did it',this._scrollY)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
//this.scrollToPage(this.model.currentPage)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageWrapper(pg) {
|
||||||
|
if (!this._pageWrapperMap.has(pg)) {
|
||||||
|
throw new Error("No wrapper for page ", pg)
|
||||||
|
}
|
||||||
|
return this._pageWrapperMap.get(pg)
|
||||||
|
}
|
||||||
|
|
||||||
|
isRendered(pg) {
|
||||||
|
return this._renderedPageSet.includes(pg)
|
||||||
|
}
|
||||||
|
|
||||||
|
get isChapterFullyRendered() {
|
||||||
|
return this.renderedPages === this.model.totalPages
|
||||||
|
}
|
||||||
|
|
||||||
|
render(pg) {
|
||||||
|
if (!this.isChapterFullyRendered && !this.isRendered(pg)) {
|
||||||
|
return this.model.getPage(pg)
|
||||||
|
.catch(p => Promise.resolve(p))
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEndBlock() {
|
||||||
|
this._endBlock = this.createAndAppendWrapper({
|
||||||
|
number: this.model.totalPages + 1,
|
||||||
|
chapter: this.model.chapter.id,
|
||||||
|
})
|
||||||
|
this._endBlock.textContent = 'End of chapter / Go to next'
|
||||||
|
this._endBlock.classList.add('reader-image-block', 'py-3', 'd-none')
|
||||||
|
this._endBlock.addEventListener('click', (evt) => {
|
||||||
|
evt.stopPropagation()
|
||||||
|
this.view.moveToChapter(this.model.chapter.nextChapterId)
|
||||||
|
}, { once: true })
|
||||||
|
if (this.isChapterFullyRendered) {
|
||||||
|
this.showEndBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showEndBlock() {
|
||||||
|
this._endBlock.classList.remove('d-none')
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentPage() {
|
||||||
|
if (this.renderedPages > 0 && !this._updating) {
|
||||||
|
this._updating = true
|
||||||
|
if (ReaderView.isScrolledToTop) {
|
||||||
|
this.model.setCurrentPage(this._renderedPageSet[0])
|
||||||
|
this.view.replaceHistory()
|
||||||
|
} else if (ReaderView.isScrolledToBottom) {
|
||||||
|
this.model.setCurrentPage(this.lastRenderedPage)
|
||||||
|
this.view.replaceHistory()
|
||||||
|
} else {
|
||||||
|
const scrollY = Math.floor(window.pageYOffset)
|
||||||
|
for (let i = this._renderedPageSet.length - 1; i >= 0; --i) {
|
||||||
|
const pg = this._renderedPageSet[i]
|
||||||
|
const wrapper = this.getPageWrapper(pg)
|
||||||
|
if (scrollY >= wrapper.offsetTop) {
|
||||||
|
if (this.model.setCurrentPage(pg)) {
|
||||||
|
this.view.replaceHistory()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._updating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToPage(pg) {
|
||||||
|
// requestAnimationFrame(() => {
|
||||||
|
const wrapper = this.getPageWrapper(pg)
|
||||||
|
if (this.isRendered(pg) && wrapper) {
|
||||||
|
// console.log('scrolling to', pg, wrapper.offsetTop + 1)
|
||||||
|
//window.scrollTo(window.pageXOffset, wrapper.offsetTop + 1)
|
||||||
|
wrapper.scrollIntoView(true)
|
||||||
|
if (!ReaderView.isScrolledToBottom) {
|
||||||
|
ReaderView.scroll(0, -document.querySelector('nav.navbar').offsetHeight + 1)
|
||||||
|
}
|
||||||
|
// requestAnimationFrame(() => {
|
||||||
|
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
|
||||||
|
addScrollHandler() {
|
||||||
|
if (!this._scrollHandler) {
|
||||||
|
const update = () => {
|
||||||
|
if (this.model.chapter) {
|
||||||
|
this.updateCurrentPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Modernizr.requestanimationframe) {
|
||||||
|
let wait = false
|
||||||
|
this._scrollHandler = () => {
|
||||||
|
if (!wait) {
|
||||||
|
wait = true
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
update()
|
||||||
|
wait = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._scrollHandler = () => {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', this._scrollHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeScrollHandler() {
|
||||||
|
if (this._scrollHandler) {
|
||||||
|
window.removeEventListener('scroll', this._scrollHandler)
|
||||||
|
this._scrollHandler = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class Alert extends AbstractRenderer {
|
||||||
|
get name() { return 'alert' }
|
||||||
|
|
||||||
|
pageStateHandler() { }
|
||||||
|
|
||||||
|
renderChapterButtons(data) {
|
||||||
|
const chBtnContainer = this.el.appendChild(document.createElement('div'))
|
||||||
|
chBtnContainer.classList.add('row', 'm-auto', 'justify-content-center', 'directional')
|
||||||
|
const buttons = [
|
||||||
|
{ text: 'Previous chapter', id: data.prevChapterId, order: 1 },
|
||||||
|
{ text: 'Next chapter', id: data.nextChapterId, order: 2 },
|
||||||
|
]
|
||||||
|
const classes = ['col-auto', 'hover', 'text-dark']
|
||||||
|
for (let btn of buttons) {
|
||||||
|
const link = chBtnContainer.appendChild(Alert.container(btn.text, 'dark', 'a'))
|
||||||
|
link.setAttribute('href', this.view.pageURL(btn.id))
|
||||||
|
link.dataset.action = 'chapter'
|
||||||
|
link.dataset.chapter = btn.id
|
||||||
|
link.classList.add(...classes)
|
||||||
|
link.classList.replace('m-auto', 'm-1')
|
||||||
|
link.style.order = btn.order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(data) {
|
||||||
|
this.clearImageContainer()
|
||||||
|
if (typeof data !== 'object') {
|
||||||
|
return Promise.reject({ message: "Data is not an object", data: data, revert: true })
|
||||||
|
}
|
||||||
|
if (data.isExternal) {
|
||||||
|
this.el.appendChild(Alert.container(`This chapter can be read for free on the official publisher's website.<br>Feel free to write your comments about it here on MangaDex!`, 'info'))
|
||||||
|
const link = Alert.container(`${Alert.icon('external-link-alt', 'Website')} <strong>Read the chapter</strong>`, 'success', 'a', false)
|
||||||
|
link.target = '_blank'
|
||||||
|
link.rel = 'noopener noreferrer'
|
||||||
|
link.href = data.pages
|
||||||
|
this.el.appendChild(link)
|
||||||
|
this.renderChapterButtons(data)
|
||||||
|
} else if (data.isDelayed) {
|
||||||
|
const now = new Date()
|
||||||
|
const release = new Date(data.timestamp * 1000)
|
||||||
|
const relativeDate = release > now ? formatDistance(release, now, { addSuffix: true }) : 'within a few minutes'
|
||||||
|
this.el.appendChild(Alert.container(`Due to the group's delay policy, this chapter will be available ${relativeDate}.`, 'danger'))
|
||||||
|
this.el.appendChild(Alert.container(`You might be able to read it on the group's <a target='_blank' rel='noopener noreferrer' href='${data.groupWebsite}'>${Alert.icon('external-link-alt', 'Website')} <strong>website</strong></a>.`, 'info'))
|
||||||
|
this.renderChapterButtons(data)
|
||||||
|
} else if (data.isSpoilerNet) {
|
||||||
|
const alert = document.createElement('div')
|
||||||
|
alert.classList.add('alert', `alert-warning`, 'text-center', 'm-auto')
|
||||||
|
alert.attributes.role = 'alert'
|
||||||
|
alert.innerHTML = `<h3>${Alert.icon('warning')} Spoiler Warning</h3><div class="my-3"><div>There seems to be a gap between chapters (${Chapter.getResource(data.prevChapterId).fullTitle} → ${Chapter.getResource(data.chapterId).fullTitle}).</div><div>This may be an attempt to troll you into reading a chapter early.</div>`
|
||||||
|
const button = document.createElement('button')
|
||||||
|
button.classList.add('btn', 'btn-secondary')
|
||||||
|
button.type = 'button'
|
||||||
|
button.textContent = "I understand, I'm fine with spoilers!"
|
||||||
|
button.addEventListener('click', (evt) => {
|
||||||
|
evt.stopPropagation()
|
||||||
|
this.view.moveToChapter(data.chapterId, 1, true, true)
|
||||||
|
})
|
||||||
|
alert.appendChild(button)
|
||||||
|
this.el.appendChild(alert)
|
||||||
|
} else if (data.isNotFound) {
|
||||||
|
this.el.appendChild(Alert.container(`Data not found${data.message ? ': ' + data.message : '.'}`, 'danger'))
|
||||||
|
} else if (data.isDeleted) {
|
||||||
|
this.el.appendChild(Alert.container(`This chapter has been deleted.`, 'danger'))
|
||||||
|
} else if (data.isRestricted) {
|
||||||
|
this.el.appendChild(Alert.container(`This chapter is unavailable.`, 'danger'))
|
||||||
|
} else if (data.isMangaFailed) {
|
||||||
|
const alert = this.createMangaError(data)
|
||||||
|
alert.querySelector('.message').textContent = "The manga data failed to load."
|
||||||
|
alert.draggable = false
|
||||||
|
alert.classList.add('noselect', 'nodrag', 'cursor-pointer')
|
||||||
|
this.el.appendChild(alert)
|
||||||
|
} else if (data.status === 'unavailable') {
|
||||||
|
this.el.appendChild(Alert.container(`This chapter is unavailable.`, 'danger'))
|
||||||
|
} else if (data != null) {
|
||||||
|
const isError = data instanceof Error || data.error instanceof Error
|
||||||
|
const type = data.type || isError ? 'danger' : undefined
|
||||||
|
const msg = data.message || data.error && data.error.message || data.error && data.error.status || data
|
||||||
|
this.el.appendChild(Alert.container(msg, type))
|
||||||
|
// if (isError) {
|
||||||
|
// this.el.appendChild(Alert.container('dark', data.stack || data.error.stack))
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
static icon(type, title) {
|
||||||
|
type = Alert.iconTypes[type] || type
|
||||||
|
return `<span class='fas fa-${type} fa-fw' aria-hidden='true'${title ? ` title=${title}` : ''}></span> `
|
||||||
|
}
|
||||||
|
|
||||||
|
static get iconTypes() {
|
||||||
|
return {
|
||||||
|
success: 'check-circle',
|
||||||
|
danger: 'times',
|
||||||
|
info: 'info',
|
||||||
|
warning: 'exclamation-triangle',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static container(message = '', type = 'dark', element = 'div', icon = true) {
|
||||||
|
const div = document.createElement(element)
|
||||||
|
div.classList.add('alert', `alert-${type}`, 'text-center', 'm-auto')
|
||||||
|
div.attributes.role = 'alert'
|
||||||
|
if (icon && Alert.iconTypes[type]) {
|
||||||
|
div.innerHTML = Alert.icon(type)
|
||||||
|
}
|
||||||
|
const span = div.appendChild(document.createElement('span'))
|
||||||
|
span.classList.add('message')
|
||||||
|
span.innerHTML = message
|
||||||
|
return div
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Recommendations extends AbstractRenderer {
|
||||||
|
get name() { return 'recommendations' }
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.model.recommendations) {
|
||||||
|
this.el.innerHTML = ''
|
||||||
|
return this.el.appendChild(Alert.container("No recommendations found. You must be logged in and have followed some titles.", "danger"))
|
||||||
|
}
|
||||||
|
let recStr = ''
|
||||||
|
for (let [manga, chapters] of this.model.recommendations.unreadChaptersGroupedByManga) {
|
||||||
|
if (chapters.length > 0) {
|
||||||
|
const chapter = chapters[chapters.length - 1]
|
||||||
|
const more = chapters.length >= 2 ? ` <em class="ml-1">(+${chapters.length - 1} more)</em>` : ''
|
||||||
|
const relativeDate = formatDistance(new Date(chapter.timestamp * 1000), new Date(), { addSuffix: true })
|
||||||
|
recStr += `
|
||||||
|
<div class="col-xl-3 col-md-4 col-sm-6 border-bottom p-2 text-left">
|
||||||
|
<div class="rounded sm_md_logo float-left mr-2">
|
||||||
|
<a href="${manga.url}">
|
||||||
|
<img class="rounded max-width" src="${manga.coverThumb}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-truncate py-0 mb-1 border-bottom">
|
||||||
|
<span class="fas fa-book fa-fw" aria-hidden="true" title=""></span>
|
||||||
|
<a class="manga_title" title="${manga.title}" href="${manga.url}">${manga.title}</a>
|
||||||
|
</div>
|
||||||
|
<p class="py-0 mb-1 row no-gutters align-items-center flex-nowrap">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="col-auto px-0">
|
||||||
|
<span class="rounded flag flag-${chapter.language}"></span>
|
||||||
|
</span>
|
||||||
|
<a class="" href="${chapter.url}" data-chapter="${chapter.id}">${chapter.fullTitle}</a>
|
||||||
|
${more}
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p class="text-truncate py-0 mb-1"><span class="far fa-clock fa-fw " aria-hidden="true" title=""></span> ${relativeDate}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.el.innerHTML = `<h2 class="text-left">Recommendations</h2><h3 class="text-left">Unread Follows</h3><div class="row no-gutters">${recStr}</div>`
|
||||||
|
const handler = (evt) => {
|
||||||
|
const chapter = evt.target.dataset.chapter || evt.currentTarget.dataset.chapter
|
||||||
|
if (chapter) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
this.view.moveToChapter(parseInt(chapter), 1)
|
||||||
|
// this.model.setRenderer(this.model.settings.renderingMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.el.querySelectorAll('a').forEach(c => c.addEventListener('click', handler, true))
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
pageStateHandler() { }
|
||||||
|
|
||||||
|
getChapterTitle(ch, numOnly) {
|
||||||
|
let title = ''
|
||||||
|
if (ch.volume) title += `Vol. ${ch.volume} `
|
||||||
|
if (ch.chapter) title += `Ch. ${ch.chapter} `
|
||||||
|
if (ch.title && !numOnly) title += `${ch.title}`
|
||||||
|
if (!title) title = 'Oneshot'
|
||||||
|
return title.trim()
|
||||||
|
}
|
||||||
|
}
|
158
scripts/reader/resource/Chapter.js
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import Resource from './Resource'
|
||||||
|
import Manga from './Manga'
|
||||||
|
import Group from './Group'
|
||||||
|
|
||||||
|
const VOL_SHORT = 'Vol.'
|
||||||
|
const CH_SHORT = 'Ch.'
|
||||||
|
const TITLE_ONESHOT = 'Oneshot'
|
||||||
|
const TITLE_EMPTY = '(Untitled)'
|
||||||
|
|
||||||
|
export default class Chapter extends Resource {
|
||||||
|
static get resourceType() { return 'chapter' }
|
||||||
|
|
||||||
|
constructor(data = {}, responseCode = -1, responseStatus = null) {
|
||||||
|
super(data, responseCode, responseStatus)
|
||||||
|
this.id = data.id
|
||||||
|
this.hash = data.hash
|
||||||
|
this.mangaId = data.mangaId
|
||||||
|
this.chapter = data.chapter
|
||||||
|
this.volume = data.volume
|
||||||
|
this.title = data.title
|
||||||
|
this.timestamp = data.timestamp
|
||||||
|
this.language = data.language
|
||||||
|
this.threadId = data.threadId || null
|
||||||
|
this.comments = data.comments
|
||||||
|
this.server = data.server
|
||||||
|
this.serverFallback = data.serverFallback
|
||||||
|
this.groupIds = (data.groups || []).map(g => g.id || g)
|
||||||
|
this.groupWebsite = data.groupWebsite
|
||||||
|
this.pages = data.pages || []
|
||||||
|
this.read = data.read || false
|
||||||
|
this.status = data.status
|
||||||
|
}
|
||||||
|
|
||||||
|
// derived data
|
||||||
|
get textTitle() {
|
||||||
|
return this.title || ((this.volume || this.chapter) ? TITLE_EMPTY : TITLE_ONESHOT)
|
||||||
|
}
|
||||||
|
get numberTitle() {
|
||||||
|
return [
|
||||||
|
this.volume ? `${VOL_SHORT} ${this.volume}` : '',
|
||||||
|
this.chapter ? `${CH_SHORT} ${this.chapter}` : '',
|
||||||
|
].filter(n => n).join(' ')
|
||||||
|
}
|
||||||
|
get fullTitle() {
|
||||||
|
return [
|
||||||
|
this.numberTitle,
|
||||||
|
this.title,
|
||||||
|
].filter(n => n).join(' ') || TITLE_ONESHOT
|
||||||
|
}
|
||||||
|
get manga() { return Manga.getResource(this.mangaId) }
|
||||||
|
get groups() { return this.groupIds.map(id => Group.getResource(id)) }
|
||||||
|
get totalPages() { return !this.isExternal ? this.pages.length : 1 }
|
||||||
|
get url() { return `/chapter/${this.id}` }
|
||||||
|
get isNotFound() { return this._response.code === 404 }
|
||||||
|
get isDeleted() { return this.status === 'deleted' }
|
||||||
|
get isDelayed() { return this.status === 'delayed' }
|
||||||
|
get isUnavailable() { return this.status === 'unavailable' }
|
||||||
|
get isRestricted() { return this.status === 'restricted' }
|
||||||
|
get isExternal() { return this.status === 'external' }
|
||||||
|
get isFullyLoaded() { return this.hash != null && this.server != null }
|
||||||
|
get isUnnumbered() { return !this.volume && !this.chapter }
|
||||||
|
get isLastChapter() { return this.manga && this.manga.isLastChapter(this) }
|
||||||
|
get nextChapterId() { if (this.manga) return this.manga.getNextChapterId(this.id) }
|
||||||
|
get prevChapterId() { if (this.manga) return this.manga.getPrevChapterId(this.id) }
|
||||||
|
get nextChapter() { return Chapter.getResource(this.nextChapterId) }
|
||||||
|
get prevChapter() { return Chapter.getResource(this.prevChapterId) }
|
||||||
|
get isNetworkServer() {
|
||||||
|
if (this._isNetworkServer == null) {
|
||||||
|
this._isNetworkServer = /mangadex\.network/.test(this.server || '')
|
||||||
|
}
|
||||||
|
return this._isNetworkServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// methods
|
||||||
|
getPage(i) {
|
||||||
|
return (i >= 1 && i <= this.totalPages && !this.isExternal) ? this.pages[i - 1] : ''
|
||||||
|
}
|
||||||
|
getPageUrl(i) {
|
||||||
|
return this.server + this.hash + '/' + this.getPage(i)
|
||||||
|
}
|
||||||
|
getAllPageUrls() {
|
||||||
|
function serverOrUndefined(server, hash, pg) {
|
||||||
|
if (!server) {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
return server + hash + '/' + pg;
|
||||||
|
}
|
||||||
|
return !this.isExternal ? this.pages.map(pg => [serverOrUndefined(this.server, this.hash, pg), serverOrUndefined(this.serverFallback, this.hash, pg)]) : []
|
||||||
|
}
|
||||||
|
|
||||||
|
isAlternativeOf(ch) {
|
||||||
|
return ch.chapter == this.chapter
|
||||||
|
&& (ch.volume == this.volume || ch.volume == '' || this.volume == '')
|
||||||
|
&& (!this.isUnnumbered || ch.title == this.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
isSequentialWith(chapter) {
|
||||||
|
if (!chapter) {
|
||||||
|
return undefined
|
||||||
|
} else if (typeof chapter == 'number') {
|
||||||
|
chapter = Chapter.getResource(chapter)
|
||||||
|
}
|
||||||
|
const ch1 = parseFloat(this.chapter)
|
||||||
|
const ch2 = parseFloat(chapter.chapter)
|
||||||
|
const vol1 = parseInt(this.volume) || 0
|
||||||
|
const vol2 = parseInt(chapter.volume) || 0
|
||||||
|
if (isNaN(ch1) || isNaN(ch2) || this.isUnnumbered || chapter.isUnnumbered) {
|
||||||
|
return undefined
|
||||||
|
} else if (this.isAlternativeOf(chapter)) {
|
||||||
|
return undefined
|
||||||
|
} else if (Math.abs(ch1 - ch2).toFixed(1) <= 1.1) {
|
||||||
|
return true
|
||||||
|
} else if ((ch1 === 1 && vol1 === vol2 + 1) || (ch2 === 1 && vol2 === vol1 + 1)) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSameGroupsWith(chapter) {
|
||||||
|
if (this === chapter) {
|
||||||
|
return true
|
||||||
|
} else if (this.groups.length !== chapter.groups.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < this.groups.length; ++i) {
|
||||||
|
if (this.groups[i].id !== chapter.groups[i].id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
static async load(id, params = {}, cache = true) {
|
||||||
|
const json = await super.load(`chapter/${id}`, params)
|
||||||
|
try {
|
||||||
|
json.data.groups.forEach(g => Group.fromJSON(g))
|
||||||
|
return this.fromJSON(json.data, json.code, json.status, cache)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw this.fromJSON({ id, status: json.message }, json.code, json.status, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async loadWithManga(id, params = {}, cache = true) {
|
||||||
|
const json = await super.load(`chapter/${id}`, Object.assign(params, { include: 'manga' }))
|
||||||
|
try {
|
||||||
|
json.data.chapter.groups.forEach(g => Group.fromJSON(g))
|
||||||
|
const chapter = this.fromJSON(json.data.chapter, json.code, json.status, cache)
|
||||||
|
Manga.fromJSON(json.data.manga, json.code, json.status, cache)
|
||||||
|
return chapter
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw this.fromJSON({ id, status: json.message }, json.code, json.status, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
36
scripts/reader/resource/Follows.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import Cookies from 'js-cookie'
|
||||||
|
import Resource from './Resource'
|
||||||
|
import Chapter from './Chapter'
|
||||||
|
import Manga from './Manga'
|
||||||
|
|
||||||
|
export default class Follows extends Resource {
|
||||||
|
static get resourceType() { return 'follows' }
|
||||||
|
|
||||||
|
constructor(data = {}, responseCode = -1, responseStatus = null) {
|
||||||
|
super(data, responseCode, responseStatus)
|
||||||
|
this.chapters = data.chapters.map(c => new Chapter(c)).sort((a, b) => b.timestamp - a.timestamp)
|
||||||
|
this.manga = new Map(Object.values(data.manga).map(m => [m.id, new Manga(m)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
get unreadChapters() {
|
||||||
|
return this.chapters.filter(c => !c.read)
|
||||||
|
}
|
||||||
|
|
||||||
|
get unreadChaptersGroupedByManga() {
|
||||||
|
return this.unreadChapters.reduce((map, ch) => {
|
||||||
|
const manga = this.manga.get(ch.mangaId)
|
||||||
|
if (!map.has(manga)) {
|
||||||
|
map.set(manga, [])
|
||||||
|
}
|
||||||
|
map.get(manga).push(ch)
|
||||||
|
return map
|
||||||
|
}, new Map())
|
||||||
|
}
|
||||||
|
|
||||||
|
static async load(params = {}, cache = true) {
|
||||||
|
const type = 1 // Reading
|
||||||
|
const hentai = Cookies.get('mangadex_h_toggle')
|
||||||
|
const json = await super.load(`user/me/followed-updates`, Object.assign({ type, hentai }, params)) // type 1 = Reading
|
||||||
|
return this.fromJSON(json.data, json.code, json.status, cache)
|
||||||
|
}
|
||||||
|
}
|
15
scripts/reader/resource/Group.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import Resource from './Resource'
|
||||||
|
export default class Group extends Resource {
|
||||||
|
static get resourceType() { return 'group' }
|
||||||
|
|
||||||
|
constructor(data = {}, responseCode = -1, responseStatus = null) {
|
||||||
|
super(data, responseCode, responseStatus)
|
||||||
|
this.id = data.id
|
||||||
|
this.name = data.name
|
||||||
|
}
|
||||||
|
|
||||||
|
static async load(id, cache = true) {
|
||||||
|
const json = await super.load(`group/${id}`, {})
|
||||||
|
return this.fromJSON(json.data, json.code, json.status, cache)
|
||||||
|
}
|
||||||
|
}
|
165
scripts/reader/resource/Manga.js
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { differenceInHours } from 'date-fns'
|
||||||
|
import Resource from './Resource'
|
||||||
|
import Chapter from './Chapter'
|
||||||
|
import Group from './Group'
|
||||||
|
import Utils from '../utils'
|
||||||
|
import natsort from 'natsort'
|
||||||
|
|
||||||
|
|
||||||
|
export default class Manga extends Resource {
|
||||||
|
static get resourceType() { return 'manga' }
|
||||||
|
|
||||||
|
constructor(data = {}, responseCode = -1, responseStatus = null) {
|
||||||
|
super(data, responseCode, responseStatus)
|
||||||
|
this.id = data.id
|
||||||
|
this.title = data.title
|
||||||
|
this.language = data.publication ? data.publication.language : null
|
||||||
|
this.isHentai = data.isHentai
|
||||||
|
this.lastChapter = data.lastChapter || ''
|
||||||
|
this.lastVolume = data.lastVolume || ''
|
||||||
|
this.mainCover = data.mainCover
|
||||||
|
this.tags = data.tags || []
|
||||||
|
this.links = data.links || {}
|
||||||
|
this._uniqueChapters = []
|
||||||
|
this._chapters = []
|
||||||
|
this.chapterListLastLoaded = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// derived data
|
||||||
|
get isLongStrip() { return this.tags.indexOf(36) !== -1 }
|
||||||
|
get isDoujinshi() { return this.tags.indexOf(7) !== -1 }
|
||||||
|
get url() {
|
||||||
|
const title = this.title.toLowerCase().replace(/&[0-9a-z]+;/gi, '').replace(/[^0-9a-z]/gi, ' ').split(' ').filter(t => t).join('-')
|
||||||
|
return `/title/${this.id}/${title}`
|
||||||
|
}
|
||||||
|
get coverThumb() { return `/images/manga/${this.id}.thumb.jpg` }
|
||||||
|
get isChapterListOutdated() { return differenceInHours(this.chapterListLastLoaded, new Date()) >= 1 }
|
||||||
|
|
||||||
|
// methods
|
||||||
|
isLastChapter(chapter) {
|
||||||
|
return this.lastChapter && (chapter.chapter === this.lastChapter) && (chapter.volume == this.lastVolume)
|
||||||
|
}
|
||||||
|
|
||||||
|
getChapter(id) {
|
||||||
|
return Chapter.getResource(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
get chapterList() {
|
||||||
|
return this._chapters
|
||||||
|
}
|
||||||
|
|
||||||
|
get uniqueChapterList() {
|
||||||
|
return this._uniqueChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChapterList(baseChapter, restrictChLang = false) {
|
||||||
|
let chapters = Chapter.findResources(c => c.mangaId === this.id)
|
||||||
|
if (restrictChLang) {
|
||||||
|
chapters = chapters.filter(c => c.language === baseChapter.language)
|
||||||
|
}
|
||||||
|
const chapterIds = Manga.createSortedChapterIdList(chapters)
|
||||||
|
this._chapters = chapterIds.map(id => Chapter.getResource(id))
|
||||||
|
this._uniqueChapters = Manga.createUniqueChapterList(baseChapter, this._chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextChapterId(id) {
|
||||||
|
const index = this._uniqueChapters.map(c => c.id).indexOf(id)
|
||||||
|
return (index >= 0 && index + 1 < this._uniqueChapters.length) ? this._uniqueChapters[index + 1].id : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrevChapterId(id) {
|
||||||
|
const index = this._uniqueChapters.map(c => c.id).indexOf(id)
|
||||||
|
return (index > 0) ? this._uniqueChapters[index - 1].id : -1
|
||||||
|
}
|
||||||
|
|
||||||
|
getAltChapters(id) {
|
||||||
|
const cur = Chapter.getResource(id)
|
||||||
|
return cur ? this.chapterList.filter(ch => cur.isAlternativeOf(ch)) : []
|
||||||
|
}
|
||||||
|
|
||||||
|
static createSortedChapterIdList(chapters) {
|
||||||
|
chapters = chapters.map(ch => {
|
||||||
|
return !ch ? null : {
|
||||||
|
id: ch.id,
|
||||||
|
timestamp: ch.timestamp,
|
||||||
|
chapter: ch.chapter,
|
||||||
|
volume: ch.volume,
|
||||||
|
group: ch.groupIds[0],
|
||||||
|
}
|
||||||
|
}).filter(c => c)
|
||||||
|
const sorter = natsort({ asc: true, insensitive: true })
|
||||||
|
// sort by timestamp desc
|
||||||
|
Utils.stableSort(chapters, (a, b) => sorter(b.timestamp, a.timestamp))
|
||||||
|
// sort by volume desc, so that vol null > vol number where ch are equal
|
||||||
|
Utils.stableSort(chapters, (a, b) => sorter(b.volume, a.volume))
|
||||||
|
// sort by group
|
||||||
|
Utils.stableSort(chapters, (a, b) => sorter(a.group, b.group))
|
||||||
|
// sort by chapter number
|
||||||
|
Utils.stableSort(chapters, (a, b) => sorter(a.chapter, b.chapter))
|
||||||
|
// add "ghost" prev vol numbers
|
||||||
|
let pv = '0'
|
||||||
|
for (let c of chapters) {
|
||||||
|
c.prevVolume = pv
|
||||||
|
pv = c.volume ? c.volume : pv
|
||||||
|
}
|
||||||
|
// sort by vol or prev vol
|
||||||
|
Utils.stableSort(chapters, (a, b) => sorter(a.volume || a.prevVolume, b.volume || b.prevVolume))
|
||||||
|
return chapters.map(c => c.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
static createUniqueChapterList(baseChapter, chapters) {
|
||||||
|
const list = []
|
||||||
|
if (chapters.length > 0) {
|
||||||
|
let best = chapters[0]
|
||||||
|
for (let ch of chapters.slice(1)) {
|
||||||
|
if (!ch.isAlternativeOf(best)) {
|
||||||
|
list.push(best)
|
||||||
|
best = ch
|
||||||
|
} else if (best !== baseChapter && ch.hasSameGroupsWith(baseChapter)) {
|
||||||
|
best = ch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.push(best)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
static async load(id, params = {}, cache = true) {
|
||||||
|
const json = await super.load(`manga/${id}`, params)
|
||||||
|
try {
|
||||||
|
return this.fromJSON(json.data, json.code, json.status, cache)
|
||||||
|
} catch (error) {
|
||||||
|
throw this.fromJSON({ id, status: json.message }, json.code, json.status, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async loadChapterList(id, params = {}, cache = true) {
|
||||||
|
const json = await super.load(`manga/${id}/chapters`, params)
|
||||||
|
try {
|
||||||
|
const { chapters } = this.fromChapterListJSON(id, json, cache)
|
||||||
|
return chapters
|
||||||
|
} catch (error) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async loadWithChapterList(id, params = {}, cache = true) {
|
||||||
|
const json = await super.load(`manga/${id}`, Object.assign(params, { include: 'chapters' }))
|
||||||
|
try {
|
||||||
|
const { manga } = this.fromChapterListJSON(id, json, cache)
|
||||||
|
return manga
|
||||||
|
} catch (error) {
|
||||||
|
throw this.fromJSON({ id, status: json.message }, json.code, json.status, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromChapterListJSON(id, json, cache) {
|
||||||
|
// avoid overwriting group/chapter resources that already exist because these are going to be incomplete
|
||||||
|
const groups = json.data.groups.map(g => Group.getResource(g.id) || Group.fromJSON(g))
|
||||||
|
const chapters = json.data.chapters.map(c => Chapter.getResource(c.id) || Chapter.fromJSON(c, json.code, json.status, cache))
|
||||||
|
const manga = json.data.manga ? this.fromJSON(json.data.manga, json.code, json.status, cache) : this.getResource(id)
|
||||||
|
manga.chapterListLastLoaded = new Date()
|
||||||
|
//manga.updateChapterList()
|
||||||
|
return { groups, chapters, manga }
|
||||||
|
}
|
||||||
|
}
|
62
scripts/reader/resource/Resource.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
export default class Resource {
|
||||||
|
constructor(data = {}, responseCode = -1, responseStatus = null) {
|
||||||
|
this._response = {
|
||||||
|
code: responseCode,
|
||||||
|
status: responseStatus,
|
||||||
|
}
|
||||||
|
Resource.addResource(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
static addResource(resource) {
|
||||||
|
const id = resource.id
|
||||||
|
const type = resource.constructor.resourceType
|
||||||
|
if (id && type) {
|
||||||
|
if (!(type in Resource.cache)) {
|
||||||
|
Resource.cache[type] = {}
|
||||||
|
}
|
||||||
|
Resource.cache[type][id] = resource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getResource(id) {
|
||||||
|
try { return Resource.cache[this.resourceType][id] }
|
||||||
|
catch (e) { return null }
|
||||||
|
}
|
||||||
|
|
||||||
|
static findResources(filterFn) {
|
||||||
|
try { return Object.values(Resource.cache[this.resourceType]).filter(filterFn) }
|
||||||
|
catch (e) { return [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJSON(data, responseCode = -1, responseStatus = null, cache = true) {
|
||||||
|
const resource = new this(data, responseCode, responseStatus)
|
||||||
|
if (cache) {
|
||||||
|
Resource.addResource(resource)
|
||||||
|
}
|
||||||
|
return resource
|
||||||
|
}
|
||||||
|
|
||||||
|
static async load(resourceURL, params = {}) {
|
||||||
|
try {
|
||||||
|
const url = new URL(process.env.API_URL + resourceURL)
|
||||||
|
for (let key in params) {
|
||||||
|
if (params[key] != null) {
|
||||||
|
url.searchParams.append(key, params[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await fetch(url, {
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
if (res.status >= 500) {
|
||||||
|
throw new Error("Error while loading a resource. The server may be busy at the moment.")
|
||||||
|
}
|
||||||
|
return await res.json()
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Resource loading error:", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource.cache = {}
|
356
scripts/reader/ui.js
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
/* global Renderer */
|
||||||
|
|
||||||
|
const utils = {
|
||||||
|
empty: (node) => {
|
||||||
|
while (node.firstChild) {
|
||||||
|
node.removeChild(node.firstChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UI {
|
||||||
|
constructor(reader) {
|
||||||
|
this.reader = reader
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSinglePage() { return this.renderingMode === UI.RENDERING_MODE.SINGLE }
|
||||||
|
get isDoublePage() { return this.renderingMode === UI.RENDERING_MODE.DOUBLE }
|
||||||
|
get isLongStrip() { return this.renderingMode === UI.RENDERING_MODE.LONG }
|
||||||
|
get isNoResize() { return this.displayFit === UI.DISPLAY_FIT.NO_RESIZE }
|
||||||
|
get isFitHeight() { return this.displayFit === UI.DISPLAY_FIT.FIT_HEIGHT }
|
||||||
|
get isFitWidth() { return this.displayFit === UI.DISPLAY_FIT.FIT_WIDTH }
|
||||||
|
get isFitBoth() { return this.displayFit === UI.DISPLAY_FIT.FIT_BOTH }
|
||||||
|
get isDirectionLTR() { return this.direction === UI.DIRECTION.LTR }
|
||||||
|
get isDirectionRTL() { return this.direction === UI.DIRECTION.RTL }
|
||||||
|
get renderedPages() {
|
||||||
|
if (this.renderer == null) {
|
||||||
|
return 0
|
||||||
|
} else if (this.isLongStrip) {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
return this.renderer.renderedPages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeContainer(userIsGuest = false) {
|
||||||
|
this.container = document.querySelector('div[role="main"]')
|
||||||
|
this.container.classList.remove('container')
|
||||||
|
this.container.classList.add('reader', 'row', 'flex-column', 'flex-lg-row', 'no-gutters')
|
||||||
|
this.imageContainer = this.container.querySelector('.reader-images')
|
||||||
|
document.querySelector('footer').classList.add('d-none')
|
||||||
|
document.body.style.removeProperty('margin-bottom')
|
||||||
|
if (userIsGuest) {
|
||||||
|
const reportBtn = this.container.querySelector('#report-button')
|
||||||
|
reportBtn.dataset.toggle = ''
|
||||||
|
reportBtn.href = '/login'
|
||||||
|
reportBtn.firstElementChild.classList.replace('fa-flag', 'fa-sign-in-alt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRenderer(mode = this.reader.settings.renderingMode, useHistory = true) {
|
||||||
|
if (this.renderingMode !== mode) {
|
||||||
|
if (this.renderer != null) {
|
||||||
|
this.renderer.destroy()
|
||||||
|
}
|
||||||
|
this.renderingMode = mode
|
||||||
|
switch(mode) {
|
||||||
|
case UI.RENDERING_MODE.LONG:
|
||||||
|
this.renderer = new Renderer.LongStrip(this.reader)
|
||||||
|
break
|
||||||
|
case UI.RENDERING_MODE.DOUBLE:
|
||||||
|
this.renderer = new Renderer.DoublePage(this.reader)
|
||||||
|
break
|
||||||
|
case UI.RENDERING_MODE.SINGLE:
|
||||||
|
default:
|
||||||
|
this.renderer = new Renderer.SinglePage(this.reader)
|
||||||
|
break
|
||||||
|
case UI.RENDERING_MODE.ALERT:
|
||||||
|
this.renderer = new Renderer.Alert(this.reader)
|
||||||
|
break
|
||||||
|
case UI.RENDERING_MODE.RECS:
|
||||||
|
this.renderer = new Renderer.Recommendations(this.reader)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
this.container.dataset.renderer = this.renderer.name
|
||||||
|
if (useHistory) {
|
||||||
|
this.pushHistory(this.reader.currentPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDirection(direction) {
|
||||||
|
this.direction = direction
|
||||||
|
this.container.dataset.direction = UI.DIRECTION.LTR === direction ? 'ltr' : 'rtl'
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisplayFit(fit) {
|
||||||
|
this.displayFit = fit
|
||||||
|
this.container.dataset.display = UI.DISPLAY_FIT_STR[fit]
|
||||||
|
this.container.classList.toggle('fit-horizontal', this.isFitBoth || this.isFitWidth)
|
||||||
|
this.container.classList.toggle('fit-vertical', this.isFitBoth || this.isFitHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
onChapterChange(chapter) {
|
||||||
|
if (this.renderer) {
|
||||||
|
this.renderer.initialize()
|
||||||
|
}
|
||||||
|
this.container.classList.toggle('native-long-strip', chapter.manga.isLongStrip)
|
||||||
|
this.setRenderer(chapter.manga.isLongStrip ? UI.RENDERING_MODE.LONG : this.reader.settings.renderer, false)
|
||||||
|
this.setDisplayFit(chapter.manga.isLongStrip ? UI.DISPLAY_FIT.FIT_WIDTH : this.reader.settings.displayFit)
|
||||||
|
this.resetPageBar(chapter.totalPages)
|
||||||
|
// TODO?: it's assumed that the input is already escaped; when not, change innerHTML to textContent
|
||||||
|
this.updateTitles(chapter)
|
||||||
|
this.updateChapterDropdown(chapter, !this.reader.settings.showDropdownTitles)
|
||||||
|
this.updatePageDropdown(chapter.totalPages, this.reader.currentPage)
|
||||||
|
this.updateGroupList(chapter)
|
||||||
|
this.updateCommentsButton(chapter)
|
||||||
|
this.updateChapterLinks(chapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSetting(key, value) {
|
||||||
|
switch (key) {
|
||||||
|
case 'direction':
|
||||||
|
this.setDirection(value)
|
||||||
|
this.updateChapterLinks()
|
||||||
|
this.setRenderer()
|
||||||
|
if (this.reader.currentPage) {
|
||||||
|
this.render(this.reader.currentPage)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'renderingMode':
|
||||||
|
this.setRenderer(value)
|
||||||
|
if (this.reader.currentPage) {
|
||||||
|
this.render(this.reader.currentPage)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'displayFit':
|
||||||
|
this.setDisplayFit(value)
|
||||||
|
break
|
||||||
|
case 'containerWidth':
|
||||||
|
if (!value) {
|
||||||
|
value = null
|
||||||
|
}
|
||||||
|
this.imageContainer.style.maxWidth = value ? `${value}px` : null
|
||||||
|
break
|
||||||
|
case 'showDropdownTitles':
|
||||||
|
this.updateChapterDropdown(this.reader.chapter, !value)
|
||||||
|
break
|
||||||
|
case 'hideHeader':
|
||||||
|
this.container.classList.toggle('hide-header', value)
|
||||||
|
document.querySelector('nav.navbar').classList.toggle('d-none', value)
|
||||||
|
document.querySelector('#fullscreen-button').classList.toggle('active', value)
|
||||||
|
break
|
||||||
|
case 'hideSidebar':
|
||||||
|
this.container.classList.toggle('hide-sidebar', value)
|
||||||
|
break
|
||||||
|
case 'hidePagebar':
|
||||||
|
this.container.classList.toggle('hide-page-bar', value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Array.from(this.container.querySelectorAll(`#modal-settings input[data-setting="${key}"]`)).forEach(n => { n.value = value })
|
||||||
|
Array.from(this.container.querySelectorAll(`#modal-settings select[data-setting="${key}"]`)).forEach(n => { n.value = value })
|
||||||
|
Array.from(this.container.querySelectorAll(`#modal-settings button[data-setting="${key}"]`)).forEach(n => { n.classList.toggle('active', n.dataset.value == value) })
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTitles(chapter) {
|
||||||
|
const manga = chapter.manga
|
||||||
|
document.title = `${manga.title} - ${manga.getChapterTitle(chapter.id)} - MangaDex`
|
||||||
|
const mangaFlag = this.container.querySelector('.reader-controls-title .lang-flag')
|
||||||
|
mangaFlag.parentElement.replaceChild(UI.flagImg(manga.langCode, manga.langName), mangaFlag)
|
||||||
|
const mangaLink = this.container.querySelector('.manga-link')
|
||||||
|
mangaLink.href = manga.url
|
||||||
|
mangaLink.title = manga.title
|
||||||
|
mangaLink.innerHTML = manga.title
|
||||||
|
this.container.querySelector('.chapter-title').innerHTML = chapter.title
|
||||||
|
this.container.querySelector('.chapter-tag-h').classList.toggle('d-none', !manga.isHentai)
|
||||||
|
this.container.querySelector('.chapter-tag-end').classList.toggle('d-none', !chapter.isLastChapter)
|
||||||
|
this.container.querySelector('.chapter-tag-doujinshi').classList.toggle('d-none', !manga.isDoujinshi)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChapterDropdown(chapter, hideTitles) {
|
||||||
|
if (chapter) {
|
||||||
|
const manga = chapter.manga
|
||||||
|
const chapters = this.container.querySelector('#jump-chapter')
|
||||||
|
utils.empty(chapters)
|
||||||
|
for (let ch of manga.chapterList.slice().reverse()) {
|
||||||
|
const option = chapters.appendChild(document.createElement('option'))
|
||||||
|
option.value = ch.id
|
||||||
|
option.selected = ch.id === chapter.id
|
||||||
|
option.appendChild(document.createTextNode(manga.getChapterTitle(ch.id, hideTitles)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePageDropdown(totalPages, currentPage) {
|
||||||
|
const pages = this.container.querySelector('#jump-page')
|
||||||
|
utils.empty(pages)
|
||||||
|
for (let i = 1; i <= totalPages; ++i) {
|
||||||
|
const option = pages.appendChild(document.createElement('option'))
|
||||||
|
option.value = i
|
||||||
|
option.selected = currentPage === i
|
||||||
|
option.appendChild(document.createTextNode(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGroupList(chapter) {
|
||||||
|
const groups = this.container.querySelector('.reader-controls-groups ul')
|
||||||
|
utils.empty(groups)
|
||||||
|
for (let g of chapter.manga.getGroupsOfChapter(chapter.id)) {
|
||||||
|
const li = groups.appendChild(document.createElement('li'))
|
||||||
|
const flag = li.appendChild(UI.flagImg(g.lang_code, g.lang_code))
|
||||||
|
flag.classList.add('mr-1')
|
||||||
|
const link = li.appendChild(document.createElement(g.id == chapter.id ? 'b' : 'a'))
|
||||||
|
link.innerHTML = [g.group_name, g.group_name_2, g.group_name_3].filter(n => n).join(' | ')
|
||||||
|
link.dataset.chapter = g.id
|
||||||
|
link.href = `/chapter/${g.id}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCommentsButton(chapter) {
|
||||||
|
this.container.querySelector('#comment-button').href = this.pageURL(chapter.id) + '/comments'
|
||||||
|
this.container.querySelector('.comment-amount').textContent = chapter.comments || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChapterLinks(chapter) {
|
||||||
|
if (chapter) {
|
||||||
|
const update = (toLeft) => {
|
||||||
|
if (this.direction != null) {
|
||||||
|
let id = (toLeft === this.isDirectionLTR) ? chapter.prevChapterId : chapter.nextChapterId
|
||||||
|
return (a) => {
|
||||||
|
a.dataset.chapter = id
|
||||||
|
a.href = this.pageURL(id)
|
||||||
|
a.title = chapter.manga.getChapterTitle(id) || 'Back to manga'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Array.from(this.container.querySelectorAll('a.chapter-link-left')).forEach(update(true))
|
||||||
|
Array.from(this.container.querySelectorAll('a.chapter-link-right')).forEach(update(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePageLinks(chapter, pg) {
|
||||||
|
if (chapter && typeof pg === 'number') {
|
||||||
|
const pgStr = `${pg}${this.renderedPages===2 ? ` - ${pg + 1}` : ''}`
|
||||||
|
this.container.querySelector('.reader-controls-pages .current-page').textContent = pgStr
|
||||||
|
this.container.querySelector('.reader-controls-pages .total-pages').textContent = chapter.totalPages
|
||||||
|
this.container.querySelector('.reader-controls-pages .page-link-left').href = this.pageLeftURL(chapter.id, 1)
|
||||||
|
this.container.querySelector('.reader-controls-pages .page-link-right').href = this.pageRightURL(chapter.id, 1)
|
||||||
|
this.container.querySelector('#jump-page').value = pg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageURL(id, pg) {
|
||||||
|
if (id != null && id > 0) {
|
||||||
|
if (pg != null) {
|
||||||
|
if (pg === 0) {
|
||||||
|
return this.pageURL(this.reader.chapter.prevChapterId, -1)
|
||||||
|
} else if (pg > this.reader.chapter.totalPages) {
|
||||||
|
return this.pageURL(this.reader.chapter.nextChapterId)
|
||||||
|
}
|
||||||
|
return this.isTestReader ? `/?page=chapter_test&id=${id}&p=${pg}` : `/chapter/${id}/${pg}`
|
||||||
|
}
|
||||||
|
return this.isTestReader ? `/?page=chapter_test&id=${id}` : `/chapter/${id}`
|
||||||
|
}
|
||||||
|
return this.manga.url
|
||||||
|
}
|
||||||
|
pageLeftURL(id, pages = this.isDoublePage ? 2 : 1) {
|
||||||
|
return this.pageURL(id, Math.min(this.reader.currentPage + (this.isDirectionLTR ? -pages : pages)), 0)
|
||||||
|
}
|
||||||
|
pageRightURL(id, pages = this.isDoublePage ? 2 : 1) {
|
||||||
|
return this.pageURL(id, Math.min(this.reader.currentPage + (this.isDirectionLTR ? pages : -pages)), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPageBar(totalPages) {
|
||||||
|
if (totalPages) {
|
||||||
|
const notches = this.container.querySelector('.reader-page-bar .notches')
|
||||||
|
utils.empty(notches)
|
||||||
|
for (let i = 1; i <= totalPages; ++i) {
|
||||||
|
const notch = notches.appendChild(document.createElement('div'))
|
||||||
|
notch.classList.add('notch', 'col')
|
||||||
|
notch.style.order = i
|
||||||
|
notch.dataset.page = i
|
||||||
|
notch.title = `Page ${i}`
|
||||||
|
// notch.classList.toggle('trail', i <= pg-this.renderedPages)
|
||||||
|
// notch.classList.toggle('thumb', i > pg-this.renderedPages && i <= pg)
|
||||||
|
}
|
||||||
|
this.updatePageBar(totalPages, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePageBar(totalPages, pg) {
|
||||||
|
if (totalPages && typeof pg === 'number') {
|
||||||
|
const trail = this.container.querySelector('.reader-page-bar .trail')
|
||||||
|
const thumb = this.container.querySelector('.reader-page-bar .thumb')
|
||||||
|
const notchSize = 100 / Math.max(totalPages, 1)
|
||||||
|
trail.style.width = Math.min(pg * notchSize, 100) + '%'
|
||||||
|
thumb.style.width = (100 / pg * Math.max(this.reader.renderedPages, 1)) + '%'
|
||||||
|
trail.style.right = this.reader.isDirectionLTR ? null : 0
|
||||||
|
thumb.style.float = this.reader.isDirectionLTR ? 'right' : 'left'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(chapter, pg) {
|
||||||
|
if (!pg || chapter == null || this.ui.renderer == null) {
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
return this.renderer.render(pg).then(() => {
|
||||||
|
this.updatePageLinks(chapter, this.reader.currentPage)
|
||||||
|
this.updatePageBar(chapter, this.reader.currentPage + this.renderedPages - 1)
|
||||||
|
}).catch((err) => this.renderError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
renderError(err) {
|
||||||
|
console.error('Render error', err)
|
||||||
|
this.setRenderer(UI.RENDERING_MODE.ALERT)
|
||||||
|
return this.renderer.render({ type: 'danger', 'message': err.message, 'err': err })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static flagImg (langCode = 'jp', langName = 'Unknown') {
|
||||||
|
const flag = document.createElement('img')
|
||||||
|
flag.classList.add('lang-flag')
|
||||||
|
flag.src = `https://mangadex.org/images/flags/${langCode}.png`
|
||||||
|
flag.alt = langName
|
||||||
|
flag.title = langName
|
||||||
|
return flag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UI.RENDERING_MODE = {
|
||||||
|
SINGLE: 1,
|
||||||
|
DOUBLE: 2,
|
||||||
|
LONG: 3,
|
||||||
|
ALERT: 4,
|
||||||
|
RECS: 5,
|
||||||
|
}
|
||||||
|
UI.DIRECTION = {
|
||||||
|
LTR: 1,
|
||||||
|
RTL: 2,
|
||||||
|
}
|
||||||
|
UI.DISPLAY_FIT = {
|
||||||
|
FIT_BOTH: 1,
|
||||||
|
FIT_WIDTH: 2,
|
||||||
|
FIT_HEIGHT: 3,
|
||||||
|
NO_RESIZE: 4,
|
||||||
|
}
|
||||||
|
UI.DISPLAY_FIT_STR = {
|
||||||
|
1: 'fit-both',
|
||||||
|
2: 'fit-width',
|
||||||
|
3: 'fit-height',
|
||||||
|
4: 'no-resize',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof exports === 'object' && typeof module === 'object') {
|
||||||
|
module.exports = UI
|
||||||
|
} else if (typeof define === 'function' && define.amd) {
|
||||||
|
define([], function () {
|
||||||
|
return UI
|
||||||
|
})
|
||||||
|
} else if (typeof exports === 'object') {
|
||||||
|
exports.UI = UI
|
||||||
|
} else {
|
||||||
|
(typeof window !== 'undefined' ? window : this).UI = UI
|
||||||
|
}
|
69
scripts/reader/utils.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
export default class Utils {
|
||||||
|
static range (start, end, step = 1) {
|
||||||
|
const r = []
|
||||||
|
for (let i = start; i < end; i += step) {
|
||||||
|
r.push(i)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
static clamp (val, min, max) {
|
||||||
|
return Math.max(min, Math.min(val, max))
|
||||||
|
}
|
||||||
|
|
||||||
|
static emptyNode (node) {
|
||||||
|
while (node && node.firstChild) {
|
||||||
|
node.removeChild(node.firstChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static scrollBy () {
|
||||||
|
window.scrollBy.apply(null, arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
static stableSort (array, cmp) {
|
||||||
|
// https://medium.com/@fsufitch/is-javascript-array-sort-stable-46b90822543f
|
||||||
|
cmp = !!cmp ? cmp : (a, b) => {
|
||||||
|
if (a < b) return -1
|
||||||
|
if (a > b) return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const stabilized = array.map((el, index) => [el, index])
|
||||||
|
const stableCmp = (a, b) => {
|
||||||
|
const order = cmp(a[0], b[0])
|
||||||
|
return order != 0 ? order : a[1] - b[1]
|
||||||
|
}
|
||||||
|
stabilized.sort(stableCmp)
|
||||||
|
for (let i = 0; i < array.length; ++i) {
|
||||||
|
array[i] = stabilized[i][0]
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static htmlTextDecodeHack(str) {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.innerHTML = str
|
||||||
|
return textarea.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.scrollBy({ top: 0, behavior: "smooth" })
|
||||||
|
} catch(e) {
|
||||||
|
Utils.scrollBy = function() {
|
||||||
|
const arg = arguments[0]
|
||||||
|
switch(typeof arg) {
|
||||||
|
case 'object':
|
||||||
|
return window.scrollBy(arg.top || 0, arg.left || 0)
|
||||||
|
case 'number':
|
||||||
|
return window.scrollBy.apply(null, arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof exports === 'object' && typeof module === 'object') {
|
||||||
|
module.exports = Utils
|
||||||
|
}
|
58
sql.php
@ -2,9 +2,22 @@
|
|||||||
if (PHP_SAPI !== 'cli')
|
if (PHP_SAPI !== 'cli')
|
||||||
die();
|
die();
|
||||||
|
|
||||||
require_once ('/home/www/mangadex.org/bootstrap.php'); //must be like this
|
require_once ('/var/www/mangadex.org/bootstrap.php'); //must be like this
|
||||||
|
|
||||||
require_once (ABSPATH . "/scripts/header.req.php");
|
require_once (ABSPATH . "/scripts/header.req.php");
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
for ($id = 0; $id < 2491159; $id++) {
|
||||||
|
$memcached->delete("user_{$id}_friends_user_ids");
|
||||||
|
$memcached->delete("user_{$id}_pending_friends_user_ids");
|
||||||
|
$memcached->delete("user_{$id}_friends_user_ids");
|
||||||
|
$memcached->delete("user_{$id}_pending_friends_user_ids");
|
||||||
|
if ($id % 1000 === 0) echo ".";
|
||||||
|
}
|
||||||
|
die("\nend\n");
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
$result = $sql->query_read('x', " SELECT COUNT(*) AS `Rows`, `user_id` FROM `mangadex_clients` where approved = 1 GROUP BY `user_id` ORDER BY `user_id` ", 'fetchAll', PDO::FETCH_ASSOC, -1);
|
$result = $sql->query_read('x', " SELECT COUNT(*) AS `Rows`, `user_id` FROM `mangadex_clients` where approved = 1 GROUP BY `user_id` ORDER BY `user_id` ", 'fetchAll', PDO::FETCH_ASSOC, -1);
|
||||||
|
|
||||||
@ -154,7 +167,7 @@ foreach ($txs as $tx) {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
/*
|
/*
|
||||||
$joined_timestamp = $sql->query_read('x', " SELECT joined_timestamp FROM mangadex_users WHERE joined_timestamp >= 1594166400 ", 'fetchAll', PDO::FETCH_COLUMN, -1);
|
$joined_timestamp = $sql->query_read('x', " SELECT joined_timestamp FROM mangadex_users WHERE joined_timestamp >= 1597622400 ", 'fetchAll', PDO::FETCH_COLUMN, -1);
|
||||||
|
|
||||||
foreach($joined_timestamp as $value) {
|
foreach($joined_timestamp as $value) {
|
||||||
$date = date('Y-m-d', $value);
|
$date = date('Y-m-d', $value);
|
||||||
@ -259,19 +272,19 @@ foreach ($results as $ro) {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
$dir = '/home/www/mangadex.org/data/';
|
$dir = '/var/www/mangadex.org/data/';
|
||||||
$files = array_diff(scandir($dir), array('..', '.'));
|
$files = array_diff(scandir($dir), array('..', '.'));
|
||||||
|
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
|
|
||||||
$chapter_id = $sql->query_read('chapter_id', " SELECT chapter_id FROM mangadex_chapters WHERE chapter_hash LIKE '$file' ", 'fetchColumn', '', -1);
|
$chapter_id = $sql->query_read('chapter_id', " SELECT chapter_id FROM mangadex_chapters WHERE chapter_hash LIKE '$file' ", 'fetchColumn', '', -1);
|
||||||
if ($chapter_id) {
|
if (!$chapter_id) {
|
||||||
print $file . " - $chapter_id\n";
|
print $file . " - $chapter_id\n";
|
||||||
|
|
||||||
$sql->modify('update', " UPDATE mangadex_chapters SET server = 1 WHERE chapter_id = ? LIMIT 1; ", [$chapter_id]);
|
//$sql->modify('update', " UPDATE mangadex_chapters SET server = 1 WHERE chapter_id = ? LIMIT 1; ", [$chapter_id]);
|
||||||
$memcached->delete("chapter_$chapter_id");
|
//$memcached->delete("chapter_$chapter_id");
|
||||||
|
|
||||||
//rename("/home/www/mangadex.org/data/$file", "/home/www/mangadex.org/delete/$file");
|
rename("/var/www/mangadex.org/data/$file", "/var/www/mangadex.org/delete/$file");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -338,7 +351,7 @@ foreach ($array as $manga) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
$results = $sql->query_read('x', " SELECT * FROM mangadex_users where level_id = 0 and user_id > 1900000 order by user_id desc ", 'fetchAll', PDO::FETCH_ASSOC, -1);
|
$results = $sql->query_read('x', " SELECT * FROM mangadex_users where level_id = 0 and user_id > 1900000 order by user_id desc ", 'fetchAll', PDO::FETCH_ASSOC, -1);
|
||||||
foreach ($results as $row) {
|
foreach ($results as $row) {
|
||||||
$uid = $row['user_id'];
|
$uid = $row['user_id'];
|
||||||
@ -348,7 +361,7 @@ foreach ($results as $row) {
|
|||||||
print $uid . ' ';
|
print $uid . ' ';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
/*
|
/*
|
||||||
foreach (WALLET_QR['ETH'] as $qr) {
|
foreach (WALLET_QR['ETH'] as $qr) {
|
||||||
print "Fetching $qr\n\n";
|
print "Fetching $qr\n\n";
|
||||||
@ -409,12 +422,30 @@ foreach ($txs as $tx) {
|
|||||||
|
|
||||||
|
|
||||||
//$result = $sql->query_read('x', " SELECT chapters.*, users.level_id, users.user_id, users.username FROM `mangadex_chapters` as chapters left join mangadex_users as users on chapters.user_id = users.user_id where users.level_id = 0 and chapters.chapter_deleted = 1 and chapters.server = 0 ", 'fetchAll', PDO::FETCH_ASSOC, -1);
|
//$result = $sql->query_read('x', " SELECT chapters.*, users.level_id, users.user_id, users.username FROM `mangadex_chapters` as chapters left join mangadex_users as users on chapters.user_id = users.user_id where users.level_id = 0 and chapters.chapter_deleted = 1 and chapters.server = 0 ", 'fetchAll', PDO::FETCH_ASSOC, -1);
|
||||||
/*
|
|
||||||
$result = $sql->query_read('x', " SELECT * FROM `mangadex_chapters` WHERE `manga_id` = 47 AND `server` = 1 AND `chapter_deleted` = 1 ", 'fetchAll', PDO::FETCH_ASSOC, -1);
|
//$result = $sql->query_read('x', " SELECT * FROM `mangadex_chapters` WHERE `manga_id` = 47 AND `server` = 0 AND `chapter_deleted` = 1 ", 'fetchAll', PDO::FETCH_ASSOC, -1);
|
||||||
|
|
||||||
|
$result = $sql->query_read('x', "
|
||||||
|
SELECT * FROM `mangadex_chapters` WHERE `upload_timestamp` > 1604707200 AND upload_timestamp < 1605312000 and `group_id` != 9097 AND `server` = 0 AND `chapter_deleted` = 0
|
||||||
|
", 'fetchAll', PDO::FETCH_ASSOC, -1);
|
||||||
|
|
||||||
foreach ($result as $row) {
|
foreach ($result as $row) {
|
||||||
print $row['chapter_hash'] . ' ';
|
//print $row['chapter_hash'] . ' ';
|
||||||
}*/
|
$file = $row['chapter_hash'];
|
||||||
|
$chapter_id = $row['chapter_id'];
|
||||||
|
$memcached->delete("chapter_$chapter_id");
|
||||||
|
|
||||||
|
|
||||||
|
print "$file - $chapter_id\n";
|
||||||
|
|
||||||
|
$sql->modify('update', " UPDATE mangadex_chapters SET server = 1 WHERE chapter_id = ? LIMIT 1; ", [$chapter_id]);
|
||||||
|
//$memcached->delete("chapter_$chapter_id");
|
||||||
|
|
||||||
|
rename("/var/www/mangadex.org/data/$file", "/var/www/mangadex.org/transferred/$file");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
//$result = $sql->query_read('x', " SELECT chapters.*, users.level_id, users.user_id, users.username FROM `mangadex_chapters` as chapters left join mangadex_users as users on chapters.user_id = users.user_id where users.level_id = 0 and chapters.chapter_deleted = 1 and chapters.server = 0 ", 'fetchAll', PDO::FETCH_ASSOC, -1);
|
//$result = $sql->query_read('x', " SELECT chapters.*, users.level_id, users.user_id, users.username FROM `mangadex_chapters` as chapters left join mangadex_users as users on chapters.user_id = users.user_id where users.level_id = 0 and chapters.chapter_deleted = 1 and chapters.server = 0 ", 'fetchAll', PDO::FETCH_ASSOC, -1);
|
||||||
|
|
||||||
@ -482,3 +513,4 @@ foreach ($result as $row) {
|
|||||||
|
|
||||||
//var_dump(is_banned_asn('177.100.112.109'));
|
//var_dump(is_banned_asn('177.100.112.109'));
|
||||||
//var_dump(get_asn('177.100.112.109'));
|
//var_dump(get_asn('177.100.112.109'));
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ use Mangadex\Exception\Http\UnavailableForLegalReasonsHttpException;
|
|||||||
|
|
||||||
class ChapterController extends APIController
|
class ChapterController extends APIController
|
||||||
{
|
{
|
||||||
const CHAPTERS_LIMIT = 6000;
|
const CHAPTERS_LIMIT = 8000;
|
||||||
const CH_STATUS_OK = 'OK';
|
const CH_STATUS_OK = 'OK';
|
||||||
const CH_STATUS_DELETED = 'deleted';
|
const CH_STATUS_DELETED = 'deleted';
|
||||||
const CH_STATUS_DELAYED = 'delayed';
|
const CH_STATUS_DELAYED = 'delayed';
|
||||||
@ -36,10 +36,9 @@ class ChapterController extends APIController
|
|||||||
|
|
||||||
public function view($path)
|
public function view($path)
|
||||||
{
|
{
|
||||||
/**
|
$id = $path[0] ?? null;
|
||||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
$subResource = $path[1] ?? null;
|
||||||
*/
|
$subResourceId = $path[2] ?? null;
|
||||||
[$id, $subResource, $subResourceId] = $path;
|
|
||||||
|
|
||||||
$id = $this->validateId($id);
|
$id = $this->validateId($id);
|
||||||
|
|
||||||
@ -53,6 +52,13 @@ class ChapterController extends APIController
|
|||||||
if (isset($normalized['pages'])) {
|
if (isset($normalized['pages'])) {
|
||||||
$this->updateChapterViews($chapter);
|
$this->updateChapterViews($chapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (in_array('manga', $this->request->query->getList('include'))) {
|
||||||
|
$mangaController = new MangaController();
|
||||||
|
$manga = $mangaController->normalize($mangaController->fetch($normalized['mangaId']));
|
||||||
|
$normalized = ['chapter' => $normalized, 'manga' => $manga];
|
||||||
|
}
|
||||||
|
|
||||||
return $normalized;
|
return $normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,6 +93,13 @@ class ChapterController extends APIController
|
|||||||
throw new BadRequestHttpException("Invalid limit, range must be within 10 - 100.");
|
throw new BadRequestHttpException("Invalid limit, range must be within 10 - 100.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->request->query->getBoolean('blockgroups', true)) {
|
||||||
|
$blockedGroups = $this->user->get_blocked_groups();
|
||||||
|
if ($blockedGroups) {
|
||||||
|
$search['blocked_groups'] = array_keys($blockedGroups);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$chapters = new \Chapters($search);
|
$chapters = new \Chapters($search);
|
||||||
$list = $chapters->query_read($order, self::CHAPTERS_LIMIT, 1);
|
$list = $chapters->query_read($order, self::CHAPTERS_LIMIT, 1);
|
||||||
if ($page > 0) {
|
if ($page > 0) {
|
||||||
@ -153,10 +166,12 @@ class ChapterController extends APIController
|
|||||||
if (!empty($langFilter)) {
|
if (!empty($langFilter)) {
|
||||||
$search["multi_lang_id"] = $langFilter;
|
$search["multi_lang_id"] = $langFilter;
|
||||||
}
|
}
|
||||||
|
if ($this->request->query->getBoolean('blockgroups', true)) {
|
||||||
$blockedGroups = $this->user->get_blocked_groups();
|
$blockedGroups = $this->user->get_blocked_groups();
|
||||||
if ($blockedGroups) {
|
if ($blockedGroups) {
|
||||||
$search['blocked_groups'] = array_keys($blockedGroups);
|
$search['blocked_groups'] = array_keys($blockedGroups);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if ($hentai !== 1) { // i.e. if hentai is 0 (hide) or >1 (show only)
|
if ($hentai !== 1) { // i.e. if hentai is 0 (hide) or >1 (show only)
|
||||||
$search['manga_hentai'] = $hentai ? 1 : 0;
|
$search['manga_hentai'] = $hentai ? 1 : 0;
|
||||||
}
|
}
|
||||||
@ -174,13 +189,25 @@ class ChapterController extends APIController
|
|||||||
$chaptersResult = $chapters->query_read($order, $limit, max($page, 1));
|
$chaptersResult = $chapters->query_read($order, $limit, max($page, 1));
|
||||||
$normalized = $this->normalizeList($chaptersResult, false);
|
$normalized = $this->normalizeList($chaptersResult, false);
|
||||||
|
|
||||||
|
if ($userResource->user_id === $this->user->user_id) {
|
||||||
|
$readChapters = $this->user->get_read_chapters();
|
||||||
|
foreach ($normalized['chapters'] as &$chapter) {
|
||||||
|
$chapter['read'] = in_array(
|
||||||
|
$chapter['id'],
|
||||||
|
$readChapters
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// include basic manga entities
|
// include basic manga entities
|
||||||
$manga = [];
|
$manga = [];
|
||||||
foreach ($chaptersResult as $chapter) {
|
foreach ($chaptersResult as &$chapter) {
|
||||||
if (!isset($manga[$chapter['manga_id']])) {
|
if (!isset($manga[$chapter['manga_id']])) {
|
||||||
$manga[$chapter['manga_id']] = [
|
$manga[$chapter['manga_id']] = [
|
||||||
'id' => $chapter['manga_id'],
|
'id' => $chapter['manga_id'],
|
||||||
|
// TODO: remove 'name'
|
||||||
'name' => $chapter['manga_name'],
|
'name' => $chapter['manga_name'],
|
||||||
|
'title' => $chapter['manga_name'],
|
||||||
'isHentai' => (bool)$chapter['manga_hentai'],
|
'isHentai' => (bool)$chapter['manga_hentai'],
|
||||||
'lastChapter' => (!empty($chapter['manga_last_chapter']) && $chapter['manga_last_chapter'] !== '0') ? $chapter['manga_last_chapter'] : null,
|
'lastChapter' => (!empty($chapter['manga_last_chapter']) && $chapter['manga_last_chapter'] !== '0') ? $chapter['manga_last_chapter'] : null,
|
||||||
'lastVolume' => (string)$chapter['manga_last_volume'] ?: null,
|
'lastVolume' => (string)$chapter['manga_last_volume'] ?: null,
|
||||||
@ -208,6 +235,7 @@ class ChapterController extends APIController
|
|||||||
'groups' => [],
|
'groups' => [],
|
||||||
'uploader' => $chapter->user_id,
|
'uploader' => $chapter->user_id,
|
||||||
'timestamp' => $chapter->upload_timestamp,
|
'timestamp' => $chapter->upload_timestamp,
|
||||||
|
'threadId' => $chapter->thread_id,
|
||||||
'comments' => $chapter->thread_posts ?? 0,
|
'comments' => $chapter->thread_posts ?? 0,
|
||||||
'views' => $chapter->chapter_views ?? 0,
|
'views' => $chapter->chapter_views ?? 0,
|
||||||
];
|
];
|
||||||
@ -218,36 +246,58 @@ class ChapterController extends APIController
|
|||||||
});
|
});
|
||||||
$normalized['groups'] = array_map(function ($g) {
|
$normalized['groups'] = array_map(function ($g) {
|
||||||
return ['id' => $g[0], 'name' => $g[1]];
|
return ['id' => $g[0], 'name' => $g[1]];
|
||||||
}, $groupsFiltered);
|
}, array_values($groupsFiltered));
|
||||||
|
|
||||||
if ($fullData) {
|
if ($fullData) {
|
||||||
|
$isValidated = validate_level($this->user, 'pr')
|
||||||
|
|| $this->request->headers->get("API_KEY") === PRIVATE_API_KEY;
|
||||||
|
|
||||||
$normalized['status'] = self::CH_STATUS_OK;
|
$normalized['status'] = self::CH_STATUS_OK;
|
||||||
$isExternal = substr($chapter->page_order, 0, 4) === 'http';
|
$isExternal = substr($chapter->page_order, 0, 4) === 'http';
|
||||||
|
$isRestricted = in_array($chapter->manga_id, RESTRICTED_MANGA_IDS) &&
|
||||||
|
!validate_level($this->user, 'contributor') &&
|
||||||
|
!$hasPrivateAuth &&
|
||||||
|
$this->user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA;
|
||||||
|
$countryCode = strtoupper(get_country_code($this->user->last_ip));
|
||||||
|
$isRegionBlocked = isset(REGION_BLOCKED_MANGA[$countryCode]) &&
|
||||||
|
in_array($chapter->manga_id, REGION_BLOCKED_MANGA[$countryCode]) &&
|
||||||
|
!$isValidated;
|
||||||
|
|
||||||
// Set status when something other than OK
|
// Set status when something other than OK
|
||||||
if ($chapter->chapter_deleted) {
|
if ($chapter->chapter_deleted) {
|
||||||
if (!validate_level($this->user, 'pr')) {
|
if (!$isValidated) {
|
||||||
throw new GoneHttpException(self::CH_STATUS_DELETED);
|
throw new GoneHttpException(self::CH_STATUS_DELETED);
|
||||||
}
|
}
|
||||||
$normalized['status'] = self::CH_STATUS_DELETED;
|
$normalized['status'] = self::CH_STATUS_DELETED;
|
||||||
} else if (!$chapter->available) {
|
} else if (!$chapter->available) {
|
||||||
if (!validate_level($this->user, 'pr')) {
|
if (!$isValidated) {
|
||||||
throw new UnavailableForLegalReasonsHttpException(self::CH_STATUS_UNAVAILABLE);
|
throw new UnavailableForLegalReasonsHttpException(self::CH_STATUS_UNAVAILABLE);
|
||||||
}
|
}
|
||||||
$normalized['status'] = self::CH_STATUS_UNAVAILABLE;
|
$normalized['status'] = self::CH_STATUS_UNAVAILABLE;
|
||||||
$normalized['groups'] = [];
|
$normalized['groups'] = [];
|
||||||
} else if ($chapter->upload_timestamp > time()) {
|
} else if ($chapter->upload_timestamp > time()) {
|
||||||
|
if (!$isValidated) {
|
||||||
|
$groupLeaderIds = [$chapter->group_leader_id, $chapter->group_leader_id_2, $chapter->group_leader_id_3];
|
||||||
|
$isValidated = in_array($this->user->user_id, array_filter($groupLeaderIds, function ($n) {
|
||||||
|
return $n > 0;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (!$isValidated) {
|
||||||
|
$groups = array_map(function ($g) {
|
||||||
|
return new \Group($g['id']);
|
||||||
|
}, $normalized['groups']);
|
||||||
|
$groupMemberIds = array_reduce($groups, function ($acc, $g) {
|
||||||
|
return array_merge($acc, array_keys($g->get_members()));
|
||||||
|
}, []);
|
||||||
|
$isValidated = in_array($this->user->user_id, $groupMemberIds);
|
||||||
|
}
|
||||||
$normalized['status'] = self::CH_STATUS_DELAYED;
|
$normalized['status'] = self::CH_STATUS_DELAYED;
|
||||||
$normalized['groupWebsite'] = $chapter->group_website ?: null;
|
$normalized['groupWebsite'] = $chapter->group_website ?: null;
|
||||||
} else if ($isExternal) {
|
} else if ($isExternal) {
|
||||||
$normalized['status'] = self::CH_STATUS_EXTERNAL;
|
$normalized['status'] = self::CH_STATUS_EXTERNAL;
|
||||||
$normalized['pages'] = $chapter->page_order;
|
$normalized['pages'] = $chapter->page_order;
|
||||||
} else if (
|
} else if ($isRestricted || $isRegionBlocked) {
|
||||||
in_array($chapter->manga_id, RESTRICTED_MANGA_IDS) &&
|
if (!$isValidated) {
|
||||||
!validate_level($this->user, 'contributor') &&
|
|
||||||
$this->user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA
|
|
||||||
) {
|
|
||||||
if (!validate_level($this->user, 'pr')) {
|
|
||||||
throw new ForbiddenHttpException(self::CH_STATUS_RESTRICTED);
|
throw new ForbiddenHttpException(self::CH_STATUS_RESTRICTED);
|
||||||
}
|
}
|
||||||
$normalized = [
|
$normalized = [
|
||||||
@ -257,37 +307,21 @@ class ChapterController extends APIController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Include page information for non-external chapters and only for non-restricted users
|
// Include page information for non-external chapters and only for non-restricted users
|
||||||
if (!$isExternal && ($normalized['status'] === self::CH_STATUS_OK || validate_level($this->user, 'pr'))) {
|
if (!$isExternal && ($normalized['status'] === self::CH_STATUS_OK || $isValidated)) {
|
||||||
$pages = explode(',', $chapter->page_order);
|
$pages = explode(',', $chapter->page_order);
|
||||||
|
|
||||||
$serverFallback = LOCAL_SERVER_URL;
|
$serverFallback = IMG_SERVER_URL;
|
||||||
$serverNetwork = null;
|
$serverNetwork = null;
|
||||||
// when a chapter does not exist on the local webserver, it gets an id
|
|
||||||
// since all imageservers share the same data, we can assign any imageserver with the best location to the user
|
// use md@h for all images
|
||||||
if ($chapter->server > 0) {
|
|
||||||
if ($this->user->md_at_home ?? false) {
|
|
||||||
try {
|
try {
|
||||||
$subsubdomain = $this->mdAtHomeClient->getServerUrl($chapter->chapter_hash, $pages, _IP);
|
$subsubdomain = $this->mdAtHomeClient->getServerUrl($chapter->chapter_hash, $pages, _IP, $this->user->mdh_portlimit ?? false);
|
||||||
if (!empty($subsubdomain)) {
|
if (!empty($subsubdomain)) {
|
||||||
$serverNetwork = $subsubdomain;
|
$serverNetwork = $subsubdomain;
|
||||||
}
|
}
|
||||||
} catch (\Throwable $t) {
|
} catch (\Throwable $t) {
|
||||||
trigger_error($t->getMessage(), E_USER_WARNING);
|
trigger_error($t->getMessage(), E_USER_WARNING);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
$serverId = -1;
|
|
||||||
if ($this->request->query->has('server')) {
|
|
||||||
// if the parameter was trash, this returns -1
|
|
||||||
$serverId = get_server_id_by_code($this->request->query->get('server'));
|
|
||||||
}
|
|
||||||
if ($serverId < 1) {
|
|
||||||
// try to select a region-based server if we haven't one set already
|
|
||||||
$serverId = get_server_id_by_geography();
|
|
||||||
}
|
|
||||||
if ($serverId > 0) {
|
|
||||||
$serverFallback = "https://s$serverId.mangadex.org";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$server = $serverNetwork ?: $serverFallback;
|
$server = $serverNetwork ?: $serverFallback;
|
||||||
$dataDir = $this->request->query->getBoolean('saver') ? '/data-saver/' : '/data/';
|
$dataDir = $this->request->query->getBoolean('saver') ? '/data-saver/' : '/data/';
|
||||||
|
|
||||||
|
@ -8,10 +8,9 @@ class FollowsController extends APIController
|
|||||||
{
|
{
|
||||||
public function view($path)
|
public function view($path)
|
||||||
{
|
{
|
||||||
/**
|
$id = $path[0] ?? null;
|
||||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
$subResource = $path[1] ?? null;
|
||||||
*/
|
$subResourceId = $path[2] ?? null;
|
||||||
[$id, $subResource, $subResourceId] = $path;
|
|
||||||
|
|
||||||
if (!empty($id)) {
|
if (!empty($id)) {
|
||||||
throw new NotFoundHttpException();
|
throw new NotFoundHttpException();
|
||||||
|
@ -8,10 +8,9 @@ class GroupController extends APIController
|
|||||||
{
|
{
|
||||||
public function view($path)
|
public function view($path)
|
||||||
{
|
{
|
||||||
/**
|
$id = $path[0] ?? null;
|
||||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
$subResource = $path[1] ?? null;
|
||||||
*/
|
$subResourceId = $path[2] ?? null;
|
||||||
[$id, $subResource, $subResourceId] = $path;
|
|
||||||
|
|
||||||
$id = $this->validateId($id);
|
$id = $this->validateId($id);
|
||||||
|
|
||||||
|
42
src/Controller/API/HighestChapterIDController.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Mangadex\Controller\API;
|
||||||
|
|
||||||
|
use Mangadex\Exception\Http\ForbiddenHttpException;
|
||||||
|
|
||||||
|
class HighestChapterIDController extends APIController
|
||||||
|
{
|
||||||
|
public function view($path)
|
||||||
|
{
|
||||||
|
$isAuthorized = validate_level($this->user, 'pr') || $this->request->headers->get("API_KEY") === PRIVATE_API_KEY;
|
||||||
|
if (!$isAuthorized) {
|
||||||
|
throw new ForbiddenHttpException("You are not authorized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $this->fetch();
|
||||||
|
$normalized = $this->normalize($id);
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetch()
|
||||||
|
{
|
||||||
|
global $sql;
|
||||||
|
return $sql->prep(
|
||||||
|
"latest_chapter_id",
|
||||||
|
" SELECT MAX(chapter_id) FROM mangadex_chapters",
|
||||||
|
[],
|
||||||
|
'fetchColumn',
|
||||||
|
'',
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalize($id)
|
||||||
|
{
|
||||||
|
$normalized = [
|
||||||
|
'id' => $id,
|
||||||
|
];
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ class IndexController extends APIController
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
"information" => "Authentication is achieved by the same means as logging in to the site (i.e. the mangadex_session, mangadex_rememberme_token cookies, correct User-Agent). Some chapters may require authenticated permissions to access. The Content-Type header for requests with bodies must be application/json, and the content must be valid JSON. Boolean query values are evaluated 1/true/on/yes for true, otherwise false.",
|
"information" => "Authentication is achieved by the same means as logging in to the site (i.e. the mangadex_session, mangadex_rememberme_token cookies, correct User-Agent). Some chapters may require authenticated permissions to access. The Content-Type header for requests with bodies must be application/json, and the content must be valid JSON. Boolean query values are evaluated 1/true/on/yes for true, otherwise false.",
|
||||||
"baseUrl" => URL . "api/v2",
|
"baseUrl" => defined('API_V2_URL') ? API_V2_URL : "https://api.mangadex.org/v2/",
|
||||||
"resources" => [
|
"resources" => [
|
||||||
"GET /" => [
|
"GET /" => [
|
||||||
"description" => "The current page, the API index.",
|
"description" => "The current page, the API index.",
|
||||||
@ -29,6 +29,7 @@ class IndexController extends APIController
|
|||||||
"queryParameters" => [
|
"queryParameters" => [
|
||||||
"p" => "(Optional) The current page of the paginated results, starting from 1. Integer, default disables pagination.",
|
"p" => "(Optional) The current page of the paginated results, starting from 1. Integer, default disables pagination.",
|
||||||
"limit" => "(Optional) The limit of the paginated results, allowed range 10 - 100. Integer, default 100.",
|
"limit" => "(Optional) The limit of the paginated results, allowed range 10 - 100. Integer, default 100.",
|
||||||
|
"blockgroups" => "(Optional) Do not include chapters by groups blocked by the user. Boolean, default true.",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"GET /manga/{id}/covers" => [
|
"GET /manga/{id}/covers" => [
|
||||||
@ -61,6 +62,7 @@ class IndexController extends APIController
|
|||||||
"queryParameters" => [
|
"queryParameters" => [
|
||||||
"p" => "(Optional) The current page of the paginated results, starting from 1. Integer, default disables pagination.",
|
"p" => "(Optional) The current page of the paginated results, starting from 1. Integer, default disables pagination.",
|
||||||
"limit" => "(Optional) The limit of the paginated results, allowed range 10 - 100. Integer, default 100.",
|
"limit" => "(Optional) The limit of the paginated results, allowed range 10 - 100. Integer, default 100.",
|
||||||
|
"blockgroups" => "(Optional) Do not include chapters by groups blocked by the user. Boolean, default true.",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -79,13 +81,18 @@ class IndexController extends APIController
|
|||||||
"queryParameters" => [
|
"queryParameters" => [
|
||||||
"p" => "(Optional) The current page of the paginated results, starting from 1. Integer, default disables pagination.",
|
"p" => "(Optional) The current page of the paginated results, starting from 1. Integer, default disables pagination.",
|
||||||
"limit" => "(Optional) The limit of the paginated results, allowed range 10 - 100. Integer, default 100.",
|
"limit" => "(Optional) The limit of the paginated results, allowed range 10 - 100. Integer, default 100.",
|
||||||
|
"blockgroups" => "(Optional) Do not include chapters by groups blocked by the user. Boolean, default true.",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"GET /user/{id}/settings" => [
|
"GET /user/{id}/settings" => [
|
||||||
"description" => "(Authorization required) Get a user's website settings.",
|
"description" => "(Authorization required) Get a user's website settings.",
|
||||||
],
|
],
|
||||||
"GET /user/{id}/followed-manga" => [
|
"GET /user/{id}/followed-manga" => [
|
||||||
"description" => "(Authorization required) Get a user's followed manga and personal data for them.",
|
"description" => "(Authorization required) Get a user's followed manga and personal data for them. The target user's MDList privacy setting is taken into account when determining authorization.",
|
||||||
|
"queryParameters" => [
|
||||||
|
"type" => "(Optional) Filter the results by the follow type ID (i.e. 1 = Reading, 2 = Completed etc). Use 0 to remove filtering. Integer, default 0.",
|
||||||
|
"hentai" => "(Optional) Filter results based on whether the titles are marked as hentai. 0 = Hide H, 1 = Show all, 2 = Show H only. Integer, default 0.",
|
||||||
|
],
|
||||||
],
|
],
|
||||||
"GET /user/{id}/followed-updates" => [
|
"GET /user/{id}/followed-updates" => [
|
||||||
"description" => "(Authorization required) Get the latest uploaded chapters for the manga that the user has followed, as well as basic related manga information. Ordered by timestamp descending (the datetime when the chapter is available). Limit 100 chapters per page. Note that the results are automatically filtered by the authorized user's chapter language filter setting.",
|
"description" => "(Authorization required) Get the latest uploaded chapters for the manga that the user has followed, as well as basic related manga information. Ordered by timestamp descending (the datetime when the chapter is available). Limit 100 chapters per page. Note that the results are automatically filtered by the authorized user's chapter language filter setting.",
|
||||||
@ -94,6 +101,7 @@ class IndexController extends APIController
|
|||||||
"type" => "(Optional) Filter the results by the follow type ID (i.e. 1 = Reading, 2 = Completed etc). Use 0 to remove filtering. Integer, default 0.",
|
"type" => "(Optional) Filter the results by the follow type ID (i.e. 1 = Reading, 2 = Completed etc). Use 0 to remove filtering. Integer, default 0.",
|
||||||
"hentai" => "(Optional) Filter results based on whether the titles are marked as hentai. 0 = Hide H, 1 = Show all, 2 = Show H only. Integer, default 0.",
|
"hentai" => "(Optional) Filter results based on whether the titles are marked as hentai. 0 = Hide H, 1 = Show all, 2 = Show H only. Integer, default 0.",
|
||||||
"delayed" => "(Optional) Include delayed chapters in the results. Boolean, default false.",
|
"delayed" => "(Optional) Include delayed chapters in the results. Boolean, default false.",
|
||||||
|
"blockgroups" => "(Optional) Do not include chapters by groups blocked by the user. Boolean, default true.",
|
||||||
//"langs" => "(Optional) Filter results based on the scanlation language. Use a comma-separated list of language IDs.",
|
//"langs" => "(Optional) Filter results based on the scanlation language. Use a comma-separated list of language IDs.",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -8,10 +8,9 @@ class MangaController extends APIController
|
|||||||
{
|
{
|
||||||
public function view($path)
|
public function view($path)
|
||||||
{
|
{
|
||||||
/**
|
$id = $path[0] ?? null;
|
||||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
$subResource = $path[1] ?? null;
|
||||||
*/
|
$subResourceId = $path[2] ?? null;
|
||||||
[$id, $subResource, $subResourceId] = $path;
|
|
||||||
|
|
||||||
$id = $this->validateId($id);
|
$id = $this->validateId($id);
|
||||||
|
|
||||||
@ -45,20 +44,22 @@ class MangaController extends APIController
|
|||||||
return $manga;
|
return $manga;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalize($manga)
|
public function normalize($manga)
|
||||||
{
|
{
|
||||||
$coverPath = "/images/manga/$manga->manga_id.$manga->manga_image";
|
|
||||||
|
|
||||||
$normalized = [
|
$normalized = [
|
||||||
//'type' => 'manga',
|
//'type' => 'manga',
|
||||||
'id' => $manga->manga_id,
|
'id' => $manga->manga_id,
|
||||||
'title' => $manga->manga_name,
|
'title' => $manga->manga_name,
|
||||||
'altTitles' => array_map(function ($alt_name) {
|
'altTitles' => array_map(function ($alt_name) {
|
||||||
return \html_entity_decode($alt_name);
|
return trim(\html_entity_decode($alt_name));
|
||||||
}, $manga->get_manga_alt_names()),
|
}, $manga->get_manga_alt_names()),
|
||||||
'description' => $manga->manga_description,
|
'description' => $manga->manga_description,
|
||||||
'artist' => explode(',', $manga->manga_artist),
|
'artist' => array_map(function ($a) {
|
||||||
'author' => explode(',', $manga->manga_author),
|
return trim($a);
|
||||||
|
}, explode(',', $manga->manga_artist)),
|
||||||
|
'author' => array_map(function ($a) {
|
||||||
|
return trim($a);
|
||||||
|
}, explode(',', $manga->manga_author)),
|
||||||
'publication' => [
|
'publication' => [
|
||||||
'language' => $manga->lang_flag,
|
'language' => $manga->lang_flag,
|
||||||
'status' => $manga->manga_status_id,
|
'status' => $manga->manga_status_id,
|
||||||
@ -74,7 +75,7 @@ class MangaController extends APIController
|
|||||||
'id' => $relation['related_manga_id'],
|
'id' => $relation['related_manga_id'],
|
||||||
'title' => $relation['manga_name'],
|
'title' => $relation['manga_name'],
|
||||||
'type' => $relation['relation_id'],
|
'type' => $relation['relation_id'],
|
||||||
'isHentai' => (bool)$relation['hentai'],
|
'isHentai' => (bool)$relation['manga_hentai'],
|
||||||
];
|
];
|
||||||
}, $manga->get_related_manga()),
|
}, $manga->get_related_manga()),
|
||||||
'rating' => [
|
'rating' => [
|
||||||
|
@ -8,10 +8,9 @@ class RelationTypeController extends APIController
|
|||||||
{
|
{
|
||||||
public function view($path)
|
public function view($path)
|
||||||
{
|
{
|
||||||
/**
|
$id = $path[0] ?? null;
|
||||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
$subResource = $path[1] ?? null;
|
||||||
*/
|
$subResourceId = $path[2] ?? null;
|
||||||
[$id, $subResource, $subResourceId] = $path;
|
|
||||||
|
|
||||||
if (!empty($id)) {
|
if (!empty($id)) {
|
||||||
throw new NotFoundHttpException();
|
throw new NotFoundHttpException();
|
||||||
|
@ -8,10 +8,9 @@ class TagController extends APIController
|
|||||||
{
|
{
|
||||||
public function view($path)
|
public function view($path)
|
||||||
{
|
{
|
||||||
/**
|
$id = $path[0] ?? null;
|
||||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
$subResource = $path[1] ?? null;
|
||||||
*/
|
$subResourceId = $path[2] ?? null;
|
||||||
[$id, $subResource, $subResourceId] = $path;
|
|
||||||
|
|
||||||
if ($id) {
|
if ($id) {
|
||||||
$id = $this->validateId($id);
|
$id = $this->validateId($id);
|
||||||
|
@ -24,12 +24,26 @@ class UserController extends APIController
|
|||||||
return $id == $this->user->user_id || ($level !== null && validate_level($this->user, $level));
|
return $id == $this->user->user_id || ($level !== null && validate_level($this->user, $level));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function isAuthorizedUserForMangaList($targetUser, $level)
|
||||||
|
{
|
||||||
|
if ($this->isAuthorizedUser($targetUser->user_id, $level)) {
|
||||||
|
return true;
|
||||||
|
} else if ($targetUser->list_privacy === 1) {
|
||||||
|
return true;
|
||||||
|
} else if ($targetUser->list_privacy === 2) {
|
||||||
|
$friends = $targetUser->get_friends_user_ids();
|
||||||
|
if ($friends[$this->user->user_id]['accepted'] ?? false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function view($path)
|
public function view($path)
|
||||||
{
|
{
|
||||||
/**
|
$id = $path[0] ?? null;
|
||||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
$subResource = $path[1] ?? null;
|
||||||
*/
|
$subResourceId = $path[2] ?? null;
|
||||||
[$id, $subResource, $subResourceId] = $path;
|
|
||||||
|
|
||||||
$id = $this->validateId($id);
|
$id = $this->validateId($id);
|
||||||
|
|
||||||
@ -38,7 +52,8 @@ class UserController extends APIController
|
|||||||
$this->fetch($id); // check if exists
|
$this->fetch($id); // check if exists
|
||||||
return (new ChapterController())->fetchForUser($id);
|
return (new ChapterController())->fetchForUser($id);
|
||||||
case 'followed-manga':
|
case 'followed-manga':
|
||||||
if (!$this->isAuthorizedUser($id)) {
|
$user = $this->fetch($id);
|
||||||
|
if (!$this->isAuthorizedUserForMangaList($user, 'mod')) {
|
||||||
throw new ForbiddenHttpException();
|
throw new ForbiddenHttpException();
|
||||||
}
|
}
|
||||||
return $this->fetchFollowedManga($id);
|
return $this->fetchFollowedManga($id);
|
||||||
@ -123,10 +138,13 @@ class UserController extends APIController
|
|||||||
return [
|
return [
|
||||||
'userId' => $userId,
|
'userId' => $userId,
|
||||||
'mangaId' => $data['manga_id'],
|
'mangaId' => $data['manga_id'],
|
||||||
|
'mangaTitle' => $data['title'],
|
||||||
|
'isHentai' => (bool) $data['manga_hentai'],
|
||||||
'followType' => $data['follow_type'],
|
'followType' => $data['follow_type'],
|
||||||
'volume' => $data['volume'],
|
'volume' => $data['volume'],
|
||||||
'chapter' => $data['chapter'],
|
'chapter' => $data['chapter'],
|
||||||
'rating' => $data['rating'] ?: null,
|
'rating' => $data['rating'] ?: null,
|
||||||
|
'mainCover' => $data['manga_image'] ? $this->getFileUrl("/images/manga/{$data['manga_id']}.{$data['manga_image']}") : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,9 +152,21 @@ class UserController extends APIController
|
|||||||
{
|
{
|
||||||
$userResource = $this->fetch($id);
|
$userResource = $this->fetch($id);
|
||||||
$follows = $userResource->get_followed_manga_ids_api();
|
$follows = $userResource->get_followed_manga_ids_api();
|
||||||
return array_map(function ($data) use ($id) {
|
if ($this->request->query->has('type')) {
|
||||||
|
$type = $this->request->query->getInt('type');
|
||||||
|
$follows = array_filter($follows, function ($m) use ($type) {
|
||||||
|
return $m['follow_type'] === $type;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$hentai = $this->request->query->getInt('hentai', 0);
|
||||||
|
if ($hentai !== 1) {
|
||||||
|
$follows = array_filter($follows, function ($m) use ($hentai) {
|
||||||
|
return $m['manga_hentai'] === 0 && $hentai === 0 || $m['manga_hentai'] === 1 && $hentai === 2;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return array_values(array_map(function ($data) use ($id) {
|
||||||
return $this->normalizeMangaUserData($id, $data);
|
return $this->normalizeMangaUserData($id, $data);
|
||||||
}, $follows);
|
}, $follows));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fetchFollowedUpdates($id)
|
public function fetchFollowedUpdates($id)
|
||||||
@ -148,7 +178,7 @@ class UserController extends APIController
|
|||||||
public function fetchMangaUserData($id, $mangaId)
|
public function fetchMangaUserData($id, $mangaId)
|
||||||
{
|
{
|
||||||
$userResource = $this->fetch($id);
|
$userResource = $this->fetch($id);
|
||||||
$data = $userResource->get_manga_userdata($mangaId)[0] ?? null;
|
$data = $userResource->get_manga_userdata($mangaId) ?? null;
|
||||||
if ($data === null) {
|
if ($data === null) {
|
||||||
throw new NotFoundHttpException("Manga not found.");
|
throw new NotFoundHttpException("Manga not found.");
|
||||||
}
|
}
|
||||||
@ -171,20 +201,28 @@ class UserController extends APIController
|
|||||||
public function fetchSettings($id)
|
public function fetchSettings($id)
|
||||||
{
|
{
|
||||||
$user = $this->fetch($id);
|
$user = $this->fetch($id);
|
||||||
|
$langIds = explode(',', $user->default_lang_ids ?? '');
|
||||||
|
$exludedTags = explode(',', $user->excluded_genres ?? '');
|
||||||
return [
|
return [
|
||||||
'id' => $user->user_id,
|
'id' => $user->user_id,
|
||||||
'hentaiMode' => $user->hentai_mode,
|
'hentaiMode' => $user->hentai_mode,
|
||||||
'latestUpdates' => $user->latest_updates,
|
'latestUpdates' => $user->latest_updates,
|
||||||
'showModeratedPosts' => (bool)$user->display_moderated,
|
'showModeratedPosts' => (bool)$user->display_moderated,
|
||||||
'showUnavailableChapters' => (bool)$user->show_unavailable,
|
'showUnavailableChapters' => (bool)$user->show_unavailable,
|
||||||
'shownChapterLangs' => explode(',', $user->default_lang_ids ?: ''),
|
'shownChapterLangs' => array_map(function ($id) {
|
||||||
'excludedTags' => explode(',', $user->excluded_genres ?: ''),
|
return ['id' => $id];
|
||||||
|
}, $langIds),
|
||||||
|
'excludedTags' => array_map(function ($id) {
|
||||||
|
return ['id' => (int)$id];
|
||||||
|
}, $exludedTags),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create($path)
|
public function create($path)
|
||||||
{
|
{
|
||||||
[$id, $subResource, $subResourceId] = $path;
|
$id = $path[0] ?? null;
|
||||||
|
$subResource = $path[1] ?? null;
|
||||||
|
$subResourceId = $path[2] ?? null;
|
||||||
|
|
||||||
$id = $this->validateId($id);
|
$id = $this->validateId($id);
|
||||||
$content = $this->decodeJSONContent();
|
$content = $this->decodeJSONContent();
|
||||||
|
@ -75,6 +75,10 @@ class Guard
|
|||||||
$check = $this->sql->prep('user_rememberme_check', 'SELECT user_id, region_data FROM mangadex_sessions WHERE session_token = ? AND created > UNIX_TIMESTAMP() - ?',
|
$check = $this->sql->prep('user_rememberme_check', 'SELECT user_id, region_data FROM mangadex_sessions WHERE session_token = ? AND created > UNIX_TIMESTAMP() - ?',
|
||||||
[$rememberMeToken, SESSION_REMEMBERME_TIMEOUT], 'fetch', \PDO::FETCH_ASSOC, -1);
|
[$rememberMeToken, SESSION_REMEMBERME_TIMEOUT], 'fetch', \PDO::FETCH_ASSOC, -1);
|
||||||
|
|
||||||
|
if (!$check) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$tokenRegionData = json_decode($check['region_data'], 1);
|
$tokenRegionData = json_decode($check['region_data'], 1);
|
||||||
$userRegionData = $this->getClientDetails();
|
$userRegionData = $this->getClientDetails();
|
||||||
|
|
||||||
@ -142,7 +146,7 @@ class Guard
|
|||||||
$sessionInfo['updated'] = time();
|
$sessionInfo['updated'] = time();
|
||||||
$sessionInfo['ip'] = _IP;
|
$sessionInfo['ip'] = _IP;
|
||||||
|
|
||||||
$this->memcached->set('session:'.$sessionId, $sessionInfo, time() + SESSION_TIMEOUT);
|
$this->memcached->setSynced('session:'.$sessionId, $sessionInfo, time() + SESSION_TIMEOUT);
|
||||||
setcookie(SESSION_COOKIE_NAME,
|
setcookie(SESSION_COOKIE_NAME,
|
||||||
$sessionId,
|
$sessionId,
|
||||||
time() + SESSION_TIMEOUT,
|
time() + SESSION_TIMEOUT,
|
||||||
@ -169,7 +173,7 @@ class Guard
|
|||||||
$sessionInfo['userid']
|
$sessionInfo['userid']
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
$this->memcached->set("user_{$sessionInfo['userid']}_lastseen", 1, 60);
|
$this->memcached->setSynced("user_{$sessionInfo['userid']}_lastseen", 1, 60);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No session found? It could've been kicked out of memcached. Lets destroy the session cookie
|
// No session found? It could've been kicked out of memcached. Lets destroy the session cookie
|
||||||
@ -196,7 +200,7 @@ class Guard
|
|||||||
'userid' => (int)$userId,
|
'userid' => (int)$userId,
|
||||||
'is_rememberme' => $isRemembermeSession,
|
'is_rememberme' => $isRemembermeSession,
|
||||||
];
|
];
|
||||||
$this->memcached->set('session:'.$sessionId, $sessionInfo, time() + SESSION_TIMEOUT);
|
$this->memcached->setSynced('session:'.$sessionId, $sessionInfo, time() + SESSION_TIMEOUT);
|
||||||
setcookie(SESSION_COOKIE_NAME,
|
setcookie(SESSION_COOKIE_NAME,
|
||||||
$sessionId,
|
$sessionId,
|
||||||
time() + SESSION_TIMEOUT,
|
time() + SESSION_TIMEOUT,
|
||||||
@ -249,6 +253,9 @@ class Guard
|
|||||||
public function verifyUserCredentials($userId, $rawPassword)
|
public function verifyUserCredentials($userId, $rawPassword)
|
||||||
{
|
{
|
||||||
$user = $this->getUser($userId);
|
$user = $this->getUser($userId);
|
||||||
|
if (!$user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return password_verify($rawPassword, $user->password);
|
return password_verify($rawPassword, $user->password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,22 +5,46 @@ namespace Mangadex\Model;
|
|||||||
class MdexAtHomeClient
|
class MdexAtHomeClient
|
||||||
{
|
{
|
||||||
|
|
||||||
public function getServerUrl(string $chapterHash, array $chapterPages, string $ip): string
|
public function getServerUrl(string $chapterHash, array $chapterPages, string $ip, bool $onlySsl): string
|
||||||
{
|
{
|
||||||
$path = '/assign';
|
$path = '/assign';
|
||||||
$payload = [
|
$payload = [
|
||||||
'ip' => $ip,
|
'ip' => $ip,
|
||||||
'hash' => $chapterHash,
|
'hash' => $chapterHash,
|
||||||
'images' => $chapterPages,
|
'images' => $chapterPages,
|
||||||
|
'only_443' => $onlySsl,
|
||||||
];
|
];
|
||||||
|
|
||||||
$ch = $this->getCurl($path, $ip.$chapterHash.implode($chapterPages), $payload);
|
$ch = $this->getCurl($path, $ip.$chapterHash.implode($chapterPages), $payload);
|
||||||
|
|
||||||
$res = curl_exec($ch);
|
return $this->queryAssign($ch, $payload);
|
||||||
curl_close($ch);
|
|
||||||
if ($res === false) {
|
|
||||||
throw new \RuntimeException('MD@H::getServerUrl curl error: '.curl_error($ch));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getClientUrl(string $chapterHash, array $chapterPages, string $ip, string $clientId): string
|
||||||
|
{
|
||||||
|
$path = '/assign';
|
||||||
|
$payload = [
|
||||||
|
'ip' => $ip,
|
||||||
|
'hash' => $chapterHash,
|
||||||
|
'images' => $chapterPages,
|
||||||
|
'client_id' => $clientId,
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = $this->getCurl($path, $ip.$chapterHash.implode($chapterPages), $payload);
|
||||||
|
|
||||||
|
return $this->queryAssign($ch, $payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function queryAssign($ch, $payload): string
|
||||||
|
{
|
||||||
|
$res = curl_exec($ch);
|
||||||
|
if ($res === false) {
|
||||||
|
$error = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
throw new \RuntimeException('MD@H::getServerUrl curl error: '.$error);
|
||||||
|
}
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
$dec = \json_decode($res, true);
|
$dec = \json_decode($res, true);
|
||||||
if (!$dec) {
|
if (!$dec) {
|
||||||
throw new \RuntimeException('MD@H::getServerUrl failed to decode: '.$res);
|
throw new \RuntimeException('MD@H::getServerUrl failed to decode: '.$res);
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover table-sm">
|
<table class="table table-striped table-hover table-sm">
|
||||||
<?php foreach ($templateVar['stats'] ?? [] AS $dsn => $serverStats) : ?>
|
<?php foreach ($templateVar['stats'] ?? [] AS $dsn => $serverStats) : ?>
|
||||||
<?php if (empty($serverStats)) continue; ?>
|
<?php if (empty($serverStats) || empty($serverStats[0] ?? [])) continue; ?>
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-top-0">
|
<tr class="border-top-0">
|
||||||
<th colspan="2">Server: <?=$dsn?></th>
|
<th colspan="2">Server: <?=$dsn?></th>
|
||||||
@ -24,8 +24,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($serverStats as $key => $val) : ?>
|
<?php foreach ($serverStats[0] as $key => $val) : ?>
|
||||||
<?php if (!in_array($key, ['Slave_IO_State', 'Slave_IO_Running', 'Seconds_Behind_Master'])) continue; ?>
|
<?php if (!in_array($key, ['Relay_Log_File', 'Relay_Log_Space', 'Last_Error', 'Last_IO_Error', 'Last_SQL_Error', 'Slave_SQL_Running_State', 'Slave_IO_State', 'Slave_IO_Running', 'Seconds_Behind_Master'])) continue; ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?=$key?></td>
|
<td><?=$key?></td>
|
||||||
<td><?=$val?></td>
|
<td><?=$val?></td>
|
||||||
|
27
templates/bootstrap4/ads/mobile_app_ad.tpl.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!-- ad template -->
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$banner = $templateVar[array_rand($templateVar)];
|
||||||
|
?>
|
||||||
|
<?php if (!isset($_COOKIE["hide_banner"])) : ?>
|
||||||
|
<div id="affiliate-banner" class="affiliate-banner mb-3">
|
||||||
|
<div class="d-flex position-relative">
|
||||||
|
<img id="close-banner" src="/images/banners/close.png" style="right:0;top:0;bottom:0;height:100%;cursor:pointer;" class="position-absolute"><a href="/support/affiliates">
|
||||||
|
<img class="w-100" src="/images/banners/affiliatebanner<?= $banner['banner_id'] ?>.<?= $banner['ext'] ?>">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="d-none d-md-block" style="text-align: right;margin-right:20px;">
|
||||||
|
This affiliate banner was designed by <?= $banner["is_anonymous"] ? "an anonymous user" : display_user_link_v2($banner) ?>
|
||||||
|
</div>
|
||||||
|
<div class="d-md-none small" style="text-align: right;margin-right:20px;">
|
||||||
|
This affiliate banner was designed by <?= $banner["is_anonymous"] ? "an anonymous user" : display_user_link_v2($banner) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
const closeButton = document.querySelector('#close-banner')
|
||||||
|
closeButton.addEventListener('click', evt => {
|
||||||
|
document.querySelector("#affiliate-banner").classList.add("d-none")
|
||||||
|
document.cookie = "hide_banner=true; max-age=2678400"
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
|
<?= parse_template('ads/mobile_app_ad', $templateVar['banners']) ?>
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<h6 class="card-header text-center"><?= display_fa_icon('external-link-alt') ?> <a href="/updates">Latest updates</a></h6>
|
<h6 class="card-header text-center"><?= display_fa_icon('external-link-alt') ?> <a href="/updates">Latest updates</a></h6>
|
||||||
<div class="card-header p-0">
|
<div class="card-header p-0">
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
$env = (defined('DEBUG') && DEBUG) ? 'dev' : 'prod';
|
||||||
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
@ -29,11 +33,22 @@
|
|||||||
<title><?= $templateVar['og']['title'] ?></title>
|
<title><?= $templateVar['og']['title'] ?></title>
|
||||||
|
|
||||||
<!-- Google Tag Manager -->
|
<!-- Google Tag Manager -->
|
||||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
<script>
|
||||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
(function(w, d, s, l, i) {
|
||||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
w[l] = w[l] || [];
|
||||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
w[l].push({
|
||||||
})(window,document,'script','dataLayer','GTM-TS59XX9');</script>
|
'gtm.start': new Date().getTime(),
|
||||||
|
event: 'gtm.js'
|
||||||
|
});
|
||||||
|
var f = d.getElementsByTagName(s)[0],
|
||||||
|
j = d.createElement(s),
|
||||||
|
dl = l != 'dataLayer' ? '&l=' + l : '';
|
||||||
|
j.async = true;
|
||||||
|
j.src =
|
||||||
|
'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
|
||||||
|
f.parentNode.insertBefore(j, f);
|
||||||
|
})(window, document, 'script', 'dataLayer', 'GTM-TS59XX9');
|
||||||
|
</script>
|
||||||
<!-- End Google Tag Manager -->
|
<!-- End Google Tag Manager -->
|
||||||
|
|
||||||
<!-- Google fonts -->
|
<!-- Google fonts -->
|
||||||
@ -69,11 +84,7 @@
|
|||||||
<link href="/scripts/css/reader.css?<?= @filemtime(ABSPATH . "/scripts/css/reader.css") ?>" rel="stylesheet" />
|
<link href="/scripts/css/reader.css?<?= @filemtime(ABSPATH . "/scripts/css/reader.css") ?>" rel="stylesheet" />
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<?php if (defined('DEBUG') && DEBUG): ?>
|
<script type="module" src="/dist/js/bundle.<?= $env ?>.js?<?= @filemtime(ABSPATH . "/dist/js/bundle.$env.js") ?>"></script>
|
||||||
<script type="module" src="/dist/js/bundle.dev.js?<?= @filemtime(ABSPATH . "/dist/js/bundle.dev.js") ?>"></script>
|
|
||||||
<?php else: ?>
|
|
||||||
<script type="module" src="/dist/js/bundle.prod.js?<?= @filemtime(ABSPATH . "/dist/js/bundle.prod.js") ?>"></script>
|
|
||||||
<?php endif; ?>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@ -102,7 +113,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
/** Print page content */
|
/** Print page content */
|
||||||
print $templateVar['page_html'];
|
print $templateVar['page_html'];
|
||||||
@ -196,7 +206,7 @@
|
|||||||
<?= parse_template('partials/report_modal', $templateVar); ?>
|
<?= parse_template('partials/report_modal', $templateVar); ?>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<p class="m-0 text-center text-muted">© <?= date('Y') ?> <a href="/" title="<?php print_r($templateVar['memcached']->get($templateVar['ip'])) ?>">MangaDex</a> | <a href="https://path.net/" target="_blank" title="Provider of DDoS mitigation services">Path Network</a> | <a href="https://sdbx.moe/" target="_blank" title="seedbox provider">sdbx.moe</a></p>
|
<p class="m-0 text-center text-muted">© <?= date('Y') ?> <a href="/" title="<?php print_r($templateVar['memcached']->get($templateVar['ip'])) ?>">MangaDex</a> | <a href="https://path.net/" target="_blank" title="Provider of DDoS mitigation services">Path Network</a> | <a href="https://sdbx.moe/" target="_blank" title="seedbox provider">sdbx.moe</a> | <a href="https://ddos-guard.net?affiliate=119953" target="_blank" title="ddos-guard">DDoS Protection by DDoS-GUARD</a> | <a href="https://onramper.com/" target="_blank" title="Crypto Widget">Onramper</a></p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
@ -224,20 +234,21 @@
|
|||||||
<script>
|
<script>
|
||||||
if (!('URL' in window) || !('URLSearchParams' in window)) {
|
if (!('URL' in window) || !('URLSearchParams' in window)) {
|
||||||
document.head.appendChild(Object.assign(document.createElement("script"), {
|
document.head.appendChild(Object.assign(document.createElement("script"), {
|
||||||
"src": "/dist/js/polyfills.prod.js?<?= @filemtime(ABSPATH . "/dist/js/polyfills.prod.js") ?>", "async": true,
|
"src": "/dist/js/polyfills.prod.js?<?= @filemtime(ABSPATH . "/dist/js/polyfills.prod.js") ?>",
|
||||||
|
"async": true,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<?php if (in_array($templateVar['page'], ['chapter', 'chapter_test']) && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?>
|
<?php if ($templateVar['page'] == 'chapter' && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?>
|
||||||
<script src="/scripts/modernizr-custom.js"></script>
|
<script src="/scripts/modernizr-custom.js"></script>
|
||||||
<?php }
|
<script async src="/dist/js/reader.<?= $env ?>.js?<?= @filemtime(ABSPATH . "/dist/js/reader.$env.js") ?>"></script>
|
||||||
if ($templateVar['page'] == 'chapter' && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?>
|
<?php if ($env !== 'prod') { ?>
|
||||||
<script async src="/scripts/reader.min.js?<?= @filemtime(ABSPATH . "/scripts/reader.min.js") ?>"></script>
|
<script nomodule src="/dist/js/reader.prod.js?<?= @filemtime(ABSPATH . "/dist/js/reader.prod.js") ?>"></script>
|
||||||
|
<?php } ?>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<script src="/scripts/js/reporting.js"></script>
|
<script src="/scripts/js/reporting.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
<?php if (defined('INCLUDE_JS_REDIRECT') && INCLUDE_JS_REDIRECT) : ?>
|
<?php if (defined('INCLUDE_JS_REDIRECT') && INCLUDE_JS_REDIRECT) : ?>
|
||||||
var t = 'mang';
|
var t = 'mang';
|
||||||
t = t + 'adex.org';
|
t = t + 'adex.org';
|
||||||
@ -344,7 +355,9 @@
|
|||||||
|
|
||||||
function highlightPost(node) {
|
function highlightPost(node) {
|
||||||
if (node) {
|
if (node) {
|
||||||
Array.from(document.querySelectorAll('.highlighted')).forEach(function(n) {n.classList.remove('highlighted')});
|
Array.from(document.querySelectorAll('.highlighted')).forEach(function(n) {
|
||||||
|
n.classList.remove('highlighted')
|
||||||
|
});
|
||||||
node.classList.add('highlighted')
|
node.classList.add('highlighted')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -360,7 +373,7 @@
|
|||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -42,9 +42,7 @@
|
|||||||
?>
|
?>
|
||||||
<img class="long-strip <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="/img.php?x=/data/<?= "{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
|
<img class="long-strip <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="/img.php?x=/data/<?= "{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
|
||||||
<?php
|
<?php
|
||||||
}
|
} else {
|
||||||
|
|
||||||
else {
|
|
||||||
?>
|
?>
|
||||||
<img class="long-strip <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="<?= "{$templateVar['server']}{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
|
<img class="long-strip <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="<?= "{$templateVar['server']}{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
|
||||||
<?php
|
<?php
|
||||||
@ -59,9 +57,7 @@
|
|||||||
?>
|
?>
|
||||||
<img class="webtoon <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="/img.php?x=/data/<?= "{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
|
<img class="webtoon <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="/img.php?x=/data/<?= "{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
|
||||||
<?php
|
<?php
|
||||||
}
|
} else {
|
||||||
|
|
||||||
else {
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<img class="webtoon <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="<?= "{$templateVar['server']}{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
|
<img class="webtoon <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="<?= "{$templateVar['server']}{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
|
||||||
@ -123,7 +119,9 @@
|
|||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<select required title="Select a reason" class="form-control selectpicker" id="type_id" name="type_id">
|
<select required title="Select a reason" class="form-control selectpicker" id="type_id" name="type_id">
|
||||||
<?php
|
<?php
|
||||||
$chapter_reasons = array_filter($templateVar['report_reasons'], function($reason) { return REPORT_TYPES[$reason['type_id']] === 'Chapter'; });
|
$chapter_reasons = array_filter($templateVar['report_reasons'], function ($reason) {
|
||||||
|
return REPORT_TYPES[$reason['type_id']] === 'Chapter';
|
||||||
|
});
|
||||||
foreach ($chapter_reasons as $reason) : ?>
|
foreach ($chapter_reasons as $reason) : ?>
|
||||||
<option value="<?= $reason['id'] ?>"><?= $reason['text'] ?><?= $reason['is_info_required'] ? ' *' : '' ?></option>
|
<option value="<?= $reason['id'] ?>"><?= $reason['text'] ?><?= $reason['is_info_required'] ? ' *' : '' ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@ -176,6 +174,15 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="data_saver" class="col-md-3 col-form-label">Data saver:</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<select class="form-control selectpicker" id="data_saver" name="data_saver">
|
||||||
|
<option <?= !$templateVar['user']->data_saver ? 'selected' : '' ?> value="0">Off</option>
|
||||||
|
<option <?= $templateVar['user']->data_saver ? 'selected' : '' ?> value="1">On</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="reader_mode" class="col-md-3 col-form-label">Reader mode:</label>
|
<label for="reader_mode" class="col-md-3 col-form-label">Reader mode:</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
|
@ -187,6 +187,7 @@ $links_array = ($templateVar['manga']->manga_links) ? json_decode($templateVar['
|
|||||||
<div class="col-lg-3 col-xl-2 strong">Mod:</div>
|
<div class="col-lg-3 col-xl-2 strong">Mod:</div>
|
||||||
<div class="col-lg-9 col-xl-10">
|
<div class="col-lg-9 col-xl-10">
|
||||||
<?= display_lock_manga($templateVar['user'], $templateVar['manga']) ?>
|
<?= display_lock_manga($templateVar['user'], $templateVar['manga']) ?>
|
||||||
|
<?= display_regenerate_manga_thumb($templateVar['user']) ?>
|
||||||
<?= display_delete_manga($templateVar['user']) ?>
|
<?= display_delete_manga($templateVar['user']) ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
<li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'stats') ? 'active' : '' ?>" href="/md_at_home/stats"><?= display_fa_icon('chart-line', 'Statistics') ?> <span class="d-none d-lg-inline">Statistics</span></a></li>
|
<li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'stats') ? 'active' : '' ?>" href="/md_at_home/stats"><?= display_fa_icon('chart-line', 'Statistics') ?> <span class="d-none d-lg-inline">Statistics</span></a></li>
|
||||||
<li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'request') ? 'active' : '' ?>" href="/md_at_home/request"><?= display_fa_icon('envelope', 'Request a client') ?> <span class="d-none d-lg-inline">Request a client</span></a></li>
|
<li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'request') ? 'active' : '' ?>" href="/md_at_home/request"><?= display_fa_icon('envelope', 'Request a client') ?> <span class="d-none d-lg-inline">Request a client</span></a></li>
|
||||||
<?php if (validate_level($templateVar['user'], 'member')) : ?>
|
<?php if (validate_level($templateVar['user'], 'member')) : ?>
|
||||||
<li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'options') ? 'active' : '' ?>" href="/md_at_home/options"><?= display_fa_icon('cog', 'Options') ?> <span class="d-none d-lg-inline">Options</span></a></li>
|
|
||||||
<li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'clients') ? 'active' : '' ?>" href="/md_at_home/clients"><?= display_fa_icon('server', 'My clients') ?> <span class="d-none d-lg-inline">My clients</span></a></li>
|
<li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'clients') ? 'active' : '' ?>" href="/md_at_home/clients"><?= display_fa_icon('server', 'My clients') ?> <span class="d-none d-lg-inline">My clients</span></a></li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (validate_level($templateVar['user'], 'admin')) : ?>
|
<?php if (validate_level($templateVar['user'], 'admin')) : ?>
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><?= display_fa_icon('hashtag') ?></th>
|
<th class="text-center"><?= display_fa_icon('hashtag') ?></th>
|
||||||
<th><?= display_fa_icon('user') ?></th>
|
<th><?= display_fa_icon('user') ?></th>
|
||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
<th><?= display_fa_icon('globe-asia') ?></th>
|
<th class="text-center"><?= display_fa_icon('network-wired', 'Test') ?></th>
|
||||||
<th><?= display_fa_icon('globe') ?></th>
|
<th class="text-center"><?= display_fa_icon('globe-asia') ?></th>
|
||||||
<th><?= display_fa_icon('bolt') ?></th>
|
<th class="text-center"><?= display_fa_icon('globe') ?></th>
|
||||||
<th><?= display_fa_icon('upload', 'Mbps') ?></th>
|
<th class="text-center"><?= display_fa_icon('bolt') ?></th>
|
||||||
<th><?= display_fa_icon('download', 'Mbps') ?></th>
|
<th class="text-center"><?= display_fa_icon('upload', 'Mbps') ?></th>
|
||||||
<th><?= display_fa_icon('hdd', 'GB', '', 'far') ?></th>
|
<th class="text-center"><?= display_fa_icon('download', 'Mbps') ?></th>
|
||||||
|
<th class="text-center"><?= display_fa_icon('hdd', 'GB', '', 'far') ?></th>
|
||||||
<th><?= display_fa_icon('calendar-alt') ?></th>
|
<th><?= display_fa_icon('calendar-alt') ?></th>
|
||||||
<th><?= display_fa_icon('key') ?></th>
|
<th><?= display_fa_icon('key') ?></th>
|
||||||
<th>Data transferred (GB)</th>
|
|
||||||
<th>Daily average (GB)</th>
|
|
||||||
<th><?= display_fa_icon('check') ?><?= display_fa_icon('times') ?></th>
|
<th><?= display_fa_icon('check') ?><?= display_fa_icon('times') ?></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -23,6 +22,7 @@
|
|||||||
<td>#<?= $client_id ?></td>
|
<td>#<?= $client_id ?></td>
|
||||||
<td><?= display_user_link($client->user_id, $client->username, $client->level_colour) ?></td>
|
<td><?= display_user_link($client->user_id, $client->username, $client->level_colour) ?></td>
|
||||||
<td><?= $client->client_ip ?></td>
|
<td><?= $client->client_ip ?></td>
|
||||||
|
<td class="text-center"><?= $client->approved === 1 ? "<a target='_blank' href='{$templateVar['backend']->getClientUrl('a61fa9f7f1313194787116d1357a7784', ['N9.jpg'], $templateVar['ip'], $client_id)}/data/a61fa9f7f1313194787116d1357a7784/N9.jpg' class='btn btn-sm btn-info' title='Test your client'>" . display_fa_icon('network-wired', 'Test') . "</a>" : "<button class='btn btn-sm btn-info disabled'>" . display_fa_icon('network-wired', 'Test') . "</button>"?></td>
|
||||||
<td><?= $client->client_continent ?></td>
|
<td><?= $client->client_continent ?></td>
|
||||||
<td><img src="/images/flags/<?= $client->client_country ?>.png" alt="<?= $client->client_country ?>" /></td>
|
<td><img src="/images/flags/<?= $client->client_country ?>.png" alt="<?= $client->client_country ?>" /></td>
|
||||||
<td><a href="<?= $client->speedtest ?>" target="_blank"><?= display_fa_icon('external-link-alt') ?></a></td>
|
<td><a href="<?= $client->speedtest ?>" target="_blank"><?= display_fa_icon('external-link-alt') ?></a></td>
|
||||||
@ -31,8 +31,6 @@
|
|||||||
<td><?= $client->disk_cache_size ?></td>
|
<td><?= $client->disk_cache_size ?></td>
|
||||||
<td><?= $client->timestamp ? date('Y-m-d H:i:s', $client->timestamp) . ' UTC' : '' ?></td>
|
<td><?= $client->timestamp ? date('Y-m-d H:i:s', $client->timestamp) . ' UTC' : '' ?></td>
|
||||||
<td><code><?= $client->client_secret ?></code></td>
|
<td><code><?= $client->client_secret ?></code></td>
|
||||||
<td>0</td>
|
|
||||||
<td>0</td>
|
|
||||||
<td>
|
<td>
|
||||||
<?php if ($client->approved === 0 || $client->approved === NULL) : ?>
|
<?php if ($client->approved === 0 || $client->approved === NULL) : ?>
|
||||||
<button class="btn btn-success btn-sm approve_button" data-id="<?= $client_id ?>"><?= display_fa_icon('check') ?></button>
|
<button class="btn btn-success btn-sm approve_button" data-id="<?= $client_id ?>"><?= display_fa_icon('check') ?></button>
|
||||||
@ -60,14 +58,13 @@
|
|||||||
<th></th>
|
<th></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
<th></th>
|
||||||
<th><?= $total_upload ?></th>
|
<th><?= $total_upload ?></th>
|
||||||
<th><?= $total_download ?></th>
|
<th><?= $total_download ?></th>
|
||||||
<th><?= $total_disk ?></th>
|
<th><?= $total_disk ?></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>total</th>
|
|
||||||
<th>avg</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
@ -2,19 +2,14 @@
|
|||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><?= display_fa_icon('hashtag', 'Client ID') ?></th>
|
<th class="text-center"><?= display_fa_icon('hashtag', 'Client ID') ?></th>
|
||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
<th class="text-center"><?= display_fa_icon('network-wired', 'Test') ?></th>
|
<th class="text-center"><?= display_fa_icon('network-wired', 'Test') ?></th>
|
||||||
<th class="text-center"><?= display_fa_icon('globe-asia', 'Continent') ?></th>
|
<th class="text-center"><?= display_fa_icon('globe-asia', 'Continent') ?></th>
|
||||||
<th class="text-center"><?= display_fa_icon('globe', 'Country') ?></th>
|
<th class="text-center"><?= display_fa_icon('globe', 'Country') ?></th>
|
||||||
<th class="text-center"><?= display_fa_icon('upload', 'Upload speed (Mbps)') ?></th>
|
|
||||||
<th class="text-center"><?= display_fa_icon('download', 'Download speed (Mbps)') ?></th>
|
|
||||||
<th class="text-center"><?= display_fa_icon('hdd', 'Disk allocation (GB)') ?></th>
|
|
||||||
<th class="text-center">Status</th>
|
<th class="text-center">Status</th>
|
||||||
<th><?= display_fa_icon('calendar-alt', 'Time of approval') ?></th>
|
<th><?= display_fa_icon('calendar-alt', 'Time of approval') ?></th>
|
||||||
<th><?= display_fa_icon('key', 'Client secret') ?></th>
|
<th><?= display_fa_icon('key', 'Client secret') ?></th>
|
||||||
<!--<th>Data transferred (GB)</th>
|
|
||||||
<th>Daily average (GB)</th>-->
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -27,72 +22,31 @@
|
|||||||
<tr class="text-<?= $client['approved'] ? 'success' : ($client['approved'] === 0 ? 'danger' : 'warning' ) ?>">
|
<tr class="text-<?= $client['approved'] ? 'success' : ($client['approved'] === 0 ? 'danger' : 'warning' ) ?>">
|
||||||
<td>#<?= $client['client_id'] ?></td>
|
<td>#<?= $client['client_id'] ?></td>
|
||||||
<td><?= $client['client_ip'] ?></td>
|
<td><?= $client['client_ip'] ?></td>
|
||||||
<td class="text-center"><?= $client['approved'] ? "<a target='_blank' href='https://{$client['client_subsubdomain']}.mangadex.network:{$client['client_port']}/data/a61fa9f7f1313194787116d1357a7784/N9.jpg' class='btn btn-sm btn-info' title='Test your client'>" . display_fa_icon('network-wired', 'Test') . "</a>" : "<button class='btn btn-sm btn-info disabled'>" . display_fa_icon('network-wired', 'Test') . "</button>"?></td>
|
<td class="text-center"><?= $client['approved'] ? "<a target='_blank' href='{$templateVar['backend']->getClientUrl('a61fa9f7f1313194787116d1357a7784', ['N9.jpg'], $templateVar['ip'], $client['client_id'])}/data/a61fa9f7f1313194787116d1357a7784/N9.jpg' class='btn btn-sm btn-info' title='Test your client'>" . display_fa_icon('network-wired', 'Test') . "</a>" : "<button class='btn btn-sm btn-info disabled'>" . display_fa_icon('network-wired', 'Test') . "</button>"?></td>
|
||||||
<td class="text-center"><?= $client['client_continent'] ?></td>
|
<td class="text-center"><?= $client['client_continent'] ?></td>
|
||||||
<td class="text-center"><img src="/images/flags/<?= $client['client_country'] ?>.png" alt="<?= $client['client_country'] ?>" title="<?= $client['client_country'] ?>" /></td>
|
<td class="text-center"><img src="/images/flags/<?= $client['client_country'] ?>.png" alt="<?= $client['client_country'] ?>" title="<?= $client['client_country'] ?>" /></td>
|
||||||
<td class="text-center"><?= $client['upload_speed'] ?></td>
|
|
||||||
<td class="text-center"><?= $client['download_speed'] ?></td>
|
|
||||||
<td class="text-center"><?= $client['disk_cache_size'] ?></td>
|
|
||||||
<td class="text-center"><?= $client['approved'] ? display_fa_icon('check', 'Approved') : ($client['approved'] === 0 ? display_fa_icon('times', 'rejected') : 'Pending' ) ?></td>
|
<td class="text-center"><?= $client['approved'] ? display_fa_icon('check', 'Approved') : ($client['approved'] === 0 ? display_fa_icon('times', 'rejected') : 'Pending' ) ?></td>
|
||||||
<td><?= $client['timestamp'] ? date('Y-m-d H:i:s', $client['timestamp']) . ' UTC' : '' ?></td>
|
<td><?= $client['timestamp'] ? date('Y-m-d H:i:s', $client['timestamp']) . ' UTC' : '' ?></td>
|
||||||
<td><code><?= $client['client_secret'] ?></code></td>
|
<td><code><?= $client['client_secret'] ?></code></td>
|
||||||
<!--<td>0</td>
|
<!--<td>0</td>
|
||||||
<td>0</td>-->
|
<td>0</td>-->
|
||||||
</tr>
|
</tr>
|
||||||
<?php
|
|
||||||
$total_upload += $client['upload_speed'];
|
|
||||||
$total_download += $client['download_speed'];
|
|
||||||
$total_disk += $client['disk_cache_size'];
|
|
||||||
?>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th><?= $total_upload ?></th>
|
|
||||||
<th><?= $total_download ?></th>
|
|
||||||
<th><?= $total_disk ?></th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<!--<th>total</th>
|
|
||||||
<th>avg</th>-->
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h3>Instructions: </h3>
|
<h3>Instructions: </h3>
|
||||||
<?php if ($templateVar['approvaltime']) : ?>
|
<?php if ($templateVar['approvaltime']) : ?>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Download the <a href="/dl/mangadex_at_home-1.2.2.zip">client</a> and included settings sample (right click and save as).</li>
|
<li>Download <a href="/dl/mangadex_at_home-2.0.0.zip">the client and settings file</a>.</li>
|
||||||
<pre class="bg-dark text-light p-2">md5: 7c0c8941544ec09f637a4e8e49204d96
|
<pre class="bg-dark text-light p-2">md5: e077e54df77d406d973ce269a7e7febb
|
||||||
sha-256: 68e26adf68268ae9781919fd5dd80a29595eabf562bbc0cda3dcbe332dd959d8</pre>
|
sha-256: ef4d139b346837b223c15032dd9f38790fe77d7b5f40320f1d82f6caf7a7bb10</pre>
|
||||||
<li>settings.json needs editing to your config.</li>
|
<li>settings.sample.yaml needs editing to your config, and renamed as settings.yaml</li>
|
||||||
<pre class="bg-dark text-light p-2">settings.json example:
|
|
||||||
{
|
|
||||||
"client_secret": "iiesenpaithisisoursecret",
|
|
||||||
"client_hostname": "0.0.0.0", // "0.0.0.0" is the default and binds to everything
|
|
||||||
"client_port": 443, // 443 is recommended if possible, otherwise use something higher, e.g. 44300
|
|
||||||
"client_external_port": 0, //443 is recommended; This port will be send to mdah-backend.
|
|
||||||
//You need to forward this to the client_port in your router - 0 uses `client_port`
|
|
||||||
"threads": 16,
|
|
||||||
"graceful_shutdown_wait_seconds": 60, // Time from graceful shutdown start to force quit
|
|
||||||
// This rounds down to 15-second increments
|
|
||||||
"max_cache_size_in_mebibytes": 80000,
|
|
||||||
"max_kilobits_per_second": 0, // 0 disables max brust limiting
|
|
||||||
"max_mebibytes_per_hour": 0, // 0 disables hourly bandwidth limiting
|
|
||||||
"web_settings": { // delete this block to disable webui
|
|
||||||
"ui_hostname": "127.0.0.1", // "127.0.0.1" is the default and binds to localhost only
|
|
||||||
"ui_port": 8080
|
|
||||||
}
|
|
||||||
}</pre>
|
|
||||||
<li>To start the client, put .jar and settings.json in same folder and run: </li>
|
<li>To start the client, put .jar and settings.json in same folder and run: </li>
|
||||||
<pre class="bg-dark text-light p-2">java -Dfile-level=trace -Dstdout-level=info -jar mangadex_at_home-1.2.2-all.jar</pre>
|
<pre class="bg-dark text-light p-2">java -Dfile-level=trace -Dstdout-level=info -jar mangadex_at_home-2.0.0-all.jar</pre>
|
||||||
<li>For no logging, run: </li>
|
<li>For no logging, run: </li>
|
||||||
<pre class="bg-dark text-light p-2">java -Dfile-level=off -Dstdout-level=info -jar mangadex_at_home-1.2.2-all.jar</pre>
|
<pre class="bg-dark text-light p-2">java -Dfile-level=off -Dstdout-level=info -jar mangadex_at_home-2.0.0-all.jar</pre>
|
||||||
<li>(The name of the .jar file will change in future versions, so edit accordingly.)</li>
|
<li>(The name of the .jar file will change in future versions, so edit accordingly.)</li>
|
||||||
<li>If you need help, come on <a href="https://discord.gg/mangadex">Discord</a>.</li>
|
<li>If you need help, come on <a href="https://discord.gg/mangadex">Discord</a>.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>An IPv4 address (static or dynamic).</li>
|
<li>An IPv4 address (static or dynamic).</li>
|
||||||
<li>A minimum network speed of 80Mbps up/down.</li>
|
<li>A minimum network speed of 40Mbps up/down.</li>
|
||||||
<li>A minimum of 40GB of dedicated storage space, preferably more.</li>
|
<li>A minimum of 40GB of dedicated storage space, preferably more.</li>
|
||||||
<li>24/7 availability (This means the machine must be *on* 24/7).</li>
|
<li>24/7 availability (This means the machine must be *on* 24/7).</li>
|
||||||
</ul>
|
</ul>
|
||||||
|