Quellen-Filter über Trefferliste, dynamisches Filtering; Umlaut-Filter hinzugefügt/angepasst; Rahmen um Trefferliste

This commit is contained in:
2025-08-19 15:06:07 +02:00
parent 177e4d01ce
commit f5deb5a839
2 changed files with 161 additions and 118 deletions

142
app.py
View File

@@ -7,86 +7,106 @@ app = Flask(__name__)
def load_words() -> Tuple[List[str], Dict[str, List[str]]]: def load_words() -> Tuple[List[str], Dict[str, List[str]]]:
data_dir = Path(__file__).parent / "data" data_dir = Path(__file__).parent / "data"
txt_path = data_dir / "words_de_5.txt" txt_path = data_dir / "words_de_5.txt"
json_path = data_dir / "words_de_5_sources.json" json_path = data_dir / "words_de_5_sources.json"
words: List[str] = [] words: List[str] = []
sources_map: Dict[str, List[str]] = {} sources_map: Dict[str, List[str]] = {}
if txt_path.exists(): if txt_path.exists():
with txt_path.open("r", encoding="utf-8") as f: with txt_path.open("r", encoding="utf-8") as f:
for line in f: for line in f:
word = line.strip().lower() word = line.strip().lower()
if len(word) == 5 and word.isalpha(): if len(word) == 5 and word.isalpha():
words.append(word) words.append(word)
if json_path.exists(): if json_path.exists():
try: try:
sources_map = json.loads(json_path.read_text(encoding="utf-8")) sources_map = json.loads(json_path.read_text(encoding="utf-8"))
except Exception: except Exception:
sources_map = {} sources_map = {}
return words, sources_map return words, sources_map
def filter_words(words: List[str], position_letters: List[str], includes_text: str, excludes_text: str) -> List[str]: def filter_words(words: List[str], position_letters: List[str], includes_text: str, excludes_text: str) -> List[str]:
results: List[str] = [] results: List[str] = []
includes_letters = [ch for ch in includes_text.lower() if ch.isalpha()] includes_letters = [ch for ch in includes_text.lower() if ch.isalpha()]
excludes_letters = [ch for ch in excludes_text.lower() if ch.isalpha()] excludes_letters = [ch for ch in excludes_text.lower() if ch.isalpha()]
for word in words: for word in words:
# feste Positionen # feste Positionen
if any(ch and word[idx] != ch for idx, ch in enumerate(position_letters)): if any(ch and word[idx] != ch for idx, ch in enumerate(position_letters)):
continue continue
# muss-enthalten # muss-enthalten
if not all(ch in word for ch in includes_letters): if not all(ch in word for ch in includes_letters):
continue continue
# darf-nicht-enthalten # darf-nicht-enthalten
if any(ch in word for ch in excludes_letters): if any(ch in word for ch in excludes_letters):
continue continue
results.append(word) results.append(word)
return results return results
@app.route("/", methods=["GET", "POST"]) @app.route("/", methods=["GET", "POST"])
def index(): def index():
all_words, sources_map = load_words() all_words, sources_map = load_words()
results: List[str] | None = None results_display: List[str] | None = None
pos: List[str] = ["", "", "", "", ""] pos: List[str] = ["", "", "", "", ""]
includes: str = "" includes: str = ""
excludes: str = "" excludes: str = ""
if request.method == "POST": use_ot: bool = True
pos = [ use_wf: bool = False
(request.form.get("pos1") or "").strip().lower(), if request.method == "POST":
(request.form.get("pos2") or "").strip().lower(), pos = [
(request.form.get("pos3") or "").strip().lower(), (request.form.get("pos1") or "").strip().lower(),
(request.form.get("pos4") or "").strip().lower(), (request.form.get("pos2") or "").strip().lower(),
(request.form.get("pos5") or "").strip().lower(), (request.form.get("pos3") or "").strip().lower(),
] (request.form.get("pos4") or "").strip().lower(),
includes = (request.form.get("includes") or "").strip() (request.form.get("pos5") or "").strip().lower(),
excludes = (request.form.get("excludes") or "").strip() ]
results = filter_words(all_words, pos, includes, excludes) includes = (request.form.get("includes") or "").strip()
return render_template( excludes = (request.form.get("excludes") or "").strip()
"index.html", use_ot = request.form.get("use_ot") is not None
results=results, use_wf = request.form.get("use_wf") is not None
pos=pos,
includes=includes, # 1) Buchstaben-/Positionssuche über alle Wörter
excludes=excludes, matched = filter_words(all_words, pos, includes, excludes)
words_count=len(all_words), # 2) Quellen-Filter nur auf Ergebnisansicht anwenden
sources_map=sources_map, allowed = set()
) if use_ot:
allowed.add("ot")
if use_wf:
allowed.add("wf")
if allowed:
results_display = [w for w in matched if any(src in allowed for src in sources_map.get(w, []))]
else:
# Keine Quelle gewählt → leere Anzeige (Suche wurde dennoch ausgeführt)
results_display = []
return render_template(
"index.html",
results=results_display,
pos=pos,
includes=includes,
excludes=excludes,
words_count=len(all_words),
sources_map=sources_map,
use_ot=use_ot,
use_wf=use_wf,
error_message=None,
)
@app.route('/manifest.webmanifest') @app.route('/manifest.webmanifest')
def manifest_file(): def manifest_file():
return send_from_directory(Path(__file__).parent / 'static', 'manifest.webmanifest', mimetype='application/manifest+json') return send_from_directory(Path(__file__).parent / 'static', 'manifest.webmanifest', mimetype='application/manifest+json')
@app.route('/sw.js') @app.route('/sw.js')
def service_worker(): def service_worker():
# Service Worker muss auf Top-Level liegen # Service Worker muss auf Top-Level liegen
return send_from_directory(Path(__file__).parent / 'static', 'sw.js', mimetype='application/javascript') return send_from_directory(Path(__file__).parent / 'static', 'sw.js', mimetype='application/javascript')
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True) app.run(debug=True)

View File

@@ -29,46 +29,19 @@
})(); })();
</script> </script>
<style> <style>
:root { :root { --bg:#ffffff; --text:#111827; --muted:#6b7280; --badge-bg:#e5e7eb; --badge-text:#111827; --border:#e5e7eb; --skip-bg:#111827; --skip-text:#ffffff; --button-bg:#111827; --button-text:#ffffff; --input-bg:#ffffff; --input-text:#111827; --error:#b91c1c; }
--bg: #ffffff; [data-theme="dark"] { --bg:#0b1220; --text:#e5e7eb; --muted:#9ca3af; --badge-bg:#374151; --badge-text:#f9fafb; --border:#334155; --skip-bg:#e5e7eb; --skip-text:#111827; --button-bg:#e5e7eb; --button-text:#111827; --input-bg:#111827; --input-text:#e5e7eb; --error:#ef4444; }
--text: #111827;
--muted: #6b7280;
--badge-bg: #e5e7eb;
--badge-text: #111827;
--border: #e5e7eb;
--skip-bg: #111827;
--skip-text: #ffffff;
--button-bg: #111827;
--button-text: #ffffff;
--input-bg: #ffffff;
--input-text: #111827;
}
[data-theme="dark"] {
--bg: #0b1220;
--text: #e5e7eb;
--muted: #9ca3af;
--badge-bg: #374151;
--badge-text: #f9fafb;
--border: #334155;
--skip-bg: #e5e7eb;
--skip-text: #111827;
--button-bg: #e5e7eb;
--button-text: #111827;
--input-bg: #111827;
--input-text: #e5e7eb;
}
html, body { background: var(--bg); color: var(--text); } html, body { background: var(--bg); color: var(--text); }
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin: 2rem; } body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin: 2rem; }
.container { max-width: 800px; margin: 0 auto; } .container { max-width: 800px; margin: 0 auto; }
.page-header { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; } .page-header { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.page-header h1 { margin: 0; } .page-header h1 { margin: 0; }
@media (max-width: 480px) { @media (max-width: 480px) { .page-header h1 { font-size: 1.5rem; } }
.page-header h1 { font-size: 1.5rem; }
}
.grid { display: grid; grid-template-columns: repeat(5, 3rem); gap: .5rem; } .grid { display: grid; grid-template-columns: repeat(5, 3rem); gap: .5rem; }
.grid input { text-align: center; font-size: 1.25rem; padding: .4rem; background: var(--input-bg); color: var(--input-text); border: 1px solid var(--border); border-radius: .375rem; } .grid input { text-align: center; font-size: 1.25rem; padding: .4rem; background: var(--input-bg); color: var(--input-text); border: 1px solid var(--border); border-radius: .375rem; }
label { font-weight: 600; display: block; margin-top: 1rem; margin-bottom: .25rem; } label { font-weight: 600; display: block; margin-top: 1rem; margin-bottom: .25rem; }
.results { margin-top: 1.5rem; } .results { margin-top: 1.5rem; }
.results-box { border: 1px solid var(--border); border-radius: .5rem; padding: .75rem; }
.badge { display: inline-block; padding: .25rem .5rem; background: var(--badge-bg); color: var(--badge-text); border-radius: .375rem; margin-right: .25rem; margin-bottom: .25rem; } .badge { display: inline-block; padding: .25rem .5rem; background: var(--badge-bg); color: var(--badge-text); border-radius: .375rem; margin-right: .25rem; margin-bottom: .25rem; }
.source { font-size: .75rem; padding: .1rem .35rem; border-radius: .25rem; margin-left: .25rem; } .source { font-size: .75rem; padding: .1rem .35rem; border-radius: .25rem; margin-left: .25rem; }
.source.ot { background: #dbeafe; color: #1e40af; } .source.ot { background: #dbeafe; color: #1e40af; }
@@ -81,6 +54,8 @@
.skip-link { position: absolute; left: -9999px; top: auto; width: 1px; height: 1px; overflow: hidden; } .skip-link { position: absolute; left: -9999px; top: auto; width: 1px; height: 1px; overflow: hidden; }
.skip-link:focus { position: static; width: auto; height: auto; padding: .5rem .75rem; background: var(--skip-bg); color: var(--skip-text); border-radius: .25rem; } .skip-link:focus { position: static; width: auto; height: auto; padding: .5rem .75rem; background: var(--skip-bg); color: var(--skip-text); border-radius: .25rem; }
.hint { margin-top: .25rem; color: var(--muted); font-size: .9rem; } .hint { margin-top: .25rem; color: var(--muted); font-size: .9rem; }
.inline-controls { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.filter-box { border: 1px solid var(--border); border-radius: .5rem; padding: .5rem .75rem; margin: .5rem 0 1rem; }
.word-list { list-style: none; padding: 0; margin: 0; } .word-list { list-style: none; padding: 0; margin: 0; }
.word-list li { display: inline-block; margin: 0 .25rem .25rem 0; } .word-list li { display: inline-block; margin: 0 .25rem .25rem 0; }
fieldset { border: 1px solid var(--border); border-radius: .5rem; padding: .75rem; } fieldset { border: 1px solid var(--border); border-radius: .5rem; padding: .75rem; }
@@ -99,7 +74,7 @@
<p>Wortliste geladen: <strong>{{ words_count }}</strong> Wörter</p> <p>Wortliste geladen: <strong>{{ words_count }}</strong> Wörter</p>
<main id="main" role="main"> <main id="main" role="main">
<form method="post" aria-describedby="form-hint"> <form id="search-form" method="post" aria-describedby="form-hint">
<p id="form-hint" class="hint">Gib bekannte Buchstaben ein. Leere Felder werden ignoriert.</p> <p id="form-hint" class="hint">Gib bekannte Buchstaben ein. Leere Felder werden ignoriert.</p>
<fieldset> <fieldset>
@@ -127,30 +102,46 @@
{% if results is not none %} {% if results is not none %}
<div class="results" id="results" role="region" aria-labelledby="results-title"> <div class="results" id="results" role="region" aria-labelledby="results-title">
<h2 id="results-title">Vorschläge ({{ results|length }})</h2> <h2 id="results-title">Vorschläge (<span id="visible-count">{{ results|length }}</span>)</h2>
{% if results|length == 0 %} <fieldset class="filter-box" role="group">
<p>Keine Treffer. Bitte Bedingungen anpassen.</p> <legend>WortquellenFilter</legend>
{% else %} <div class="inline-controls">
<details open> <label><input id="filter-ot" type="checkbox" name="use_ot" form="search-form" {% if use_ot %}checked{% endif %}/> OT (OpenThesaurus)</label>
<summary>Liste anzeigen</summary> <label><input id="filter-wf" type="checkbox" name="use_wf" form="search-form" {% if use_wf %}checked{% endif %}/> WF (wordfreq)</label>
<ul class="word-list"> </div>
{% for w in results %} </fieldset>
<li> <fieldset class="filter-box" role="group">
<span class="badge">{{ w }} <legend>Umlaute</legend>
{% set srcs = sources_map.get(w, []) %} <div class="inline-controls">
{% for s in srcs %} <label><input id="filter-umlaut" type="checkbox" /> Umlaute einbeziehen (ä, ö, ü, ß)</label>
{% if s == 'ot' %} </div>
<span class="source ot">OT</span> </fieldset>
{% elif s == 'wf' %} <div class="results-box">
<span class="source wf">WF</span> {% if results|length == 0 %}
{% endif %} <p>Keine Treffer. Bitte Bedingungen anpassen.</p>
{% endfor %} {% else %}
</span> <details open>
</li> <summary>Liste anzeigen</summary>
{% endfor %} <ul class="word-list">
</ul> {% for w in results %}
</details> {% set srcs = sources_map.get(w, []) %}
{% endif %} {% set has_umlaut = ('ä' in w or 'ö' in w or 'ü' in w or 'ß' in w) %}
<li data-sources="{{ srcs|join(' ') }}" data-umlaut="{{ 1 if has_umlaut else 0 }}">
<span class="badge">{{ w }}
{% for s in srcs %}
{% if s == 'ot' %}
<span class="source ot">OT</span>
{% elif s == 'wf' %}
<span class="source wf">WF</span>
{% endif %}
{% endfor %}
</span>
</li>
{% endfor %}
</ul>
</details>
{% endif %}
</div>
<p> <p>
<strong>Legende:</strong> <strong>Legende:</strong>
<span class="source wf">WF</span> = <a href="https://pypi.org/project/wordfreq/" target="_blank" rel="noopener noreferrer">Wordfreq</a>, <span class="source wf">WF</span> = <a href="https://pypi.org/project/wordfreq/" target="_blank" rel="noopener noreferrer">Wordfreq</a>,
@@ -177,7 +168,6 @@
btn.textContent = theme === 'dark' ? '🌙' : '🌞'; btn.textContent = theme === 'dark' ? '🌙' : '🌞';
btn.title = 'Theme umschalten (' + (theme === 'dark' ? 'Dunkel' : 'Hell') + ')'; btn.title = 'Theme umschalten (' + (theme === 'dark' ? 'Dunkel' : 'Hell') + ')';
} }
// init label/state
setTheme(currentTheme()); setTheme(currentTheme());
btn.addEventListener('click', function() { btn.addEventListener('click', function() {
var next = currentTheme() === 'dark' ? 'light' : 'dark'; var next = currentTheme() === 'dark' ? 'light' : 'dark';
@@ -185,6 +175,39 @@
}); });
})(); })();
</script> </script>
<script>
function applyFilters() {
var ot = document.getElementById('filter-ot');
var wf = document.getElementById('filter-wf');
var uml = document.getElementById('filter-umlaut');
if (!ot || !wf) return;
var allowed = [];
if (ot.checked) allowed.push('ot');
if (wf.checked) allowed.push('wf');
var items = document.querySelectorAll('.word-list li');
var visible = 0;
items.forEach(function(li) {
var sources = (li.dataset.sources || '').split(/\s+/).filter(Boolean);
var hasUmlaut = li.dataset.umlaut === '1';
var showSource = allowed.length === 0 ? false : sources.some(function(s){ return allowed.indexOf(s) !== -1; });
var showUmlaut = uml && uml.checked ? true : !hasUmlaut; // an => alle, aus => ohne Umlaute
var show = showSource && showUmlaut;
li.style.display = show ? '' : 'none';
if (show) visible++;
});
var countEl = document.getElementById('visible-count');
if (countEl) countEl.textContent = String(visible);
}
window.addEventListener('load', function() {
var ot = document.getElementById('filter-ot');
var wf = document.getElementById('filter-wf');
var uml = document.getElementById('filter-umlaut');
if (ot) ot.addEventListener('change', applyFilters);
if (wf) wf.addEventListener('change', applyFilters);
if (uml) uml.addEventListener('change', applyFilters);
applyFilters();
});
</script>
<script> <script>
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', function() { window.addEventListener('load', function() {