diff --git a/.gitea/ISSUE_TEMPLATE/feature.yml b/.gitea/ISSUE_TEMPLATE/feature.yml
new file mode 100644
index 0000000..a424190
--- /dev/null
+++ b/.gitea/ISSUE_TEMPLATE/feature.yml
@@ -0,0 +1,21 @@
+name: Feature request
+description: Suggest an idea for this project
+labels: kind/feature
+
+body:
+ - type: textarea
+ id: feature
+ attributes:
+ label: Describe the feature
+ description: |
+ A clear and concise description of what the feature is.
+ What is the problem it solves? What are you trying to accomplish?
+ Include screenshots if applicable.
+ validations:
+ required: true
+ - type: textarea
+ id: additional-info
+ attributes:
+ label: Additional information
+ description: |
+ Add any other context or screenshots about the feature request here.
\ No newline at end of file
diff --git a/src/app.py b/src/app.py
index 508d206..6d94e14 100644
--- a/src/app.py
+++ b/src/app.py
@@ -2,23 +2,42 @@ from quart import Quart, render_template, request, jsonify
import httpx
from anilistapi.queries.manga import REQUESTS_QUERY, TAGS_QUERY, GENRES_QUERY
from anilistapi.schemas.manga import Manga
+from komconfig import KomConfig
from komcache import KomCache
+from komgapi import komgapi as KOMGAPI
app = Quart(__name__)
cache = KomCache()
cache.create_table(
- "CREATE TABLE IF NOT EXISTS manga_requests (id INTEGER PRIMARY KEY, manga_id INTEGER, manga_title TEXT)"
+ "CREATE TABLE IF NOT EXISTS manga_requests (id INTEGER PRIMARY KEY, manga_id INTEGER, grabbed BOOLEAN DEFAULT 0)"
)
+settings = KomConfig()
-async def fetch_data(query):
- # Simulated API response
+komga = KOMGAPI(
+ url=settings.komga.url,
+ username=settings.komga.user,
+ password=settings.komga.password,
+)
+komga_books = komga.bookList()
+
+
+async def fetch_data(data):
async with httpx.AsyncClient() as client:
try:
+ variables = {"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)
response = await client.post(
- f"https://graphql.anilist.co",
- json={"query": REQUESTS_QUERY, "variables": {"search": query}},
+ "https://graphql.anilist.co",
+ json={
+ "query": REQUESTS_QUERY,
+ "variables": variables,
+ },
)
response.raise_for_status()
data = response.json()
@@ -30,7 +49,9 @@ async def fetch_data(query):
results.append(
{
"id": manga.id,
- "title": manga.title.romaji if manga.title else "Untitled",
+ "title": manga.title.english
+ if manga.title.english
+ else manga.title.romaji,
"image": manga.coverImage.get("large")
if manga.coverImage
else "https://demofree.sirv.com/nope-not-here.jpg",
@@ -46,8 +67,11 @@ async def fetch_data(query):
)
return results
+ except httpx.RequestError as e:
+ print(f"An error occurred while requesting data: {e}")
+ return []
except Exception as e:
- print(f"Error fetching data: {e}")
+ print(f"Unexpected error: {e}")
return []
@@ -86,24 +110,24 @@ async def get_tags():
return jsonify([])
-@app.route("/", methods=["GET", "POST"])
+@app.route("/search", methods=["POST"])
+async def search():
+ data = await request.get_json()
+ print(data)
+ results = await fetch_data(data)
+ if not results:
+ return jsonify({"error": "No results found"}), 404
+ return jsonify(results)
+
+
+@app.route("/", methods=["GET"])
async def index():
- query = None
- results = []
-
- if request.method == "POST":
- form = await request.form
- query = form.get("query")
- if query:
- results = await fetch_data(query)
-
- return await render_template("index.html", results=results)
+ return await render_template("index.html")
@app.route("/request", methods=["POST"])
async def log_request():
data = await request.get_json()
- print(data)
item = data.get("item")
if item:
asynccache = KomCache()
@@ -117,4 +141,4 @@ async def log_request():
if __name__ == "__main__":
- app.run(debug=True, host="0.0.0.0", port=5000)
+ app.run(debug=True, host="0.0.0.0", port=5001)
diff --git a/src/static/style.css b/src/static/style.css
index 4edc623..3422a10 100644
--- a/src/static/style.css
+++ b/src/static/style.css
@@ -119,4 +119,38 @@ body.nsfw-disabled .image-container.nsfw:hover img {
padding: 5px;
border: 1px solid #ccc;
border-radius: 4px;
+}
+
+.dropdown-content {
+ display: none;
+ position: absolute;
+ background-color: #f9f9f9;
+ min-width: 160px;
+ box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
+ z-index: 1;
+}
+
+.dropdown-content .dropdown-item {
+ padding: 8px 16px;
+ cursor: pointer;
+}
+
+.dropdown-content .dropdown-item:hover {
+ background-color: #f1f1f1;
+}
+
+.dropdown-content.show {
+ display: block;
+}
+
+.reset {
+ /* Implement design here */
+}
+
+.info {
+ /* Implement design here */
+}
+
+.request {
+ /* Implement design here */
}
\ No newline at end of file
diff --git a/src/templates/index.html b/src/templates/index.html
index 959c1d7..a544b30 100644
--- a/src/templates/index.html
+++ b/src/templates/index.html
@@ -4,6 +4,7 @@
Anime Search and Request Page
+
@@ -12,27 +13,34 @@
-
-
-
{% if results %}
-
-
-
{% for result in results %}
@@ -45,18 +53,20 @@
{% endif %}
-
{{ result.title }}
-
-
-
+
+
{% endfor %}
{% endif %}
+
+
+
+
@@ -90,61 +100,105 @@
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").textContent = data.description || "No description available.";
+ document.getElementById("modalDescription").innerHTML = data.description || "No description available.";
document.getElementById("infoModal").style.display = "block";
}
function closeModal() {
document.getElementById("infoModal").style.display = "none";
}
- // Blur effect for NSFW images
+
document.addEventListener("DOMContentLoaded", () => {
const checkbox = document.getElementById("toggleNSFW");
const body = document.body;
- // Initial state
body.classList.add("nsfw-disabled");
- // NSFW toggle
checkbox.addEventListener("change", () => {
const enabled = checkbox.checked;
body.classList.toggle("nsfw-disabled", !enabled);
body.classList.toggle("nsfw-enabled", enabled);
- // Reset all image blur states when toggling
document.querySelectorAll('.image-container.nsfw img').forEach(img => {
img.dataset.blurred = "true";
- img.style.filter = ""; // Remove inline filter style to respect CSS hover and CSS blur
+ img.style.filter = "";
});
});
- // Fetch genres and tags on page load
- fetchOptions("/api/genres", "genreSelect");
- fetchOptions("/api/tags", "tagSelect");
+ fetchOptions("/api/genres", "genre");
+ fetchOptions("/api/tags", "tag");
});
- // Mobile: click toggles blur when NSFW is disabled
- async function fetchOptions(url, selectId) {
+
+ async function fetchOptions(url, dropdownId) {
try {
const res = await fetch(url);
const data = await res.json();
- const select = document.getElementById(selectId);
+ const dropdown = document.getElementById(dropdownId + 'Dropdown');
data.forEach(item => {
- const option = document.createElement("option");
- option.value = item;
- option.textContent = item;
- select.appendChild(option);
+ const option = document.createElement("div");
+ option.className = "dropdown-item";
+ option.textContent = item.name || item;
+ option.onclick = () => addToInput(dropdownId, item.name || item);
+ dropdown.appendChild(option);
});
} catch (err) {
- console.error("Error fetching " + selectId, err);
+ console.error("Error fetching " + dropdownId, err);
}
}
+ function addToInput(inputId, value) {
+ const input = document.getElementById(inputId + 'Input');
+ const currentValues = input.value.split(',').map(v => v.trim()).filter(v => v);
+
+ if (!currentValues.includes(value)) {
+ currentValues.push(value);
+ input.value = currentValues.join(', ');
+ }
+ }
+
+ function filterSuggestions(inputId) {
+ const input = document.getElementById(inputId + 'Input');
+ const filter = input.value.toLowerCase();
+ const dropdown = document.getElementById(inputId + 'Dropdown');
+ const items = dropdown.getElementsByClassName('dropdown-item');
+
+ let hasVisibleItems = false;
+ Array.from(items).forEach(item => {
+ const text = item.textContent.toLowerCase();
+ const isVisible = text.includes(filter);
+ item.style.display = isVisible ? '' : 'none';
+ if (isVisible) hasVisibleItems = true;
+ });
+
+ dropdown.classList.toggle('show', hasVisibleItems);
+ }
+
+ function showDropdown(inputId) {
+ const dropdown = document.getElementById(inputId + 'Dropdown');
+ dropdown.classList.add('show');
+ }
+
+ document.addEventListener('click', (event) => {
+ if (!event.target.matches('.autocomplete input')) {
+ document.querySelectorAll('.dropdown-content').forEach(dropdown => {
+ dropdown.classList.remove('show');
+ });
+ }
+ });
+
+ function resetGenres() {
+ document.getElementById('genreInput').value = '';
+ }
+
+ function resetTags() {
+ document.getElementById('tagInput').value = '';
+ }
+
function performSearch() {
const searchTerm = document.getElementById("searchInput").value.trim();
-
- const selectedGenres = Array.from(document.getElementById("genreSelect").selectedOptions).map(opt => opt.value);
- const selectedTags = Array.from(document.getElementById("tagSelect").selectedOptions).map(opt => opt.value);
+ 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 query = {
query: searchTerm,
@@ -163,12 +217,53 @@
}
function displayResults(data) {
- // Replace this with your rendering logic
- console.log(data);
+ const resultsContainer = document.querySelector('.results');
+ resultsContainer.innerHTML = ''; // Clear previous results
+
+ data.forEach(result => {
+ const card = document.createElement('div');
+ card.className = 'card';
+
+ const imageContainer = document.createElement('div');
+ imageContainer.className = `image-container ${result.isAdult ? 'nsfw' : ''}`;
+
+ const img = document.createElement('img');
+ img.src = result.image || 'https://demofree.sirv.com/nope-not-here.jpg';
+ img.alt = 'Cover';
+ imageContainer.appendChild(img);
+
+ if (result.isAdult) {
+ const badge = document.createElement('div');
+ badge.className = 'adult-badge';
+ badge.textContent = '18+';
+ imageContainer.appendChild(badge);
+ }
+
+ const title = document.createElement('p');
+ title.textContent = result.title || 'Untitled';
+
+ const actions = document.createElement('div');
+ actions.className = 'actions';
+
+ const infoButton = document.createElement('button');
+ infoButton.textContent = 'Info';
+ infoButton.onclick = () => showInfo(result);
+
+ const requestButton = document.createElement('button');
+ requestButton.textContent = 'Request';
+ requestButton.onclick = () => sendRequest(result.id);
+
+ actions.appendChild(infoButton);
+ actions.appendChild(requestButton);
+
+ card.appendChild(imageContainer);
+ card.appendChild(title);
+ card.appendChild(actions);
+
+ resultsContainer.appendChild(card);
+ });
}
-
-