commit
8ccc1db550
@ -0,0 +1,2 @@
|
||||
DATABASE_URL=mongodb://mongoadmin:something@192.168.71.137:27017
|
||||
PORT=5010
|
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
Saved
|
@ -0,0 +1,260 @@
|
||||
const {parse} = require('node-html-parser');
|
||||
const {decodeHTML} = require('../lib')
|
||||
module.exports = class AsuraModule
|
||||
{
|
||||
/**
|
||||
* @property {Scanlator} scanlator - Scanlator name
|
||||
* @typedef {String} Scanlator
|
||||
* @pattern /^[\w-]+-scans$/
|
||||
*/
|
||||
constructor()
|
||||
{
|
||||
this.scanlator = 'Asura-scans'
|
||||
this.BaseLink = 'https://asuratoon.com/'
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {String} query
|
||||
* @returns {Array<Manga>}
|
||||
* @typedef {Object} Manga
|
||||
* @property {Link} link - Manga Link
|
||||
* @property {String} title - Manga Title
|
||||
* @property {Link} img - Image Link
|
||||
* @property {Number} latestChap - Latest Chapter Number
|
||||
* @typedef {String} Link
|
||||
*/
|
||||
async Search(query)
|
||||
{
|
||||
return await fetch(this.BaseLink+'?s='+query)
|
||||
.then(handleResponse)
|
||||
.then(handleData)
|
||||
.catch(handleError);
|
||||
function handleResponse(response)
|
||||
{
|
||||
return response.text();
|
||||
}
|
||||
function handleError(error)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
function handleData(data)
|
||||
{
|
||||
data = data.split('<div class="listupd">')[1].split('<div class="pagination">')[0]
|
||||
const document = parse(data);
|
||||
var payload = [];
|
||||
for(var result of document.querySelectorAll('.bs > .bsx > a'))
|
||||
{
|
||||
var aux = {
|
||||
link:result.rawAttrs.split('"')[1],
|
||||
title:result.rawAttrs.split('"')[3],
|
||||
img:'https://'+result.querySelectorAll('img')[0].rawAttrs.split('"')[1].split('https://')[2],
|
||||
status: result.querySelector('.status')?.rawText || 'Ongoing',
|
||||
latestChap:parseInt(result.querySelector('.epxs').innerText.split(' ')[1]),
|
||||
};
|
||||
payload.push(aux);
|
||||
}
|
||||
return payload
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {String} query
|
||||
* @returns {Array<Manga>}
|
||||
* @typedef {Object} Manga
|
||||
* @property {Link} link - Manga Link
|
||||
* @property {String} title - Manga Title
|
||||
* @property {Link} img - Image Link
|
||||
* @property {Number} latestChap - Latest Chapter Number
|
||||
* @typedef {String} Link
|
||||
*/
|
||||
async SearchByTag(query, ammount=5)
|
||||
{
|
||||
return await fetch(this.BaseLink+'/genres/'+query)
|
||||
.then(handleResponse)
|
||||
.then(handleData)
|
||||
.catch(handleError);
|
||||
function handleResponse(response)
|
||||
{
|
||||
return response.text();
|
||||
}
|
||||
function handleError(error)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
function handleData(data)
|
||||
{
|
||||
data = data.split('<div class="listupd">')[1].split('<div class="pagination">')[0]
|
||||
const document = parse(data);
|
||||
var payload = [];
|
||||
var results = document.querySelectorAll('a')
|
||||
var limit = results.length<ammount?results.length:ammount;
|
||||
for(var i =0;i<limit; i++)
|
||||
{
|
||||
const result = results[i];
|
||||
var aux = {
|
||||
link:result.rawAttrs.split('"')[1],
|
||||
title:decodeHTML(result.rawAttrs.split('"')[3]),
|
||||
img:result.querySelectorAll('.limit > img')[0].rawAttrs.split('src="')[1].split(')/')[1].split('"')[0],
|
||||
status:result.querySelector('.status')?.rawText||'Ongoing',
|
||||
latestChap:parseInt(result.querySelectorAll('.epxs')[0].innerText.split(' ')[1]),
|
||||
};
|
||||
payload.push(aux);
|
||||
}
|
||||
return payload
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} name Manga/Manwha Name
|
||||
* @param {Link} link Manga url (
|
||||
* @returns {Array<Chapter>} - Chapters Array
|
||||
* @typedef {Object} Chapter
|
||||
* @property {Number} num - Chapter Number
|
||||
* @property {String} link - Chapter Link
|
||||
* @property {String} date - Chapter Release Date
|
||||
* @typedef {String} Link
|
||||
* @pattern ^(https?:\/\/)?([\w\d.-]+)\.([a-z]{2,})(:[0-9]+)?(\/\S*)?$
|
||||
*/
|
||||
async ChapterList(link, name)
|
||||
{
|
||||
return await fetch(link)
|
||||
.then(handleResponse)
|
||||
.then(handleData)
|
||||
.catch(handleError);
|
||||
function handleResponse(response)
|
||||
{
|
||||
return response.text();
|
||||
}
|
||||
function handleError(error)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
function handleData(data)
|
||||
{
|
||||
|
||||
const document = parse(data);
|
||||
const chapterlist = document.querySelectorAll('ul')[1];
|
||||
const chapters = chapterlist.querySelectorAll('li');
|
||||
var payload = [];
|
||||
try {
|
||||
for(var chapter of chapters)
|
||||
{
|
||||
var aux = {
|
||||
num:parseInt(chapter.rawAttrs.split('"')[1]),
|
||||
link:chapter.querySelector('.eph-num').innerHTML.split('"')[1],
|
||||
date:chapter.querySelector('.chapterdate').innerText
|
||||
|
||||
};
|
||||
payload.push(aux);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('This manga should be called by url', error);
|
||||
}
|
||||
return payload
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Link} Link Chapter Link
|
||||
* @returns {ChapterList}
|
||||
* @typedef {Obejct} ChapterList
|
||||
* @property {Array<Image>} List
|
||||
* @property {Link} mangaLink - Manga Link
|
||||
* @typedef {String} Image - Image link
|
||||
* @typedef {String} Link
|
||||
* @pattern ^(https?:\/\/)?([\w\d.-]+)\.([a-z]{2,})(:[0-9]+)?(\/\S*)?$
|
||||
|
||||
*/
|
||||
async Chapter(Link)
|
||||
{
|
||||
return await fetch(Link)
|
||||
.then(handleResponse)
|
||||
.then(handleData)
|
||||
.catch(handleError);
|
||||
function handleResponse(response)
|
||||
{
|
||||
return response.text();
|
||||
}
|
||||
function handleError(error)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
function handleData(data)
|
||||
{
|
||||
const document = parse(data);
|
||||
let List = [];
|
||||
let mangaLink = document.querySelectorAll('.allc')[0].querySelector('a')._rawAttrs.href;
|
||||
for(var image of document.getElementById('readerarea').querySelectorAll('img'))
|
||||
{
|
||||
if(image.rawAttrs)
|
||||
{
|
||||
let index = image.rawAttrs.split('"').findIndex(el => el.includes('http'));
|
||||
List.push(image.rawAttrs.split('"')[index].replace(/\n/g, ''));
|
||||
}
|
||||
}
|
||||
return {List, mangaLink}
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {String} Title
|
||||
* @returns {Manga}
|
||||
* @typedef {Object} Manga
|
||||
* @property {Link} Link - Manga Link
|
||||
* @property {Link} img - Image Link
|
||||
* @property {String} description - Latest Chapter Number
|
||||
* @property {String} Type - Manga Type
|
||||
* @property {String} Released - Year of Release
|
||||
* @property {String} Author - Author's name
|
||||
* @property {String} Artist - Artist Team
|
||||
* @property {String} Serialization - Serialization Company
|
||||
* @property {Number} latestChap - Latest Chap number
|
||||
* @property {Array<String>} tags - Manga Tags
|
||||
* @typedef {String} Link
|
||||
*/
|
||||
async GetManga(Link, title)
|
||||
{
|
||||
const auxTitle = title;
|
||||
return await fetch(Link)
|
||||
.then(handleResponse)
|
||||
.then(handleData)
|
||||
.catch(handleError);
|
||||
function handleResponse(response)
|
||||
{
|
||||
return response.text();
|
||||
}
|
||||
function handleError(error)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
function handleData(data)
|
||||
{
|
||||
|
||||
const parsed = parse(data)
|
||||
let description = [];
|
||||
parsed.querySelectorAll('.entry-content-single')[0].querySelectorAll('p').forEach(p=>description.push(p.innerText));
|
||||
description = decodeHTML(description.join(' '));
|
||||
const img = 'https://'+parsed.querySelectorAll('img')[1].rawAttrs.split('src="')[1].split('https://')[2].split('"')[0];
|
||||
let tags = parsed.querySelectorAll('.mgen')[0].structuredText.split('\n')[0].split(' ');
|
||||
let latestChap = parseInt(parsed.querySelectorAll('.epcurlast')[0].rawText.split(' ')[1])
|
||||
let table = parsed.querySelectorAll('.infox')[0].querySelectorAll('.fmed');
|
||||
table.splice(-3);
|
||||
const aux = {};
|
||||
for (let i = 0; i < table.length; i++) {
|
||||
const key = decodeHTML(table[i].querySelector('b').innerText);
|
||||
aux[key] = decodeHTML(table[i].querySelector('span').innerText);
|
||||
}
|
||||
aux['Status'] = parsed.querySelectorAll('.imptdt')[0].innerText.split('\n')[1].split(' ')[1];
|
||||
aux['Type'] = parsed.querySelectorAll('.imptdt')[1].innerText.split('\n')[1].split(' ')[1];
|
||||
return {description, img, ...aux, tags, title:auxTitle, latestChap}
|
||||
|
||||
}
|
||||
}
|
||||
async hasTitle(title)
|
||||
{
|
||||
const Search = await this.Search(title);
|
||||
if(!Array.isArray(Search)) return false
|
||||
return Search.filter(Manga => {return Manga.title === title})[0];
|
||||
}
|
||||
}
|
@ -0,0 +1,267 @@
|
||||
const {parse} = require('node-html-parser');
|
||||
const {decodeHTML} = require('../lib')
|
||||
module.exports = class ReaperModule
|
||||
{
|
||||
|
||||
/**
|
||||
* @property {Scanlator} scanlator - Scanlator name
|
||||
* @typedef {String} Scanlator
|
||||
* @pattern /^[\w-]+-scans$/
|
||||
*/
|
||||
constructor()
|
||||
{
|
||||
this.scanlator = 'Reaper-scans';
|
||||
this.BaseLink = 'https://reaper-scans.com/';
|
||||
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {String} query
|
||||
* @returns {Array<Manga>}
|
||||
* @typedef {Object} Manga
|
||||
* @property {Link} link - Manga Link
|
||||
* @property {String} title - Manga Title
|
||||
* @property {String} Status - Manga Status
|
||||
* @property {Link} img - Image Link
|
||||
* @property {Number} latestChap - Latest Chapter Number
|
||||
* @typedef {String} Link
|
||||
*/
|
||||
async Search(query)
|
||||
{
|
||||
return await fetch(this.BaseLink+'?s='+query)
|
||||
.then(handleResponse)
|
||||
.then(handleData)
|
||||
.catch(handleError);
|
||||
function handleResponse(response)
|
||||
{
|
||||
return response.text();
|
||||
}
|
||||
function handleError(error)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
function handleData(data)
|
||||
{
|
||||
data = data.split('<div class="listupd">')[1].split('<div class="pagination">')[0]
|
||||
const document = parse(data);
|
||||
let payload = [];
|
||||
for(var result of document.querySelectorAll('a'))
|
||||
{
|
||||
let aux = {
|
||||
link:result.rawAttrs.split('"')[1],
|
||||
title:result.rawAttrs.split('"')[3],
|
||||
img:result.querySelectorAll('.limit > img')[0].rawAttrs.split('"')[1],
|
||||
status:result.querySelector('.status')?.rawText||'Ongoing',
|
||||
latestChap:parseInt(result.childNodes[1].childNodes[1].childNodes[0].rawText.split(" ")[1]),
|
||||
};
|
||||
payload.push(aux);
|
||||
}
|
||||
return payload
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {String} query
|
||||
* @returns {Array<Manga>}
|
||||
* @typedef {Object} Manga
|
||||
* @property {Link} link - Manga Link
|
||||
* @property {String} title - Manga Title
|
||||
* @property {Link} img - Image Link
|
||||
* @property {Number} latestChap - Latest Chapter Number
|
||||
* @typedef {String} Link
|
||||
*/
|
||||
async SearchByTag(query, ammount=5)
|
||||
{
|
||||
return await fetch(this.BaseLink+'genres/'+query)
|
||||
.then(handleResponse)
|
||||
.then(handleData)
|
||||
.catch(handleError);
|
||||
function handleResponse(response)
|
||||
{
|
||||
return response.text();
|
||||
}
|
||||
function handleError(error)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
function handleData(data)
|
||||
{
|
||||
data = data.split('<div class="listupd">')[1].split('<div class="pagination">')[0]
|
||||
const document = parse(data);
|
||||
var payload = [];
|
||||
var results = document.querySelectorAll('a')
|
||||
var limit = results.length<ammount?results.length:ammount;
|
||||
for(var i =0;i<limit; i++)
|
||||
{
|
||||
const result = results[i];
|
||||
var aux = {
|
||||
link:result.rawAttrs.split('"')[1],
|
||||
title:decodeHTML(result.rawAttrs.split('"')[3]),
|
||||
img:result.querySelectorAll('.limit > img')[0].rawAttrs.split('data-src="')[1].split('"')[0],
|
||||
status:result.querySelector('.status')?.rawText||'Ongoing',
|
||||
latestChap:parseInt(result.childNodes[1].childNodes[1].childNodes[0].rawText.split(" ")[1]),
|
||||
};
|
||||
payload.push(aux);
|
||||
}
|
||||
return payload
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} name Manga/Manwha Name
|
||||
* @param {Link} link Manga url (
|
||||
* @returns {Array<Chapter>} - Chapters Array
|
||||
* @typedef {Object} Chapter
|
||||
* @property {Number} num - Chapter Number
|
||||
* @property {String} link - Chapter Link
|
||||
* @property {String} date - Chapter Release Date
|
||||
* @typedef {String} Link
|
||||
* @pattern ^(https?:\/\/)?([\w\d.-]+)\.([a-z]{2,})(:[0-9]+)?(\/\S*)?$
|
||||
*/
|
||||
async ChapterList(link, name)
|
||||
{
|
||||
return await fetch(link)
|
||||
.then(handleResponse)
|
||||
.then(handleData)
|
||||
.catch(handleError);
|
||||
function handleResponse(response)
|
||||
{
|
||||
return response.text();
|
||||
}
|
||||
function handleError(error)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
function handleData(data)
|
||||
{
|
||||
|
||||
const document = parse(data);
|
||||
const chapterlist = document.querySelectorAll('ul')[2];
|
||||
const chapters = parse(chapterlist).querySelectorAll('li');
|
||||
var payload = [];
|
||||
try {
|
||||
for(var chapter of chapters)
|
||||
{
|
||||
var aux = {
|
||||
num:parseInt(chapter.rawAttrs.split('"')[1]),
|
||||
link:chapter.childNodes[0].childNodes[0].childNodes[1].rawAttrs.split('"')[1],
|
||||
date:chapter.childNodes[0].childNodes[0].childNodes[1].childNodes[3].rawText,
|
||||
|
||||
};
|
||||
payload.push(aux);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('This manga should be called by url', error);
|
||||
}
|
||||
return payload
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Link} Link Chapter Link
|
||||
* @returns {ChapterList}
|
||||
* @typedef {Obejct} ChapterList
|
||||
* @property {Array<Image>} List
|
||||
* @property {Link} mangaLink - Manga Link
|
||||
* @typedef {String} Image - Image link
|
||||
* @typedef {String} Link
|
||||
* @pattern ^(https?:\/\/)?([\w\d.-]+)\.([a-z]{2,})(:[0-9]+)?(\/\S*)?$
|
||||
|
||||
*/
|
||||
async Chapter(Link)
|
||||
{
|
||||
return await fetch(Link)
|
||||
.then(handleResponse)
|
||||
.then(handleData)
|
||||
.catch(handleError);
|
||||
function handleResponse(response)
|
||||
{
|
||||
return response.text();
|
||||
}
|
||||
function handleError(error)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
function handleData(data)
|
||||
{
|
||||
const document = parse(data);
|
||||
var List = [];
|
||||
let mangaLink = document.querySelectorAll('.allc')[0].querySelector('a')._rawAttrs.href;
|
||||
for(var image of document.querySelectorAll('p')[1].childNodes)
|
||||
{
|
||||
if(image.rawAttrs)
|
||||
{
|
||||
let index = image.rawAttrs.split('"').findIndex(el => el.includes('http'));
|
||||
List.push(image.rawAttrs.split('"')[index].replace(/\n/g, ''));
|
||||
}
|
||||
}
|
||||
return {List, mangaLink}
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {String} Title
|
||||
* @returns {Manga}
|
||||
* @typedef {Object} Manga
|
||||
* @property {Link} Link - Manga Link
|
||||
* @property {Link} img - Image Link
|
||||
* @property {String} description - Latest Chapter Number
|
||||
* @property {String} Type - Manga Type
|
||||
* @property {String} Released - Year of Release
|
||||
* @property {String} Author - Author's name
|
||||
* @property {String} Artist - Artist Team
|
||||
* @property {String} Serialization - Serialization Company
|
||||
* @property {Number} latestChap - Latest Chap number
|
||||
* @property {Array<String>} tags - Manga Tags
|
||||
* @typedef {String} Link
|
||||
*/
|
||||
async GetManga(Link, title)
|
||||
{
|
||||
const auxTitle = title;
|
||||
return await fetch(Link)
|
||||
.then(handleResponse)
|
||||
.then(handleData)
|
||||
.catch(handleError);
|
||||
function handleResponse(response)
|
||||
{
|
||||
return response.text();
|
||||
}
|
||||
function handleError(error)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
function handleData(data)
|
||||
{
|
||||
|
||||
const parsed = parse(data)
|
||||
let description = parsed.querySelectorAll('.entry-content-single')[0].innerText
|
||||
description = decodeHTML(description);
|
||||
if(description.split('/>').length>1) description = description.split('/>')[1]
|
||||
const img = parsed.querySelectorAll('img')[1].rawAttrs.split('src="')[1].split('"')[0].includes('data:')? parsed.querySelectorAll('img')[1].rawAttrs.split('data-src="')[1].split('"')[0]:parsed.querySelectorAll('img')[1].rawAttrs.split('src="')[1].split('"')[0];
|
||||
let table = parsed.querySelectorAll('.infotable')[0].childNodes[0].structuredText.split('\n');
|
||||
let tags = parsed.querySelectorAll('.seriestugenre')[0].structuredText.split('\n')[0].split(' ');
|
||||
let latestChap = parseInt(parsed.querySelectorAll('.epcurlast')[0].rawText.split(' ')[1])
|
||||
table.splice(-6);
|
||||
const aux = {};
|
||||
for (let i = 0; i < table.length; i += 2) {
|
||||
const key = table[i].trim();
|
||||
aux[key] = table[i + 1].trim();
|
||||
}
|
||||
|
||||
return {description, img, ...aux, tags, title:auxTitle, latestChap}
|
||||
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {String} title Manga Title
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
async hasTitle(title)
|
||||
{
|
||||
const Search = await this.Search(title);
|
||||
if(!Array.isArray(Search)) return false
|
||||
return Search.filter(Manga => {return Manga.title === title})[0];
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "a123456789",
|
||||
"password2": "a123456789",
|
||||
"saveFolder": "./public/Saved",
|
||||
"hash": "e941d5c52a2f5aca8237ef40a46749d8619c213683d080f753c5d86f314ae270"
|
||||
}
|
@ -0,0 +1,405 @@
|
||||
const {Search, Chapter, Manga,SearchByTag, isValidID, Modules, Baker, Crypto, cookieParser} = require('../lib.js')
|
||||
const mongoose = require('mongoose')
|
||||
const FavoriteModel = require('../models/favorite.js');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { isNullOrUndefined } = require('util');
|
||||
|
||||
exports.home = async (req,res)=>
|
||||
{
|
||||
const cookieStr = req.headers.cookie;
|
||||
if(!cookieStr) return res.render('home.ejs',{data:null});
|
||||
const cookie = cookieParser(cookieStr, 'hash');
|
||||
res.render('home.ejs', {data:cookie});
|
||||
}
|
||||
exports.searchBar = (req, res)=>
|
||||
{
|
||||
res.render('searchBar.ejs');
|
||||
}
|
||||
exports.search = async (req, res)=>
|
||||
{
|
||||
if(!req.body?.searchstring) return res.sendStatus(404)
|
||||
var data = await new Search(req.body.searchstring).search();
|
||||
if(!Array.isArray(data)) return res.sendStatus(404);
|
||||
const favorites = await FavoriteModel.find()
|
||||
const favoriteScanlators = favorites.map(favorite => favorite.scanlator);
|
||||
data.forEach((item, index) =>
|
||||
{
|
||||
if (favoriteScanlators.includes(item.scanlator))
|
||||
{
|
||||
item.Results.find(result =>
|
||||
{
|
||||
const matchingFavorite = favorites.find(favorite =>
|
||||
{
|
||||
return result.title === favorite.title && item.scanlator === favorite.scanlator;
|
||||
});
|
||||
if (matchingFavorite)
|
||||
{
|
||||
result.favorite = matchingFavorite._id;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
});
|
||||
data = data.filter(item => item.Results.length > 0)
|
||||
data.sort((a,b)=>
|
||||
{
|
||||
const scanlatorA = a.scanlator.toLowerCase();
|
||||
const scanlatorB = b.scanlator.toLowerCase();
|
||||
return scanlatorA.localeCompare(scanlatorB);
|
||||
})
|
||||
res.render('searchResults.ejs', {data})
|
||||
}
|
||||
|
||||
|
||||
exports.bookmark = (worker)=>
|
||||
{
|
||||
return async (req, res)=>
|
||||
{
|
||||
if (!req.params || typeof req.params !== 'object' || !('scanlator' in req.params) || !('title' in req.params)) return res.sendStatus(404);
|
||||
const {scanlator, title, idorLink} = req.params;
|
||||
const {cookie} = req.headers;
|
||||
if(!cookie) return res.sendStatus(404);
|
||||
const config = require('../config.json');
|
||||
if(cookieParser(cookie, 'hash')!=config.hash) return res.sendStatus(404);
|
||||
if(isValidID(idorLink))
|
||||
{
|
||||
worker.send({ action:'remove', idorLink})
|
||||
return await FavoriteModel.findOneAndDelete({_id:idorLink}).then((deleted)=>
|
||||
{
|
||||
return res.render('bookmark.ejs', {scanlator, title, link:encodeURIComponent(deleted.link)})
|
||||
})
|
||||
}
|
||||
|
||||
const scanlatorExists = await new Modules(scanlator).exists();
|
||||
if(!scanlatorExists) return res.sendStatus(404);
|
||||
const scanlatorHasTitle = await new Modules(scanlator).titleExists(title);
|
||||
if(!scanlatorHasTitle) return res.sendStatus(404);
|
||||
|
||||
const fav = new FavoriteModel()
|
||||
fav.scanlator = scanlator;
|
||||
fav.title = title;
|
||||
fav.link = idorLink;
|
||||
await fav.save();
|
||||
|
||||
worker.send({action:'add', id:fav._id})
|
||||
res.render('bookmarked.ejs', {scanlator, title, link:idorLink, id:fav._id});
|
||||
let manga;
|
||||
try {
|
||||
manga = await new Manga(scanlator, idorLink, title).get();
|
||||
} catch (error) {
|
||||
return FavoriteModel.findByIdAndDelete(fav._id)
|
||||
}
|
||||
const {tags} = manga;
|
||||
FavoriteModel.findByIdAndUpdate(fav._id,{tags}, { new: true})
|
||||
.then((doc, err) =>
|
||||
{
|
||||
if (err) console.error('test',err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.manga = async (req, res)=>
|
||||
{
|
||||
const {scanlator, link, title} = req.params;
|
||||
const scanlatorExists = await new Modules(scanlator).exists();
|
||||
if(!scanlatorExists) return res.sendStatus(404);
|
||||
const scanlatorHasTitle = await new Modules(scanlator).titleExists(title);
|
||||
if(!scanlatorHasTitle) return res.sendStatus(404);
|
||||
let manga;
|
||||
try
|
||||
{
|
||||
manga = await new Manga(scanlator,link, title).get()
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
res.sendStatus(404);
|
||||
}
|
||||
res.render('mangaPage.ejs', {data:{...manga, scanlator}});
|
||||
|
||||
}
|
||||
exports.chapter = async (req, res)=>
|
||||
{
|
||||
const {scanlator, title, chapter, link} = req.params;
|
||||
//IF its already there even with wrong things its there just show it
|
||||
const fileExists = fs.existsSync(`./public/Saved/${scanlator}/${title}/CH_${chapter}`);
|
||||
if(fileExists)
|
||||
{
|
||||
const imgs =fs.readdirSync(`./public/Saved/${scanlator}/${title}/CH_${chapter}`);
|
||||
const latestChap = fs.readdirSync(`./public/Saved/${scanlator}/${title}`).length
|
||||
const List = imgs.map(filename => {return `./Saved/${scanlator}/${title}/CH_${chapter}/${filename}`});
|
||||
const mangaLink = await new Chapter(scanlator, link, title, chapter).getMangaLink();
|
||||
return res.render('display.ejs', {data:{title,latestChap,scanlator, chapterNum:parseInt(chapter), mangaLink, List}});
|
||||
}
|
||||
//If it isn't make sure there is no bad params being passed
|
||||
const scanlatorExists = await new Modules(scanlator).exists();
|
||||
if(!scanlatorExists) return res.sendStatus(404);
|
||||
const scanlatorHasTitle = await new Modules(scanlator).titleExists(title);
|
||||
if(!scanlatorHasTitle) return res.sendStatus(404);
|
||||
let manga;
|
||||
try
|
||||
{
|
||||
manga = await new Chapter(scanlator, link, title,chapter).get();
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.render('display.ejs', {data:{...manga, scanlator, chapterNum:parseInt(chapter)}});
|
||||
|
||||
}
|
||||
exports.favorites = async (req, res)=>
|
||||
{
|
||||
const favs = await FavoriteModel.find();
|
||||
var mangas = [];
|
||||
if(favs.length == 0) return res.render('favorites.ejs', {isEmpty:true, mangas})
|
||||
await Promise.all(Object.keys(favs).map(async (key) =>
|
||||
{
|
||||
const manga = favs[key];
|
||||
var aux = await new Manga(manga.scanlator, manga.link, manga.title).get();
|
||||
if(aux)
|
||||
{
|
||||
aux.favorite = manga._id;
|
||||
mangas.push(aux);
|
||||
}
|
||||
}));
|
||||
function getUniqueScanlators(items)
|
||||
{
|
||||
const scanlators = {};
|
||||
items.forEach(item => {
|
||||
scanlators[item.scanlator] = true;
|
||||
});
|
||||
return Object.keys(scanlators);
|
||||
}
|
||||
const uniqueScanlators = getUniqueScanlators(mangas);
|
||||
const scanlatorsArray = uniqueScanlators.map(scanlator =>
|
||||
{
|
||||
var scanlatorItems = mangas.filter(manga => manga.scanlator === scanlator);
|
||||
if(!scanlatorItems) return res.sendStatus(500);
|
||||
scanlatorItems.sort((a, b) =>
|
||||
{
|
||||
if (a.title.toUpperCase() < b.title.toUpperCase()) return -1;
|
||||
else return 1;
|
||||
});
|
||||
return {
|
||||
scanlator,
|
||||
Results: scanlatorItems
|
||||
};
|
||||
});
|
||||
scanlatorsArray.sort((a,b)=>
|
||||
{
|
||||
const scanlatorA = a.scanlator.toLowerCase();
|
||||
const scanlatorB = b.scanlator.toLowerCase();
|
||||
return scanlatorA.localeCompare(scanlatorB);
|
||||
})
|
||||
res.render('favorites.ejs', {isEmpty:false, mangas:scanlatorsArray})
|
||||
}
|
||||
exports.recommended = async (req,res)=>
|
||||
{
|
||||
const favorites = await FavoriteModel.find();
|
||||
const favoriteScanlators = favorites.map(favorite => favorite.scanlator);
|
||||
let favTags = [];
|
||||
for(var i = 0; i<favorites.length; i++)
|
||||
{
|
||||
favTags.push(...favorites[i].tags)
|
||||
}
|
||||
const counted = favTags.reduce((acc, curr)=>
|
||||
{
|
||||
acc[curr] = (acc[curr]||0)+1;
|
||||
return acc;
|
||||
}, {});
|
||||
const mostCommonTags = [...new Set(favTags)].sort((a,b)=>
|
||||
{
|
||||
if(counted[b] !== counted[a])
|
||||
{
|
||||
return counted[b]-counted[a];
|
||||
}
|
||||
else
|
||||
{
|
||||
return favTags.indexOf(a)-favTags.indexOf(b);
|
||||
}
|
||||
})
|
||||
const firstTwoTags =favoriteScanlators.length <=0 ? ['action', 'shounen'] : mostCommonTags.slice(0,2);
|
||||
let recommended = [];
|
||||
for(var i =0; i<2; i++)
|
||||
{
|
||||
const results = await new SearchByTag(firstTwoTags[i]).search();
|
||||
recommended.push(results);
|
||||
}
|
||||
const groupedByScanlator = recommended.reduce((acc, currentArray) =>
|
||||
{
|
||||
currentArray.forEach(({ Results, scanlator }) =>
|
||||
{
|
||||
if (!acc[scanlator])
|
||||
{
|
||||
acc[scanlator] = { Results: [], scanlator };
|
||||
}
|
||||
const uniqueResults = Results.filter(result =>
|
||||
!acc[scanlator].Results.some(existingResult => existingResult.title === result.title)
|
||||
);
|
||||
acc[scanlator].Results = acc[scanlator].Results.concat(uniqueResults);
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
recommended = Object.values(groupedByScanlator);
|
||||
|
||||
recommended.forEach((item, index) =>
|
||||
{
|
||||
if (favoriteScanlators.includes(item.scanlator))
|
||||
{
|
||||
item.Results.find(result =>
|
||||
{
|
||||
const matchingFavorite = favorites.find(favorite =>
|
||||
{
|
||||
return result.title === favorite.title && item.scanlator === favorite.scanlator;
|
||||
});
|
||||
if (matchingFavorite)
|
||||
{
|
||||
result.favorite = matchingFavorite._id;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
});
|
||||
recommended.sort((a,b)=>
|
||||
{
|
||||
const scanlatorA = a.scanlator.toLowerCase();
|
||||
const scanlatorB = b.scanlator.toLowerCase();
|
||||
return scanlatorA.localeCompare(scanlatorB);
|
||||
})
|
||||
res.render('searchResults.ejs', {data:recommended});
|
||||
}
|
||||
exports.chapterNavInfo = async (req, res)=>
|
||||
{
|
||||
const {scanlator, title, mangaLink} = req.params;
|
||||
const scanlatorExists = await new Modules(scanlator).exists();
|
||||
if(!scanlatorExists) return res.sendStatus(404);
|
||||
const scanlatorHasTitle = await new Modules(scanlator).titleExists(title);
|
||||
if(!scanlatorHasTitle) return res.sendStatus(404);
|
||||
let chapter = parseInt(req.params.chapter);
|
||||
let manga;
|
||||
try
|
||||
{
|
||||
manga = await new Manga(scanlator, mangaLink, title).get();
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const {latestChap, List} = manga;
|
||||
List.sort((a,b)=> {return a.num - b.num});
|
||||
let nextChapterLink = chapter>= latestChap? '' : List[chapter].link;
|
||||
let prevChapterLink = chapter<=0? '' : List[chapter-1].link;
|
||||
res.render('chapterNav.ejs', {data:{title,latestChap,scanlator, chapterNum:chapter,mangaLink, nextChapterLink, prevChapterLink}})
|
||||
}
|
||||
exports.hasConfig = (req, res, next) =>
|
||||
{
|
||||
const exists = fs.existsSync('./config.json');
|
||||
if(!exists) return this.config(req,res)
|
||||
next();
|
||||
}
|
||||
exports.config = async (req, res)=>
|
||||
{
|
||||
function getSubDirectories(dir)
|
||||
{
|
||||
let protectedSubDirs = ['node_modules', '.git', 'controllers', 'models', 'Modules', 'css','images', 'js', 'routes', 'views'];
|
||||
const map = new Map();
|
||||
const dirs = fs.readdirSync(dir)
|
||||
.filter(file => fs.statSync(path.join(dir, file)).isDirectory())
|
||||
.filter(file => !protectedSubDirs.includes(file))
|
||||
.filter(file=> !file.includes('scans'));
|
||||
for(let i =0; i<dirs.length; i++)
|
||||
{
|
||||
map.set(dirs[i], getSubDirectories(path.join(dir, dirs[i])));
|
||||
}
|
||||
return map.size==0?'':map;
|
||||
}
|
||||
function getPaths(map, prefix='.')
|
||||
{
|
||||
const paths = [];
|
||||
for (const [key, value] of map.entries())
|
||||
{
|
||||
const fullPath = `${prefix}/${key}`;
|
||||
if (value instanceof Map)
|
||||
{
|
||||
paths.push(...getPaths(value, fullPath));
|
||||
}
|
||||
else
|
||||
{
|
||||
paths.push(fullPath);
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
const subDirs = await getSubDirectories('./');
|
||||
const data = getPaths(subDirs)
|
||||
|
||||
res.render('config.ejs', {data});
|
||||
}
|
||||
exports.configPost = async (req, res)=>
|
||||
{
|
||||
const {username, password, password2, saveFolder} = req.body;
|
||||
const {body} = req;
|
||||
if(!username || !password || !password2) return res.sendStatus(404);
|
||||
if(username.length < 4) return res.sendStatus(404);
|
||||
if(password !== password2) return res.sendStatus(404);
|
||||
if(!checkValidPath(saveFolder)) return res.sendStatus(404);
|
||||
if(!saveFolder) body.saveFolder = './public/Saved'
|
||||
body['hash'] = new Crypto(username, password).Hash;
|
||||
await fs.writeFileSync('./config.json', JSON.stringify(body));
|
||||
res.redirect('/');
|
||||
}
|
||||
/**
|
||||
* @param {String} dir
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function checkValidPath(dir)
|
||||
{
|
||||
if(!dir) return true; //by pass when there is no savePath (use default);
|
||||
dir = dir;
|
||||
return !path.isAbsolute(dir)
|
||||
}
|
||||
exports.loginPage = async (req, res)=>
|
||||
{
|
||||
res.render('login.ejs');
|
||||
}
|
||||
exports.login = async (req, res)=>
|
||||
{
|
||||
const {username, password} = req.body;
|
||||
if(!username || !password ) return res.sendStatus(404);
|
||||
const config = require('../config.json');
|
||||
if(username != config.username || password != config.password) return res.sendStatus(404);
|
||||
await Baker(res, 'hash', config.hash, 24)
|
||||
//TODO: change this to dashboard
|
||||
res.redirect('/');
|
||||
}
|
||||
exports.logout = async (req, res)=>
|
||||
{
|
||||
const config = require('../config.json');
|
||||
const cookieStr = req.headers.cookie;
|
||||
if(!cookieStr) return res.redirect('/');
|
||||
const hash = cookieParser(cookieStr, 'hash');
|
||||
if(hash != config.hash) return res.redirect('/');
|
||||
await Baker(res, 'hash', '')
|
||||
res.render('dashboard.ejs');
|
||||
}
|
||||
|
||||
exports.errorPage = (req, res)=>
|
||||
{
|
||||
res.render('error.ejs')
|
||||
}
|
||||
|
||||
exports.chapterRead = async (req, res)=>
|
||||
{
|
||||
console.log('ChapterRead')
|
||||
res.sendStatus(200);
|
||||
//Send to db;
|
||||
}
|
||||
exports.dashboard = async (req, res)=>
|
||||
{
|
||||
let data = 0;
|
||||
res.render('dashboard.ejs', {data});
|
||||
}
|
@ -0,0 +1,286 @@
|
||||
const mongoose = require('mongoose');
|
||||
const mongoURI = process.env.DATABASE_URL;
|
||||
mongoose.connect(mongoURI);
|
||||
mongoose.connection.on('connected', ()=>{console.log('MongoDB - Worker - Connected')})
|
||||
.on('disconnected', ()=>{console.log('MongoDB - Disconnect')})
|
||||
.on('error', (error)=>console.log('Mongoose Error:', error));
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { Stream } = require('stream');
|
||||
const {Manga, Chapter} = require('./lib');
|
||||
const FavModel = require('./models/favorite')
|
||||
const EventEmitter = require('events');
|
||||
const Emitter = new EventEmitter();
|
||||
let config;
|
||||
if(fs.existsSync('./config.json'))
|
||||
{
|
||||
config = require('./config.json')
|
||||
}
|
||||
const BaseDir = config?.saveFolder || './public/Saved';
|
||||
|
||||
var Queue = [];
|
||||
var slots = 2;
|
||||
|
||||
var shouldKeepOnDownloading = true;
|
||||
|
||||
// updateQueue();
|
||||
// var queueUpdateTimer = setInterval(() => {
|
||||
// updateQueue();
|
||||
// }, 5*60*1000);
|
||||
|
||||
|
||||
// process.on('message', m=>
|
||||
// {
|
||||
// if(m.action==='add')
|
||||
// {
|
||||
// clearInterval(queueUpdateTimer);
|
||||
// updateQueue();
|
||||
// queueUpdateTimer = setInterval(() => {
|
||||
// updateQueue();
|
||||
// }, 5*60*1000);
|
||||
// return
|
||||
// }
|
||||
|
||||
// if(m.action==='remove') return removeFromQueue(m.id)
|
||||
// if(m.action==='pause') return shouldKeepOnDownloading=false;
|
||||
// })
|
||||
|
||||
function removeFromQueue(id)
|
||||
{
|
||||
const ItemDownloading = Queue.filter(manga => manga.inProgress == true)[0];
|
||||
if(ItemDownloading) shouldKeepOnDownloading = false;
|
||||
Queue = Queue.filter(manga => manga._id !== id);
|
||||
|
||||
}
|
||||
Emitter.on('queueUpdate', ()=>
|
||||
{
|
||||
if(!Queue[0]) return
|
||||
if(!Queue[0].inProgress)
|
||||
{
|
||||
Queue[0].inProgress = true;
|
||||
download(Queue[0])
|
||||
}
|
||||
} );
|
||||
|
||||
async function updateQueue()
|
||||
{
|
||||
shouldKeepOnDownloading = true;
|
||||
const Scanlators = fs.readdirSync(BaseDir);
|
||||
const Favs = await FavModel.find();
|
||||
let Mangas = [];
|
||||
for(let i =0; i<Scanlators.length; i++)
|
||||
{
|
||||
let mangas = fs.readdirSync(path.join(BaseDir, Scanlators[i]));
|
||||
if(mangas.length < 0) continue
|
||||
for(var j = 0; j<mangas.length; j++)
|
||||
{
|
||||
let chapters = fs.readdirSync(path.join(BaseDir, Scanlators[i], mangas[j]));
|
||||
if(chapters.length==0) continue
|
||||
chapters.sort((a, b) => { return parseInt(a.split('_')[1]) - parseInt(b.split('_')[1]) });
|
||||
let latestDownloaded = parseInt(chapters[chapters.length-1].split('_')[1]);
|
||||
let manga = {scanlator:Scanlators[i], title:mangas[j], latestDownloaded}
|
||||
Mangas.push(manga)
|
||||
}
|
||||
}
|
||||
let FavedAndDownloaded = [];
|
||||
let FavedButNotDownloaded = null;
|
||||
for (let i = 0; i < Mangas.length; i++)
|
||||
{
|
||||
FavedAndDownloaded = FavedAndDownloaded.concat(Favs.filter(item =>
|
||||
{
|
||||
return (item.scanlator === Mangas[i].scanlator && item.title === Mangas[i].title);
|
||||
}).map((matchedItem) =>{
|
||||
return ({
|
||||
...matchedItem._doc,
|
||||
latestDownloaded: Mangas[i].latestDownloaded
|
||||
})
|
||||
})
|
||||
);
|
||||
}
|
||||
FavedButNotDownloaded = Favs.filter(fav =>
|
||||
{
|
||||
// return !FavedAndDownloaded.includes(fav)
|
||||
return !FavedAndDownloaded.some(fad => fad._id === fav._id);
|
||||
});
|
||||
if(FavedButNotDownloaded)
|
||||
{
|
||||
for(let i = 0; i < FavedButNotDownloaded.length; i++)
|
||||
{
|
||||
(async(i)=>
|
||||
{
|
||||
const {title, link, scanlator, _id} = FavedButNotDownloaded[i];
|
||||
const manga = await new Manga(scanlator, link, title).get();
|
||||
let id = _id.toString();
|
||||
addToQueue(manga, id);
|
||||
})(i)
|
||||
}
|
||||
}
|
||||
if(FavedAndDownloaded)
|
||||
{
|
||||
for(let i = 0; i<FavedAndDownloaded.length; i++)
|
||||
{
|
||||
(async(i)=>
|
||||
{
|
||||
const {title, link, scanlator, latestDownloaded, _id} = FavedAndDownloaded[i];
|
||||
const manga = await new Manga(scanlator, link, title).get();
|
||||
let id = _id.toString();
|
||||
checkForMissingFiles(manga)
|
||||
if(latestDownloaded<manga.latestChap) addToQueue(manga, id, latestDownloaded);
|
||||
})(i);
|
||||
}
|
||||
}
|
||||
function addToQueue(manga, _id,startFrom=0)
|
||||
{
|
||||
const List = manga.List.filter(item => item.num > startFrom);
|
||||
Queue.push({...manga, _id, List, startFrom})
|
||||
Emitter.emit('queueUpdate', '');
|
||||
}
|
||||
}
|
||||
async function checkForMissingFiles(manga)
|
||||
{
|
||||
const {scanlator, title, List} = manga;
|
||||
let auxList = List.sort((a,b)=>{return a.num - b.num})
|
||||
for(var i = 0; i<auxList.length; i++)
|
||||
{
|
||||
const {link} = auxList[i];
|
||||
const destPath = path.join(BaseDir, scanlator, title, 'CH_'+(i+1));
|
||||
const chapterDownloaded = fs.existsSync(destPath);
|
||||
if(!chapterDownloaded)
|
||||
{
|
||||
if(shouldKeepOnDownloading)
|
||||
{
|
||||
const chapter = await new Chapter(scanlator, link, title, i+1).get();
|
||||
await downloadChapter(chapter);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const chapterDir = fs.readdirSync(destPath);
|
||||
const chapter = await new Chapter(scanlator, link, title, i+1).get();
|
||||
let auxChapterDir = chapterDir.map(item=>{return item.split('.')[0]})
|
||||
if(!chapter?.List) continue;
|
||||
var auxChapterList = chapter.List.map(item=>{return item.split('/')[item.split('/').length-1].split('.')[0]})
|
||||
var incompleteImages = [];
|
||||
for(var k = 0; k<auxChapterDir.length; k++)
|
||||
{
|
||||
const auxDir = chapterDir.sort();
|
||||
const _path = path.join(destPath, auxDir[k]);
|
||||
const isComplete = await isImageComplete(_path);
|
||||
if(!isComplete) incompleteImages.push(auxDir[k].split('.')[0])
|
||||
}
|
||||
const missingImages =[...incompleteImages, ...auxChapterList.filter(item=>!auxChapterDir.includes(item))];
|
||||
if(missingImages.length == 0) continue;
|
||||
for(var j = 0; j<missingImages.length; j++)
|
||||
{
|
||||
const imageLink = chapter.List.filter(item=>item.split('/')[item.split('/').length-1].split('.')[0] == missingImages[j])[0]
|
||||
const destination = `${destPath}/${imageLink.split('/')[(imageLink.split('/').length - 1)]}`
|
||||
if(shouldKeepOnDownloading)
|
||||
{
|
||||
const download = await downloadImage(imageLink,destination, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Path} filePath
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
async function isImageComplete(filePath)
|
||||
{
|
||||
async function IsFileComplete(filePath) {
|
||||
try {
|
||||
await sharp(filePath, { sequentialRead: true })
|
||||
.raw()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
return true
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function download(manga)
|
||||
{
|
||||
if(slots <=0) return
|
||||
slots--;
|
||||
const { scanlator, title} = manga;
|
||||
const List = manga.List.sort((a, b) => { return a.num - b.num });
|
||||
for (let i = 0; i < List.length; i++)
|
||||
{
|
||||
const ch = List[i];
|
||||
if (isNaN(ch.num)) continue;
|
||||
const { num, link } = ch;
|
||||
const chapter = await new Chapter(scanlator, link, title, num).get();
|
||||
// console.log(chapter)
|
||||
if (!chapter?.List?.length) continue;
|
||||
const dlChapter = await downloadChapter(chapter);
|
||||
if(dlChapter=='UserStoped') break;
|
||||
}
|
||||
Queue.shift();
|
||||
slots++;
|
||||
Emitter.emit('queueUpdate', '');
|
||||
}
|
||||
async function downloadChapter(chapter)
|
||||
{
|
||||
const {List, scanlator, title, chNum } = chapter;
|
||||
const images = List;
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
const destination = `./public/Saved/${scanlator}/${title}/CH_${chNum}/${img.split('/')[(img.split('/').length - 1)]}`;
|
||||
try {
|
||||
const downloaded = await downloadImage(img, destination);
|
||||
console.log(`Downloaded: Scanlator:${scanlator}; Manga: ${title} Chapter:${chNum}; IMG:${img.split('/')[(img.split('/').length - 1)]} ${downloaded.status}`);
|
||||
} catch (error) {
|
||||
console.error(`Error downloading: ${img}: ${error}`);
|
||||
if(!shouldKeepOnDownloading)
|
||||
{
|
||||
ReturnError = error.Error;
|
||||
break;
|
||||
};
|
||||
}
|
||||
}
|
||||
return !shouldKeepOnDownloading?'UserStoped':{chNum};
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Link} url
|
||||
* @param {Path} destPath
|
||||
* @param {Number} maxRetries
|
||||
* @returns {Download}
|
||||
* @typedef {String} Link
|
||||
* @typedef {Object} Download
|
||||
* @property {String} status
|
||||
* @property {Path} file
|
||||
*/
|
||||
async function downloadImage(url, destPath, maxRetries = 3)
|
||||
{
|
||||
let retries = 0;
|
||||
if(!shouldKeepOnDownloading) throw new Error('UserStoped');
|
||||
while (retries < maxRetries && shouldKeepOnDownloading) {
|
||||
try
|
||||
{
|
||||
const response = await fetch(url);
|
||||
const dir = path.dirname(destPath);
|
||||
if (!response.ok) throw new Error(`Failed to download image. Status Code: ${response.status}`);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
const fileStream = fs.createWriteStream(destPath);
|
||||
await new Promise((resolve, reject) =>
|
||||
{
|
||||
const stream = Stream.Readable.fromWeb(response.body).pipe(fileStream);
|
||||
stream.on('error', reject);
|
||||
fileStream.on('finish', () => {
|
||||
fileStream.close();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return {status:'Downloaded', file:destPath}
|
||||
} catch (error) {
|
||||
console.error(`Error downloading image: ${destPath}, Retry ${retries + 1}/${maxRetries}`);//, error);
|
||||
retries++;
|
||||
if(retries == maxRetries)
|
||||
return {status:'Failed', file:destPath}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,478 @@
|
||||
const find = require('findit');
|
||||
const path = require('path');
|
||||
const mongoose = require('mongoose');
|
||||
const crypto = require('crypto');
|
||||
|
||||
module.exports.Modules = class Modules
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {String} name Scanlator Name - Same as in the Module.
|
||||
*/
|
||||
constructor (name=null)
|
||||
{
|
||||
this.name = name;
|
||||
this.Modules;
|
||||
}
|
||||
/**
|
||||
* @returns {Array<Modules>}
|
||||
* @typedef {Class} Modules
|
||||
*/
|
||||
getAll()
|
||||
{
|
||||
let finder = find(path.resolve('Modules'));
|
||||
finder.on('file', file =>
|
||||
{
|
||||
let Module = require(file);
|
||||
if (Module.constructor)
|
||||
{
|
||||
this.Modules.push(Module);
|
||||
}
|
||||
});
|
||||
finder.on('end', () =>
|
||||
{
|
||||
return {Modules:this.Modules}
|
||||
});
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
async exists()
|
||||
{
|
||||
if (!this.name) return false;
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
const finder = find(path.resolve('Modules'));
|
||||
finder.on('file', async file =>
|
||||
{
|
||||
let Module = require(file);
|
||||
if (Module.constructor)
|
||||
{
|
||||
const test = new Module();
|
||||
if (this.name == test.scanlator)
|
||||
{
|
||||
finder.removeAllListeners();
|
||||
resolve(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
finder.on('error', err =>
|
||||
{
|
||||
reject(false);
|
||||
});
|
||||
|
||||
finder.on('end', () =>
|
||||
{
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {String} title
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
async titleExists(title)
|
||||
{
|
||||
if(!this.name) return false;
|
||||
const module = require(`./Modules/${this.name.split('-')[0]+'Module'}`);
|
||||
return await new module().hasTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.Search = class Search
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {String} searchString - String to search by.
|
||||
* @param {Scanlator} scanlator - Scanlator Name
|
||||
* @typedef {String} Scanlator
|
||||
* @pattern /^[\w-]+-scans$/
|
||||
*/
|
||||
constructor(searchString, scanlator=null)
|
||||
{
|
||||
this.ss = searchString;
|
||||
this.scanlator = scanlator;
|
||||
this.Modules = [];
|
||||
this.results = [];
|
||||
this.initialized = new Promise((resolve) =>
|
||||
{
|
||||
let finder = find(path.resolve('Modules'));
|
||||
finder.on('file', file =>
|
||||
{
|
||||
let Module = require(file);
|
||||
if (Module.constructor)
|
||||
{
|
||||
this.Modules.push(Module);
|
||||
}
|
||||
});
|
||||
finder.on('end', () =>
|
||||
{
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @returns {Array<Results>}
|
||||
* @typedef {Object} Results
|
||||
* @property {Array<Manga>} Results
|
||||
* @property {Scanlator} scanlator
|
||||
* @typedef {Object} Manga
|
||||
* @property {String} link - Manga Link
|
||||
* @property {String} title - Manga Title
|
||||
* @property {String} img - Image Link
|
||||
* @typedef {String} Scanlator
|
||||
* @pattern /^[\w-]+-scans$/
|
||||
*/
|
||||
async search()
|
||||
{
|
||||
await this.initialized;
|
||||
if(this.Modules.length==0) return
|
||||
for(const module of this.Modules)
|
||||
{
|
||||
const auxModule = new module()
|
||||
if(!this.scanlator || this.scanlator == auxModule.scanlator )
|
||||
try
|
||||
{
|
||||
var results = await auxModule.Search(this.ss);
|
||||
//TODO: Defaults for missing info from the modules;
|
||||
// console.log('Lib: Search: search method: result:', result);
|
||||
results.sort((a, b) => {
|
||||
if (a.title.toUpperCase() < b.title.toUpperCase()) return -1;
|
||||
else return 1;
|
||||
});
|
||||
for(var i = 0; i< results.length; i++)
|
||||
{
|
||||
results[i]['Status'] = results[i].status;
|
||||
}
|
||||
this.results.push({
|
||||
Results:results,
|
||||
scanlator:auxModule.scanlator
|
||||
});
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Lib: Module Errored:', auxModule.scanlator, 'Error:', error)
|
||||
}
|
||||
}
|
||||
return this.results
|
||||
}
|
||||
|
||||
}
|
||||
module.exports.SearchByTag = class SearchByTag
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {String} tag
|
||||
*/
|
||||
constructor(tag)
|
||||
{
|
||||
this.ss = tag.toLowerCase();
|
||||
this.Modules = [];
|
||||
this.results = [];
|
||||
this.initialized = new Promise((resolve) =>
|
||||
{
|
||||
let finder = find(path.resolve('Modules'));
|
||||
finder.on('file', file =>
|
||||
{
|
||||
let Module = require(file);
|
||||
if (Module.constructor)
|
||||
{
|
||||
this.Modules.push(Module);
|
||||
}
|
||||
});
|
||||
finder.on('end', () =>
|
||||
{
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
}
|
||||
/**
|
||||
* @param {Number} ammount - 5 by default
|
||||
* @returns {Array<Results>}
|
||||
* @typedef {Object} Results
|
||||
* @property {Array<Manga>} Results
|
||||
* @property {Scanlator} scanlator
|
||||
* @typedef {Object} Manga
|
||||
* @property {String} link - Manga Link
|
||||
* @property {String} title - Manga Title
|
||||
* @property {String} img - Image Link
|
||||
* @typedef {String} Scanlator
|
||||
* @pattern /^[\w-]+-scans$/
|
||||
*/
|
||||
async search(ammount=5)
|
||||
{
|
||||
await this.initialized;
|
||||
if(this.Modules.length==0) return
|
||||
for(const module of this.Modules)
|
||||
{
|
||||
const auxModule = new module()
|
||||
try
|
||||
{
|
||||
var results = await auxModule.SearchByTag(this.ss, ammount);
|
||||
//TODO: Defaults for missing info from the modules;
|
||||
// console.log('Lib: Search: search method: result:', results);
|
||||
results.sort((a, b) => {
|
||||
if (a.title.toUpperCase() < b.title.toUpperCase()) return -1;
|
||||
else return 1;
|
||||
});
|
||||
for(var i = 0; i< results.length; i++)
|
||||
{
|
||||
results[i]['Status'] = results[i].status;
|
||||
}
|
||||
this.results.push({
|
||||
Results:results,
|
||||
scanlator:auxModule.scanlator
|
||||
});
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Lib: Module Errored:', auxModule.scanlator, 'Error:', error)
|
||||
}
|
||||
}
|
||||
return this.results
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.Manga = class Manga
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {Scanlator} scanlator
|
||||
* @param {Link} link Manga Link
|
||||
* @param {String} title
|
||||
* @typedef {String} Link
|
||||
* @pattern ^(https?:\/\/)?([\w\d.-]+)\.([a-z]{2,})(:[0-9]+)?(\/\S*)?$
|
||||
* @typedef {String} Scanlator
|
||||
* @pattern /^[\w-]+-scans$/
|
||||
*/
|
||||
constructor(scanlator, link, title)
|
||||
{
|
||||
this.scanlator = scanlator;
|
||||
this.link = link;
|
||||
this.title = title;
|
||||
this.Engine;
|
||||
this.initialized = new Promise((resolve) =>
|
||||
{
|
||||
let finder = find(path.resolve('Modules'));
|
||||
finder.on('file', file =>
|
||||
{
|
||||
let Module = require(file);
|
||||
if (Module.constructor)
|
||||
{
|
||||
if(this.scanlator == new Module().scanlator)
|
||||
{
|
||||
this.Engine = Module;
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @returns {Manga}
|
||||
* @typedef {Object} Manga
|
||||
* @property {String} link - Manga Link
|
||||
* @property {String} title - Manga Title
|
||||
* @property {String} img - Image Link
|
||||
* @property {Array<String>} tags - Manga Tags
|
||||
* @property {Array<Chapter>} List - Chapter List
|
||||
* @typedef {Object} Chapter
|
||||
* @property {Number} num - Chapter Number
|
||||
* @property {Link} link - Chapter Link
|
||||
* @property {Date_} date - Chapter uploaded date
|
||||
* @typedef {String} Link
|
||||
* @typedef {String} Date_
|
||||
*/
|
||||
async get()
|
||||
{
|
||||
await this.initialized;
|
||||
try {
|
||||
const Scanlator = new this.Engine();
|
||||
const Manga = await Scanlator.GetManga(this.link, this.title);
|
||||
const List = await Scanlator.ChapterList(this.link, this.title);
|
||||
const Status = Manga.Status?Manga.Status:Manga.status;
|
||||
//TODO: Defaults for missing info from the modules;
|
||||
return {...Manga, Status, link:this.link, scanlator:this.scanlator, List, title:this.title}
|
||||
} catch (error) {
|
||||
console.error('Lib: Manga: Error:' ,error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.Chapter = class Chapter
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {Scanlator} scanlator - Scanlator Name
|
||||
* @param {Link} Link - Chapter Link
|
||||
* @param {String} title - Manga Title
|
||||
* @param {Number} chNum - Chapter Number
|
||||
* @typedef {String} Link
|
||||
* @pattern ^(https?:\/\/)?([\w\d.-]+)\.([a-z]{2,})(:[0-9]+)?(\/\S*)?$
|
||||
* @typedef {String} Scanlator
|
||||
* @pattern /^[\w-]+-scans$/
|
||||
*/
|
||||
constructor(scanlator, Link,title, chNum)
|
||||
{
|
||||
this.scanlator = scanlator;
|
||||
this.title = title;
|
||||
this.Link = Link;
|
||||
this.chNum = chNum;
|
||||
this.Engine;
|
||||
this.initialized = new Promise((resolve) =>
|
||||
{
|
||||
let finder = find(path.resolve('Modules'));
|
||||
finder.on('file', file =>
|
||||
{
|
||||
let Module = require(file);
|
||||
if (Module.constructor)
|
||||
{
|
||||
if(this.scanlator == new Module().scanlator)
|
||||
{
|
||||
this.Engine = Module;
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @returns {Chapter}
|
||||
* @typedef {Object} Chapter
|
||||
* @property {Manga} Manga - Manga Link
|
||||
* @property {Array<Img>} List - Chapter List of images
|
||||
* @property {Number} chNum - Chapter number
|
||||
* @typedef {Object} Manga
|
||||
* @property {String} link - Manga Link
|
||||
* @property {String} title - Manga Title
|
||||
* @property {String} img - Image Link
|
||||
* @typedef {String} Link
|
||||
* @typedef {Link} Img
|
||||
*/
|
||||
async get()
|
||||
{
|
||||
await this.initialized;
|
||||
try
|
||||
{
|
||||
const Scanlator = new this.Engine();
|
||||
const {List, mangaLink} = await Scanlator.Chapter(this.Link);
|
||||
const {latestChap } = await Scanlator.GetManga(mangaLink);
|
||||
//TODO: Defaults for missing info from the modules;
|
||||
return {List, latestChap,mangaLink, chNum:parseInt(this.chNum), link:this.Link,title:this.title, scanlator:this.scanlator}
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Lib: Chapter: Error:', error)
|
||||
}
|
||||
}
|
||||
async getMangaLink()
|
||||
{
|
||||
await this.initialized;
|
||||
try
|
||||
{
|
||||
const Scanlator = new this.Engine();
|
||||
const {mangaLink} = await Scanlator.Chapter(this.Link);
|
||||
return mangaLink;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Lib: Chapter: Error:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
exports.decodeHTML = (encodedString)=>
|
||||
{
|
||||
return encodedString.replace(/ |&#(\d+);|\n/g, function(match, dec)
|
||||
{
|
||||
if (dec)
|
||||
{
|
||||
return String.fromCharCode(dec); // Replace HTML character codes
|
||||
}
|
||||
else
|
||||
{
|
||||
return ''; // Remove newline characters
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exports.isValidID = (id) =>
|
||||
{
|
||||
return mongoose.Types.ObjectId.isValid(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @argument {Response} res - Response: express response object.
|
||||
* @argument {String} name - Name of the cookie.
|
||||
* @argument {*} value - Value of the cookie.
|
||||
*/
|
||||
exports.Baker = async (res, name, value, maxAge =2) =>
|
||||
{
|
||||
await res.cookie(name, '',{maxAge:0});
|
||||
await res.cookie(name, value, {maxAge:hours(2),path:'/',secure:false,httpOnly:false});
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Use it to transform an hour value into miliseconds.
|
||||
* @param {Number} Hours - Hours to transform
|
||||
*/
|
||||
function hours(Hours)
|
||||
{
|
||||
return Hours*60*60*1000;
|
||||
}
|
||||
exports.Crypto = class Crypto
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} password
|
||||
*/
|
||||
constructor(username, password)
|
||||
{
|
||||
this.salt = '4$3H6Q9B8d4VXcE3Rft4';
|
||||
this.secret = '7xJYrX@KF7oSNr9Zm4UH';
|
||||
this.pepper = String.fromCharCode(this.getRandomInt(65, 90));
|
||||
this.password = password;
|
||||
this.user = username;
|
||||
this.algorithm = 'aes-192-cbc';
|
||||
|
||||
}
|
||||
get Hash() // to use on register
|
||||
{
|
||||
return this.hash()
|
||||
}
|
||||
hash(p)
|
||||
{
|
||||
if(!p)
|
||||
{
|
||||
return crypto.createHmac('sha256', this.secret).update(this.user+this.password+this.salt+this.pepper).digest('hex');
|
||||
}
|
||||
else
|
||||
{
|
||||
return crypto.createHmac('sha256', this.secret).update(this.user+this.password+this.salt+p).digest('hex');
|
||||
}
|
||||
}
|
||||
getRandomInt(min, max)
|
||||
{
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {String} cookie
|
||||
* @param {String} name
|
||||
* @returns
|
||||
*/
|
||||
exports.cookieParser = (cookie, name)=>
|
||||
{
|
||||
return cookie.split(name+'=')[1].split(';')[0];
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
let favorite =
|
||||
new Schema(
|
||||
{
|
||||
scanlator: {type: String, required: true, max: 100},
|
||||
title: {type: String, required: true, max: 100},
|
||||
link:{type: String, require:true, max:100},
|
||||
tags:{type: Array, require:false}
|
||||
}
|
||||
);
|
||||
|
||||
const Favorite = module.exports = mongoose.model('favorite', favorite, 'mangareader');
|
||||
module.exports.get = (callback, limit)=>
|
||||
{
|
||||
Favorite.find(callback).limit(limit);
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
let chapter =
|
||||
new Schema(
|
||||
{
|
||||
scanlator: {type: String, required: true, max: 100},
|
||||
title: {type: String, required: true, max: 100},
|
||||
link:{type: String, require:true, max:100},
|
||||
chapterNum:{type: Number, require:true}
|
||||
}
|
||||
);
|
||||
|
||||
const Chapter = module.exports = mongoose.model('chapter', chapter, 'mangareader');
|
||||
module.exports.get = (callback, limit)=>
|
||||
{
|
||||
Chapter.find(callback).limit(limit);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
let user =
|
||||
new Schema(
|
||||
{
|
||||
username: {type: String, required: true, max: 100},
|
||||
hash: {type: String, required: true, max: 100}
|
||||
}
|
||||
);
|
||||
|
||||
const User = module.exports = mongoose.model('favorite', user, 'mangareader');
|
||||
module.exports.get = (callback, limit)=>
|
||||
{
|
||||
User.find(callback).limit(limit);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"ignore": ["./config.json"]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "masterhcsite",
|
||||
"version": "1.0.0",
|
||||
"description": "masterhcsite",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "nodemon test.js",
|
||||
"start": "node server.js",
|
||||
"devStart": "nodemon --env-file=.env server.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"findit": "^2.0.0",
|
||||
"mongoose": "^8.1.3",
|
||||
"morgan": "^1.10.0",
|
||||
"node-html-parser": "^6.1.12",
|
||||
"nodemon": "^3.0.3",
|
||||
"sharp": "^0.33.3",
|
||||
"three": "^0.161.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,899 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@1,900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto+Slab:100,300,400,700');
|
||||
@import url('https://fonts.googleapis.com/css?family=Raleway:300,300i,400,400i,500,500i,600,600i,700,700i,800,800i,900,900i');
|
||||
:root
|
||||
{
|
||||
|
||||
--purple-heart-50: #f3f1ff;
|
||||
--purple-heart-100: #ebe5ff;
|
||||
--purple-heart-200: #d9ceff;
|
||||
--purple-heart-300: #bea6ff;
|
||||
--purple-heart-400: #9f75ff;
|
||||
--purple-heart-500: #843dff;
|
||||
--purple-heart-600: #7916ff;
|
||||
--purple-heart-700: #6b04fd;
|
||||
--purple-heart-800: #5a03d5;
|
||||
--purple-heart-900: #4b05ad;
|
||||
--purple-heart-950: #2c0076;
|
||||
/* Colors with 25% transparency */
|
||||
--purple-heart-50-25: #f3f1ff40;
|
||||
--purple-heart-100-25: #ebe5ff40;
|
||||
--purple-heart-200-25: #d9ceff40;
|
||||
--purple-heart-300-25: #bea6ff40;
|
||||
--purple-heart-400-25: #9f75ff40;
|
||||
--purple-heart-500-25: #843dff40;
|
||||
--purple-heart-600-25: #7916ff40;
|
||||
--purple-heart-700-25: #6b04fd40;
|
||||
--purple-heart-800-25: #5a03d540;
|
||||
--purple-heart-900-25: #4b05ad40;
|
||||
--purple-heart-950-25: #2c007640;
|
||||
|
||||
/* Colors with 50% transparency */
|
||||
--purple-heart-50-50: #f3f1ff80;
|
||||
--purple-heart-100-50: #ebe5ff80;
|
||||
--purple-heart-200-50: #d9ceff80;
|
||||
--purple-heart-300-50: #bea6ff80;
|
||||
--purple-heart-400-50: #9f75ff80;
|
||||
--purple-heart-500-50: #843dff80;
|
||||
--purple-heart-600-50: #7916ff80;
|
||||
--purple-heart-700-50: #6b04fd80;
|
||||
--purple-heart-800-50: #5a03d580;
|
||||
--purple-heart-900-50: #4b05ad80;
|
||||
--purple-heart-950-50: #2c007680;
|
||||
|
||||
--bg:var(--bgBetter);
|
||||
--bgBetter:linear-gradient(135deg, var(--purple-heart-400) 0%, var(--purple-heart-800) 100%);
|
||||
--lbg:var(--purple-heart-600);
|
||||
--lbgT:var(--purple-heart-600-50);
|
||||
--primary:var(--purple-heart-600);
|
||||
--accent:var(--purple-heart-800);
|
||||
--border: var(--purple-heart-500);
|
||||
--border-hover: var(--purple-heart-300);
|
||||
--text:var(--purple-heart-100);
|
||||
--favorite:var(--purple-heart-950);
|
||||
--completed: var(--purple-heart-400);
|
||||
--dropped: #de3b3b;
|
||||
|
||||
}
|
||||
body
|
||||
{
|
||||
margin:0;
|
||||
height:100%;
|
||||
width:100%;
|
||||
margin: 0 0 0 0;
|
||||
position:absolute;
|
||||
top:0%;
|
||||
left:0%;
|
||||
font-family: 'lato';
|
||||
background-color: var(--bg);
|
||||
background-image: linear-gradient(135deg, var(--purple-heart-400) 0%, var(--purple-heart-950) 100%);;
|
||||
background: -webkit-gradient(linear, left top, right bottom, from(var(--purple-heart-400)), to(var(--purple-heart-950))) fixed;
|
||||
color:var(--text);
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.nav
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
.nav > .button-container
|
||||
{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap:2em;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button-container > .active
|
||||
{
|
||||
color:var(--accent);
|
||||
}
|
||||
.bt
|
||||
{
|
||||
cursor:pointer;
|
||||
}
|
||||
.bt:hover
|
||||
{
|
||||
color:var(--accent)
|
||||
}
|
||||
button
|
||||
{
|
||||
cursor: pointer;
|
||||
width: 10em;
|
||||
border:2px solid var(--border);
|
||||
border-radius: 15px;
|
||||
font-family: 'lato';
|
||||
color: var(--text);
|
||||
background-color: var(--purple-heart-800);
|
||||
}
|
||||
button:hover
|
||||
{
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
#display
|
||||
{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#search
|
||||
{
|
||||
background-color: var(--purple-heart-950);
|
||||
border: 2px solid var(--text);
|
||||
border-radius: 15px;
|
||||
height: 2em;
|
||||
text-align: center;
|
||||
color:var(--text);
|
||||
|
||||
}
|
||||
#search:focus-visible
|
||||
{
|
||||
background-color: var(--purple-heart-950);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 15px;
|
||||
height: 2em;
|
||||
text-align: center;
|
||||
color:var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
transform: translateX(-1em);
|
||||
cursor: pointer;
|
||||
color:var(--accent);
|
||||
font-variation-settings:
|
||||
'FILL' 0,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 24
|
||||
|
||||
}
|
||||
.fill
|
||||
{
|
||||
cursor: pointer;
|
||||
color:var(--favorite);
|
||||
font-variation-settings:
|
||||
'FILL' 1,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 24
|
||||
}
|
||||
.scanlator-container > h2
|
||||
{
|
||||
margin-left: 3em;
|
||||
margin-right: 3em;
|
||||
border-bottom:4px solid var(--border-hover);
|
||||
}
|
||||
.carousel > .buttonContainer
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2em;
|
||||
height: 3em;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
top: -15.5em;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.carousel
|
||||
{
|
||||
height: 25em;
|
||||
align-items: center;
|
||||
width: 90%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cards-container
|
||||
{
|
||||
display: flex;
|
||||
transition: transform 0.5s ease;
|
||||
background-color: var(--purple-heart-800-25);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
height: 100%;
|
||||
gap:2em;
|
||||
}
|
||||
|
||||
.chevron
|
||||
{
|
||||
transform: translateX(0em);
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
font-variation-settings:
|
||||
'FILL' 0,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 24
|
||||
|
||||
}
|
||||
.pullBackUp
|
||||
{
|
||||
position: fixed;
|
||||
right: 2em;
|
||||
bottom: 2em;
|
||||
color:var(--text);
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
.chevronUp
|
||||
{
|
||||
color: var(---text);
|
||||
margin-left: 0.73em;
|
||||
top: -0.07em;
|
||||
position: relative;
|
||||
}
|
||||
.prev-btn, .next-btn
|
||||
{
|
||||
position: relative;
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
background-color: var(--accent);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 50%;
|
||||
height: 3em;
|
||||
width: 3em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
/* align-content: center; */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card
|
||||
{
|
||||
transition: all .4s cubic-bezier(0.175, 0.885, 0, 1);
|
||||
background-color: var(--lbg);
|
||||
min-width: 200px;
|
||||
max-width: 15%;
|
||||
height: 98.5%;
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--border);
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 13px 10px -7px rgba(0, 0, 0,0.1);
|
||||
}
|
||||
|
||||
.bookmark
|
||||
{
|
||||
margin-left: 1em;
|
||||
margin-top: 0.5em;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
.card__img
|
||||
{
|
||||
transition: 0.2s all ease-out;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
height: 75%;
|
||||
border-top-left-radius: 12px;
|
||||
border-top-right-radius: 12px;
|
||||
top: 0;
|
||||
|
||||
}
|
||||
|
||||
.card__info
|
||||
{
|
||||
background-color:var(--lbg);
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card__title {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
font-family: 'lato', serif;
|
||||
height: 2.6em;
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card:hover > .card__info
|
||||
{
|
||||
background-color: var(--lbgT);
|
||||
}
|
||||
.card:hover > .card__img
|
||||
{
|
||||
height: 100%;
|
||||
}
|
||||
.infoCard
|
||||
{
|
||||
height: 15em;
|
||||
width: 80%;
|
||||
display: flex;
|
||||
border: 3px solid var(--border);
|
||||
border-radius: 15px;
|
||||
margin-left: 10%;
|
||||
background-color: var(--lbg);
|
||||
}
|
||||
.infoCard > .infoText
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 1em;
|
||||
}
|
||||
.infoCard > .infoText > h2
|
||||
{
|
||||
margin-top: 0.3em;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
.infoCard > .infoText > .description
|
||||
{
|
||||
height: 8em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.infoCard > img
|
||||
{
|
||||
border-radius: 5px;
|
||||
margin:10px;
|
||||
}
|
||||
.infoCard > .infoText > a
|
||||
{
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
}
|
||||
.infoCard > .infoText > .description
|
||||
{
|
||||
color:var(--text);
|
||||
}
|
||||
.list
|
||||
{
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
border: 3px solid var(--border);
|
||||
background-color: var(--bg);
|
||||
border-radius: 15px;
|
||||
margin-left: 10%;
|
||||
gap:5px;
|
||||
}
|
||||
.list > .chapterButton
|
||||
{
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 15px;
|
||||
background-color: var(--lbg);
|
||||
margin: 5px;
|
||||
width: 130px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chapterButton:hover
|
||||
{
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
.list > .chapterButton > a
|
||||
{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.list > .chapterButton > a > h4 , .list > .chapterButton > a > h5
|
||||
{
|
||||
/* margin-left: 0.6em; */
|
||||
margin-bottom: 0.1em;
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
.infoCard > .infoText > .tags
|
||||
{
|
||||
display:flex;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
align-self: center;
|
||||
gap:5px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.infoCard > .infoText > .tags > .tag
|
||||
{
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 15px;
|
||||
background-color: var(--accent);
|
||||
padding-left: 0.2em;
|
||||
padding-right: .2em;
|
||||
}
|
||||
|
||||
#display > img
|
||||
{
|
||||
width: 60%;
|
||||
}
|
||||
.chapterNav
|
||||
{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.chapterNavButton
|
||||
{
|
||||
background-color: var(--purple-heart-400-50);
|
||||
width: 100px;
|
||||
height: 30px;
|
||||
border: 2px solid var(--accent);
|
||||
border-radius: 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chapterNavButton:hover
|
||||
{
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
.searchCarousel
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
.chapterNavChapterLabel
|
||||
{
|
||||
width: 40%;
|
||||
text-align: center;
|
||||
}
|
||||
.hidden
|
||||
{
|
||||
display: none !important;
|
||||
}
|
||||
.searchBarWrapper
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 2em;
|
||||
}
|
||||
.searchIcon
|
||||
{
|
||||
color:var(--text)
|
||||
}
|
||||
.searchIcon:hover
|
||||
{
|
||||
color:var(--accent)
|
||||
}
|
||||
.progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
background-clip: padding-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress .indeterminate:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background-color: var(--accent);
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
will-change: left, right;
|
||||
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395)
|
||||
infinite;
|
||||
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
|
||||
}
|
||||
.progress .indeterminate:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background-color: var(--accent);
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
will-change: left, right;
|
||||
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
|
||||
infinite;
|
||||
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
|
||||
infinite;
|
||||
-webkit-animation-delay: 1.15s;
|
||||
animation-delay: 1.15s;
|
||||
}
|
||||
.progress {
|
||||
display: none;
|
||||
}
|
||||
.htmx-request .progress {
|
||||
display: inline;
|
||||
}
|
||||
.htmx-request.progress {
|
||||
display: inline;
|
||||
}
|
||||
@-webkit-keyframes indeterminate {
|
||||
0% {
|
||||
left: -35%;
|
||||
right: 100%;
|
||||
}
|
||||
60% {
|
||||
left: 100%;
|
||||
right: -90%;
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
right: -90%;
|
||||
}
|
||||
}
|
||||
@keyframes indeterminate {
|
||||
0% {
|
||||
left: -35%;
|
||||
right: 100%;
|
||||
}
|
||||
60% {
|
||||
left: 100%;
|
||||
right: -90%;
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
right: -90%;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes indeterminate-short {
|
||||
0% {
|
||||
left: -200%;
|
||||
right: 100%;
|
||||
}
|
||||
60% {
|
||||
left: 107%;
|
||||
right: -8%;
|
||||
}
|
||||
100% {
|
||||
left: 107%;
|
||||
right: -8%;
|
||||
}
|
||||
}
|
||||
@keyframes indeterminate-short {
|
||||
0% {
|
||||
left: -200%;
|
||||
right: 100%;
|
||||
}
|
||||
60% {
|
||||
left: 107%;
|
||||
right: -8%;
|
||||
}
|
||||
100% {
|
||||
left: 107%;
|
||||
right: -8%;
|
||||
}
|
||||
}
|
||||
#RecommendTitle
|
||||
{
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
color: var(--purple-heart-50);
|
||||
border-bottom: 3px solid var(--purple-heart-50);
|
||||
width: 80%;
|
||||
left: 10%;
|
||||
position: relative;
|
||||
}
|
||||
.chapterNavChapterLabel
|
||||
{
|
||||
cursor: pointer;
|
||||
}
|
||||
.chapterNavChapterLabel:hover
|
||||
{
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.status
|
||||
{
|
||||
position: absolute;
|
||||
top: 2em;
|
||||
left: -3.5em;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
padding: 0.5em 4em;
|
||||
background: var(--dropped);
|
||||
-ms-transform: rotate(-45deg);
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
.Completed
|
||||
{
|
||||
background: var(--completed);
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 0.6em;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--border-hover)
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background:var(--purple-heart-950-50);
|
||||
border: 0 solid transparent;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background:var(--purple-heart-950) ; /* color of the thumb on hover */
|
||||
}
|
||||
.NoFavs
|
||||
{
|
||||
text-align: center;
|
||||
color: var(--text);
|
||||
font-size: 3em;
|
||||
}
|
||||
.login
|
||||
{
|
||||
color: var(--text);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 15px;
|
||||
background-color: var(--purple-heart-500);
|
||||
height: 2em;
|
||||
width: 4em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top:1em;
|
||||
right: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login:hover
|
||||
{
|
||||
background-color: var(--purple-heart-600);
|
||||
}
|
||||
#configWrapper
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.configForm
|
||||
{
|
||||
background-color: var(--purple-heart-600-50);
|
||||
border-radius: 13px;
|
||||
height:25em;
|
||||
}
|
||||
.configForm section
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.buttonSection
|
||||
{
|
||||
margin: 1em;
|
||||
}
|
||||
#configSubmitBt
|
||||
{
|
||||
height: 2em;
|
||||
}
|
||||
.configForm label, .configForm input
|
||||
{
|
||||
margin: 1em 0 0 1em;
|
||||
width: 30vw;
|
||||
}
|
||||
.configForm::before
|
||||
{
|
||||
content: '';
|
||||
background-color: var(--accent);
|
||||
border-top-left-radius: 13px;
|
||||
border-bottom-left-radius: 13px;
|
||||
z-index: 10;
|
||||
height: 100%;
|
||||
width: 1em;
|
||||
position: relative;
|
||||
float:left;
|
||||
}
|
||||
.configForm input
|
||||
{
|
||||
background-color: var(--purple-heart-950);
|
||||
border: 2px solid var(--text);
|
||||
border-radius: 15px;
|
||||
height: 2em;
|
||||
text-align: center;
|
||||
color:var(--text);
|
||||
margin-right: 2em;
|
||||
|
||||
}
|
||||
.configForm input:focus-visible
|
||||
{
|
||||
background-color: var(--purple-heart-950);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 15px;
|
||||
height: 2em;
|
||||
text-align: center;
|
||||
color:var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.formWrapper
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
.loginForm
|
||||
{
|
||||
background-color: var(--purple-heart-600-50);
|
||||
border-radius: 13px;
|
||||
height:25em;
|
||||
}
|
||||
|
||||
.loginForm section
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.loginForm label, .loginForm input
|
||||
{
|
||||
margin: 1em 0 0 1em;
|
||||
width: 30vw;
|
||||
}
|
||||
.loginForm::before
|
||||
{
|
||||
content: '';
|
||||
background-color: var(--accent);
|
||||
border-top-left-radius: 13px;
|
||||
border-bottom-left-radius: 13px;
|
||||
z-index: 10;
|
||||
height: 100%;
|
||||
width: 1em;
|
||||
position: relative;
|
||||
float:left;
|
||||
}
|
||||
.loginForm input
|
||||
{
|
||||
background-color: var(--purple-heart-950);
|
||||
border: 2px solid var(--text);
|
||||
border-radius: 15px;
|
||||
height: 2em;
|
||||
text-align: center;
|
||||
color:var(--text);
|
||||
margin-right: 2em;
|
||||
|
||||
}
|
||||
.loginForm input:focus-visible
|
||||
{
|
||||
background-color: var(--purple-heart-950);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 15px;
|
||||
height: 2em;
|
||||
text-align: center;
|
||||
color:var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.loginFormFlexBox
|
||||
{
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap:2em;
|
||||
}
|
||||
#loginSubmitBt
|
||||
{
|
||||
height: 2em;
|
||||
}
|
||||
.burgerMenu > span
|
||||
{
|
||||
color:var(--text);
|
||||
margin-left: 2em;
|
||||
}
|
||||
.wrapperReaderBox
|
||||
{
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.spacer
|
||||
{
|
||||
width: 25%;
|
||||
}
|
||||
.chapterOverview
|
||||
{
|
||||
position: fixed;
|
||||
height: 85vh;
|
||||
top: 10vh;
|
||||
width: 1.7vw;
|
||||
background-color: var(--purple-heart-500-50);
|
||||
border: 0 solid;
|
||||
border-radius: 15px;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
}
|
||||
.chapterOverview > .flexBox
|
||||
{
|
||||
display: none;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.chapterOverview > .expand
|
||||
{
|
||||
color:var(--text);
|
||||
border:0 solid transparent;
|
||||
border-top-right-radius: 15px ;
|
||||
border-bottom-right-radius: 15px ;
|
||||
background-color: var(--purple-heart-600);
|
||||
width: 1.65vw;
|
||||
cursor:pointer;
|
||||
|
||||
}
|
||||
.chapterOverview > .expand > span
|
||||
{
|
||||
position: relative;
|
||||
left: 1em;
|
||||
top: 50%;
|
||||
color: var(--text);
|
||||
}
|
||||
.chapterOverview > .expand:hover
|
||||
{
|
||||
background-color: var(--purple-heart-400);
|
||||
|
||||
}
|
||||
.flexBox > img
|
||||
{
|
||||
max-width: 75px;
|
||||
height: auto;
|
||||
border: 0 solid transparent;
|
||||
border-radius: 5px;
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
function triggerInputClick() {
|
||||
var inputElement = document.getElementById('search');
|
||||
inputElement.click();
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
const router = require('express').Router();
|
||||
const api = require('../controllers/api.js');
|
||||
|
||||
module.exports = (worker)=>
|
||||
{
|
||||
router.route('*').get(api.hasConfig);
|
||||
router.route('/config').post(api.configPost);
|
||||
|
||||
router.route('/').get( api.home);
|
||||
router.route('/home').get(api.home);
|
||||
router.route('/search').get(api.search)
|
||||
.post(api.search);
|
||||
router.route('/searchbar').get(api.searchBar);
|
||||
router.route('/recommended').get(api.recommended);
|
||||
router.route('/favorites').get(api.favorites);
|
||||
router.route('/bookmark/:scanlator/:title/:idorLink?').post(api.bookmark(worker));
|
||||
router.route('/manga/:scanlator/:link/:title').get(api.manga);
|
||||
router.route('/chapter/:scanlator/:link/:title/:chapter').get(api.chapter);
|
||||
router.route('/chapternavinfo/:scanlator/:mangaLink/:title/:chapter').get(api.chapterNavInfo);
|
||||
router.route('/dashboard').get(api.dashboard);
|
||||
router.route('/login').get(api.loginPage)
|
||||
.post(api.login);
|
||||
router.route('/logout').get(api.logout);
|
||||
router.route('/chapterRead/:scanlator/:link/:title/:chapter').post(api.chapterRead);
|
||||
|
||||
|
||||
//Undefined Routes v
|
||||
router.use(api.errorPage);
|
||||
return router;
|
||||
};
|
@ -0,0 +1,26 @@
|
||||
const express = require('express');
|
||||
var app = express();
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const mongoose = require('mongoose');
|
||||
const morgan = require('morgan')
|
||||
const mongoURI = process.env.DATABASE_URL;
|
||||
const Spawner = require('child_process')
|
||||
|
||||
mongoose.connect(mongoURI);
|
||||
mongoose.connection.on('connected', ()=>{console.log('MongoDB - Connected')})
|
||||
.on('disconnected', ()=>{console.log('MongoDB - Disconnect')})
|
||||
.on('error', (error)=>console.log('Mongoose Error:', error));
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
app.use(morgan('dev'))
|
||||
app.set("view engine", "ejs");
|
||||
app.use(require('cors')())
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({extended:true}));
|
||||
const server = http.createServer(app);
|
||||
server.listen(process.env.PORT, async () =>
|
||||
{
|
||||
const worker = Spawner.fork('./downloader.js', [mongoURI]);
|
||||
app.use('/', require('./routes/routes')(worker));
|
||||
console.log(`Http - Server UP`);
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
const {SearchByTag, Search, Manga, Modules, cookieParser} = require('./lib');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const mongoose = require('mongoose')
|
||||
const FavoriteModel = require('./models/favorite.js');
|
||||
const mongoURI = 'mongodb://mongoadmin:something@192.168.71.137:27017';
|
||||
|
||||
mongoose.connect(mongoURI);
|
||||
mongoose.connection.on('connected', ()=>{console.log('MongoDB - Connected')})
|
||||
.on('disconnected', ()=>{console.log('MongoDB - Disconnect')})
|
||||
.on('error', (error)=>console.log('Mongoose Error:', error));
|
||||
|
||||
|
||||
|
||||
(async ()=>
|
||||
{
|
||||
const exists = await new Modules('Asura-scans').titleExists('Absolute Necromancer');
|
||||
const parsed = cookieParser('hash=e941d5c52a2f5aca8237ef40a46749d8619c213683d080f753c5d86f314ae270;coiso=coiso', 'coiso');
|
||||
console.log(parsed)
|
||||
console.log({exists})
|
||||
})()
|
@ -0,0 +1,229 @@
|
||||
const mongoose = require('mongoose');
|
||||
const mongoURI = 'mongodb://mongoadmin:something@192.168.71.137:27017';
|
||||
|
||||
mongoose.connect(mongoURI);
|
||||
mongoose.connection.on('connected', ()=>{console.log('MongoDB - Connected')})
|
||||
.on('disconnected', ()=>{console.log('MongoDB - Disconnect')})
|
||||
.on('error', (error)=>console.log('Mongoose Error:', error));
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { Stream } = require('stream');
|
||||
const {Manga, Chapter} = require('./lib');
|
||||
const FavModel = require('./models/favorite')
|
||||
const EventEmitter = require('events');
|
||||
const Emitter = new EventEmitter();
|
||||
const BaseDir = './public/Saved'
|
||||
|
||||
var Queue = [];
|
||||
var slots = 2;
|
||||
updateQueue();
|
||||
var queueUpdateTimer = setInterval(() => {
|
||||
updateQueue();
|
||||
}, 5*60*1000);
|
||||
|
||||
|
||||
process.on('message', m=>
|
||||
{
|
||||
if(m.action==='add')
|
||||
{
|
||||
clearInterval(queueUpdateTimer);
|
||||
updateQueue();
|
||||
queueUpdateTimer = setInterval(() => {
|
||||
updateQueue();
|
||||
}, 5*60*1000);
|
||||
return
|
||||
}
|
||||
|
||||
if(m.action==='remove') return removeFromQueue(m.id)
|
||||
})
|
||||
function removeFromQueue(id)
|
||||
{
|
||||
Queue = Queue.filter(manga => manga._id !== id);
|
||||
}
|
||||
Emitter.on('queueUpdate', ()=>
|
||||
{
|
||||
if(!Queue[0]) return
|
||||
if(!Queue[0].inProgress)
|
||||
{
|
||||
Queue[0].inProgress = true;
|
||||
download(Queue[0])
|
||||
}
|
||||
} );
|
||||
|
||||
async function updateQueue()
|
||||
{
|
||||
const Scanlators = fs.readdirSync(BaseDir);
|
||||
const Favs = await FavModel.find();
|
||||
let Mangas = [];
|
||||
for(let i =0; i<Scanlators.length; i++)
|
||||
{
|
||||
let mangas = fs.readdirSync(path.join(BaseDir, Scanlators[i]));
|
||||
if(mangas.length < 0) continue
|
||||
for(var j = 0; j<mangas.length; j++)
|
||||
{
|
||||
let chapters = fs.readdirSync(path.join(BaseDir, Scanlators[i], mangas[j]));
|
||||
if(chapters.length==0) continue
|
||||
chapters.sort((a, b) => { return parseInt(a.split('_')[1]) - parseInt(b.split('_')[1]) });
|
||||
let latestDownloaded = parseInt(chapters[chapters.length-1].split('_')[1]);
|
||||
let manga = {scanlator:Scanlators[i], title:mangas[j], latestDownloaded}
|
||||
Mangas.push(manga)
|
||||
}
|
||||
}
|
||||
let FavedAndDownloaded = [];
|
||||
let FavedButNotDownloaded = null;
|
||||
for (let i = 0; i < Mangas.length; i++)
|
||||
{
|
||||
FavedAndDownloaded = FavedAndDownloaded.concat(Favs.filter(item =>
|
||||
{
|
||||
return (item.scanlator === Mangas[i].scanlator && item.title === Mangas[i].title);
|
||||
}).map((matchedItem) =>{
|
||||
return ({
|
||||
...matchedItem._doc,
|
||||
latestDownloaded: Mangas[i].latestDownloaded
|
||||
})
|
||||
})
|
||||
);
|
||||
}
|
||||
FavedButNotDownloaded = Favs.filter(fav =>
|
||||
{
|
||||
// return !FavedAndDownloaded.includes(fav)
|
||||
return !FavedAndDownloaded.some(fad => fad._id === fav._id);
|
||||
});
|
||||
if(FavedButNotDownloaded)
|
||||
{
|
||||
for(let i = 0; i < FavedButNotDownloaded.length; i++)
|
||||
{
|
||||
(async(i)=>
|
||||
{
|
||||
const {title, link, scanlator, _id} = FavedButNotDownloaded[i];
|
||||
const manga = await new Manga(scanlator, link, title).get();
|
||||
let id = _id.toString();
|
||||
addToQueue(manga, id);
|
||||
})(i)
|
||||
}
|
||||
}
|
||||
if(FavedAndDownloaded)
|
||||
{
|
||||
for(let i = 0; i<FavedAndDownloaded.length; i++)
|
||||
{
|
||||
(async(i)=>
|
||||
{
|
||||
const {title, link, scanlator, latestDownloaded, _id} = FavedAndDownloaded[i];
|
||||
const manga = await new Manga(scanlator, link, title).get();
|
||||
let id = _id.toString();
|
||||
checkForMissingFiles(manga)
|
||||
if(latestDownloaded<manga.latestChap) addToQueue(manga, id, latestDownloaded);
|
||||
})(i);
|
||||
}
|
||||
}
|
||||
function addToQueue(manga, _id,startFrom=0)
|
||||
{
|
||||
const List = manga.List.filter(item => item.num > startFrom);
|
||||
Queue.push({...manga, _id, List, startFrom})
|
||||
Emitter.emit('queueUpdate', '');
|
||||
}
|
||||
}
|
||||
async function checkForMissingFiles(manga)
|
||||
{
|
||||
const {scanlator, title, List} = manga;
|
||||
let auxList = List.sort((a,b)=>{return a.num - b.num})
|
||||
for(var i = 0; i<auxList.length; i++)
|
||||
{
|
||||
const {link} = auxList[i];
|
||||
const destPath = path.join(BaseDir, scanlator, title, 'CH_'+(i+1));
|
||||
const chapterDownloaded = fs.existsSync(destPath);
|
||||
if(!chapterDownloaded) continue;
|
||||
const chapterDir = fs.readdirSync(destPath);
|
||||
const chapter = await new Chapter(scanlator, link, title, i+1).get();
|
||||
let auxChapterDir = chapterDir.map(item=>{return item.split('.')[0]})
|
||||
var auxChapterList = chapter.List.map(item=>{return item.split('/')[item.split('/').length-1].split('.')[0]})
|
||||
const missingImages = auxChapterList.filter(item=>!auxChapterDir.includes(item));
|
||||
if(missingImages.length == 0) continue;
|
||||
for(var j = 0; j<missingImages.length; j++)
|
||||
{
|
||||
const imageLink = chapter.List.filter(item=>item.split('/')[item.split('/').length-1].split('.')[0] == missingImages[j])[0]
|
||||
const destination = `${destPath}/${imageLink.split('/')[(imageLink.split('/').length - 1)]}`
|
||||
const download = await downloadImage(imageLink,destination, 5);
|
||||
console.log('MissingImage:',scanlator, title, 'chapter',i+1, download.status)
|
||||
}
|
||||
}
|
||||
}
|
||||
async function download(manga)
|
||||
{
|
||||
if(slots <=0) return
|
||||
slots--;
|
||||
const { scanlator, title} = manga;
|
||||
const List = manga.List.sort((a, b) => { return a.num - b.num });
|
||||
for (let i = 0; i < List.length; i++)
|
||||
{
|
||||
const ch = List[i];
|
||||
if (isNaN(ch.num)) continue;
|
||||
const { num, link } = ch;
|
||||
const chapter = await new Chapter(scanlator, link, title, num).get();
|
||||
// console.log(chapter)
|
||||
if (!chapter?.List?.length) continue;
|
||||
await downloadChapter(chapter);
|
||||
}
|
||||
Queue.shift();
|
||||
slots++;
|
||||
Emitter.emit('queueUpdate', '');
|
||||
}
|
||||
async function downloadChapter(chapter)
|
||||
{
|
||||
const {List, scanlator, title, chNum } = chapter;
|
||||
const images = List;
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
const destination = `./public/Saved/${scanlator}/${title}/CH_${chNum}/${img.split('/')[(img.split('/').length - 1)]}`;
|
||||
try {
|
||||
const downloaded = await downloadImage(img, destination);
|
||||
console.log(`Downloaded: Scanlator:${scanlator}; Manga: ${title} Chapter:${chNum}; IMG:${img.split('/')[(img.split('/').length - 1)]} ${downloaded.status}`);
|
||||
} catch (error) {
|
||||
console.error(`Error downloading ${img}: ${error}`);
|
||||
}
|
||||
}
|
||||
return {chNum}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Link} url
|
||||
* @param {Path} destPath
|
||||
* @param {Number} maxRetries
|
||||
* @returns {Download}
|
||||
* @typedef {String} Link
|
||||
* @typedef {Object} Download
|
||||
* @property {String} status
|
||||
* @property {Path} file
|
||||
*/
|
||||
async function downloadImage(url, destPath, maxRetries = 3)
|
||||
{
|
||||
let retries = 0;
|
||||
while (retries < maxRetries) {
|
||||
try
|
||||
{
|
||||
const response = await fetch(url);
|
||||
const dir = path.dirname(destPath);
|
||||
if (!response.ok) throw new Error(`Failed to download image. Status Code: ${response.status}`);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
const fileStream = fs.createWriteStream(destPath);
|
||||
await new Promise((resolve, reject) =>
|
||||
{
|
||||
const stream = Stream.Readable.fromWeb(response.body).pipe(fileStream);
|
||||
stream.on('error', reject);
|
||||
fileStream.on('finish', () => {
|
||||
fileStream.close();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return {status:'Downloaded', file:destPath}
|
||||
} catch (error) {
|
||||
console.error(`Error downloading image: ${destPath}, Retry ${retries + 1}/${maxRetries}`);//, error);
|
||||
retries++;
|
||||
if(retries == maxRetries)
|
||||
return {status:'Failed', file:destPath}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<span class="material-symbols-outlined bookmark" hx-post="/bookmark/<%=scanlator%>/<%=title%>/<%= link %>" hx-trigger="click" hx-target="this" hx-swap="outerHTML">
|
||||
bookmark
|
||||
</span>
|
||||
|
@ -0,0 +1,3 @@
|
||||
<span class="material-symbols-outlined bookmark fill " hx-post="/bookmark/<%=scanlator%>/<%=title%>/<%= id %> " hx-trigger="click" hx-target="this"hx-swap="outerHTML">
|
||||
bookmark
|
||||
</span>
|
@ -0,0 +1,10 @@
|
||||
<div class="chapterButton">
|
||||
<a hx-get="/chapter/<%=scanlator %>/<%=encodeURIComponent(chapter.link)%> /<%= title %>/<%= chapter.num %>" hx-trigger="click" hx-target="#container" hx-indicator=".progress">
|
||||
<h4 class="chapterNum">
|
||||
Chapter <%= chapter.num %>
|
||||
</h4>
|
||||
<h5 class="date">
|
||||
<%= chapter.date %>
|
||||
</h5>
|
||||
</a>
|
||||
</div>
|
@ -0,0 +1,15 @@
|
||||
<div class="chapterNav">
|
||||
<% if(data.chapterNum!=1){%>
|
||||
<div class="chapterNavButton" hx-get="/chapter/<%=data.scanlator %>/<%= encodeURIComponent(data.prevChapterLink) %>/<%= data.title %>/<%= data.chapterNum-1 %>" hx-trigger="click" hx-target="#container">
|
||||
<h3 class="prev">Prev</h3>
|
||||
</div>
|
||||
<% } %>
|
||||
<h1 class="chapterNavChapterLabel"
|
||||
hx-get="/manga/<%=data.scanlator %>/<%= encodeURIComponent(data.mangaLink) %>/<%= data.title %>" hx-trigger="click" hx-target="#container"
|
||||
> <%= data.title %> - Chapter: <%= data.chapterNum %> </h1>
|
||||
<% if(data.chapterNum<data.latestChap){%>
|
||||
<div class="chapterNavButton" hx-get="/chapter/<%=data.scanlator %>/<%= encodeURIComponent(data.nextChapterLink) %>/<%= data.title %>/<%= data.chapterNum+1 %>" hx-trigger="click" hx-target="#container">
|
||||
<h3 class="prev">Next</h3>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
@ -0,0 +1,137 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="icon" type="image/ico" href="images/favicon.ico"/>
|
||||
<title>Manga Reader</title>
|
||||
<link rel="stylesheet" type="text/css" href="/css/home.css" id="pagesheet"/>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
||||
<script type="module" src="/js/home.js"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="progress" style="height: 3px; background-color: white;">
|
||||
<div class="indeterminate" style="background-color: red;"></div>
|
||||
</div>
|
||||
<div id="configWrapper">
|
||||
<form class="configForm" id="configForm" action="/config" method="post" >
|
||||
<section class="usernameSection">
|
||||
<!-- <div class="marker"></div> -->
|
||||
<label for="username" class="username">Username:</label>
|
||||
<input type="text" name="username" id="username" class="formInput" required>
|
||||
</section>
|
||||
<section class="passwordSection">
|
||||
<!-- <div class="marker"></div> -->
|
||||
<label for="password" class="password">Password:</label>
|
||||
<input type="password" name="password" id="password"class="formInput" required>
|
||||
<label for="password2" class="password">Confirm password:</label>
|
||||
<input type="password" name="password2"id="password2" class="formInput" required>
|
||||
</section>
|
||||
<section class="folderPathSection">
|
||||
<!-- <div class="marker"></div> -->
|
||||
<label for="saveFolder" class="saveFolder">Save folder Path: </label>
|
||||
<input type="text" name="saveFolder" id="saveFolder" class="formInput" list="dirs" placeholder="Can be left blank if default is fine">
|
||||
<datalist id="dirs">
|
||||
<% data.forEach(dir=>{ %>
|
||||
<option value="<%= dir %>">
|
||||
<%}) %>
|
||||
</datalist>
|
||||
</section>
|
||||
<section class="buttonSection">
|
||||
<button id="configSubmitBt" type="submit">Configure</button>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
const regex = /^[a-zA-Z]+$/;
|
||||
const Errors = {
|
||||
Mismatch:'Passwords don\'t match.',
|
||||
Short:'Password is too short. (min 10 characters)',
|
||||
Arguments:'Password must have special characters.',
|
||||
}
|
||||
function handler(event)
|
||||
{
|
||||
let passwordInput = document.getElementById('password');
|
||||
let password2Input = document.getElementById('password2');
|
||||
let password = passwordInput.value;
|
||||
let password2 = password2Input.value;
|
||||
let userInput = document.getElementById('username');
|
||||
let user = userInput.value;
|
||||
resetValidity([passwordInput, password2Input, userInput])
|
||||
console.log('Chec')
|
||||
if(user.length < 4) return userInput.setCustomValidity(Errors.Short);
|
||||
if(password.length < 10) return passwordInput.setCustomValidity(Errors.Short);
|
||||
if(password!=password2) return password2Input.setCustomValidity(Errors.Mismatch);
|
||||
if(regex.test(password)) return passwordInput.setCustomValidity(Errors.Arguments);
|
||||
}
|
||||
function resetValidity(Elements)
|
||||
{
|
||||
for(var element of Elements)
|
||||
{
|
||||
element.setCustomValidity('');
|
||||
}
|
||||
}
|
||||
document.getElementById('username').addEventListener('keyup', handler);
|
||||
document.getElementById('password').addEventListener('keyup', handler);
|
||||
document.getElementById('password2').addEventListener('keyup', handler);
|
||||
document.getElementById('saveFolder').addEventListener('keyup', handler);
|
||||
input.onfocus = function () {
|
||||
browsers.style.display = 'block';
|
||||
input.style.borderRadius = "5px 5px 0 0";
|
||||
};
|
||||
for (let option of browsers.options) {
|
||||
option.onclick = function () {
|
||||
input.value = option.value;
|
||||
browsers.style.display = 'none';
|
||||
input.style.borderRadius = "5px";
|
||||
}
|
||||
};
|
||||
|
||||
input.oninput = function() {
|
||||
currentFocus = -1;
|
||||
var text = input.value.toUpperCase();
|
||||
for (let option of browsers.options) {
|
||||
if(option.value.toUpperCase().indexOf(text) > -1){
|
||||
option.style.display = "block";
|
||||
}else{
|
||||
option.style.display = "none";
|
||||
}
|
||||
};
|
||||
}
|
||||
var currentFocus = -1;
|
||||
input.onkeydown = function(e) {
|
||||
if(e.keyCode == 40){
|
||||
currentFocus++
|
||||
addActive(browsers.options);
|
||||
}
|
||||
else if(e.keyCode == 38){
|
||||
currentFocus--
|
||||
addActive(browsers.options);
|
||||
}
|
||||
else if(e.keyCode == 13){
|
||||
e.preventDefault();
|
||||
if (currentFocus > -1) {
|
||||
/*and simulate a click on the "active" item:*/
|
||||
if (browsers.options) browsers.options[currentFocus].click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addActive(x) {
|
||||
if (!x) return false;
|
||||
removeActive(x);
|
||||
if (currentFocus >= x.length) currentFocus = 0;
|
||||
if (currentFocus < 0) currentFocus = (x.length - 1);
|
||||
x[currentFocus].classList.add("active");
|
||||
}
|
||||
function removeActive(x) {
|
||||
for (var i = 0; i < x.length; i++) {
|
||||
x[i].classList.remove("active");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</html>
|
@ -0,0 +1,169 @@
|
||||
|
||||
<div class="chapterOverview" id="chapterOverview">
|
||||
<div class="flexBox" id="flexBox">
|
||||
<% for(var img of data.List){ %>
|
||||
<img class="overview" src=<%= img.replaceAll(' ','%20') %> />
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="expand" onclick="toggle()">
|
||||
<span class="material-symbols-outlined" id="expandChevron">chevron_left</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapperReaderBox">
|
||||
<div class="spacer" id="spacer"></div>
|
||||
<div class="readerDisplay">
|
||||
|
||||
<div class="chapterNav"
|
||||
hx-get="/chapternavinfo/<%= data.scanlator %>/<%= encodeURIComponent(data.mangaLink)%>/<%= data.title %>/<%= data.chapterNum %>"
|
||||
hx-trigger="load"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator=".progress"
|
||||
>
|
||||
</div>
|
||||
<button class="pullBackUp" onclick="pullUp()"> <span class="material-symbols-outlined chevronUp">expand_less</span></button>
|
||||
<div id="display">
|
||||
<% for(var img of data.List){ %>
|
||||
<img src=<%= img.replaceAll(' ','%20') %> />
|
||||
<% } %>
|
||||
<div id="last-image"
|
||||
hx-post="/chapterRead/<%= data.scanlator %>/<%= encodeURIComponent(data.mangaLink)%>/<%= data.title %>/<%= data.chapterNum %>"
|
||||
hx-swap="none"
|
||||
></div>
|
||||
</div>
|
||||
<div class="chapterNav "
|
||||
hx-get="/chapternavinfo/<%= data.scanlator %>/<%= encodeURIComponent(data.mangaLink)%>/<%= data.title %>/<%= data.chapterNum %>"
|
||||
hx-trigger="load"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator=".progress"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
clearVariables();
|
||||
function pullUp()
|
||||
{
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
window.addEventListener('scroll', function() {
|
||||
var elements = document.querySelectorAll('.your-element-class'); // Replace '.your-element-class' with the class of the elements you want to check
|
||||
var windowHeight = window.innerHeight;
|
||||
|
||||
elements.forEach(function(element) {
|
||||
var bounding = element.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
bounding.top >= 0 &&
|
||||
bounding.bottom <= windowHeight
|
||||
) {
|
||||
// Element is in view
|
||||
console.log(element.textContent + ' is in view.');
|
||||
} else {
|
||||
// Element is not in view
|
||||
console.log(element.textContent + ' is not in view.');
|
||||
}
|
||||
});
|
||||
});
|
||||
function toggle()
|
||||
{
|
||||
let flexBox = document.getElementById('flexBox');
|
||||
if( flexBox.style.display!='none') return closePreview();
|
||||
return openPreview();
|
||||
|
||||
}
|
||||
function openPreview()
|
||||
{
|
||||
let flexBox = document.getElementById('flexBox');
|
||||
let chapterOverview = document.getElementById('chapterOverview');
|
||||
let chevron = document.getElementById('expandChevron');
|
||||
let spacer = document.getElementById('spacer');
|
||||
flexBox.style.display='flex';
|
||||
chevron.innerText='chevron_left';
|
||||
chapterOverview.style.width = '25vw';
|
||||
spacer.style.width = '25%';
|
||||
setCookie("preview", "preview", 1)
|
||||
}
|
||||
|
||||
function closePreview()
|
||||
{
|
||||
let flexBox = document.getElementById('flexBox');
|
||||
let chapterOverview = document.getElementById('chapterOverview');
|
||||
let chevron = document.getElementById('expandChevron');
|
||||
let spacer = document.getElementById('spacer');
|
||||
flexBox.style.display = 'none';
|
||||
chevron.innerText='chevron_right';
|
||||
chapterOverview.style.width = '1vw';
|
||||
spacer.style.width = '0%';
|
||||
setCookie("preview", "dontpreview", 1)
|
||||
}
|
||||
function setCookie(name, value, days)
|
||||
{
|
||||
var expires = "";
|
||||
if (days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
expires = "; expires=" + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||
}
|
||||
function getCookie(name)
|
||||
{
|
||||
if(!document.cookie) return '';
|
||||
return document.cookie.split(name+'=')[1].split(';')[0];
|
||||
}
|
||||
function clearVariables()
|
||||
{
|
||||
[ 'lastImage',
|
||||
'observer',
|
||||
'flexBox',
|
||||
'chapterOverview',
|
||||
'chevron',
|
||||
'spacer',
|
||||
'preview',
|
||||
].forEach((variable) =>
|
||||
{
|
||||
if (window.hasOwnProperty(variable))
|
||||
{
|
||||
delete window[variable];
|
||||
}
|
||||
});
|
||||
}
|
||||
function hasFinished()
|
||||
{
|
||||
const lastImage = document.getElementById('last-image');
|
||||
|
||||
const observer = new IntersectionObserver(entries =>
|
||||
{
|
||||
if (entries[0].isIntersecting)
|
||||
{
|
||||
lastImage.click();
|
||||
}
|
||||
});
|
||||
observer.observe(lastImage);
|
||||
}
|
||||
function checkPreview()
|
||||
{
|
||||
let preview = getCookie('preview');
|
||||
console.log(preview, preview == 'preview')
|
||||
if(preview=='preview') return openPreview();
|
||||
return closePreview();
|
||||
}
|
||||
function imageInView()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
hasFinished();
|
||||
imageInView();
|
||||
checkPreview();
|
||||
} catch (error) {
|
||||
clearVariables();
|
||||
hasFinished();
|
||||
imageInView();
|
||||
checkPreview();
|
||||
}
|
||||
</script>
|
@ -0,0 +1,17 @@
|
||||
<% if(!isEmpty){%>
|
||||
<% mangas.forEach(scanlator=>{ %>
|
||||
<div class="scanlator-container">
|
||||
<h2><%= scanlator.scanlator %></h2>
|
||||
<br>
|
||||
<div class="searchCarousel" >
|
||||
<%- include('searchCarousel.ejs', {Results:scanlator.Results, scan:scanlator} )%>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
<% if(isEmpty){ %>
|
||||
<h2 class="NoFavs">
|
||||
You haven't favorited any manga/manhwa!
|
||||
</h2>
|
||||
<% } %>
|
||||
|
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="icon" type="image/ico" href="images/favicon.ico"/>
|
||||
<title>Manga Reader</title>
|
||||
<link rel="stylesheet" type="text/css" href="/css/home.css" id="pagesheet"/>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
||||
<script type="module" src="/js/home.js"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="progress" style="height: 3px; background-color: white;">
|
||||
<div class="indeterminate" style="background-color: red;"></div>
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<%- include('navBar.ejs', {data}) %>
|
||||
<div class="container" id="container" >
|
||||
<h2 id="RecommendTitle">Recommended</h2>
|
||||
<div class="recommended" hx-get="/recommended" hx-trigger="load" hx-indicator=".progress"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -0,0 +1,21 @@
|
||||
<br>
|
||||
<br>
|
||||
<div class="formWrapper">
|
||||
<form class="loginForm" id="loginForm" action="/login" method="post" >
|
||||
<div class="loginFormFlexBox">
|
||||
<section class="usernameSection">
|
||||
<!-- <div class="marker"></div> -->
|
||||
<label for="username" class="username">Username:</label>
|
||||
<input type="text" name="username" id="username" class="formInput" required>
|
||||
</section>
|
||||
<section class="passwordSection">
|
||||
<!-- <div class="marker"></div> -->
|
||||
<label for="password" class="password">Password:</label>
|
||||
<input type="password" name="password" id="password"class="formInput" required>
|
||||
</section>
|
||||
<section class="buttonSection">
|
||||
<button id="loginSubmitBt" type="submit">Login</button>
|
||||
</section>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,26 @@
|
||||
<div class="infoCard">
|
||||
<img src="<%=data.img%> " alt="<%= data.title %> Cover Image">
|
||||
<div class="infoText">
|
||||
<a href="<%=data.link%>" target="_blank"></a>
|
||||
<% if(data.Status=='Ongoing'){%>
|
||||
<h2 class="title">🟢<%=data.title %> </h2>
|
||||
<% }else{ %>
|
||||
<h2 class="title">🔴<%=data.title %> </h2>
|
||||
<% } %>
|
||||
</a>
|
||||
<p class="description"> <%= data.description %> </p>
|
||||
<div class="tags">
|
||||
<% data.tags.forEach(tag=>{ %>
|
||||
<div class="tag">
|
||||
<%=tag %>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="list">
|
||||
<% data.List.forEach(chapter=>{%>
|
||||
<%- include('chapterButton.ejs', {chapter, scanlator:data.scanlator, title:data.title}) %>
|
||||
<% }) %>
|
||||
</div>
|
@ -0,0 +1,49 @@
|
||||
|
||||
<div class="nav">
|
||||
<div class="button-container" id="button-container" >
|
||||
<div class="bt first"
|
||||
hx-get="/home"
|
||||
hx-select="#container"
|
||||
hx-indicator=".progress"
|
||||
hx-on:click="resetSearch()"
|
||||
hx-target="#container"
|
||||
>
|
||||
<h1 class="bt-inner">Home</h1>
|
||||
</div>
|
||||
<div class="bt"
|
||||
hx-get="/favorites"
|
||||
hx-target="#container"
|
||||
hx-on:click="resetSearch()"
|
||||
hx-indicator=".progress"
|
||||
>
|
||||
<h1 class="bt-inner">Collection</h1>
|
||||
</div>
|
||||
<%- include('searchBar.ejs') %>
|
||||
</div>
|
||||
<% if(!data){%>
|
||||
<button class="login"
|
||||
hx-get="/login"
|
||||
hx-indicator=".progress"
|
||||
hx-target="#container"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<% } else {%>
|
||||
<button class="login burgerMenu"
|
||||
hx-get="/dashboard"
|
||||
hx-indicator=".progress"
|
||||
hx-target="#container"
|
||||
>
|
||||
<span class="material-symbols-outlined">
|
||||
menu
|
||||
</span>
|
||||
</button>
|
||||
<% } %>
|
||||
</div>
|
||||
<script>
|
||||
function resetSearch()
|
||||
{
|
||||
let search = document.getElementById('search');
|
||||
search.value = '';
|
||||
}
|
||||
</script>
|
@ -0,0 +1,18 @@
|
||||
<div class="searchBarWrapper" id="searchBarWrapper">
|
||||
<input name="searchstring"
|
||||
hx-post="/search"
|
||||
hx-trigger="keyup[keyCode==13]"
|
||||
hx-indicator=".progress"
|
||||
hx-target="#container"
|
||||
id="search"
|
||||
placeholder="Search">
|
||||
</input>
|
||||
<span class="material-symbols-outlined searchIcon" onclick="triggerInputClick()">search</span>
|
||||
</div>
|
||||
<script>
|
||||
function triggerInputClick() {
|
||||
var inputElement = document.getElementById('search');
|
||||
var enterEvent = new KeyboardEvent('keyup', {keyCode: 13});
|
||||
inputElement.dispatchEvent(enterEvent);
|
||||
}
|
||||
</script>
|
@ -0,0 +1,20 @@
|
||||
<article class="card">
|
||||
<% let scanlatorID = scan.scanlator.replace('-', '') %>
|
||||
<div class="card__img img_<%=scanlatorID %>" style="background-image: url(<%=result.img%>)">
|
||||
<% if(result.favorite){%>
|
||||
<%-include('bookmarked.ejs', {scanlator, title:result.title, link:encodeURIComponent(result.link), id:result.favorite}) %>
|
||||
<% } else { %>
|
||||
<%-include('bookmark.ejs', {scanlator, title:result.title, link:encodeURIComponent(result.link)}) %>
|
||||
<% } %>
|
||||
<%- include('status.ejs', {status:result.Status}) %>
|
||||
</div>
|
||||
<div
|
||||
class="card__info"
|
||||
hx-get="/manga/<%= scanlator %>/<%=result.link?encodeURIComponent(result.link):'nolink'%>/<%= result.title %>"
|
||||
hx-target="#container"
|
||||
hx-trigger="click"
|
||||
>
|
||||
<h3 class="card__title"><%= result.title %> </h3>
|
||||
<p> Latest Chapter: <%= result.latestChap %> </p>
|
||||
</div>
|
||||
</article>
|
@ -0,0 +1,112 @@
|
||||
<div class="carousel">
|
||||
<% let scanlatorID = scan.scanlator.replace('-', '') %>
|
||||
<div class="cards-container cards-container_<%=scanlatorID%>">
|
||||
<% scan.Results.forEach(result=>{%>
|
||||
<%- include('searchCards.ejs', {result, scanlator:scan.scanlator}) %>
|
||||
<% }) %>
|
||||
</div>
|
||||
<div class="buttonContainer" id="buttonContainer<%= scanlatorID %>">
|
||||
<button class="prev-btn" onclick="handlePrev_<%= scanlatorID %>()"><span class="material-symbols-outlined chevron">
|
||||
chevron_left
|
||||
</span></button>
|
||||
<button class="next-btn" onclick="handleNext_<%= scanlatorID %>()"><span class="material-symbols-outlined chevron">
|
||||
chevron_right
|
||||
</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
clearVariables_<%= scanlatorID %>();
|
||||
needsChevrons_<%= scanlatorID %>();
|
||||
|
||||
function clearVariables_<%= scanlatorID %>()
|
||||
{
|
||||
['cardsContainer_<%= scanlatorID %>',
|
||||
'cardsAmount_<%= scanlatorID %>',
|
||||
'cardWidth_<%= scanlatorID %>',
|
||||
'currentPosition_<%= scanlatorID %>',
|
||||
'maxPosition_<%= scanlatorID %>',
|
||||
'imageAmount_<%= scanlatorID %>',
|
||||
'imageOffSetWith_<%= scanlatorID %>',
|
||||
'carouselWidth_<%= scanlatorID %>',
|
||||
'gap_<%= scanlatorID %>',
|
||||
'cards_<%= scanlatorID %>',
|
||||
'lastCard_<%= scanlatorID %>',
|
||||
'lastCardBoundingRect_<%= scanlatorID %>',
|
||||
'nextButtonBoudingRect_<%= scanlatorID %>'].forEach((variable) =>
|
||||
{
|
||||
if (window.hasOwnProperty(variable))
|
||||
{
|
||||
delete window[variable];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
// Initializing variables
|
||||
try {
|
||||
var cardAmount_<%= scanlatorID %> = document.querySelectorAll('.cards-container_<%=scanlatorID %> .card').length - 3;
|
||||
var cardWidth_<%= scanlatorID %> = document.querySelector('.cards-container_<%=scanlatorID %> .card').offsetWidth;
|
||||
var currentPosition_<%= scanlatorID %> = 0;
|
||||
var maxPosition_<%= scanlatorID %> = - cardAmount_<%= scanlatorID %>;
|
||||
|
||||
|
||||
} catch (error) {
|
||||
clearVariables_<%= scanlatorID %>();
|
||||
var cardAmount_<%= scanlatorID %> = document.querySelectorAll('.cards-container_<%=scanlatorID %> .card').length - 3;
|
||||
var cardWidth_<%= scanlatorID %> = document.querySelector('.cards-container_<%=scanlatorID %> .card').offsetWidth;
|
||||
var currentPosition_<%= scanlatorID %> = 0;
|
||||
var maxPosition_<%= scanlatorID %> = - cardAmount_<%= scanlatorID %>;
|
||||
|
||||
}
|
||||
function needsChevrons_<%= scanlatorID %>()
|
||||
{
|
||||
let cardsContainer_<%= scanlatorID %> = document.querySelectorAll('.cards-container_<%= scanlatorID %>')[0]
|
||||
let gap_<%= scanlatorID %> = parseFloat(window.getComputedStyle(cardsContainer_<%= scanlatorID %>).gap);
|
||||
let imageAmount_<%= scanlatorID %> = document.querySelectorAll('.img_<%= scanlatorID %>').length;
|
||||
let imageOffSetWith_<%= scanlatorID %> = document.querySelectorAll('.img_<%= scanlatorID %>')[0].offsetWidth+gap_<%= scanlatorID %>
|
||||
let carouselWidth_<%= scanlatorID %> = document.getElementById('buttonContainer<%= scanlatorID %>').offsetWidth;
|
||||
// console.log('Chevron check', carouselWidth_<%= scanlatorID %>,(imageAmount_<%= scanlatorID %> * imageOffSetWith_<%= scanlatorID %>),(imageAmount_<%= scanlatorID %> * imageOffSetWith_<%= scanlatorID %>) < carouselWidth_<%= scanlatorID %>)
|
||||
if ((imageAmount_<%= scanlatorID %> * imageOffSetWith_<%= scanlatorID %>) < carouselWidth_<%= scanlatorID %>)
|
||||
{
|
||||
return document.getElementById('buttonContainer<%= scanlatorID %>').classList.add('hidden');
|
||||
}
|
||||
document.getElementById('buttonContainer<%= scanlatorID %>').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Hiding button container if all cards fit in view
|
||||
window.addEventListener('resize', (event) =>
|
||||
{
|
||||
needsChevrons_<%= scanlatorID %>();
|
||||
});
|
||||
|
||||
|
||||
// Function to handle previous button click
|
||||
function handlePrev_<%= scanlatorID %>()
|
||||
{
|
||||
let cardsContainer_<%= scanlatorID %> = document.querySelector('.cards-container_<%=scanlatorID %>');
|
||||
if (currentPosition_<%= scanlatorID %> == 0) return;
|
||||
currentPosition_<%= scanlatorID %>++;
|
||||
cardsContainer_<%= scanlatorID %>.style.transform = `translateX(${currentPosition_<%= scanlatorID %>*cardWidth_<%= scanlatorID %>}px)`;
|
||||
}
|
||||
|
||||
// Function to handle next button click
|
||||
function handleNext_<%= scanlatorID %>()
|
||||
{
|
||||
let cardsContainer_<%= scanlatorID %> = document.querySelector('.cards-container_<%= scanlatorID %>');
|
||||
let cards_<%= scanlatorID %> = document.querySelectorAll('.cards-container_<%= scanlatorID %> .card')
|
||||
let lastCard_<%= scanlatorID %> = cards_<%= scanlatorID %>[cards_<%= scanlatorID %>.length-1];
|
||||
let lastCardBoundingRect_<%= scanlatorID %> = lastCard_<%= scanlatorID %>.getBoundingClientRect();
|
||||
let nextButtonBoudingRect_<%= scanlatorID %> = document.querySelector('#buttonContainer<%= scanlatorID %> .next-btn').getBoundingClientRect();
|
||||
if((lastCardBoundingRect_<%= scanlatorID %>.x+lastCardBoundingRect_<%= scanlatorID %>.width)<nextButtonBoudingRect_<%= scanlatorID %>.x) return;
|
||||
if (currentPosition_<%= scanlatorID %> == maxPosition_<%= scanlatorID %>) return;
|
||||
currentPosition_<%= scanlatorID %>--;
|
||||
cardsContainer_<%= scanlatorID %>.style.transform = `translateX(${currentPosition_<%= scanlatorID %>*cardWidth_<%= scanlatorID %>}px)`;
|
||||
cards_<%= scanlatorID %> = document.querySelectorAll('.cards-container_<%= scanlatorID %> .card')
|
||||
lastCard_<%= scanlatorID %> = cards_<%= scanlatorID %>[cards_<%= scanlatorID %>.length-1];
|
||||
lastCardBoundingRect_<%= scanlatorID %> = lastCard_<%= scanlatorID %>.getBoundingClientRect();
|
||||
nextButtonBoudingRect_<%= scanlatorID %> = document.querySelector('#buttonContainer<%= scanlatorID %> .next-btn').getBoundingClientRect();
|
||||
}
|
||||
|
||||
</script>
|
@ -0,0 +1,9 @@
|
||||
<% data.forEach(scan=>{ %>
|
||||
<div class="scanlator-container">
|
||||
<h2><%= scan.scanlator %></h2>
|
||||
<br>
|
||||
<div class="searchCarousel">
|
||||
<%- include('searchCarousel.ejs', {scan} )%>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
@ -0,0 +1,5 @@
|
||||
<% if(status && status != 'Ongoing'){ %>
|
||||
<span class="status <%=status%>">
|
||||
<%= status %>
|
||||
</span>
|
||||
<% } %>
|
Loading…
Reference in new issue