Merge pull request #7 from richardnixondev/refactor/htmx-all-config-forms
refactor(frontend): migrate remaining 13 config forms to HTMX
This commit is contained in:
commit
3802707c3d
7 changed files with 182 additions and 335 deletions
|
|
@ -7,7 +7,7 @@ packages = ["src"]
|
|||
|
||||
[project]
|
||||
name = "reddit-media-collector"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
description = "Self-hosted media collector for Reddit with Immich integration"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
|
@ -19,6 +19,23 @@ def _wants_html(request: Request) -> bool:
|
|||
return "text/html" in request.headers.get("accept", "")
|
||||
|
||||
|
||||
def _empty_html_or_json(request: Request, payload: dict) -> Response | dict:
|
||||
"""DELETE response: empty HTML body for HTMX (swap removes the element);
|
||||
JSON for legacy callers."""
|
||||
if _wants_html(request):
|
||||
return Response(status_code=200)
|
||||
return payload
|
||||
|
||||
|
||||
def _tag_fragment(request: Request, prefix: str, value: str, delete_url: str, confirm: str):
|
||||
"""Render the generic blacklist tag chip."""
|
||||
return _templates.TemplateResponse(
|
||||
request,
|
||||
"partials/_tag.html",
|
||||
{"prefix": prefix, "value": value, "delete_url": delete_url, "confirm": confirm},
|
||||
)
|
||||
|
||||
|
||||
# 60 mutations/minute per (IP, path) — protects config.yaml from runaway
|
||||
# loops or naive scripted spam. Shared dep instance; rate_limit's bucket key
|
||||
# already includes request.url.path, so each route gets its own counter.
|
||||
|
|
@ -78,13 +95,13 @@ async def add_subreddit(data: SubredditCreate, request: Request):
|
|||
|
||||
|
||||
@router.delete("/api/subreddits/{name}", dependencies=[_mut])
|
||||
async def delete_subreddit(name: str):
|
||||
async def delete_subreddit(name: str, request: Request):
|
||||
"""Remove a subreddit."""
|
||||
success = config_manager.remove_subreddit(name)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Subreddit not found")
|
||||
|
||||
return {"message": f"Subreddit '{name}' removed successfully"}
|
||||
return _empty_html_or_json(request, {"message": f"Subreddit '{name}' removed successfully"})
|
||||
|
||||
|
||||
@router.get("/api/users")
|
||||
|
|
@ -94,7 +111,7 @@ async def list_users():
|
|||
|
||||
|
||||
@router.post("/api/users", dependencies=[_mut])
|
||||
async def add_user(data: UserCreate):
|
||||
async def add_user(data: UserCreate, request: Request):
|
||||
"""Add a new user."""
|
||||
if not data.name:
|
||||
raise HTTPException(status_code=400, detail="Name is required")
|
||||
|
|
@ -103,17 +120,24 @@ async def add_user(data: UserCreate):
|
|||
if not success:
|
||||
raise HTTPException(status_code=409, detail="User already exists")
|
||||
|
||||
if _wants_html(request):
|
||||
return _templates.TemplateResponse(
|
||||
request,
|
||||
"partials/_item_user.html",
|
||||
{"user": {"name": data.name, "limit": data.limit}},
|
||||
)
|
||||
|
||||
return {"message": f"User '{data.name}' added successfully"}
|
||||
|
||||
|
||||
@router.delete("/api/users/{name}", dependencies=[_mut])
|
||||
async def delete_user(name: str):
|
||||
async def delete_user(name: str, request: Request):
|
||||
"""Remove a user."""
|
||||
success = config_manager.remove_user(name)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
return {"message": f"User '{name}' removed successfully"}
|
||||
return _empty_html_or_json(request, {"message": f"User '{name}' removed successfully"})
|
||||
|
||||
|
||||
# Blacklist endpoints
|
||||
|
|
@ -126,7 +150,7 @@ async def get_blacklist():
|
|||
|
||||
|
||||
@router.post("/api/blacklist/authors", dependencies=[_mut])
|
||||
async def add_blacklist_author(data: BlacklistItem):
|
||||
async def add_blacklist_author(data: BlacklistItem, request: Request):
|
||||
"""Add an author to the blacklist."""
|
||||
if not data.value:
|
||||
raise HTTPException(status_code=400, detail="Value is required")
|
||||
|
|
@ -135,21 +159,30 @@ async def add_blacklist_author(data: BlacklistItem):
|
|||
if not success:
|
||||
raise HTTPException(status_code=409, detail="Author already blacklisted")
|
||||
|
||||
if _wants_html(request):
|
||||
return _tag_fragment(
|
||||
request,
|
||||
prefix="u/",
|
||||
value=data.value,
|
||||
delete_url=f"/api/blacklist/authors/{data.value}",
|
||||
confirm=f"Remover u/{data.value} da blacklist?",
|
||||
)
|
||||
|
||||
return {"message": f"Author '{data.value}' added to blacklist"}
|
||||
|
||||
|
||||
@router.delete("/api/blacklist/authors/{author}", dependencies=[_mut])
|
||||
async def remove_blacklist_author(author: str):
|
||||
async def remove_blacklist_author(author: str, request: Request):
|
||||
"""Remove an author from the blacklist."""
|
||||
success = config_manager.remove_blacklist_author(author)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Author not found in blacklist")
|
||||
|
||||
return {"message": f"Author '{author}' removed from blacklist"}
|
||||
return _empty_html_or_json(request, {"message": f"Author '{author}' removed from blacklist"})
|
||||
|
||||
|
||||
@router.post("/api/blacklist/subreddits", dependencies=[_mut])
|
||||
async def add_blacklist_subreddit(data: BlacklistItem):
|
||||
async def add_blacklist_subreddit(data: BlacklistItem, request: Request):
|
||||
"""Add a subreddit to the blacklist."""
|
||||
if not data.value:
|
||||
raise HTTPException(status_code=400, detail="Value is required")
|
||||
|
|
@ -158,21 +191,30 @@ async def add_blacklist_subreddit(data: BlacklistItem):
|
|||
if not success:
|
||||
raise HTTPException(status_code=409, detail="Subreddit already blacklisted")
|
||||
|
||||
if _wants_html(request):
|
||||
return _tag_fragment(
|
||||
request,
|
||||
prefix="r/",
|
||||
value=data.value,
|
||||
delete_url=f"/api/blacklist/subreddits/{data.value}",
|
||||
confirm=f"Remover r/{data.value} da blacklist?",
|
||||
)
|
||||
|
||||
return {"message": f"Subreddit '{data.value}' added to blacklist"}
|
||||
|
||||
|
||||
@router.delete("/api/blacklist/subreddits/{subreddit}", dependencies=[_mut])
|
||||
async def remove_blacklist_subreddit(subreddit: str):
|
||||
async def remove_blacklist_subreddit(subreddit: str, request: Request):
|
||||
"""Remove a subreddit from the blacklist."""
|
||||
success = config_manager.remove_blacklist_subreddit(subreddit)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Subreddit not found in blacklist")
|
||||
|
||||
return {"message": f"Subreddit '{subreddit}' removed from blacklist"}
|
||||
return _empty_html_or_json(request, {"message": f"Subreddit '{subreddit}' removed from blacklist"})
|
||||
|
||||
|
||||
@router.post("/api/blacklist/keywords", dependencies=[_mut])
|
||||
async def add_blacklist_keyword(data: BlacklistItem):
|
||||
async def add_blacklist_keyword(data: BlacklistItem, request: Request):
|
||||
"""Add a title keyword to the blacklist."""
|
||||
if not data.value:
|
||||
raise HTTPException(status_code=400, detail="Value is required")
|
||||
|
|
@ -181,21 +223,30 @@ async def add_blacklist_keyword(data: BlacklistItem):
|
|||
if not success:
|
||||
raise HTTPException(status_code=409, detail="Keyword already blacklisted")
|
||||
|
||||
if _wants_html(request):
|
||||
return _tag_fragment(
|
||||
request,
|
||||
prefix="",
|
||||
value=data.value,
|
||||
delete_url=f"/api/blacklist/keywords/{data.value}",
|
||||
confirm=f'Remover "{data.value}" da blacklist?',
|
||||
)
|
||||
|
||||
return {"message": f"Keyword '{data.value}' added to blacklist"}
|
||||
|
||||
|
||||
@router.delete("/api/blacklist/keywords/{keyword:path}", dependencies=[_mut])
|
||||
async def remove_blacklist_keyword(keyword: str):
|
||||
async def remove_blacklist_keyword(keyword: str, request: Request):
|
||||
"""Remove a title keyword from the blacklist."""
|
||||
success = config_manager.remove_blacklist_keyword(keyword)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Keyword not found in blacklist")
|
||||
|
||||
return {"message": f"Keyword '{keyword}' removed from blacklist"}
|
||||
return _empty_html_or_json(request, {"message": f"Keyword '{keyword}' removed from blacklist"})
|
||||
|
||||
|
||||
@router.post("/api/blacklist/domains", dependencies=[_mut])
|
||||
async def add_blacklist_domain(data: BlacklistItem):
|
||||
async def add_blacklist_domain(data: BlacklistItem, request: Request):
|
||||
"""Add a domain to the blacklist."""
|
||||
if not data.value:
|
||||
raise HTTPException(status_code=400, detail="Value is required")
|
||||
|
|
@ -204,17 +255,26 @@ async def add_blacklist_domain(data: BlacklistItem):
|
|||
if not success:
|
||||
raise HTTPException(status_code=409, detail="Domain already blacklisted")
|
||||
|
||||
if _wants_html(request):
|
||||
return _tag_fragment(
|
||||
request,
|
||||
prefix="",
|
||||
value=data.value,
|
||||
delete_url=f"/api/blacklist/domains/{data.value}",
|
||||
confirm=f'Remover "{data.value}" da blacklist?',
|
||||
)
|
||||
|
||||
return {"message": f"Domain '{data.value}' added to blacklist"}
|
||||
|
||||
|
||||
@router.delete("/api/blacklist/domains/{domain:path}", dependencies=[_mut])
|
||||
async def remove_blacklist_domain(domain: str):
|
||||
async def remove_blacklist_domain(domain: str, request: Request):
|
||||
"""Remove a domain from the blacklist."""
|
||||
success = config_manager.remove_blacklist_domain(domain)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Domain not found in blacklist")
|
||||
|
||||
return {"message": f"Domain '{domain}' removed from blacklist"}
|
||||
return _empty_html_or_json(request, {"message": f"Domain '{domain}' removed from blacklist"})
|
||||
|
||||
|
||||
# Settings endpoints
|
||||
|
|
|
|||
|
|
@ -238,255 +238,9 @@ async function loadRecentDownloads() {
|
|||
}
|
||||
}
|
||||
|
||||
// Form handlers — POST attaches via addEventListener at DOMContentLoaded
|
||||
function attachFormHandlers() {
|
||||
const subredditForm = document.getElementById('subreddit-form');
|
||||
if (subredditForm) subredditForm.addEventListener('submit', handleSubredditSubmit);
|
||||
|
||||
const userForm = document.getElementById('user-form');
|
||||
if (userForm) userForm.addEventListener('submit', handleUserSubmit);
|
||||
|
||||
const authorBlacklistForm = document.getElementById('author-blacklist-form');
|
||||
if (authorBlacklistForm) authorBlacklistForm.addEventListener('submit', handleAuthorBlacklistSubmit);
|
||||
|
||||
const keywordBlacklistForm = document.getElementById('keyword-blacklist-form');
|
||||
if (keywordBlacklistForm) keywordBlacklistForm.addEventListener('submit', handleKeywordBlacklistSubmit);
|
||||
|
||||
const domainBlacklistForm = document.getElementById('domain-blacklist-form');
|
||||
if (domainBlacklistForm) domainBlacklistForm.addEventListener('submit', handleDomainBlacklistSubmit);
|
||||
|
||||
const subredditBlacklistForm = document.getElementById('subreddit-blacklist-form');
|
||||
if (subredditBlacklistForm) subredditBlacklistForm.addEventListener('submit', handleSubredditBlacklistSubmit);
|
||||
}
|
||||
|
||||
async function handleSubredditSubmit(e) {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('sub-name').value.trim();
|
||||
const limit = parseInt(document.getElementById('sub-limit').value);
|
||||
const sort = document.getElementById('sub-sort').value;
|
||||
try {
|
||||
const response = await fetch('/api/subreddits', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name, limit, sort})
|
||||
});
|
||||
if (response.ok) {
|
||||
showAlert('Subreddit adicionado!', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showAlert(data.detail || 'Erro ao adicionar', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showApiError(err, 'Erro de conexao');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUserSubmit(e) {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('user-name').value.trim();
|
||||
const limit = parseInt(document.getElementById('user-limit').value);
|
||||
try {
|
||||
const response = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name, limit})
|
||||
});
|
||||
if (response.ok) {
|
||||
showAlert('User adicionado!', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showAlert(data.detail || 'Erro ao adicionar', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showApiError(err, 'Erro de conexao');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSubreddit(name) {
|
||||
if (!confirm(`Remover r/${name}?`)) return;
|
||||
try {
|
||||
const response = await fetch(`/api/subreddits/${encodeURIComponent(name)}`, {method: 'DELETE'});
|
||||
if (response.ok) {
|
||||
showAlert('Subreddit removido!', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showAlert(data.detail || 'Erro ao remover', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showApiError(err, 'Erro de conexao');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeUser(name) {
|
||||
if (!confirm(`Remover u/${name}?`)) return;
|
||||
try {
|
||||
const response = await fetch(`/api/users/${encodeURIComponent(name)}`, {method: 'DELETE'});
|
||||
if (response.ok) {
|
||||
showAlert('User removido!', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showAlert(data.detail || 'Erro ao remover', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showApiError(err, 'Erro de conexao');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAuthorBlacklistSubmit(e) {
|
||||
e.preventDefault();
|
||||
const value = document.getElementById('blacklist-author').value.trim();
|
||||
if (!value) return;
|
||||
try {
|
||||
const response = await fetch('/api/blacklist/authors', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({value})
|
||||
});
|
||||
if (response.ok) {
|
||||
showAlert('Autor adicionado a blacklist!', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showAlert(data.detail || 'Erro ao adicionar', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showApiError(err, 'Erro de conexao');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleKeywordBlacklistSubmit(e) {
|
||||
e.preventDefault();
|
||||
const value = document.getElementById('blacklist-keyword').value.trim();
|
||||
if (!value) return;
|
||||
try {
|
||||
const response = await fetch('/api/blacklist/keywords', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({value})
|
||||
});
|
||||
if (response.ok) {
|
||||
showAlert('Keyword adicionada a blacklist!', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showAlert(data.detail || 'Erro ao adicionar', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showApiError(err, 'Erro de conexao');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDomainBlacklistSubmit(e) {
|
||||
e.preventDefault();
|
||||
const value = document.getElementById('blacklist-domain').value.trim();
|
||||
if (!value) return;
|
||||
try {
|
||||
const response = await fetch('/api/blacklist/domains', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({value})
|
||||
});
|
||||
if (response.ok) {
|
||||
showAlert('Dominio adicionado a blacklist!', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showAlert(data.detail || 'Erro ao adicionar', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showApiError(err, 'Erro de conexao');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubredditBlacklistSubmit(e) {
|
||||
e.preventDefault();
|
||||
const value = document.getElementById('blacklist-subreddit').value.trim();
|
||||
if (!value) return;
|
||||
try {
|
||||
const response = await fetch('/api/blacklist/subreddits', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({value})
|
||||
});
|
||||
if (response.ok) {
|
||||
showAlert('Subreddit adicionado a blacklist!', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showAlert(data.detail || 'Erro ao adicionar', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showApiError(err, 'Erro de conexao');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeBlacklistAuthor(author) {
|
||||
if (!confirm(`Remover u/${author} da blacklist?`)) return;
|
||||
try {
|
||||
const response = await fetch(`/api/blacklist/authors/${encodeURIComponent(author)}`, {method: 'DELETE'});
|
||||
if (response.ok) {
|
||||
showAlert('Autor removido da blacklist!', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showAlert(data.detail || 'Erro ao remover', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showApiError(err, 'Erro de conexao');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeBlacklistSubreddit(subreddit) {
|
||||
if (!confirm(`Remover r/${subreddit} da blacklist?`)) return;
|
||||
try {
|
||||
const response = await fetch(`/api/blacklist/subreddits/${encodeURIComponent(subreddit)}`, {method: 'DELETE'});
|
||||
if (response.ok) {
|
||||
showAlert('Subreddit removido da blacklist!', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showAlert(data.detail || 'Erro ao remover', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showApiError(err, 'Erro de conexao');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeBlacklistKeyword(keyword) {
|
||||
if (!confirm(`Remover "${keyword}" da blacklist?`)) return;
|
||||
try {
|
||||
const response = await fetch(`/api/blacklist/keywords/${encodeURIComponent(keyword)}`, {method: 'DELETE'});
|
||||
if (response.ok) {
|
||||
showAlert('Keyword removida da blacklist!', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showAlert(data.detail || 'Erro ao remover', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showApiError(err, 'Erro de conexao');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeBlacklistDomain(domain) {
|
||||
if (!confirm(`Remover "${domain}" da blacklist?`)) return;
|
||||
try {
|
||||
const response = await fetch(`/api/blacklist/domains/${encodeURIComponent(domain)}`, {method: 'DELETE'});
|
||||
if (response.ok) {
|
||||
showAlert('Dominio removido da blacklist!', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showAlert(data.detail || 'Erro ao remover', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showApiError(err, 'Erro de conexao');
|
||||
}
|
||||
}
|
||||
// Form handlers for config (subreddits, users, blacklist) moved to HTMX —
|
||||
// markup in partials/tab_sources.html. JS keeps only handlers for forms
|
||||
// that still need it (settings, scheduler, individual collection).
|
||||
|
||||
async function cleanupBlacklist() {
|
||||
const btn = document.getElementById('cleanup-blacklist-btn');
|
||||
|
|
@ -1640,7 +1394,6 @@ document.addEventListener('visibilitychange', () => {
|
|||
|
||||
// Boot
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
attachFormHandlers();
|
||||
startStatusPolling();
|
||||
updateStatus();
|
||||
loadStats();
|
||||
|
|
|
|||
|
|
@ -3,5 +3,10 @@
|
|||
<div class="list-item-name">r/{{ sub.name }}</div>
|
||||
<div class="list-item-details">Limite: {{ sub.limit }} | Ordem: {{ sub.sort or 'new' }}</div>
|
||||
</div>
|
||||
<button class="btn-remove" onclick="removeSubreddit('{{ sub.name }}')">Remover</button>
|
||||
<button class="btn-remove"
|
||||
hx-delete="/api/subreddits/{{ sub.name }}"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-target="closest .list-item"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Remover r/{{ sub.name }}?">Remover</button>
|
||||
</div>
|
||||
|
|
|
|||
12
src/web/templates/partials/_item_user.html
Normal file
12
src/web/templates/partials/_item_user.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<div class="list-item" data-name="{{ user.name }}">
|
||||
<div class="list-item-info">
|
||||
<div class="list-item-name">u/{{ user.name }}</div>
|
||||
<div class="list-item-details">Limite: {{ user.limit }}</div>
|
||||
</div>
|
||||
<button class="btn-remove"
|
||||
hx-delete="/api/users/{{ user.name }}"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-target="closest .list-item"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Remover u/{{ user.name }}?">Remover</button>
|
||||
</div>
|
||||
15
src/web/templates/partials/_tag.html
Normal file
15
src/web/templates/partials/_tag.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{# Generic blacklist tag chip. Params:
|
||||
- prefix: "u/", "r/" or "" (for keywords/domains)
|
||||
- value: tag value (e.g. "spammer", "promo")
|
||||
- delete_url: full URL of the DELETE endpoint
|
||||
- confirm: text to show in hx-confirm
|
||||
#}
|
||||
<div class="tag">
|
||||
<span>{{ prefix }}{{ value }}</span>
|
||||
<button class="tag-remove"
|
||||
hx-delete="{{ delete_url }}"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-target="closest .tag"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="{{ confirm }}">×</button>
|
||||
</div>
|
||||
|
|
@ -31,51 +31,41 @@
|
|||
</form>
|
||||
|
||||
<div class="list" id="subreddit-list">
|
||||
{% if subreddits %}
|
||||
{% for sub in subreddits %}
|
||||
<div class="list-item" data-name="{{ sub.name }}">
|
||||
<div class="list-item-info">
|
||||
<div class="list-item-name">r/{{ sub.name }}</div>
|
||||
<div class="list-item-details">Limite: {{ sub.limit }} | Ordem: {{ sub.sort or 'new' }}</div>
|
||||
</div>
|
||||
<button class="btn-remove" onclick="removeSubreddit('{{ sub.name }}')">Remover</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for sub in subreddits %}
|
||||
{% include 'partials/_item_subreddit.html' %}
|
||||
{% else %}
|
||||
<div class="empty">Nenhum subreddit configurado</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Users ({{ users|length }})</h2>
|
||||
|
||||
<form id="user-form" class="inline-form-user">
|
||||
<form id="user-form" class="inline-form-user"
|
||||
hx-post="/api/users"
|
||||
hx-ext="json-enc"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-target="#user-list"
|
||||
hx-swap="beforeend"
|
||||
hx-on::after-request="if(event.detail.successful){this.reset();window.showAlert&&showAlert('User adicionado!','success');}else{window.showApiError&&showApiError(event.detail.xhr,'Erro ao adicionar');}">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" id="user-name" placeholder="ex: username" required>
|
||||
<input type="text" name="name" id="user-name" placeholder="ex: username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Limite</label>
|
||||
<input type="number" id="user-limit" value="100" min="1" max="500">
|
||||
<input type="number" name="limit" id="user-limit" value="100" min="1" max="500">
|
||||
</div>
|
||||
<button type="submit" class="btn-add">Adicionar</button>
|
||||
</form>
|
||||
|
||||
<div class="list" id="user-list">
|
||||
{% if users %}
|
||||
{% for user in users %}
|
||||
<div class="list-item" data-name="{{ user.name }}">
|
||||
<div class="list-item-info">
|
||||
<div class="list-item-name">u/{{ user.name }}</div>
|
||||
<div class="list-item-details">Limite: {{ user.limit }}</div>
|
||||
</div>
|
||||
<button class="btn-remove" onclick="removeUser('{{ user.name }}')">Remover</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for user in users %}
|
||||
{% include 'partials/_item_user.html' %}
|
||||
{% else %}
|
||||
<div class="empty">Nenhum user configurado</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -90,89 +80,101 @@
|
|||
<div class="blacklist-grid" style="grid-template-columns: repeat(2, 1fr);">
|
||||
<div class="blacklist-card">
|
||||
<h3>Autores Bloqueados ({{ blacklist.authors|length }})</h3>
|
||||
<form id="author-blacklist-form" class="inline-form-simple">
|
||||
<form id="author-blacklist-form" class="inline-form-simple"
|
||||
hx-post="/api/blacklist/authors"
|
||||
hx-ext="json-enc"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-target="#author-tags"
|
||||
hx-swap="beforeend"
|
||||
hx-on::after-request="if(event.detail.successful){this.reset();window.showAlert&&showAlert('Autor adicionado a blacklist!','success');}else{window.showApiError&&showApiError(event.detail.xhr,'Erro ao adicionar');}">
|
||||
<div class="form-group">
|
||||
<input type="text" id="blacklist-author" placeholder="username">
|
||||
<input type="text" name="value" id="blacklist-author" placeholder="username">
|
||||
</div>
|
||||
<button type="submit" class="btn-add">Adicionar</button>
|
||||
</form>
|
||||
<div class="tag-list" id="author-tags">
|
||||
{% if blacklist.authors %}
|
||||
{% for author in blacklist.authors %}
|
||||
<div class="tag">
|
||||
<span>u/{{ author }}</span>
|
||||
<button class="tag-remove" onclick="removeBlacklistAuthor('{{ author }}')">×</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for author in blacklist.authors %}
|
||||
{% with prefix='u/', value=author, delete_url='/api/blacklist/authors/' ~ author, confirm='Remover u/' ~ author ~ ' da blacklist?' %}
|
||||
{% include 'partials/_tag.html' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<span class="empty" style="padding: 10px 0;">Nenhum autor bloqueado</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blacklist-card">
|
||||
<h3>Subreddits Bloqueados ({{ blacklist.subreddits|length if blacklist.subreddits else 0 }})</h3>
|
||||
<form id="subreddit-blacklist-form" class="inline-form-simple">
|
||||
<form id="subreddit-blacklist-form" class="inline-form-simple"
|
||||
hx-post="/api/blacklist/subreddits"
|
||||
hx-ext="json-enc"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-target="#subreddit-tags"
|
||||
hx-swap="beforeend"
|
||||
hx-on::after-request="if(event.detail.successful){this.reset();window.showAlert&&showAlert('Subreddit adicionado a blacklist!','success');}else{window.showApiError&&showApiError(event.detail.xhr,'Erro ao adicionar');}">
|
||||
<div class="form-group">
|
||||
<input type="text" id="blacklist-subreddit" placeholder="ex: pics">
|
||||
<input type="text" name="value" id="blacklist-subreddit" placeholder="ex: pics">
|
||||
</div>
|
||||
<button type="submit" class="btn-add">Adicionar</button>
|
||||
</form>
|
||||
<div class="tag-list" id="subreddit-tags">
|
||||
{% if blacklist.subreddits %}
|
||||
{% for sub in blacklist.subreddits %}
|
||||
<div class="tag">
|
||||
<span>r/{{ sub }}</span>
|
||||
<button class="tag-remove" onclick="removeBlacklistSubreddit('{{ sub }}')">×</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for sub in blacklist.subreddits or [] %}
|
||||
{% with prefix='r/', value=sub, delete_url='/api/blacklist/subreddits/' ~ sub, confirm='Remover r/' ~ sub ~ ' da blacklist?' %}
|
||||
{% include 'partials/_tag.html' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<span class="empty" style="padding: 10px 0;">Nenhum subreddit bloqueado</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blacklist-card">
|
||||
<h3>Palavras no Titulo ({{ blacklist.title_keywords|length }})</h3>
|
||||
<form id="keyword-blacklist-form" class="inline-form-simple">
|
||||
<form id="keyword-blacklist-form" class="inline-form-simple"
|
||||
hx-post="/api/blacklist/keywords"
|
||||
hx-ext="json-enc"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-target="#keyword-tags"
|
||||
hx-swap="beforeend"
|
||||
hx-on::after-request="if(event.detail.successful){this.reset();window.showAlert&&showAlert('Keyword adicionada a blacklist!','success');}else{window.showApiError&&showApiError(event.detail.xhr,'Erro ao adicionar');}">
|
||||
<div class="form-group">
|
||||
<input type="text" id="blacklist-keyword" placeholder="ex: selling, promo">
|
||||
<input type="text" name="value" id="blacklist-keyword" placeholder="ex: selling, promo">
|
||||
</div>
|
||||
<button type="submit" class="btn-add">Adicionar</button>
|
||||
</form>
|
||||
<div class="tag-list" id="keyword-tags">
|
||||
{% if blacklist.title_keywords %}
|
||||
{% for keyword in blacklist.title_keywords %}
|
||||
<div class="tag">
|
||||
<span>{{ keyword }}</span>
|
||||
<button class="tag-remove" onclick="removeBlacklistKeyword('{{ keyword }}')">×</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for keyword in blacklist.title_keywords %}
|
||||
{% with prefix='', value=keyword, delete_url='/api/blacklist/keywords/' ~ keyword, confirm='Remover "' ~ keyword ~ '" da blacklist?' %}
|
||||
{% include 'partials/_tag.html' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<span class="empty" style="padding: 10px 0;">Nenhuma palavra bloqueada</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blacklist-card">
|
||||
<h3>Dominios Bloqueados ({{ blacklist.domains|length }})</h3>
|
||||
<form id="domain-blacklist-form" class="inline-form-simple">
|
||||
<form id="domain-blacklist-form" class="inline-form-simple"
|
||||
hx-post="/api/blacklist/domains"
|
||||
hx-ext="json-enc"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-target="#domain-tags"
|
||||
hx-swap="beforeend"
|
||||
hx-on::after-request="if(event.detail.successful){this.reset();window.showAlert&&showAlert('Dominio adicionado a blacklist!','success');}else{window.showApiError&&showApiError(event.detail.xhr,'Erro ao adicionar');}">
|
||||
<div class="form-group">
|
||||
<input type="text" id="blacklist-domain" placeholder="ex: linktr.ee">
|
||||
<input type="text" name="value" id="blacklist-domain" placeholder="ex: linktr.ee">
|
||||
</div>
|
||||
<button type="submit" class="btn-add">Adicionar</button>
|
||||
</form>
|
||||
<div class="tag-list" id="domain-tags">
|
||||
{% if blacklist.domains %}
|
||||
{% for domain in blacklist.domains %}
|
||||
<div class="tag">
|
||||
<span>{{ domain }}</span>
|
||||
<button class="tag-remove" onclick="removeBlacklistDomain('{{ domain }}')">×</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for domain in blacklist.domains %}
|
||||
{% with prefix='', value=domain, delete_url='/api/blacklist/domains/' ~ domain, confirm='Remover "' ~ domain ~ '" da blacklist?' %}
|
||||
{% include 'partials/_tag.html' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<span class="empty" style="padding: 10px 0;">Nenhum dominio bloqueado</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue