mirror of
https://github.com/ajurna/cbwebreader.git
synced 2025-12-06 06:17:17 +00:00
Rewrite of Comic lists so that tehy have thumbnails. (#26)
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
import os
|
||||
from os.path import isdir
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import UnidentifiedImageError
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from loguru import logger
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
|
||||
from comic.models import ComicBook, Directory, ComicStatus
|
||||
from comic.models import ComicBook, Directory
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -15,6 +13,7 @@ class Command(BaseCommand):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.OUTPUT = False
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
@@ -91,4 +90,10 @@ class Command(BaseCommand):
|
||||
book.version = 1
|
||||
book.save()
|
||||
except ComicBook.DoesNotExist:
|
||||
ComicBook.process_comic_book(file, directory)
|
||||
book = ComicBook.process_comic_book(file, directory)
|
||||
try:
|
||||
book.generate_thumbnail()
|
||||
except UnidentifiedImageError:
|
||||
book.generate_thumbnail(1)
|
||||
except:
|
||||
pass
|
||||
|
||||
19
comic/migrations/0022_comicbook_thumbnail.py
Normal file
19
comic/migrations/0022_comicbook_thumbnail.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2 on 2021-04-21 11:13
|
||||
|
||||
from django.db import migrations
|
||||
import imagekit.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comic', '0021_delete_setting'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='comicbook',
|
||||
name='thumbnail',
|
||||
field=imagekit.models.fields.ProcessedImageField(null=True, upload_to='thumbs'),
|
||||
),
|
||||
]
|
||||
19
comic/migrations/0023_directory_thumbnail.py
Normal file
19
comic/migrations/0023_directory_thumbnail.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2 on 2021-04-21 17:44
|
||||
|
||||
from django.db import migrations
|
||||
import imagekit.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comic', '0022_comicbook_thumbnail'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='directory',
|
||||
name='thumbnail',
|
||||
field=imagekit.models.fields.ProcessedImageField(null=True, upload_to='thumbs'),
|
||||
),
|
||||
]
|
||||
29
comic/migrations/0024_auto_20210422_0855.py
Normal file
29
comic/migrations/0024_auto_20210422_0855.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.2 on 2021-04-22 07:55
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comic', '0023_directory_thumbnail'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='comicbook',
|
||||
name='thumbnail_index',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='directory',
|
||||
name='thumbnail_index',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='directory',
|
||||
name='thumbnail_issue',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='directory_thumbnail_issue', to='comic.comicbook'),
|
||||
),
|
||||
]
|
||||
132
comic/models.py
132
comic/models.py
@@ -8,15 +8,18 @@ from os import listdir
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Union, Tuple
|
||||
|
||||
import PyPDF4
|
||||
import PyPDF4.utils
|
||||
import rarfile
|
||||
from PIL import Image
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.db import models
|
||||
from django.db.transaction import atomic
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
import PyPDF4
|
||||
import PyPDF4.utils
|
||||
|
||||
import rarfile
|
||||
from imagekit.models import ProcessedImageField
|
||||
from imagekit.processors import ResizeToFill
|
||||
|
||||
from comic.errors import NotCompatibleArchive
|
||||
|
||||
@@ -28,6 +31,15 @@ class Directory(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
parent = models.ForeignKey("Directory", null=True, blank=True, on_delete=models.CASCADE)
|
||||
selector = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True)
|
||||
thumbnail = ProcessedImageField(upload_to='thumbs',
|
||||
processors=[ResizeToFill(200, 300)],
|
||||
format='JPEG',
|
||||
options={'quality': 60},
|
||||
null=True)
|
||||
thumbnail_issue = models.ForeignKey("ComicBook", null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='directory_thumbnail_issue')
|
||||
thumbnail_index = models.PositiveIntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@@ -35,6 +47,39 @@ class Directory(models.Model):
|
||||
def __str__(self):
|
||||
return "Directory: {0}; {1}".format(self.name, self.parent)
|
||||
|
||||
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()
|
||||
return self.thumbnail.url
|
||||
|
||||
def generate_thumbnail(self):
|
||||
book = ComicBook.objects.filter(directory=self).order_by('file_name').first()
|
||||
if not book:
|
||||
return
|
||||
img, content_type = book.get_image(0)
|
||||
pil_data = Image.open(img)
|
||||
self.thumbnail = InMemoryUploadedFile(
|
||||
img,
|
||||
None,
|
||||
f'{self.name}.jpg',
|
||||
content_type,
|
||||
pil_data.tell(),
|
||||
None
|
||||
)
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return self.get_path()
|
||||
@@ -63,6 +108,10 @@ 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)
|
||||
|
||||
|
||||
class ComicBook(models.Model):
|
||||
file_name = models.TextField()
|
||||
@@ -70,12 +119,33 @@ class ComicBook(models.Model):
|
||||
directory = models.ForeignKey(Directory, blank=True, null=True, on_delete=models.CASCADE)
|
||||
selector = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True)
|
||||
version = models.IntegerField(default=1)
|
||||
thumbnail = ProcessedImageField(upload_to='thumbs',
|
||||
processors=[ResizeToFill(200, 300)],
|
||||
format='JPEG',
|
||||
options={'quality': 60},
|
||||
null=True)
|
||||
thumbnail_index = models.PositiveIntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return self.file_name
|
||||
|
||||
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 selector_string(self):
|
||||
def url_safe_selector(self):
|
||||
return urlsafe_base64_encode(self.selector.bytes)
|
||||
|
||||
def get_pdf(self):
|
||||
@@ -99,6 +169,26 @@ 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(self, page_index: int = 0):
|
||||
img, content_type = self.get_image(page_index)
|
||||
pil_data = Image.open(img)
|
||||
self.thumbnail = InMemoryUploadedFile(
|
||||
img,
|
||||
None,
|
||||
f'{self.file_name}.jpg',
|
||||
content_type,
|
||||
pil_data.tell(),
|
||||
None
|
||||
)
|
||||
self.save()
|
||||
|
||||
def is_last_page(self, page):
|
||||
if (self.page_count - 1) == page:
|
||||
return True
|
||||
@@ -209,22 +299,15 @@ class ComicBook(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def pages(self):
|
||||
return [cp for cp in ComicPage.objects.filter(Comic=self).order_by("index")]
|
||||
|
||||
def page_name(self, index):
|
||||
return ComicPage.objects.get(Comic=self, index=index).page_file_name
|
||||
|
||||
@staticmethod
|
||||
def process_comic_book(comic_file_name: Path, directory: "Directory" = False) -> Union["ComicBook", Path]:
|
||||
def process_comic_book(comic_file_path: Path, directory: "Directory" = False) -> Union["ComicBook", Path]:
|
||||
"""
|
||||
|
||||
:type comic_file_name: str
|
||||
:type comic_file_path: str
|
||||
:type directory: Directory
|
||||
"""
|
||||
try:
|
||||
book = ComicBook.objects.get(file_name=comic_file_name, version=0)
|
||||
book = ComicBook.objects.get(file_name=comic_file_path.name, version=0)
|
||||
book.directory = directory
|
||||
book.version = 1
|
||||
book.save()
|
||||
@@ -232,12 +315,12 @@ class ComicBook(models.Model):
|
||||
except ComicBook.DoesNotExist:
|
||||
pass
|
||||
|
||||
book = ComicBook(file_name=comic_file_name, directory=directory if directory else None)
|
||||
book = ComicBook(file_name=comic_file_path.name, directory=directory if directory else None)
|
||||
book.save()
|
||||
try:
|
||||
archive, archive_type = book.get_archive()
|
||||
except NotCompatibleArchive:
|
||||
return comic_file_name
|
||||
return comic_file_path
|
||||
|
||||
if archive_type == 'archive':
|
||||
book.verify_pages()
|
||||
@@ -340,9 +423,18 @@ class ComicStatus(models.Model):
|
||||
unread = models.BooleanField(default=True)
|
||||
finished = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def read(self):
|
||||
return self.last_read_page
|
||||
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):
|
||||
return self.__repr__()
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
<script type="text/javascript" src="{% static "reveal.js/reveal.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "reveal.js/plugin/menu/menu.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "js/hammer.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "js/isotope.pkgd.min.js" %}"></script>
|
||||
|
||||
{% block script %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,121 +1,165 @@
|
||||
{% extends "base.html" %}
|
||||
{% 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>Comics</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 style="text-align: center;"><span class="fa fa-file"></span></th>
|
||||
<th width="100%">File/Folder</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 class="row">
|
||||
<div class="input-group">
|
||||
<input type="text" id="quicksearch" class="form-control" placeholder="Search" aria-label="Search list of comics" aria-describedby="button-addon4">
|
||||
<div id="filters" class="input-group-append" id="button-addon4">
|
||||
<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" title="Mark Un-Read" onclick="comic_action('{{ selector }}', 'Directory', 'mark_unread')"><i class="fas fa-book">Mark Un-Read</i></button>
|
||||
<button type="button" class="btn btn-primary dropdown-item" title="Mark Read" onclick="comic_action('{{ selector }}', 'Directory', 'mark_read')"><i class="fas fa-book-open">Mark Read</i></button>
|
||||
{# <button type="button" class="btn btn-primary dropdown-item" 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">
|
||||
{% for file in files %}
|
||||
<div class="m-2 grid-item {% if file.percent == 100 %}read{% else %}unread{% endif %}">
|
||||
<div class="card" style="width: 200px;">
|
||||
{% 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 }}" onerror="this.onerror=null;this.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 }}" onerror="this.onerror=null;this.src='{% static "img/placeholder.png" %}';">
|
||||
{% elif file.item_type == 'ComicBook' %}
|
||||
<img src="{% url 'comic_thumbnail' file.selector %}" class="card-img-top" alt="{{ file.name }}" onerror="this.onerror=null;this.src='{% static "img/placeholder.png" %}';">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
{% 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">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" style="width: {{ file.percent }}%;" aria-valuenow="{{ file.percent }}" aria-valuemin="0" aria-valuemax="100">{{ file.percent }}%</div>
|
||||
</div>
|
||||
</p>
|
||||
<div class="btn-group" role="group" aria-label="Comic Actions">
|
||||
<button type="button" class="btn btn-primary" title="Mark Un-Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_unread')"><i class="fas fa-book"></i></button>
|
||||
<button type="button" class="btn btn-primary" title="Mark Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_read')"><i class="fas fa-book-open"></i></button>
|
||||
<div class="btn-group" role="group">
|
||||
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||
<button type="button" class="btn btn-primary dropdown-item" title="Mark Un-Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_unread')"><i class="fas fa-book">Mark Un-Read</i></button>
|
||||
<button type="button" class="btn btn-primary dropdown-item" title="Mark Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_read')"><i class="fas fa-book-open">Mark Read</i></button>
|
||||
{% if file.item_type != 'Directory' %}
|
||||
<button type="button" class="btn btn-primary dropdown-item" title="Mark Previous Read"><i class="fas fa-book" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_previous')"><i class="fas fa-arrow-up">Mark Previous Read</i></i></button>
|
||||
{% endif %}
|
||||
{# <button type="button" class="btn btn-primary dropdown-item" title="Edit Comic"><i class="fas fa-edit">Edit Comic</i></button>#}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var table = $('#comic_list').DataTable({
|
||||
"processing": true,
|
||||
"stateSave": true,
|
||||
"ajax": {
|
||||
"type": "POST",
|
||||
"url": "{{ json_url }}",
|
||||
"data": function ( d ) {
|
||||
d.csrfmiddlewaretoken = Cookies.get('csrftoken');
|
||||
}
|
||||
},
|
||||
"rowCallback": function( row, data, index ) {
|
||||
var r = $(row);
|
||||
var cols = $('td:nth-child(n+2)', row);
|
||||
|
||||
if (data['selector'] === '0') {
|
||||
|
||||
} else {
|
||||
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);
|
||||
if (data['type'] === 'directory') {
|
||||
tds.html('');
|
||||
} else {
|
||||
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" : "blank", "orderable": false },
|
||||
{ "data" : "icon", "orderable": false },
|
||||
{ "data" : "name" },
|
||||
{"data": "label"}
|
||||
],
|
||||
|
||||
"order": [[2, 'asc']]
|
||||
var qsRegex;
|
||||
var buttonFilter;
|
||||
var $grid = $('.comic-container').isotope({
|
||||
itemSelector: '.grid-item',
|
||||
layoutMode: 'fitRows',
|
||||
filter: function() {
|
||||
var $this = $(this);
|
||||
var searchResult = qsRegex ? $this.text().match( qsRegex ) : true;
|
||||
var buttonResult = buttonFilter ? $this.is( buttonFilter ) : true;
|
||||
return searchResult && buttonResult;
|
||||
{#return searchResult#}
|
||||
}
|
||||
});
|
||||
$(".clickable-row").click(function() {
|
||||
window.document.location = $(this).data("href");
|
||||
$('#filters').on( 'click', 'button', function() {
|
||||
buttonFilter = $( this ).attr('data-filter');
|
||||
sessionStorage.setItem(window.location.href+"button", buttonFilter);
|
||||
$grid.isotope();
|
||||
});
|
||||
$('#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();
|
||||
var $quicksearch = $('#quicksearch').keyup( debounce( function() {
|
||||
qsRegex = new RegExp($quicksearch.val(), 'gi');
|
||||
sessionStorage.setItem(window.location.href+'text', $quicksearch.val());
|
||||
$grid.isotope();
|
||||
}) );
|
||||
|
||||
// debounce so filtering doesn't happen every millisecond
|
||||
function debounce( fn, threshold ) {
|
||||
var timeout;
|
||||
threshold = threshold || 100;
|
||||
return function debounced() {
|
||||
clearTimeout( timeout );
|
||||
var args = arguments;
|
||||
var _this = this;
|
||||
function delayed() {
|
||||
fn.apply( _this, args );
|
||||
}
|
||||
$('table tr td:first-child input').each(function(chkbx) {
|
||||
row = $(this);
|
||||
if (row.prop('checked') !== cb.prop('checked')){
|
||||
row.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
} );
|
||||
timeout = setTimeout( delayed, threshold );
|
||||
};
|
||||
}
|
||||
setInterval(function (){
|
||||
$grid.isotope();
|
||||
}, 1000)
|
||||
|
||||
let field = document.getElementById("quicksearch");
|
||||
|
||||
// See if we have an autosave value
|
||||
// (this will only happen if the page is accidentally refreshed)
|
||||
if (sessionStorage.getItem(window.location.href+'text') || sessionStorage.getItem(window.location.href+'button')) {
|
||||
// Restore the contents of the text field
|
||||
field.value = sessionStorage.getItem(window.location.href+'text');
|
||||
qsRegex = new RegExp($quicksearch.val(), 'gi');
|
||||
buttonFilter = sessionStorage.getItem(window.location.href+'button');
|
||||
$grid.isotope();
|
||||
}
|
||||
|
||||
// Listen for changes in the text field
|
||||
field.addEventListener("change", function() {
|
||||
// And save the results into the session storage object
|
||||
|
||||
});
|
||||
|
||||
function comic_action(selector, item_type, action) {
|
||||
$.ajax({
|
||||
url: '/comic/action/' + action + '/' + item_type + '/' + selector + '/',
|
||||
success: function (){window.location.reload()}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -103,43 +103,23 @@ class ComicBookTests(TestCase):
|
||||
folders = generate_directory(user)
|
||||
dir1 = folders[0]
|
||||
self.assertEqual(dir1.name, "test_folder")
|
||||
self.assertEqual(dir1.type, "directory")
|
||||
self.assertEqual("fa-folder-open", dir1.icon)
|
||||
d = Directory.objects.get(name="test_folder", parent__isnull=True)
|
||||
location = "/comic/{0}/".format(urlsafe_base64_encode(d.selector.bytes))
|
||||
self.assertEqual(dir1.location, location)
|
||||
self.assertEqual(dir1.label, '<center><span class="label label-default">Empty</span></center>')
|
||||
self.assertEqual(dir1.item_type, "Directory")
|
||||
|
||||
dir2 = folders[1]
|
||||
self.assertEqual(dir2.name, "test1.rar")
|
||||
self.assertEqual(dir2.type, "book")
|
||||
self.assertEqual("fa-book", dir2.icon)
|
||||
c = ComicBook.objects.get(file_name="test1.rar", directory__isnull=True)
|
||||
location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes))
|
||||
self.assertEqual(dir2.location, location)
|
||||
self.assertEqual(dir2.label, '<center><span class="label label-default">Unread</span></center>')
|
||||
self.assertEqual(dir2.item_type, "ComicBook")
|
||||
|
||||
dir3 = folders[2]
|
||||
self.assertEqual(dir3.name, "test2.rar")
|
||||
self.assertEqual(dir3.type, "book")
|
||||
self.assertEqual("fa-book", dir3.icon)
|
||||
c = ComicBook.objects.get(file_name="test2.rar", directory__isnull=True)
|
||||
location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes))
|
||||
self.assertEqual(dir3.location, location)
|
||||
self.assertEqual(dir3.label, '<center><span class="label label-primary">3/4</span></center>')
|
||||
self.assertEqual(dir2.item_type, "ComicBook")
|
||||
|
||||
dir4 = folders[3]
|
||||
self.assertEqual(dir4.name, "test3.rar")
|
||||
self.assertEqual(dir4.type, "book")
|
||||
self.assertEqual("fa-book", dir3.icon)
|
||||
c = ComicBook.objects.get(file_name="test3.rar", directory__isnull=True)
|
||||
location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes))
|
||||
self.assertEqual(dir4.location, location)
|
||||
self.assertEqual(dir4.label, '<center><span class="label label-default">Unread</span></center>')
|
||||
self.assertEqual(dir4.item_type, "ComicBook")
|
||||
|
||||
def test_pages(self):
|
||||
book = ComicBook.objects.get(file_name="test1.rar")
|
||||
pages = book.pages
|
||||
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")
|
||||
@@ -149,9 +129,6 @@ class ComicBookTests(TestCase):
|
||||
self.assertEqual(pages[3].page_file_name, "img4.bmp")
|
||||
self.assertEqual(pages[3].index, 3)
|
||||
|
||||
def test_page_name(self):
|
||||
book = ComicBook.objects.get(file_name="test1.rar")
|
||||
self.assertEqual(book.page_name(0), "img1.jpg")
|
||||
|
||||
def test_comic_list(self):
|
||||
c = Client()
|
||||
@@ -166,18 +143,6 @@ class ComicBookTests(TestCase):
|
||||
response = c.get(f"/comic/{urlsafe_base64_encode(directory.selector.bytes)}/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_comic_list_json(self):
|
||||
c = Client()
|
||||
response = c.post("/comic/list_json/")
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
c.login(username="test", password="test")
|
||||
response = c.post("/comic/list_json/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
directory = Directory.objects.first()
|
||||
response = c.post(f"/comic/list_json/{urlsafe_base64_encode(directory.selector.bytes)}/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_recent_comics(self):
|
||||
c = Client()
|
||||
response = c.get("/comic/recent/")
|
||||
@@ -240,7 +205,7 @@ class ComicBookTests(TestCase):
|
||||
response = c.get("/comic/edit/")
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
req_data = {"comic_list_length": 10, "func": "unread", "selected": book.selector_string}
|
||||
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)
|
||||
|
||||
|
||||
@@ -10,14 +10,15 @@ urlpatterns = [
|
||||
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("list_json/", views.comic_list_json, name="comic_list_json1"),
|
||||
path("list_json/<directory_selector>/", views.comic_list_json, name="comic_list_json2"),
|
||||
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/<int: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")
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from collections import OrderedDict
|
||||
from os import listdir
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Count, Q, F
|
||||
@@ -78,44 +79,30 @@ def generate_breadcrumbs_from_menu(paths):
|
||||
return output
|
||||
|
||||
|
||||
@dataclass
|
||||
class DirFile:
|
||||
def __init__(self):
|
||||
self.name = ""
|
||||
self.icon = ""
|
||||
self.location = ""
|
||||
self.label = ""
|
||||
self.type = ""
|
||||
self.selector = ""
|
||||
obj: Union[Directory, ComicBook]
|
||||
name: str = ''
|
||||
item_type: str = ''
|
||||
percent: int = 0
|
||||
selector: str = ''
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def populate_directory(self, directory, user):
|
||||
self.name = directory.name
|
||||
self.icon = "fa-folder-open"
|
||||
self.selector = urlsafe_base64_encode(directory.selector.bytes)
|
||||
self.location = "/comic/{0}/".format(self.selector)
|
||||
self.label = generate_dir_status(directory.total, directory.total_read)
|
||||
self.type = "directory"
|
||||
|
||||
def populate_comic(self, comic, user):
|
||||
if type(comic) == str:
|
||||
self.icon = "fa-exclamation-circle"
|
||||
self.name = comic
|
||||
self.selector = "0"
|
||||
self.location = "/"
|
||||
self.label = '<center><span class="label label-danger">Error</span></center>'
|
||||
self.type = "book"
|
||||
else:
|
||||
self.icon = "fa-book"
|
||||
self.name = comic.file_name
|
||||
self.selector = urlsafe_base64_encode(comic.selector.bytes)
|
||||
self.location = "/comic/read/{0}/".format(self.selector)
|
||||
self.label = generate_label(comic)
|
||||
self.type = "book"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DirFile: {self.name}: {self.type}>"
|
||||
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
|
||||
try:
|
||||
self.percent = int((self.obj.total_read / (self.obj.total - total_adjustment)) * 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
|
||||
|
||||
|
||||
def generate_directory(user, directory=False):
|
||||
@@ -127,11 +114,9 @@ def generate_directory(user, directory=False):
|
||||
files = []
|
||||
if directory:
|
||||
dir_path = Path(base_dir, directory.path)
|
||||
# ordered_dir_list = sorted(dir_path.glob('*'))
|
||||
dir_list = [x for x in sorted(dir_path.glob('*')) if Path(base_dir, directory.path, x).is_dir()]
|
||||
else:
|
||||
dir_path = base_dir
|
||||
# ordered_dir_list = base_dir.glob('*')
|
||||
dir_list = [x for x in sorted(dir_path.glob('*')) if Path(base_dir, x).is_dir()]
|
||||
file_list = [x for x in sorted(dir_path.glob('*')) if x.is_file()]
|
||||
if directory:
|
||||
@@ -154,23 +139,19 @@ def generate_directory(user, directory=False):
|
||||
ComicStatus.objects.bulk_create(new_status)
|
||||
|
||||
file_list_obj = file_list_obj.annotate(
|
||||
total_pages=Count('comicpage', distinct=True),
|
||||
last_read_page=F('comicstatus__last_read_page'),
|
||||
total=Count('comicpage', distinct=True),
|
||||
total_read=F('comicstatus__last_read_page'),
|
||||
finished=F('comicstatus__finished'),
|
||||
unread=F('comicstatus__unread'),
|
||||
user=F('comicstatus__user')
|
||||
).filter(Q(user__isnull=True) | Q(user=user.id))
|
||||
|
||||
for directory_obj in dir_list_obj:
|
||||
df = DirFile()
|
||||
df.populate_directory(directory_obj, user)
|
||||
files.append(df)
|
||||
files.append(DirFile(directory_obj))
|
||||
dir_list.remove(Path(dir_path, directory_obj.name))
|
||||
|
||||
for file_obj in file_list_obj:
|
||||
df = DirFile()
|
||||
df.populate_comic(file_obj, user)
|
||||
files.append(df)
|
||||
files.append(DirFile(file_obj))
|
||||
file_list.remove(Path(dir_path, file_obj.file_name))
|
||||
|
||||
for directory_name in dir_list:
|
||||
@@ -181,18 +162,14 @@ def generate_directory(user, directory=False):
|
||||
directory_obj.save()
|
||||
directory_obj.total = 0
|
||||
directory_obj.total_read = 0
|
||||
df = DirFile()
|
||||
df.populate_directory(directory_obj, user)
|
||||
files.append(df)
|
||||
files.append(DirFile(directory_obj))
|
||||
|
||||
for file_name in file_list:
|
||||
if file_name.suffix.lower() in [".rar", ".zip", ".cbr", ".cbz", ".pdf"]:
|
||||
book = ComicBook.process_comic_book(file_name.name, directory)
|
||||
df = DirFile()
|
||||
df.populate_comic(book, user)
|
||||
files.append(df)
|
||||
book = ComicBook.process_comic_book(file_name, directory)
|
||||
files.append(DirFile(book))
|
||||
files.sort(key=lambda x: x.name)
|
||||
files.sort(key=lambda x: x.type, reverse=True)
|
||||
files.sort(key=lambda x: x.item_type, reverse=True)
|
||||
return files
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from PIL import Image
|
||||
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.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.db.models import Max, Count, F
|
||||
from django.db.transaction import atomic
|
||||
from django.http import HttpResponse, FileResponse
|
||||
@@ -31,60 +33,55 @@ from .util import (
|
||||
def comic_list(request, directory_selector=False):
|
||||
if User.objects.all().count() == 0:
|
||||
return redirect("/comic/settings/")
|
||||
# try:
|
||||
# base_dir = Setting.objects.get(name="BASE_DIR").value
|
||||
# except Setting.DoesNotExist:
|
||||
# return redirect("/comic/settings/")
|
||||
# if not path.isdir(base_dir):
|
||||
# return redirect("/comic/settings/")
|
||||
|
||||
directory = None
|
||||
if directory_selector:
|
||||
selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
|
||||
directory = Directory.objects.get(selector=selector)
|
||||
else:
|
||||
directory = False
|
||||
|
||||
if directory:
|
||||
title = generate_title_from_path(directory.path)
|
||||
breadcrumbs = generate_breadcrumbs_from_path(directory)
|
||||
json_url = "/comic/list_json/{0}/".format(directory_selector)
|
||||
else:
|
||||
title = generate_title_from_path("Home")
|
||||
breadcrumbs = generate_breadcrumbs_from_path()
|
||||
json_url = "/comic/list_json/"
|
||||
|
||||
files = generate_directory(request.user, directory)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"comic/comic_list.html",
|
||||
{"breadcrumbs": breadcrumbs, "menu": Menu(request.user, "Browse"), "title": title, "json_url": json_url},
|
||||
{
|
||||
"breadcrumbs": breadcrumbs,
|
||||
"menu": Menu(request.user, "Browse"),
|
||||
"title": title,
|
||||
"files": files,
|
||||
"selector": directory_selector if directory_selector else 'None'
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def comic_list_json(request, directory_selector=False):
|
||||
icon_str = '<span class="fa {0}"></span>'
|
||||
if directory_selector:
|
||||
directory_selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
|
||||
directory = Directory.objects.get(selector=directory_selector)
|
||||
else:
|
||||
directory = False
|
||||
files = generate_directory(request.user, directory)
|
||||
response_data = dict()
|
||||
response_data["data"] = []
|
||||
for file in files:
|
||||
response_data["data"].append(
|
||||
{
|
||||
"blank": "",
|
||||
"selector": file.selector,
|
||||
"type": file.type,
|
||||
"icon": icon_str.format(file.icon),
|
||||
"name": file.name,
|
||||
"label": file.label,
|
||||
"url": file.location,
|
||||
}
|
||||
)
|
||||
return HttpResponse(json.dumps(response_data), content_type="application/json")
|
||||
def perform_action(request, operation, item_type, selector):
|
||||
if operation not in ['mark_read', 'mark_unread', 'mark_previous']:
|
||||
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)
|
||||
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)
|
||||
getattr(directory, operation)(request.user)
|
||||
return HttpResponse(204)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -328,6 +325,22 @@ def get_image(_, comic_selector, page):
|
||||
return FileResponse(img, content_type=content)
|
||||
|
||||
|
||||
@xframe_options_sameorigin
|
||||
@login_required
|
||||
def comic_thumbnail(_, comic_selector):
|
||||
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
|
||||
book = ComicBook.objects.get(selector=selector)
|
||||
return redirect(book.get_thumbnail_url())
|
||||
|
||||
|
||||
@xframe_options_sameorigin
|
||||
@login_required
|
||||
def directory_thumbnail(_, directory_selector):
|
||||
selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
|
||||
folder = Directory.objects.get(selector=selector)
|
||||
return redirect(folder.get_thumbnail_url())
|
||||
|
||||
|
||||
@login_required
|
||||
def get_pdf(_, comic_selector):
|
||||
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
|
||||
|
||||
Reference in New Issue
Block a user