Merge branch 'csp_hell'

# Conflicts:
#	static/css/base.min.css
This commit is contained in:
2021-05-04 09:31:09 +01:00
21 changed files with 474 additions and 440 deletions

View File

@@ -50,6 +50,7 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
'csp.middleware.CSPMiddleware',
] ]
ROOT_URLCONF = "cbreader.urls" ROOT_URLCONF = "cbreader.urls"
@@ -145,3 +146,11 @@ BOOTSTRAP4 = {
"crossorigin": "anonymous", "crossorigin": "anonymous",
}, },
} }
CSP_DEFAULT_SRC = ("'none'")
CSP_STYLE_SRC = ("'self'", 'cdn.jsdelivr.net', 'cdn.datatables.net')
CSP_IMG_SRC = ("'self'", "data:")
CSP_FONT_SRC = ("'self'")
CSP_SCRIPT_SRC = ("'self'", 'code.jquery.com', 'cdn.jsdelivr.net', 'cdn.datatables.net')
CSP_CONNECT_SRC = ("'self'")
CSP_INCLUDE_NONCE_IN = ['script-src']
CSP_SCRIPT_SRC_ATTR = ("'self'", "'unsafe-inline'")

View File

@@ -2,7 +2,6 @@ import io
import mimetypes import mimetypes
import uuid import uuid
import zipfile import zipfile
from dataclasses import dataclass
from functools import reduce from functools import reduce
from itertools import zip_longest from itertools import zip_longest
from os import listdir from os import listdir
@@ -13,7 +12,7 @@ import fitz
import rarfile import rarfile
from PIL import Image, UnidentifiedImageError from PIL import Image, UnidentifiedImageError
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User, AbstractUser
from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.files.uploadedfile import InMemoryUploadedFile
from django.db import models from django.db import models
from django.db.transaction import atomic from django.db.transaction import atomic
@@ -230,18 +229,12 @@ class ComicBook(models.Model):
def page_count(self): def page_count(self):
return ComicPage.objects.filter(Comic=self).count() return ComicPage.objects.filter(Comic=self).count()
@dataclass
class Navigation:
next_path: str
prev_path: str
cur_path: str
def nav(self, user): def nav(self, user):
return self.Navigation( return {
next_path=self.nav_get_next_comic(user), "next_path": self.nav_get_next_comic(user),
prev_path=self.nav_get_prev_comic(user), "prev_path": self.nav_get_prev_comic(user),
cur_path=urlsafe_base64_encode(self.selector.bytes) "cur_path": urlsafe_base64_encode(self.selector.bytes)
) }
def nav_get_prev_comic(self, user) -> str: def nav_get_prev_comic(self, user) -> str:
base_dir = settings.COMIC_BOOK_VOLUME base_dir = settings.COMIC_BOOK_VOLUME

View File

@@ -11,13 +11,13 @@
<meta name="author" content="Ajurna"> <meta name="author" content="Ajurna">
<link rel="icon" href="{% static "favicon.ico" %}"> <link rel="icon" href="{% static "favicon.ico" %}">
<title>{% block title %}CB Reader{% endblock %}</title> <title>{% block title %}CB Web Reader{% endblock %}</title>
<!-- Bootstrap core CSS --> <!-- Bootstrap core CSS -->
{% bootstrap_css %} {% bootstrap_css %}
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/dt-1.10.21/b-1.6.2/b-colvis-1.6.2/r-2.2.4/datatables.min.css"/> <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/dt-1.10.21/b-1.6.2/b-colvis-1.6.2/r-2.2.4/datatables.min.css"/>
<!-- Custom styles for this template --> <!-- Custom styles for this template -->
<link href="{% static "css/base.css" %}" rel="stylesheet"> <link href="{% static "css/base.min.css" %}" rel="stylesheet">
<link href="{% static "font-awesome/css/all.css" %}" rel="stylesheet"> <link href="{% static "font-awesome/css/all.css" %}" rel="stylesheet">
{# <link href="{% static "reveal.js/css/reveal.css" %}" rel="stylesheet">#} {# <link href="{% static "reveal.js/css/reveal.css" %}" rel="stylesheet">#}
@@ -28,7 +28,7 @@
<body> <body>
<nav class="navbar navbar-expand-lg navbar-light bg-light"> <nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/"><img src="{% static 'img/logo.svg' %}" class="d-inline-block align-top" height="35px"> Web Reader</a> <a class="navbar-brand" href="/"><img src="{% static 'img/logo.svg' %}" class="d-inline-block align-top" height="35px" alt="CB"> Web Reader</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
@@ -56,7 +56,7 @@
<!-- /.container --> <!-- /.container -->
<footer class="footer mt-auto py-3"> <footer class="footer mt-auto py-3">
<div class="container text-center"> <div class="container text-center">
<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="Creative Commons Licence" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" /></a><br /><span xmlns:dct="http://purl.org/dc/terms/" href="http://purl.org/dc/dcmitype/InteractiveResource" property="dct:title" rel="dct:type">CBReader</span> by <span xmlns:cc="http://creativecommons.org/ns#" property="cc:attributionName">Ajurna</span> is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.<br />Based on a work at <a xmlns:dct="http://purl.org/dc/terms/" href="https://github.com/ajurna/cbreader" rel="dct:source">https://github.com/ajurna/cbreader</a>. <a rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/"><img alt="Creative Commons Licence" src="{% static "img/ccbysa.png" %}" /></a><br /><span xmlns:dct="https://purl.org/dc/terms/" href="https://purl.org/dc/dcmitype/InteractiveResource" property="dct:title" rel="dct:type">CBReader</span> by <span xmlns:cc="https://creativecommons.org/ns#" property="cc:attributionName">Ajurna</span> is licensed under a <a rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.<br />Based on a work at <a xmlns:dct="https://purl.org/dc/terms/" href="https://github.com/ajurna/cbreader" rel="dct:source">https://github.com/ajurna/cbreader</a>.
</div> </div>
</footer> </footer>

View File

@@ -8,7 +8,7 @@
<div class="row"> <div class="row">
<div class="input-group"> <div class="input-group">
<input type="text" id="quicksearch" class="form-control" placeholder="Search" aria-label="Search list of comics" aria-describedby="button-addon4"> <input type="text" id="quicksearch" class="form-control" placeholder="Search" aria-label="Search list of comics" aria-describedby="button-addon4">
<div id="filters" class="input-group-append" id="button-addon4"> <div id="filters" class="input-group-append">
<button class="btn btn-outline-secondary filters" type="button" data-filter="*">All</button> <button class="btn btn-outline-secondary filters" type="button" data-filter="*">All</button>
<button class="btn btn-outline-secondary filters" type="button" data-filter=".read">Read</button> <button class="btn btn-outline-secondary filters" type="button" data-filter=".read">Read</button>
<button class="btn btn-outline-secondary filters" type="button" data-filter=".unread">Unread</button> <button class="btn btn-outline-secondary filters" type="button" data-filter=".unread">Unread</button>
@@ -17,8 +17,8 @@
Actions Actions
</button> </button>
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1"> <div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
<button type="button" class="btn btn-primary dropdown-item" title="Mark Un-Read" onclick="comic_action('{{ selector }}', 'Directory', 'mark_unread')"><i class="fas fa-book">Mark Un-Read</i></button> <button type="button" class="btn btn-primary dropdown-item comic_action" title="Mark Un-Read" selector="{{ selector }}" itemtype="Directory" comic_action="mark_unread"><i class="fas fa-book">Mark Un-Read</i></button>
<button type="button" class="btn btn-primary dropdown-item" title="Mark Read" onclick="comic_action('{{ selector }}', 'Directory', 'mark_read')"><i class="fas fa-book-open">Mark Read</i></button> <button type="button" class="btn btn-primary dropdown-item comic_action" title="Mark Read" selector="{{ selector }}" itemtype="Directory" comic_action="mark_read"><i class="fas fa-book-open">Mark Read</i></button>
{# <button type="button" class="btn btn-primary dropdown-item" title="Edit Comic"><i class="fas fa-edit">Edit Comic</i></button>#} {# <button type="button" class="btn btn-primary dropdown-item" title="Edit Comic"><i class="fas fa-edit">Edit Comic</i></button>#}
</div> </div>
</div> </div>
@@ -27,10 +27,10 @@
</div> </div>
</div> </div>
<div class="container comic-container"> <div class="container comic-container">
<div class="row grid"> <div class="row grid ">
{% for file in files %} {% for file in files %}
<div class="m-2 grid-item {% if file.percent == 100 %}read{% else %}unread{% endif %}"> <div class="m-2 grid-item {% if file.percent == 100 %}read{% else %}unread{% endif %}">
<div class="card" style="width: 200px;"> <div class="card card_list_card">
{% if file.item_type == 'Directory' %} {% if file.item_type == 'Directory' %}
<a href="{% url "comic_list" file.selector %}"> <a href="{% url "comic_list" file.selector %}">
{% elif file.item_type == 'ComicBook' %} {% elif file.item_type == 'ComicBook' %}
@@ -38,12 +38,12 @@
{% endif %} {% endif %}
{% if file.obj.thumbnail %} {% if file.obj.thumbnail %}
<img src="{{file.obj.thumbnail.url}}" class="card-img-top" alt="{{ file.name }}" onerror="this.onerror=null;this.src='{% static "img/placeholder.png" %}';"> <img src="{{file.obj.thumbnail.url}}" class="card-img-top" alt="{{ file.name }}" alt_src="{% static "/img/placeholder.png" %}" onerror="this.onerror=null;this.src=this.getAttribute('alt_src');">
{% else %} {% else %}
{% if file.item_type == 'Directory' %} {% if file.item_type == 'Directory' %}
<img src="{% url 'directory_thumbnail' file.selector %}" class="card-img-top" alt="{{ file.name }}" onerror="this.onerror=null;this.src='{% static "img/placeholder.png" %}';"> <img src="{% url 'directory_thumbnail' file.selector %}" class="card-img-top" alt="{{ file.name }}" alt_src="{% static "/img/placeholder.png" %}" onerror="this.onerror=null;this.src=this.getAttribute('alt_src');">
{% elif file.item_type == 'ComicBook' %} {% elif file.item_type == 'ComicBook' %}
<img src="{% url 'comic_thumbnail' file.selector %}" class="card-img-top" alt="{{ file.name }}" onerror="this.onerror=null;this.src='{% static "img/placeholder.png" %}';"> <img src="{% url 'comic_thumbnail' file.selector %}" class="card-img-top" alt="{{ file.name }}" alt_src="{% static "/img/placeholder.png" %}" onerror="this.onerror=null;this.src=this.getAttribute('alt_src');">
{% endif %} {% endif %}
{% endif %} {% endif %}
</a> </a>
@@ -58,28 +58,33 @@
</a> </a>
</h5> </h5>
<p class="card-text"> <p class="card-text">
<figure class="text-center w-100 mb-0">{{ file.total_read }} / {{ file.total }}</figure>
<div class="progress"> <div class="progress">
<div class="progress-bar" role="progressbar" style="width: {{ file.percent }}%;" aria-valuenow="{{ file.percent }}" aria-valuemin="0" aria-valuemax="100">{{ file.percent }}%</div> <div class="progress-bar" role="progressbar" aria-valuenow="{{ file.percent }}" aria-valuemin="0" aria-valuemax="100"></div>
</div> </div>
</p> </p>
<div class="btn-group" role="group" aria-label="Comic Actions"> <div class="btn-group" role="group" aria-label="Comic Actions">
<button type="button" class="btn btn-primary" title="Mark Un-Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_unread')"><i class="fas fa-book"></i></button> <button type="button" class="btn btn-primary comic_action" title="Mark Un-Read" selector="{{ file.selector }}" itemtype="{{ file.item_type }}" comic_action="mark_unread"><i class="fas fa-book"></i></button>
<button type="button" class="btn btn-primary" title="Mark Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_read')"><i class="fas fa-book-open"></i></button> <button type="button" class="btn btn-primary comic_action" title="Mark Read" selector="{{ file.selector }}" itemtype="{{ file.item_type }}" comic_action="mark_read"><i class="fas fa-book-open"></i></button>
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
</button> </button>
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1"> <div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
<button type="button" class="btn btn-primary dropdown-item" title="Mark Un-Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_unread')"><i class="fas fa-book">Mark Un-Read</i></button> <button type="button" class="btn btn-primary dropdown-item comic_action" title="Mark Un-Read" selector="{{ file.selector }}" itemtype="{{ file.item_type }}" comic_action="mark_unread"><i class="fas fa-book">Mark Un-Read</i></button>
<button type="button" class="btn btn-primary dropdown-item" title="Mark Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_read')"><i class="fas fa-book-open">Mark Read</i></button> <button type="button" class="btn btn-primary dropdown-item comic_action" title="Mark Read" selector="{{ file.selector }}" itemtype="{{ file.item_type }}" comic_action="mark_read"><i class="fas fa-book-open">Mark Read</i></button>
{% if file.item_type != 'Directory' %} {% if file.item_type != 'Directory' %}
<button type="button" class="btn btn-primary dropdown-item" title="Mark Previous Read"><i class="fas fa-book" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_previous')"><i class="fas fa-arrow-up">Mark Previous Read</i></i></button> <button type="button" class="btn btn-primary dropdown-item comic_action" title="Mark Previous Read" selector="{{ file.selector }}" itemtype="{{ file.item_type }}" comic_action="mark_previous"><i class="fas fa-book"><i class="fas fa-arrow-up">Mark Previous Read</i></i></button>
{% endif %} {% endif %}
{# <button type="button" class="btn btn-primary dropdown-item" title="Edit Comic"><i class="fas fa-edit">Edit Comic</i></button>#} {# <button type="button" class="btn btn-primary dropdown-item" title="Edit Comic"><i class="fas fa-edit">Edit Comic</i></button>#}
</div> </div>
</div> </div>
</div> </div>
{% if file.total_unread and file.item_type == 'Directory' %}
<span class="badge rounded-pill bg-primary card-badge">{{ file.total_unread }}</span>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -90,76 +95,5 @@
{% endblock %} {% endblock %}
{% block script %} {% block script %}
<script> <script type="text/javascript" src="{% static "js/comic_list.min.js" %}"></script>
var qsRegex;
var buttonFilter;
var $grid = $('.comic-container').isotope({
itemSelector: '.grid-item',
layoutMode: 'fitRows',
filter: function() {
var $this = $(this);
var searchResult = qsRegex ? $this.text().match( qsRegex ) : true;
var buttonResult = buttonFilter ? $this.is( buttonFilter ) : true;
return searchResult && buttonResult;
{#return searchResult#}
}
});
$('#filters').on( 'click', 'button', function() {
buttonFilter = $( this ).attr('data-filter');
sessionStorage.setItem(window.location.href+"button", buttonFilter);
$grid.isotope();
});
var $quicksearch = $('#quicksearch').keyup( debounce( function() {
qsRegex = new RegExp($quicksearch.val(), 'gi');
sessionStorage.setItem(window.location.href+'text', $quicksearch.val());
$grid.isotope();
}) );
// debounce so filtering doesn't happen every millisecond
function debounce( fn, threshold ) {
var timeout;
threshold = threshold || 100;
return function debounced() {
clearTimeout( timeout );
var args = arguments;
var _this = this;
function delayed() {
fn.apply( _this, args );
}
timeout = setTimeout( delayed, threshold );
};
}
setInterval(function (){
$grid.isotope();
}, 1000)
let field = document.getElementById("quicksearch");
// See if we have an autosave value
// (this will only happen if the page is accidentally refreshed)
if (sessionStorage.getItem(window.location.href+'text') || sessionStorage.getItem(window.location.href+'button')) {
// Restore the contents of the text field
field.value = sessionStorage.getItem(window.location.href+'text');
qsRegex = new RegExp($quicksearch.val(), 'gi');
buttonFilter = sessionStorage.getItem(window.location.href+'button');
$grid.isotope();
}
// Listen for changes in the text field
field.addEventListener("change", function() {
// And save the results into the session storage object
});
function comic_action(selector, item_type, action) {
$.ajax({
url: '/comic/action/' + action + '/' + item_type + '/' + selector + '/',
success: function (){window.location.reload()}
})
}
</script>
{% endblock %} {% endblock %}

View File

@@ -1,17 +1,18 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block content %} {% block content %}
<div class="reveal" id="comic_box"> <div class="reveal" id="comic_box">
<div class="slides" onclick="nextPage()"> <div id="slides_div" class="slides">
{% for page in pages %} {% for page in pages %}
<section data-menu-title="{{ page.page_file_name }}"> <section data-menu-title="{{ page.page_file_name }}">
{% if page.content_type|first in 'image' %} {% if page.content_type|first in 'image' %}
<img data-src="{% url "get_image" nav.cur_path page.index %}" class=" w-100" alt="{{ page.page_file_name }}"> <img data-src="{% url "get_image" nav.cur_path page.index %}" class=" w-100" alt="{{ page.page_file_name }}">
{% else %} {% else %}
<p><embed type="{{ page.content_type }}" src="{% url "get_image" nav.cur_path page.index %}" onclick="nextPage()"></p> <p><embed class="comic_embed" type="{{ page.content_type }}" src="{% url "get_image" nav.cur_path page.index %}"></p>
{% endif %} {% endif %}
</section> </section>
{% endfor %} {% endfor %}
@@ -21,65 +22,7 @@
{% endblock %} {% endblock %}
{% block script %} {% block script %}
<script> {{ nav|json_script:"nav" }}
Reveal.initialize({ {{ status.last_read_page|json_script:"last_read_page" }}
controls: false, <script type="text/javascript" src="{% static "js/read_comic.min.js" %}"></script>
hash: true,
width: "100%",
height: "100%",
margin: 0,
minScale: 1,
maxScale: 1,
disableLayout: true,
progress: true,
keyboard: {
37: () => {prevPage()},
39: () => {nextPage()},
38: () => {window.scrollTo({ top: window.scrollY-window.innerHeight*.6, left: 0, behavior: 'smooth' })},
40: () => {window.scrollTo({ top: window.scrollY+window.innerHeight*.6, left: 0, behavior: 'smooth' })},
},
touch: false,
transition: 'slide',
plugins: [ RevealMenu ]
}).then(() => {
Reveal.slide({{ status.last_read_page }})
});
Reveal.on( 'slidechanged', event => {
setTimeout(() =>{document.getElementsByClassName('slides')[0].scrollIntoView({behavior: 'smooth'})}, 100)
$.ajax({url: "/comic/set_page/{{nav.cur_path}}/" + event.indexh + "/"})
});
const hammertime = new Hammer(document.getElementById('comic_box'), {});
hammertime.on('swipeleft', function (ev) {
if (Reveal.isLastSlide()){
window.location = "{% url "read_comic" nav.next_path %}"
} else {
Reveal.next()
}
});
hammertime.on('swiperight', function (ev) {
if (Reveal.isFirstSlide()){
window.location = "{% url "read_comic" nav.prev_path %}"
} else {
Reveal.prev();
}
});
function prevPage() {
if (Reveal.isFirstSlide()){
window.location = "{% url "read_comic" nav.prev_path %}"
} else {
Reveal.prev();
}
}
function nextPage() {
if (Reveal.isLastSlide()){
window.location = "{% url "read_comic" nav.next_path %}"
} else {
Reveal.next()
}
}
</script>
{% endblock %} {% endblock %}

View File

@@ -24,150 +24,7 @@
{% block script %} {% block script %}
<script type="text/javascript" src="{% static "pdfjs/build/pdf.js" %}"></script> <script type="text/javascript" src="{% static "pdfjs/build/pdf.js" %}"></script>
<script> {{ nav|json_script:"nav" }}
// If absolute URL from the remote server is provided, configure the CORS {{ status.last_read_page|json_script:"last_read_page" }}
// header on that server. <script type="text/javascript" src="{% static 'js/read_comic_pdf.min.js' %}"></script>
var url = '{% url "get_pdf" nav.cur_path %}';
// Loaded via <script> tag, create shortcut to access PDF.js exports.
var pdfjsLib = window['pdfjs-dist/build/pdf'];
// The workerSrc property shall be specified.
pdfjsLib.GlobalWorkerOptions.workerSrc = '{% static "pdfjs/build/pdf.worker.js" %}';
var pdfDoc = null,
pageNum = {{ status.last_read_page }},
pageRendering = false,
pageNumPending = null,
scale = 0.8,
canvas = document.getElementById('the-canvas'),
ctx = canvas.getContext('2d');
/**
* Get page info from document, resize canvas accordingly, and render page.
* @param num Page number.
*/
function renderPage(num) {
pageRendering = true;
// Using promise to fetch the page
pdfDoc.getPage(num).then(function(page) {
let viewport = page.getViewport({scale: (window.innerWidth *.95) / page.getViewport({scale:1.0}).width});
canvas.height = viewport.height;
canvas.width = viewport.width;
// Render PDF page into canvas context
let renderContext = {
canvasContext: ctx,
viewport: viewport
};
let renderTask = page.render(renderContext);
// Wait for rendering to finish
renderTask.promise.then(function() {
pageRendering = false;
if (pageNumPending !== null) {
// New page rendering is pending
renderPage(pageNumPending);
pageNumPending = null;
}
}).then(function () {
document.getElementById('the-canvas').scrollIntoView({behavior: 'smooth'})
$.ajax({url: "/comic/set_page/{{nav.cur_path}}/" + (num-1) + "/"})
});
});
// Update page counters
document.getElementById('page_num').textContent = num;
}
/**
* If another page rendering in progress, waits until the rendering is
* finised. Otherwise, executes rendering immediately.
*/
function queueRenderPage(num) {
if (pageRendering) {
pageNumPending = num;
} else {
renderPage(num);
}
}
/**
* Displays previous page.
*/
function onPrevPage() {
if (pageNum <= 1) {
window.location = "{% url "read_comic" nav.prev_path %}"
}
pageNum--;
queueRenderPage(pageNum);
}
document.getElementById('prev').addEventListener('click', onPrevPage);
/**
* Displays next page.
*/
function onNextPage() {
if (pageNum >= pdfDoc.numPages) {
window.location = "{% url "read_comic" nav.next_path %}"
}
pageNum++;
queueRenderPage(pageNum);
}
document.getElementById('next').addEventListener('click', onNextPage);
/**
* Asynchronously downloads PDF.
*/
pdfjsLib.getDocument(url).promise.then(function(pdfDoc_) {
pdfDoc = pdfDoc_;
document.getElementById('page_count').textContent = pdfDoc.numPages;
// Initial/first page rendering
renderPage(pageNum);
});
$(document).keydown(function(e) { // add arrow key support
switch(e.which) {
case 37: // left
onPrevPage()
break;
case 38: // up
window.scrollTo({
top: window.scrollY-window.innerHeight*.7,
left: 0,
behavior: 'smooth'
});
break;
case 39: // right
onNextPage()
break;
case 40: // down
window.scrollTo({
top: window.scrollY+window.innerHeight*.7,
left: 0,
behavior: 'smooth'
});
break;
default: return; // exit this handler for other keys
}
e.preventDefault(); // prevent the default action (scroll / move caret)
});
var hammertime = new Hammer(document.getElementById('the-canvas'), {});
hammertime.on('swipeleft', function () {
onNextPage()
})
hammertime.on('swiperight', function () {
onPrevPage()
})
hammertime.on('tap', function () {
onNextPage()
})
</script>
{% endblock %} {% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block content %} {% block content %}
@@ -40,83 +41,5 @@
{% endblock %} {% endblock %}
{% block script %} {% block script %}
<script> <script type="text/javascript" src="{% static "js/recent_comics.min.js" %}"></script>
$(document).ready(function() {
var table = $('#comic_list').DataTable({
"processing": true,
"stateSave": true,
"serverSide": true,
"ajax": {
"type": "POST",
"url": "/comic/recent/json/",
"data": function ( d ) {
d.csrfmiddlewaretoken = Cookies.get('csrftoken');
},
},
"rowCallback": function( row, data, index ) {
var r = $(row);
var cols = $('td:nth-child(n+2)', row);
cols.attr('data-href', data['url']);
cols.attr('style', 'cursor: pointer;');
cols.click(function() {
window.document.location = $(this).data("href");
});
var tds = $('td:eq(0)', row);
tds.html('<input type="checkbox" name="selected" value="'+data['selector']+'" data-type="'+data['type']+'"/>');
var cb = $('input', tds);
cb.change(function() {
$(this).closest('tr').toggleClass('info')
});
},
"drawCallback": function( settings ) {
var tds = $('table tr td:first-child');
tds.click(function(event){
if (!$(event.target).is('input')) {
var $cb = $('input', this);
$cb.click();
}
});
},
"columns": [
{ "data" : "selector", "orderable": false },
{ "data" : "icon", "orderable": false },
{ "data" : "name" },
{ "data" : "date" },
{ "data" : "label", "orderable": false },
],
"order": [[ 3, 'desc' ]],
});
$(".clickable-row").click(function() {
window.document.location = $(this).data("href");
});
$('#func_selector').on('change', function() {
$.post('/comic/edit/', $('#comic_form').serialize())
.done(function(){
$('#func_selector').val('choose');
$('#select-all input').prop('checked', false);
table.ajax.reload();
}).fail(function(){
alert('Error Submitting Change');
})
});
$('#select-all').click(function(event){
var cb = $('input', this);
if (!$(event.target).is('input')) {
cb.click();
}
$('table tr td:first-child input').each(function(chkbx) {
row = $(this);
if (row.prop('checked') != cb.prop('checked')){
row.click();
}
});
});
} );
</script>
{% endblock %} {% endblock %}

View File

@@ -86,6 +86,9 @@ class DirFile:
item_type: str = '' item_type: str = ''
percent: int = 0 percent: int = 0
selector: str = '' selector: str = ''
total: int = None
total_read: int = None
total_unread: int = None
def __post_init__(self): def __post_init__(self):
self.item_type = type(self.obj).__name__ self.item_type = type(self.obj).__name__
@@ -94,10 +97,14 @@ class DirFile:
total_adjustment = 1 total_adjustment = 1
if isinstance(self.obj, Directory): if isinstance(self.obj, Directory):
total_adjustment = 0 total_adjustment = 0
self.total = self.obj.total - total_adjustment
self.total_read = self.obj.total_read
self.total_unread = self.total - self.total_read
try: try:
self.percent = int((self.obj.total_read / (self.obj.total - total_adjustment)) * 100) self.percent = int((self.obj.total_read / self.total) * 100)
except ZeroDivisionError: except ZeroDivisionError:
self.percent = 0 self.percent = 0
self.selector = self.obj.url_safe_selector self.selector = self.obj.url_safe_selector
if isinstance(self.obj, Directory): if isinstance(self.obj, Directory):
self.name = self.obj.name self.name = self.obj.name

21
poetry.lock generated
View File

@@ -119,6 +119,21 @@ python-versions = ">=3.6"
beautifulsoup4 = ">=4.8.0" beautifulsoup4 = ">=4.8.0"
Django = ">=2.2" Django = ">=2.2"
[[package]]
name = "django-csp"
version = "3.7"
description = "Django Content Security Policy support."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
Django = ">=1.8"
[package.extras]
jinja2 = ["jinja2 (>=2.9.6)"]
tests = ["pytest (<4.0)", "pytest-django", "pytest-flakes (==1.0.1)", "pytest-pep8 (==1.0.6)", "pep8 (==1.4.6)", "mock (==1.0.1)", "six (==1.12.0)", "jinja2 (>=2.9.6)"]
[[package]] [[package]]
name = "django-extensions" name = "django-extensions"
version = "3.1.3" version = "3.1.3"
@@ -455,7 +470,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "c099b73f4400e26ba585774697d71eb475d22e365ad1ce9e6699086b30f403ad" content-hash = "71642aa577156d70c6033dbc260a2ab03d247a17d9b0b0500a9c9a0e0228fd68"
[metadata.files] [metadata.files]
asgiref = [ asgiref = [
@@ -553,6 +568,10 @@ django-bootstrap4 = [
{file = "django-bootstrap4-3.0.0.tar.gz", hash = "sha256:bffc96f65386fbd49cae1474393e01d4b414c12fcab0fff50545e6142e7ba19b"}, {file = "django-bootstrap4-3.0.0.tar.gz", hash = "sha256:bffc96f65386fbd49cae1474393e01d4b414c12fcab0fff50545e6142e7ba19b"},
{file = "django_bootstrap4-3.0.0-py3-none-any.whl", hash = "sha256:76a52fb22a8d3dbb2f7609b21908ce863e941a4462be079bf1d12025e551af37"}, {file = "django_bootstrap4-3.0.0-py3-none-any.whl", hash = "sha256:76a52fb22a8d3dbb2f7609b21908ce863e941a4462be079bf1d12025e551af37"},
] ]
django-csp = [
{file = "django_csp-3.7-py2.py3-none-any.whl", hash = "sha256:01443a07723f9a479d498bd7bb63571aaa771e690f64bde515db6cdb76e8041a"},
{file = "django_csp-3.7.tar.gz", hash = "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727"},
]
django-extensions = [ django-extensions = [
{file = "django-extensions-3.1.3.tar.gz", hash = "sha256:5f0fea7bf131ca303090352577a9e7f8bfbf5489bd9d9c8aea9401db28db34a0"}, {file = "django-extensions-3.1.3.tar.gz", hash = "sha256:5f0fea7bf131ca303090352577a9e7f8bfbf5489bd9d9c8aea9401db28db34a0"},
{file = "django_extensions-3.1.3-py3-none-any.whl", hash = "sha256:50de8977794a66a91575dd40f87d5053608f679561731845edbd325ceeb387e3"}, {file = "django_extensions-3.1.3-py3-none-any.whl", hash = "sha256:50de8977794a66a91575dd40f87d5053608f679561731845edbd325ceeb387e3"},

View File

@@ -3,7 +3,7 @@ line_length = 119
[tool.poetry] [tool.poetry]
name = "cbwebreader" name = "cbwebreader"
version = "0.2.1" version = "0.3.0"
description = "CBR/Z Web Reader" description = "CBR/Z Web Reader"
authors = ["ajurna <ajurna@gmail.com>"] authors = ["ajurna <ajurna@gmail.com>"]
license = "Creative Commons Attribution-ShareAlike 4.0 International License" license = "Creative Commons Attribution-ShareAlike 4.0 International License"
@@ -26,6 +26,7 @@ Pillow = "^8.2.0"
django-imagekit = "^4.0.2" django-imagekit = "^4.0.2"
PyMuPDF = "^1.18.12" PyMuPDF = "^1.18.12"
django-bootstrap4 = "^3.0.0" django-bootstrap4 = "^3.0.0"
django-csp = "^3.7"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
mypy = "^0.812" mypy = "^0.812"

View File

@@ -1,46 +1,14 @@
/*.navbar {*/
/* margin: 0px;*/
/*}*/
/*.starter-template {*/
/* padding: 40px 15px;*/
/* text-align: center;*/
/*}*/
/*!* Sticky footer styles*/
/*-------------------------------------------------- *!*/
/*html {*/
/* position: relative;*/
/* min-height: 100%;*/
/*}*/
/*body {*/
/* !* Margin bottom by footer height *!*/
/* margin-bottom: 80px;*/
/*}*/
/*.footer {*/
/* position: absolute;*/
/* bottom: 0;*/
/* width: 100%;*/
/* !* Set the fixed height of the footer here *!*/
/* height: 80px;*/
/* background-color: #f5f5f5;*/
/*}*/
/*.comic_box {*/
/* width: 100%;*/
/*}*/
/*#dropdown-list{*/
/* max-height: 300px;*/
/* overflow: auto;*/
/* box-shadow: none;*/
/* }*/
/* td a {*/
/* display:block;*/
/* width:100%;*/
/* }*/
/* tr.clickable-row {*/
/* cursor: pointer;*/
/* }*/
#comic_list caption { #comic_list caption {
caption-side: top; caption-side: top;
} }
.card_list_card {
width: 200px;
}
.card .card-badge {
position:absolute;
top:10px;
left:10px;
padding:5px;
color:white;
}

View File

@@ -1,2 +1 @@
/*!* Sticky footer styles*/ #comic_list caption{caption-side:top}.card_list_card{width:200px}.card .card-badge{position:absolute;top:10px;left:10px;padding:5px;color:#fff}
#comic_list caption{caption-side:top}

BIN
static/img/ccbysa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

86
static/js/comic_list.js Normal file
View File

@@ -0,0 +1,86 @@
var qsRegex;
var buttonFilter;
var $grid = $('.comic-container').isotope({
itemSelector: '.grid-item',
layoutMode: 'fitRows',
filter: function() {
var $this = $(this);
var searchResult = qsRegex ? $this.text().match( qsRegex ) : true;
var buttonResult = buttonFilter ? $this.is( buttonFilter ) : true;
return searchResult && buttonResult;
}
});
$('#filters').on( 'click', 'button', function() {
if (typeof $( this ).attr('data-filter') === "undefined") {
}else {
buttonFilter = $( this ).attr('data-filter');
sessionStorage.setItem(window.location.href+"button", buttonFilter);
$grid.isotope();
}
});
var $quicksearch = $('#quicksearch').keyup( debounce( function() {
qsRegex = new RegExp($quicksearch.val(), 'gi');
sessionStorage.setItem(window.location.href+'text', $quicksearch.val());
$grid.isotope();
}) );
// debounce so filtering doesn't happen every millisecond
function debounce( fn, threshold ) {
var timeout;
threshold = threshold || 100;
return function debounced() {
clearTimeout( timeout );
var args = arguments;
var _this = this;
function delayed() {
fn.apply( _this, args );
}
timeout = setTimeout( delayed, threshold );
};
}
setInterval(function (){
$grid.isotope();
}, 1000)
let field = document.getElementById("quicksearch");
// See if we have an autosave value
// (this will only happen if the page is accidentally refreshed)
if (sessionStorage.getItem(window.location.href+'text') || sessionStorage.getItem(window.location.href+'button')) {
// Restore the contents of the text field
field.value = sessionStorage.getItem(window.location.href+'text');
qsRegex = new RegExp($quicksearch.val(), 'gi');
buttonFilter = sessionStorage.getItem(window.location.href+'button');
$grid.isotope();
}
// Listen for changes in the text field
field.addEventListener("change", function() {
// And save the results into the session storage object
});
function comic_action(selector, item_type, action) {
$.ajax({
url: '/comic/action/' + action + '/' + item_type + '/' + selector + '/',
success: function (){window.location.reload()}
})
}
$( ".progress-bar" ).each(function( index ) {
let bar = $(this)
bar.css('width', bar.attr('aria-valuenow') + '%')
});
let comic_action_elements = document.getElementsByClassName('comic_action')
comic_action_elements.forEach(el => el.addEventListener('click', event => {
let target = $(event.target).closest('button')
let selector = target.attr('selector')
let item_type = target.attr('itemtype')
let action = target.attr('comic_action')
comic_action(selector, item_type, action)
}));

1
static/js/comic_list.min.js vendored Normal file
View File

@@ -0,0 +1 @@
var qsRegex;var buttonFilter;var $grid=$(".comic-container").isotope({itemSelector:".grid-item",layoutMode:"fitRows",filter:function(){var $this=$(this);var searchResult=qsRegex?$this.text().match(qsRegex):true;var buttonResult=buttonFilter?$this.is(buttonFilter):true;return searchResult&&buttonResult}});$("#filters").on("click","button",function(){if(typeof $(this).attr("data-filter")==="undefined"){}else{buttonFilter=$(this).attr("data-filter");sessionStorage.setItem(window.location.href+"button",buttonFilter);$grid.isotope()}});var $quicksearch=$("#quicksearch").keyup(debounce(function(){qsRegex=new RegExp($quicksearch.val(),"gi");sessionStorage.setItem(window.location.href+"text",$quicksearch.val());$grid.isotope()}));function debounce(fn,threshold){var timeout;threshold=threshold||100;return function debounced(){clearTimeout(timeout);var args=arguments;var _this=this;function delayed(){fn.apply(_this,args)}timeout=setTimeout(delayed,threshold)}}setInterval(function(){$grid.isotope()},1e3);let field=document.getElementById("quicksearch");if(sessionStorage.getItem(window.location.href+"text")||sessionStorage.getItem(window.location.href+"button")){field.value=sessionStorage.getItem(window.location.href+"text");qsRegex=new RegExp($quicksearch.val(),"gi");buttonFilter=sessionStorage.getItem(window.location.href+"button");$grid.isotope()}field.addEventListener("change",function(){});function comic_action(selector,item_type,action){$.ajax({url:"/comic/action/"+action+"/"+item_type+"/"+selector+"/",success:function(){window.location.reload()}})}$(".progress-bar").each(function(index){let bar=$(this);bar.css("width",bar.attr("aria-valuenow")+"%")});let comic_action_elements=document.getElementsByClassName("comic_action");comic_action_elements.forEach(el=>el.addEventListener("click",event=>{let target=$(event.target).closest("button");let selector=target.attr("selector");let item_type=target.attr("itemtype");let action=target.attr("comic_action");comic_action(selector,item_type,action)}));

70
static/js/read_comic.js Normal file
View File

@@ -0,0 +1,70 @@
const nav = JSON.parse(document.getElementById('nav').textContent);
const last_read_page = JSON.parse(document.getElementById('last_read_page').textContent);
Reveal.initialize({
controls: false,
hash: true,
width: "100%",
height: "100%",
margin: 0,
minScale: 1,
maxScale: 1,
disableLayout: true,
progress: true,
keyboard: {
37: () => {prevPage()},
39: () => {nextPage()},
38: () => {window.scrollTo({ top: window.scrollY-window.innerHeight*.6, left: 0, behavior: 'smooth' })},
40: () => {window.scrollTo({ top: window.scrollY+window.innerHeight*.6, left: 0, behavior: 'smooth' })},
},
touch: false,
transition: 'slide',
plugins: [ RevealMenu ]
}).then(() => {
Reveal.slide(last_read_page)
});
Reveal.on( 'slidechanged', event => {
setTimeout(() =>{document.getElementsByClassName('slides')[0].scrollIntoView({behavior: 'smooth'})}, 100)
$.ajax({url: "/comic/set_page/" + nav.cur_path + "/" + event.indexh + "/"})
});
const hammertime = new Hammer(document.getElementById('comic_box'), {});
hammertime.on('swipeleft', function (ev) {
if (Reveal.isLastSlide()){
window.location = "/comic/read/"+ nav.next_path +"/"
} else {
Reveal.next()
}
});
hammertime.on('swiperight', function (ev) {
if (Reveal.isFirstSlide()){
window.location = "/comic/read/"+ nav.prev_path +"/"
} else {
Reveal.prev();
}
});
function prevPage() {
if (Reveal.isFirstSlide()){
window.location = "/comic/read/"+ nav.prev_path +"/"
} else {
Reveal.prev();
}
}
function nextPage() {
if (Reveal.isLastSlide()){
window.location = "/comic/read/"+ nav.next_path +"/"
} else {
Reveal.next()
}
}
let slides_div = document.getElementById('slides_div')
slides_div.addEventListener('click', nextPage)
let embeds = document.getElementsByClassName('comic_embed')
embeds.forEach(function (embed){
embed.addEventListener('click', nextPage)
})

1
static/js/read_comic.min.js vendored Normal file
View File

@@ -0,0 +1 @@
const nav=JSON.parse(document.getElementById("nav").textContent);const last_read_page=JSON.parse(document.getElementById("last_read_page").textContent);Reveal.initialize({controls:false,hash:true,width:"100%",height:"100%",margin:0,minScale:1,maxScale:1,disableLayout:true,progress:true,keyboard:{37:()=>{prevPage()},39:()=>{nextPage()},38:()=>{window.scrollTo({top:window.scrollY-window.innerHeight*.6,left:0,behavior:"smooth"})},40:()=>{window.scrollTo({top:window.scrollY+window.innerHeight*.6,left:0,behavior:"smooth"})}},touch:false,transition:"slide",plugins:[RevealMenu]}).then(()=>{Reveal.slide(last_read_page)});Reveal.on("slidechanged",event=>{setTimeout(()=>{document.getElementsByClassName("slides")[0].scrollIntoView({behavior:"smooth"})},100);$.ajax({url:"/comic/set_page/"+nav.cur_path+"/"+event.indexh+"/"})});const hammertime=new Hammer(document.getElementById("comic_box"),{});hammertime.on("swipeleft",function(ev){if(Reveal.isLastSlide()){window.location="/comic/read/"+nav.next_path+"/"}else{Reveal.next()}});hammertime.on("swiperight",function(ev){if(Reveal.isFirstSlide()){window.location="/comic/read/"+nav.prev_path+"/"}else{Reveal.prev()}});function prevPage(){if(Reveal.isFirstSlide()){window.location="/comic/read/"+nav.prev_path+"/"}else{Reveal.prev()}}function nextPage(){if(Reveal.isLastSlide()){window.location="/comic/read/"+nav.next_path+"/"}else{Reveal.next()}}let slides_div=document.getElementById("slides_div");slides_div.addEventListener("click",nextPage);let embeds=document.getElementsByClassName("comic_embed");embeds.forEach(function(embed){embed.addEventListener("click",nextPage)});

146
static/js/read_comic_pdf.js Normal file
View File

@@ -0,0 +1,146 @@
// If absolute URL from the remote server is provided, configure the CORS
// header on that server.
const nav = JSON.parse(document.getElementById('nav').textContent);
const last_read_page = JSON.parse(document.getElementById('last_read_page').textContent);
var url = "/comic/read/" + nav.cur_path + "/pdf"
// Loaded via <script> tag, create shortcut to access PDF.js exports.
var pdfjsLib = window['pdfjs-dist/build/pdf'];
// The workerSrc property shall be specified.
pdfjsLib.GlobalWorkerOptions.workerSrc = '/static/pdfjs/build/pdf.worker.js';
var pdfDoc = null,
pageNum = last_read_page,
pageRendering = false,
pageNumPending = null,
scale = 0.8,
canvas = document.getElementById('the-canvas'),
ctx = canvas.getContext('2d');
/**
* Get page info from document, resize canvas accordingly, and render page.
* @param num Page number.
*/
function renderPage(num) {
pageRendering = true;
// Using promise to fetch the page
pdfDoc.getPage(num).then(function(page) {
let viewport = page.getViewport({scale: (window.innerWidth *.95) / page.getViewport({scale:1.0}).width});
canvas.height = viewport.height;
canvas.width = viewport.width;
// Render PDF page into canvas context
let renderContext = {
canvasContext: ctx,
viewport: viewport
};
let renderTask = page.render(renderContext);
// Wait for rendering to finish
renderTask.promise.then(function() {
pageRendering = false;
if (pageNumPending !== null) {
// New page rendering is pending
renderPage(pageNumPending);
pageNumPending = null;
}
}).then(function () {
document.getElementById('the-canvas').scrollIntoView({behavior: 'smooth'})
$.ajax({url: "/comic/set_page/" + nav.cur_path + "/" + (num-1) + "/"})
});
});
// Update page counters
document.getElementById('page_num').textContent = num;
}
/**
* If another page rendering in progress, waits until the rendering is
* finised. Otherwise, executes rendering immediately.
*/
function queueRenderPage(num) {
if (pageRendering) {
pageNumPending = num;
} else {
renderPage(num);
}
}
/**
* Displays previous page.
*/
function onPrevPage() {
if (pageNum <= 1) {
window.location = "/comic/read/"+ nav.prev_path +"/"
}
pageNum--;
queueRenderPage(pageNum);
}
document.getElementById('prev').addEventListener('click', onPrevPage);
/**
* Displays next page.
*/
function onNextPage() {
if (pageNum >= pdfDoc.numPages) {
window.location = "/comic/read/"+ nav.next_path +"/"
}
pageNum++;
queueRenderPage(pageNum);
}
document.getElementById('next').addEventListener('click', onNextPage);
/**
* Asynchronously downloads PDF.
*/
pdfjsLib.getDocument(url).promise.then(function(pdfDoc_) {
pdfDoc = pdfDoc_;
document.getElementById('page_count').textContent = pdfDoc.numPages;
// Initial/first page rendering
renderPage(pageNum);
});
$(document).keydown(function(e) { // add arrow key support
switch(e.which) {
case 37: // left
onPrevPage()
break;
case 38: // up
window.scrollTo({
top: window.scrollY-window.innerHeight*.7,
left: 0,
behavior: 'smooth'
});
break;
case 39: // right
onNextPage()
break;
case 40: // down
window.scrollTo({
top: window.scrollY+window.innerHeight*.7,
left: 0,
behavior: 'smooth'
});
break;
default: return; // exit this handler for other keys
}
e.preventDefault(); // prevent the default action (scroll / move caret)
});
var hammertime = new Hammer(document.getElementById('the-canvas'), {});
hammertime.on('swipeleft', function () {
onNextPage()
})
hammertime.on('swiperight', function () {
onPrevPage()
})
hammertime.on('tap', function () {
onNextPage()
})

1
static/js/read_comic_pdf.min.js vendored Normal file
View File

@@ -0,0 +1 @@
const nav=JSON.parse(document.getElementById("nav").textContent);const last_read_page=JSON.parse(document.getElementById("last_read_page").textContent);var url="/comic/read/"+nav.cur_path+"/pdf";var pdfjsLib=window["pdfjs-dist/build/pdf"];pdfjsLib.GlobalWorkerOptions.workerSrc="/static/pdfjs/build/pdf.worker.js";var pdfDoc=null,pageNum=last_read_page,pageRendering=false,pageNumPending=null,scale=.8,canvas=document.getElementById("the-canvas"),ctx=canvas.getContext("2d");function renderPage(num){pageRendering=true;pdfDoc.getPage(num).then(function(page){let viewport=page.getViewport({scale:window.innerWidth*.95/page.getViewport({scale:1}).width});canvas.height=viewport.height;canvas.width=viewport.width;let renderContext={canvasContext:ctx,viewport:viewport};let renderTask=page.render(renderContext);renderTask.promise.then(function(){pageRendering=false;if(pageNumPending!==null){renderPage(pageNumPending);pageNumPending=null}}).then(function(){document.getElementById("the-canvas").scrollIntoView({behavior:"smooth"});$.ajax({url:"/comic/set_page/"+nav.cur_path+"/"+(num-1)+"/"})})});document.getElementById("page_num").textContent=num}function queueRenderPage(num){if(pageRendering){pageNumPending=num}else{renderPage(num)}}function onPrevPage(){if(pageNum<=1){window.location="/comic/read/"+nav.prev_path+"/"}pageNum--;queueRenderPage(pageNum)}document.getElementById("prev").addEventListener("click",onPrevPage);function onNextPage(){if(pageNum>=pdfDoc.numPages){window.location="/comic/read/"+nav.next_path+"/"}pageNum++;queueRenderPage(pageNum)}document.getElementById("next").addEventListener("click",onNextPage);pdfjsLib.getDocument(url).promise.then(function(pdfDoc_){pdfDoc=pdfDoc_;document.getElementById("page_count").textContent=pdfDoc.numPages;renderPage(pageNum)});$(document).keydown(function(e){switch(e.which){case 37:onPrevPage();break;case 38:window.scrollTo({top:window.scrollY-window.innerHeight*.7,left:0,behavior:"smooth"});break;case 39:onNextPage();break;case 40:window.scrollTo({top:window.scrollY+window.innerHeight*.7,left:0,behavior:"smooth"});break;default:return}e.preventDefault()});var hammertime=new Hammer(document.getElementById("the-canvas"),{});hammertime.on("swipeleft",function(){onNextPage()});hammertime.on("swiperight",function(){onPrevPage()});hammertime.on("tap",function(){onNextPage()});

View File

@@ -0,0 +1,75 @@
$(document).ready(function() {
var table = $('#comic_list').DataTable({
"processing": true,
"stateSave": true,
"serverSide": true,
"ajax": {
"type": "POST",
"url": "/comic/recent/json/",
"data": function ( d ) {
d.csrfmiddlewaretoken = Cookies.get('csrftoken');
},
},
"rowCallback": function( row, data, index ) {
var r = $(row);
var cols = $('td:nth-child(n+2)', row);
cols.attr('data-href', data['url']);
cols.attr('style', 'cursor: pointer;');
cols.click(function() {
window.document.location = $(this).data("href");
});
var tds = $('td:eq(0)', row);
tds.html('<input type="checkbox" name="selected" value="'+data['selector']+'" data-type="'+data['type']+'"/>');
var cb = $('input', tds);
cb.change(function() {
$(this).closest('tr').toggleClass('info')
});
},
"drawCallback": function( settings ) {
var tds = $('table tr td:first-child');
tds.click(function(event){
if (!$(event.target).is('input')) {
var $cb = $('input', this);
$cb.click();
}
});
},
"columns": [
{ "data" : "selector", "orderable": false },
{ "data" : "icon", "orderable": false },
{ "data" : "name" },
{ "data" : "date" },
{ "data" : "label", "orderable": false },
],
"order": [[ 3, 'desc' ]],
});
$(".clickable-row").click(function() {
window.document.location = $(this).data("href");
});
$('#func_selector').on('change', function() {
$.post('/comic/edit/', $('#comic_form').serialize())
.done(function(){
$('#func_selector').val('choose');
$('#select-all input').prop('checked', false);
table.ajax.reload();
}).fail(function(){
alert('Error Submitting Change');
})
});
$('#select-all').click(function(event){
var cb = $('input', this);
if (!$(event.target).is('input')) {
cb.click();
}
$('table tr td:first-child input').each(function(chkbx) {
row = $(this);
if (row.prop('checked') != cb.prop('checked')){
row.click();
}
});
});
} );

1
static/js/recent_comics.min.js vendored Normal file
View File

@@ -0,0 +1 @@
$(document).ready(function(){var table=$("#comic_list").DataTable({processing:true,stateSave:true,serverSide:true,ajax:{type:"POST",url:"/comic/recent/json/",data:function(d){d.csrfmiddlewaretoken=Cookies.get("csrftoken")}},rowCallback:function(row,data,index){var r=$(row);var cols=$("td:nth-child(n+2)",row);cols.attr("data-href",data["url"]);cols.attr("style","cursor: pointer;");cols.click(function(){window.document.location=$(this).data("href")});var tds=$("td:eq(0)",row);tds.html('<input type="checkbox" name="selected" value="'+data["selector"]+'" data-type="'+data["type"]+'"/>');var cb=$("input",tds);cb.change(function(){$(this).closest("tr").toggleClass("info")})},drawCallback:function(settings){var tds=$("table tr td:first-child");tds.click(function(event){if(!$(event.target).is("input")){var $cb=$("input",this);$cb.click()}})},columns:[{data:"selector",orderable:false},{data:"icon",orderable:false},{data:"name"},{data:"date"},{data:"label",orderable:false}],order:[[3,"desc"]]});$(".clickable-row").click(function(){window.document.location=$(this).data("href")});$("#func_selector").on("change",function(){$.post("/comic/edit/",$("#comic_form").serialize()).done(function(){$("#func_selector").val("choose");$("#select-all input").prop("checked",false);table.ajax.reload()}).fail(function(){alert("Error Submitting Change")})});$("#select-all").click(function(event){var cb=$("input",this);if(!$(event.target).is("input")){cb.click()}$("table tr td:first-child input").each(function(chkbx){row=$(this);if(row.prop("checked")!=cb.prop("checked")){row.click()}})})});