V1 cleanup (#73)

* removed obsolete code

* removed obsolete code

* removed obsolete code and added type annotations

* removed obsolete code and added type annotations

* version bump

Co-authored-by: Peter Dwyer <peter.dwyer@clanwilliamhealth.com>
This commit is contained in:
2022-08-26 13:11:29 +01:00
committed by GitHub
parent 1aeda89b74
commit 40b79c2f5d
42 changed files with 79 additions and 2720 deletions

View File

@@ -22,7 +22,6 @@ SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", None)
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DJANGO_DEBUG', False) == 'True' DEBUG = os.getenv('DJANGO_DEBUG', False) == 'True'
# DEBUG = False
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",") ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",")
@@ -39,7 +38,6 @@ INSTALLED_APPS = (
'webpack_loader', 'webpack_loader',
'bootstrap4', 'bootstrap4',
"comic", "comic",
"comic_auth",
'django_extensions', 'django_extensions',
'imagekit', 'imagekit',
'django_boost', 'django_boost',
@@ -102,8 +100,6 @@ STATIC_URL = "/static/"
STATICFILES_DIRS = [ STATICFILES_DIRS = [
Path(BASE_DIR, "static"), Path(BASE_DIR, "static"),
# Path(BASE_DIR, "node_modules"),
Path(BASE_DIR, "frontend", "node_modules"),
Path(BASE_DIR, "frontend", "dist") Path(BASE_DIR, "frontend", "dist")
] ]
@@ -169,14 +165,6 @@ PERMISSIONS_POLICY = {
"usb": [], "usb": [],
} }
# SESSION_COOKIE_HTTPONLY = True
# SESSION_COOKIE_SECURE = True
# SESSION_COOKIE_SAMESITE = 'Strict'
# CSRF_COOKIE_HTTPONLY = True
# CSRF_COOKIE_SECURE = True
# CSRF_COOKIE_SAMESITE = 'Strict'
# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
REST_FRAMEWORK = { REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions, # Use Django's standard `django.contrib.auth` permissions,

View File

@@ -18,14 +18,14 @@ from django.conf.urls import include
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import path, re_path from django.urls import path, re_path
from rest_framework import routers, permissions
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from rest_framework_extensions.routers import ExtendedDefaultRouter
from django.views.generic import TemplateView from django.views.generic import TemplateView
from drf_yasg.views import get_schema_view
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions
from rest_framework_extensions.routers import ExtendedDefaultRouter
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from comic import rest, feeds
schema_view = get_schema_view( schema_view = get_schema_view(
openapi.Info( openapi.Info(
@@ -39,15 +39,8 @@ schema_view = get_schema_view(
permission_classes=[permissions.AllowAny] permission_classes=[permissions.AllowAny]
) )
import comic.views
import comic_auth.views
from comic import rest, feeds
router = ExtendedDefaultRouter() router = ExtendedDefaultRouter()
router.register(r'users', rest.UserViewSet) router.register(r'users', rest.UserViewSet)
# router.register(r'usermisc', rest.UserMiscViewSet)
# router.register(r'groups', rest.GroupViewSet)
router.register(r'browse', rest.BrowseViewSet, basename='browse') router.register(r'browse', rest.BrowseViewSet, basename='browse')
router.register(r'generate_thumbnail', rest.GenerateThumbnailViewSet, basename='generate_thumbnail') router.register(r'generate_thumbnail', rest.GenerateThumbnailViewSet, basename='generate_thumbnail')
router.register(r'read', rest.ReadViewSet, basename='read')\ router.register(r'read', rest.ReadViewSet, basename='read')\
@@ -60,11 +53,6 @@ router.register(r'initial_setup', rest.InitialSetup, basename='initial_setup')
urlpatterns = [ urlpatterns = [
# url(r"^$", comic.views.comic_redirect),
# url(r"^login/", comic_auth.views.comic_login),
# url(r"^logout/", comic_auth.views.comic_logout),
# url(r"^setup/", comic.views.initial_setup),
# url(r"^comic/", include("comic.urls")),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path("feed/<user_selector>/", feeds.RecentComicsAPI()), path("feed/<user_selector>/", feeds.RecentComicsAPI()),
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),

View File

@@ -1,49 +1,10 @@
import uuid
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.syndication.views import Feed from django.contrib.syndication.views import Feed
from django.db.models import Case, When, PositiveSmallIntegerField, F from django.db.models import Case, When, PositiveSmallIntegerField, F
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from .models import ComicBook, UserMisc, Directory from comic.models import ComicBook, UserMisc, Directory
class RecentComics(Feed):
title = "CBWebReader Recent Comics"
link = "/comics/"
description = "Recently added Comics"
user: User
def get_object(self, request: HttpRequest, user_selector: str, *args, **kwargs) -> UserMisc:
user_selector = uuid.UUID(bytes=urlsafe_base64_decode(user_selector))
user_misc = get_object_or_404(UserMisc, feed_id=user_selector)
self.user = user_misc.user
return user_misc.user
def items(self) -> ComicBook:
comics = ComicBook.objects.order_by("-date_added")
comics = comics.annotate(
classification=Case(
When(directory__isnull=True, then=Directory.Classification.C_18),
default=F('directory__classification'),
output_field=PositiveSmallIntegerField(choices=Directory.Classification.choices)
)
)
comics = comics.filter(classification__lte=self.user.usermisc.allowed_to_read)
return comics[:10]
def item_title(self, item: ComicBook) -> str:
return item.file_name
def item_description(self, item: ComicBook) -> str:
return item.date_added.strftime("%a, %e %b %Y %H:%M")
# item_link is only needed if NewsItem has no get_absolute_url method.
def item_link(self, item: ComicBook) -> str:
return reverse('read_comic', args=(urlsafe_base64_encode(item.selector.bytes),))
class RecentComicsAPI(Feed): class RecentComicsAPI(Feed):

View File

@@ -1,111 +0,0 @@
from os import path
from django import forms
from django.contrib.auth.models import User
from comic.models import Directory
class InitialSetupForm(forms.Form):
username = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"}))
email = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"}))
password = forms.CharField(widget=forms.PasswordInput(attrs={"class": "form-control"}))
password_confirm = forms.CharField(
widget=forms.PasswordInput(attrs={"class": "form-control"})
)
def clean(self):
form_data = self.cleaned_data
if form_data["password"] != form_data["password_confirm"]:
raise forms.ValidationError("Passwords do not match.")
if len(form_data["password"]) < 8:
raise forms.ValidationError("Password is too short")
return form_data
class AccountForm(forms.Form):
username = forms.CharField(
required=False,
widget=forms.TextInput(attrs={"class": "form-control disabled", "readonly": True}),
)
email = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"}))
password = forms.CharField(
required=False, widget=forms.PasswordInput(attrs={"class": "form-control"})
)
password_confirm = forms.CharField(
required=False,
widget=forms.PasswordInput(attrs={"class": "form-control"}),
)
def clean_email(self):
data = self.cleaned_data["email"]
user = User.objects.get(username=self.cleaned_data["username"])
if data == user.email:
return data
if User.objects.filter(email=data).exists():
raise forms.ValidationError("Email Address is in use")
return data
def clean(self):
form_data = self.cleaned_data
if form_data["password"] != form_data["password_confirm"]:
raise forms.ValidationError("Passwords do not match.")
if len(form_data["password"]) < 8 & len(form_data["password"]) != 0:
raise forms.ValidationError("Password is too short")
return form_data
class AddUserForm(forms.Form):
username = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"}))
email = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"}))
password = forms.CharField(widget=forms.PasswordInput(attrs={"class": "form-control"}))
password_confirm = forms.CharField(
widget=forms.PasswordInput(attrs={"class": "form-control"})
)
def clean_username(self):
data = self.cleaned_data["username"]
if User.objects.filter(username=data).exists():
raise forms.ValidationError("This username Exists.")
return data
def clean_email(self):
data = self.cleaned_data["email"]
if User.objects.filter(email=data).exists():
raise forms.ValidationError("Email Address is in use")
return data
def clean(self):
form_data = self.cleaned_data
if form_data["password"] != form_data["password_confirm"]:
raise forms.ValidationError("Passwords do not match.")
if len(form_data["password"]) < 8:
raise forms.ValidationError("Password is too short")
return form_data
class EditUserForm(forms.Form):
username = forms.CharField(
required=False,
widget=forms.TextInput(attrs={"class": "form-control disabled", "readonly": True}),
)
email = forms.EmailField(widget=forms.TextInput(attrs={"class": "form-control"}))
password = forms.CharField(
required=False, widget=forms.PasswordInput(attrs={"class": "form-control"})
)
allowed_to_read = forms.ChoiceField(choices=Directory.Classification.choices)
@staticmethod
def get_initial_values(user):
out = {"username": user.username, "email": user.email, "allowed_to_read": user.usermisc.allowed_to_read}
return out
def clean_password(self):
data = self.cleaned_data["password"]
if len(data) < 8 & len(data) != 0:
raise forms.ValidationError("Password is too short")
return data
class DirectoryEditForm(forms.Form):
classification = forms.ChoiceField(choices=Directory.Classification.choices)

View File

@@ -3,21 +3,21 @@ import mimetypes
import uuid import uuid
import zipfile import zipfile
from functools import reduce from functools import reduce
from itertools import zip_longest, chain from itertools import zip_longest
from pathlib import Path from pathlib import Path
from typing import Optional, List, Union, Tuple from typing import Optional, List, Union, Tuple, Final
# noinspection PyPackageRequirements
import fitz import fitz
import rarfile import rarfile
from PIL import Image, UnidentifiedImageError from PIL import Image, UnidentifiedImageError
from PIL.Image import Image as Image_type
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User, AbstractUser 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.models import UniqueConstraint from django.db.models import UniqueConstraint
from django.db.transaction import atomic from django.db.transaction import atomic
from django.templatetags.static import static
from django.utils.http import urlsafe_base64_encode
from django_boost.models.fields import AutoOneToOneField from django_boost.models.fields import AutoOneToOneField
from imagekit.models import ProcessedImageField from imagekit.models import ProcessedImageField
from imagekit.processors import ResizeToFill from imagekit.processors import ResizeToFill
@@ -57,34 +57,14 @@ class Directory(models.Model):
return "Directory: {0}; {1}".format(self.name, self.parent) return "Directory: {0}; {1}".format(self.name, self.parent)
@property @property
def title(self): def title(self) -> str:
return self.name return self.name
@property @property
def type(self): def type(self) -> str:
return 'Directory' return 'Directory'
def mark_read(self, user): def generate_thumbnail(self) -> None:
books = ComicBook.objects.filter(directory=self)
for book in books:
book.mark_read(user)
def mark_unread(self, user):
books = ComicBook.objects.filter(directory=self)
for book in books:
book.mark_unread(user)
def get_thumbnail_url(self):
if self.thumbnail:
return self.thumbnail.url
else:
self.generate_thumbnail()
if self.thumbnail:
return self.thumbnail.url
else:
return static('img/placeholder.png')
def generate_thumbnail(self):
book: ComicBook = ComicBook.objects.filter(directory=self).order_by('file_name').first() book: ComicBook = ComicBook.objects.filter(directory=self).order_by('file_name').first()
if not book: if not book:
return return
@@ -105,7 +85,7 @@ class Directory(models.Model):
else: else:
return Path(path_items[0]) return Path(path_items[0])
def get_path_items(self, p: Optional[List] = None) -> List[str]: def get_path_items(self, p: Optional[List] = None) -> List[Path]:
if p is None: if p is None:
p = [] p = []
p.append(self.name) p.append(self.name)
@@ -113,7 +93,7 @@ class Directory(models.Model):
self.parent.get_path_items(p) self.parent.get_path_items(p)
return p return p
def get_path_objects(self, p=None): def get_path_objects(self, p=None) -> List["Directory"]:
if p is None: if p is None:
p = [] p = []
p.append(self) p.append(self)
@@ -121,14 +101,6 @@ class Directory(models.Model):
self.parent.get_path_objects(p) self.parent.get_path_objects(p)
return p return p
@property
def url_safe_selector(self):
return urlsafe_base64_encode(self.selector.bytes)
def set_classification(self, form_data):
self.classification = form_data['classification']
self.save()
class ComicBook(models.Model): class ComicBook(models.Model):
file_name = models.TextField() file_name = models.TextField()
@@ -148,36 +120,17 @@ class ComicBook(models.Model):
UniqueConstraint(fields=['directory', 'file_name'], name='one_comic_name_per_directory') UniqueConstraint(fields=['directory', 'file_name'], name='one_comic_name_per_directory')
] ]
def __str__(self): def __str__(self) -> str:
return self.file_name return self.file_name
@property @property
def title(self): def title(self) -> str:
return self.file_name return self.file_name
@property @property
def type(self): def type(self) -> str:
return 'ComicBook' return 'ComicBook'
def mark_read(self, user: User):
status, _ = ComicStatus.objects.get_or_create(comic=self, user=user)
status.mark_read()
def mark_unread(self, user: User):
status, _ = ComicStatus.objects.get_or_create(comic=self, user=user)
status.mark_unread()
def mark_previous(self, user):
books = ComicBook.objects.filter(directory=self.directory).order_by('file_name')
for book in books:
if book == self:
break
book.mark_read(user)
@property
def url_safe_selector(self):
return urlsafe_base64_encode(self.selector.bytes)
def get_pdf(self) -> Path: def get_pdf(self) -> Path:
base_dir = settings.COMIC_BOOK_VOLUME base_dir = settings.COMIC_BOOK_VOLUME
if self.directory: if self.directory:
@@ -185,7 +138,7 @@ class ComicBook(models.Model):
else: else:
return Path(base_dir, self.file_name) return Path(base_dir, self.file_name)
def get_image(self, page: int): def get_image(self, page: int) -> Union[Tuple[io.BytesIO, Image_type], Tuple[bool, bool]]:
base_dir = settings.COMIC_BOOK_VOLUME base_dir = settings.COMIC_BOOK_VOLUME
if self.directory: if self.directory:
archive_path = Path(base_dir, self.directory.path, self.file_name) archive_path = Path(base_dir, self.directory.path, self.file_name)
@@ -196,7 +149,7 @@ class ComicBook(models.Model):
except rarfile.NotRarFile: except rarfile.NotRarFile:
archive = zipfile.ZipFile(archive_path) archive = zipfile.ZipFile(archive_path)
except zipfile.BadZipfile: except zipfile.BadZipfile:
return False return False, False
page_obj = ComicPage.objects.get(Comic=self, index=page) page_obj = ComicPage.objects.get(Comic=self, index=page)
try: try:
@@ -207,22 +160,13 @@ class ComicBook(models.Model):
out = (archive.open(page_obj.page_file_name), page_obj.content_type) out = (archive.open(page_obj.page_file_name), page_obj.content_type)
return out return out
def get_thumbnail_url(self): def generate_thumbnail_pdf(self, page_index: int = 0) -> Tuple[io.BytesIO, Image_type, str]:
if self.thumbnail: img, pil_data = self._get_pdf_image(page_index if page_index else 0)
return self.thumbnail.url return img, pil_data, 'Image/JPEG'
else:
self.generate_thumbnail()
return self.thumbnail.url
def generate_thumbnail(self, page_index: int = None): def generate_thumbnail_archive(self, page_index: int = 0) -> Union[Tuple[io.BytesIO, Image_type, str],
Tuple[bool, bool, bool]]:
if Path(self.file_name).suffix.lower() == '.pdf': img, content_type, pil_data = False, False, False
if page_index:
img, pil_data = self._get_pdf_image(page_index)
else:
img, pil_data = self._get_pdf_image(0)
content_type = 'Image/JPEG'
else:
if page_index: if page_index:
img, content_type = self.get_image(page_index) img, content_type = self.get_image(page_index)
pil_data = Image.open(img) pil_data = Image.open(img)
@@ -234,11 +178,15 @@ class ComicBook(models.Model):
break break
except UnidentifiedImageError: except UnidentifiedImageError:
continue continue
try: return img, pil_data, content_type
img
content_type def generate_thumbnail(self, page_index: int = None) -> None:
pil_data
except NameError: if Path(self.file_name).suffix.lower() == '.pdf':
img, pil_data, content_type = self.generate_thumbnail_pdf(page_index if page_index else 0)
else:
img, pil_data, content_type = self.generate_thumbnail_archive(page_index)
if not img:
return return
self.thumbnail = InMemoryUploadedFile( self.thumbnail = InMemoryUploadedFile(
img, img,
@@ -250,133 +198,25 @@ class ComicBook(models.Model):
) )
self.save() self.save()
def _get_pdf_image(self, page_index: int): def _get_pdf_image(self, page_index: int) -> Tuple[io.BytesIO, Image_type]:
# noinspection PyTypeChecker # noinspection PyUnresolvedReferences
doc = fitz.open(self.get_pdf()) doc = fitz.open(self.get_pdf())
page = doc[page_index] page = doc[page_index]
pix = page.get_pixmap() pix = page.get_pixmap()
mode = "RGBA" if pix.alpha else "RGB" mode: Final = "RGBA" if pix.alpha else "RGB"
pil_data = Image.frombytes(mode, [pix.width, pix.height], pix.samples) # noinspection PyTypeChecker
pil_data = Image.frombytes(mode, (pix.width, pix.height), pix.samples)
img = io.BytesIO() img = io.BytesIO()
pil_data.save(img, format="JPEG") pil_data.save(img, format="JPEG")
img.seek(0) img.seek(0)
return img, pil_data return img, pil_data
def is_last_page(self, page):
if (self.page_count - 1) == page:
return True
return False
@property @property
def page_count(self): def page_count(self) -> int:
return ComicPage.objects.filter(Comic=self).count() return ComicPage.objects.filter(Comic=self).count()
def nav(self, user):
next_path, next_type = self.nav_get_next_comic(user)
prev_path, prev_type = self.nav_get_prev_comic(user)
return {
"next_path": next_path,
"next_type": next_type,
"prev_path": prev_path,
"prev_type": prev_type,
"cur_path": urlsafe_base64_encode(self.selector.bytes)
}
def nav_get_prev_comic(self, user) -> str:
base_dir = settings.COMIC_BOOK_VOLUME
if self.directory:
folder = Path(base_dir, self.directory.path)
else:
folder = base_dir
dir_list = ComicBook.get_ordered_dir_list(folder)
comic_index = dir_list.index(self.file_name)
if comic_index == 0:
if self.directory:
comic_path = urlsafe_base64_encode(self.directory.selector.bytes), type(self.directory).__name__
else:
comic_path = "", None
else:
prev_comic = dir_list[comic_index - 1]
if Path(folder, prev_comic).is_dir():
if self.directory:
comic_path = urlsafe_base64_encode(self.directory.selector.bytes), type(self.directory).__name__
else:
comic_path = "", None
else:
try:
if self.directory:
book = ComicBook.objects.get(file_name=prev_comic, directory=self.directory)
else:
book = ComicBook.objects.get(file_name=prev_comic, directory__isnull=True)
except ComicBook.DoesNotExist:
if self.directory:
book = ComicBook.process_comic_book(Path(prev_comic), self.directory)
else:
book = ComicBook.process_comic_book(Path(prev_comic))
cs, _ = ComicStatus.objects.get_or_create(comic=book, user=user)
comic_path = urlsafe_base64_encode(book.selector.bytes), type(book).__name__
return comic_path
def nav_get_next_comic(self, user):
base_dir = settings.COMIC_BOOK_VOLUME
if self.directory:
folder = Path(base_dir, self.directory.path)
else:
folder = base_dir
dir_list = ComicBook.get_ordered_dir_list(folder)
comic_index = dir_list.index(self.file_name)
try:
next_comic = dir_list[comic_index + 1]
try:
if self.directory:
book = ComicBook.objects.get(file_name=next_comic, directory=self.directory)
else:
book = ComicBook.objects.get(file_name=next_comic, directory__isnull=True)
except ComicBook.DoesNotExist:
if self.directory:
book = ComicBook.process_comic_book(Path(next_comic), self.directory)
else:
book = ComicBook.process_comic_book(Path(next_comic))
except ComicBook.MultipleObjectsReturned:
if self.directory:
books = ComicBook.objects.filter(file_name=next_comic, directory=self.directory).order_by('id')
else:
books = ComicBook.objects.get(file_name=next_comic, directory__isnull=True).order_by('id')
book = books.first()
books = books.exclude(id=book.id)
books.delete()
if type(book) is str:
raise IndexError
comic_path = urlsafe_base64_encode(book.selector.bytes), type(book).__name__
except IndexError:
if self.directory:
comic_path = urlsafe_base64_encode(self.directory.selector.bytes), type(self.directory).__name__
else:
comic_path = "", None
return comic_path
class DirFile:
def __init__(self):
self.name = ""
self.isdir = False
self.icon = ""
self.iscb = False
self.location = ""
self.label = ""
self.cur_page = 0
def __str__(self):
return self.name
@staticmethod @staticmethod
def process_comic_book(comic_file_path: Path, directory: "Directory" = False) -> Union["ComicBook", Path]: def process_comic_book(comic_file_path: Path, directory: "Directory" = False) -> Union["ComicBook", Path]:
"""
:type comic_file_path: str
:type directory: Directory
"""
try: try:
book = ComicBook.objects.get(file_name=comic_file_path.name, version=0) book = ComicBook.objects.get(file_name=comic_file_path.name, version=0)
book.directory = directory book.directory = directory
@@ -404,19 +244,8 @@ class ComicBook(models.Model):
page.save() page.save()
return book return book
@staticmethod
def get_ordered_dir_list(folder: Path) -> List[str]:
directories = []
files = []
for item in folder.glob('*'):
if item.is_dir():
directories.append(item)
else:
files.append(item)
return [x.name for x in chain(sorted(directories), sorted(files))]
@property @property
def get_archive_path(self): def get_archive_path(self) -> Path:
if self.directory: if self.directory:
return Path(settings.COMIC_BOOK_VOLUME, self.directory.get_path(), self.file_name) return Path(settings.COMIC_BOOK_VOLUME, self.directory.get_path(), self.file_name)
else: else:
@@ -441,13 +270,13 @@ class ComicBook(models.Model):
raise NotCompatibleArchive raise NotCompatibleArchive
@staticmethod @staticmethod
def get_archive_files(archive): def get_archive_files(archive) -> List[Tuple[str, str]]:
return [ return [
(x, mimetypes.guess_type(x)[0]) for x in sorted(archive.namelist()) (x, mimetypes.guess_type(x)[0]) for x in sorted(archive.namelist())
if not x.endswith('/') and mimetypes.guess_type(x)[0] if not x.endswith('/') and mimetypes.guess_type(x)[0]
] ]
def verify_pages(self, pages: Optional["ComicPage"] = None): def verify_pages(self, pages: Optional["ComicPage"] = None) -> None:
if not pages: if not pages:
pages = ComicPage.objects.filter(Comic=self) pages = ComicPage.objects.filter(Comic=self)
@@ -493,7 +322,8 @@ class ComicPage(models.Model):
class ComicStatus(models.Model): class ComicStatus(models.Model):
user = models.ForeignKey(User, unique=False, null=False, on_delete=models.CASCADE) user = models.ForeignKey(User, unique=False, null=False, on_delete=models.CASCADE)
comic = models.ForeignKey(ComicBook, unique=False, blank=False, null=False, on_delete=models.CASCADE, to_field="selector") comic = models.ForeignKey(ComicBook, unique=False, blank=False, null=False, on_delete=models.CASCADE,
to_field="selector")
last_read_page = models.IntegerField(default=0) last_read_page = models.IntegerField(default=0)
unread = models.BooleanField(default=True) unread = models.BooleanField(default=True)
finished = models.BooleanField(default=False) finished = models.BooleanField(default=False)
@@ -503,23 +333,10 @@ class ComicStatus(models.Model):
UniqueConstraint(fields=['user', 'comic'], name='one_per_user_per_comic') UniqueConstraint(fields=['user', 'comic'], name='one_per_user_per_comic')
] ]
def mark_read(self): def __str__(self) -> str:
page_count = ComicPage.objects.filter(Comic=self.comic).count()
self.unread = False
self.finished = True
self.last_read_page = page_count - 1
self.save()
def mark_unread(self):
self.unread = True
self.finished = False
self.last_read_page = 0
self.save()
def __str__(self):
return self.__repr__() return self.__repr__()
def __repr__(self): def __repr__(self) -> str:
return ( return (
f"<ComicStatus:{self.user.username}:{self.comic.file_name}:{self.last_read_page}:" f"<ComicStatus:{self.user.username}:{self.comic.file_name}:{self.last_read_page}:"
f"{self.unread}:{self.finished}" f"{self.unread}:{self.finished}"

View File

@@ -5,7 +5,7 @@ from typing import Union, NamedTuple, List
from uuid import UUID from uuid import UUID
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Count, Case, When, F, PositiveSmallIntegerField, Q from django.db.models import Count, Case, When, F, PositiveSmallIntegerField, Q
@@ -88,33 +88,6 @@ class UserViewSet(viewsets.ModelViewSet):
return Response(serializer.errors, status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status.HTTP_400_BAD_REQUEST)
class UserMiscSerializer(serializers.ModelSerializer):
class Meta:
model = models.UserMisc
fields = ['user', 'feed_id', 'allowed_to_read']
class UserMiscViewSet(viewsets.ModelViewSet):
queryset = models.UserMisc.objects.all()
serializer_class = UserMiscSerializer
permission_classes = [permissions.IsAdminUser]
class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Group
fields = ['url', 'name']
class GroupViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to be viewed or edited.
"""
queryset = Group.objects.all()
serializer_class = GroupSerializer
permission_classes = [permissions.IsAuthenticated]
class BrowseFileField(serializers.FileField): class BrowseFileField(serializers.FileField):
def to_representation(self, value): def to_representation(self, value):
if not value: if not value:

View File

@@ -1,85 +0,0 @@
{% load bootstrap4 %}
{% load static %}
{% load sri %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="Ajurna">
<link rel="icon" href="{% static "img/logo.svg" %}">
<title>{% block title %}CB Web Reader{% endblock %}</title>
<!-- Bootstrap core CSS -->
{% sri_static "bootstrap/dist/css/bootstrap.min.css" %}
{% sri_static "datatables.net-bs4/css/dataTables.bootstrap4.min.css" %}
<!-- Custom styles for this template -->
{% sri_static "css/base.min.css" %}
{% sri_static "@fortawesome/fontawesome-free/css/all.min.css" %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light" aria-label="menu">
<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>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
{% for page, link in menu.menu_items.items %}
<a class="nav-item nav-link {% if menu.current_page == page %}active{% endif %}" href="{{ link }}">{{ page }}</a>
{% endfor %}
</div>
</div>
</nav>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{% if breadcrumbs %}
{% include "comic/breadcrumbs.html" %}
{% else %}
<li class="breadcrumb-item"><a href="#">Home</a></li>
{% endif %}
</ol>
</nav>
{% block content %}{% endblock %}
<!-- /.container -->
<footer class="footer mt-auto py-3">
<div class="container text-center">
<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>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
{# {% bootstrap_javascript jquery='full' %}#}
<script type="text/javascript">
globalThis.regeneratorRuntime = undefined;
</script>
{% sri_static "jquery/dist/jquery.min.js" %}
{% sri_static "bootstrap/dist/js/bootstrap.bundle.js" %}
{% sri_static "datatables.net/js/jquery.dataTables.min.js" %}
{% sri_static "datatables.net-bs4/js/dataTables.bootstrap4.min.js" %}
{% sri_static "js-cookie/src/js.cookie.js" %}
{% sri_static "reveal.js/dist/reveal.js" %}
{% sri_static "reveal.js-menu/menu.js" %}
{% sri_static "hammerjs/hammer.js" %}
{% sri_static "isotope-layout/dist/isotope.pkgd.min.js" %}
{% block script %}
{% endblock %}
</body>
</html>

View File

@@ -1,7 +0,0 @@
{% for crumb in breadcrumbs %}
{% if not forloop.last %}
<li class="breadcrumb-item"><a href="{{ crumb.url }}">{{ crumb.name }}</a></li>
{% else %}
<li class="breadcrumb-item active" aria-current="page">{{ crumb.name }}</li>
{% endif %}
{% endfor %}

View File

@@ -1,124 +0,0 @@
{% extends "base.html" %}
{% load sri %}
{% load bootstrap4 %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container">
<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">
<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>
<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">
Actions
</button>
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
<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>
</div>
</div>
</div>
</div>
<div class="container comic-container">
<div class="row grid card-group">
{% for file in files %}
<div class="m-2 grid-item {% if file.percent == 100 %}read{% else %}unread{% endif %}">
<div class="card card_list_card">
{% if file.item_type == 'Directory' %}
<a href="{% url "comic_list" file.selector %}">
{% elif file.item_type == 'ComicBook' %}
<a href="{% url "read_comic" file.selector %}">
{% endif %}
{% if file.obj.thumbnail %}
<img src="{{file.obj.thumbnail.url}}" class="card-img-top" alt="{{ file.name }}" alt_src="{% static "/img/placeholder.png" %}">
{% else %}
{% if file.item_type == 'Directory' %}
<img src="{% url 'directory_thumbnail' file.selector %}" class="card-img-top" alt="{{ file.name }}" alt_src="{% static "/img/placeholder.png" %}">
{% elif file.item_type == 'ComicBook' %}
<img src="{% url 'comic_thumbnail' file.selector %}" class="card-img-top" alt="{{ file.name }}" alt_src="{% static "/img/placeholder.png" %}">
{% endif %}
{% endif %}
</a>
<div class="card-body">
<h5 class="card-title {{ file.selector }}">
{% if file.item_type == 'Directory' %}
<a href="{% url "comic_list" file.selector %}" class="search-name">
{% elif file.item_type == 'ComicBook' %}
<a href="{% url "read_comic" file.selector %}" class="search-name">
{% endif %}
{{ file.name }}
</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" aria-valuenow="{{ file.percent }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</p>
<div class="btn-group w-100" role="group" aria-label="Comic Actions">
<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">F
</button>
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
<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 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>
{% else %}
<button type="button" class="btn btn-primary dropdown-item modal-button" title="Edit Comic" data-toggle="modal" data-target="#editModal" selector="{{ file.selector }}" itemtype="{{ file.item_type }}"><i class="fas fa-edit">Edit Comic</i></button>
{% endif %}
</div>
</div>
</div>
</div>
{% if file.total_unread and file.item_type == 'Directory' %}
<span class="badge rounded-pill bg-primary unread-badge">{{ file.total_unread }}</span>
{% endif %}
<span class="badge rounded-pill bg-warning {{ file.selector }} classification-badge" classification="{{ file.obj.classification }}">{{ file.obj.get_classification_display }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{% csrf_token %}
{% bootstrap_form form %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="save_button">Save changes</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block script %}
{{ js_urls|json_script:'js_urls' }}
{% sri_static "js/comic_list.min.js" %}
{% endblock %}

View File

@@ -1,29 +0,0 @@
{% extends "base.html" %}
{% load sri %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="reveal" id="comic_box">
<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 class="comic_embed" type="{{ page.content_type }}" src="{% url "get_image" nav.cur_path page.index %}"></p>
{% endif %}
</section>
{% endfor %}
</div>
</div>
{% endblock %}
{% block script %}
{{ nav|json_script:"nav" }}
{{ status.last_read_page|json_script:"last_read_page" }}
{% sri_static "js/read_comic.min.js" %}
{% endblock %}

View File

@@ -1,31 +0,0 @@
{% extends "base.html" %}
{% load sri %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container m-0">
<div class="row justify-content-center w-100">
<div class="btn-group" role="group" aria-label="Basic example">
<button id="prev" class="btn btn-secondary">Previous</button>
<button id="pages" class="btn btn-secondary"><span id="page_num"></span> / <span id="page_count"></span></button>
<button id="next" class="btn btn-secondary">Next</button>
</div>
</div>
<div class="row w-100">
<canvas id="the-canvas" width="100%"></canvas>
</div>
</div>
{% endblock %}
{% block script %}
{% sri_static "pdfjs-dist/build/pdf.min.js" %}
{{ nav|json_script:"nav" }}
{{ status.last_read_page|json_script:"last_read_page" }}
{% sri_static 'js/read_comic_pdf.min.js' %}
{% endblock %}

View File

@@ -1,46 +0,0 @@
{% extends "base.html" %}
{% load sri %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container">
<form id="comic_form" method="post" action="/comic/edit/">
{% csrf_token %}
<table class="table table-bordered table-striped table-hover" id="comic_list">
<caption><h2>Recent Comics - <a href="/comic/feed/{{ feed_id }}/">Feed</a></h2>
mark selected issues as:
<select name="func" id="func_selector">
<option value="choose">Choose...</option>
<option value="read">Read</option>
<option value="unread">Un-Read</option>
</select>
</caption>
<thead>
<tr>
<th id="select-all"><input type="checkbox" id="select-all-cb"></th>
<th>
<div class="text-center"><span class="glyphicon glyphicon-file"></span></div>
</th>
<th width="100%">File/Folder</th>
<th>Date&nbsp;Added</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr class="clickable-row" data-href="/comic/">
<td></td>
<td></td>
<td>loading data</td>
<td></td>
</tr>
</tbody>
</table>
</form>
</div>
{% endblock %}
{% block script %}
{% sri_static "js/recent_comics.min.js" %}
{% endblock %}

View File

@@ -1,37 +0,0 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container">
{% if error_message %}
<div class="alert alert-danger" role="alert">{{ error_message|safe }}</div>
{% endif %}
{% if success_message %}
<div class="alert alert-success" role="alert">{{ success_message|safe }}</div>
{% endif %}
<form method="POST">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-secondary">Submit</button>
{% endbuttons %}
</form>
{% endblock %}
{% block content2 %}
{% if error_message %}
<div class="alert alert-danger" role="alert">{{ error_message }}</div>
{% endif %}
<form method="POST">
{% csrf_token %}
<div class="form-group">
<label for="base_directory">Base Directory</label>
<input type="text" class="form-control" id="base_directory" name="base_directory" placeholder="Base Directory" value="{{ base_dir.value }}">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
{% endblock %}

View File

@@ -1,36 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
{% if error_message %}
<div class="alert alert-danger" role="alert">{{ error_message|safe }}</div>
{% endif %}
{% if success_message %}
<div class="alert alert-success" role="alert">{{ success_message|safe }}</div>
{% endif %}
<form method="POST">
{% csrf_token %}
{% for item in form %}
<div class="form-group">
<label for="{{ item.id_for_label }}">{{ item.help_text }}</label>
{{ item }}
</div>
{% endfor %}
<button type="submit" class="btn btn-default">Submit</button>
</form>
{% endblock %}
{% block content2 %}
{% if error_message %}
<div class="alert alert-danger" role="alert">{{ error_message }}</div>
{% endif %}
<form method="POST">
{% csrf_token %}
<div class="form-group">
<label for="base_directory">Base Directory</label>
<input type="text" class="form-control" id="base_directory" name="base_directory" placeholder="Base Directory" value="{{ base_dir.value }}">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
{% endblock %}

View File

@@ -1,34 +0,0 @@
{% extends "base.html" %}
{% block title %}CBWebReader - Users{% endblock %}
{% block content %}
<div class="container">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>#</th>
<th>Username</th>
<th>Email</th>
<th>Superuser</th>
<th>Classification</th>
</tr>
</thead>
<tbody data-link="row" class="rowlink">
{% for user in users %}
<tr>
<td>{{user.id}}</td>
<td><a href="{% url 'user_details' user.id %}">{{user.username}}</a></td>
<td>{{user.email}}</td>
<td>{{user.is_superuser}}</td>
<td>{{ user.usermisc.get_allowed_to_read_display }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a class="btn btn-secondary" href="{% url 'add_users' %}" role="button">Add User</a>
</div>
{% endblock %}
{% block script %}
{% endblock %}

View File

@@ -1,4 +0,0 @@
from django import template
register = template.Library()

View File

@@ -1,273 +0,0 @@
import json
from pathlib import Path
from django.conf import settings
from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.utils.http import urlsafe_base64_encode
from comic.models import ComicBook, ComicPage, ComicStatus, Directory
from comic.util import generate_directory
# from os import path
class ComicBookTests(TestCase):
def setUp(self):
settings.COMIC_BOOK_VOLUME = Path(Path.cwd(), 'test_comics')
User.objects.create_user("test", "test@test.com", "test")
user = User.objects.first()
ComicBook.process_comic_book(Path("test1.rar"))
book = ComicBook.process_comic_book(Path("test2.rar"))
status = ComicStatus(user=user, comic=book, last_read_page=2, unread=False)
status.save()
ComicBook.process_comic_book(Path("test4.rar"))
def test_comic_processing(self):
book = ComicBook.objects.get(file_name="test1.rar")
self.assertEqual(book.file_name, "test1.rar")
page0 = ComicPage.objects.get(Comic=book, index=0)
self.assertEqual(page0.page_file_name, "img1.jpg")
self.assertEqual(page0.content_type, "image/jpeg")
page1 = ComicPage.objects.get(Comic=book, index=1)
self.assertEqual(page1.page_file_name, "img2.png")
self.assertEqual(page1.content_type, "image/png")
page2 = ComicPage.objects.get(Comic=book, index=2)
self.assertEqual(page2.page_file_name, "img3.gif")
self.assertEqual(page2.content_type, "image/gif")
page3 = ComicPage.objects.get(Comic=book, index=3)
self.assertEqual(page3.page_file_name, "img4.bmp")
self.assertEqual(page3.content_type, "image/bmp")
self.assertEqual(ComicPage.objects.filter(Comic=book).count(), 4)
def test_page_count(self):
book = ComicBook.objects.get(file_name="test1.rar")
self.assertEqual(book.page_count, 4)
def test_is_last_page(self):
book = ComicBook.objects.get(file_name="test1.rar")
self.assertEqual(book.is_last_page(3), True)
self.assertEqual(book.is_last_page(2), False)
def test_get_image(self):
book = ComicBook.objects.get(file_name="test1.rar")
img, content_type = book.get_image(0)
self.assertEqual(content_type, "image/jpeg")
self.assertEqual(img.read(), b"img1.jpg")
def test_nav_with_folder_above(self):
user = User.objects.get(username="test")
generate_directory(user)
book = ComicBook.objects.get(file_name="test1.rar")
nav = book.nav(user)
self.assertEqual(nav['prev_path'], "")
self.assertEqual(nav['cur_path'], urlsafe_base64_encode(book.selector.bytes))
def test_nav_with_comic_above(self):
user = User.objects.get(username="test")
generate_directory(user)
prev_book = ComicBook.objects.get(file_name="test1.rar", directory__isnull=True)
book = ComicBook.objects.get(file_name="test2.rar", directory__isnull=True)
next_book = ComicBook.objects.get(file_name="test3.rar", directory__isnull=True)
nav = book.nav(user)
self.assertEqual(nav['prev_path'], urlsafe_base64_encode(prev_book.selector.bytes))
self.assertEqual(nav['cur_path'], urlsafe_base64_encode(book.selector.bytes))
self.assertEqual(nav['next_path'], urlsafe_base64_encode(next_book.selector.bytes))
def test_nav_with_comic_below(self):
user = User.objects.get(username="test")
generate_directory(user)
book = ComicBook.objects.get(file_name="test1.rar", directory__isnull=True)
next_book = ComicBook.objects.get(file_name="test2.rar", directory__isnull=True)
nav = book.nav(user)
self.assertEqual(nav['cur_path'], urlsafe_base64_encode(book.selector.bytes))
self.assertEqual(nav['next_path'], urlsafe_base64_encode(next_book.selector.bytes))
def test_nav_with_nothing_below(self):
user = User.objects.get(username="test")
generate_directory(user)
book = ComicBook.objects.get(file_name="test4.rar")
nav = book.nav(user)
self.assertEqual(nav['cur_path'], urlsafe_base64_encode(book.selector.bytes))
self.assertEqual(nav['next_path'], "")
def test_generate_directory(self):
user = User.objects.get(username="test")
folders = generate_directory(user)
dir1 = folders[0]
self.assertEqual(dir1.name, "test_folder")
self.assertEqual(dir1.item_type, "Directory")
dir2 = folders[1]
self.assertEqual(dir2.name, "test1.rar")
self.assertEqual(dir2.item_type, "ComicBook")
dir3 = folders[2]
self.assertEqual(dir3.name, "test2.rar")
self.assertEqual(dir2.item_type, "ComicBook")
dir4 = folders[3]
self.assertEqual(dir4.name, "test3.rar")
self.assertEqual(dir4.item_type, "ComicBook")
def test_pages(self):
book = ComicBook.objects.get(file_name="test1.rar")
pages = [cp for cp in ComicPage.objects.filter(Comic=book).order_by("index")]
self.assertEqual(pages[0].page_file_name, "img1.jpg")
self.assertEqual(pages[0].index, 0)
self.assertEqual(pages[1].page_file_name, "img2.png")
self.assertEqual(pages[1].index, 1)
self.assertEqual(pages[2].page_file_name, "img3.gif")
self.assertEqual(pages[2].index, 2)
self.assertEqual(pages[3].page_file_name, "img4.bmp")
self.assertEqual(pages[3].index, 3)
def test_comic_list(self):
c = Client()
response = c.get("/comic/")
self.assertEqual(response.status_code, 302)
c.login(username="test", password="test")
response = c.get("/comic/")
self.assertEqual(response.status_code, 200)
user = User.objects.first()
generate_directory(user)
directory = Directory.objects.first()
response = c.get(f"/comic/{urlsafe_base64_encode(directory.selector.bytes)}/")
self.assertEqual(response.status_code, 200)
def test_recent_comics(self):
c = Client()
response = c.get("/comic/recent/")
self.assertEqual(response.status_code, 302)
c.login(username="test", password="test")
response = c.get("/comic/recent/")
self.assertEqual(response.status_code, 200)
def test_recent_comics_json(self):
c = Client()
response = c.post("/comic/recent/json/")
self.assertEqual(response.status_code, 302)
c.login(username="test", password="test")
user = User.objects.get(username="test")
folders = generate_directory(user)
req_data = {"start": "0", "length": "10", "search[value]": "", "order[0][dir]": "desc"}
response = c.post("/comic/recent/json/", req_data)
self.assertEqual(response.status_code, 200)
req_data["search[value]"] = "test1.rar"
response = c.post("/comic/recent/json/", req_data)
self.assertEqual(response.status_code, 200)
self.maxDiff = None
book = ComicBook.objects.get(file_name="test1.rar")
self.assertDictEqual(
json.loads(response.content),
{
"data": [
{
"date": book.date_added.strftime("%d/%m/%y-%H:%M"),
"icon": '<span class="fa fa-book"></span>',
"label": '<center><span class="label ' 'label-default">Unread</span></center>',
"name": "test1.rar",
"selector": urlsafe_base64_encode(book.selector.bytes),
"type": "book",
"url": f"/comic/read/" f"{urlsafe_base64_encode(book.selector.bytes)}/",
}
],
"recordsFiltered": 1,
"recordsTotal": 4,
},
)
req_data["search[value]"] = ""
req_data["order[0][dir]"] = 3
response = c.post("/comic/recent/json/", req_data)
self.assertEqual(response.status_code, 200)
def test_comic_edit(self):
c = Client()
book: ComicBook = ComicBook.objects.first()
user = User.objects.get(username="test")
response = c.get("/comic/edit/")
self.assertEqual(response.status_code, 302)
c.login(username="test", password="test")
response = c.get("/comic/edit/")
self.assertEqual(response.status_code, 405)
req_data = {"comic_list_length": 10, "func": "unread", "selected": book.url_safe_selector}
response = c.post("/comic/edit/", req_data)
self.assertEqual(response.status_code, 200)
status = ComicStatus.objects.get(comic=book, user=user)
self.assertEqual(status.last_read_page, 0)
self.assertTrue(status.unread)
self.assertFalse(status.finished)
req_data["func"] = "read"
response = c.post("/comic/edit/", req_data)
self.assertEqual(response.status_code, 200)
status.refresh_from_db()
self.assertEqual(status.last_read_page, 3)
self.assertFalse(status.unread)
self.assertTrue(status.finished)
req_data["func"] = "choose"
response = c.post("/comic/edit/", req_data)
self.assertEqual(response.status_code, 200)
status.refresh_from_db()
self.assertEqual(status.last_read_page, 3)
self.assertFalse(status.unread)
self.assertTrue(status.finished)
del req_data["selected"]
response = c.post("/comic/edit/", req_data)
self.assertEqual(response.status_code, 200)
def test_account_page(self):
c = Client()
user = User.objects.get(username="test")
self.assertEqual(user.username, "test")
response = c.get("/comic/account/")
self.assertEqual(response.status_code, 302)
c.login(username="test", password="test")
response = c.get("/comic/account/")
self.assertEqual(response.status_code, 200)
def test_file_not_in_archive(self):
c = Client()
user = User.objects.get(username="test")
book = ComicBook.objects.get(file_name='test1.rar')
page = ComicPage.objects.get(Comic=book, index=0)
page.page_file_name = 'doesnt_exist'
page.save()
generate_directory(user)
c.login(username="test", password="test")
book.verify_pages()
response = c.get(f"/comic/read/{urlsafe_base64_encode(book.selector.bytes)}/0/img")
self.assertEqual(response.status_code, 200)
def test_duplicate_pages(self):
c = Client()
user = User.objects.get(username="test")
generate_directory(user)
book = ComicBook.objects.get(file_name='test1.rar')
page = ComicPage.objects.get(Comic=book, index=0)
dup_page = ComicPage(Comic=book, index=0, page_file_name=page.page_file_name, content_type=page.content_type)
dup_page.save()
c.login(username="test", password="test")
book.verify_pages()
response = c.get(f"/comic/read/{urlsafe_base64_encode(book.selector.bytes)}/0/img")
self.assertEqual(response.status_code, 200)

View File

@@ -1,23 +0,0 @@
from django.urls import path
from . import feeds, views
urlpatterns = [
path("", views.comic_list, name="index"),
path("settings/users/", views.users_page, name="users"),
path("settings/users/<int:user_id>/", views.user_config_page, name="user_details"),
path("settings/users/add/", views.user_add_page, name="add_users"),
path("account/", views.account_page, name="account"),
path("read/<comic_selector>/", views.read_comic, name="read_comic"),
path("read/<comic_selector>/thumb", views.comic_thumbnail, name="comic_thumbnail"),
path("set_page/<comic_selector>/<int:page>/", views.set_read_page, name="set_read_page"),
path("read/<comic_selector>/<int:page>/img", views.get_image, name="get_image"),
path("read/<comic_selector>/pdf", views.get_pdf, name="get_pdf"),
path("recent/", views.recent_comics, name="recent_comics"),
path("recent/json/", views.recent_comics_json, name="recent_comics_json"),
path("edit/", views.comic_edit, name="comic_edit"),
# path("feed/<user_selector>/", feeds.RecentComics()),
path("<directory_selector>/", views.comic_list, name="comic_list"),
path("<directory_selector>/thumb", views.directory_thumbnail, name="directory_thumbnail"),
path("action/<operation>/<item_type>/<selector>/", views.perform_action, name="perform_action")
]

View File

@@ -1,50 +1,12 @@
from collections import OrderedDict
from dataclasses import dataclass from dataclasses import dataclass
from itertools import chain
from pathlib import Path
from typing import Union, Iterable
from django.conf import settings from .models import ComicBook, Directory
from django.contrib.auth.models import User
from django.db.models import Count, Q, F, Case, When, PositiveSmallIntegerField
from django.utils.http import urlsafe_base64_encode
from .models import ComicBook, Directory, ComicStatus
def generate_title_from_path(file_path: Path):
if file_path == "Home":
return "CBWebReader"
return f'CBWebReader - {" - ".join(p for p in file_path.parts)}'
class Menu:
def __init__(self, user, page=""):
"""
:type page: str
"""
self.menu_items = OrderedDict()
self.menu_items["Browse"] = "/comic/"
self.menu_items["Recent"] = "/comic/recent/"
self.menu_items["Account"] = "/comic/account/"
if user.is_superuser:
self.menu_items["Users"] = "/comic/settings/users/"
self.menu_items["Logout"] = "/logout/"
self.current_page = page
@dataclass()
class Breadcrumb: class Breadcrumb:
def __init__(self): name: str = 'Home'
self.name = "Home" selector: str = ''
self.url = "/comic/"
self.selector = ''
def __str__(self):
return self.name
def __unicode__(self):
return self.name
def generate_breadcrumbs_from_path(directory=False, book=False): def generate_breadcrumbs_from_path(directory=False, book=False):
@@ -59,197 +21,17 @@ def generate_breadcrumbs_from_path(directory=False, book=False):
else: else:
folders = [] folders = []
for item in folders[::-1]: for item in folders[::-1]:
bc = Breadcrumb() output.append(
bc.name = item.name Breadcrumb(
bc.url = "/comic/" + urlsafe_base64_encode(item.selector.bytes) name=item.name,
bc.selector = item.selector selector=item.selector
output.append(bc) )
)
if book: if book:
bc = Breadcrumb() output.append(
bc.name = book.file_name Breadcrumb(
bc.url = "/read/" + urlsafe_base64_encode(book.selector.bytes) name=book.file_name,
bc.selector = book.selector selector=book.selector
output.append(bc) )
)
return output return output
def generate_breadcrumbs_from_menu(paths):
output = [Breadcrumb()]
for item in paths:
bc = Breadcrumb()
bc.name = item[0]
bc.url = item[1]
output.append(bc)
return output
@dataclass
class DirFile:
obj: Union[Directory, ComicBook]
name: str = ''
item_type: str = ''
percent: int = 0
selector: str = ''
total: int = 0
total_read: int = 0
total_unread: int = 0
def __post_init__(self):
self.item_type = type(self.obj).__name__
if hasattr(self.obj, 'total') and hasattr(self.obj, 'total_read'):
# because pages count from zero.
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.total) * 100)
except ZeroDivisionError:
self.percent = 0
self.selector = self.obj.url_safe_selector
if isinstance(self.obj, Directory):
self.name = self.obj.name
elif isinstance(self.obj, ComicBook):
self.name = self.obj.file_name
@property
def type(self):
return 'ComicBook'
@property
def title(self):
return self.name
@property
def progress(self):
return self.total_read
@property
def thumbnail(self):
return '/error.jpg'
def generate_directory(user: User, directory=None):
"""
:type user: User
:type directory: Directory
"""
base_dir = settings.COMIC_BOOK_VOLUME
files = []
dir_path = Path(base_dir, directory.path) if directory else base_dir
dir_list = [x for x in sorted(dir_path.glob('*')) if x.is_dir()]
file_list = [x for x in sorted(dir_path.glob('*')) if x.is_file()]
dir_list_obj = Directory.objects.filter(name__in=[x.name for x in dir_list], parent=directory)
file_list_obj = ComicBook.objects.filter(file_name__in=[x.name for x in file_list], directory=directory)
for file in chain(file_list_obj, dir_list_obj):
if file.thumbnail and not Path(file.thumbnail.path).exists():
file.thumbnail.delete()
file.save()
dir_list_obj = dir_list_obj.annotate(
total=Count('comicbook', distinct=True),
total_read=Count('comicbook__comicstatus', Q(comicbook__comicstatus__finished=True,
comicbook__comicstatus__user=user), distinct=True)
)
# Create Missing Status
status_list = [x.comic for x in
ComicStatus.objects.filter(comic__in=file_list_obj, user=user).select_related('comic')]
new_status = [ComicStatus(comic=file, user=user) for file in file_list_obj if file not in status_list]
ComicStatus.objects.bulk_create(new_status)
file_list_obj = file_list_obj.annotate(
total=Count('comicpage', distinct=True),
total_read=F('comicstatus__last_read_page'),
finished=F('comicstatus__finished'),
unread=F('comicstatus__unread'),
user=F('comicstatus__user'),
classification=Case(
When(directory__isnull=True, then=Directory.Classification.C_18),
default=F('directory__classification'),
output_field=PositiveSmallIntegerField(choices=Directory.Classification.choices)
)
).filter(Q(user__isnull=True) | Q(user=user.id))
for directory_obj in dir_list_obj:
files.append(DirFile(directory_obj))
dir_list.remove(Path(dir_path, directory_obj.name))
for file_obj in file_list_obj:
files.append(DirFile(file_obj))
file_list.remove(Path(dir_path, file_obj.file_name))
for directory_name in dir_list:
if directory:
directory_obj = Directory(name=directory_name.name, parent=directory)
else:
directory_obj = Directory(name=directory_name.name)
directory_obj.save()
directory_obj.total = 0
directory_obj.total_read = 0
files.append(DirFile(directory_obj))
files = [file for file in files if file.obj.classification <= user.usermisc.allowed_to_read]
comics_to_annotate = []
for file_name in file_list:
if file_name.suffix.lower() in [".rar", ".zip", ".cbr", ".cbz", ".pdf"]:
book = ComicBook.process_comic_book(file_name, directory)
ComicStatus(user=user, comic=book).save()
comics_to_annotate.append(book.selector)
if comics_to_annotate:
new_comics = ComicBook.objects.filter(selector__in=comics_to_annotate).annotate(
total=Count('comicpage', distinct=True),
total_read=F('comicstatus__last_read_page'),
finished=F('comicstatus__finished'),
unread=F('comicstatus__unread'),
user=F('comicstatus__user'),
classification=Case(
When(directory__isnull=True, then=Directory.Classification.C_18),
default=F('directory__classification'),
output_field=PositiveSmallIntegerField(choices=Directory.Classification.choices)
)
).filter(Q(user__isnull=True) | Q(user=user.id))
files.extend([DirFile(b) for b in new_comics])
files.sort(key=lambda x: x.name)
files.sort(key=lambda x: x.item_type, reverse=True)
return files
def generate_label(book):
"""
book need to be annotated with the following from ComicStatus
* unread
* finished
* last_read_page
* total_pages
:param book: ComicBook
:return: str
"""
unread_text = '<center><span class="label label-default">Unread</span></center>'
if not hasattr(book, 'unread'):
label_text = unread_text
elif book.unread or book.unread is None:
label_text = unread_text
elif book.finished:
label_text = '<center><span class="label label-success">Read</span></center>'
else:
label_text = '<center><span class="label label-primary">%s/%s</span></center>' % (
book.last_read_page + 1,
book.total_pages,
)
return label_text
def generate_dir_status(total, total_read):
if total == 0:
return '<center><span class="label label-default">Empty</span></center>'
elif total == total_read:
return '<center><span class="label label-success">All Read</span></center>'
elif total_read == 0:
return '<center><span class="label label-default">Unread</span></center>'
return f'<center><span class="label label-primary">{total_read}/{total}</span></center>'

View File

@@ -1,408 +0,0 @@
import json
import uuid
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.db.models import Max, Count, F, Case, When, PositiveSmallIntegerField
from django.db.transaction import atomic
from django.http import HttpResponse, FileResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_POST
from .forms import AccountForm, AddUserForm, EditUserForm, InitialSetupForm, DirectoryEditForm
from .models import ComicBook, ComicPage, ComicStatus, Directory, UserMisc
from .util import (
Menu,
generate_breadcrumbs_from_menu,
generate_breadcrumbs_from_path,
generate_directory,
generate_label,
generate_title_from_path,
)
# noinspection PyTypeChecker
@ensure_csrf_cookie
@login_required
def comic_list(request, directory_selector=False):
if User.objects.all().count() == 0:
return redirect("/comic/settings/")
directory = None
if directory_selector:
selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
directory = Directory.objects.get(selector=selector)
if directory:
title = generate_title_from_path(directory.path)
breadcrumbs = generate_breadcrumbs_from_path(directory)
else:
title = generate_title_from_path("Home")
breadcrumbs = generate_breadcrumbs_from_path()
files = generate_directory(request.user, directory)
form = DirectoryEditForm()
return render(
request,
"comic/comic_list.html",
{
"breadcrumbs": breadcrumbs,
"menu": Menu(request.user, "Browse"),
"title": title,
"files": files,
"form": form,
"selector": directory_selector if directory_selector else 'None',
'js_urls': {
"perform_action": reverse('perform_action', args=('operation', 'item_type', 'selector'))
}
},
)
@login_required
def perform_action(request, operation, item_type, selector):
if operation not in ['mark_read', 'mark_unread', 'mark_previous', 'set_classification']:
return HttpResponse(400)
elif operation == 'mark_previous' and item_type == 'Directory':
return HttpResponse(422)
try:
selector_uuid = uuid.UUID(bytes=urlsafe_base64_decode(selector))
except ValueError:
if item_type == 'Directory':
for book in ComicBook.objects.filter(directory__isnull=True):
getattr(book, operation)(request.user)
return HttpResponse(204)
else:
return HttpResponse(400)
if operation == 'set_classification':
form = DirectoryEditForm(request.POST)
if not (form.is_valid() and item_type == 'Directory'):
return HttpResponse(400)
if item_type == 'ComicBook':
book = get_object_or_404(ComicBook, selector=selector_uuid)
getattr(book, operation)(request.user)
return HttpResponse(204)
elif item_type == 'Directory':
directory = get_object_or_404(Directory, selector=selector_uuid)
if operation == 'set_classification':
getattr(directory, operation)(form.cleaned_data)
else:
getattr(directory, operation)(request.user)
return HttpResponse(204)
@login_required
def recent_comics(request):
return render(
request,
"comic/recent_comics.html",
{
"breadcrumbs": generate_breadcrumbs_from_menu([("Recent", "/comic/recent/")]),
"menu": Menu(request.user, "Recent"),
"title": "Recent Comics",
"feed_id": urlsafe_base64_encode(request.user.usermisc.feed_id.bytes),
},
)
@login_required
@require_POST
def recent_comics_json(request):
start = int(request.POST["start"])
end = start + int(request.POST["length"])
icon = '<span class="fa fa-book"></span>'
comics = ComicBook.objects.all().annotate(total_pages=Count('comicpage'))
response_data = dict()
response_data["recordsTotal"] = comics.count()
if request.POST["search[value]"]:
comics = comics.filter(file_name__contains=request.POST["search[value]"])
order_string = ""
# Ordering
if request.POST["order[0][dir]"] == "desc":
order_string += "-"
order_string += "date_added"
comics = comics.order_by(order_string)
comics = comics.annotate(
unread=Case(When(comicstatus__user=request.user, then='comicstatus__unread')),
finished=Case(When(comicstatus__user=request.user, then='comicstatus__finished')),
last_read_page=Case(When(comicstatus__user=request.user, then='comicstatus__last_read_page')),
classification=Case(
When(directory__isnull=True, then=Directory.Classification.C_18),
default=F('directory__classification'),
output_field=PositiveSmallIntegerField(choices=Directory.Classification.choices)
)
)
misc, _ = UserMisc.objects.get_or_create(user=request.user)
comics = comics.filter(classification__lte=misc.allowed_to_read)
response_data["recordsFiltered"] = comics.count()
response_data["data"] = list()
for book in comics[start:end]:
response_data["data"].append(
{
"selector": urlsafe_base64_encode(book.selector.bytes),
"icon": icon,
"type": "book",
"name": book.file_name,
"date": book.date_added.strftime("%d/%m/%y-%H:%M"),
"label": generate_label(book),
"url": "/comic/read/{0}/".format(urlsafe_base64_encode(book.selector.bytes)),
}
)
return HttpResponse(json.dumps(response_data), content_type="application/json")
@login_required
@require_POST
def comic_edit(request):
if "selected" not in request.POST:
return HttpResponse(status=200)
if request.POST["func"] == "choose":
return HttpResponse(status=200)
selected = [uuid.UUID(bytes=urlsafe_base64_decode(item)) for item in request.POST.getlist("selected")]
comics = ComicBook.objects.filter(selector__in=selected)
with atomic():
for comic in comics:
status, _ = ComicStatus.objects.get_or_create(comic=comic, user=request.user)
if request.POST["func"] == "read":
status.unread = False
status.finished = True
status.last_read_page = comic.page_count - 1
elif request.POST["func"] == "unread":
status.unread = True
status.finished = False
status.last_read_page = 0
status.save()
return HttpResponse(status=200)
@login_required
def account_page(request):
success_message = []
if request.POST:
form = AccountForm(request.POST)
if form.is_valid():
if form.cleaned_data["email"] != request.user.email:
request.user.email = form.cleaned_data["email"]
success_message.append("Email Updated.")
if len(form.cleaned_data["password"]) != 0:
request.user.set_password(form.cleaned_data["password"])
success_message.append("Password Updated.")
request.user.save()
else:
form = AccountForm(initial={"username": request.user.username, "email": request.user.email})
crumbs = [("Account", "/comic/account/")]
context = {
"form": form,
"menu": Menu(request.user, "Account"),
"error_message": form.errors,
"success_message": "</br>".join(success_message),
"breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
"title": "CBWebReader - Account",
}
return render(request, "comic/settings_page.html", context)
@user_passes_test(lambda u: u.is_superuser)
def users_page(request):
users = User.objects.all().select_related('usermisc')
crumbs = [("Users", "/comic/settings/users/")]
context = {
"users": users,
"menu": Menu(request.user, "Users"),
"breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
}
return render(request, "comic/users_page.html", context)
@user_passes_test(lambda u: u.is_superuser)
def user_config_page(request, user_id):
user = get_object_or_404(User, id=user_id)
success_message = []
if request.POST:
form = EditUserForm(request.POST)
if form.is_valid():
if "password" in form.cleaned_data and len(form.cleaned_data["password"]) != 0:
user.set_password(form.cleaned_data["password"])
success_message.append("Password Updated.")
if form.cleaned_data["email"] != user.email:
user.email = form.cleaned_data["email"]
success_message.append("Email Updated.</br>")
user.usermisc.allowed_to_read = form.cleaned_data['allowed_to_read']
user.usermisc.save()
user.save()
else:
form = EditUserForm(initial=EditUserForm.get_initial_values(user))
users = User.objects.all()
crumbs = [("Users", "/comic/settings/users/"), (user.username, "/comic/settings/users/" + str(user.id))]
context = {
"form": form,
"users": users,
"menu": Menu(request.user, "Users"),
"error_message": form.errors,
"breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
"success_message": "</br>".join(success_message),
"title": "CBWebReader - Edit User - " + user.username,
}
return render(request, "comic/settings_page.html", context)
@user_passes_test(lambda u: u.is_superuser)
def user_add_page(request):
success_message = ""
if request.POST:
form = AddUserForm(request.POST)
if form.is_valid():
user = User(username=form.cleaned_data["username"], email=form.cleaned_data["email"])
user.set_password(form.cleaned_data["password"])
user.save()
success_message = "User {} created.".format(user.username)
else:
form = AddUserForm()
crumbs = [("Users", "/comic/settings/users/"), ("Add", "/comic/settings/users/add/")]
context = {
"form": form,
"menu": Menu(request.user, "Users"),
"breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
"error_message": form.errors,
"success_message": success_message,
"title": "CBWebReader - Add User",
}
return render(request, "comic/settings_page.html", context)
@login_required
def read_comic(request, comic_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
book = get_object_or_404(ComicBook, selector=selector)
misc, _ = UserMisc.objects.get_or_create(user=request.user)
if book.directory and book.directory.classification > misc.allowed_to_read:
return redirect('index')
pages = ComicPage.objects.filter(Comic=book)
status, _ = ComicStatus.objects.get_or_create(comic=book, user=request.user)
title = "CBWebReader - " + book.file_name
context = {
"book": book,
"pages": pages,
"nav": book.nav(request.user),
"status": status,
"breadcrumbs": generate_breadcrumbs_from_path(book.directory, book),
"menu": Menu(request.user),
"title": title,
}
if book.file_name.lower().endswith('pdf'):
context['status'].last_read_page += 1
return render(request, "comic/read_comic_pdf.html", context)
else:
book.verify_pages(pages)
context['pages'] = ComicPage.objects.filter(Comic=book)
return render(request, "comic/read_comic.html", context)
@login_required
def set_read_page(request, comic_selector, page):
page = int(page)
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
book = get_object_or_404(ComicBook, selector=selector)
status, _ = ComicStatus.objects.get_or_create(comic=book, user=request.user)
status.unread = False
status.last_read_page = page
if ComicPage.objects.filter(Comic=book).aggregate(Max("index"))["index__max"] == status.last_read_page:
status.finished = True
else:
status.finished = False
status.save()
return HttpResponse(status=200)
@xframe_options_sameorigin
@login_required
def get_image(request, comic_selector, page):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
book = ComicBook.objects.get(selector=selector)
misc, _ = UserMisc.objects.get_or_create(user=request.user)
if book.directory and book.directory.classification > misc.allowed_to_read:
return HttpResponse(status=401)
img, content = book.get_image(int(page))
return FileResponse(img, content_type=content)
@login_required
def get_image_api(request, selector, page):
book = ComicBook.objects.get(selector=selector)
misc, _ = UserMisc.objects.get_or_create(user=request.user)
if book.directory and book.directory.classification > misc.allowed_to_read:
return HttpResponse(status=401)
img, content = book.get_image(int(page))
return FileResponse(img, content_type=content)
@xframe_options_sameorigin
@login_required
def comic_thumbnail(request, comic_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
book = ComicBook.objects.get(selector=selector)
misc, _ = UserMisc.objects.get_or_create(user=request.user)
if book.directory and book.directory.classification > misc.allowed_to_read:
return HttpResponse(status=401)
return redirect(book.get_thumbnail_url())
@xframe_options_sameorigin
@login_required
def directory_thumbnail(request, directory_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
folder = Directory.objects.get(selector=selector)
misc, _ = UserMisc.objects.get_or_create(user=request.user)
if folder.classification > misc.allowed_to_read:
return HttpResponse(status=401)
return redirect(folder.get_thumbnail_url())
@login_required
def get_pdf(request, comic_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
book = ComicBook.objects.get(selector=selector)
misc, _ = UserMisc.objects.get_or_create(user=request.user)
if book.directory.classification > misc.allowed_to_read:
return HttpResponse(status=401)
return FileResponse(open(book.get_pdf(), 'rb'), content_type='application/pdf')
def initial_setup(request):
if User.objects.all().exists():
return redirect("/comic/")
if request.POST:
form = InitialSetupForm(request.POST)
if form.is_valid():
user = User(
username=form.cleaned_data["username"],
email=form.cleaned_data["email"],
is_staff=True,
is_superuser=True,
)
user.set_password(form.cleaned_data["password"])
user.save()
user = authenticate(username=form.cleaned_data["username"], password=form.cleaned_data["password"])
login(request, user)
return redirect("/comic/")
else:
form = InitialSetupForm()
context = {"form": form, "title": "CBWebReader - Setup", "error_message": form.errors}
return render(request, "comic/settings_page.html", context)
def comic_redirect(_):
return redirect("/comic/")

View File

View File

View File

@@ -1,16 +0,0 @@
from django import forms
class LoginForm(forms.Form):
username = forms.CharField(
max_length=50,
label="",
widget=forms.TextInput(
attrs={"class": "form-control", "placeholder": "Username", "autofocus": True, "required": True}
),
)
password = forms.CharField(
label="Password",
widget=forms.PasswordInput(attrs={"class": "form-control", "placeholder": "Password", "required": True}),
)

View File

View File

@@ -1,19 +0,0 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% block title %}CBWebReader - Login{% endblock %}
{% block content %}
<div class="container">
{% if error %}
<div class="alert alert-danger" role="alert"><p>Your username and password didn't match. Please try again.</p></div>
{% endif %}
<form method="post">
{% csrf_token %}
<h2 class="form-signin-heading">Please sign in</h2>
{% bootstrap_form form %}
{% buttons %}
<button class="btn btn-lg btn-secondary btn-block" type="submit">Sign in</button>
{% endbuttons %}
</form>
</div>
{% endblock %}

View File

@@ -1,40 +0,0 @@
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.shortcuts import redirect, render
from django.utils.http import url_has_allowed_host_and_scheme
from comic_auth.forms import LoginForm
def comic_login(request):
if request.POST:
form = LoginForm(request.POST)
if form.is_valid():
user = authenticate(username=form.cleaned_data["username"], password=form.cleaned_data["password"])
if user is not None:
if user.is_active:
login(request, user)
if "next" in request.GET:
if url_has_allowed_host_and_scheme(request.GET["next"], allowed_hosts=None):
return redirect(request.GET["next"])
else:
return redirect("/comic/")
else:
return redirect("/comic/")
else:
return render(request, "comic_auth/login.html", {"error": True})
else:
return render(request, "comic_auth/login.html", {"error": True, "form": form})
else:
return render(request, "comic_auth/login.html", {"error": True, "form": form})
else:
if not User.objects.all().exists():
return redirect("/setup/")
form = LoginForm()
context = {"form": form}
return render(request, "comic_auth/login.html", context)
def comic_logout(request):
logout(request)
return redirect("/login/")

320
package-lock.json generated
View File

@@ -1,320 +0,0 @@
{
"name": "npm-proj-1661104548614-0.682394455872383331qpFK",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",
"bootstrap": "^4.6.0",
"datatables.net-bs4": "^1.12.1",
"hammerjs": "^2.0.8",
"isotope-layout": "^3.0.6",
"jquery": "^3.6.0",
"js-cookie": "^2.2.1",
"pdfjs-dist": "^2.15.349",
"reveal.js": "^4.1.0",
"reveal.js-menu": "^2.1.0"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
"integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/bootstrap": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz",
"integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
},
"peerDependencies": {
"jquery": "1.9.1 - 3",
"popper.js": "^1.16.1"
}
},
"node_modules/datatables.net": {
"version": "1.11.5",
"resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.11.5.tgz",
"integrity": "sha512-nlFst2xfwSWaQgaOg5sXVG3cxYC0tH8E8d65289w9ROgF2TmLULOOpcdMpyxxUim/qEwVSEem42RjkTWEpr3eA==",
"dependencies": {
"jquery": ">=1.7"
}
},
"node_modules/datatables.net-bs4": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/datatables.net-bs4/-/datatables.net-bs4-1.12.1.tgz",
"integrity": "sha512-LBeC8zUNVYyQT7ytC2lYqyXDn+k2kYpqvijC83oOjlcnEtb/8Tduzgquox5FrNKUJPcUrj9r+h5B0TDBbob/Gg==",
"dependencies": {
"datatables.net": ">=1.11.3",
"jquery": ">=1.7"
}
},
"node_modules/desandro-matches-selector": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/desandro-matches-selector/-/desandro-matches-selector-2.0.2.tgz",
"integrity": "sha1-cXvu1NwT59jzdi9wem1YpndCGOE="
},
"node_modules/dommatrix": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dommatrix/-/dommatrix-1.0.3.tgz",
"integrity": "sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww=="
},
"node_modules/ev-emitter": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-1.1.1.tgz",
"integrity": "sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q=="
},
"node_modules/fizzy-ui-utils": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz",
"integrity": "sha512-CZXDVXQ1If3/r8s0T+v+qVeMshhfcuq0rqIFgJnrtd+Bu8GmDmqMjntjUePypVtjHXKJ6V4sw9zeyox34n9aCg==",
"dependencies": {
"desandro-matches-selector": "^2.0.0"
}
},
"node_modules/get-size": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/get-size/-/get-size-2.0.3.tgz",
"integrity": "sha512-lXNzT/h/dTjTxRbm9BXb+SGxxzkm97h/PCIKtlN/CBCxxmkkIVV21udumMS93MuVTDX583gqc94v3RjuHmI+2Q=="
},
"node_modules/hammerjs": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
"integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/isotope-layout": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/isotope-layout/-/isotope-layout-3.0.6.tgz",
"integrity": "sha512-z2ZKablhocXhoNyWwzJPFd7u7FWbYbVJA51Nvsqsod8jH2ExGc1SwDsSWKE54e3PhXzqf2yZPhFSq/c2MR1arw==",
"dependencies": {
"desandro-matches-selector": "^2.0.0",
"fizzy-ui-utils": "^2.0.4",
"get-size": "^2.0.0",
"masonry-layout": "^4.1.0",
"outlayer": "^2.1.0"
}
},
"node_modules/jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
},
"node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"node_modules/masonry-layout": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/masonry-layout/-/masonry-layout-4.2.2.tgz",
"integrity": "sha512-iGtAlrpHNyxaR19CvKC3npnEcAwszXoyJiI8ARV2ePi7fmYhIud25MHK8Zx4P0LCC4d3TNO9+rFa1KoK1OEOaA==",
"dependencies": {
"get-size": "^2.0.2",
"outlayer": "^2.1.0"
}
},
"node_modules/outlayer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/outlayer/-/outlayer-2.1.1.tgz",
"integrity": "sha1-KYY7beEOpdrf/8rfoNcokHOH6aI=",
"dependencies": {
"ev-emitter": "^1.0.0",
"fizzy-ui-utils": "^2.0.0",
"get-size": "^2.0.2"
}
},
"node_modules/pdfjs-dist": {
"version": "2.15.349",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.15.349.tgz",
"integrity": "sha512-EeCfqj6xi4/aegKNS7Bs+TCg3Y5gmKmG0s/5xXI0PqWW66x+Nm7iFXBpVcup7HnR8sNDm+5NESfFr8T6DeWp9Q==",
"dependencies": {
"dommatrix": "^1.0.3",
"web-streams-polyfill": "^3.2.1"
},
"peerDependencies": {
"worker-loader": "^3.0.8"
},
"peerDependenciesMeta": {
"worker-loader": {
"optional": true
}
}
},
"node_modules/popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/reveal.js": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/reveal.js/-/reveal.js-4.3.1.tgz",
"integrity": "sha512-1kyEnWeUkaCdBdX//XXq9dtBK95ppvIlSwlHelrP8/wrX6LcsYp4HT9WTFoFEOUBfVqkm8C2aHQ367o+UKfcxw==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/reveal.js-menu": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/reveal.js-menu/-/reveal.js-menu-2.1.0.tgz",
"integrity": "sha512-35zp4fHSMyWd15+3CvQ8LrpS+4Gj2qvlkxX3lo5LpITDe6ZkA4A9y1E5fE63YlQl5fp7W1mNgNJr4kCU0s14lA=="
},
"node_modules/web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
"engines": {
"node": ">= 8"
}
}
},
"dependencies": {
"@fortawesome/fontawesome-free": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
"integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg=="
},
"bootstrap": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz",
"integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og==",
"requires": {}
},
"datatables.net": {
"version": "1.11.5",
"resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.11.5.tgz",
"integrity": "sha512-nlFst2xfwSWaQgaOg5sXVG3cxYC0tH8E8d65289w9ROgF2TmLULOOpcdMpyxxUim/qEwVSEem42RjkTWEpr3eA==",
"requires": {
"jquery": ">=1.7"
}
},
"datatables.net-bs4": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/datatables.net-bs4/-/datatables.net-bs4-1.12.1.tgz",
"integrity": "sha512-LBeC8zUNVYyQT7ytC2lYqyXDn+k2kYpqvijC83oOjlcnEtb/8Tduzgquox5FrNKUJPcUrj9r+h5B0TDBbob/Gg==",
"requires": {
"datatables.net": ">=1.11.3",
"jquery": ">=1.7"
}
},
"desandro-matches-selector": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/desandro-matches-selector/-/desandro-matches-selector-2.0.2.tgz",
"integrity": "sha1-cXvu1NwT59jzdi9wem1YpndCGOE="
},
"dommatrix": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dommatrix/-/dommatrix-1.0.3.tgz",
"integrity": "sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww=="
},
"ev-emitter": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-1.1.1.tgz",
"integrity": "sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q=="
},
"fizzy-ui-utils": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz",
"integrity": "sha512-CZXDVXQ1If3/r8s0T+v+qVeMshhfcuq0rqIFgJnrtd+Bu8GmDmqMjntjUePypVtjHXKJ6V4sw9zeyox34n9aCg==",
"requires": {
"desandro-matches-selector": "^2.0.0"
}
},
"get-size": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/get-size/-/get-size-2.0.3.tgz",
"integrity": "sha512-lXNzT/h/dTjTxRbm9BXb+SGxxzkm97h/PCIKtlN/CBCxxmkkIVV21udumMS93MuVTDX583gqc94v3RjuHmI+2Q=="
},
"hammerjs": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
"integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE="
},
"isotope-layout": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/isotope-layout/-/isotope-layout-3.0.6.tgz",
"integrity": "sha512-z2ZKablhocXhoNyWwzJPFd7u7FWbYbVJA51Nvsqsod8jH2ExGc1SwDsSWKE54e3PhXzqf2yZPhFSq/c2MR1arw==",
"requires": {
"desandro-matches-selector": "^2.0.0",
"fizzy-ui-utils": "^2.0.4",
"get-size": "^2.0.0",
"masonry-layout": "^4.1.0",
"outlayer": "^2.1.0"
}
},
"jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
},
"js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"masonry-layout": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/masonry-layout/-/masonry-layout-4.2.2.tgz",
"integrity": "sha512-iGtAlrpHNyxaR19CvKC3npnEcAwszXoyJiI8ARV2ePi7fmYhIud25MHK8Zx4P0LCC4d3TNO9+rFa1KoK1OEOaA==",
"requires": {
"get-size": "^2.0.2",
"outlayer": "^2.1.0"
}
},
"outlayer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/outlayer/-/outlayer-2.1.1.tgz",
"integrity": "sha1-KYY7beEOpdrf/8rfoNcokHOH6aI=",
"requires": {
"ev-emitter": "^1.0.0",
"fizzy-ui-utils": "^2.0.0",
"get-size": "^2.0.2"
}
},
"pdfjs-dist": {
"version": "2.15.349",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.15.349.tgz",
"integrity": "sha512-EeCfqj6xi4/aegKNS7Bs+TCg3Y5gmKmG0s/5xXI0PqWW66x+Nm7iFXBpVcup7HnR8sNDm+5NESfFr8T6DeWp9Q==",
"requires": {
"dommatrix": "^1.0.3",
"web-streams-polyfill": "^3.2.1"
}
},
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"peer": true
},
"reveal.js": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/reveal.js/-/reveal.js-4.3.1.tgz",
"integrity": "sha512-1kyEnWeUkaCdBdX//XXq9dtBK95ppvIlSwlHelrP8/wrX6LcsYp4HT9WTFoFEOUBfVqkm8C2aHQ367o+UKfcxw=="
},
"reveal.js-menu": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/reveal.js-menu/-/reveal.js-menu-2.1.0.tgz",
"integrity": "sha512-35zp4fHSMyWd15+3CvQ8LrpS+4Gj2qvlkxX3lo5LpITDe6ZkA4A9y1E5fE63YlQl5fp7W1mNgNJr4kCU0s14lA=="
},
"web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q=="
}
}
}

View File

@@ -1,14 +0,0 @@
{
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",
"bootstrap": "^4.6.0",
"datatables.net-bs4": "^1.12.1",
"hammerjs": "^2.0.8",
"isotope-layout": "^3.0.6",
"jquery": "^3.6.0",
"js-cookie": "^2.2.1",
"pdfjs-dist": "^2.15.349",
"reveal.js": "^4.1.0",
"reveal.js-menu": "^2.1.0"
}
}

28
poetry.lock generated
View File

@@ -133,7 +133,7 @@ jinja2 = "*"
name = "coverage" name = "coverage"
version = "6.4.4" version = "6.4.4"
description = "Code coverage measurement for Python" description = "Code coverage measurement for Python"
category = "main" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
@@ -296,14 +296,6 @@ async = ["django-celery (>=3.0)"]
async_dramatiq = ["django-dramatiq (>=0.4.0)"] async_dramatiq = ["django-dramatiq (>=0.4.0)"]
async_rq = ["django-rq (>=0.6.0)"] async_rq = ["django-rq (>=0.6.0)"]
[[package]]
name = "django-node-assets"
version = "0.9.11"
description = "The Django application that allows install and serve assets via Node.js package manager infrastructure."
category = "main"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "django-permissions-policy" name = "django-permissions-policy"
version = "4.13.0" version = "4.13.0"
@@ -550,17 +542,6 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
[package.extras] [package.extras]
dev = ["colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "black (>=19.10b0)", "isort (>=5.1.1)", "Sphinx (>=4.1.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)"] dev = ["colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "black (>=19.10b0)", "isort (>=5.1.1)", "Sphinx (>=4.1.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)"]
[[package]]
name = "markdown"
version = "3.4.1"
description = "Python implementation of Markdown."
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
testing = ["coverage", "pyyaml"]
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "2.1.1" version = "2.1.1"
@@ -1024,7 +1005,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "4a1018e12d5d29a99175c79c0afd14a4faaf846a535f2544b670842739d867e0" content-hash = "02ffb9bcd22babd30fb99e50a293f2ed1421375c4c36e5ee534c5530b63ca4e8"
[metadata.files] [metadata.files]
appnope = [ appnope = [
@@ -1176,10 +1157,6 @@ django-imagekit = [
{file = "django-imagekit-4.1.0.tar.gz", hash = "sha256:e559aeaae43a33b34f87631a9fa5696455e4451ffa738a42635fde442fedac5c"}, {file = "django-imagekit-4.1.0.tar.gz", hash = "sha256:e559aeaae43a33b34f87631a9fa5696455e4451ffa738a42635fde442fedac5c"},
{file = "django_imagekit-4.1.0-py2.py3-none-any.whl", hash = "sha256:87e36f8dc1d8745647881f4366ef4965225f048042dacebbee0dcb87425defef"}, {file = "django_imagekit-4.1.0-py2.py3-none-any.whl", hash = "sha256:87e36f8dc1d8745647881f4366ef4965225f048042dacebbee0dcb87425defef"},
] ]
django-node-assets = [
{file = "django-node-assets-0.9.11.tar.gz", hash = "sha256:df6ca9aeb868aa9692cbf8f6265132b6159798866b15ac95d7d0d4dd5f3cb6da"},
{file = "django_node_assets-0.9.11-py3-none-any.whl", hash = "sha256:4d37659c07976dc4ebccb6704051c25204e3381aa5e4f98a4a76b57e33cb1776"},
]
django-permissions-policy = [] django-permissions-policy = []
django-silk = [] django-silk = []
django-sri = [] django-sri = []
@@ -1216,7 +1193,6 @@ loguru = [
{file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"},
{file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"},
] ]
markdown = []
markupsafe = [ markupsafe = [
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},

View File

@@ -3,7 +3,7 @@ line_length = 119
[tool.poetry] [tool.poetry]
name = "cbwebreader" name = "cbwebreader"
version = "1.0.0" version = "1.0.1"
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"
@@ -19,7 +19,6 @@ django-silk = "^5.0.0"
mysqlclient = "^2.0.1" mysqlclient = "^2.0.1"
psycopg2 = "^2.8.6" psycopg2 = "^2.8.6"
rarfile = "^4.0" rarfile = "^4.0"
coverage = "^6.2"
django-extensions = "^3.2.0" django-extensions = "^3.2.0"
Pillow = "^9.1.1" Pillow = "^9.1.1"
django-imagekit = "^4.0.2" django-imagekit = "^4.0.2"
@@ -28,10 +27,8 @@ django-bootstrap4 = "^22.1"
django-csp = "^3.7" django-csp = "^3.7"
django-boost = "^2.0" django-boost = "^2.0"
django-sri = "^0.4.0" django-sri = "^0.4.0"
django-node-assets = "^0.9.9"
django-permissions-policy = "^4.9.0" django-permissions-policy = "^4.9.0"
djangorestframework = "^3.13.1" djangorestframework = "^3.13.1"
Markdown = "^3.3.7"
django-filter = "^22.1" django-filter = "^22.1"
django-cors-headers = "^3.13.0" django-cors-headers = "^3.13.0"
djangorestframework-simplejwt = "^5.2.0" djangorestframework-simplejwt = "^5.2.0"
@@ -44,6 +41,7 @@ mypy = "^0.971"
Werkzeug = "<2.1" Werkzeug = "<2.1"
pyOpenSSL = "^22.0.0" pyOpenSSL = "^22.0.0"
ipython = "^8.4.0" ipython = "^8.4.0"
coverage = "^6.2"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

View File

@@ -1,21 +0,0 @@
#comic_list caption {
caption-side: top;
}
.card_list_card {
width: 200px;
}
.card .unread-badge {
position:absolute;
top:10px;
left:10px;
padding:5px;
color:white;
}
.card .classification-badge {
position:absolute;
top:10px;
right: 10px;
padding:5px;
color:black;
}

View File

@@ -1 +0,0 @@
#comic_list caption{caption-side:top}.card_list_card{width:200px}.card .unread-badge{position:absolute;top:10px;left:10px;padding:5px;color:#fff}.card .classification-badge{position:absolute;top:10px;right:10px;padding:5px;color:#000}

View File

@@ -1,138 +0,0 @@
let qsRegex;
let buttonFilter;
const js_urls = JSON.parse(document.getElementById('js_urls').textContent)
let $grid = $('.comic-container').isotope({
itemSelector: '.grid-item',
layoutMode: 'fitRows',
filter: function() {
let $this = $(this);
let searchResult = qsRegex ? $this.text().match( qsRegex ) : true;
let 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();
}
});
let $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)
}));
let modal_buttons = document.getElementsByClassName('modal-button')
modal_buttons.forEach(el => el.addEventListener('click', event => {
let target = $(event.target).closest('button')
let selector = target.attr('selector')
let modal = $('#editModal')
modal.attr('selector', selector)
modal.attr('itemtype', target.attr('itemtype'))
let title = $('#editModalLabel')
let title_source = $('.card-title.'+selector)
title.text(title_source.text())
let classification = $('select[name="classification"]')
let classification_value = $('.classification-badge.'+selector)
classification.val(classification_value.attr('classification'))
}))
let save_button = document.getElementById('save_button')
save_button.addEventListener('click', function (event){
let modal = $('#editModal')
let selector = modal.attr('selector')
let itemtype = modal.attr('itemtype')
let classification = $('select[name="classification"]')
let classification_badge = $('.classification-badge.'+selector)
let action_url = js_urls.perform_action.replace('operation', 'set_classification').replace('item_type', itemtype).replace('selector', selector)
$.ajax({
type: "POST",
url: action_url,
data: {
classification: classification.val(),
csrfmiddlewaretoken: $('input[name="csrfmiddlewaretoken"]').attr('value')
},
success: function (ev){
classification_badge.text($('select[name="classification"] option:selected').text())
modal.modal('hide')
},
})
})
$( "img" ).on("error", function() {
$(this).src=$(this).attr('alt_src');
})

View File

@@ -1 +0,0 @@
let qsRegex;let buttonFilter;const js_urls=JSON.parse(document.getElementById("js_urls").textContent);let $grid=$(".comic-container").isotope({itemSelector:".grid-item",layoutMode:"fitRows",filter:function(){let $this=$(this);let searchResult=qsRegex?$this.text().match(qsRegex):true;let 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()}});let $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)}));let modal_buttons=document.getElementsByClassName("modal-button");modal_buttons.forEach(el=>el.addEventListener("click",event=>{let target=$(event.target).closest("button");let selector=target.attr("selector");let modal=$("#editModal");modal.attr("selector",selector);modal.attr("itemtype",target.attr("itemtype"));let title=$("#editModalLabel");let title_source=$(".card-title."+selector);title.text(title_source.text());let classification=$('select[name="classification"]');let classification_value=$(".classification-badge."+selector);classification.val(classification_value.attr("classification"))}));let save_button=document.getElementById("save_button");save_button.addEventListener("click",function(event){let modal=$("#editModal");let selector=modal.attr("selector");let itemtype=modal.attr("itemtype");let classification=$('select[name="classification"]');let classification_badge=$(".classification-badge."+selector);let action_url=js_urls.perform_action.replace("operation","set_classification").replace("item_type",itemtype).replace("selector",selector);$.ajax({type:"POST",url:action_url,data:{classification:classification.val(),csrfmiddlewaretoken:$('input[name="csrfmiddlewaretoken"]').attr("value")},success:function(ev){classification_badge.text($('select[name="classification"] option:selected').text());modal.modal("hide")}})});$("img").on("error",function(){$(this).src=$(this).attr("alt_src")});

View File

@@ -1,70 +0,0 @@
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) {
nextPage()
});
hammertime.on('swiperight', function (ev) {
prevPage()
});
function prevPage() {
if (Reveal.isFirstSlide()){
if (nav.prev_type === 'ComicBook'){
window.location = "/comic/read/"+ nav.prev_path +"/"
} else {
window.location = "/comic/"+ nav.prev_path +"/"
}
} else {
Reveal.prev();
}
}
function nextPage() {
if (Reveal.isLastSlide()){
if (nav.next_type === 'ComicBook'){
window.location = "/comic/read/"+ nav.next_path +"/"
} else {
window.location = "/comic/"+ 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)
})

View File

@@ -1 +0,0 @@
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){nextPage()});hammertime.on("swiperight",function(ev){prevPage()});function prevPage(){if(Reveal.isFirstSlide()){if(nav.prev_type==="ComicBook"){window.location="/comic/read/"+nav.prev_path+"/"}else{window.location="/comic/"+nav.prev_path+"/"}}else{Reveal.prev()}}function nextPage(){if(Reveal.isLastSlide()){if(nav.next_type==="ComicBook"){window.location="/comic/read/"+nav.next_path+"/"}else{window.location="/comic/"+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)});

View File

@@ -1,158 +0,0 @@
// 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-dist/build/pdf.worker.min.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) {
if (nav.prev_type === 'ComicBook'){
window.location = "/comic/read/"+ nav.prev_path +"/"
} else {
window.location = "/comic/"+ nav.prev_path +"/"
}
} else {
pageNum--;
queueRenderPage(pageNum);
}
}
document.getElementById('prev').addEventListener('click', onPrevPage);
/**
* Displays next page.
*/
function onNextPage() {
if (pageNum >= pdfDoc.numPages) {
if (nav.next_type === 'ComicBook'){
window.location = "/comic/read/"+ nav.next_path +"/"
} else {
window.location = "/comic/"+ nav.next_path +"/"
}
} else {
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()
})

View File

@@ -1 +0,0 @@
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-dist/build/pdf.worker.min.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){if(nav.prev_type==="ComicBook"){window.location="/comic/read/"+nav.prev_path+"/"}else{window.location="/comic/"+nav.prev_path+"/"}}else{pageNum--;queueRenderPage(pageNum)}}document.getElementById("prev").addEventListener("click",onPrevPage);function onNextPage(){if(pageNum>=pdfDoc.numPages){if(nav.next_type==="ComicBook"){window.location="/comic/read/"+nav.next_path+"/"}else{window.location="/comic/"+nav.next_path+"/"}}else{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

@@ -1,75 +0,0 @@
$(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 = $("input[name='csrfmiddlewaretoken']")[0].value;
},
},
"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();
}
});
});
} );

View File

@@ -1 +0,0 @@
$(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=$("input[name='csrfmiddlewaretoken']")[0].value}},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()}})})});