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.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
'csp.middleware.CSPMiddleware',
]
ROOT_URLCONF = "cbreader.urls"
@@ -145,3 +146,11 @@ BOOTSTRAP4 = {
"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 uuid
import zipfile
from dataclasses import dataclass
from functools import reduce
from itertools import zip_longest
from os import listdir
@@ -13,7 +12,7 @@ import fitz
import rarfile
from PIL import Image, UnidentifiedImageError
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.db import models
from django.db.transaction import atomic
@@ -230,18 +229,12 @@ class ComicBook(models.Model):
def page_count(self):
return ComicPage.objects.filter(Comic=self).count()
@dataclass
class Navigation:
next_path: str
prev_path: str
cur_path: str
def nav(self, user):
return self.Navigation(
next_path=self.nav_get_next_comic(user),
prev_path=self.nav_get_prev_comic(user),
cur_path=urlsafe_base64_encode(self.selector.bytes)
)
return {
"next_path": self.nav_get_next_comic(user),
"prev_path": self.nav_get_prev_comic(user),
"cur_path": urlsafe_base64_encode(self.selector.bytes)
}
def nav_get_prev_comic(self, user) -> str:
base_dir = settings.COMIC_BOOK_VOLUME

View File

@@ -11,13 +11,13 @@
<meta name="author" content="Ajurna">
<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_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 -->
<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 "reveal.js/css/reveal.css" %}" rel="stylesheet">#}
@@ -28,7 +28,7 @@
<body>
<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">
<span class="navbar-toggler-icon"></span>
</button>
@@ -56,7 +56,7 @@
<!-- /.container -->
<footer class="footer mt-auto py-3">
<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>
</footer>

View File

@@ -8,7 +8,7 @@
<div class="row">
<div class="input-group">
<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=".read">Read</button>
<button class="btn btn-outline-secondary filters" type="button" data-filter=".unread">Unread</button>
@@ -17,8 +17,8 @@
Actions
</button>
<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" 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 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 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>#}
</div>
</div>
@@ -27,10 +27,10 @@
</div>
</div>
<div class="container comic-container">
<div class="row grid">
<div class="row grid ">
{% for file in files %}
<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' %}
<a href="{% url "comic_list" file.selector %}">
{% elif file.item_type == 'ComicBook' %}
@@ -38,12 +38,12 @@
{% endif %}
{% 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 %}
{% 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' %}
<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 %}
</a>
@@ -58,28 +58,33 @@
</a>
</h5>
<p class="card-text">
<figure class="text-center w-100 mb-0">{{ file.total_read }} / {{ file.total }}</figure>
<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>
</p>
<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" 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 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 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">
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
</button>
<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" 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 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 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' %}
<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 %}
{# <button type="button" class="btn btn-primary dropdown-item" title="Edit Comic"><i class="fas fa-edit">Edit Comic</i></button>#}
</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>
@@ -90,76 +95,5 @@
{% endblock %}
{% block script %}
<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>
<script type="text/javascript" src="{% static "js/comic_list.min.js" %}"></script>
{% endblock %}

View File

@@ -1,17 +1,18 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="reveal" id="comic_box">
<div class="slides" onclick="nextPage()">
<div id="slides_div" class="slides">
{% for page in pages %}
<section data-menu-title="{{ page.page_file_name }}">
{% 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 }}">
{% 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 %}
</section>
{% endfor %}
@@ -21,65 +22,7 @@
{% endblock %}
{% block script %}
<script>
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({{ 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>
{{ nav|json_script:"nav" }}
{{ status.last_read_page|json_script:"last_read_page" }}
<script type="text/javascript" src="{% static "js/read_comic.min.js" %}"></script>
{% endblock %}

View File

@@ -24,150 +24,7 @@
{% block script %}
<script type="text/javascript" src="{% static "pdfjs/build/pdf.js" %}"></script>
<script>
// If absolute URL from the remote server is provided, configure the CORS
// header on that server.
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>
{{ nav|json_script:"nav" }}
{{ status.last_read_page|json_script:"last_read_page" }}
<script type="text/javascript" src="{% static 'js/read_comic_pdf.min.js' %}"></script>
{% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
@@ -40,83 +41,5 @@
{% endblock %}
{% block script %}
<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>
<script type="text/javascript" src="{% static "js/recent_comics.min.js" %}"></script>
{% endblock %}

View File

@@ -86,6 +86,9 @@ class DirFile:
item_type: str = ''
percent: int = 0
selector: str = ''
total: int = None
total_read: int = None
total_unread: int = None
def __post_init__(self):
self.item_type = type(self.obj).__name__
@@ -94,10 +97,14 @@ class DirFile:
total_adjustment = 1
if isinstance(self.obj, Directory):
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:
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:
self.percent = 0
self.selector = self.obj.url_safe_selector
if isinstance(self.obj, Directory):
self.name = self.obj.name

21
poetry.lock generated
View File

@@ -119,6 +119,21 @@ python-versions = ">=3.6"
beautifulsoup4 = ">=4.8.0"
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]]
name = "django-extensions"
version = "3.1.3"
@@ -455,7 +470,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "c099b73f4400e26ba585774697d71eb475d22e365ad1ce9e6699086b30f403ad"
content-hash = "71642aa577156d70c6033dbc260a2ab03d247a17d9b0b0500a9c9a0e0228fd68"
[metadata.files]
asgiref = [
@@ -553,6 +568,10 @@ django-bootstrap4 = [
{file = "django-bootstrap4-3.0.0.tar.gz", hash = "sha256:bffc96f65386fbd49cae1474393e01d4b414c12fcab0fff50545e6142e7ba19b"},
{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 = [
{file = "django-extensions-3.1.3.tar.gz", hash = "sha256:5f0fea7bf131ca303090352577a9e7f8bfbf5489bd9d9c8aea9401db28db34a0"},
{file = "django_extensions-3.1.3-py3-none-any.whl", hash = "sha256:50de8977794a66a91575dd40f87d5053608f679561731845edbd325ceeb387e3"},

View File

@@ -3,7 +3,7 @@ line_length = 119
[tool.poetry]
name = "cbwebreader"
version = "0.2.1"
version = "0.3.0"
description = "CBR/Z Web Reader"
authors = ["ajurna <ajurna@gmail.com>"]
license = "Creative Commons Attribution-ShareAlike 4.0 International License"
@@ -26,6 +26,7 @@ Pillow = "^8.2.0"
django-imagekit = "^4.0.2"
PyMuPDF = "^1.18.12"
django-bootstrap4 = "^3.0.0"
django-csp = "^3.7"
[tool.poetry.dev-dependencies]
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 {
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}
#comic_list caption{caption-side:top}.card_list_card{width:200px}.card .card-badge{position:absolute;top:10px;left:10px;padding:5px;color:#fff}

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()}})})});