diff --git a/controllers/api.js b/controllers/api.js index e7dafb7..d53d5fb 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -4,7 +4,6 @@ const FavoriteModel = require('../models/favorite.js'); const ChapterModel = require('../models/readChapter.js'); const fs = require('fs'); const path = require('path'); -const { isNullOrUndefined } = require('util'); exports.home = async (req,res)=> { @@ -45,7 +44,6 @@ exports.search = async (req, res)=> const scanlatorB = b.scanlator.toLowerCase(); return scanlatorA.localeCompare(scanlatorB); }) - console.log(data[1].Results) res.render('searchResults.ejs', {data}) } @@ -118,6 +116,7 @@ exports.manga = async (req, res)=> { manga.List[i]['completely'] = chaptersRead[j].completely manga.List[i]['Read'] = true; + manga.List[i]['_id'] = chaptersRead[j]._id; } } } @@ -127,8 +126,13 @@ exports.manga = async (req, res)=> const sortedChapters = chaptersRead.sort((a, b) => a.chapterNum - b.chapterNum); const sortedAndFilteredChapters = sortedChapters.filter(item=> item.lastImageRead && !item.completely); const lastChapterWithReadImages=sortedAndFilteredChapters.shift(); - const chapterNumToContinue = getChapterNumToContinue(lastCompletelyRead, lastChapterWithReadImages); - manga.List[chapterNumToContinue-1]['continue'] = true; + let chapterNumToContinue = getChapterNumToContinue(lastCompletelyRead, lastChapterWithReadImages, manga.List); + if(!manga.List.some(item=> item.num==chapterNumToContinue)) chapterNumToContinue = Math.trunc(chapterNumToContinue); + let index = manga.List.indexOf(manga.List.filter(item=> + { + return item.num == chapterNumToContinue + })[0]); + manga.List[index]['continue'] = true; } catch (error) { @@ -136,15 +140,19 @@ exports.manga = async (req, res)=> } res.render('mangaPage.ejs', {data:{...manga, scanlator}}); } -function getChapterNumToContinue(lastCompletelyRead, lastChapterWithReadImages) +function getChapterNumToContinue(lastCompletelyRead, lastChapterWithReadImages, List) { + const firstIndex = List.indexOf(List.filter(item=>item.num == lastCompletelyRead.chapterNum)[0]); if(lastCompletelyRead) { if(lastCompletelyRead.chapterNum < lastChapterWithReadImages.chapterNum) { return lastChapterWithReadImages.chapterNum; } - return lastCompletelyRead.chapterNum+1; + const chNum = lastCompletelyRead.chapterNum+1; + const secondIndex = List.indexOf(List.filter(item=>item.num == chNum)[0]); + if(secondIndex - firstIndex > 1) return List[firstIndex + 1].num; + return chNum } return lastChapterWithReadImages.chapterNum; } @@ -161,7 +169,7 @@ exports.chapter = async (req, res)=> 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('chapterPage.ejs', {data:{title,latestChap,scanlator, chapterNum:parseInt(chapter), mangaLink, List}}); + return res.render('chapterPage.ejs', {data:{title,latestChap,scanlator, chapterNum:parseFloat(chapter), mangaLink, List}}); } //If it isn't make sure there is no bad params being passed const scanlatorExists = await new Modules(scanlator).exists(); @@ -181,7 +189,7 @@ exports.chapter = async (req, res)=> { return res.redirect('/') } - res.render('chapterPage.ejs', {data:{...manga, scanlator, chapterNum:parseInt(chapter)}}); + res.render('chapterPage.ejs', {data:{...manga, scanlator, chapterNum:parseFloat(chapter)}}); } exports.favorites = async (req, res)=> @@ -313,7 +321,7 @@ exports.chapterNavInfo = async (req, res)=> 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 chapter = parseFloat(req.params.chapter); let manga; try { @@ -429,6 +437,8 @@ exports.errorPage = (req, res)=> exports.chapterRead = async (req, res)=> { + const cookieStr = req.headers.cookie; + if(!cookieStr) return res.sendStatus(404); const {scanlator, title, chapter, link} = req.params; const scanlatorExists = await new Modules(scanlator).exists(); if(!scanlatorExists) return res.sendStatus(404); @@ -439,13 +449,21 @@ exports.chapterRead = async (req, res)=> if(chapter > latestChap || chapter < 0) return res.sendStatus(404); const chap = new ChapterModel(); let chapterNum = chapter; - let chapterLink = List.filter(item=>item.num == chapterNum)[0].link; + let chapFromList = List.filter(item=>item.num == chapterNum)[0]; + let chapterLink = chapFromList.link; + let chapterDate = chapFromList.date; if(!chapterLink) return res.sendStatus(404); - const _chapter = await new Chapter(scanlator, chapterLink,title, chapter).get(); + const _chapter = await new Chapter(scanlator, chapterLink,title, chapterNum).get(); let lastImageRead = _chapter.List[_chapter.List.length-1]; chap.lastImageRead = lastImageRead; const chaps = await ChapterModel.find({scanlator, title, link, chapterNum}); - if(chaps.length>0) return updateChapter(chaps[0],res, lastImageRead, true) + if(chaps.length>0) updateChapter(chaps[0], lastImageRead, true); + _chapter['num'] = _chapter.chNum; + _chapter['date'] = chapterDate; + _chapter['Read'] = true; + _chapter['_id'] = chap._id; + _chapter['completely'] = true; + res.render('chapterButton.ejs', {chapter:_chapter, scanlator, title, link, shortCut:false}); chap.scanlator = scanlator; chap.title = title; chap.link = link; @@ -453,12 +471,11 @@ exports.chapterRead = async (req, res)=> chap.lastImageRead = lastImageRead; chap.completely = true; chap.save() - res.sendStatus(200); } -async function updateChapter(chap, res, lastImageRead, completely=false) +async function updateChapter(chap,lastImageRead, completely=false,res) { await ChapterModel.findOneAndUpdate(chap._id,{completely, lastImageRead:lastImageRead}, {new:true}); - res.sendStatus(200); + if(res) return res.sendStatus(200) } exports.chapterIncompleteRead = async (req,res)=> { @@ -569,26 +586,7 @@ exports.continue = async (req, res)=> return [...results, ...uniqueContinuationChapters, ...chaptersToStart]; } - async function getNextChapter (chapter) - { - const {scanlator, link, title, chapterNum, _id } = chapter; - const manga = await new Manga(scanlator, link, title).get(); - if((chapterNum+1)>manga.latestChap) return false; - const chapLink = manga.List.filter(item=>item.num == (chapterNum+1))[0].link - const nextChapter = await new Chapter(scanlator, chapLink, title, (chapterNum+1)).get(); - let index = nextChapter.List.indexOf(chapter.lastImageRead) - const imgReadOutOfTotal = (index !=-1 ? index:0)+'/'+(nextChapter.List.length-1) - return { - ...nextChapter, - _id, - mangaCoverImg:manga.img, - chapterNum:nextChapter.chNum, - lastImageRead:nextChapter.List[0], - completely:false, - imgReadOutOfTotal, - byPass:true - }; - } + const chaptersToContinue = await getChaptersToContinue(); chaptersToContinue.sort((a, b) => { @@ -614,11 +612,49 @@ exports.continue = async (req, res)=> } res.render('chaptersToContinue.ejs', {data:chaptersToContinue}); } +async function getNextChapter (chapter) +{ + const {scanlator, link, title, _id } = chapter; + let chapterNum = chapter.chapterNum; + const manga = await new Manga(scanlator, link, title).get(); + if((chapterNum+1)>manga.latestChap) return false; + if(!manga.List.some(item=>item.num == (chapterNum+1))) chapterNum = Math.trunc(chapterNum); + const chapterFromList = manga.List.filter(item=>item.num == (chapterNum+1))[0] + const {date, num} = chapterFromList; + const nextChapter = await new Chapter(scanlator, chapterFromList.link, title, (chapterNum+1)).get(); + let index = chapter.lastImageRead?nextChapter.List.indexOf(chapter.lastImageRead):-1; + const imgReadOutOfTotal = (index !=-1 ? index:0)+'/'+(nextChapter.List.length-1) + return { + ...nextChapter, + num, + date, + _id, + mangaCoverImg:manga.img, + chapterNum:nextChapter.chNum, + lastImageRead:nextChapter.List[0], + completely:false, + imgReadOutOfTotal, + byPass:true + }; +} +exports.chapterToContinue = async (req, res)=> +{ + const {scanlator, mangaLink, title} = req.params; + const chaptersRead = await ChapterModel.find({scanlator, title}).then(res=>{return res}); + let sorted = chaptersRead.sort((a,b)=>{return b.chapterNum-a.chapterNum}); + const newChapter = await getNextChapter({scanlator, link:mangaLink, title,chapterNum:parseFloat(sorted[0].chapterNum), _id:null}); + res.render('chapterButton.ejs',{chapter:newChapter, scanlator, title,shortCut:true}) + +} exports.removeChapter = async (req, res)=> { - if(!isValidID(req.params.id)) return res.sendStatus(404) - const chapter = await ChapterModel.findOneAndDelete({_id:req.params.id}); - res.render('empty.ejs'); + const cookieStr = req.headers.cookie; + if(!cookieStr) return res.sendStatus(404); + if(!isValidID(req.params.id)) return res.sendStatus(404); + const {link, scanlator, title, chapterNum} = await ChapterModel.findOneAndDelete({_id:req.params.id}).then(deleted=>{return deleted}); + const {List} = await new Manga(scanlator, link, title).get(); + const chapter = List.filter(item=>item.num == chapterNum)[0]; + res.render('chapterButton.ejs', {chapter, scanlator, title, shortCut:false}); } exports.dashboard = async (req, res)=> { diff --git a/public/css/home.css b/public/css/home.css index 72d959b..82684c4 100644 --- a/public/css/home.css +++ b/public/css/home.css @@ -396,6 +396,7 @@ button:hover border-radius: 15px; margin-left: 10%; background-color: var(--lbg); + margin-top: 8em; } .infoCard > .infoText { @@ -464,11 +465,21 @@ button:hover border: 2px solid var(--border); border-radius: 15px; background-color: var(--lbg); - margin: 5px; width: 130px; height: 40px; cursor: pointer; + position: relative; + top:-1.3em; +} +.chapterButtonWrapper +{ + margin: 5px; + width: 140px; + height: 50px; + overflow: hidden; + } + .chapterButton:hover { border-color: var(--border-hover); @@ -486,7 +497,7 @@ button:hover flex-direction: row; align-items: center; justify-content: center; - height:100% + height:100%; } .chapterButton > a >.buttonWrapper > div > h4 , .chapterButton > a >.buttonWrapper > div > h5 @@ -499,7 +510,7 @@ button:hover width:auto; color: var(--purple-heart-400); position: relative; - left: 0.7em; + left: 1em; } .readMarkerComplete { @@ -1112,4 +1123,31 @@ button:hover .accentColor { color:var(--accent) -} \ No newline at end of file +} +.markAsRead +{ + color: var(--text); + transform: scaleX(-1) rotate(90deg); + z-index: 2; + position: relative; + +} +.markAsRead:hover +{ + color:var(--accent); +} +.chapterSpacer +{ + width: 1em; + height: 1em; +} + +.hitbox +{ + height:1.3em; + width:1.3em; + position: relative; + top:0.6em; + left:.4em; + z-index: 4; +} diff --git a/routes/routes.js b/routes/routes.js index 0e720fa..0bbb0e9 100644 --- a/routes/routes.js +++ b/routes/routes.js @@ -18,6 +18,7 @@ module.exports = (worker)=> router.route('/manga/:scanlator/:link/:title').get(api.manga); router.route('/chapter/:scanlator/:link/:title/:chapter').get(api.chapter); router.route('/removeChapter/:id').get(api.removeChapter) + router.route('/chaptertocontinue/:scanlator/:mangaLink/:title/').get(api.chapterToContinue); router.route('/chapternavinfo/:scanlator/:mangaLink/:title/:chapter').get(api.chapterNavInfo); router.route('/dashboard').get(api.dashboard); router.route('/login').get(api.loginPage) diff --git a/test.js b/test.js index dec736e..a07a788 100644 --- a/test.js +++ b/test.js @@ -16,116 +16,6 @@ mongoose.connection.on('connected', ()=>{console.log('MongoDB - Connected')}) (async ()=> { + // const manga = await new Manga('Reaper-scans', 'https://reaper-scans.com/manga/kagurabachi/','Kagurabachi').get(); - async function getChaptersToContinue() - { - const favs = await FavoriteModel.find(); - const chaps = await ChapterModel.find(); - const favMap = new Map(favs.map(fav => [`${fav.title}_${fav.scanlator}`, {latestComplete: null, incomplete: []}])); - const favoritesNotYetStarted = []; - // Filter chapters directly into their respective categories - chaps.forEach(chap => - { - const key = `${chap.title}_${chap.scanlator}`; - if (favMap.has(key)) - { - const group = favMap.get(key); - if (chap.completely) { - if (!group.latestComplete || chap.chapterNum > group.latestComplete.chapterNum) group.latestComplete = chap; - } - else - { - group.incomplete.push(chap); - } - } - }); - favs.forEach((element) => - { - const {title, scanlator} = element; - if (!chaps.some(chap => chap.title == title && chap.scanlator == scanlator)) { - favoritesNotYetStarted.push(element); - } - }); - - // Compile the results - let results = Array.from(favMap.values()).flatMap(group => - group.latestComplete ? [group.latestComplete, ...group.incomplete] : group.incomplete - ); - - results = await Promise.all(results.map(async (chap) => - { - if (!chap.completely) return chap; - let next = await getNextChapter(chap); - return next ? next : null; - })); - - results = results.filter(chap => chap !== null); - // Filter to find unique chapters that are not completed - const uniqueContinuationChapters = chaps.filter(chap => - { - return !chap.completely && !results.some(f => f.scanlator === chap.scanlator && f.title === chap.title) - }); - - const chaptersToStart = []; - for(var i = 0; i< favoritesNotYetStarted.length; i++) - { - const {title, link, scanlator} = favoritesNotYetStarted[i]; - const {List, img} = await new Manga(scanlator, link, title).get(); - let aux = {byPass:true, chapterNum:0}; - const chap = await new Chapter(scanlator, List[0].link, title, List[0].num).get(); - const imgReadOutOfTotal = 0+'/'+(chap.List.length-1) - aux = {...aux, ...chap, mangaCoverImg:img, imgReadOutOfTotal,lastImageRead:chap.List[0]} - chaptersToStart.push(aux); - } - - return [...results, ...uniqueContinuationChapters, ...chaptersToStart]; - } - async function getNextChapter (chapter) - { - const {scanlator, link, title, chapterNum, _id } = chapter; - const manga = await new Manga(scanlator, link, title).get(); - if((chapterNum+1)>manga.latestChap) return false; - const chapLink = manga.List.filter(item=>item.num == (chapterNum+1))[0].link - const nextChapter = await new Chapter(scanlator, chapLink, title, (chapterNum+1)).get(); - let index = nextChapter.List.indexOf(chapter.lastImageRead) - const imgReadOutOfTotal = (index !=-1 ? index:0)+'/'+(nextChapter.List.length-1) - return { - ...nextChapter, - _id, - mangaCoverImg:manga.img, - chapterNum:nextChapter.chNum, - lastImageRead:nextChapter.List[0], - completely:false, - imgReadOutOfTotal, - byPass:true - }; - } - - const chaptersToContinue = await getChaptersToContinue(); - chaptersToContinue.sort((a, b) => { - if (a.chapterNum === 0) return 1; - if (b.chapterNum === 0) return -1; - return a.chapterNum - b.chapterNum; - }); - for(let i = 0; i<chaptersToContinue.length; i++) - { - if(chaptersToContinue[i].byPass) continue - if(!chaptersToContinue[i].mangaLink) - { - const {scanlator, link, title, chapterNum} = chaptersToContinue[i]; - const {List, img} = await new Manga(scanlator, link, title).get(); - if(!List.some(item=>item.num == chapterNum)) return; - let chapter = await new Chapter(scanlator,List[chapterNum-1].link, title,chapterNum).get(); - chaptersToContinue[i].link = chapter.link; - let index = chapter.List.indexOf(chaptersToContinue[i].lastImageRead) - const imgReadOutOfTotal = (index !=-1 ? index:0)+'/'+(chapter.List.length-1) - let aux = {...chaptersToContinue[i]._doc, mangaCoverImg:img, imgReadOutOfTotal} - chaptersToContinue[i] = aux; - } - } - // console.log(chaptersToContinue) - const c = chaptersToContinue.filter(item=>item.title=='Kagurabachi').sort((a,b)=>a.chapterNum-b.chapterNum) - console.log(c) - - // const chapter = await ChapterModel.findOneAndDelete({_id:'66251e66ab92b12ec150dad9'}); })(); diff --git a/views/bookmark.ejs b/views/bookmark.ejs index 8397b22..eb07b31 100644 --- a/views/bookmark.ejs +++ b/views/bookmark.ejs @@ -1,4 +1,4 @@ <span class="material-symbols-outlined bookmark" hx-post="/bookmark/<%=scanlator%>/<%=title%>/<%= link %>" hx-trigger="click" hx-target="this" hx-swap="outerHTML"> - bookmark_add + bookmark </span> diff --git a/views/bookmarked.ejs b/views/bookmarked.ejs index ba6a34e..4dfd9f5 100644 --- a/views/bookmarked.ejs +++ b/views/bookmarked.ejs @@ -1,3 +1,9 @@ -<span class="material-symbols-outlined bookmark fill " hx-post="/bookmark/<%=scanlator%>/<%=title%>/<%= id %> " hx-trigger="click" hx-target="this"hx-swap="outerHTML"> +<span + class="material-symbols-outlined bookmark fill " + hx-post="/bookmark/<%=scanlator%>/<%=title%>/<%= id %> " + hx-trigger="click" + hx-target="this" + hx-swap="outerHTML" +> bookmark </span> \ No newline at end of file diff --git a/views/chapterButton.ejs b/views/chapterButton.ejs index e2a988b..0843fce 100644 --- a/views/chapterButton.ejs +++ b/views/chapterButton.ejs @@ -1,20 +1,43 @@ -<div class="chapterButton"> - <a href="/chapter/<%=scanlator %>/<%=encodeURIComponent(chapter.link)%>/<%= title %>/<%= chapter.num %>" > - <div class="buttonWrapper"> - <% if(chapter.Read){ %> - <span class="material-symbols-outlined readMarker <%= chapter.completely?'readMarkerComplete fill':'' %> "> +<div + class="chapterButtonWrapper chapterButton_<%= (new String(chapter.num)).replaceAll('.', '_') %><%= shortCut?'shortCut':'' %>" +> + <div class="hitbox"> + + <% if(!chapter.Read){%> + <span + hx-post="/chapterRead/<%= scanlator %>/<%= encodeURIComponent(chapter.mangaLink) %>/<%= title %>/<%= chapter.num %>/" + hx-target=".chapterButton_<%= (new String(chapter.num)).replaceAll('.', '_') %>" + hx-swap="outerHTML" + class="material-symbols-outlined markAsRead" + > + new_label + </span> + <%} else { %> + <span + class="material-symbols-outlined readMarker <%= chapter.completely?'readMarkerComplete fill':'' %> " + hx-get="/removeChapter/<%= chapter._id %>" + hx-trigger="click" + hx-target=".chapterButton_<%= (new String(chapter.num)).replaceAll('.', '_') %>" + hx-swap="outerHTML" + > beenhere - </span> - <% } %> - <div> - <h4 class="chapterNum"> - Chapter <%= chapter.num %> - </h4> - <h5 class="date"> - <%= chapter.date %> - </h5> + </span> + <% }%> + </div> + <div class="chapterButton" > + <a href="/chapter/<%=scanlator %>/<%=encodeURIComponent(chapter.link)%>/<%= title %>/<%= chapter.num %>" > + <div class="buttonWrapper"> + <div class="chapterSpacer readMarker_<%= scanlator.concat(title) %>"></div> + <div> + <h4 class="chapterNum"> + Chapter <%= chapter.num %> + </h4> + <h5 class="date"> + <%= chapter.date %> + </h5> + </div> </div> - </div> - </a> + </a> + </div> </div> \ No newline at end of file diff --git a/views/chapterShortcutButtonRow.ejs b/views/chapterShortcutButtonRow.ejs index 0344b3c..1ae7041 100644 --- a/views/chapterShortcutButtonRow.ejs +++ b/views/chapterShortcutButtonRow.ejs @@ -1,17 +1,67 @@ <div class="shortcutWrapper"> <div> <p> First:</p> - <%- include('chapterButton.ejs', {chapter:List[0], scanlator, title}) %> + <%- include('chapterButton.ejs', {chapter:List[0], scanlator, title, shortCut:true}) %> </div> <%for(let i = 0; i<List.length; i++){if(List[i].continue) { %> - <div> + <div id="continueWrapper"> <p> Continue:</p> - <%- include('chapterButton.ejs', {chapter:List[i], scanlator, title}) %> + <%- include('chapterButton.ejs', {chapter:List[i], scanlator, title,shortCut:true}) %> </div> <%}}%> <div> <p>Last:</p> - <%- include('chapterButton.ejs', {chapter:List[List.length-1],}) %> + <%- include('chapterButton.ejs', {chapter:List[List.length-1], scanlator, title,shortCut:true}) %> </div> -</div> \ No newline at end of file +</div> +<script> + clearVariablesChapterShortCut(); + + function clearVariablesChapterShortCut() + { + [ + 'classList', + 'classListIndex', + 'chapterNum' + ].forEach((variable) => + { + if (window.hasOwnProperty(variable)) + { + delete window[variable]; + } + }); + + + } + + function createListener() + { + document.getElementById('List').addEventListener('htmx:afterSwap', event=> + { + let classList = event.target.classList; + for(let i = 0; i<classList.length; i++) + { + if(classList[i].includes('chapterButton_')) + { + let classListIndex = Array.from(document.getElementById('continueWrapper').childNodes[3].classList).indexOf(classList[i]+'shortCut'); + if(classListIndex!=-1) + { + // let chapterNum = classList[i].split('_')[1].split('shortCut')[0]; + htmx.ajax('GET', `/chaptertocontinue/<%=scanlator%>/<%= encodeURIComponent(List[0].mangaLink)%>/<%=title%>`,'.'+classList[i]+'shortCut'); + } + } + } + }); + } + try { + createListener(); + } catch (error) { + clearVariablesChapterShortCut(); + createListener(); + } + +</script> + + + diff --git a/views/mangaPage.ejs b/views/mangaPage.ejs index 312371d..1b7112b 100644 --- a/views/mangaPage.ejs +++ b/views/mangaPage.ejs @@ -19,11 +19,9 @@ </div> </div> <br> - <%- include('chapterShortcutButtonRow.ejs', {...data}) %> -<div class="list"> +<div class="list" id="List"> <% data.List.forEach(chapter=>{%> - <%- include('chapterButton.ejs', {chapter, scanlator:data.scanlator, title:data.title}) %> + <%- include('chapterButton.ejs', {chapter, scanlator:data.scanlator, title:data.title, _id:chapter._id, shortCut:false}) %> <% }) %> </div> - diff --git a/views/readMarker.ejs b/views/readMarker.ejs new file mode 100644 index 0000000..d666d34 --- /dev/null +++ b/views/readMarker.ejs @@ -0,0 +1,6 @@ +<span + class="material-symbols-outlined readMarker readMarkerComplete fill" + +> + beenhere +</span> \ No newline at end of file