mirror of
https://github.com/ajurna/cbwebreader.git
synced 2025-12-06 06:17:17 +00:00
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:
@@ -22,7 +22,6 @@ SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", None)
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = os.getenv('DJANGO_DEBUG', False) == 'True'
|
||||
# DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",")
|
||||
|
||||
@@ -39,7 +38,6 @@ INSTALLED_APPS = (
|
||||
'webpack_loader',
|
||||
'bootstrap4',
|
||||
"comic",
|
||||
"comic_auth",
|
||||
'django_extensions',
|
||||
'imagekit',
|
||||
'django_boost',
|
||||
@@ -102,8 +100,6 @@ STATIC_URL = "/static/"
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
Path(BASE_DIR, "static"),
|
||||
# Path(BASE_DIR, "node_modules"),
|
||||
Path(BASE_DIR, "frontend", "node_modules"),
|
||||
Path(BASE_DIR, "frontend", "dist")
|
||||
]
|
||||
|
||||
@@ -169,14 +165,6 @@ PERMISSIONS_POLICY = {
|
||||
"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 = {
|
||||
# Use Django's standard `django.contrib.auth` permissions,
|
||||
|
||||
@@ -18,14 +18,14 @@ from django.conf.urls import include
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
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 drf_yasg.views import get_schema_view
|
||||
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(
|
||||
openapi.Info(
|
||||
@@ -39,15 +39,8 @@ schema_view = get_schema_view(
|
||||
permission_classes=[permissions.AllowAny]
|
||||
)
|
||||
|
||||
|
||||
import comic.views
|
||||
import comic_auth.views
|
||||
from comic import rest, feeds
|
||||
|
||||
router = ExtendedDefaultRouter()
|
||||
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'generate_thumbnail', rest.GenerateThumbnailViewSet, basename='generate_thumbnail')
|
||||
router.register(r'read', rest.ReadViewSet, basename='read')\
|
||||
@@ -60,11 +53,6 @@ router.register(r'initial_setup', rest.InitialSetup, basename='initial_setup')
|
||||
|
||||
|
||||
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("feed/<user_selector>/", feeds.RecentComicsAPI()),
|
||||
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
||||
|
||||
@@ -1,49 +1,10 @@
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.db.models import Case, When, PositiveSmallIntegerField, F
|
||||
from django.http import HttpRequest
|
||||
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
|
||||
|
||||
|
||||
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),))
|
||||
from comic.models import ComicBook, UserMisc, Directory
|
||||
|
||||
|
||||
class RecentComicsAPI(Feed):
|
||||
|
||||
111
comic/forms.py
111
comic/forms.py
@@ -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)
|
||||
|
||||
285
comic/models.py
285
comic/models.py
@@ -3,21 +3,21 @@ import mimetypes
|
||||
import uuid
|
||||
import zipfile
|
||||
from functools import reduce
|
||||
from itertools import zip_longest, chain
|
||||
from itertools import zip_longest
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Union, Tuple
|
||||
from typing import Optional, List, Union, Tuple, Final
|
||||
|
||||
# noinspection PyPackageRequirements
|
||||
import fitz
|
||||
import rarfile
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from PIL.Image import Image as Image_type
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, AbstractUser
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.db import models
|
||||
from django.db.models import UniqueConstraint
|
||||
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 imagekit.models import ProcessedImageField
|
||||
from imagekit.processors import ResizeToFill
|
||||
@@ -57,34 +57,14 @@ class Directory(models.Model):
|
||||
return "Directory: {0}; {1}".format(self.name, self.parent)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
def title(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
def type(self) -> str:
|
||||
return 'Directory'
|
||||
|
||||
def mark_read(self, user):
|
||||
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):
|
||||
def generate_thumbnail(self) -> None:
|
||||
book: ComicBook = ComicBook.objects.filter(directory=self).order_by('file_name').first()
|
||||
if not book:
|
||||
return
|
||||
@@ -105,7 +85,7 @@ class Directory(models.Model):
|
||||
else:
|
||||
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:
|
||||
p = []
|
||||
p.append(self.name)
|
||||
@@ -113,7 +93,7 @@ class Directory(models.Model):
|
||||
self.parent.get_path_items(p)
|
||||
return p
|
||||
|
||||
def get_path_objects(self, p=None):
|
||||
def get_path_objects(self, p=None) -> List["Directory"]:
|
||||
if p is None:
|
||||
p = []
|
||||
p.append(self)
|
||||
@@ -121,14 +101,6 @@ class Directory(models.Model):
|
||||
self.parent.get_path_objects(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):
|
||||
file_name = models.TextField()
|
||||
@@ -148,36 +120,17 @@ class ComicBook(models.Model):
|
||||
UniqueConstraint(fields=['directory', 'file_name'], name='one_comic_name_per_directory')
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.file_name
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
def title(self) -> str:
|
||||
return self.file_name
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
def type(self) -> str:
|
||||
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:
|
||||
base_dir = settings.COMIC_BOOK_VOLUME
|
||||
if self.directory:
|
||||
@@ -185,7 +138,7 @@ class ComicBook(models.Model):
|
||||
else:
|
||||
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
|
||||
if self.directory:
|
||||
archive_path = Path(base_dir, self.directory.path, self.file_name)
|
||||
@@ -196,7 +149,7 @@ class ComicBook(models.Model):
|
||||
except rarfile.NotRarFile:
|
||||
archive = zipfile.ZipFile(archive_path)
|
||||
except zipfile.BadZipfile:
|
||||
return False
|
||||
return False, False
|
||||
|
||||
page_obj = ComicPage.objects.get(Comic=self, index=page)
|
||||
try:
|
||||
@@ -207,39 +160,34 @@ class ComicBook(models.Model):
|
||||
out = (archive.open(page_obj.page_file_name), page_obj.content_type)
|
||||
return out
|
||||
|
||||
def get_thumbnail_url(self):
|
||||
if self.thumbnail:
|
||||
return self.thumbnail.url
|
||||
else:
|
||||
self.generate_thumbnail()
|
||||
return self.thumbnail.url
|
||||
def generate_thumbnail_pdf(self, page_index: int = 0) -> Tuple[io.BytesIO, Image_type, str]:
|
||||
img, pil_data = self._get_pdf_image(page_index if page_index else 0)
|
||||
return img, pil_data, 'Image/JPEG'
|
||||
|
||||
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]]:
|
||||
img, content_type, pil_data = False, False, False
|
||||
if page_index:
|
||||
img, content_type = self.get_image(page_index)
|
||||
pil_data = Image.open(img)
|
||||
else:
|
||||
for x in range(ComicPage.objects.filter(Comic=self).count()):
|
||||
try:
|
||||
img, content_type = self.get_image(x)
|
||||
pil_data = Image.open(img)
|
||||
break
|
||||
except UnidentifiedImageError:
|
||||
continue
|
||||
return img, pil_data, content_type
|
||||
|
||||
def generate_thumbnail(self, page_index: int = None) -> None:
|
||||
|
||||
if Path(self.file_name).suffix.lower() == '.pdf':
|
||||
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'
|
||||
img, pil_data, content_type = self.generate_thumbnail_pdf(page_index if page_index else 0)
|
||||
else:
|
||||
if page_index:
|
||||
img, content_type = self.get_image(page_index)
|
||||
pil_data = Image.open(img)
|
||||
else:
|
||||
for x in range(ComicPage.objects.filter(Comic=self).count()):
|
||||
try:
|
||||
img, content_type = self.get_image(x)
|
||||
pil_data = Image.open(img)
|
||||
break
|
||||
except UnidentifiedImageError:
|
||||
continue
|
||||
try:
|
||||
img
|
||||
content_type
|
||||
pil_data
|
||||
except NameError:
|
||||
return
|
||||
img, pil_data, content_type = self.generate_thumbnail_archive(page_index)
|
||||
if not img:
|
||||
return
|
||||
self.thumbnail = InMemoryUploadedFile(
|
||||
img,
|
||||
None,
|
||||
@@ -250,133 +198,25 @@ class ComicBook(models.Model):
|
||||
)
|
||||
self.save()
|
||||
|
||||
def _get_pdf_image(self, page_index: int):
|
||||
# noinspection PyTypeChecker
|
||||
def _get_pdf_image(self, page_index: int) -> Tuple[io.BytesIO, Image_type]:
|
||||
# noinspection PyUnresolvedReferences
|
||||
doc = fitz.open(self.get_pdf())
|
||||
page = doc[page_index]
|
||||
pix = page.get_pixmap()
|
||||
mode = "RGBA" if pix.alpha else "RGB"
|
||||
pil_data = Image.frombytes(mode, [pix.width, pix.height], pix.samples)
|
||||
mode: Final = "RGBA" if pix.alpha else "RGB"
|
||||
# noinspection PyTypeChecker
|
||||
pil_data = Image.frombytes(mode, (pix.width, pix.height), pix.samples)
|
||||
img = io.BytesIO()
|
||||
pil_data.save(img, format="JPEG")
|
||||
img.seek(0)
|
||||
return img, pil_data
|
||||
|
||||
def is_last_page(self, page):
|
||||
if (self.page_count - 1) == page:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def page_count(self):
|
||||
def page_count(self) -> int:
|
||||
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
|
||||
def process_comic_book(comic_file_path: Path, directory: "Directory" = False) -> Union["ComicBook", Path]:
|
||||
"""
|
||||
|
||||
:type comic_file_path: str
|
||||
:type directory: Directory
|
||||
"""
|
||||
try:
|
||||
book = ComicBook.objects.get(file_name=comic_file_path.name, version=0)
|
||||
book.directory = directory
|
||||
@@ -404,19 +244,8 @@ class ComicBook(models.Model):
|
||||
page.save()
|
||||
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
|
||||
def get_archive_path(self):
|
||||
def get_archive_path(self) -> Path:
|
||||
if self.directory:
|
||||
return Path(settings.COMIC_BOOK_VOLUME, self.directory.get_path(), self.file_name)
|
||||
else:
|
||||
@@ -441,13 +270,13 @@ class ComicBook(models.Model):
|
||||
raise NotCompatibleArchive
|
||||
|
||||
@staticmethod
|
||||
def get_archive_files(archive):
|
||||
def get_archive_files(archive) -> List[Tuple[str, str]]:
|
||||
return [
|
||||
(x, mimetypes.guess_type(x)[0]) for x in sorted(archive.namelist())
|
||||
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:
|
||||
pages = ComicPage.objects.filter(Comic=self)
|
||||
|
||||
@@ -493,7 +322,8 @@ class ComicPage(models.Model):
|
||||
|
||||
class ComicStatus(models.Model):
|
||||
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)
|
||||
unread = models.BooleanField(default=True)
|
||||
finished = models.BooleanField(default=False)
|
||||
@@ -503,23 +333,10 @@ class ComicStatus(models.Model):
|
||||
UniqueConstraint(fields=['user', 'comic'], name='one_per_user_per_comic')
|
||||
]
|
||||
|
||||
def mark_read(self):
|
||||
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):
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<ComicStatus:{self.user.username}:{self.comic.file_name}:{self.last_read_page}:"
|
||||
f"{self.unread}:{self.finished}"
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Union, NamedTuple, List
|
||||
from uuid import UUID
|
||||
|
||||
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.core.exceptions import ValidationError
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
def to_representation(self, value):
|
||||
if not value:
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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">×</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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -1,4 +0,0 @@
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
]
|
||||
|
||||
250
comic/util.py
250
comic/util.py
@@ -1,50 +1,12 @@
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from typing import Union, Iterable
|
||||
|
||||
from django.conf import settings
|
||||
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
|
||||
from .models import ComicBook, Directory
|
||||
|
||||
|
||||
@dataclass()
|
||||
class Breadcrumb:
|
||||
def __init__(self):
|
||||
self.name = "Home"
|
||||
self.url = "/comic/"
|
||||
self.selector = ''
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
name: str = 'Home'
|
||||
selector: str = ''
|
||||
|
||||
|
||||
def generate_breadcrumbs_from_path(directory=False, book=False):
|
||||
@@ -59,197 +21,17 @@ def generate_breadcrumbs_from_path(directory=False, book=False):
|
||||
else:
|
||||
folders = []
|
||||
for item in folders[::-1]:
|
||||
bc = Breadcrumb()
|
||||
bc.name = item.name
|
||||
bc.url = "/comic/" + urlsafe_base64_encode(item.selector.bytes)
|
||||
bc.selector = item.selector
|
||||
output.append(bc)
|
||||
output.append(
|
||||
Breadcrumb(
|
||||
name=item.name,
|
||||
selector=item.selector
|
||||
)
|
||||
)
|
||||
if book:
|
||||
bc = Breadcrumb()
|
||||
bc.name = book.file_name
|
||||
bc.url = "/read/" + urlsafe_base64_encode(book.selector.bytes)
|
||||
bc.selector = book.selector
|
||||
output.append(bc)
|
||||
|
||||
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)
|
||||
output.append(
|
||||
Breadcrumb(
|
||||
name=book.file_name,
|
||||
selector=book.selector
|
||||
)
|
||||
)
|
||||
).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>'
|
||||
return output
|
||||
|
||||
408
comic/views.py
408
comic/views.py
@@ -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/")
|
||||
|
||||
@@ -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}),
|
||||
)
|
||||
@@ -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 %}
|
||||
@@ -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
320
package-lock.json
generated
@@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
14
package.json
14
package.json
@@ -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
28
poetry.lock
generated
@@ -133,7 +133,7 @@ jinja2 = "*"
|
||||
name = "coverage"
|
||||
version = "6.4.4"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
@@ -296,14 +296,6 @@ async = ["django-celery (>=3.0)"]
|
||||
async_dramatiq = ["django-dramatiq (>=0.4.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]]
|
||||
name = "django-permissions-policy"
|
||||
version = "4.13.0"
|
||||
@@ -550,17 +542,6 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
|
||||
[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)"]
|
||||
|
||||
[[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]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.1"
|
||||
@@ -1024,7 +1005,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "4a1018e12d5d29a99175c79c0afd14a4faaf846a535f2544b670842739d867e0"
|
||||
content-hash = "02ffb9bcd22babd30fb99e50a293f2ed1421375c4c36e5ee534c5530b63ca4e8"
|
||||
|
||||
[metadata.files]
|
||||
appnope = [
|
||||
@@ -1176,10 +1157,6 @@ django-imagekit = [
|
||||
{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"},
|
||||
]
|
||||
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-silk = []
|
||||
django-sri = []
|
||||
@@ -1216,7 +1193,6 @@ loguru = [
|
||||
{file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"},
|
||||
{file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"},
|
||||
]
|
||||
markdown = []
|
||||
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_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
|
||||
|
||||
@@ -3,7 +3,7 @@ line_length = 119
|
||||
|
||||
[tool.poetry]
|
||||
name = "cbwebreader"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
description = "CBR/Z Web Reader"
|
||||
authors = ["ajurna <ajurna@gmail.com>"]
|
||||
license = "Creative Commons Attribution-ShareAlike 4.0 International License"
|
||||
@@ -19,7 +19,6 @@ django-silk = "^5.0.0"
|
||||
mysqlclient = "^2.0.1"
|
||||
psycopg2 = "^2.8.6"
|
||||
rarfile = "^4.0"
|
||||
coverage = "^6.2"
|
||||
django-extensions = "^3.2.0"
|
||||
Pillow = "^9.1.1"
|
||||
django-imagekit = "^4.0.2"
|
||||
@@ -28,10 +27,8 @@ django-bootstrap4 = "^22.1"
|
||||
django-csp = "^3.7"
|
||||
django-boost = "^2.0"
|
||||
django-sri = "^0.4.0"
|
||||
django-node-assets = "^0.9.9"
|
||||
django-permissions-policy = "^4.9.0"
|
||||
djangorestframework = "^3.13.1"
|
||||
Markdown = "^3.3.7"
|
||||
django-filter = "^22.1"
|
||||
django-cors-headers = "^3.13.0"
|
||||
djangorestframework-simplejwt = "^5.2.0"
|
||||
@@ -44,6 +41,7 @@ mypy = "^0.971"
|
||||
Werkzeug = "<2.1"
|
||||
pyOpenSSL = "^22.0.0"
|
||||
ipython = "^8.4.0"
|
||||
coverage = "^6.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
1
static/css/base.min.css
vendored
1
static/css/base.min.css
vendored
@@ -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}
|
||||
@@ -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');
|
||||
})
|
||||
1
static/js/comic_list.min.js
vendored
1
static/js/comic_list.min.js
vendored
@@ -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")});
|
||||
@@ -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)
|
||||
})
|
||||
1
static/js/read_comic.min.js
vendored
1
static/js/read_comic.min.js
vendored
@@ -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)});
|
||||
@@ -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()
|
||||
})
|
||||
1
static/js/read_comic_pdf.min.js
vendored
1
static/js/read_comic_pdf.min.js
vendored
@@ -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()});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
} );
|
||||
1
static/js/recent_comics.min.js
vendored
1
static/js/recent_comics.min.js
vendored
@@ -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()}})})});
|
||||
Reference in New Issue
Block a user