8 Commits
v0.1 ... v0.2

Author SHA1 Message Date
eneller
c5ef95d69b fix: table headers 2025-03-20 21:53:26 +01:00
eneller
c728f65e73 fix: search 2025-03-20 21:46:53 +01:00
eneller
c1f813d70c frontend tweaks 2025-03-20 20:53:19 +01:00
eneller
aa66c06f88 feat: CTRL + K search, css 2025-03-20 20:15:51 +01:00
eneller
518a8a1744 minor tweaks 2025-03-20 15:07:06 +01:00
eneller
b65d878981 build: add watchman 2025-03-19 22:25:23 +01:00
eneller
3d10081846 feat: error handling 2025-03-19 22:04:23 +01:00
eneller
67b0654514 fix: bookmarklet 2025-03-19 19:34:53 +01:00
10 changed files with 123 additions and 51 deletions

1
.gitignore vendored
View File

@@ -372,3 +372,4 @@ pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python,django,visualstudiocode,intellij+all # End of https://www.toptal.com/developers/gitignore/api/python,django,visualstudiocode,intellij+all
staticfiles/

View File

@@ -1,2 +1,6 @@
# epub2go-web # epub2go-web
A simple Website to provide a `NNI (Non-Nerd Interface)` to [epub2go.py](https://github.com/eneller/epub2go.py), a web to epub converter. A simple Website to provide a `NNI (Non-Nerd Interface)` to [epub2go.py](https://github.com/eneller/epub2go.py), a web to epub converter.
## Development
This project uses [watchman](https://facebook.github.io/watchman/) for file watching and reloading.
Follow the [official instructions](https://facebook.github.io/watchman/docs/install.html) for your system to install, django will default to its standard watcher otherwise.

View File

@@ -8,6 +8,8 @@ dependencies = [
"celery>=5.4.0", "celery>=5.4.0",
"django>=5.1.6", "django>=5.1.6",
"epub2go", "epub2go",
"python-dotenv>=1.0.1",
"pywatchman>=2.0.0",
] ]
[tool.uv.sources] [tool.uv.sources]

4
src/.watchmanconfig Normal file
View File

@@ -0,0 +1,4 @@
{
"ignore_dirs": ["node_modules"]
}

View File

@@ -1,5 +1,5 @@
from django.shortcuts import render from django.shortcuts import render
from django.http import HttpRequest, HttpResponse, FileResponse from django.http import HttpRequest, HttpResponse, FileResponse, HttpResponseBadRequest
from django.conf import settings from django.conf import settings
from celery import shared_task from celery import shared_task
@@ -12,7 +12,7 @@ import logging
logger = logging.getLogger(__name__) #TODO configure logging logger = logging.getLogger(__name__) #TODO configure logging
converter = GBConvert(downloaddir=settings.MEDIA_ROOT) converter = GBConvert(downloaddir=settings.MEDIA_ROOT)
books = get_all_books()# TODO get from pickle books = sorted(get_all_books(), key= lambda b: b.title)# TODO get from pickle
gbnetloc = urlparse(allbooks_url).netloc gbnetloc = urlparse(allbooks_url).netloc
def index(request: HttpRequest): def index(request: HttpRequest):
@@ -21,22 +21,26 @@ def index(request: HttpRequest):
'http_host': request.META['HTTP_HOST'], 'http_host': request.META['HTTP_HOST'],
'books': books, 'books': books,
'book_count': len(books), 'book_count': len(books),
'allbooks_url': allbooks_url,
} }
targetParam = request.GET.get('t', None) targetParam = request.GET.get('t', None)
if validateUrl(targetParam): if targetParam:
fpath = getEpub(targetParam) if validateUrl(targetParam):
fname = os.path.basename(fpath) # download file
file = open(fpath, 'rb') fpath = getEpub(targetParam)
response = FileResponse(file) fname = os.path.basename(fpath)
response['Content-Type'] = 'application/octet-stream' file = open(fpath, 'rb')
response['Content-Disposition'] = f'attachment; filename="{fname}"' response = FileResponse(file)
return response response['Content-Type'] = 'application/octet-stream'
response['Content-Disposition'] = f'attachment; filename="{fname}"'
return render(request, 'index.html', context) return response
else: return HttpResponseBadRequest('Input URL invalid.')
else:
# return base view
return render(request, 'index.html', context)
def validateUrl(param)->bool : def validateUrl(param)->bool :
if not param: return False
netloc = urlparse(param).netloc netloc = urlparse(param).netloc
if(netloc == gbnetloc): return True if(netloc == gbnetloc): return True

View File

@@ -26,7 +26,7 @@ SECRET_KEY = "django-insecure-^@m5bl*8x+=@c^b0lhkgb-%_#9#&oad=v15jq=!0$g#x17zjf8
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = ['*']
# Application definition # Application definition
@@ -123,6 +123,7 @@ STATIC_URL = "static/"
STATICFILES_DIRS = [ STATICFILES_DIRS = [
PROJ_DIR / "static/", PROJ_DIR / "static/",
] ]
STATIC_ROOT = PROJ_DIR/ "staticfiles"
MEDIA_URL = "media/" MEDIA_URL = "media/"
MEDIA_ROOT = PROJ_DIR / "media/" MEDIA_ROOT = PROJ_DIR / "media/"

View File

@@ -2,12 +2,19 @@
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const searchInput = document.getElementById('searchInput'); const searchInput = document.getElementById('searchInput');
const table = document.getElementById('table'); const table = document.getElementById('table');
const table_r = Array.from(table.getElementsByTagName('tr')); const table_r = Array.from(table.getElementsByClassName('table-entry'));
// allow search from url parameter document.addEventListener('keydown', (event)=>{
if (event.ctrlKey && event.key === 'k'){
event.preventDefault();
searchInput.select();
}
});
// search from url parameter
let searchParam = params.get('s'); let searchParam = params.get('s');
if (searchParam){ if (searchParam){
searchInput.value = searchParam; searchInput.value = searchParam;
console.log(searchParam);
search(searchParam); search(searchParam);
} }
@@ -16,9 +23,10 @@ function submitSearch(event){
search(); search();
} }
function search(searchStr = searchInput.value){ function search(searchStr = searchInput.value){
searchStr= searchStr.toLowerCase();
function showMatch(tr){ function showMatch(tr){
// match search with list // match search with list
let searchSuccess = Array.from(tr.getElementsByClassName('table-data')).map(e => e.textContent) let searchSuccess = Array.from(tr.getElementsByClassName('table-data')).map(e => e.textContent.toLowerCase())
.join(' ') .join(' ')
.indexOf(searchStr) > -1; .indexOf(searchStr) > -1;
if (searchSuccess) tr.style.display = ""; if (searchSuccess) tr.style.display = "";

View File

@@ -1,54 +1,78 @@
/* this is part of the http://bettermotherfuckingwebsite.com/ */ /*TODO also style svg icons accordingly */
:root{
--bg:#faf0e673;
--bg-acc:#EEEEEE;
--bg-hover: #DDDDDD;
--fg:#444;
--fg-deemph: #777;
}
html{
font-family: serif;
}
body{ body{
background-color: var(--bg);
margin:40px auto; margin:40px auto;
max-width:650px; max-width:800px;
line-height:1.6; line-height:1.4;
font-size:18px; font-size:18px;
color:#444; color:var(--fg);
padding:0 padding:0 10px;
10px} }
h1,h2,h3{ h1,h2,h3{
line-height:1.2 line-height:1.2;
} letter-spacing: -2%;
/* custom styles here */
:root{
--white:#faf0e673;
}
body{
background-color: var(--white)
} }
header{ header{
text-align: center; text-align: center;
justify-content: center; justify-content: center;
margin-bottom: 4%;
}
small{
color: var(--fg-deemph);
} }
.searchbar{ .searchbar{
width: fit-content; width: 100%;
}
#searchInput {
background-color: var(--bg);
width: 100%;
padding: .2em;
font-size: larger;
border-radius: 10px;
border-color: var(--bg-acc);
box-shadow: var(--bg-acc) 2px 2px;
}
table, tr{
/* make table not resize when elements are hidden by searching */
width: 100%;
}
th{
text-align: left;
} }
tr:nth-child(even){ tr:nth-child(even){
background-color: #EEEEEE; background-color: var(--bg-acc);
} }
tr:hover{ tr:hover{
background-color: #DDDDDD; background-color: var(--bg-hover);
transition: all 2ms; transition: all 2ms;
} }
.inline-icon{ .inline-icon, .header-icon{
vertical-align: middle; vertical-align: middle;
height: 1em; height: 1em;
} }
.header-icon{ .header-icon{
vertical-align: middle;
height: 1em;
padding: .5em; padding: .5em;
fill: var(--fg-deemph);
} }
a:hover, a:any-link{ a:hover, a:any-link{
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
.table-link{ .table-link{
/* TODO fix links with no title/content being almost unclickable */
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 3px; padding: 1px;
} }

View File

@@ -10,19 +10,21 @@
<body> <body>
<header> <header>
<div> <div>
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
<a href="javascript:void(window.open('{{ http_host }}/?t='+encodeURIComponent(window.location.toString())))" title="Als Lesezeichen speichern"><!--TODO fix domain part as variable--> <h2>1 Click Download für Literatur</h2>
<img src="{% static 'bookmark.svg' %}" alt="Bookmarklet" class="header-icon"> </div>
</a>
<a href="https://github.com/eneller/epub2go-web">
<img src="{% static 'github.svg' %}" alt="GitHub" class="header-icon">
</a></div>
<search> <search>
<form onsubmit="submitSearch(event)" class="searchbar"> <form onsubmit="submitSearch(event)" class="searchbar">
<input type="search" id="searchInput" placeholder="Suche nach Titel" minlength="3"> <input type="search" id="searchInput" placeholder="Suche nach Titel" minlength="3">
</form> </form>
</search> </search>
<p>Im Moment finden sich hier {{ book_count }} Bücher. </p> <small>Im Moment finden sich hier <a href="{{ allbooks_url }}">{{ book_count }} Bücher.</a> </small>
<a href="javascript:void(window.open('http://{{ http_host }}/?t=' + encodeURIComponent(window.location.toString())))" title="Als Lesezeichen speichern"><!--TODO fix domain part as variable-->
<img src="{% static 'bookmark.svg' %}" alt="Bookmarklet" class="header-icon">
</a>
<a href="https://github.com/eneller/epub2go-web">
<img src="{% static 'github.svg' %}" alt="GitHub" class="header-icon">
</a>
</header> </header>
<main> <main>
<!-- NOTE use dl here?--> <!-- NOTE use dl here?-->
@@ -30,13 +32,13 @@
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th>Title</th> <th>Titel</th>
<th>Author</th> <th>Autor</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for item in books %} {% for item in books %}
<tr> <tr class="table-entry">
<td> <td>
<a href= {{ item.url }} target="_blank" rel="noopener noreferrer" class="table-link"> <a href= {{ item.url }} target="_blank" rel="noopener noreferrer" class="table-link">
<img src="{% static 'open-link.svg' %}" alt="Open Link" class="inline-icon"> <img src="{% static 'open-link.svg' %}" alt="Open Link" class="inline-icon">

22
uv.lock generated
View File

@@ -201,6 +201,8 @@ dependencies = [
{ name = "celery" }, { name = "celery" },
{ name = "django" }, { name = "django" },
{ name = "epub2go" }, { name = "epub2go" },
{ name = "python-dotenv" },
{ name = "pywatchman" },
] ]
[package.metadata] [package.metadata]
@@ -208,6 +210,8 @@ requires-dist = [
{ name = "celery", specifier = ">=5.4.0" }, { name = "celery", specifier = ">=5.4.0" },
{ name = "django", specifier = ">=5.1.6" }, { name = "django", specifier = ">=5.1.6" },
{ name = "epub2go", git = "https://github.com/eneller/epub2go.py" }, { name = "epub2go", git = "https://github.com/eneller/epub2go.py" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "pywatchman", specifier = ">=2.0.0" },
] ]
[[package]] [[package]]
@@ -266,6 +270,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
] ]
[[package]]
name = "python-dotenv"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "pywatchman"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/39/fc10dd952ac72a3a293936cd66a4551fdeb9012d2db99234a376100641ce/pywatchman-2.0.0.tar.gz", hash = "sha256:25354d9e3647f94411a4c13e510c83a1ceecc17977b0525ba41b16e7019c7b0c", size = 40570 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/7f/5d68d803489770cffa5d2b44be99b978c866f8a4d8e835f9da850415ed8a/pywatchman-2.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:51c2b4c72bea6b9fd90caf20759f5bc47febf0fd27bf2f247b87c66e2f6bab02", size = 52557 },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.3" version = "2.32.3"