dev #17

Merged
WorldTeacher merged 8 commits from dev into main 2025-05-23 14:47:45 +01:00
11 changed files with 776 additions and 72 deletions

21
.bumpversion.toml Normal file
View File

@@ -0,0 +1,21 @@
[tool.bumpversion]
current_version = "0.1.0"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}"
replace = "{new_version}"
regex = false
ignore_missing_version = false
ignore_missing_files = false
tag = false
sign_tags = false
tag_name = "v{new_version}"
tag_message = "Bump version: {current_version} → {new_version}"
allow_dirty = false
commit = false
message = "Bump version: {current_version} → {new_version}"
moveable_tags = []
commit_args = ""
setup_hooks = []
pre_commit_hooks = []
post_commit_hooks = []

View File

@@ -0,0 +1,57 @@
name: Bug Report
description: Report a bug in this project
title: "[Bug]: "
labels:
- kind/bug
- triage/needs triage
body:
- type: textarea
id: bug
attributes:
label: Describe the bug
description: |
A clear and concise description of what the bug is.
What did you expect to happen? What happened instead?
Include screenshots if applicable.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: |
A clear and concise description of how to reproduce the bug.
Include steps, code snippets, or screenshots if applicable.
validations:
required: true
- type: textarea
id: additional-info
attributes:
label: Additional information
description: |
Add any other context or screenshots about the bug here.
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- type: dropdown
id: operating-systems
attributes:
label: What operating systems are you seeing the problem on?
multiple: true
options:
- Windows
- macOS
- Linux
- iOS
- Android

View File

@@ -0,0 +1,120 @@
on:
workflow_dispatch:
inputs:
release_notes:
description: Release notes (use \n for newlines)
type: string
required: false
github_release:
description: 'Create Gitea Release'
default: true
type: boolean
docker_release:
description: 'Push Docker images'
default: true
type: boolean
bump:
description: 'Bump type'
required: true
default: 'patch'
type: choice
options:
- 'major'
- 'minor'
- 'patch'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.TOKEN }}
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Set up Python
run: uv python install
with:
python-version-file: "pyproject.toml"
- name: Install the project
run: uv sync --locked --all-extras --dev
- name: Set Git identity
run: |
git config user.name "Gitea CI"
git config user.email "ci@git.theprivateserver.de"
- name: Bump version
id: bump
run: |
uv tool install bump-my-version
uv tool run bump-my-version bump ${{ github.event.inputs.bump }}
# echo the version to github env, the version is shown by using uv tool run bump-my-version show current_version
echo "VERSION<<EOF" >> $GITHUB_ENV
echo "$(uv tool run bump-my-version show current_version)" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Check version
run: echo ${{ env.VERSION }}
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
- name: Add release notes to environment
id: add_release_notes
run: |
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "${{ github.event.inputs.release_notes }}" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Build and store Docker image
if: ${{ github.event.inputs.docker_release == 'true' }}
env:
TAG: ${{ github.sha }}
run: |
REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
docker buildx build \
--platform linux/amd64 \
--tag ${{ secrets.REGISTRY }}/${REPO_NAME}:latest \
--tag ${{ secrets.REGISTRY }}/${REPO_NAME}:${TAG} \
--push .
- name: Generate changelog
id: changelog
uses: metcalfc/changelog-generator@v4.6.2
with:
token: ${{ secrets.TOKEN }}
- name: Get the changelog
run: |
cat << "EOF"
${{ steps.changelog.outputs.changelog }}
EOF
- name: Create release
id: create_release
if: ${{ github.event.inputs.github_release == 'true' }}
uses: softprops/action-gh-release@master
with:
tag_name: ${{ env.VERSION }}
release_name: Release ${{ env.VERSION }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
make_latest: true
env:
GITHUB_TOKEN: ${{ secrets.TOKEN }}

1
.gitignore vendored
View File

@@ -227,7 +227,6 @@ docs/
config.yaml
**/tempCodeRunnerFile.py
uv.lock
.history
.venv
venv

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
# Use the official Python image as the base image
FROM python:3.11-slim
# Set the working directory in the container
WORKDIR /app
# Copy the application files into the container
COPY . /app
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Expose the port the app runs on
EXPOSE 5001
# Set the default command to run the application
CMD ["python", "src/app.py"]

View File

@@ -5,15 +5,22 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"flask>=3.1.0",
"anilistapi",
"flask>=3.1.1",
"httpx>=0.28.1",
"httpx-retries>=0.3.2",
"jinja2>=3.1.6",
"komcache",
"komconfig",
"komgapi",
"komsuite-nyaapy",
"limit>=0.2.3",
"quart>=0.20.0",
]
[tool.uv.sources]
anilistapi = { workspace = true }
komconfig = { workspace = true }
komcache = { workspace = true }
komsuite-nyaapy = { workspace = true }
komgapi = { workspace = true }
komcache = { workspace = true }
komconfig = { workspace = true }
anilistapi = { workspace = true }

88
requirements.txt Normal file
View File

@@ -0,0 +1,88 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml -o requirements.txt
-e file:///home/alexander/GitHub/KomSuite/packages/KomConfig
# via komgapi
-e file:///home/alexander/GitHub/KomSuite/packages/komgAPI
# via kompage (pyproject.toml)
aiofiles==24.1.0
# via quart
antlr4-python3-runtime==4.9.3
# via omegaconf
anyio==4.9.0
# via httpx
blinker==1.9.0
# via
# flask
# quart
certifi==2025.4.26
# via
# httpcore
# httpx
click==8.2.0
# via
# flask
# quart
flask==3.1.1
# via
# kompage (pyproject.toml)
# quart
h11==0.16.0
# via
# httpcore
# hypercorn
# wsproto
h2==4.2.0
# via hypercorn
hpack==4.1.0
# via h2
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via
# kompage (pyproject.toml)
# komgapi
hypercorn==0.17.3
# via quart
hyperframe==6.1.0
# via h2
idna==3.10
# via
# anyio
# httpx
itsdangerous==2.2.0
# via
# flask
# quart
jinja2==3.1.6
# via
# kompage (pyproject.toml)
# flask
# quart
limit==0.2.3
# via kompage (pyproject.toml)
loguru==0.7.3
# via komgapi
markupsafe==3.0.2
# via
# flask
# jinja2
# quart
# werkzeug
omegaconf==2.3.0
# via komconfig
priority==2.0.0
# via hypercorn
pyyaml==6.0.2
# via omegaconf
quart==0.20.0
# via kompage (pyproject.toml)
sniffio==1.3.1
# via anyio
typing-extensions==4.13.2
# via komgapi
werkzeug==3.1.3
# via
# flask
# quart
wsproto==1.2.0
# via hypercorn

View File

@@ -1,21 +1,31 @@
from quart import Quart, render_template, request, jsonify
import httpx
from anilistapi.queries.manga import REQUESTS_QUERY, TAGS_QUERY, GENRES_QUERY
from anilistapi.queries.manga import (
REQUESTS_QUERY,
TAGS_QUERY,
GENRES_QUERY,
REQUESTED_QUERY,
)
from anilistapi.schemas.manga import Manga
from komconfig import KomConfig
from komcache import KomCache
from komgapi import komgapi as KOMGAPI
from typing import Any, Dict, List
from limit import limit
import loguru
import sys
from komsuite_nyaapy import Nyaa
log = loguru.logger
log.remove()
log.add("application.log", rotation="1 week", retention="1 month")
log.add(sys.stdout)
app = Quart(__name__)
cache = KomCache()
cache.create_table(
"CREATE TABLE IF NOT EXISTS manga_requests (id INTEGER PRIMARY KEY, manga_id INTEGER, grabbed BOOLEAN DEFAULT 0)"
)
cache.create_table(
"CREATE TABLE IF NOT EXISTS manga_titles (id INTEGER PRIMARY KEY, anilist_id INTEGER DEFAULT 0, komga_title UNIQUE)"
)
settings = KomConfig()
@@ -25,35 +35,32 @@ komga = KOMGAPI(
password=settings.komga.password,
)
komga_series = komga.seriesList()
# store the entries in the database table manga_titles, komga_series is a list of strings of the series names
for series in komga_series:
# check if the series is already in the database
existing_series = cache.fetch_one(
query="SELECT komga_title FROM manga_titles WHERE komga_title = ?",
args=(series,),
)
if existing_series:
# series already exists, skip
continue
else:
cache.insert(
# insert into if not in database
query="INSERT OR IGNORE INTO manga_titles (komga_title) VALUES (?)",
args=(series,),
)
komga_series = [series.lower() for series in komga_series]
# Update type annotations for fetch_data
async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]:
@limit(90, 60)
async def fetch_data(
inputdata: Dict[str, Any], check_downloads: bool = False
) -> List[Dict[str, Any]]:
log.error(f"fetch_data called with data: {inputdata}")
async with httpx.AsyncClient() as client:
try:
variables: Dict[str, Any] = {"search": data["query"]}
if len(data["genres"]) > 0:
variables["genres"] = data["genres"]
if len(data["tags"]) > 0:
variables["tags"] = data["tags"]
print(data["query"], variables)
# Log the incoming data for debugging
# Ensure 'query' key exists in the data dictionary
if "query" not in inputdata or not inputdata["query"]:
raise ValueError("Missing or empty 'query' key in data")
variables: Dict[str, Any] = {"search": inputdata["query"]}
if inputdata.get("genres"):
variables["genres"] = inputdata["genres"]
if inputdata.get("tags"):
variables["tags"] = inputdata["tags"]
if inputdata.get("type"):
if inputdata["type"] in ["MANGA", "NOVEL"]:
variables["format"] = inputdata["type"]
log.debug(f"GraphQL variables: {variables}")
response = await client.post(
"https://graphql.anilist.co",
json={
@@ -62,7 +69,9 @@ async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]:
},
)
response.raise_for_status()
data = response.json()
log.debug(f"GraphQL response: {data}")
results: List[Dict[str, Any]] = []
for item in data.get("data", {}).get("Page", {}).get("media", []):
@@ -70,7 +79,11 @@ async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]:
in_komga = komga.getSeries(
manga.title.english if manga.title.english else manga.title.romaji
)
print(in_komga, manga.title.english)
requested = cache.fetch_one(
query="SELECT manga_id, grabbed FROM manga_requests WHERE manga_id = ?",
args=(manga.id,),
)
komga_request = bool(requested)
results.append(
{
"id": manga.id,
@@ -81,7 +94,7 @@ async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]:
if manga.coverImage
else "https://demofree.sirv.com/nope-not-here.jpg",
"status": manga.status,
"type": manga.type,
"type": manga.format,
"genres": manga.genres or [],
"tags": [tag.name for tag in (manga.tags or [])],
"description": manga.description.replace("<br>", "\n")
@@ -89,24 +102,107 @@ async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]:
else "No description available",
"isAdult": manga.isAdult,
"in_komga": in_komga,
"requested": komga_request,
"siteUrl": manga.siteUrl,
}
)
if check_downloads:
for result in results:
downloads = Nyaa().search(result.get("title"), 3, 1)
else:
downloads = []
result["download"] = len(downloads) if downloads else 0
log.debug(f"Fetched {len(results)} results for query: {inputdata['query']}")
log.error(f"Results: {results}")
return results
except ValueError as ve:
log.error(f"ValueError in fetch_data: {ve}")
return []
except httpx.RequestError as e:
print(f"An error occurred while requesting data: {e}")
log.error(f"HTTP request error in fetch_data: {e}")
return []
except Exception as e:
print(f"Unexpected error: {e}")
log.error(f"Unexpected error in fetch_data: {e}")
return []
@limit(90, 60)
async def fetch_requested_data(data: list[int]) -> List[Dict[str, Any]]:
requested_data: list[dict[str, Any]] = []
for manga_id in data:
async with httpx.AsyncClient() as client:
try:
variables: Dict[str, Any] = {
"search": manga_id,
"type": "MANGA",
} # TODO: replace the type once the UI has been updated
# print(data, variables)
response = await client.post(
"https://graphql.anilist.co",
json={
"query": REQUESTED_QUERY,
"variables": variables,
},
)
response.raise_for_status()
data = response.json()
# print(data)
entry = data.get("data", {}).get("Media", {})
if entry:
manga = Manga(**entry)
in_komga = komga.getSeries(
manga.title.english
if manga.title.english
else manga.title.romaji
)
requested = cache.fetch_one(
query="SELECT grabbed FROM manga_requests WHERE manga_id = ?",
args=(manga.id,),
)
komga_request = bool(requested)
requested_data.append(
{
"id": manga.id,
"title": manga.title.english
if manga.title.english
else manga.title.romaji,
"image": manga.coverImage.get("large")
if manga.coverImage
else None,
"status": manga.status,
"type": manga.format,
"genres": manga.genres or [],
"tags": [tag.name for tag in (manga.tags or [])],
"description": manga.description.replace("<br>", "\n")
if manga.description
else None,
"isAdult": manga.isAdult,
"in_komga": bool(in_komga),
"requested": bool(komga_request),
}
)
log.debug(
requested_data,
)
except httpx.RequestError as e:
print(f"An error occurred while requesting data: {e}")
requested_data.append({"id": manga_id})
except Exception as e:
print(f"Unexpected error: {e}")
requested_data.append({"id": manga_id})
return requested_data
@app.route("/api/genres")
async def get_genres():
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"https://graphql.anilist.co",
"https://graphql.anilist.co",
json={"query": GENRES_QUERY},
)
response.raise_for_status()
@@ -124,7 +220,7 @@ async def get_tags():
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"https://graphql.anilist.co",
"https://graphql.anilist.co",
json={"query": TAGS_QUERY},
)
response.raise_for_status()
@@ -146,6 +242,43 @@ async def search():
return jsonify(results)
@app.route("/api/search", methods=["GET"])
async def api_search():
"""
API endpoint for searching manga.
Parameters:
- q: Query string to search for
Returns:
- JSON array of matching manga entries
"""
try:
query = request.args.get("q")
if not query:
return jsonify({"error": "Missing required parameter: q"}), 400
# Build search parameters
search_data: Dict[str, Any] = {"query": query}
# Add optional filters if provided
log.info(f"API search request: {search_data}")
# Fetch results using existing fetch_data function
results = await fetch_data(search_data, check_downloads=False)
if not results:
return jsonify({"results": [], "count": 0}), 404
return jsonify({"results": results, "count": len(results)})
except Exception as e:
log.error(f"Error in API search: {e}")
return jsonify({"error": "An unexpected error occurred"}), 500
@app.route("/", methods=["GET"])
async def index():
return await render_template("index.html", komga_series=komga_series)
@@ -154,17 +287,34 @@ async def index():
@app.route("/request", methods=["POST"])
async def log_request():
data = await request.get_json()
item = data.get("title")
item = data.get("item")
if item:
asynccache = KomCache()
manga_title = data.get("title")
asynccache.insert(
"INSERT INTO manga_requests (manga_id, manga_title) VALUES (?, ?)",
(item, manga_title),
"INSERT INTO manga_requests (manga_id) VALUES (?)",
(item,),
)
return jsonify({"status": "success"})
return jsonify({"status": "failed"}), 400
@app.route("/requests", methods=["GET"])
async def requests_page():
requests = (
cache.fetch_all(query="SELECT manga_id, grabbed FROM manga_requests") or []
)
entries: List[Dict[str, Any]] = []
req_ids = [req[0] for req in requests]
if req_ids:
entries = await fetch_requested_data(req_ids)
else:
entries = []
data = []
for entry in entries:
data.append(dict(entry))
return await render_template("requests.html", requests=data)
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5001)

View File

@@ -1,8 +1,9 @@
/* other styles are in magic.css */
/* ========== GRID LAYOUT ========== */
.results {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1em;
margin-top: 20px;
}
@@ -20,11 +21,16 @@
border: 1px solid #ccc;
background: #fafafa;
text-align: center;
transition: box-shadow 0.2s;
transition: box-shadow 0.2s transform 0.2s;
border-radius: 10px;
}
.card:hover {
box-shadow: 0 0 10px #aaa;
transform: scale(1.1);
/* pop out by 10% */
z-index: 2;
/* ensure it appears above others */
}
.card img {
@@ -39,6 +45,32 @@
justify-content: space-around;
}
.actions .info {
background-color: #007bff;
color: white;
border: none;
padding: 0.5em 1em;
cursor: pointer;
border-radius: 5px;
margin-right: 0.5em;
}
.actions .info:hover {
background-color: #0056b3;
}
.actions .request {
background-color: #28a745;
color: white;
border: none;
padding: 0.5em 1em;
cursor: pointer;
border-radius: 5px;
}
.actions .request:hover {
background-color: #1e7e34;
}
/* ========== MODAL ========== */
.modal {
position: fixed;
@@ -84,12 +116,15 @@
/* ========== BLUR CONTROL VIA BODY CLASS ========== */
body.nsfw-disabled .image-container.nsfw img {
filter: blur(8px);
filter: grayscale(100%) blur(5px) !important;
/* Ensure the blur is applied */
transition: filter 0.3s ease;
/* Add smooth transition */
}
body.nsfw-disabled .image-container.nsfw:hover img {
filter: none;
/* Remove blur on hover, set as important */
filter: none !important;
/* Remove blur on hover */
}
@@ -145,21 +180,54 @@ body.nsfw-disabled .image-container.nsfw:hover img {
.reset {
/* Implement design here */
background-color: #f44336;
/* round the edges of the button */
border-radius: 9px;
}
.selectors {
display: flex;
gap: 10px;
margin-bottom: 8px;
margin-top: 5px;
flex-wrap: wrap;
}
.info {
/* Implement design here */
}
.request {
/* Implement design here */
}
/* ========== KOMGA SPECIFIC STYLES ========== */
.card.komga {
border: 3px solid green;
box-sizing: border-box;
/* disable the request button for entries in Komga */
/* only allow the info button to be clicked */
opacity: 0.3;
}
.card.requested {
border: 3px solid orange;
box-sizing: border-box;
/* disable the request button for entries in Komga */
/* only allow the info button to be clicked */
opacity: 0.3;
}
.card.manga.requested .request {
pointer-events: none;
opacity: 0.5;
}
.card.komga .request {
pointer-events: none;
opacity: 0.5;
}
.card.komga:hover,
.card.requested:hover {
transform: none !important;
box-shadow: none !important;
z-index: auto !important;
}

View File

@@ -14,6 +14,7 @@
<input type="text" id="searchInput" placeholder="Search..." />
<div class="selectors">
<div class="autocomplete">
<input type="text" id="genreInput" placeholder="Select genres..." readonly
oninput="filterSuggestions('genre')" onclick="showDropdown('genre')">
<button onclick="resetGenres()" class="reset">Reset</button>
@@ -30,22 +31,39 @@
<!-- Tag suggestions will be dynamically populated here -->
</div>
</div>
<div class="type-select">
<label for="typeSelect">Type:</label>
<select id="typeSelect">
<option value="ALL">All</option>
<option value="MANGA">Manga</option>
<option value="NOVEL">Novel</option>
</select>
</div>
</div>
<button onclick="performSearch()">Search</button>
<div style="margin-bottom: 1em;">
<label>
<input type="checkbox" id="toggleNSFW" />
Show NSFW content
Unblur NSFW images
</label>
<!-- if typeselect is manga or all, show, else hide -->
<label style="margin-left: 1em;">
<input type="checkbox" id="toggleNovels" checked />
Show Novels
</label>
</div>
<button class="request-page" onclick="window.location.href='/requests'">View Requests</button>
</div>
{% if results %}
<div class="results">
{% if results %}
{% for result in results %}
<div class="card {% if result.in_komga %}komga{% endif %}">
<div class="card {{ result.type | lower }} {% if result.in_komga %}komga{% endif %}">
<div class="image-container {{ 'nsfw' if result.isAdult else '' }}">
<img src="{{ result.image }}" alt="Cover">
@@ -56,20 +74,19 @@
<p>{{ result.title }}</p>
<div class="actions">
<button onclick="showInfo({{ result | tojson | safe }})" , class="info">Info</button>
<button class="info" onclick="showInfo({{ result | tojson | safe }})">Info</button>
<!-- if entry has in_komga == true, do not show the request button, else show it -->
{% if not result.in_komga %}
<button onclick="sendRequest('{{ result.id }}')" class="request">Request</button>
<button class="request" onclick="sendRequest('{{ result.id }}')">Request</button>
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% endif %}
<div class="results">
<!-- Results will be dynamically populated here -->
</div>
<!-- Info Modal -->
<div id="infoModal" class="modal" style="display:none;">
@@ -86,6 +103,11 @@
</div>
</div>
<div id="loading" style="display: none; text-align: center; margin-top: 1em;">
<p>Loading...</p>
</div>
<script>
function sendRequest(item) {
fetch("/request", {
@@ -111,15 +133,23 @@
function closeModal() {
document.getElementById("infoModal").style.display = "none";
}
// Close when clicking outside modal content
window.onclick = function (event) {
const modal = document.getElementById("infoModal");
if (event.target === modal) {
closeModal();
}
}
document.addEventListener("DOMContentLoaded", () => {
const checkbox = document.getElementById("toggleNSFW");
const nsfwCheckbox = document.getElementById("toggleNSFW");
const novelsCheckbox = document.getElementById("toggleNovels");
const body = document.body;
body.classList.add("nsfw-disabled");
checkbox.addEventListener("change", () => {
const enabled = checkbox.checked;
nsfwCheckbox.addEventListener("change", () => {
const enabled = nsfwCheckbox.checked;
body.classList.toggle("nsfw-disabled", !enabled);
body.classList.toggle("nsfw-enabled", enabled);
@@ -129,8 +159,24 @@
});
});
novelsCheckbox.addEventListener("change", () => {
const enabled = novelsCheckbox.checked;
document.querySelectorAll('.card.novel').forEach(card => {
card.style.display = enabled ? '' : 'none';
});
});
fetchOptions("/api/genres", "genre");
fetchOptions("/api/tags", "tag");
// Add classes to buttons dynamically
document.querySelectorAll(".actions button").forEach(button => {
if (button.textContent.trim() === "Info") {
button.classList.add("info");
} else if (button.textContent.trim() === "Request") {
button.classList.add("request");
}
});
});
@@ -221,21 +267,33 @@
const searchTerm = document.getElementById("searchInput").value.trim();
const selectedGenres = document.getElementById("genreInput").value.split(',').map(v => v.trim()).filter(v => v);
const selectedTags = document.getElementById("tagInput").value.split(',').map(v => v.trim()).filter(v => v);
const selectedType = document.getElementById("typeSelect").value;
const query = {
query: searchTerm,
genres: selectedGenres,
tags: selectedTags
tags: selectedTags,
type: selectedType // Include the selected type in the query
};
const loadingElement = document.getElementById("loading");
loadingElement.style.display = "block"; // Show loading animation
fetch("/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(query)
})
.then(res => res.json())
.then(data => displayResults(data))
.catch(err => console.error("Search failed", err));
.then((data) => {
displayResults(data);
loadingElement.style.display = "none"; // Hide loading animation
})
.catch((err) => {
console.error("Search failed", err);
loadingElement.style.display = "none"; // Hide loading animation
});
}
function displayResults(data) {
@@ -245,6 +303,8 @@
data.forEach(result => {
const card = document.createElement('div');
card.className = `card ${result.in_komga ? 'komga' : ''}`;
card.className += ` ${result.type.toLowerCase()}`;
// card.className += ` ${result.requested ? 'requested' : ''}`;
const imageContainer = document.createElement('div');
imageContainer.className = `image-container ${result.isAdult ? 'nsfw' : ''}`;
@@ -269,12 +329,22 @@
const infoButton = document.createElement('button');
infoButton.textContent = 'Info';
infoButton.className = 'info';
infoButton.onclick = () => showInfo(result);
const requestButton = document.createElement('button');
requestButton.textContent = 'Request';
requestButton.className = 'request';
requestButton.onclick = () => sendRequest(result.id);
// Disable request button if the result is in Komga
if (result.in_komga) {
requestButton.disabled = true;
}
if (result.requested) {
requestButton.disabled = true;
}
actions.appendChild(infoButton);
actions.appendChild(requestButton);

107
src/templates/requests.html Normal file
View File

@@ -0,0 +1,107 @@
<!doctype html>
<html>
<head>
<title>Requested Manga</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<h1>Requested Manga</h1>
<div style="margin-bottom: 1em; text-align: center;">
<button class="refresh" onclick="window.location.reload()">Refresh</button>
<button class="index" onclick="window.location.href='/'">Back to Index</button>
</div>
<div class="results">
{% if requests %}
{% for request in requests %}
<div class="card {{ request.type | lower }} {% if request.in_komga %}komga{% else %}requested{% endif %}"
data-info="{{ request | tojson }}">
<div class="image-container {{ 'nsfw' if request.isAdult else '' }}">
<img src="{{ request.image }}" alt="Cover">
{% if request.isAdult %}
<div class="adult-badge">18+</div>
{% endif %}
</div>
<p>{{ request.title }}</p>
<div class="actions">
<button class="info">Info</button>
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% if not requests %}
<p>No requests found.</p>
{% endif %}
<!-- Info Modal -->
<div id="infoModal" class="modal" style="display:none;">
<div class="modal-content">
<span class="close" onclick="closeModal()">&times;</span>
<h2 id="modalTitle"></h2>
<p><strong>Status:</strong> <span id="modalStatus"></span></p>
<p><strong>Type:</strong> <span id="modalType"></span></p>
<p><strong>Genres:</strong> <span id="modalGenres"></span></p>
<p><strong>Tags:</strong> <span id="modalTags"></span></p>
<p><strong>Adult Content:</strong> <span id="modalAdult"></span></p>
<p><strong>Description:</strong></p>
<p id="modalDescription"></p>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
// Add click event listeners to all info buttons
document.querySelectorAll('.card .info').forEach(button => {
button.addEventListener('click', (e) => {
// Get the parent card element
const card = e.target.closest('.card');
// Get the data from the data-info attribute
try {
const data = JSON.parse(card.dataset.info);
console.log("Info button clicked, data:", data);
showInfo(data);
} catch (error) {
console.error("Error parsing data:", error);
console.error("Raw data:", card.dataset.info);
}
});
});
});
function showInfo(data) {
console.log("showInfo data:", data);
try {
const modal = document.getElementById("infoModal");
document.getElementById("modalTitle").textContent = data.title || "Unknown";
document.getElementById("modalStatus").textContent = data.status || "Unknown";
document.getElementById("modalType").textContent = data.type || "Unknown";
document.getElementById("modalGenres").textContent = (data.genres || []).join(", ");
document.getElementById("modalTags").textContent = (data.tags || []).join(", ");
document.getElementById("modalAdult").textContent = data.isAdult ? "Yes" : "No";
document.getElementById("modalDescription").innerHTML = data.description || "No description available.";
modal.style.display = "block";
} catch (error) {
console.error("Error displaying info:", error);
}
}
function closeModal() {
document.getElementById("infoModal").style.display = "none";
}
// Close when clicking outside modal content
window.onclick = function (event) {
const modal = document.getElementById("infoModal");
if (event.target === modal) {
closeModal();
}
}
</script>
</body>
</html>