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
|
||||
|
||||
//if (isset($_GET['_'])) {
|
||||
// http_response_code(666);
|
||||
// die();
|
||||
//}
|
||||
|
||||
require_once ('../bootstrap.php');
|
||||
|
||||
define('IS_NOJS', (isset($_GET['nojs']) && $_GET['nojs']));
|
||||
@ -48,9 +43,9 @@ $function = $_GET['function'];
|
||||
foreach (read_dir('ajax/actions') as $file) {
|
||||
require_once (ABSPATH . "/ajax/actions/$file");
|
||||
} //require every file in actions
|
||||
|
||||
|
||||
switch ($function) {
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
@ -58,7 +53,7 @@ switch ($function) {
|
||||
*/
|
||||
case 'hentai_toggle':
|
||||
$mode = $_GET['mode'];
|
||||
|
||||
|
||||
if ($mode == 1) {
|
||||
setcookie('mangadex_h_toggle', $mode, $timestamp + (86400 * 3650), '/', DOMAIN); // 86400 = 1 day
|
||||
$details = 'Everything displayed.';
|
||||
@ -71,48 +66,48 @@ switch ($function) {
|
||||
setcookie('mangadex_h_toggle', '', $timestamp - 3600, '/', DOMAIN);
|
||||
$details = 'Hentai hidden.';
|
||||
}
|
||||
|
||||
print display_alert('success', 'Success', $details);
|
||||
|
||||
|
||||
print display_alert('success', 'Success', $details);
|
||||
|
||||
$result = 1;
|
||||
break;
|
||||
|
||||
case 'set_display_lang':
|
||||
$display_lang_id = $_GET['id'];
|
||||
|
||||
|
||||
if (!$user->user_id)
|
||||
setcookie('mangadex_display_lang', $display_lang_id, $timestamp + 86400, '/', DOMAIN); // 86400 = 1 day
|
||||
else {
|
||||
$sql->modify('set_display_lang', ' UPDATE mangadex_users SET display_lang_id = ? WHERE user_id = ? LIMIT 1 ', [$display_lang_id, $user->user_id]);
|
||||
|
||||
|
||||
$memcached->delete("user_$user->user_id");
|
||||
}
|
||||
|
||||
|
||||
$details = 'Display language set.';
|
||||
|
||||
print display_alert('success', 'Success', $details);
|
||||
|
||||
|
||||
print display_alert('success', 'Success', $details);
|
||||
|
||||
$result = 1;
|
||||
break;
|
||||
|
||||
case 'set_mangas_view':
|
||||
$mode = $_GET['mode'];
|
||||
|
||||
|
||||
if (!$user->user_id)
|
||||
setcookie('mangadex_title_mode', $mode, $timestamp + 86400, '/', DOMAIN); // 86400 = 1 day
|
||||
else {
|
||||
$sql->modify('set_mangas_view', ' UPDATE mangadex_users SET mangas_view = ? WHERE user_id = ? LIMIT 1 ', [$mode, $user->user_id]);
|
||||
|
||||
|
||||
$memcached->delete("user_$user->user_id");
|
||||
}
|
||||
|
||||
|
||||
$details = 'View mode set.';
|
||||
|
||||
print display_alert('success', 'Success', $details);
|
||||
|
||||
|
||||
print display_alert('success', 'Success', $details);
|
||||
|
||||
$result = 1;
|
||||
break;
|
||||
|
||||
|
||||
/*
|
||||
// user functions
|
||||
*/
|
||||
@ -120,74 +115,95 @@ switch ($function) {
|
||||
|
||||
case 'ban_user':
|
||||
$id = prepare_numeric($_GET['id']);
|
||||
|
||||
|
||||
$target_user = new User($id, 'user_id');
|
||||
|
||||
|
||||
if (validate_level($user, 'admin') && !validate_level($target_user, 'admin') && validate_level($target_user, 'validating')) {
|
||||
$sql->modify('ban_user', ' UPDATE mangadex_users SET level_id = 0 WHERE user_id = ? LIMIT 1 ', [$id]);
|
||||
|
||||
|
||||
$memcached->delete("user_$id");
|
||||
|
||||
|
||||
$details = $id;
|
||||
}
|
||||
else {
|
||||
$details = "You can't ban $target_user->username.";
|
||||
print display_alert('danger', 'Failed', $details); //fail
|
||||
}
|
||||
|
||||
|
||||
$result = (!is_numeric($details)) ? 0 : 1;
|
||||
break;
|
||||
|
||||
case 'unban_user':
|
||||
$id = prepare_numeric($_GET['id']);
|
||||
|
||||
|
||||
$target_user = new User($id, 'user_id');
|
||||
|
||||
|
||||
if (validate_level($user, 'admin') && !$target_user->level_id) {
|
||||
$sql->modify('unban_user', ' UPDATE mangadex_users SET level_id = 3 WHERE user_id = ? LIMIT 1 ', [$id]);
|
||||
|
||||
|
||||
$memcached->delete("user_$id");
|
||||
|
||||
|
||||
$details = $id;
|
||||
}
|
||||
else {
|
||||
$details = "You can't unban $target_user->username.";
|
||||
print display_alert('danger', 'Failed', $details); //fail
|
||||
}
|
||||
|
||||
$result = (!is_numeric($details)) ? 0 : 1;
|
||||
break;
|
||||
|
||||
|
||||
|
||||
$result = (!is_numeric($details)) ? 0 : 1;
|
||||
break;
|
||||
|
||||
|
||||
|
||||
/*
|
||||
// message functions
|
||||
*/
|
||||
|
||||
*/
|
||||
|
||||
case 'msg_reply':
|
||||
$id = prepare_numeric($_GET['id']);
|
||||
|
||||
|
||||
$reply = str_replace(['javascript:'], '', htmlentities($_POST['text']));
|
||||
|
||||
$thread = new PM_Thread($id);
|
||||
|
||||
$thread = new PM_Thread($id);
|
||||
|
||||
$recipient_user = new User($thread->recipient_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();
|
||||
$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 = $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]);
|
||||
|
||||
if ($thread->sender_id == $user->user_id)
|
||||
|
||||
if ($thread->sender_id == $user->user_id)
|
||||
$sql->modify('msg_reply', ' UPDATE mangadex_pm_threads SET recipient_read = 0, recipient_deleted = 0, thread_timestamp = UNIX_TIMESTAMP() WHERE thread_id = ? LIMIT 1 ', [$id]);
|
||||
else
|
||||
else
|
||||
$sql->modify('msg_reply', ' UPDATE mangadex_pm_threads SET sender_read = 0, sender_deleted = 0, thread_timestamp = UNIX_TIMESTAMP() WHERE thread_id = ? LIMIT 1 ', [$id]);
|
||||
|
||||
|
||||
$memcached->delete("user_{$thread->recipient_id}_unread_msgs");
|
||||
$memcached->delete("user_{$thread->sender_id}_unread_msgs");
|
||||
$memcached->delete("PM_{$thread->thread_id}");
|
||||
@ -199,20 +215,22 @@ switch ($function) {
|
||||
$details = "You can't reply to the message because they are blocked.";
|
||||
elseif (isset($recipient_blocked[$thread->sender_id]))
|
||||
$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)
|
||||
$details = $user->get_restriction_message(USER_RESTRICTION_CREATE_DM) ?? "You can't reply to this dm.";
|
||||
else
|
||||
$details = "You can't reply on thread $id.";
|
||||
|
||||
|
||||
print display_alert('danger', 'Failed', $details); //fail
|
||||
}
|
||||
|
||||
|
||||
$result = (!is_numeric($details)) ? 0 : 1;
|
||||
break;
|
||||
|
||||
case 'msg_send':
|
||||
$recipient = htmlentities($_POST['recipient']);
|
||||
$subject = htmlentities($_POST['subject']);
|
||||
case 'msg_send':
|
||||
$recipient = htmlentities($_POST['recipient']);
|
||||
$subject = htmlentities($_POST['subject']);
|
||||
$message = str_replace(['javascript:'], '', htmlentities($_POST['text']));
|
||||
|
||||
// Process captcha
|
||||
@ -244,10 +262,24 @@ switch ($function) {
|
||||
}
|
||||
|
||||
$last_message_timestamp = $sql->prep('last_message_timestamp', ' SELECT timestamp FROM mangadex_pm_msgs WHERE user_id = ? ORDER BY timestamp DESC LIMIT 1 ', [$user->user_id], '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');
|
||||
|
||||
|
||||
$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();
|
||||
$recipient_blocked = $recipient_user->get_blocked_user_ids();
|
||||
|
||||
@ -256,48 +288,45 @@ switch ($function) {
|
||||
// staff members ignore banned words and dm timeout
|
||||
$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);
|
||||
|
||||
$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]);
|
||||
|
||||
if(!validate_level($user, 'member') || $dm_restriction){
|
||||
$details = "You can't send messages.";
|
||||
}
|
||||
else if ($has_banned_word) {
|
||||
$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, 1, 0, 1) ', [$subject, $user->user_id, $recipient_id]);
|
||||
|
||||
$sql->modify('msg_send', ' INSERT INTO mangadex_pm_msgs (msg_id, thread_id, user_id, timestamp, text)
|
||||
|
||||
$sql->modify('msg_send', ' INSERT INTO mangadex_pm_msgs (msg_id, thread_id, user_id, timestamp, text)
|
||||
VALUES (NULL, ?, ?, UNIX_TIMESTAMP(), ?) ', [$thread_id, $user->user_id, $message]);
|
||||
|
||||
|
||||
$memcached->delete("user_{$recipient_id}_unread_msgs");
|
||||
|
||||
$details = $thread_id;
|
||||
}
|
||||
else if($has_dmed_recently) {
|
||||
} else if ($has_dmed_recently) {
|
||||
$details = "Please wait before sending another message.";
|
||||
}
|
||||
else if(!$is_valid_recipient) {
|
||||
} else if (!$canReceiveDms) {
|
||||
$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.";
|
||||
}
|
||||
else if($is_blocked) {
|
||||
} else if ($is_blocked) {
|
||||
$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.';
|
||||
}
|
||||
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)
|
||||
} 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)
|
||||
VALUES (NULL, ?, ?, ?, UNIX_TIMESTAMP(), 1, 0, 0, 0) ', [$subject, $user->user_id, $recipient_id]);
|
||||
|
||||
$sql->modify('msg_send', ' INSERT INTO mangadex_pm_msgs (msg_id, thread_id, user_id, timestamp, text)
|
||||
|
||||
$sql->modify('msg_send', ' INSERT INTO mangadex_pm_msgs (msg_id, thread_id, user_id, timestamp, text)
|
||||
VALUES (NULL, ?, ?, UNIX_TIMESTAMP(), ?) ', [$thread_id, $user->user_id, $message]);
|
||||
|
||||
|
||||
$memcached->delete("user_{$recipient_id}_unread_msgs");
|
||||
|
||||
$details = $thread_id;
|
||||
}
|
||||
|
||||
|
||||
$result = (!is_numeric($details)) ? 0 : 1;
|
||||
if(!$result){
|
||||
print display_alert('danger', 'Failed', $details); //fail
|
||||
@ -307,10 +336,10 @@ switch ($function) {
|
||||
case 'msg_del':
|
||||
if ($user->user_id && !empty($_POST['msg_ids']) && is_array($_POST['msg_ids'])) {
|
||||
foreach ($_POST['msg_ids'] as $id) {
|
||||
|
||||
|
||||
$id = prepare_numeric($id);
|
||||
$thread = new PM_Thread($id);
|
||||
|
||||
$thread = new PM_Thread($id);
|
||||
|
||||
if ($user->user_id == $thread->sender_id)
|
||||
$sql->modify('msg_del', ' UPDATE mangadex_pm_threads SET sender_deleted = 1 WHERE thread_id = ? LIMIT 1 ', [$id]);
|
||||
else
|
||||
@ -320,52 +349,52 @@ switch ($function) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!$user->user_id)
|
||||
if (!$user->user_id)
|
||||
$details = "Your session has timed out. Please log in again.";
|
||||
else
|
||||
else
|
||||
$details = "No messages selected.";
|
||||
|
||||
|
||||
print display_alert('danger', 'Failed', $details); //fail
|
||||
}
|
||||
|
||||
|
||||
$result = (!is_numeric($details)) ? 0 : 1;
|
||||
break;
|
||||
|
||||
|
||||
/*
|
||||
// mod functions
|
||||
*/
|
||||
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
// other functions
|
||||
*/
|
||||
|
||||
*/
|
||||
|
||||
case 'translate':
|
||||
$id = prepare_numeric($_GET['id']);
|
||||
$json = json_encode($_POST);
|
||||
|
||||
|
||||
if (in_array($user->user_id, TL_USER_IDS) || validate_level($user, 'gmod')) {
|
||||
$sql->modify('translate', ' UPDATE mangadex_languages SET navbar = ? WHERE lang_id = ? LIMIT 1 ', [$json, $id]);
|
||||
|
||||
|
||||
$memcached->delete("lang_$id");
|
||||
|
||||
$details = $id;
|
||||
|
||||
$details = $id;
|
||||
}
|
||||
else {
|
||||
$details = "Denied.";
|
||||
print display_alert('danger', 'Failed', $details); // fail
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$result = ($details) ? 0 : 1;
|
||||
break;
|
||||
|
||||
case "read_announcement":
|
||||
|
||||
case "read_announcement":
|
||||
if (validate_level($user, 'member')) {
|
||||
$sql->modify('read_announcement', ' UPDATE mangadex_users SET read_announcement = 1 WHERE user_id = ? LIMIT 1 ', [$user->user_id]);
|
||||
|
||||
|
||||
$memcached->delete("user_$user->user_id");
|
||||
|
||||
|
||||
$result = 1;
|
||||
}
|
||||
break;
|
||||
@ -484,10 +513,10 @@ switch ($function) {
|
||||
|
||||
$result = 1;
|
||||
break;
|
||||
|
||||
case "admin_ip_unban":
|
||||
|
||||
case "admin_ip_unban":
|
||||
$ip_unban = $_POST['ip'];
|
||||
|
||||
|
||||
if (validate_level($user, 'admin')) {
|
||||
// Check if this is an ip that is in the database
|
||||
$affectedRows = $sql->modify('ip_unban', "DELETE FROM mangadex_ip_bans WHERE ip = ?", [$ip_unban]);
|
||||
@ -499,9 +528,9 @@ switch ($function) {
|
||||
$memcached->delete('ip_banlist');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$result = ($details) ? 0 : 1;
|
||||
break;
|
||||
break;
|
||||
|
||||
case "admin_ip_ban":
|
||||
$ip_ban = $_POST['ip'];
|
||||
@ -545,10 +574,93 @@ switch ($function) {
|
||||
|
||||
$result = ($details) ? 0 : 1;
|
||||
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']))
|
||||
$sql->modify('action_log', ' INSERT INTO mangadex_logs_actions (action_id, action_name, action_user_id, action_timestamp, action_ip, action_result, action_details)
|
||||
$sql->modify('action_log', ' INSERT INTO mangadex_logs_actions (action_id, action_name, action_user_id, action_timestamp, action_ip, action_result, action_details)
|
||||
VALUES (NULL, ?, ?, UNIX_TIMESTAMP(), ?, ?, ?) ', [$function, $user->user_id, $ip, $result, strlen($details) > 128 ? substr($details, 0, 128) : $details]);
|
||||
*/
|
||||
?>
|
||||
|
@ -236,7 +236,7 @@ switch ($function) {
|
||||
|
||||
$to = $email1;
|
||||
$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!";
|
||||
|
||||
send_email($to, $subject, $body);
|
||||
@ -427,7 +427,7 @@ switch ($function) {
|
||||
FROM mangadex_users u
|
||||
JOIN mangadex_ip_bans b
|
||||
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){
|
||||
$sql->modify('activate', ' UPDATE mangadex_users SET level_id = 0, activated = 1 WHERE user_id = ? AND activated = 0 LIMIT 1 ', [$user->user_id]);
|
||||
}
|
||||
@ -464,6 +464,39 @@ switch ($function) {
|
||||
|
||||
$result = 1;
|
||||
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":
|
||||
|
||||
|
@ -738,6 +738,25 @@ switch ($function) {
|
||||
$result = (!is_numeric($details)) ? 0 : 1;
|
||||
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":
|
||||
$id = prepare_numeric($_GET['id']);
|
||||
$report_text = htmlentities($_POST["report_text"]);
|
||||
|
@ -25,10 +25,10 @@ switch ($function) {
|
||||
|
||||
if (!$user->user_id)
|
||||
$error .= display_alert('danger', 'Failed', "Your session has timed out. Please log in again.");
|
||||
elseif ($upload < 80)
|
||||
$error .= display_alert('danger', 'Failed', "Your upload speed must be at least 80 Mbps.");
|
||||
elseif ($download < 80)
|
||||
$error .= display_alert('danger', 'Failed', "Your download speed must be at least 80 Mbps.");
|
||||
elseif ($upload < 40)
|
||||
$error .= display_alert('danger', 'Failed', "Your upload speed must be at least 40 Mbps.");
|
||||
elseif ($download < 40)
|
||||
$error .= display_alert('danger', 'Failed', "Your download speed must be at least 40 Mbps.");
|
||||
elseif ($upload > 65535)
|
||||
$error .= display_alert('danger', 'Failed', "Your upload speed is too high.");
|
||||
elseif ($download > 65535)
|
||||
@ -229,4 +229,4 @@ Please connect your client as soon as possible. Clients that do not connect with
|
||||
case 'delete_client':
|
||||
die();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -52,9 +52,9 @@ switch ($function) {
|
||||
$id = prepare_numeric($_GET['id']);
|
||||
|
||||
if (validate_level($user, 'member') && $user->user_id != $id) {
|
||||
$sql->modify('friend_accept', '
|
||||
$sql->modify('friend_accept', '
|
||||
INSERT INTO mangadex_user_relations (user_id, relation_id, target_user_id, accepted) VALUES (?, 1, ?, 1)
|
||||
ON DUPLICATE KEY UPDATE accepted = 1
|
||||
ON DUPLICATE KEY UPDATE accepted = 1
|
||||
', [$user->user_id, $id]);
|
||||
|
||||
$sql->modify('friend_accept', ' UPDATE mangadex_user_relations SET accepted = 1 WHERE user_id = ? AND relation_id = 1 AND target_user_id = ? LIMIT 1 ', [$id, $user->user_id]);
|
||||
@ -251,7 +251,7 @@ switch ($function) {
|
||||
case 'supporter_settings':
|
||||
$show_premium_badge = !empty($_POST['show_supporter_badge']) ? 1 : 0;
|
||||
$show_mah_badge = !empty($_POST['show_mah_badge']) ? 1 : 0;
|
||||
|
||||
|
||||
if ($user->user_id) {
|
||||
if ($user->premium) {
|
||||
$sql->modify('supporter_settings', ' UPDATE mangadex_user_options SET show_premium_badge = ? WHERE user_id = ? LIMIT 1 ', [$show_premium_badge, $user->user_id]);
|
||||
@ -259,8 +259,8 @@ switch ($function) {
|
||||
if (count($user->get_clients())) {
|
||||
$approvaltime = $user->get_client_approval_time();
|
||||
if ($show_mah_badge && $approvaltime < 1593561600) {
|
||||
$show_mah_badge = 2;
|
||||
}
|
||||
$show_mah_badge = 2;
|
||||
}
|
||||
$sql->modify('supporter_settings', ' UPDATE mangadex_user_options SET show_md_at_home_badge = ? WHERE user_id = ? LIMIT 1 ', [$show_mah_badge, $user->user_id]);
|
||||
}
|
||||
|
||||
@ -298,6 +298,7 @@ switch ($function) {
|
||||
$post_sensitivity = prepare_numeric($_POST['swipe_sensitivity']);
|
||||
$reader_mode = prepare_numeric($_POST['reader_mode']) ?? 0;
|
||||
$image_fit = prepare_numeric($_POST['image_fit']) ?? 0;
|
||||
$data_saver = prepare_numeric($_POST['data_saver']) ?? 0;
|
||||
$img_server = prepare_numeric($_POST['img_server']);
|
||||
if ($reader_mode && $image_fit == 2)
|
||||
$image_fit = 0;
|
||||
@ -309,9 +310,10 @@ switch ($function) {
|
||||
$swipe_sensitivity = 150;
|
||||
|
||||
if ($user->user_id) {
|
||||
$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
|
||||
$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
|
||||
', [$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");
|
||||
}
|
||||
@ -328,6 +330,7 @@ switch ($function) {
|
||||
$website = str_replace(['javascript:'], '', htmlentities($_POST['website']));
|
||||
$user_bio = str_replace(['javascript:'], '', htmlentities($_POST['user_bio']));
|
||||
$old_file = $_FILES['file']['name'];
|
||||
$email = $_POST['email'];
|
||||
|
||||
// Make sure website has http://
|
||||
if (!empty($website) && stripos($website, 'http://') === false && stripos($website, 'https://') === false)
|
||||
@ -351,14 +354,29 @@ 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)
|
||||
$error .= display_alert('danger', 'Failed', 'Your session has timed out. Please log in again.'); //success
|
||||
|
||||
|
||||
if (!validate_level($user, 'member'))
|
||||
$error .= display_alert('danger', 'Failed', 'You need to be at least a member.'); //success
|
||||
|
||||
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) {
|
||||
$arr = explode('.', $_FILES['file']['name']);
|
||||
@ -399,8 +417,9 @@ switch ($function) {
|
||||
$theme_id = prepare_numeric($_POST['theme_id']);
|
||||
$navigation = prepare_numeric($_POST['navigation']);
|
||||
$list_privacy = prepare_numeric($_POST['list_privacy']);
|
||||
$dm_privacy = prepare_numeric($_POST['dm_privacy']);
|
||||
$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']);
|
||||
$old_file = $_FILES['file']['name'];
|
||||
$hentai_mode = prepare_numeric($_POST["hentai_mode"]);
|
||||
@ -417,17 +436,17 @@ switch ($function) {
|
||||
|
||||
if (!$user->user_id)
|
||||
$error .= display_alert('danger', 'Failed', "Your session has timed out. Please log in again."); //success
|
||||
|
||||
|
||||
if (!validate_level($user, 'member'))
|
||||
$error .= display_alert('danger', 'Failed', 'You need to be at least a member.'); //success
|
||||
|
||||
if (!$error) {
|
||||
$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
|
||||
', [$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]);
|
||||
|
||||
$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_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, $dm_privacy, $show_unavailable, $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) {
|
||||
$arr = explode(".", $_FILES["file"]["name"]);
|
||||
$ext = strtolower(end($arr));
|
||||
@ -608,7 +627,7 @@ switch ($function) {
|
||||
//var_dump($target_user_id, $mod_user_id, $restriction_type_id, $expiration_timestamp, $comment);
|
||||
|
||||
$sql->modify('user_restrictions_all_'.$target_user_id, '
|
||||
INSERT INTO mangadex_user_restrictions
|
||||
INSERT INTO mangadex_user_restrictions
|
||||
(target_user_id, restriction_type_id, mod_user_id, expiration_timestamp, comment)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?)', [$target_user_id, $restriction_type_id, $mod_user_id, $expiration_timestamp, $comment]);
|
||||
@ -635,7 +654,7 @@ switch ($function) {
|
||||
//var_dump($restriction_id, $mod_user_id);
|
||||
|
||||
$sql->modify('user_restrictions_all_'.$target_user_id, '
|
||||
UPDATE mangadex_user_restrictions
|
||||
UPDATE mangadex_user_restrictions
|
||||
SET
|
||||
mod_user_id = ?,
|
||||
expiration_timestamp = ?
|
||||
@ -659,7 +678,7 @@ switch ($function) {
|
||||
} else {
|
||||
$user_id = prepare_numeric($_GET["id"]);
|
||||
|
||||
$posts = $sql->prep('posts_nuke_select', '
|
||||
$posts = $sql->prep('posts_nuke_select', '
|
||||
SELECT posts.post_id, posts.thread_id, threads.forum_id
|
||||
FROM mangadex_forum_posts AS posts
|
||||
LEFT JOIN mangadex_threads AS threads
|
||||
@ -667,7 +686,7 @@ switch ($function) {
|
||||
WHERE posts.user_id = ? AND posts.deleted = 0
|
||||
', [$user_id], 'fetchAll', PDO::FETCH_ASSOC, -1);
|
||||
|
||||
$sql->modify('posts_nuke_update', '
|
||||
$sql->modify('posts_nuke_update', '
|
||||
UPDATE mangadex_forum_posts AS posts
|
||||
SET deleted = 1
|
||||
WHERE posts.user_id = ?
|
||||
@ -737,8 +756,8 @@ switch ($function) {
|
||||
}
|
||||
|
||||
if ($is_admin) {
|
||||
$sql->modify('admin_edit_user', '
|
||||
UPDATE mangadex_users SET username = ?, level_id = ?, email = ?, language = ?, avatar = ?, upload_group_id = ?, upload_lang_id = ?, user_bio = ?, user_website = ? WHERE user_id = ?
|
||||
$sql->modify('admin_edit_user', '
|
||||
UPDATE mangadex_users SET username = ?, level_id = ?, email = ?, language = ?, avatar = ?, upload_group_id = ?, upload_lang_id = ?, user_bio = ?, user_website = ? WHERE user_id = ?
|
||||
', [$username, $level_id, $email, $lang_id, $avatar, $upload_group_id, $upload_lang_id, $user_bio, $website, $id]);
|
||||
|
||||
if ($level_id == 0) {
|
||||
@ -752,8 +771,8 @@ switch ($function) {
|
||||
$sql->modify('admin_edit_user', ' UPDATE mangadex_users SET password = ? WHERE user_id = ? LIMIT 1 ', [$password_hash, $id]);
|
||||
}
|
||||
} else {
|
||||
$sql->modify('admin_edit_user', '
|
||||
UPDATE mangadex_users SET avatar = ?, user_bio = ?, user_website = ? WHERE user_id = ?
|
||||
$sql->modify('admin_edit_user', '
|
||||
UPDATE mangadex_users SET avatar = ?, user_bio = ?, user_website = ? WHERE user_id = ?
|
||||
', [$avatar, $user_bio, $website, $id]);
|
||||
}
|
||||
|
||||
@ -768,4 +787,4 @@ switch ($function) {
|
||||
|
||||
$result = (!is_numeric($details)) ? 0 : 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ if (isset($_GET['_'])) {
|
||||
http_response_code(666);
|
||||
die();
|
||||
}
|
||||
|
||||
|
||||
use Mangadex\Model\Guard;
|
||||
|
||||
require_once ('../bootstrap.php');
|
||||
@ -280,35 +280,17 @@ switch ($type) {
|
||||
$page_array = array_combine(range(1, count($arr)), array_values($arr));
|
||||
}
|
||||
|
||||
$server_fallback = LOCAL_SERVER_URL;
|
||||
$server_fallback = IMG_SERVER_URL;
|
||||
$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
|
||||
// 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 {
|
||||
$subsubdomain = $mdAtHomeClient->getServerUrl($chapter->chapter_hash, explode(',', $chapter->page_order), _IP);
|
||||
if (!empty($subsubdomain)) {
|
||||
$server_network = $subsubdomain;
|
||||
}
|
||||
} catch (Throwable $t) {
|
||||
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";
|
||||
// use md@h for all images
|
||||
try {
|
||||
$subsubdomain = $mdAtHomeClient->getServerUrl($chapter->chapter_hash, explode(',', $chapter->page_order), _IP, $user->mdh_portlimit ?? false);
|
||||
if (!empty($subsubdomain)) {
|
||||
$server_network = $subsubdomain;
|
||||
}
|
||||
} catch (\Throwable $t) {
|
||||
trigger_error($t->getMessage(), E_USER_WARNING);
|
||||
}
|
||||
|
||||
$server = $server_network ?: $server_fallback;
|
||||
@ -341,11 +323,16 @@ switch ($type) {
|
||||
if (!empty($server_network)) {
|
||||
$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') {
|
||||
$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 = [
|
||||
'id' => $chapter->chapter_id,
|
||||
'status' => 'restricted',
|
||||
@ -432,7 +419,7 @@ SQL;
|
||||
} else {
|
||||
$limit = 200;
|
||||
$offset = $limit * ((int) max(1, (int) min(50, $_GET['page'] ?? 1)) - 1);
|
||||
|
||||
|
||||
$follows = $user->get_followed_manga_ids_api();
|
||||
foreach ($follows AS &$follow) {
|
||||
$follow['title'] = html_entity_decode($follow['title'] ?? '', null, 'UTF-8');
|
||||
|
@ -11,6 +11,7 @@ use Mangadex\Controller\API\MangaController;
|
||||
use Mangadex\Controller\API\RelationTypeController;
|
||||
use Mangadex\Controller\API\TagController;
|
||||
use Mangadex\Controller\API\UserController;
|
||||
use Mangadex\Controller\API\HighestChapterIDController;
|
||||
use Mangadex\Exception\Http\HttpException;
|
||||
use Mangadex\Exception\Http\NotFoundHttpException;
|
||||
use Mangadex\Exception\Http\TooManyRequestsHttpException;
|
||||
@ -67,6 +68,9 @@ try {
|
||||
case 'index':
|
||||
$controller = new IndexController();
|
||||
break;
|
||||
case 'highest_chapter_id':
|
||||
$controller = new HighestChapterIDController();
|
||||
break;
|
||||
default:
|
||||
throw new NotFoundHttpException("Invalid endpoint");
|
||||
break;
|
||||
|
@ -8,6 +8,7 @@ define('DB_USER', 'mangadex');
|
||||
define('DB_PASSWORD', '');
|
||||
define('DB_NAME', 'mangadex');
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_PERSISTENT', false);
|
||||
|
||||
define('DB_READ_HOSTS', ['127.0.0.1']);
|
||||
define('DB_READ_NAME', DB_NAME);
|
||||
@ -29,6 +30,8 @@ define('URL', 'https://mangadex.org/');
|
||||
define('TITLE', 'MangaDex');
|
||||
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_SYNC_HOST', null);
|
||||
define('MEMCACHED_SYNC_PORT', null);
|
||||
|
||||
define('GOOGLE_CAPTCHA_SITEKEY', '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('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']);
|
||||
|
||||
@ -90,8 +94,9 @@ define('MAX_IMAGE_FILESIZE', 1048576);
|
||||
define('ALLOWED_IMG_EXT', ['jpg', 'jpeg', 'png', 'gif']);
|
||||
define('ALLOWED_MIME_TYPES', ['image/png', 'image/jpeg', 'image/gif']);
|
||||
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('API_V2_URL', 'https://api.mangadex.org/v2/');
|
||||
|
||||
//$server_array = ['eu2' => 1, 'na' => 2, 'eu' => 3, 'na2' => 4, 'na3' => 5];
|
||||
define('IMAGE_SERVER_INFO', [
|
||||
|
@ -9,6 +9,9 @@ require_once (__DIR__.'/../bootstrap.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
|
||||
$dirh = opendir(sys_get_temp_dir());
|
||||
$nameFormat = 'remote_file_dl_';
|
||||
@ -23,13 +26,14 @@ while (false !== ($entry = readdir($dirh))) {
|
||||
}
|
||||
|
||||
//updated featured
|
||||
echo "featured ...\n";
|
||||
$memcached->delete('featured');
|
||||
$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)) {
|
||||
$manga_ids_in = prepare_in($array_of_featured_manga_ids);
|
||||
$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,
|
||||
(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
|
||||
@ -40,51 +44,55 @@ if (!empty($array_of_featured_manga_ids)) {
|
||||
AND mangas.manga_id IN ($manga_ids_in)
|
||||
GROUP BY chapters.manga_id
|
||||
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
|
||||
echo "new_manga ...\n";
|
||||
$memcached->delete('new_manga');
|
||||
$new_manga = $sql->query_read('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
|
||||
$new_manga = $sql->prep('new_manga', "
|
||||
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
|
||||
LEFT JOIN mangadex_chapters AS chapters
|
||||
ON mangas.manga_id = chapters.manga_id
|
||||
WHERE mangas.manga_hentai = 0 AND chapters.chapter_id IS NOT NULL
|
||||
GROUP BY mangas.manga_id
|
||||
ORDER BY mangas.manga_id DESC LIMIT 10
|
||||
", 'fetchAll', PDO::FETCH_ASSOC, 3600);
|
||||
", [], 'fetchAll', PDO::FETCH_ASSOC, 3600, true);
|
||||
|
||||
//update top follows
|
||||
echo "top_follows ...\n";
|
||||
$memcached->delete('top_follows');
|
||||
$top_follows = $sql->query_read('top_follows', "
|
||||
SELECT mangas.manga_id, mangas.manga_image, mangas.manga_name, mangas.manga_hentai, mangas.manga_bayesian,
|
||||
$top_follows = $sql->prep('top_follows', "
|
||||
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_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows
|
||||
FROM mangadex_mangas AS mangas
|
||||
WHERE mangas.manga_hentai = 0
|
||||
ORDER BY count_follows DESC LIMIT 10
|
||||
", 'fetchAll', PDO::FETCH_ASSOC, 3600);
|
||||
", [], 'fetchAll', PDO::FETCH_ASSOC, 3600, true);
|
||||
|
||||
//update top rating
|
||||
echo "top_rating ...\n";
|
||||
$memcached->delete('top_rating');
|
||||
$top_rating = $sql->query_read('top_rating', "
|
||||
SELECT mangas.manga_id, mangas.manga_image, mangas.manga_name, mangas.manga_hentai, mangas.manga_bayesian,
|
||||
$top_rating = $sql->prep('top_rating', "
|
||||
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_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows
|
||||
FROM mangadex_mangas AS mangas
|
||||
WHERE mangas.manga_hentai = 0
|
||||
ORDER BY manga_bayesian DESC LIMIT 10
|
||||
", 'fetchAll', PDO::FETCH_ASSOC, 3600);
|
||||
", [], 'fetchAll', PDO::FETCH_ASSOC, 3600, true);
|
||||
|
||||
//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) {
|
||||
$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_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_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->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_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_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->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('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
|
||||
echo "prune_manga_history ...\n";
|
||||
$cutoff = time() - (60 * 60 * 24 * 90); // 90 days
|
||||
$sql->modify('prune_manga_history', 'DELETE FROM mangadex_manga_history WHERE `timestamp` < ?', [$cutoff]);
|
||||
|
||||
// Prune expired ip bans
|
||||
echo "prune_ip_bans ...\n";
|
||||
$sql->modify('prune_ip_bans', 'DELETE FROM mangadex_ip_bans WHERE expires < UNIX_TIMESTAMP()', []);
|
||||
|
||||
// Prune expired sessions each month on the 1st
|
||||
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]);
|
||||
}
|
||||
|
||||
//prune old chapter_live_views for trending data
|
||||
$sql->modify('prune_trending', 'DELETE FROM `mangadex_chapter_live_views` WHERE (timestamp + ?) < UNIX_TIMESTAMP()', [60*60*25]);
|
||||
echo "prune_trending ...\n";
|
||||
$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
|
||||
|
||||
if (PHP_SAPI !== 'cli')
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
die();
|
||||
}
|
||||
|
||||
echo "START @ ".date("F j, Y, g:i a")."\n";
|
||||
|
||||
require_once (__DIR__.'/../bootstrap.php');
|
||||
|
||||
require_once (ABSPATH . "/scripts/header.req.php");
|
||||
|
||||
echo "latest_manga_comments ...\n";
|
||||
$memcached->delete("latest_manga_comments");
|
||||
$latest_manga_comments = $sql->query_read('latest_manga_comments', "
|
||||
SELECT posts.post_id, posts.text, posts.timestamp, posts.thread_id, mangas.manga_name, mangas.manga_id,
|
||||
$latest_manga_comments = $sql->prep('latest_manga_comments', "
|
||||
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
|
||||
WHERE mangadex_forum_posts.post_id <= posts.post_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
|
||||
WHERE threads.forum_id = 11 AND threads.thread_deleted = 0
|
||||
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");
|
||||
$latest_forum_posts = $sql->query_read('latest_forum_posts', "
|
||||
SELECT posts.post_id, posts.text, posts.timestamp, posts.thread_id, threads.thread_name, forums.forum_name,
|
||||
$latest_forum_posts = $sql->prep('latest_forum_posts', "
|
||||
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
|
||||
WHERE mangadex_forum_posts.post_id <= posts.post_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
|
||||
WHERE threads.forum_id NOT IN (11, 12, 14, 17, 18, 20) AND threads.thread_deleted = 0
|
||||
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");
|
||||
$latest_forum_posts = $sql->query_read('latest_news_posts', "
|
||||
SELECT posts.post_id, posts.text, posts.timestamp, posts.thread_id, threads.thread_name, forums.forum_name,
|
||||
$latest_forum_posts = $sql->prep('latest_news_posts', "
|
||||
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
|
||||
WHERE mangadex_forum_posts.post_id <= posts.post_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
|
||||
WHERE threads.forum_id = 26 AND threads.thread_sticky = 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
|
||||
///
|
||||
|
||||
echo "expired_delayed_chapters ... \n";
|
||||
// Collect all chapters that have been uploaded as delayed, but where the delay is expired
|
||||
$expired_delayed_chapters = $sql->query_read('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
|
||||
', 'fetchAll', PDO::FETCH_ASSOC, -1);
|
||||
$expired_delayed_chapters = $sql->prep('expired_delayed_chapters', '
|
||||
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, true);
|
||||
// Only process if we found any
|
||||
if (!empty($expired_delayed_chapters)) {
|
||||
// Collect all chapter ids in this array, so we can unset them as delayed after this
|
||||
@ -94,22 +101,31 @@ 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.')', []);
|
||||
}
|
||||
|
||||
$stats = $mdAtHomeClient->getStatus();
|
||||
echo "mdAtHomeClient->getStatus(); ... ";
|
||||
try {
|
||||
$stats = $mdAtHomeClient->getStatus();
|
||||
|
||||
foreach ($stats as $client) {
|
||||
$client_id = (int) $client['client_id'];
|
||||
$user_id = (int) $client['user_id'];
|
||||
$subsubdomain = substr($client['url'], 8, 27);
|
||||
if (strpos($subsubdomain, '.') === 0)
|
||||
$subsubdomain = '*.' . substr($client['url'], 9, 13);
|
||||
$url = explode(':', $client['url']);
|
||||
$port = (int) $url[2];
|
||||
$client_ip = $client['ip'];
|
||||
$available = (int) $client['available'];
|
||||
$shard_count = (int) $client['shard_count'];
|
||||
$speed = ((int) $client['speed']) / 125000;
|
||||
$images_served = (int) $client['images_served'];
|
||||
$images_failed = (int) $client['images_failed'];
|
||||
$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]);
|
||||
}
|
||||
foreach ($stats as $client) {
|
||||
$client_id = (int) $client['client_id'];
|
||||
$user_id = (int) $client['user_id'];
|
||||
$subsubdomain = substr($client['url'], 8, 27);
|
||||
if (strpos($subsubdomain, '.') === 0)
|
||||
$subsubdomain = '*.' . substr($client['url'], 9, 13);
|
||||
$url = explode(':', $client['url']);
|
||||
$port = (int) $url[2];
|
||||
$client_ip = $client['ip'];
|
||||
$available = (int) $client['available'];
|
||||
$shard_count = (int) $client['shard_count'];
|
||||
$speed = ((int) $client['speed']) / 125000;
|
||||
$images_served = (int) $client['images_served'];
|
||||
$images_failed = (int) $client['images_failed'];
|
||||
$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]);
|
||||
}
|
||||
|
||||
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 |
4342
package-lock.json
generated
39
package.json
@ -5,33 +5,34 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"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-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",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.3.3",
|
||||
"@babel/plugin-transform-runtime": "^7.2.0",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-decorators": "^6.24.1",
|
||||
"webpack": "^4.29.5",
|
||||
"webpack-cli": "^3.2.3",
|
||||
"webpack-merge": "^4.2.1"
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"babel-loader": "^8.2.2",
|
||||
"core-js": "^3.8.2",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-merge": "^4.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.2.5",
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"date-fns": "^2.0.0-alpha.27",
|
||||
"dotenv-webpack": "^1.7.0",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"abortcontroller-polyfill": "^1.7.1",
|
||||
"date-fns": "^2.16.1",
|
||||
"dotenv-webpack": "^1.8.0",
|
||||
"eligrey-classlist-js-polyfill": "^1.2.20180112",
|
||||
"formdata-polyfill": "^3.0.18",
|
||||
"polyfill-queryselector": "^1.0.2",
|
||||
"url-polyfill": "^1.1.3",
|
||||
"formdata-polyfill": "^3.0.20",
|
||||
"js-cookie": "^2.2.1",
|
||||
"natsort": "^2.0.2",
|
||||
"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
|
||||
$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 = [];
|
||||
|
||||
|
@ -441,6 +441,7 @@ else {
|
||||
", array_keys($blocked_group_ids), 'fetchAll', PDO::FETCH_ASSOC, 60);
|
||||
}
|
||||
|
||||
$banners = get_banners();
|
||||
|
||||
$featured = $memcached->get('featured');
|
||||
|
||||
@ -471,6 +472,7 @@ $templateVars = [
|
||||
'latest_news_posts' => $latest_news_posts,
|
||||
'featured' => $featured,
|
||||
'new_manga' => $new_manga,
|
||||
'banners' => $banners,
|
||||
];
|
||||
|
||||
$page_html = parse_template('home', $templateVars);
|
||||
|
@ -9,7 +9,7 @@ else {
|
||||
FROM mangadex_chapters AS chapters
|
||||
LEFT JOIN mangadex_mangas AS mangas
|
||||
ON mangas.manga_id = chapters.manga_id
|
||||
WHERE mangas.manga_hentai = 0
|
||||
WHERE mangas.manga_hentai = 0
|
||||
AND chapters.chapter_deleted = 0
|
||||
AND chapters.lang_id IN ($in)
|
||||
", $lang_id_filter_array , 'fetchAll', PDO::FETCH_COLUMN, 86400);
|
||||
@ -35,12 +35,17 @@ $manga = new Manga($id);
|
||||
|
||||
$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)) {
|
||||
$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) {
|
||||
$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 {
|
||||
|
||||
update_views_v2($page, $manga->manga_id, $ip);
|
||||
@ -54,11 +59,11 @@ else {
|
||||
case 'chapters':
|
||||
|
||||
$search['manga_id'] = $manga->manga_id;
|
||||
|
||||
|
||||
$blocked_groups = $user->get_blocked_groups();
|
||||
if ($blocked_groups)
|
||||
$search['blocked_groups'] = array_keys($blocked_groups);
|
||||
|
||||
|
||||
//multi_lang
|
||||
if ($user->user_id && $user->default_lang_ids)
|
||||
$search["multi_lang_id"] = $user->default_lang_ids;
|
||||
|
@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Mangadex\Model\MdexAtHomeClient;
|
||||
|
||||
$section = $_GET['section'] ?? 'info';
|
||||
|
||||
$approvaltime = $user->get_client_approval_time();
|
||||
@ -39,7 +42,9 @@ switch ($section) {
|
||||
'user' => $user,
|
||||
'section' => $section,
|
||||
'user_clients' => $user->get_clients(),
|
||||
'approvaltime' => $approvaltime,
|
||||
'approvaltime' => $approvaltime,
|
||||
'ip' => _IP,
|
||||
'backend' => new MdexAtHomeClient(),
|
||||
];
|
||||
|
||||
if (validate_level($user, 'member')) {
|
||||
@ -53,8 +58,10 @@ switch ($section) {
|
||||
$templateVars = [
|
||||
'user' => $user,
|
||||
'section' => $section,
|
||||
'clients' => $clients,
|
||||
];
|
||||
'clients' => $clients,
|
||||
'ip' => _IP,
|
||||
'backend' => new MdexAtHomeClient(),
|
||||
];
|
||||
|
||||
if (validate_level($user, 'admin')) {
|
||||
$tab_html = parse_template('md_at_home/partials/admin', $templateVars);
|
||||
|
@ -34,13 +34,20 @@ switch ($mode) {
|
||||
default:
|
||||
$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_obj = $threads->query_read();
|
||||
$threads_obj = $threads->query_read($current_page, $limit);
|
||||
|
||||
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 {
|
||||
$templateVars = [
|
||||
'thread_count' => $threads->num_rows,
|
||||
'current_page' => $current_page,
|
||||
'mode' => $mode,
|
||||
'limit' => $limit,
|
||||
'threads' => $threads_obj,
|
||||
'user' => $user,
|
||||
'deleted' => $deleted,
|
||||
|
106
pages/pr.req.php
@ -1,52 +1,68 @@
|
||||
<?php
|
||||
if (!validate_level($user, 'pr')) die('No access');
|
||||
|
||||
$search = [];
|
||||
//pages
|
||||
$mode = $_GET['mode'] ?? 'banners';
|
||||
|
||||
if (isset($_GET['username']) && !empty($_GET['username']))
|
||||
$search['username'] = trim($_GET['username']);
|
||||
else
|
||||
$search['username'] = '';
|
||||
|
||||
if (isset($_GET['email']) && !empty($_GET['email']))
|
||||
$search['email'] = trim($_GET['email']);
|
||||
else
|
||||
$search['email'] = '';
|
||||
|
||||
$sort = (isset($_GET['s']) && $_GET['s'] > 0) ? $_GET['s'] : 0;
|
||||
$order = SORT_ARRAY_USERS[$sort];
|
||||
$limit = $limit ?? 100;
|
||||
$current_page = (isset($_GET['p']) && $_GET['p'] > 0) ? $_GET['p'] : 1;
|
||||
|
||||
if ($search['username'] || $search['email']) {
|
||||
$users = new Users($search);
|
||||
$users_obj = $users->query_read($order, $limit, $current_page);
|
||||
}
|
||||
else {
|
||||
$users->num_rows = 0;
|
||||
$users_obj = new stdClass();
|
||||
}
|
||||
|
||||
$page_html = "";
|
||||
|
||||
$templateVars = ['search' => $search];
|
||||
|
||||
$page_html .= parse_template('pr/partials/user_list_searchbox', $templateVars);
|
||||
|
||||
$templateVars = [
|
||||
'search' => $search,
|
||||
'sort' => $sort,
|
||||
'limit' => $limit,
|
||||
'current_page' => $current_page,
|
||||
'page' => $page,
|
||||
'user_list' => $users_obj,
|
||||
'user_count' => $users->num_rows,
|
||||
'mode' => $mode,
|
||||
];
|
||||
|
||||
if (!$search['username'] && !$search['email']) {
|
||||
$page_html .= parse_template('partials/alert', ['type' => 'info mt-3', 'strong' => 'Notice', 'text' => 'No search string']);
|
||||
} elseif ($users->num_rows < 1) {
|
||||
$page_html .= parse_template('partials/alert', ['type' => 'info mt-3', 'strong' => 'Notice', 'text' => 'There are no users found with your search criteria.']);
|
||||
} else {
|
||||
$page_html .= parse_template('user/user_list', $templateVars);
|
||||
}
|
||||
$page_html = parse_template('pr/partials/pr_navtabs', $templateVars);
|
||||
|
||||
switch ($mode) {
|
||||
case 'email_search':
|
||||
$search = [];
|
||||
|
||||
if (isset($_GET['username']) && !empty($_GET['username']))
|
||||
$search['username'] = trim($_GET['username']);
|
||||
else
|
||||
$search['username'] = '';
|
||||
|
||||
if (isset($_GET['email']) && !empty($_GET['email']))
|
||||
$search['email'] = trim($_GET['email']);
|
||||
else
|
||||
$search['email'] = '';
|
||||
|
||||
$sort = (isset($_GET['s']) && $_GET['s'] > 0) ? $_GET['s'] : 0;
|
||||
$order = SORT_ARRAY_USERS[$sort];
|
||||
$limit = $limit ?? 100;
|
||||
$current_page = (isset($_GET['p']) && $_GET['p'] > 0) ? $_GET['p'] : 1;
|
||||
|
||||
if ($search['username'] || $search['email']) {
|
||||
$users = new Users($search);
|
||||
$users_obj = $users->query_read($order, $limit, $current_page);
|
||||
}
|
||||
else {
|
||||
$users->num_rows = 0;
|
||||
$users_obj = new stdClass();
|
||||
}
|
||||
|
||||
$templateVars = ['search' => $search];
|
||||
$page_html .= parse_template('pr/partials/user_list_searchbox', $templateVars);
|
||||
$templateVars = [
|
||||
'search' => $search,
|
||||
'sort' => $sort,
|
||||
'limit' => $limit,
|
||||
'current_page' => $current_page,
|
||||
'page' => $page,
|
||||
'user_list' => $users_obj,
|
||||
'user_count' => $users->num_rows,
|
||||
];
|
||||
|
||||
if (!$search['username'] && !$search['email']) {
|
||||
$page_html .= parse_template('partials/alert', ['type' => 'info mt-3', 'strong' => 'Notice', 'text' => 'No search string']);
|
||||
} elseif ($users->num_rows < 1) {
|
||||
$page_html .= parse_template('partials/alert', ['type' => 'info mt-3', 'strong' => 'Notice', 'text' => 'There are no users found with your search criteria.']);
|
||||
} else {
|
||||
$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,17 +1,42 @@
|
||||
<?php
|
||||
$transactions = $user->get_transactions();
|
||||
$mode = $_GET['mode'] ?? 'home';
|
||||
|
||||
if ($transactions) {
|
||||
$sql->modify('claim_transaction', ' UPDATE mangadex_user_transactions SET user_id = ? WHERE user_id = 0 AND email LIKE ? ', [$user->user_id, $transactions[0]['email']]);
|
||||
|
||||
$memcached->delete("user_{$user->user_id}_transactions");
|
||||
$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();
|
||||
|
||||
if ($transactions) {
|
||||
$sql->modify('claim_transaction', ' UPDATE mangadex_user_transactions SET user_id = ? WHERE user_id = 0 AND email LIKE ? ', [$user->user_id, $transactions[0]['email']]);
|
||||
|
||||
$memcached->delete("user_{$user->user_id}_transactions");
|
||||
}
|
||||
|
||||
$page_html .= parse_template('support/history', [
|
||||
'user' => $user
|
||||
]);
|
||||
break;
|
||||
case 'affiliates':
|
||||
$page_html .= parse_template('support/affiliates', $templateVars);
|
||||
break;
|
||||
}
|
||||
|
||||
$wallet_no = substr($user->user_id, -1);
|
||||
$wallet_no_2 = floor(substr($user->user_id, -1) / 2);
|
||||
|
||||
$page_html = parse_template('user/support', [
|
||||
'user' => $user,
|
||||
'wallet_no' => $wallet_no,
|
||||
'wallet_no_2' => $wallet_no_2,
|
||||
]);
|
9
scripts/axios.min.js
vendored
@ -1,6 +1,41 @@
|
||||
<?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 = [
|
||||
@ -76,7 +111,7 @@ class Cache extends Memcached
|
||||
|
||||
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->log[] = [
|
||||
'method' => "GET",
|
||||
@ -88,7 +123,7 @@ class Cache extends Memcached
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function set($key, $value, $expiration = 0)
|
||||
public function set($key, $value, $expiration = 0, $udf_flags = 0)
|
||||
{
|
||||
parent::set($key, $value, $expiration);
|
||||
$this->stats['set']++;
|
||||
@ -103,8 +138,7 @@ class Cache extends Memcached
|
||||
|
||||
public function delete($key, $time = 0)
|
||||
{
|
||||
|
||||
parent::delete($key, $time); // TODO: Change the autogenerated stub
|
||||
parent::delete($key, $time);
|
||||
$this->stats['delete']++;
|
||||
$this->log[] = [
|
||||
'method' => "DEL",
|
||||
|
@ -4,13 +4,13 @@ class Chapters {
|
||||
public function __construct($search) {
|
||||
global $sql;
|
||||
$this->sql = $sql;
|
||||
|
||||
|
||||
$search_string = "";
|
||||
$pdo_bind = [];
|
||||
|
||||
if (!isset($search['chapter_deleted']))
|
||||
$search['chapter_deleted'] = 0;
|
||||
|
||||
|
||||
foreach ($search as $key => $value) {
|
||||
switch ($key) {
|
||||
case 'multi_lang_id':
|
||||
@ -19,41 +19,41 @@ class Chapters {
|
||||
$search_string .= "chapters.lang_id IN ($in) AND ";
|
||||
$pdo_bind = array_merge($pdo_bind, $arr);
|
||||
break;
|
||||
|
||||
|
||||
case 'manga_hentai':
|
||||
$search_string .= "mangas.manga_hentai = ? AND ";
|
||||
$pdo_bind[] = $value;
|
||||
break;
|
||||
|
||||
|
||||
case 'upload_timestamp':
|
||||
$search_string .= "upload_timestamp > (UNIX_TIMESTAMP() - $value) AND ";
|
||||
break;
|
||||
|
||||
|
||||
case 'exclude_delayed':
|
||||
$search_string .= "upload_timestamp < UNIX_TIMESTAMP() AND ";
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'manga_id':
|
||||
$search_string .= "chapters.manga_id = ? AND ";
|
||||
$pdo_bind[] = $value;
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'group_id':
|
||||
$search_string .= "(chapters.group_id = ? OR chapters.group_id_2 = ? OR chapters.group_id_3 = ?) AND ";
|
||||
$pdo_bind[] = $value;
|
||||
$pdo_bind[] = $value;
|
||||
$pdo_bind[] = $value;
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'user_id':
|
||||
$search_string .= "chapters.user_id = ? AND ";
|
||||
$pdo_bind[] = $value;
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'lang_id':
|
||||
$search_string .= "chapters.lang_id = ? AND ";
|
||||
$pdo_bind[] = $value;
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'manga_ids_array':
|
||||
$in = prepare_in($value);
|
||||
@ -68,7 +68,7 @@ class Chapters {
|
||||
$search_string .= "mangas.manga_id NOT IN (SELECT manga_id FROM mangadex_manga_genres genres WHERE genres.genre_id IN ($in)) AND ";
|
||||
$pdo_bind = array_merge($pdo_bind, $value);
|
||||
break;
|
||||
|
||||
|
||||
case 'blocked_groups':
|
||||
if (!is_array($value))
|
||||
$value = explode(',', $value);
|
||||
@ -76,7 +76,7 @@ class Chapters {
|
||||
$search_string .= "chapters.group_id NOT IN ($in) AND ";
|
||||
$pdo_bind = array_merge($pdo_bind, $value);
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
$field = prepare_identifier($key);
|
||||
$search_string .= "$field = ? AND ";
|
||||
@ -87,51 +87,51 @@ class Chapters {
|
||||
|
||||
// TODO: Is this ever used anywhere? Seems pointless.
|
||||
$this->num_rows = $sql->prep("chapters_query_" . hash_array($pdo_bind) . "_num_rows", "
|
||||
SELECT count(*)
|
||||
SELECT count(*)
|
||||
FROM mangadex_chapters AS chapters
|
||||
LEFT JOIN mangadex_mangas AS mangas
|
||||
ON mangas.manga_id = chapters.manga_id
|
||||
WHERE $search_string 1=1
|
||||
ON mangas.manga_id = chapters.manga_id
|
||||
WHERE $search_string 1=1
|
||||
", $pdo_bind, 'fetchColumn', '', 60);
|
||||
$this->search_string = $search_string;
|
||||
$this->pdo_bind = $pdo_bind;
|
||||
}
|
||||
|
||||
$this->pdo_bind = $pdo_bind;
|
||||
}
|
||||
|
||||
public function query_read($order, $limit, $current_page) {
|
||||
$orderby = prepare_orderby($order, ["upload_timestamp DESC", "(CASE volume WHEN '' THEN 1 END) DESC, abs(volume) DESC, abs(chapter) DESC, group_id ASC"]);
|
||||
$limit = prepare_numeric($limit);
|
||||
$offset = prepare_numeric($limit * ($current_page - 1));
|
||||
|
||||
$results = $this->sql->prep("chapters_query_" . hash_array($this->pdo_bind) . "_orderby_".md5($orderby)."_offset_$offset", "
|
||||
SELECT chapters.*,
|
||||
lang.*,
|
||||
users.username,
|
||||
$results = $this->sql->prep("chapters_query_" . hash_array($this->pdo_bind) . "_orderby_" . md5($orderby) . "_offset_$offset" . "_limit_$limit", "
|
||||
SELECT chapters.*,
|
||||
lang.*,
|
||||
users.username,
|
||||
options.show_premium_badge,
|
||||
options.show_md_at_home_badge,
|
||||
mangas.manga_name,
|
||||
mangas.manga_image,
|
||||
mangas.manga_hentai,
|
||||
mangas.manga_name,
|
||||
mangas.manga_image,
|
||||
mangas.manga_hentai,
|
||||
mangas.manga_last_chapter,
|
||||
mangas.manga_last_volume,
|
||||
group1.group_name AS group_name,
|
||||
group2.group_name AS group_name_2,
|
||||
group3.group_name AS group_name_3,
|
||||
group1.group_leader_id AS group_leader_id,
|
||||
group2.group_leader_id AS group_leader_id_2,
|
||||
group1.group_name AS group_name,
|
||||
group2.group_name AS group_name_2,
|
||||
group3.group_name AS group_name_3,
|
||||
group1.group_leader_id AS group_leader_id,
|
||||
group2.group_leader_id AS group_leader_id_2,
|
||||
group3.group_leader_id AS group_leader_id_3,
|
||||
levels.level_colour,
|
||||
threads.thread_posts
|
||||
FROM mangadex_chapters AS chapters
|
||||
LEFT JOIN mangadex_groups AS group1
|
||||
ON group1.group_id = chapters.group_id
|
||||
LEFT JOIN mangadex_groups AS group2
|
||||
LEFT JOIN mangadex_groups AS group1
|
||||
ON group1.group_id = chapters.group_id
|
||||
LEFT JOIN mangadex_groups AS group2
|
||||
ON group2.group_id = chapters.group_id_2
|
||||
LEFT JOIN mangadex_groups AS group3
|
||||
LEFT JOIN mangadex_groups AS group3
|
||||
ON group3.group_id = chapters.group_id_3
|
||||
LEFT JOIN mangadex_mangas AS mangas
|
||||
ON mangas.manga_id = chapters.manga_id
|
||||
ON mangas.manga_id = chapters.manga_id
|
||||
LEFT JOIN mangadex_languages AS lang
|
||||
ON lang.lang_id = chapters.lang_id
|
||||
ON lang.lang_id = chapters.lang_id
|
||||
LEFT JOIN mangadex_users AS users
|
||||
ON users.user_id = chapters.user_id
|
||||
LEFT JOIN mangadex_user_options AS options
|
||||
@ -140,11 +140,11 @@ class Chapters {
|
||||
ON levels.level_id = users.level_id
|
||||
LEFT JOIN mangadex_threads AS threads
|
||||
ON threads.thread_id = chapters.thread_id
|
||||
WHERE $this->search_string 1=1
|
||||
ORDER BY $orderby
|
||||
WHERE $this->search_string 1=1
|
||||
ORDER BY $orderby
|
||||
LIMIT $limit OFFSET $offset
|
||||
", $this->pdo_bind, 'fetchAll', PDO::FETCH_ASSOC, 60); // using PDO::FETCH_ASSOC instead of PDO::FETCH_UNIQUE adds the chapter_id to the resulting array. FETCH_UNIQUE should never be used!
|
||||
|
||||
|
||||
//return get_results_as_object($results, 'chapter_id'); // TODO: Why would we ever want to return an array as an object?
|
||||
return $results;
|
||||
}
|
||||
@ -159,42 +159,42 @@ class Chapter {
|
||||
$this->sql = $sql;
|
||||
$this->pdo = $pdo;
|
||||
$id = prepare_numeric($id);
|
||||
|
||||
|
||||
$row = $sql->prep("chapter_$id", "
|
||||
SELECT chapters.*,
|
||||
lang.*,
|
||||
mangas.manga_name,
|
||||
mangas.manga_hentai,
|
||||
group1.group_website,
|
||||
group1.group_name AS group_name,
|
||||
group2.group_name AS group_name_2,
|
||||
group3.group_name AS group_name_3,
|
||||
group1.group_leader_id AS group_leader_id,
|
||||
group2.group_leader_id AS group_leader_id_2,
|
||||
SELECT chapters.*,
|
||||
lang.*,
|
||||
mangas.manga_name,
|
||||
mangas.manga_hentai,
|
||||
group1.group_website,
|
||||
group1.group_name AS group_name,
|
||||
group2.group_name AS group_name_2,
|
||||
group3.group_name AS group_name_3,
|
||||
group1.group_leader_id AS group_leader_id,
|
||||
group2.group_leader_id AS group_leader_id_2,
|
||||
group3.group_leader_id AS group_leader_id_3,
|
||||
threads.thread_posts
|
||||
FROM mangadex_chapters AS chapters
|
||||
LEFT JOIN mangadex_mangas AS mangas
|
||||
ON mangas.manga_id = chapters.manga_id
|
||||
ON mangas.manga_id = chapters.manga_id
|
||||
LEFT JOIN mangadex_languages AS lang
|
||||
ON lang.lang_id = chapters.lang_id
|
||||
LEFT JOIN mangadex_groups AS group1
|
||||
ON group1.group_id = chapters.group_id
|
||||
LEFT JOIN mangadex_groups AS group2
|
||||
ON lang.lang_id = chapters.lang_id
|
||||
LEFT JOIN mangadex_groups AS group1
|
||||
ON group1.group_id = chapters.group_id
|
||||
LEFT JOIN mangadex_groups AS group2
|
||||
ON group2.group_id = chapters.group_id_2
|
||||
LEFT JOIN mangadex_groups AS group3
|
||||
LEFT JOIN mangadex_groups AS group3
|
||||
ON group3.group_id = chapters.group_id_3
|
||||
LEFT JOIN mangadex_threads AS threads
|
||||
ON chapters.thread_id = threads.thread_id
|
||||
WHERE chapters.chapter_id = ?
|
||||
ON chapters.thread_id = threads.thread_id
|
||||
WHERE chapters.chapter_id = ?
|
||||
", [$id], 'fetch', PDO::FETCH_OBJ, 86400);
|
||||
|
||||
|
||||
//copy $row into $this
|
||||
if ($row) {
|
||||
if ($row) {
|
||||
foreach ($row as $key => $value) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
|
||||
|
||||
$this->chapter_comments = ($this->thread_posts) ? "<span class='badge'>$this->thread_posts</span>" : "";
|
||||
}
|
||||
}
|
||||
@ -204,35 +204,35 @@ class Chapter {
|
||||
SELECT posts.*, users.username, users.avatar, user_levels.level_colour, user_levels.level_id, user_levels.level_name,
|
||||
editor.username AS editor_username,
|
||||
editor_levels.level_colour AS editor_level_colour,
|
||||
(SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts
|
||||
WHERE mangadex_forum_posts.post_id <= posts.post_id
|
||||
(SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts
|
||||
WHERE mangadex_forum_posts.post_id <= posts.post_id
|
||||
AND mangadex_forum_posts.thread_id = posts.thread_id
|
||||
AND mangadex_forum_posts.deleted = 0) AS thread_page
|
||||
FROM mangadex_forum_posts AS posts
|
||||
LEFT JOIN mangadex_users AS users
|
||||
ON posts.user_id = users.user_id
|
||||
ON posts.user_id = users.user_id
|
||||
LEFT JOIN mangadex_user_levels AS user_levels
|
||||
ON users.level_id = user_levels.level_id
|
||||
LEFT JOIN mangadex_users AS editor
|
||||
ON users.level_id = user_levels.level_id
|
||||
LEFT JOIN mangadex_users AS editor
|
||||
ON posts.edit_user_id = editor.user_id
|
||||
LEFT JOIN mangadex_user_levels AS editor_levels
|
||||
LEFT JOIN mangadex_user_levels AS editor_levels
|
||||
ON editor.level_id = editor_levels.level_id
|
||||
WHERE posts.thread_id = ? AND posts.deleted = 0
|
||||
ORDER BY timestamp DESC
|
||||
WHERE posts.thread_id = ? AND posts.deleted = 0
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 20
|
||||
", [$this->thread_id], 'fetchAll', PDO::FETCH_UNIQUE, -1);
|
||||
|
||||
|
||||
return get_results_as_object($results, 'post_id');
|
||||
}
|
||||
|
||||
|
||||
public function get_other_chapters($group_id) {
|
||||
$results = $this->sql->prep("chapter_{$this->chapter_id}_other_chapters", "
|
||||
SELECT CONCAT(`volume`, ',', `chapter`) AS volch, chapter, chapter_id, volume, title, group_id
|
||||
FROM mangadex_chapters
|
||||
WHERE manga_id = ? AND lang_id = ? AND chapter_deleted = 0
|
||||
FROM mangadex_chapters
|
||||
WHERE manga_id = ? AND lang_id = ? AND chapter_deleted = 0
|
||||
ORDER BY (CASE volume WHEN '' THEN 1 END) DESC, abs(volume) DESC, abs(chapter) DESC
|
||||
", [$this->manga_id, $this->lang_id], 'fetchAll', PDO::FETCH_GROUP, 60);
|
||||
|
||||
|
||||
foreach ($results as $volch => $row) {
|
||||
if (count($row) == 1)
|
||||
$temp[$volch] = $row[0];
|
||||
@ -248,77 +248,77 @@ class Chapter {
|
||||
$temp[$volch] = $row[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach ($temp as $volch => $row) {
|
||||
if ($row['volume'] || $row['chapter']) {
|
||||
$array["name"][$row['chapter_id']] = ($row['volume'] ? "Volume {$row['volume']} " : "") . ($row['chapter'] ? "Chapter {$row['chapter']} " : "Chapter 0");
|
||||
$array["name"][$row['chapter_id']] = ($row['volume'] ? "Volume {$row['volume']} " : "") . ($row['chapter'] ? "Chapter {$row['chapter']} " : "Chapter 0");
|
||||
}
|
||||
else {
|
||||
$array["name"][$row['chapter_id']] = $row['title'];
|
||||
}
|
||||
|
||||
|
||||
$array["id"][] = $row['chapter_id'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
foreach ($array["name"] as $key => $name) {
|
||||
$array['tea'][] = [
|
||||
'id' => $key,
|
||||
'name' => trim($name)
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return $array; //array of groups or group_ids
|
||||
}
|
||||
|
||||
|
||||
public function get_other_groups() {
|
||||
return $this->sql->prep("chapter_{$this->chapter_id}_other_groups", "
|
||||
SELECT mangadex_chapters.chapter_id,
|
||||
mangadex_languages.lang_name, mangadex_languages.lang_flag,
|
||||
mangadex_chapters.group_id,
|
||||
mangadex_chapters.group_id_2,
|
||||
mangadex_chapters.group_id_3,
|
||||
group1.group_name AS group_name,
|
||||
group2.group_name AS group_name_2,
|
||||
SELECT mangadex_chapters.chapter_id,
|
||||
mangadex_languages.lang_name, mangadex_languages.lang_flag,
|
||||
mangadex_chapters.group_id,
|
||||
mangadex_chapters.group_id_2,
|
||||
mangadex_chapters.group_id_3,
|
||||
group1.group_name AS group_name,
|
||||
group2.group_name AS group_name_2,
|
||||
group3.group_name AS group_name_3
|
||||
FROM mangadex_chapters
|
||||
LEFT JOIN mangadex_groups AS group1
|
||||
ON group1.group_id = mangadex_chapters.group_id
|
||||
LEFT JOIN mangadex_groups AS group2
|
||||
FROM mangadex_chapters
|
||||
LEFT JOIN mangadex_groups AS group1
|
||||
ON group1.group_id = mangadex_chapters.group_id
|
||||
LEFT JOIN mangadex_groups AS group2
|
||||
ON group2.group_id = mangadex_chapters.group_id_2
|
||||
LEFT JOIN mangadex_groups AS group3
|
||||
LEFT JOIN mangadex_groups AS group3
|
||||
ON group3.group_id = mangadex_chapters.group_id_3
|
||||
LEFT JOIN mangadex_languages
|
||||
ON mangadex_languages.lang_id = mangadex_chapters.lang_id
|
||||
WHERE mangadex_chapters.manga_id = ?
|
||||
AND mangadex_chapters.lang_id = ?
|
||||
AND mangadex_chapters.volume = ?
|
||||
AND mangadex_chapters.chapter = ?
|
||||
AND mangadex_chapters.chapter_deleted = 0
|
||||
LEFT JOIN mangadex_languages
|
||||
ON mangadex_languages.lang_id = mangadex_chapters.lang_id
|
||||
WHERE mangadex_chapters.manga_id = ?
|
||||
AND mangadex_chapters.lang_id = ?
|
||||
AND mangadex_chapters.volume = ?
|
||||
AND mangadex_chapters.chapter = ?
|
||||
AND mangadex_chapters.chapter_deleted = 0
|
||||
ORDER BY mangadex_chapters.group_id ASC
|
||||
", [$this->manga_id, $this->lang_id, $this->volume, $this->chapter], 'fetchAll', PDO::FETCH_UNIQUE, 60);
|
||||
}
|
||||
|
||||
|
||||
public function get_pages_of_prev_chapter($id) {
|
||||
$page_order = $this->sql->prep("chapter_{$this->chapter_id}_pages_of_prev_chapter_$id", "
|
||||
SELECT page_order
|
||||
FROM mangadex_chapters
|
||||
WHERE chapter_id = ?
|
||||
SELECT page_order
|
||||
FROM mangadex_chapters
|
||||
WHERE chapter_id = ?
|
||||
LIMIT 1
|
||||
", [$id], 'fetchColumn', '', 60);
|
||||
|
||||
|
||||
return count(explode(",", $page_order));
|
||||
}
|
||||
|
||||
|
||||
/*public function update_chapter_views($array, $user_id) {
|
||||
if (!$array)
|
||||
$this->pdo->prepare(" INSERT IGNORE INTO mangadex_chapter_views_v2 (user_id, chapter_id) VALUES (?, ?) ")->execute([$user_id, $this->chapter_id]);
|
||||
|
||||
|
||||
elseif (!in_array($this->chapter_id, $array))
|
||||
$this->pdo->prepare(" UPDATE mangadex_chapter_views_v2 SET chapter_id = CONCAT(chapter_id, ?) WHERE user_id = ? LIMIT 1 ")->execute([",$this->chapter_id", $user_id]);
|
||||
}*/
|
||||
|
||||
|
||||
public function update_chapter_views($user_id, $array_of_user_ids) {
|
||||
if (isset($array_of_user_ids[$user_id]) && !in_array($array_of_user_ids[$user_id], [2,3,4,5])) {
|
||||
global $memcached;
|
||||
@ -326,7 +326,7 @@ class Chapter {
|
||||
$memcached->delete("user_{$user_id}_read_chapters");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function update_reading_history($user_id, $reading_history) {
|
||||
global $memcached;
|
||||
|
||||
@ -382,50 +382,50 @@ class Chapter_reports {
|
||||
public function __construct($age, $limit = 300) {
|
||||
global $sql;
|
||||
$age_operator = ($age == "new") ? "=" : ">";
|
||||
|
||||
|
||||
$limit = prepare_numeric($limit);
|
||||
//$offset = prepare_numeric($offset);
|
||||
|
||||
|
||||
$results = $sql->query_read("chapter_reports", "
|
||||
SELECT reports.*,
|
||||
chapters.manga_id,
|
||||
reporter.username AS reported_name,
|
||||
actioned.username AS actioned_name,
|
||||
reporter_levels.level_colour AS reported_level_colour,
|
||||
actioned_levels.level_colour AS actioned_level_colour
|
||||
SELECT reports.*,
|
||||
chapters.manga_id,
|
||||
reporter.username AS reported_name,
|
||||
actioned.username AS actioned_name,
|
||||
reporter_levels.level_colour AS reported_level_colour,
|
||||
actioned_levels.level_colour AS actioned_level_colour
|
||||
FROM mangadex_reports_chapters AS reports
|
||||
LEFT JOIN mangadex_users AS reporter
|
||||
LEFT JOIN mangadex_users AS reporter
|
||||
ON reports.report_user_id = reporter.user_id
|
||||
LEFT JOIN mangadex_user_levels AS reporter_levels
|
||||
LEFT JOIN mangadex_user_levels AS reporter_levels
|
||||
ON reporter.level_id = reporter_levels.level_id
|
||||
LEFT JOIN mangadex_users AS actioned
|
||||
LEFT JOIN mangadex_users AS actioned
|
||||
ON reports.report_mod_user_id = actioned.user_id
|
||||
LEFT JOIN mangadex_user_levels AS actioned_levels
|
||||
LEFT JOIN mangadex_user_levels AS actioned_levels
|
||||
ON actioned.level_id = actioned_levels.level_id
|
||||
LEFT JOIN mangadex_chapters AS chapters
|
||||
ON reports.report_chapter_id = chapters.chapter_id
|
||||
ON reports.report_chapter_id = chapters.chapter_id
|
||||
WHERE reports.report_mod_user_id $age_operator 0
|
||||
ORDER BY reports.report_timestamp DESC
|
||||
LIMIT $limit
|
||||
LIMIT $limit
|
||||
", 'fetchAll', PDO::FETCH_UNIQUE, -1);
|
||||
|
||||
|
||||
foreach ($results as $i => $report) {
|
||||
$this->{$i} = new \stdClass();
|
||||
foreach ($report as $key => $value) {
|
||||
$this->{$i}->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Upload_queue {
|
||||
public function __construct() {
|
||||
global $sql;
|
||||
|
||||
|
||||
$results = $sql->query_read("upload_queue", "
|
||||
SELECT queue.*,
|
||||
users.username, levels.level_colour
|
||||
FROM mangadex_upload_queue AS queue
|
||||
FROM mangadex_upload_queue AS queue
|
||||
LEFT JOIN mangadex_users AS users
|
||||
ON queue.user_id = users.user_id
|
||||
LEFT JOIN mangadex_chapters AS chapters
|
||||
@ -435,7 +435,7 @@ class Upload_queue {
|
||||
WHERE queue.queue_conclusion IS NULL
|
||||
ORDER BY chapters.upload_timestamp DESC
|
||||
", 'fetchAll', PDO::FETCH_UNIQUE, -1);
|
||||
|
||||
|
||||
foreach ($results as $i => $queue) {
|
||||
$this->{$i} = new \stdClass();
|
||||
foreach ($queue as $key => $value) {
|
||||
|
@ -182,7 +182,7 @@ class User {
|
||||
SELECT count(*)
|
||||
FROM mangadex_pm_threads
|
||||
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() {
|
||||
@ -190,7 +190,7 @@ class User {
|
||||
SELECT count(*)
|
||||
FROM mangadex_notifications
|
||||
WHERE mentionee_user_id = ? AND is_read = 0
|
||||
", [$this->user_id], 'fetchColumn', '');
|
||||
", [$this->user_id], 'fetchColumn', '', 60);
|
||||
}
|
||||
|
||||
public function get_groups() {
|
||||
@ -236,21 +236,19 @@ class User {
|
||||
", [$this->user_id], 'fetchAll', PDO::FETCH_UNIQUE); //contains progress tracker (volume and chapter)
|
||||
}
|
||||
|
||||
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", "
|
||||
SELECT m.manga_id, m.manga_name AS title, f.follow_type, f.volume, f.chapter, COALESCE(r.rating, 0) as rating
|
||||
FROM mangadex_mangas m
|
||||
LEFT JOIN mangadex_follow_user_manga f
|
||||
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 = ?
|
||||
WHERE m.manga_id = ?
|
||||
", [$this->user_id, $this->user_id, $manga_id], 'fetchAll', PDO::FETCH_ASSOC);
|
||||
public function get_manga_userdata($manga_id) { //contains progress data, title, and rating
|
||||
$follows = $this->get_followed_manga_ids_api();
|
||||
foreach ($follows as $manga) {
|
||||
if ($manga['manga_id'] == $manga_id) {
|
||||
return $manga;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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", "
|
||||
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
|
||||
JOIN mangadex_mangas m
|
||||
ON m.manga_id = f.manga_id
|
||||
@ -330,7 +328,7 @@ class User {
|
||||
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
|
||||
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
|
||||
LEFT JOIN mangadex_user_levels AS user_level
|
||||
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", "
|
||||
SELECT relations.user_id, user.user_id, user.username, user.last_seen_timestamp, user_level.level_colour
|
||||
FROM mangadex_user_relations AS relations
|
||||
LEFT JOIN mangadex_users AS user
|
||||
JOIN mangadex_users AS user
|
||||
ON relations.user_id = user.user_id
|
||||
LEFT JOIN mangadex_user_levels AS user_level
|
||||
ON user.level_id = user_level.level_id
|
||||
WHERE relations.relation_id = 1 AND relations.accepted = 0 AND relations.target_user_id = ?
|
||||
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() {
|
||||
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
|
||||
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
|
||||
LEFT JOIN mangadex_user_levels AS user_level
|
||||
ON user.level_id = user_level.level_id
|
||||
WHERE relations.relation_id = 0 AND relations.user_id = ? AND user.level_id < ?
|
||||
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() {
|
||||
@ -515,15 +513,18 @@ class PM_Threads {
|
||||
FROM mangadex_pm_threads
|
||||
WHERE (sender_id = ? AND sender_deleted = ?)
|
||||
OR (recipient_id = ? AND recipient_deleted = ?)
|
||||
ORDER BY thread_timestamp DESC LIMIT 20
|
||||
", [$user_id, $deleted, $user_id, $deleted], 'fetchColumn', '', -1);
|
||||
|
||||
$this->user_id = $user_id;
|
||||
$this->deleted = $deleted;
|
||||
}
|
||||
|
||||
public function query_read() {
|
||||
$results = $this->sql->prep("user_{$this->user_id}_PMs", "
|
||||
public function query_read($page = 1, $limit = 100)
|
||||
{
|
||||
$offset = ($page - 1) * $limit;
|
||||
$results = $this->sql->prep(
|
||||
"user_{$this->user_id}_PMs",
|
||||
"
|
||||
SELECT threads.*,
|
||||
sender.username AS sender_username,
|
||||
recipient.username AS recipient_username,
|
||||
@ -541,8 +542,13 @@ class PM_Threads {
|
||||
WHERE (threads.sender_id = ? AND threads.sender_deleted = ?)
|
||||
OR (threads.recipient_id = ? AND threads.recipient_deleted = ?)
|
||||
ORDER BY threads.thread_timestamp DESC
|
||||
LIMIT 100
|
||||
", [$this->user_id, $this->deleted, $this->user_id, $this->deleted], 'fetchAll', PDO::FETCH_ASSOC, -1);
|
||||
LIMIT ? OFFSET ?
|
||||
",
|
||||
[$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 $results;
|
||||
@ -630,4 +636,4 @@ class Notifications {
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
?>
|
||||
?>
|
||||
|
@ -19,7 +19,6 @@
|
||||
border-radius: 5px;
|
||||
}
|
||||
.noselect {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-epub-user-select: none;
|
||||
-moz-user-select: none;
|
||||
@ -29,11 +28,6 @@
|
||||
}
|
||||
.nodrag {
|
||||
-webkit-user-drag: none;
|
||||
-epub-user-drag: none;
|
||||
-moz-user-drag: none;
|
||||
-ms-user-drag: none;
|
||||
-o-user-drag: none;
|
||||
user-drag: none;
|
||||
}
|
||||
.noevents {
|
||||
-webkit-pointer-events: none;
|
||||
@ -70,12 +64,14 @@ body {
|
||||
.reader-page-bar,
|
||||
.reader-page-bar .trail,
|
||||
.reader-page-bar .thumb,
|
||||
.reader-page-bar .track {
|
||||
.reader-page-bar .track,
|
||||
.reader-controls-collapser:before,
|
||||
.reader-controls-collapser span {
|
||||
transition-property: all;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
#reader-controls-collapser span {
|
||||
transition-property: all;
|
||||
#reader-controls-collapser-bar {
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.4s;
|
||||
}
|
||||
|
||||
@ -88,18 +84,12 @@ nav.navbar {
|
||||
.footer {
|
||||
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 */
|
||||
|
||||
#modal-settings:not(.show-advanced) .advanced {
|
||||
display: none;
|
||||
}
|
||||
#modal-settings .advanced label {
|
||||
}
|
||||
#modal-settings .advanced label:before {
|
||||
content: '* ';
|
||||
}
|
||||
@ -110,12 +100,70 @@ nav.navbar {
|
||||
left: 0;
|
||||
max-width: 100vw;
|
||||
}
|
||||
#reader-controls-collapser {
|
||||
width: 34px;
|
||||
.reader-controls-collapser {
|
||||
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);
|
||||
}
|
||||
#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) {
|
||||
display: none
|
||||
}
|
||||
@ -333,9 +381,6 @@ body {
|
||||
.reader-page-bar:hover .thumb {
|
||||
background: #eee;
|
||||
}
|
||||
.reader-page-bar:hover .track {
|
||||
/*border: 2px solid #ccc;*/
|
||||
}
|
||||
.reader-page-bar .notch:not(.loaded) {
|
||||
background: repeating-linear-gradient(
|
||||
-45deg,
|
||||
@ -431,6 +476,11 @@ body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* cursor hiding */
|
||||
.hide-cursor .reader-images img {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
/* Modernizr */
|
||||
|
||||
.no-localstorage #alert-storage-warning {
|
||||
@ -454,6 +504,15 @@ body {
|
||||
/* desktop definitions */
|
||||
|
||||
@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 {
|
||||
order: 2;
|
||||
@ -462,6 +521,10 @@ body {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
#right_swipe_area {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* controls */
|
||||
|
||||
.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-footer,
|
||||
.reader:not(.layout-horizontal) #reader-controls-collapser
|
||||
.reader:not(.layout-horizontal) .reader-controls-collapser
|
||||
{
|
||||
display: none !important;
|
||||
}
|
||||
@ -486,12 +549,17 @@ body {
|
||||
top: 0;
|
||||
}
|
||||
.reader.layout-horizontal.hide-sidebar .reader-controls-wrapper {
|
||||
width: 34px;
|
||||
width: 0;
|
||||
}
|
||||
.reader.layout-horizontal.hide-sidebar .reader-controls {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reader[data-collapser="bar"] #reader-controls-collapser-bar,
|
||||
.reader[data-collapser="button"] #reader-controls-collapser-button {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* load icon */
|
||||
|
||||
.reader.layout-horizontal .reader-load-icon {
|
||||
@ -511,7 +579,7 @@ body {
|
||||
width: auto;
|
||||
}
|
||||
.reader.layout-horizontal.hide-sidebar .reader-page-bar {
|
||||
right: 34px;
|
||||
right: 0;
|
||||
}
|
||||
.reader.layout-horizontal .reader-page-bar:hover .track {
|
||||
height: 50px;
|
||||
@ -532,7 +600,8 @@ body {
|
||||
padding-right: 20vw !important;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
user-select: none;
|
||||
|
@ -498,7 +498,7 @@ function display_manga_ext_links($links_array) {
|
||||
case "bw":
|
||||
$return .= "<li class='list-inline-item'><img src='" . LOCAL_SERVER_URL . "/images/misc/$type.png' /> <a rel='noopener noreferrer' target='_blank' href='https://bookwalker.jp/" . htmlspecialchars($id, ENT_QUOTES) . "/'>Bookwalker</a></li>";
|
||||
break;
|
||||
|
||||
|
||||
case "al":
|
||||
$return .= "<li class='list-inline-item'><img src='" . LOCAL_SERVER_URL . "/images/misc/$type.png' /> <a rel='noopener noreferrer' target='_blank' href='https://anilist.co/manga/" . htmlspecialchars($id, ENT_QUOTES) . "/'>AniList</a></li>";
|
||||
break;
|
||||
@ -510,7 +510,7 @@ function display_manga_ext_links($links_array) {
|
||||
case 'ap':
|
||||
$return .= "<li class='list-inline-item'><img src='" . LOCAL_SERVER_URL . "/images/misc/$type.png' /> <a rel='noopener noreferrer' target='_blank' href='https://www.anime-planet.com/manga/" . htmlspecialchars($id, ENT_QUOTES). "'>Anime-Planet</a></li>";
|
||||
break;
|
||||
|
||||
|
||||
case 'dj':
|
||||
$return .= "<li class='list-inline-item'><img src='" . LOCAL_SERVER_URL . "/images/misc/$type.png' /> <a rel='noopener noreferrer' target='_blank' href='https://www.doujinshi.org/book/" . htmlspecialchars($id, ENT_QUOTES). "'>Doujinshi.org</a></li>";
|
||||
break;
|
||||
@ -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() {
|
||||
return "
|
||||
$('.bbcode').click(function(){
|
||||
@ -640,7 +634,7 @@ function display_forum($forum, $user) {
|
||||
$return = "
|
||||
<div class='d-flex row m-0 py-1 border-bottom align-items-center'>
|
||||
<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 class='col p-0 text-truncate'>
|
||||
<div class='row m-2'>
|
||||
@ -983,13 +977,13 @@ function display_user_link_v2($user, $note = '') {
|
||||
$levelClassname = str_replace(' ', '', ltrim(strtolower(preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '_$0', $user->level_name ?? 'guest')), '_'));
|
||||
|
||||
$string = "<a class='user_level_$levelClassname' style='color: #$user->level_colour; ' href='/user/$user->user_id/" . strtolower($user->username) . "'>$user->username</a>";
|
||||
|
||||
|
||||
if ($user->show_premium_badge ?? false)
|
||||
$string .= " <a href='/support'>" . display_fa_icon('gem', 'Supporter', '', 'far') . "</a>";
|
||||
|
||||
|
||||
if ($user->show_md_at_home_badge ?? false)
|
||||
$string .= " <a href='/md_at_home'>" . display_fa_icon('network-wired', 'MD@H Host', '', 'fas' . ($user->show_md_at_home_badge == 2 ? ' text-warning' : '')) . "</a>";
|
||||
|
||||
|
||||
if ($user->is_thread_starter ?? false)
|
||||
$string .= " <span class='badge badge-primary'>OP</span>";
|
||||
if ($note)
|
||||
@ -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) {
|
||||
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>";
|
||||
|
@ -998,6 +998,24 @@ function get_zip_originalsize($filename) {
|
||||
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
|
||||
*************************************/
|
||||
|
@ -2,17 +2,17 @@
|
||||
|
||||
// Sentry error handling (must be as early as possible to catch any init exceptions=
|
||||
if (defined('SENTRY_DSN') && SENTRY_DSN && class_exists('Raven_Client')) {
|
||||
$sentry = new Raven_Client(SENTRY_DSN, [
|
||||
'sample_rate' => SENTRY_SAMPLE_RATE,
|
||||
'curl_method' => SENTRY_CURL_METHOD,
|
||||
'timeout' => SENTRY_TIMEOUT
|
||||
]);
|
||||
try {
|
||||
$sentry->install();
|
||||
} catch (\Raven_Exception $e) {
|
||||
// This should land in the logfiles at least but not block script execution
|
||||
trigger_error('Failed to install Sentry client: '.$e->getMessage(), E_USER_WARNING);
|
||||
}
|
||||
$sentry = new Raven_Client(SENTRY_DSN, [
|
||||
'sample_rate' => SENTRY_SAMPLE_RATE,
|
||||
'curl_method' => SENTRY_CURL_METHOD,
|
||||
'timeout' => SENTRY_TIMEOUT
|
||||
]);
|
||||
try {
|
||||
$sentry->install();
|
||||
} catch (\Raven_Exception $e) {
|
||||
// This should land in the logfiles at least but not block script execution
|
||||
trigger_error('Failed to install Sentry client: '.$e->getMessage(), E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
//database stuff
|
||||
@ -24,217 +24,249 @@ $dsn_master = "mysql:host=$host;dbname=$db;charset=$charset";
|
||||
$dsn_slaves = [];
|
||||
|
||||
foreach (DB_READ_HOSTS ?? [] AS $slave_host) {
|
||||
$slave_db = DB_READ_NAME;
|
||||
$slave_port = 3306;
|
||||
if (strpos($slave_host, ':') !== false) {
|
||||
[$slave_host, $slave_port] = explode(':', $slave_host, 2);
|
||||
}
|
||||
$dsn_slaves[] = "mysql:host=$slave_host;port=$slave_port;dbname=$slave_db;charset=$charset";
|
||||
$slave_db = DB_READ_NAME;
|
||||
$slave_port = 3306;
|
||||
if (strpos($slave_host, ':') !== false) {
|
||||
[$slave_host, $slave_port] = explode(':', $slave_host, 2);
|
||||
}
|
||||
$dsn_slaves[] = "mysql:host=$slave_host;port=$slave_port;dbname=$slave_db;charset=$charset";
|
||||
}
|
||||
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_PERSISTENT => defined('DB_PERSISTENT') ? (bool)DB_PERSISTENT : false,
|
||||
];
|
||||
|
||||
class SQL extends PDO {
|
||||
private $debug = [];
|
||||
private $time_array = [];
|
||||
private $debug = [];
|
||||
private $time_array = [];
|
||||
|
||||
/** @var \PDO */
|
||||
private $slave_sql;
|
||||
/** @var \PDO */
|
||||
private $slave_sql;
|
||||
|
||||
public function __construct(string $dsn_master, array $dsn_slaves, $username = null, $passwd = null, $options = null)
|
||||
{
|
||||
// Establish connection with master
|
||||
parent::__construct($dsn_master, $username, $passwd, $options);
|
||||
|
||||
// Randomize pick order
|
||||
shuffle($dsn_slaves);
|
||||
|
||||
while (!empty($dsn_slaves)) {
|
||||
$dsn_slave = array_pop($dsn_slaves);
|
||||
$error = 'Slave failed with unknown reason';
|
||||
try {
|
||||
$this->slave_sql = new \PDO($dsn_slave, DB_READ_USER, DB_READ_PASSWORD, $options);
|
||||
// Try a ping
|
||||
if (false === $this->slave_sql->query('SELECT 1')) {
|
||||
throw new \RuntimeException('Ping on slave failed!');
|
||||
}
|
||||
// Connection successful
|
||||
return;
|
||||
} catch (\PDOException $e) {
|
||||
$error = sprintf('Slave failed with error code %s', $e->getCode());
|
||||
} catch (\Throwable $e) {
|
||||
$error = sprintf('Unexpected exception: %s', $e->getMessage());
|
||||
}
|
||||
// A slave failed, report warning to sentry
|
||||
trigger_error($error, E_USER_WARNING);
|
||||
}
|
||||
// Fall back to master
|
||||
$this->slave_sql = $this;
|
||||
}
|
||||
|
||||
public function query_read($name, $query, $fetch, $pdo_mode, $expiry = 0) {
|
||||
global $memcached;
|
||||
|
||||
$name = str_replace(' ', '_', $name);
|
||||
|
||||
if ($expiry < 0) //delete from cache and update
|
||||
$memcached->delete($name);
|
||||
|
||||
$start = microtime(true);
|
||||
|
||||
$cache = $memcached->get($name);
|
||||
$from_cache = 'Y';
|
||||
|
||||
if ($cache === FALSE) {
|
||||
if ($fetch == 'fetchAll')
|
||||
$cache = $this->slave_sql->query($query)->fetchAll($pdo_mode);
|
||||
elseif ($fetch == 'fetchColumn')
|
||||
$cache = $this->slave_sql->query($query)->fetchColumn();
|
||||
else
|
||||
$cache = $this->slave_sql->query($query)->fetch($pdo_mode);
|
||||
|
||||
if ($expiry >= 0)
|
||||
$memcached->set($name, $cache, $expiry);
|
||||
$from_cache = 'N';
|
||||
}
|
||||
|
||||
$time_taken = round((microtime(true) - $start) * 1000, 2);
|
||||
|
||||
$this->time_array[] = $time_taken;
|
||||
$this->debug[] = [$name, nl2br(trim($query)), $fetch, $from_cache, $time_taken];
|
||||
|
||||
return $cache;
|
||||
}
|
||||
|
||||
public function prep($name, $query, $bind, $fetch, $pdo_mode = '', $expiry = 0) {
|
||||
global $memcached;
|
||||
|
||||
$name = str_replace(' ', '_', $name);
|
||||
|
||||
if ($expiry < 0) //delete from cache and update
|
||||
$memcached->delete($name);
|
||||
|
||||
$start = microtime(true);
|
||||
|
||||
$cache = $memcached->get($name);
|
||||
$from_cache = 'Y';
|
||||
|
||||
if ($cache === FALSE) {
|
||||
$stmt = $this->slave_sql->prepare($query);
|
||||
$stmt->execute($bind);
|
||||
|
||||
switch ($fetch) {
|
||||
case 'fetch':
|
||||
$cache = $stmt->fetch($pdo_mode);
|
||||
break;
|
||||
|
||||
case 'fetchColumn':
|
||||
$cache = $stmt->fetchColumn();
|
||||
break;
|
||||
|
||||
default:
|
||||
$cache = $stmt->fetchAll($pdo_mode);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($expiry >= 0)
|
||||
$memcached->set($name, $cache, $expiry);
|
||||
$from_cache = 'N';
|
||||
}
|
||||
|
||||
$time_taken = round((microtime(true) - $start) * 1000, 2);
|
||||
|
||||
$this->time_array[] = $time_taken;
|
||||
|
||||
$query = preg_replace(array_fill(0, count($bind), '/\?/'), array_fill(0, count($bind), "<span style='color: red'>~</span>"), $query, 1);
|
||||
$query = preg_replace(array_fill(0, count($bind), '/~/'), $bind, $query, 1);
|
||||
$this->debug[] = [$name, nl2br(trim($query)), $fetch, $from_cache, $time_taken];
|
||||
|
||||
return $cache;
|
||||
}
|
||||
|
||||
public function modify($name, $query, $bind) {
|
||||
$name = str_replace(' ', '_', $name);
|
||||
|
||||
$start = microtime(true);
|
||||
|
||||
$from_cache = '/';
|
||||
|
||||
$stmt = $this->prepare($query);
|
||||
$stmt->execute($bind);
|
||||
|
||||
$time_taken = round((microtime(true) - $start) * 1000, 2);
|
||||
|
||||
$this->time_array[] = $time_taken;
|
||||
|
||||
$query = preg_replace(array_fill(0, count($bind), '/\?/'), array_fill(0, count($bind), "<span style='color: red'>~</span>"), $query, 1);
|
||||
$query = preg_replace(array_fill(0, count($bind), '/~/'), $bind, $query, 1);
|
||||
$this->debug[] = [$name, nl2br(trim($query)), 'modify', $from_cache, $time_taken];
|
||||
|
||||
return $this->lastInsertId();
|
||||
}
|
||||
|
||||
public function modify_deferred($name, $query, $bind) {
|
||||
private $credentials = [];
|
||||
private $isConnected = false;
|
||||
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
||||
public function debug() {
|
||||
global $memcached;
|
||||
|
||||
// ======== Add sql table
|
||||
$return = "
|
||||
<table style='margin-top: 50px;' class='table table-condensed table-striped'>
|
||||
<tr>
|
||||
<th><button id='toggle-sql-table'><i class='fa fa-eye'></i></button></th>
|
||||
<th>N</th>
|
||||
<th>Q</th>
|
||||
<th>M</th>
|
||||
<th>C</th>
|
||||
<th>T</th>
|
||||
</tr>";
|
||||
|
||||
foreach ($this->debug as $key => $array) {
|
||||
++$key;
|
||||
$return .= "<tr style='display:none'>";
|
||||
$return .= "<td>$key</td>";
|
||||
foreach ($array as $value) {
|
||||
$return .= "<td>$value</td>";
|
||||
}
|
||||
$return .= "</tr>";
|
||||
}
|
||||
|
||||
$total_time = array_sum($this->time_array);
|
||||
|
||||
$return .= "
|
||||
<tr style='display:none'>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Query</th>
|
||||
<th>Mode</th>
|
||||
<th>Cache</th>
|
||||
<th>$total_time</th>
|
||||
</tr>
|
||||
</table>";
|
||||
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 = [];
|
||||
|
||||
if (defined('CAPTURE_CACHE_STATS') && CAPTURE_CACHE_STATS) {
|
||||
// Establish connection with master
|
||||
parent::__construct($dsn_master, $username, $passwd, $options);
|
||||
|
||||
$this->isConnected = true;
|
||||
|
||||
// Randomize pick order
|
||||
shuffle($dsn_slaves);
|
||||
|
||||
while (!empty($dsn_slaves)) {
|
||||
$dsn_slave = array_pop($dsn_slaves);
|
||||
$error = 'Slave failed with unknown reason';
|
||||
try {
|
||||
$this->slave_sql = new \PDO($dsn_slave, DB_READ_USER, DB_READ_PASSWORD, $options);
|
||||
// Try a ping
|
||||
if (false === $this->slave_sql->query('SELECT 1')) {
|
||||
throw new \RuntimeException('Ping on slave failed!');
|
||||
}
|
||||
// Connection successful
|
||||
return;
|
||||
} catch (\PDOException $e) {
|
||||
$error = sprintf('Slave failed with error code %s', $e->getCode());
|
||||
} catch (\Throwable $e) {
|
||||
$error = sprintf('Unexpected exception: %s', $e->getMessage());
|
||||
}
|
||||
// A slave failed, report warning to sentry
|
||||
trigger_error($error, E_USER_WARNING);
|
||||
}
|
||||
// Fall back to master
|
||||
$this->slave_sql = $this;
|
||||
}
|
||||
}
|
||||
|
||||
public function query_read($name, $query, $fetch, $pdo_mode, $expiry = 0) {
|
||||
global $memcached;
|
||||
|
||||
$name = str_replace(' ', '_', $name);
|
||||
|
||||
if ($expiry < 0) {
|
||||
$memcached->delete($name);
|
||||
}
|
||||
|
||||
$start = microtime(true);
|
||||
|
||||
$cache = $memcached->get($name);
|
||||
$from_cache = 'Y';
|
||||
|
||||
if ($cache === FALSE) {
|
||||
$this->ensureConnected();
|
||||
if ($fetch === 'fetchAll') {
|
||||
$cache = $this->slave_sql->query($query)->fetchAll($pdo_mode);
|
||||
}
|
||||
elseif ($fetch === 'fetchColumn') {
|
||||
$cache = $this->slave_sql->query($query)->fetchColumn();
|
||||
}
|
||||
else {
|
||||
$cache = $this->slave_sql->query($query)->fetch($pdo_mode);
|
||||
}
|
||||
|
||||
if ($expiry >= 0) {
|
||||
$memcached->set($name, $cache, $expiry);
|
||||
}
|
||||
$from_cache = 'N';
|
||||
}
|
||||
|
||||
$time_taken = round((microtime(true) - $start) * 1000, 2);
|
||||
|
||||
$this->time_array[] = $time_taken;
|
||||
$this->debug[] = [$name, nl2br(trim($query)), $fetch, $from_cache, $time_taken];
|
||||
|
||||
return $cache;
|
||||
}
|
||||
|
||||
public function prep($name, $query, $bind, $fetch, $pdo_mode = '', $expiry = 0, $force_master = false) {
|
||||
global $memcached;
|
||||
|
||||
$name = str_replace(' ', '_', $name);
|
||||
|
||||
if ($expiry < 0) {
|
||||
$memcached->delete($name);
|
||||
}
|
||||
|
||||
$start = microtime(true);
|
||||
|
||||
$cache = $memcached->get($name);
|
||||
$from_cache = 'Y';
|
||||
|
||||
if ($cache === FALSE) {
|
||||
$this->ensureConnected();
|
||||
$stmt = $force_master ? $this->prepare($query) : $this->slave_sql->prepare($query);
|
||||
$stmt->execute($bind);
|
||||
|
||||
switch ($fetch) {
|
||||
case 'fetch':
|
||||
$cache = $stmt->fetch($pdo_mode);
|
||||
break;
|
||||
|
||||
case 'fetchColumn':
|
||||
$cache = $stmt->fetchColumn();
|
||||
break;
|
||||
|
||||
default:
|
||||
$cache = $stmt->fetchAll($pdo_mode);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($expiry >= 0) {
|
||||
$memcached->set($name, $cache, $expiry);
|
||||
}
|
||||
$from_cache = 'N';
|
||||
}
|
||||
|
||||
$time_taken = round((microtime(true) - $start) * 1000, 2);
|
||||
|
||||
$this->time_array[] = $time_taken;
|
||||
|
||||
$query = preg_replace(array_fill(0, count($bind), '/\?/'), array_fill(0, count($bind), "<span style='color: red'>~</span>"), $query, 1);
|
||||
$query = preg_replace(array_fill(0, count($bind), '/~/'), $bind, $query, 1);
|
||||
$this->debug[] = [$name, nl2br(trim($query)), $fetch, $from_cache, $time_taken];
|
||||
|
||||
return $cache;
|
||||
}
|
||||
|
||||
public function modify($name, $query, $bind) {
|
||||
$name = str_replace(' ', '_', $name);
|
||||
|
||||
$start = microtime(true);
|
||||
|
||||
$from_cache = '/';
|
||||
|
||||
$this->ensureConnected();
|
||||
$stmt = $this->prepare($query);
|
||||
$stmt->execute($bind);
|
||||
|
||||
$time_taken = round((microtime(true) - $start) * 1000, 2);
|
||||
|
||||
$this->time_array[] = $time_taken;
|
||||
|
||||
$query = preg_replace(array_fill(0, count($bind), '/\?/'), array_fill(0, count($bind), "<span style='color: red'>~</span>"), $query, 1);
|
||||
$query = preg_replace(array_fill(0, count($bind), '/~/'), $bind, $query, 1);
|
||||
$this->debug[] = [$name, nl2br(trim($query)), 'modify', $from_cache, $time_taken];
|
||||
|
||||
return $this->lastInsertId();
|
||||
}
|
||||
|
||||
public function debug() {
|
||||
global $memcached;
|
||||
|
||||
// ======== Add sql table
|
||||
$return = "
|
||||
<table style='margin-top: 50px;' class='table table-condensed table-striped'>
|
||||
<tr>
|
||||
<th><button id='toggle-sql-table'><i class='fa fa-eye'></i></button></th>
|
||||
<th>N</th>
|
||||
<th>Q</th>
|
||||
<th>M</th>
|
||||
<th>C</th>
|
||||
<th>T</th>
|
||||
</tr>";
|
||||
|
||||
foreach ($this->debug as $key => $array) {
|
||||
++$key;
|
||||
$return .= "<tr style='display:none'>";
|
||||
$return .= "<td>$key</td>";
|
||||
foreach ($array as $value) {
|
||||
$return .= "<td>$value</td>";
|
||||
}
|
||||
$return .= "</tr>";
|
||||
}
|
||||
|
||||
$total_time = array_sum($this->time_array);
|
||||
|
||||
$return .= "
|
||||
<tr style='display:none'>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Query</th>
|
||||
<th>Mode</th>
|
||||
<th>Cache</th>
|
||||
<th>$total_time</th>
|
||||
</tr>
|
||||
</table>";
|
||||
|
||||
if (defined('CAPTURE_CACHE_STATS') && CAPTURE_CACHE_STATS) {
|
||||
// ======== Add cache table
|
||||
|
||||
$cacheDebug = $memcached->toArray();
|
||||
|
||||
$return .= "
|
||||
<table style='margin-top: 50px;' class='table table-condensed table-striped'>
|
||||
<tr>
|
||||
<th><button id='toggle-cache-table'><i class='fa fa-eye'></i></button></th>
|
||||
<th>Method</th>
|
||||
<th>Time (s)</th>
|
||||
<th>Key</th>
|
||||
<th>Result</th>
|
||||
<th>Call Stack</th>
|
||||
</tr>";
|
||||
<table style='margin-top: 50px;' class='table table-condensed table-striped'>
|
||||
<tr>
|
||||
<th><button id='toggle-cache-table'><i class='fa fa-eye'></i></button></th>
|
||||
<th>Method</th>
|
||||
<th>Time (s)</th>
|
||||
<th>Key</th>
|
||||
<th>Result</th>
|
||||
<th>Call Stack</th>
|
||||
</tr>";
|
||||
|
||||
$return .= "
|
||||
<tr style='display:none'>
|
||||
@ -256,18 +288,18 @@ class SQL extends PDO {
|
||||
$total_time = array_sum($this->time_array);
|
||||
|
||||
$return .= "
|
||||
<tr style='display:none'>
|
||||
<th>#</th>
|
||||
<th>Method</th>
|
||||
<th>Time (s)</th>
|
||||
<th>Key</th>
|
||||
<th>Result</th>
|
||||
<th>$cacheDebug[time]</th>
|
||||
</tr>
|
||||
</table>";
|
||||
<tr style='display:none'>
|
||||
<th>#</th>
|
||||
<th>Method</th>
|
||||
<th>Time (s)</th>
|
||||
<th>Key</th>
|
||||
<th>Result</th>
|
||||
<th>$cacheDebug[time]</th>
|
||||
</tr>
|
||||
</table>";
|
||||
}
|
||||
|
||||
$return .= "
|
||||
$return .= "
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
$('#toggle-sql-table').click(function(ev){
|
||||
@ -286,9 +318,9 @@ class SQL extends PDO {
|
||||
});
|
||||
});
|
||||
</script>";
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@ -305,7 +337,7 @@ require_once ABSPATH . '/scripts/classes/cache.class.req.php';
|
||||
if (defined('CAPTURE_CACHE_STATS') && CAPTURE_CACHE_STATS) {
|
||||
$memcached = new Cache();
|
||||
} else {
|
||||
$memcached = new Memcached();
|
||||
$memcached = new Synced_Memcached();
|
||||
}
|
||||
$memcached->addServer(MEMCACHED_HOST, 11211);
|
||||
|
||||
@ -314,7 +346,7 @@ require_once (ABSPATH . '/scripts/functions.req.php');
|
||||
|
||||
foreach (read_dir('scripts/classes') as $file) {
|
||||
$file = str_replace(['..', '/', '\\', '`', '´', '"', "'"], '', $file);
|
||||
require_once (ABSPATH . "/scripts/classes/$file");
|
||||
require_once (ABSPATH . "/scripts/classes/$file");
|
||||
} //require every file in classes
|
||||
|
||||
require_once (ABSPATH . '/scripts/display.req.php');
|
||||
|
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_regenerate_thumb", $id, '', "Regenerate thumb", "Regenerating thumb", "You have regenerated the thumbnail.", "location.reload();") ?>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
<?php if (validate_level($user, 'gmod')) { ?>
|
||||
|
@ -1,7 +1,93 @@
|
||||
$("#user_search_form").submit(function(event) {
|
||||
var email = encodeURIComponent($("#email").val());
|
||||
var username = encodeURIComponent($("#username").val());
|
||||
$("#search_button").html("<?= display_fa_icon('spinner', '', 'fa-pulse') ?> Searching...").attr("disabled", true);
|
||||
location.href = "/pr/?username="+username+"&email="+email;
|
||||
event.preventDefault();
|
||||
});
|
||||
<?php
|
||||
switch ($mode) {
|
||||
case 'email_search':
|
||||
?>
|
||||
$("#user_search_form").submit(function(event) {
|
||||
var email = encodeURIComponent($("#email").val());
|
||||
var username = encodeURIComponent($("#username").val());
|
||||
$("#search_button").html("<?= display_fa_icon('spinner', '', 'fa-pulse') ?> Searching...").attr("disabled", true);
|
||||
location.href = "/pr/email_search?username="+username+"&email="+email;
|
||||
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
|
||||
}
|
60
sql.php
@ -2,9 +2,22 @@
|
||||
if (PHP_SAPI !== 'cli')
|
||||
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");
|
||||
|
||||
|
||||
/*
|
||||
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);
|
||||
|
||||
@ -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) {
|
||||
$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('..', '.'));
|
||||
|
||||
foreach ($files as $file) {
|
||||
|
||||
$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";
|
||||
|
||||
$sql->modify('update', " UPDATE mangadex_chapters SET server = 1 WHERE chapter_id = ? LIMIT 1; ", [$chapter_id]);
|
||||
$memcached->delete("chapter_$chapter_id");
|
||||
//$sql->modify('update', " UPDATE mangadex_chapters SET server = 1 WHERE chapter_id = ? LIMIT 1; ", [$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);
|
||||
foreach ($results as $row) {
|
||||
$uid = $row['user_id'];
|
||||
@ -348,7 +361,7 @@ foreach ($results as $row) {
|
||||
print $uid . ' ';
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
/*
|
||||
foreach (WALLET_QR['ETH'] as $qr) {
|
||||
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 * 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) {
|
||||
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);
|
||||
|
||||
@ -481,4 +512,5 @@ foreach ($result as $row) {
|
||||
}*/
|
||||
|
||||
//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
|
||||
{
|
||||
const CHAPTERS_LIMIT = 6000;
|
||||
const CHAPTERS_LIMIT = 8000;
|
||||
const CH_STATUS_OK = 'OK';
|
||||
const CH_STATUS_DELETED = 'deleted';
|
||||
const CH_STATUS_DELAYED = 'delayed';
|
||||
@ -36,10 +36,9 @@ class ChapterController extends APIController
|
||||
|
||||
public function view($path)
|
||||
{
|
||||
/**
|
||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
||||
*/
|
||||
[$id, $subResource, $subResourceId] = $path;
|
||||
$id = $path[0] ?? null;
|
||||
$subResource = $path[1] ?? null;
|
||||
$subResourceId = $path[2] ?? null;
|
||||
|
||||
$id = $this->validateId($id);
|
||||
|
||||
@ -53,6 +52,13 @@ class ChapterController extends APIController
|
||||
if (isset($normalized['pages'])) {
|
||||
$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;
|
||||
}
|
||||
|
||||
@ -87,6 +93,13 @@ class ChapterController extends APIController
|
||||
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);
|
||||
$list = $chapters->query_read($order, self::CHAPTERS_LIMIT, 1);
|
||||
if ($page > 0) {
|
||||
@ -153,9 +166,11 @@ class ChapterController extends APIController
|
||||
if (!empty($langFilter)) {
|
||||
$search["multi_lang_id"] = $langFilter;
|
||||
}
|
||||
$blockedGroups = $this->user->get_blocked_groups();
|
||||
if ($blockedGroups) {
|
||||
$search['blocked_groups'] = array_keys($blockedGroups);
|
||||
if ($this->request->query->getBoolean('blockgroups', true)) {
|
||||
$blockedGroups = $this->user->get_blocked_groups();
|
||||
if ($blockedGroups) {
|
||||
$search['blocked_groups'] = array_keys($blockedGroups);
|
||||
}
|
||||
}
|
||||
if ($hentai !== 1) { // i.e. if hentai is 0 (hide) or >1 (show only)
|
||||
$search['manga_hentai'] = $hentai ? 1 : 0;
|
||||
@ -174,13 +189,25 @@ class ChapterController extends APIController
|
||||
$chaptersResult = $chapters->query_read($order, $limit, max($page, 1));
|
||||
$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
|
||||
$manga = [];
|
||||
foreach ($chaptersResult as $chapter) {
|
||||
foreach ($chaptersResult as &$chapter) {
|
||||
if (!isset($manga[$chapter['manga_id']])) {
|
||||
$manga[$chapter['manga_id']] = [
|
||||
'id' => $chapter['manga_id'],
|
||||
// TODO: remove 'name'
|
||||
'name' => $chapter['manga_name'],
|
||||
'title' => $chapter['manga_name'],
|
||||
'isHentai' => (bool)$chapter['manga_hentai'],
|
||||
'lastChapter' => (!empty($chapter['manga_last_chapter']) && $chapter['manga_last_chapter'] !== '0') ? $chapter['manga_last_chapter'] : null,
|
||||
'lastVolume' => (string)$chapter['manga_last_volume'] ?: null,
|
||||
@ -208,6 +235,7 @@ class ChapterController extends APIController
|
||||
'groups' => [],
|
||||
'uploader' => $chapter->user_id,
|
||||
'timestamp' => $chapter->upload_timestamp,
|
||||
'threadId' => $chapter->thread_id,
|
||||
'comments' => $chapter->thread_posts ?? 0,
|
||||
'views' => $chapter->chapter_views ?? 0,
|
||||
];
|
||||
@ -218,36 +246,58 @@ class ChapterController extends APIController
|
||||
});
|
||||
$normalized['groups'] = array_map(function ($g) {
|
||||
return ['id' => $g[0], 'name' => $g[1]];
|
||||
}, $groupsFiltered);
|
||||
}, array_values($groupsFiltered));
|
||||
|
||||
if ($fullData) {
|
||||
$isValidated = validate_level($this->user, 'pr')
|
||||
|| $this->request->headers->get("API_KEY") === PRIVATE_API_KEY;
|
||||
|
||||
$normalized['status'] = self::CH_STATUS_OK;
|
||||
$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
|
||||
if ($chapter->chapter_deleted) {
|
||||
if (!validate_level($this->user, 'pr')) {
|
||||
if (!$isValidated) {
|
||||
throw new GoneHttpException(self::CH_STATUS_DELETED);
|
||||
}
|
||||
$normalized['status'] = self::CH_STATUS_DELETED;
|
||||
} else if (!$chapter->available) {
|
||||
if (!validate_level($this->user, 'pr')) {
|
||||
if (!$isValidated) {
|
||||
throw new UnavailableForLegalReasonsHttpException(self::CH_STATUS_UNAVAILABLE);
|
||||
}
|
||||
$normalized['status'] = self::CH_STATUS_UNAVAILABLE;
|
||||
$normalized['groups'] = [];
|
||||
} 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['groupWebsite'] = $chapter->group_website ?: null;
|
||||
} else if ($isExternal) {
|
||||
$normalized['status'] = self::CH_STATUS_EXTERNAL;
|
||||
$normalized['pages'] = $chapter->page_order;
|
||||
} else if (
|
||||
in_array($chapter->manga_id, RESTRICTED_MANGA_IDS) &&
|
||||
!validate_level($this->user, 'contributor') &&
|
||||
$this->user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA
|
||||
) {
|
||||
if (!validate_level($this->user, 'pr')) {
|
||||
} else if ($isRestricted || $isRegionBlocked) {
|
||||
if (!$isValidated) {
|
||||
throw new ForbiddenHttpException(self::CH_STATUS_RESTRICTED);
|
||||
}
|
||||
$normalized = [
|
||||
@ -257,36 +307,20 @@ class ChapterController extends APIController
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
$serverFallback = LOCAL_SERVER_URL;
|
||||
$serverFallback = IMG_SERVER_URL;
|
||||
$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
|
||||
if ($chapter->server > 0) {
|
||||
if ($this->user->md_at_home ?? false) {
|
||||
try {
|
||||
$subsubdomain = $this->mdAtHomeClient->getServerUrl($chapter->chapter_hash, $pages, _IP);
|
||||
if (!empty($subsubdomain)) {
|
||||
$serverNetwork = $subsubdomain;
|
||||
}
|
||||
} catch (\Throwable $t) {
|
||||
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";
|
||||
|
||||
// use md@h for all images
|
||||
try {
|
||||
$subsubdomain = $this->mdAtHomeClient->getServerUrl($chapter->chapter_hash, $pages, _IP, $this->user->mdh_portlimit ?? false);
|
||||
if (!empty($subsubdomain)) {
|
||||
$serverNetwork = $subsubdomain;
|
||||
}
|
||||
} catch (\Throwable $t) {
|
||||
trigger_error($t->getMessage(), E_USER_WARNING);
|
||||
}
|
||||
$server = $serverNetwork ?: $serverFallback;
|
||||
$dataDir = $this->request->query->getBoolean('saver') ? '/data-saver/' : '/data/';
|
||||
|
@ -8,10 +8,9 @@ class FollowsController extends APIController
|
||||
{
|
||||
public function view($path)
|
||||
{
|
||||
/**
|
||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
||||
*/
|
||||
[$id, $subResource, $subResourceId] = $path;
|
||||
$id = $path[0] ?? null;
|
||||
$subResource = $path[1] ?? null;
|
||||
$subResourceId = $path[2] ?? null;
|
||||
|
||||
if (!empty($id)) {
|
||||
throw new NotFoundHttpException();
|
||||
|
@ -8,10 +8,9 @@ class GroupController extends APIController
|
||||
{
|
||||
public function view($path)
|
||||
{
|
||||
/**
|
||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
||||
*/
|
||||
[$id, $subResource, $subResourceId] = $path;
|
||||
$id = $path[0] ?? null;
|
||||
$subResource = $path[1] ?? null;
|
||||
$subResourceId = $path[2] ?? null;
|
||||
|
||||
$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 [
|
||||
"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" => [
|
||||
"GET /" => [
|
||||
"description" => "The current page, the API index.",
|
||||
@ -29,6 +29,7 @@ class IndexController extends APIController
|
||||
"queryParameters" => [
|
||||
"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.",
|
||||
"blockgroups" => "(Optional) Do not include chapters by groups blocked by the user. Boolean, default true.",
|
||||
],
|
||||
],
|
||||
"GET /manga/{id}/covers" => [
|
||||
@ -61,6 +62,7 @@ class IndexController extends APIController
|
||||
"queryParameters" => [
|
||||
"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.",
|
||||
"blockgroups" => "(Optional) Do not include chapters by groups blocked by the user. Boolean, default true.",
|
||||
],
|
||||
],
|
||||
],
|
||||
@ -79,13 +81,18 @@ class IndexController extends APIController
|
||||
"queryParameters" => [
|
||||
"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.",
|
||||
"blockgroups" => "(Optional) Do not include chapters by groups blocked by the user. Boolean, default true.",
|
||||
],
|
||||
],
|
||||
"GET /user/{id}/settings" => [
|
||||
"description" => "(Authorization required) Get a user's website settings.",
|
||||
],
|
||||
"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" => [
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
],
|
||||
],
|
||||
|
@ -8,10 +8,9 @@ class MangaController extends APIController
|
||||
{
|
||||
public function view($path)
|
||||
{
|
||||
/**
|
||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
||||
*/
|
||||
[$id, $subResource, $subResourceId] = $path;
|
||||
$id = $path[0] ?? null;
|
||||
$subResource = $path[1] ?? null;
|
||||
$subResourceId = $path[2] ?? null;
|
||||
|
||||
$id = $this->validateId($id);
|
||||
|
||||
@ -45,20 +44,22 @@ class MangaController extends APIController
|
||||
return $manga;
|
||||
}
|
||||
|
||||
private function normalize($manga)
|
||||
public function normalize($manga)
|
||||
{
|
||||
$coverPath = "/images/manga/$manga->manga_id.$manga->manga_image";
|
||||
|
||||
$normalized = [
|
||||
//'type' => 'manga',
|
||||
'id' => $manga->manga_id,
|
||||
'title' => $manga->manga_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()),
|
||||
'description' => $manga->manga_description,
|
||||
'artist' => explode(',', $manga->manga_artist),
|
||||
'author' => explode(',', $manga->manga_author),
|
||||
'artist' => array_map(function ($a) {
|
||||
return trim($a);
|
||||
}, explode(',', $manga->manga_artist)),
|
||||
'author' => array_map(function ($a) {
|
||||
return trim($a);
|
||||
}, explode(',', $manga->manga_author)),
|
||||
'publication' => [
|
||||
'language' => $manga->lang_flag,
|
||||
'status' => $manga->manga_status_id,
|
||||
@ -74,7 +75,7 @@ class MangaController extends APIController
|
||||
'id' => $relation['related_manga_id'],
|
||||
'title' => $relation['manga_name'],
|
||||
'type' => $relation['relation_id'],
|
||||
'isHentai' => (bool)$relation['hentai'],
|
||||
'isHentai' => (bool)$relation['manga_hentai'],
|
||||
];
|
||||
}, $manga->get_related_manga()),
|
||||
'rating' => [
|
||||
|
@ -8,10 +8,9 @@ class RelationTypeController extends APIController
|
||||
{
|
||||
public function view($path)
|
||||
{
|
||||
/**
|
||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
||||
*/
|
||||
[$id, $subResource, $subResourceId] = $path;
|
||||
$id = $path[0] ?? null;
|
||||
$subResource = $path[1] ?? null;
|
||||
$subResourceId = $path[2] ?? null;
|
||||
|
||||
if (!empty($id)) {
|
||||
throw new NotFoundHttpException();
|
||||
|
@ -8,10 +8,9 @@ class TagController extends APIController
|
||||
{
|
||||
public function view($path)
|
||||
{
|
||||
/**
|
||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
||||
*/
|
||||
[$id, $subResource, $subResourceId] = $path;
|
||||
$id = $path[0] ?? null;
|
||||
$subResource = $path[1] ?? null;
|
||||
$subResourceId = $path[2] ?? null;
|
||||
|
||||
if ($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));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
/**
|
||||
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path
|
||||
*/
|
||||
[$id, $subResource, $subResourceId] = $path;
|
||||
$id = $path[0] ?? null;
|
||||
$subResource = $path[1] ?? null;
|
||||
$subResourceId = $path[2] ?? null;
|
||||
|
||||
$id = $this->validateId($id);
|
||||
|
||||
@ -38,7 +52,8 @@ class UserController extends APIController
|
||||
$this->fetch($id); // check if exists
|
||||
return (new ChapterController())->fetchForUser($id);
|
||||
case 'followed-manga':
|
||||
if (!$this->isAuthorizedUser($id)) {
|
||||
$user = $this->fetch($id);
|
||||
if (!$this->isAuthorizedUserForMangaList($user, 'mod')) {
|
||||
throw new ForbiddenHttpException();
|
||||
}
|
||||
return $this->fetchFollowedManga($id);
|
||||
@ -123,10 +138,13 @@ class UserController extends APIController
|
||||
return [
|
||||
'userId' => $userId,
|
||||
'mangaId' => $data['manga_id'],
|
||||
'mangaTitle' => $data['title'],
|
||||
'isHentai' => (bool) $data['manga_hentai'],
|
||||
'followType' => $data['follow_type'],
|
||||
'volume' => $data['volume'],
|
||||
'chapter' => $data['chapter'],
|
||||
'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);
|
||||
$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);
|
||||
}, $follows);
|
||||
}, $follows));
|
||||
}
|
||||
|
||||
public function fetchFollowedUpdates($id)
|
||||
@ -148,7 +178,7 @@ class UserController extends APIController
|
||||
public function fetchMangaUserData($id, $mangaId)
|
||||
{
|
||||
$userResource = $this->fetch($id);
|
||||
$data = $userResource->get_manga_userdata($mangaId)[0] ?? null;
|
||||
$data = $userResource->get_manga_userdata($mangaId) ?? null;
|
||||
if ($data === null) {
|
||||
throw new NotFoundHttpException("Manga not found.");
|
||||
}
|
||||
@ -171,20 +201,28 @@ class UserController extends APIController
|
||||
public function fetchSettings($id)
|
||||
{
|
||||
$user = $this->fetch($id);
|
||||
$langIds = explode(',', $user->default_lang_ids ?? '');
|
||||
$exludedTags = explode(',', $user->excluded_genres ?? '');
|
||||
return [
|
||||
'id' => $user->user_id,
|
||||
'hentaiMode' => $user->hentai_mode,
|
||||
'latestUpdates' => $user->latest_updates,
|
||||
'showModeratedPosts' => (bool)$user->display_moderated,
|
||||
'showUnavailableChapters' => (bool)$user->show_unavailable,
|
||||
'shownChapterLangs' => explode(',', $user->default_lang_ids ?: ''),
|
||||
'excludedTags' => explode(',', $user->excluded_genres ?: ''),
|
||||
'shownChapterLangs' => array_map(function ($id) {
|
||||
return ['id' => $id];
|
||||
}, $langIds),
|
||||
'excludedTags' => array_map(function ($id) {
|
||||
return ['id' => (int)$id];
|
||||
}, $exludedTags),
|
||||
];
|
||||
}
|
||||
|
||||
public function create($path)
|
||||
{
|
||||
[$id, $subResource, $subResourceId] = $path;
|
||||
$id = $path[0] ?? null;
|
||||
$subResource = $path[1] ?? null;
|
||||
$subResourceId = $path[2] ?? null;
|
||||
|
||||
$id = $this->validateId($id);
|
||||
$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() - ?',
|
||||
[$rememberMeToken, SESSION_REMEMBERME_TIMEOUT], 'fetch', \PDO::FETCH_ASSOC, -1);
|
||||
|
||||
if (!$check) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tokenRegionData = json_decode($check['region_data'], 1);
|
||||
$userRegionData = $this->getClientDetails();
|
||||
|
||||
@ -142,7 +146,7 @@ class Guard
|
||||
$sessionInfo['updated'] = time();
|
||||
$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,
|
||||
$sessionId,
|
||||
time() + SESSION_TIMEOUT,
|
||||
@ -169,7 +173,7 @@ class Guard
|
||||
$sessionInfo['userid']
|
||||
]
|
||||
);
|
||||
$this->memcached->set("user_{$sessionInfo['userid']}_lastseen", 1, 60);
|
||||
$this->memcached->setSynced("user_{$sessionInfo['userid']}_lastseen", 1, 60);
|
||||
}
|
||||
} else {
|
||||
// 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,
|
||||
'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,
|
||||
$sessionId,
|
||||
time() + SESSION_TIMEOUT,
|
||||
@ -249,6 +253,9 @@ class Guard
|
||||
public function verifyUserCredentials($userId, $rawPassword)
|
||||
{
|
||||
$user = $this->getUser($userId);
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
return password_verify($rawPassword, $user->password);
|
||||
}
|
||||
|
||||
|
@ -5,22 +5,46 @@ namespace Mangadex\Model;
|
||||
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';
|
||||
$payload = [
|
||||
'ip' => $ip,
|
||||
'hash' => $chapterHash,
|
||||
'images' => $chapterPages,
|
||||
'only_443' => $onlySsl,
|
||||
];
|
||||
|
||||
$ch = $this->getCurl($path, $ip.$chapterHash.implode($chapterPages), $payload);
|
||||
|
||||
return $this->queryAssign($ch, $payload);
|
||||
}
|
||||
|
||||
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);
|
||||
curl_close($ch);
|
||||
if ($res === false) {
|
||||
throw new \RuntimeException('MD@H::getServerUrl curl error: '.curl_error($ch));
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new \RuntimeException('MD@H::getServerUrl curl error: '.$error);
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
$dec = \json_decode($res, true);
|
||||
if (!$dec) {
|
||||
throw new \RuntimeException('MD@H::getServerUrl failed to decode: '.$res);
|
||||
|
@ -13,7 +13,7 @@
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<?php foreach ($templateVar['stats'] ?? [] AS $dsn => $serverStats) : ?>
|
||||
<?php if (empty($serverStats)) continue; ?>
|
||||
<?php if (empty($serverStats) || empty($serverStats[0] ?? [])) continue; ?>
|
||||
<thead>
|
||||
<tr class="border-top-0">
|
||||
<th colspan="2">Server: <?=$dsn?></th>
|
||||
@ -24,8 +24,8 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($serverStats as $key => $val) : ?>
|
||||
<?php if (!in_array($key, ['Slave_IO_State', 'Slave_IO_Running', 'Seconds_Behind_Master'])) continue; ?>
|
||||
<?php foreach ($serverStats[0] as $key => $val) : ?>
|
||||
<?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>
|
||||
<td><?=$key?></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; ?>
|
@ -164,4 +164,4 @@ $paging = pagination($templateVar['thread_count'], $templateVar['current_page'],
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
|
@ -35,4 +35,4 @@ foreach ($templateVar['categories'] as $cat_id => $category) {
|
||||
<?= ($templateVar['user']->user_id) ? $templateVar['online_users_string'] : "Not viewable by guests." ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
|
||||
<?= parse_template('ads/mobile_app_ad', $templateVar['banners']) ?>
|
||||
<div class="card mb-3">
|
||||
<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">
|
||||
|
@ -1,5 +1,9 @@
|
||||
<?php
|
||||
$env = (defined('DEBUG') && DEBUG) ? 'dev' : 'prod';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
@ -18,29 +22,40 @@
|
||||
<meta property="og:url" content="<?= "https://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]" ?>" />
|
||||
<meta property="og:description" content="<?= $templateVar['og']['description'] ?>" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:site" content="@mangadex" />
|
||||
<meta name="twitter:site" content="@mangadex" />
|
||||
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png?1">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png?1">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png?1">
|
||||
<link rel="manifest" href="/manifest.json" crossOrigin="use-credentials">
|
||||
<link rel="search" type="application/opensearchdescription+xml" title="MangaDex Quick Search" href="/opensearch.xml">
|
||||
<link rel="manifest" href="/manifest.json" crossOrigin="use-credentials">
|
||||
<link rel="search" type="application/opensearchdescription+xml" title="MangaDex Quick Search" href="/opensearch.xml">
|
||||
|
||||
<?= $templateVar['og']['canonical'] ?>
|
||||
<title><?= $templateVar['og']['title'] ?></title>
|
||||
|
||||
<!-- Google Tag Manager -->
|
||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'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>
|
||||
<script>
|
||||
(function(w, d, s, l, i) {
|
||||
w[l] = w[l] || [];
|
||||
w[l].push({
|
||||
'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 -->
|
||||
|
||||
<!-- Google fonts -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Ubuntu:regular,regularitalic,bold" rel="stylesheet">
|
||||
<?php if ($templateVar['page'] == 'drama') { ?>
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:regular,regularitalic,bold" rel="stylesheet">
|
||||
<?php } ?>
|
||||
<?php if ($templateVar['page'] == 'drama') { ?>
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:regular,regularitalic,bold" rel="stylesheet">
|
||||
<?php } ?>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link href="/bootstrap/css/bootstrap.css?<?= @filemtime(ABSPATH . '/bootstrap/css/bootstrap.css') ?>" rel="stylesheet" />
|
||||
@ -50,7 +65,7 @@
|
||||
|
||||
<!-- OWL CSS -->
|
||||
<link href="/scripts/owl/assets/owl.carousel.min.css" rel="stylesheet" />
|
||||
<link href="/scripts/owl/assets/owl.theme.default.min.css" rel="stylesheet" />
|
||||
<link href="/scripts/owl/assets/owl.theme.default.min.css" rel="stylesheet" />
|
||||
|
||||
<!-- Fontawesone glyphicons -->
|
||||
<link href="/fontawesome/css/all.css" rel="stylesheet" />
|
||||
@ -65,20 +80,16 @@
|
||||
<?php if (in_array($templateVar['page'], ['chapter', 'chapter_test']) && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?>
|
||||
<meta name="app" content="MangaDex" data-guest="<?= $templateVar['user']->user_id ? 0 : 1 ?>" data-chapter-id="<?= $_GET['id'] ?? '' ?>" data-page="<?= isset($_GET['p']) ? $_GET['p'] : 1 ?>" />
|
||||
<?php }
|
||||
if (in_array($templateVar['page'], ['chapter']) && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?>
|
||||
if (in_array($templateVar['page'], ['chapter']) && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?>
|
||||
<link href="/scripts/css/reader.css?<?= @filemtime(ABSPATH . "/scripts/css/reader.css") ?>" rel="stylesheet" />
|
||||
<?php } ?>
|
||||
|
||||
<?php if (defined('DEBUG') && DEBUG): ?>
|
||||
<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; ?>
|
||||
<script type="module" src="/dist/js/bundle.<?= $env ?>.js?<?= @filemtime(ABSPATH . "/dist/js/bundle.$env.js") ?>"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-TS59XX9" height="0" width="0" style="display:none; visibility:hidden"></iframe></noscript>
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-TS59XX9" height="0" width="0" style="display:none; visibility:hidden"></iframe></noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
|
||||
<!-- Fixed navbar -->
|
||||
@ -88,27 +99,26 @@
|
||||
<?php if (!$templateVar['user']->activated && $templateVar['user']->user_id)
|
||||
print display_alert("warning", "Warning", "Your account is currently unactivated. Please enter your activation code <a href='/activation'>here</a> for access to all of " . TITLE . "'s features."); ?>
|
||||
|
||||
<?php if (is_array($templateVar['announcement']) && count($templateVar['announcement']) > 0 && !$templateVar['user']->read_announcement && !in_array($templateVar['page'], ['chapter', 'list'])) { ?>
|
||||
<div id="announcement" class="alert alert-success <?= $templateVar['user']->user_id ? 'alert-dismissible ' : ''?>fade show text-center" role="alert">
|
||||
<?php if ($templateVar['user']->user_id) { ?>
|
||||
<button id="read_announcement_button" type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<?php } ?>
|
||||
<?php foreach ($templateVar['announcement'] as $idx=>$row) { ?>
|
||||
<strong>Announcement (<?= date('M-d', $row->timestamp) ?>):</strong> <?= $row->thread_name ?> <a title="Go to forum thread" href="/thread/<?= $row->thread_id ?>"><?= display_fa_icon('external-link-alt', 'Forum thread') ?></a>
|
||||
<?php if($idx < count($templateVar['announcement']) - 1){ ?>
|
||||
<hr style="margin-right: -<?= $templateVar['user']->user_id ? 4 : 1.25 ?>rem; margin-left: -1.25rem;" />
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if (is_array($templateVar['announcement']) && count($templateVar['announcement']) > 0 && !$templateVar['user']->read_announcement && !in_array($templateVar['page'], ['chapter', 'list'])) { ?>
|
||||
<div id="announcement" class="alert alert-success <?= $templateVar['user']->user_id ? 'alert-dismissible ' : '' ?>fade show text-center" role="alert">
|
||||
<?php if ($templateVar['user']->user_id) { ?>
|
||||
<button id="read_announcement_button" type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<?php } ?>
|
||||
<?php foreach ($templateVar['announcement'] as $idx => $row) { ?>
|
||||
<strong>Announcement (<?= date('M-d', $row->timestamp) ?>):</strong> <?= $row->thread_name ?> <a title="Go to forum thread" href="/thread/<?= $row->thread_id ?>"><?= display_fa_icon('external-link-alt', 'Forum thread') ?></a>
|
||||
<?php if ($idx < count($templateVar['announcement']) - 1) { ?>
|
||||
<hr style="margin-right: -<?= $templateVar['user']->user_id ? 4 : 1.25 ?>rem; margin-left: -1.25rem;" />
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
|
||||
<?php
|
||||
/** Print page content */
|
||||
print $templateVar['page_html'];
|
||||
/** Print page content */
|
||||
print $templateVar['page_html'];
|
||||
|
||||
if (validate_level($templateVar['user'], 'admin') && $templateVar['page'] != 'chapter')
|
||||
print_r ($templateVar['sql']->debug());
|
||||
if (validate_level($templateVar['user'], 'admin') && $templateVar['page'] != 'chapter')
|
||||
print_r($templateVar['sql']->debug());
|
||||
?>
|
||||
|
||||
</div> <!-- /container -->
|
||||
@ -122,7 +132,7 @@
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="homepage_settings_label"><?= display_fa_icon('cog')?> MangaDex settings</h5>
|
||||
<h5 class="modal-title" id="homepage_settings_label"><?= display_fa_icon('cog') ?> MangaDex settings</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
@ -160,24 +170,24 @@
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($templateVar['user']->hentai_mode) { ?>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-form-label-modal" for="hentai_mode">Hentai:</label>
|
||||
<div class="col-lg-9">
|
||||
<select class="form-control selectpicker show-tick" id="hentai_mode" name="hentai_mode">
|
||||
<option value="0" <?= (!$templateVar['hentai_toggle']) ? 'selected' : '' ?> data-content="<?= $templateVar['hentai_options'][0] ?>">Hide H</option>
|
||||
<option value="1" <?= ($templateVar['hentai_toggle'] == 1) ? 'selected' : '' ?> data-content="<?= $templateVar['hentai_options'][1] ?>">All</option>
|
||||
<option value="2" <?= ($templateVar['hentai_toggle'] == 2) ? 'selected' : '' ?> data-content="<?= $templateVar['hentai_options'][2] ?>">Only H</option>
|
||||
</select>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-form-label-modal" for="hentai_mode">Hentai:</label>
|
||||
<div class="col-lg-9">
|
||||
<select class="form-control selectpicker show-tick" id="hentai_mode" name="hentai_mode">
|
||||
<option value="0" <?= (!$templateVar['hentai_toggle']) ? 'selected' : '' ?> data-content="<?= $templateVar['hentai_options'][0] ?>">Hide H</option>
|
||||
<option value="1" <?= ($templateVar['hentai_toggle'] == 1) ? 'selected' : '' ?> data-content="<?= $templateVar['hentai_options'][1] ?>">All</option>
|
||||
<option value="2" <?= ($templateVar['hentai_toggle'] == 2) ? 'selected' : '' ?> data-content="<?= $templateVar['hentai_options'][2] ?>">Only H</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div class="form-group row">
|
||||
<div class="col-lg-3 text-right">
|
||||
<button type="submit" class="btn btn-secondary" id="homepage_settings_button"><?= display_fa_icon('save') ?> Save</button>
|
||||
</div>
|
||||
<div class="col-lg-9 text-left">
|
||||
<div class="col-lg-3 text-right">
|
||||
<button type="submit" class="btn btn-secondary" id="homepage_settings_button"><?= display_fa_icon('save') ?> Save</button>
|
||||
</div>
|
||||
<div class="col-lg-9 text-left">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -189,14 +199,14 @@
|
||||
<a class="btn btn-secondary mx-auto" role="button" href="/settings"><?= display_fa_icon('cog') ?> More settings</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= parse_template('partials/report_modal', $templateVar); ?>
|
||||
|
||||
<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>
|
||||
|
||||
<?php
|
||||
@ -209,158 +219,161 @@
|
||||
<!-- Bootstrap core JavaScript
|
||||
================================================== -->
|
||||
<!-- Placed at the end of the document so the pages load faster -->
|
||||
<script nomodule src="/dist/js/polyfills.prod.js?<?= @filemtime(ABSPATH . "/dist/js/polyfills.prod.js") ?>"></script>
|
||||
<script nomodule src="/dist/js/bundle.prod.js?<?= @filemtime(ABSPATH . "/dist/js/bundle.prod.js") ?>"></script>
|
||||
<script src="/scripts/jquery.min.js?<?= @filemtime(ABSPATH . "/scripts/jquery.min.js") ?>"></script>
|
||||
<script nomodule src="/dist/js/polyfills.prod.js?<?= @filemtime(ABSPATH . "/dist/js/polyfills.prod.js") ?>"></script>
|
||||
<script nomodule src="/dist/js/bundle.prod.js?<?= @filemtime(ABSPATH . "/dist/js/bundle.prod.js") ?>"></script>
|
||||
<script src="/scripts/jquery.min.js?<?= @filemtime(ABSPATH . "/scripts/jquery.min.js") ?>"></script>
|
||||
<script src="/scripts/jquery.touchSwipe.min.js"></script>
|
||||
<script src="/bootstrap/js/popper.min.js"></script>
|
||||
<script src="/bootstrap/js/bootstrap.min.js?1"></script>
|
||||
<script src="/bootstrap/js/bootstrap-select.min.js?1"></script>
|
||||
<script src="/scripts/lightbox2/js/lightbox.js"></script>
|
||||
<script src="/scripts/chart.min.js"></script>
|
||||
<?php if ($templateVar['page'] == 'home') { ?>
|
||||
<script src="/scripts/owl/owl.carousel.js"></script>
|
||||
<?php if ($templateVar['page'] == 'home') { ?>
|
||||
<script src="/scripts/owl/owl.carousel.js"></script>
|
||||
<?php } ?>
|
||||
<script>
|
||||
if (!('URL' in window) || !('URLSearchParams' in window)) {
|
||||
document.head.appendChild(Object.assign(document.createElement("script"), {
|
||||
"src": "/dist/js/polyfills.prod.js?<?= @filemtime(ABSPATH . "/dist/js/polyfills.prod.js") ?>", "async": true,
|
||||
}))
|
||||
}
|
||||
</script>
|
||||
<?php if (in_array($templateVar['page'], ['chapter', 'chapter_test']) && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?>
|
||||
<script>
|
||||
if (!('URL' in window) || !('URLSearchParams' in window)) {
|
||||
document.head.appendChild(Object.assign(document.createElement("script"), {
|
||||
"src": "/dist/js/polyfills.prod.js?<?= @filemtime(ABSPATH . "/dist/js/polyfills.prod.js") ?>",
|
||||
"async": true,
|
||||
}))
|
||||
}
|
||||
</script>
|
||||
<?php if ($templateVar['page'] == 'chapter' && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?>
|
||||
<script src="/scripts/modernizr-custom.js"></script>
|
||||
<?php }
|
||||
if ($templateVar['page'] == 'chapter' && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?>
|
||||
<script async src="/scripts/reader.min.js?<?= @filemtime(ABSPATH . "/scripts/reader.min.js") ?>"></script>
|
||||
<script async src="/dist/js/reader.<?= $env ?>.js?<?= @filemtime(ABSPATH . "/dist/js/reader.$env.js") ?>"></script>
|
||||
<?php if ($env !== 'prod') { ?>
|
||||
<script nomodule src="/dist/js/reader.prod.js?<?= @filemtime(ABSPATH . "/dist/js/reader.prod.js") ?>"></script>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
|
||||
<script src="/scripts/js/reporting.js"></script>
|
||||
<script src="/scripts/js/reporting.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
<?php if (defined('INCLUDE_JS_REDIRECT') && INCLUDE_JS_REDIRECT) : ?>
|
||||
var t='mang';
|
||||
t = t+'adex.org';
|
||||
var w='www.mangadex.org';
|
||||
if (window.location.hostname != t && window.location.hostname != w ) {
|
||||
window.location='https://'+t;
|
||||
}
|
||||
<?php endif; ?>
|
||||
|
||||
var $ = jQuery;
|
||||
|
||||
$(document).on('change', '.btn-file :file', function() {
|
||||
var input = $(this),
|
||||
numFiles = input.get(0).files ? input.get(0).files.length : 1,
|
||||
label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
|
||||
input.trigger('fileselect', [numFiles, label]);
|
||||
});
|
||||
|
||||
function capitalizeFirstLetter(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
function commaMultipleSelect(id) {
|
||||
var list = document.getElementById(id);
|
||||
var selected = new Array();
|
||||
|
||||
for (i = 0; i < list.options.length; i++) {
|
||||
if (list.options[i].selected) {
|
||||
selected.push(list.options[i].value);
|
||||
<?php if (defined('INCLUDE_JS_REDIRECT') && INCLUDE_JS_REDIRECT) : ?>
|
||||
var t = 'mang';
|
||||
t = t + 'adex.org';
|
||||
var w = 'www.mangadex.org';
|
||||
if (window.location.hostname != t && window.location.hostname != w) {
|
||||
window.location = 'https://' + t;
|
||||
}
|
||||
}
|
||||
|
||||
return selected.join(',');
|
||||
}
|
||||
|
||||
function commaMultipleCheckbox(name) {
|
||||
var list = document.getElementsByName(name);
|
||||
var selected = new Array();
|
||||
|
||||
for (i = 0; i < list.length; i++) {
|
||||
if (list[i].checked) {
|
||||
selected.push(list[i].value);
|
||||
}
|
||||
}
|
||||
|
||||
return selected.join(',');
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
var query = location.search;
|
||||
|
||||
<?php if (!isset($templateVar['user']->navigation) || $templateVar['user']->navigation) : ?>
|
||||
$("#left_swipe_area").swipe({
|
||||
swipeRight:function(event, direction, distance, duration, fingerCount) {
|
||||
$('#left_modal').modal('toggle');
|
||||
},
|
||||
threshold: 50
|
||||
});
|
||||
$("#right_swipe_area").swipe({
|
||||
swipeLeft:function(event, direction, distance, duration, fingerCount) {
|
||||
$('#right_modal').modal('toggle');
|
||||
},
|
||||
threshold: 50
|
||||
});
|
||||
$("#right_modal").swipe({
|
||||
swipeRight:function(event, direction, distance, duration, fingerCount) {
|
||||
$('#right_modal').modal('toggle');
|
||||
},
|
||||
threshold: 50
|
||||
});
|
||||
$("#left_modal").swipe({
|
||||
swipeLeft:function(event, direction, distance, duration, fingerCount) {
|
||||
$('#left_modal').modal('toggle');
|
||||
},
|
||||
threshold: 50
|
||||
});
|
||||
<?php endif; ?>
|
||||
|
||||
$("#read_announcement_button").click(function(event){
|
||||
$.ajax({
|
||||
url: "/ajax/actions.ajax.php?function=read_announcement",
|
||||
type: 'GET',
|
||||
success: function (data) {
|
||||
$("#announcement").hide();
|
||||
},
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false
|
||||
});
|
||||
event.preventDefault();
|
||||
var $ = jQuery;
|
||||
|
||||
$(document).on('change', '.btn-file :file', function() {
|
||||
var input = $(this),
|
||||
numFiles = input.get(0).files ? input.get(0).files.length : 1,
|
||||
label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
|
||||
input.trigger('fileselect', [numFiles, label]);
|
||||
});
|
||||
|
||||
$(".logout").click(function(event){
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/ajax/actions.ajax.php?function=logout",
|
||||
success: function(data) {
|
||||
$("#message_container").html(data).show().delay(1500).fadeOut();
|
||||
location.reload();
|
||||
function capitalizeFirstLetter(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
function commaMultipleSelect(id) {
|
||||
var list = document.getElementById(id);
|
||||
var selected = new Array();
|
||||
|
||||
for (i = 0; i < list.options.length; i++) {
|
||||
if (list.options[i].selected) {
|
||||
selected.push(list.options[i].value);
|
||||
}
|
||||
}
|
||||
|
||||
return selected.join(',');
|
||||
}
|
||||
|
||||
function commaMultipleCheckbox(name) {
|
||||
var list = document.getElementsByName(name);
|
||||
var selected = new Array();
|
||||
|
||||
for (i = 0; i < list.length; i++) {
|
||||
if (list[i].checked) {
|
||||
selected.push(list[i].value);
|
||||
}
|
||||
}
|
||||
|
||||
return selected.join(',');
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var query = location.search;
|
||||
|
||||
<?php if (!isset($templateVar['user']->navigation) || $templateVar['user']->navigation) : ?>
|
||||
$("#left_swipe_area").swipe({
|
||||
swipeRight: function(event, direction, distance, duration, fingerCount) {
|
||||
$('#left_modal').modal('toggle');
|
||||
},
|
||||
threshold: 50
|
||||
});
|
||||
$("#right_swipe_area").swipe({
|
||||
swipeLeft: function(event, direction, distance, duration, fingerCount) {
|
||||
$('#right_modal').modal('toggle');
|
||||
},
|
||||
threshold: 50
|
||||
});
|
||||
$("#right_modal").swipe({
|
||||
swipeRight: function(event, direction, distance, duration, fingerCount) {
|
||||
$('#right_modal').modal('toggle');
|
||||
},
|
||||
threshold: 50
|
||||
});
|
||||
$("#left_modal").swipe({
|
||||
swipeLeft: function(event, direction, distance, duration, fingerCount) {
|
||||
$('#left_modal').modal('toggle');
|
||||
},
|
||||
threshold: 50
|
||||
});
|
||||
<?php endif; ?>
|
||||
|
||||
$("#read_announcement_button").click(function(event) {
|
||||
$.ajax({
|
||||
url: "/ajax/actions.ajax.php?function=read_announcement",
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
$("#announcement").hide();
|
||||
},
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false
|
||||
});
|
||||
event.preventDefault();
|
||||
});
|
||||
event.preventDefault();
|
||||
|
||||
$(".logout").click(function(event) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/ajax/actions.ajax.php?function=logout",
|
||||
success: function(data) {
|
||||
$("#message_container").html(data).show().delay(1500).fadeOut();
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
function highlightPost(node) {
|
||||
if (node) {
|
||||
Array.from(document.querySelectorAll('.highlighted')).forEach(function(n) {
|
||||
n.classList.remove('highlighted')
|
||||
});
|
||||
node.classList.add('highlighted')
|
||||
}
|
||||
}
|
||||
|
||||
if (location.hash)
|
||||
highlightPost(document.querySelector(location.hash + ' .postbody'));
|
||||
|
||||
$('a.permalink').click(function(event) {
|
||||
highlightPost(event.target.closest('.post').querySelector('.postbody'))
|
||||
});
|
||||
|
||||
<?= $templateVar['page_scripts'] ?>
|
||||
|
||||
|
||||
});
|
||||
|
||||
function highlightPost(node) {
|
||||
if (node) {
|
||||
Array.from(document.querySelectorAll('.highlighted')).forEach(function(n) {n.classList.remove('highlighted')});
|
||||
node.classList.add('highlighted')
|
||||
}
|
||||
}
|
||||
|
||||
if (location.hash)
|
||||
highlightPost(document.querySelector(location.hash + ' .postbody'));
|
||||
|
||||
$('a.permalink').click(function(event) {
|
||||
highlightPost(event.target.closest('.post').querySelector('.postbody'))
|
||||
});
|
||||
|
||||
<?= $templateVar['page_scripts'] ?>
|
||||
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -29,7 +29,7 @@
|
||||
|
||||
switch ($templateVar['user']->reader_mode) {
|
||||
case 0:
|
||||
?>
|
||||
?>
|
||||
|
||||
<img id="current_page" class="reader <?= ($templateVar['user']->image_fit == 2) ? 'max-height' : 'max-width' ?>" src="<?= $templateVar['server'] ?><?= $templateVar['chapter']->chapter_hash ?>/<?= $templateVar['page_array'][$templateVar['page']] ?? '' ?>" alt="image" data-page="<?= $templateVar['page'] ?>" />
|
||||
|
||||
@ -39,15 +39,13 @@
|
||||
case 2: //long-strip
|
||||
foreach ($templateVar['page_array'] as $key => $x) {
|
||||
if (!$templateVar['chapter']->server && $templateVar['chapter']->chapter_id == 256885 && in_array($key, [1])) {
|
||||
?>
|
||||
?>
|
||||
<img class="long-strip <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="/img.php?x=/data/<?= "{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
|
||||
<?php
|
||||
}
|
||||
|
||||
else {
|
||||
?>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<img class="long-strip <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="<?= "{$templateVar['server']}{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
|
||||
<?php
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,17 +54,15 @@
|
||||
case 3: //webtoon
|
||||
foreach ($templateVar['page_array'] as $key => $x) {
|
||||
if (!$templateVar['chapter']->server && $templateVar['chapter']->chapter_id == 256885 && in_array($key, [1])) {
|
||||
?>
|
||||
?>
|
||||
<img class="webtoon <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="/img.php?x=/data/<?= "{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
|
||||
<?php
|
||||
}
|
||||
|
||||
else {
|
||||
?>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
|
||||
<img class="webtoon <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="<?= "{$templateVar['server']}{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
|
||||
|
||||
<?php
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +71,7 @@
|
||||
default:
|
||||
?>
|
||||
<img id="current_page" class="reader <?= ($templateVar['user']->image_fit == 2) ? 'max-height' : 'max-width' ?>" src="<?= $templateVar['server'] ?><?= $templateVar['chapter']->chapter_hash ?>/<?= $templateVar['page_array'][$templateVar['page']] ?>" alt="image" data-page="<?= $templateVar['page'] ?>" />
|
||||
<?php
|
||||
<?php
|
||||
break;
|
||||
}
|
||||
?>
|
||||
@ -111,7 +107,7 @@
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="report_chapter_label"><?= display_fa_icon('cog')?> Report chapter</h5>
|
||||
<h5 class="modal-title" id="report_chapter_label"><?= display_fa_icon('cog') ?> Report chapter</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
@ -123,9 +119,11 @@
|
||||
<div class="col-md-9">
|
||||
<select required title="Select a reason" class="form-control selectpicker" id="type_id" name="type_id">
|
||||
<?php
|
||||
$chapter_reasons = array_filter($templateVar['report_reasons'], function($reason) { return REPORT_TYPES[$reason['type_id']] === 'Chapter'; });
|
||||
foreach ($chapter_reasons as $reason): ?>
|
||||
<option value="<?= $reason['id'] ?>"><?= $reason['text'] ?><?= $reason['is_info_required'] ? ' *' : '' ?></option>
|
||||
$chapter_reasons = array_filter($templateVar['report_reasons'], function ($reason) {
|
||||
return REPORT_TYPES[$reason['type_id']] === 'Chapter';
|
||||
});
|
||||
foreach ($chapter_reasons as $reason) : ?>
|
||||
<option value="<?= $reason['id'] ?>"><?= $reason['text'] ?><?= $reason['is_info_required'] ? ' *' : '' ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
@ -133,7 +131,7 @@
|
||||
<div class="form-group row">
|
||||
<label for="chapter_name" class="col-md-3 col-form-label">Explanation</label>
|
||||
<div class="col-md-9">
|
||||
<textarea class="form-control" id="info" name="info" placeholder="Optional" ></textarea>
|
||||
<textarea class="form-control" id="info" name="info" placeholder="Optional"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
@ -150,7 +148,7 @@
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="legacy_reader_settings_label"><?= display_fa_icon('cog')?> Legacy reader settings</h5>
|
||||
<h5 class="modal-title" id="legacy_reader_settings_label"><?= display_fa_icon('cog') ?> Legacy reader settings</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
@ -176,6 +174,15 @@
|
||||
</select>
|
||||
</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">
|
||||
<label for="reader_mode" class="col-md-3 col-form-label">Reader mode:</label>
|
||||
<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-9 col-xl-10">
|
||||
<?= display_lock_manga($templateVar['user'], $templateVar['manga']) ?>
|
||||
<?= display_regenerate_manga_thumb($templateVar['user']) ?>
|
||||
<?= display_delete_manga($templateVar['user']) ?>
|
||||
</div>
|
||||
</div>
|
||||
@ -422,4 +423,4 @@ $links_array = ($templateVar['manga']->manga_links) ? json_decode($templateVar['
|
||||
|
||||
</div>
|
||||
|
||||
<?= $templateVar['post_history_modal_html'] ?>
|
||||
<?= $templateVar['post_history_modal_html'] ?>
|
||||
|
@ -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'] == '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')) : ?>
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
<?php if (validate_level($templateVar['user'], 'admin')) : ?>
|
||||
@ -17,4 +16,4 @@
|
||||
|
||||
<?= $templateVar['tab_html'] ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,19 +1,18 @@
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= display_fa_icon('hashtag') ?></th>
|
||||
<th class="text-center"><?= display_fa_icon('hashtag') ?></th>
|
||||
<th><?= display_fa_icon('user') ?></th>
|
||||
<th>IP</th>
|
||||
<th><?= display_fa_icon('globe-asia') ?></th>
|
||||
<th><?= display_fa_icon('globe') ?></th>
|
||||
<th><?= display_fa_icon('bolt') ?></th>
|
||||
<th><?= display_fa_icon('upload', 'Mbps') ?></th>
|
||||
<th><?= display_fa_icon('download', 'Mbps') ?></th>
|
||||
<th><?= display_fa_icon('hdd', 'GB', '', 'far') ?></th>
|
||||
<th class="text-center"><?= display_fa_icon('network-wired', 'Test') ?></th>
|
||||
<th class="text-center"><?= display_fa_icon('globe-asia') ?></th>
|
||||
<th class="text-center"><?= display_fa_icon('globe') ?></th>
|
||||
<th class="text-center"><?= display_fa_icon('bolt') ?></th>
|
||||
<th class="text-center"><?= display_fa_icon('upload', 'Mbps') ?></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('key') ?></th>
|
||||
<th>Data transferred (GB)</th>
|
||||
<th>Daily average (GB)</th>
|
||||
<th><?= display_fa_icon('check') ?><?= display_fa_icon('times') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -23,7 +22,8 @@
|
||||
<td>#<?= $client_id ?></td>
|
||||
<td><?= display_user_link($client->user_id, $client->username, $client->level_colour) ?></td>
|
||||
<td><?= $client->client_ip ?></td>
|
||||
<td><?= $client->client_continent ?></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><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><?= $client->upload_speed ?></td>
|
||||
@ -31,8 +31,6 @@
|
||||
<td><?= $client->disk_cache_size ?></td>
|
||||
<td><?= $client->timestamp ? date('Y-m-d H:i:s', $client->timestamp) . ' UTC' : '' ?></td>
|
||||
<td><code><?= $client->client_secret ?></code></td>
|
||||
<td>0</td>
|
||||
<td>0</td>
|
||||
<td>
|
||||
<?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>
|
||||
@ -60,14 +58,13 @@
|
||||
<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>
|