Text-Erstellung mit Floating-Input-Box (Variante B)
Neuer Workflow: Klick "+ Text" in Topbar → Punkt im Viewport picken → Floating Eto-Dialog erscheint neben dem Mauszeiger → User tippt → Enter fuegt TextEntity mit Topbar-Settings ein. Esc bricht ab. Backend (rhino/text_create.py): - load_settings/save_settings — persistiert font/size/bold/italic in doc.Strings["dossier_text_settings"] (JSON) - available_fonts() — System-Font-Namen via Rhino.DocObjects.Font.AvailableFontFaceNames - _floating_input() — Eto.Dialog mit TextBox, ShowModal mit Rhino- MainWindow als Parent, positioniert bei Mouse.Position - create_text() — RhinoGet.GetPoint → _floating_input → TextEntity mit Font/Size/Bold/Italic erstellen + AddText - _apply_font() mit 2 Fallback-Pfaden (FontTable.FindOrCreate + Font.FromQuartetProperties) fuer RhinoCommon-Kompatibilitaet Backend (oberleiste.py): - CREATE_TEXT handler → text_create.create_text() - SET_TEXT_SETTINGS handler → text_create.save_settings (merge partial) - State payload: textSettings (immer) + textFonts (einmalig initial, via _fonts_sent Flag — Liste aendert sich nicht zur Laufzeit) Frontend (OberleisteApp + rhinoBridge): - createText() + setTextSettings() Bridge-Funktionen - Text-Block 2x2 Grid analog Massstab: R1: Font-Dropdown (BarCombo mit text_fields icon) | Size-Input mit "m" suffix R2: B/I-Toggles (segmented pill mit accent-Fill bei active) | "+ Text" Button - Hover-Logik analog View-Toggle (bg → bg-item-hover, color → accent-light) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -906,6 +906,22 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[OBERLEISTE] open about:", ex)
|
print("[OBERLEISTE] open about:", ex)
|
||||||
|
|
||||||
|
# --- Text-Erstellung (Floating-Input) ---------------------------
|
||||||
|
elif t == "CREATE_TEXT":
|
||||||
|
try:
|
||||||
|
import text_create
|
||||||
|
text_create.create_text()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[OBERLEISTE] create text:", ex)
|
||||||
|
elif t == "SET_TEXT_SETTINGS":
|
||||||
|
try:
|
||||||
|
import text_create
|
||||||
|
text_create.save_settings(
|
||||||
|
Rhino.RhinoDoc.ActiveDoc, p.get("settings") or {})
|
||||||
|
except Exception as ex:
|
||||||
|
print("[OBERLEISTE] text settings:", ex)
|
||||||
|
self._send_state(force=True)
|
||||||
|
|
||||||
# --- Masse (Mass-Style) -----------------------------------------
|
# --- Masse (Mass-Style) -----------------------------------------
|
||||||
elif t == "SET_MASSE_ACTIVE":
|
elif t == "SET_MASSE_ACTIVE":
|
||||||
try:
|
try:
|
||||||
@@ -1152,6 +1168,15 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
except Exception:
|
except Exception:
|
||||||
info["massePresets"] = []
|
info["massePresets"] = []
|
||||||
info["masseActiveId"] = None
|
info["masseActiveId"] = None
|
||||||
|
# Text-Settings + verfuegbare Fonts (Fonts nur einmal initial)
|
||||||
|
try:
|
||||||
|
import text_create
|
||||||
|
info["textSettings"] = text_create.load_settings(doc)
|
||||||
|
if not getattr(self, "_fonts_sent", False):
|
||||||
|
info["textFonts"] = text_create.available_fonts()
|
||||||
|
self._fonts_sent = True
|
||||||
|
except Exception:
|
||||||
|
info["textSettings"] = {}
|
||||||
# Command-Line State
|
# Command-Line State
|
||||||
prompt = _get_command_prompt()
|
prompt = _get_command_prompt()
|
||||||
info["cmdPrompt"] = prompt
|
info["cmdPrompt"] = prompt
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
#! python 3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
text_create.py
|
||||||
|
Text-Erstellungs-Workflow mit Floating-Input-Box statt Rhino-Dialog.
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. User klickt Topbar-Button "Text einfuegen"
|
||||||
|
2. RhinoGet.GetPoint(): User picked Position im Viewport
|
||||||
|
3. Eto.Dialog mit TextBox erscheint neben Maus-Cursor
|
||||||
|
4. User tippt, Enter = commit, Escape = abbrechen
|
||||||
|
5. TextEntity wird mit Topbar-Settings (Font/Size/Bold/Italic)
|
||||||
|
am gepickten Punkt erstellt
|
||||||
|
|
||||||
|
Settings werden pro Dokument in doc.Strings["dossier_text_settings"]
|
||||||
|
persistiert (JSON: font/size/bold/italic).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import Rhino
|
||||||
|
import Rhino.Geometry as rg
|
||||||
|
import scriptcontext as sc
|
||||||
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if _HERE not in sys.path:
|
||||||
|
sys.path.insert(0, _HERE)
|
||||||
|
|
||||||
|
_SETTINGS_KEY = "dossier_text_settings"
|
||||||
|
|
||||||
|
_DEFAULTS = {
|
||||||
|
"font": "Helvetica",
|
||||||
|
"size": 0.20, # in Model-Units (m bei m-Doc, mm bei mm-Doc)
|
||||||
|
"bold": False,
|
||||||
|
"italic": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_settings(doc):
|
||||||
|
if doc is None: return dict(_DEFAULTS)
|
||||||
|
try:
|
||||||
|
raw = doc.Strings.GetValue(_SETTINGS_KEY)
|
||||||
|
if not raw: return dict(_DEFAULTS)
|
||||||
|
s = json.loads(raw)
|
||||||
|
if not isinstance(s, dict): return dict(_DEFAULTS)
|
||||||
|
out = dict(_DEFAULTS)
|
||||||
|
out.update({k: s[k] for k in s if k in _DEFAULTS})
|
||||||
|
return out
|
||||||
|
except Exception:
|
||||||
|
return dict(_DEFAULTS)
|
||||||
|
|
||||||
|
|
||||||
|
def save_settings(doc, partial):
|
||||||
|
"""Merged partial-Updates rein und persistiert."""
|
||||||
|
if doc is None or not isinstance(partial, dict): return
|
||||||
|
cur = load_settings(doc)
|
||||||
|
cur.update({k: partial[k] for k in partial if k in _DEFAULTS})
|
||||||
|
try:
|
||||||
|
doc.Strings.SetString(_SETTINGS_KEY, json.dumps(cur))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[TEXT] save settings:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def available_fonts():
|
||||||
|
"""Liefert sortierte Liste verfuegbarer System-Fonts (Face-Names)."""
|
||||||
|
try:
|
||||||
|
names = Rhino.DocObjects.Font.AvailableFontFaceNames()
|
||||||
|
out = sorted({str(n) for n in names if n})
|
||||||
|
return out
|
||||||
|
except Exception as ex:
|
||||||
|
print("[TEXT] available_fonts:", ex)
|
||||||
|
return ["Helvetica", "Arial", "Times New Roman", "Courier New"]
|
||||||
|
|
||||||
|
|
||||||
|
def _floating_input():
|
||||||
|
"""Modal Eto-Dialog mit TextBox bei der Maus-Position. Returns
|
||||||
|
eingegebenen Text oder None (Escape/leer)."""
|
||||||
|
import Eto.Forms as forms
|
||||||
|
import Eto.Drawing as drawing
|
||||||
|
|
||||||
|
dlg = forms.Dialog()
|
||||||
|
dlg.Title = "Text"
|
||||||
|
dlg.Resizable = False
|
||||||
|
dlg.MinimumSize = drawing.Size(280, 0)
|
||||||
|
dlg.Padding = drawing.Padding(6)
|
||||||
|
|
||||||
|
tb = forms.TextBox()
|
||||||
|
tb.PlaceholderText = "Text eingeben — Enter = einfuegen, Esc = abbrechen"
|
||||||
|
tb.Font = drawing.Font("Helvetica", 13)
|
||||||
|
tb.Width = 280
|
||||||
|
|
||||||
|
result = {"text": None, "committed": False}
|
||||||
|
|
||||||
|
def on_keydown(sender, e):
|
||||||
|
if e.Key == forms.Keys.Enter:
|
||||||
|
result["text"] = tb.Text or ""
|
||||||
|
result["committed"] = True
|
||||||
|
try: dlg.Close()
|
||||||
|
except Exception: pass
|
||||||
|
e.Handled = True
|
||||||
|
elif e.Key == forms.Keys.Escape:
|
||||||
|
try: dlg.Close()
|
||||||
|
except Exception: pass
|
||||||
|
e.Handled = True
|
||||||
|
|
||||||
|
tb.KeyDown += on_keydown
|
||||||
|
|
||||||
|
layout = forms.StackLayout()
|
||||||
|
layout.Padding = drawing.Padding(0)
|
||||||
|
layout.Items.Add(tb)
|
||||||
|
dlg.Content = layout
|
||||||
|
|
||||||
|
# Bei Maus-Cursor positionieren (Position wo User gerade geklickt hat)
|
||||||
|
try:
|
||||||
|
m = forms.Mouse.Position
|
||||||
|
dlg.Location = drawing.Point(int(m.X) + 10, int(m.Y) + 12)
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
# ShowModal blockiert bis Close. Parent = Rhinos Main-Window damit
|
||||||
|
# der Dialog ueber dem Viewport rendert (Mac).
|
||||||
|
try:
|
||||||
|
parent = Rhino.UI.RhinoEtoApp.MainWindow
|
||||||
|
except Exception:
|
||||||
|
parent = None
|
||||||
|
try:
|
||||||
|
if parent is not None:
|
||||||
|
dlg.ShowModal(parent)
|
||||||
|
else:
|
||||||
|
dlg.ShowModal()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[TEXT] dialog show:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not result["committed"]: return None
|
||||||
|
txt = (result["text"] or "").strip()
|
||||||
|
return txt or None
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_font(te, face, bold, italic):
|
||||||
|
"""Setzt Font auf TextEntity. Mehrere Fallback-Pfade fuer
|
||||||
|
verschiedene RhinoCommon-Versionen."""
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
# Pfad 1: FindOrCreate im FontTable + zuweisen
|
||||||
|
try:
|
||||||
|
font_idx = doc.Fonts.FindOrCreate(face, bool(bold), bool(italic))
|
||||||
|
if font_idx >= 0:
|
||||||
|
font = doc.Fonts[font_idx]
|
||||||
|
if font is not None:
|
||||||
|
te.Font = font
|
||||||
|
return True
|
||||||
|
except Exception as ex:
|
||||||
|
print("[TEXT] font path 1:", ex)
|
||||||
|
# Pfad 2: statische FromQuartetProperties
|
||||||
|
try:
|
||||||
|
font = Rhino.DocObjects.Font.FromQuartetProperties(
|
||||||
|
face, bool(bold), bool(italic))
|
||||||
|
if font is not None:
|
||||||
|
te.Font = font
|
||||||
|
return True
|
||||||
|
except Exception as ex:
|
||||||
|
print("[TEXT] font path 2:", ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_text():
|
||||||
|
"""Triggered von der Oberleiste. Vollstaendiger Workflow."""
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return
|
||||||
|
settings = load_settings(doc)
|
||||||
|
|
||||||
|
rc, pt = Rhino.Input.RhinoGet.GetPoint(
|
||||||
|
"Position fuer Text", False)
|
||||||
|
if rc != Rhino.Commands.Result.Success: return
|
||||||
|
if pt is None: return
|
||||||
|
|
||||||
|
text = _floating_input()
|
||||||
|
if not text: return
|
||||||
|
|
||||||
|
try:
|
||||||
|
te = rg.TextEntity()
|
||||||
|
te.Plane = rg.Plane(pt, rg.Vector3d.ZAxis)
|
||||||
|
te.PlainText = text
|
||||||
|
try:
|
||||||
|
te.TextHeight = float(settings.get("size", 0.20))
|
||||||
|
except Exception: pass
|
||||||
|
_apply_font(te, settings.get("font") or "Helvetica",
|
||||||
|
settings.get("bold"), settings.get("italic"))
|
||||||
|
doc.Objects.AddText(te)
|
||||||
|
doc.Views.Redraw()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[TEXT] create:", ex)
|
||||||
+143
-1
@@ -11,7 +11,7 @@ import {
|
|||||||
deleteLayerCombination, openLayerCombinationsDialog,
|
deleteLayerCombination, openLayerCombinationsDialog,
|
||||||
openDossierSettings, openKameraPanel,
|
openDossierSettings, openKameraPanel,
|
||||||
setMasseActive, openMasseSettings,
|
setMasseActive, openMasseSettings,
|
||||||
openAbout,
|
openAbout, createText, setTextSettings,
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
const PRESETS = [
|
const PRESETS = [
|
||||||
@@ -305,6 +305,8 @@ export default function OberleisteApp() {
|
|||||||
overridesActivePreset: null, overridesPresets: [],
|
overridesActivePreset: null, overridesPresets: [],
|
||||||
layerCombinations: [], layerCombinationActive: null,
|
layerCombinations: [], layerCombinationActive: null,
|
||||||
massePresets: [], masseActiveId: null,
|
massePresets: [], masseActiveId: null,
|
||||||
|
textSettings: { font: 'Helvetica', size: 0.20, bold: false, italic: false },
|
||||||
|
textFonts: [],
|
||||||
})
|
})
|
||||||
const [appliedScale, setAppliedScale] = useState(null)
|
const [appliedScale, setAppliedScale] = useState(null)
|
||||||
const appliedScaleRef = useRef(null)
|
const appliedScaleRef = useRef(null)
|
||||||
@@ -759,6 +761,146 @@ export default function OberleisteApp() {
|
|||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
<div style={sep} />
|
||||||
|
|
||||||
|
{/* ====== TEXT-Block 2-Reihen ======
|
||||||
|
Reihe 1: Font-Dropdown + Groesse (mm)
|
||||||
|
Reihe 2: B/I-Toggles + "Text einfuegen"-Button
|
||||||
|
*/}
|
||||||
|
{(() => {
|
||||||
|
const ts = state.textSettings || {}
|
||||||
|
const fonts = state.textFonts || []
|
||||||
|
const updateTs = (patch) => setTextSettings({ ...ts, ...patch })
|
||||||
|
const TEXT_W = 150
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'grid', gridTemplateColumns: 'auto auto', gap: '4px 6px',
|
||||||
|
alignItems: 'center', flexShrink: 0,
|
||||||
|
}}>
|
||||||
|
{/* Reihe 1, Spalte 1: Font-Dropdown */}
|
||||||
|
<BarCombo
|
||||||
|
icon="text_fields"
|
||||||
|
value={ts.font || ''}
|
||||||
|
onChange={(v) => updateTs({ font: v })}
|
||||||
|
width={TEXT_W}
|
||||||
|
title="Schriftart"
|
||||||
|
>
|
||||||
|
{fonts.length === 0 && <option value="">—</option>}
|
||||||
|
{fonts.map(f => <option key={f} value={f}>{f}</option>)}
|
||||||
|
</BarCombo>
|
||||||
|
{/* Reihe 1, Spalte 2: Groesse (mm) */}
|
||||||
|
<div style={{
|
||||||
|
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||||
|
height: BAR_H, padding: '0 10px',
|
||||||
|
background: 'var(--bg-input)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 999,
|
||||||
|
flexShrink: 0, width: 90,
|
||||||
|
}}>
|
||||||
|
<input
|
||||||
|
type="number" step="0.01" min="0.01"
|
||||||
|
value={ts.size != null ? ts.size : 0.2}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = parseFloat(e.target.value)
|
||||||
|
if (!isNaN(v) && v > 0) updateTs({ size: v })
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
flex: 1, minWidth: 0,
|
||||||
|
background: 'transparent', border: 'none', outline: 'none',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
fontSize: 11, fontFamily: 'DM Mono, monospace',
|
||||||
|
padding: 0, textAlign: 'right',
|
||||||
|
appearance: 'auto',
|
||||||
|
}}
|
||||||
|
title="Texthoehe in Model-Units"
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: 9, color: 'var(--text-muted)' }}>m</span>
|
||||||
|
</div>
|
||||||
|
{/* Reihe 2, Spalte 1: B/I-Toggles */}
|
||||||
|
<div style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
border: '1px solid var(--border)', borderRadius: 999,
|
||||||
|
overflow: 'hidden', flexShrink: 0, width: TEXT_W,
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
onClick={() => updateTs({ bold: !ts.bold })}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (ts.bold) return
|
||||||
|
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||||
|
e.currentTarget.style.color = 'var(--accent-light)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (ts.bold) return
|
||||||
|
e.currentTarget.style.background = 'var(--bg-input)'
|
||||||
|
e.currentTarget.style.color = 'var(--text-primary)'
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
flex: 1, height: BAR_H,
|
||||||
|
background: ts.bold ? 'var(--accent)' : 'var(--bg-input)',
|
||||||
|
color: ts.bold ? 'var(--bg-panel)' : 'var(--text-primary)',
|
||||||
|
border: 'none', cursor: 'pointer',
|
||||||
|
fontWeight: 700, fontSize: 11,
|
||||||
|
transition: 'background 0.15s, color 0.15s',
|
||||||
|
}}
|
||||||
|
title="Fett"
|
||||||
|
>B</button>
|
||||||
|
<button
|
||||||
|
onClick={() => updateTs({ italic: !ts.italic })}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (ts.italic) return
|
||||||
|
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||||
|
e.currentTarget.style.color = 'var(--accent-light)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (ts.italic) return
|
||||||
|
e.currentTarget.style.background = 'var(--bg-input)'
|
||||||
|
e.currentTarget.style.color = 'var(--text-primary)'
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
flex: 1, height: BAR_H,
|
||||||
|
background: ts.italic ? 'var(--accent)' : 'var(--bg-input)',
|
||||||
|
color: ts.italic ? 'var(--bg-panel)' : 'var(--text-primary)',
|
||||||
|
border: 'none', borderLeft: '1px solid var(--border)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontStyle: 'italic', fontSize: 11,
|
||||||
|
transition: 'background 0.15s, color 0.15s',
|
||||||
|
}}
|
||||||
|
title="Kursiv"
|
||||||
|
>I</button>
|
||||||
|
</div>
|
||||||
|
{/* Reihe 2, Spalte 2: "Text einfuegen" Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => createText()}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||||
|
e.currentTarget.style.borderColor = 'var(--accent)'
|
||||||
|
e.currentTarget.style.color = 'var(--accent-light)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'var(--bg-input)'
|
||||||
|
e.currentTarget.style.borderColor = 'var(--border)'
|
||||||
|
e.currentTarget.style.color = 'var(--text-primary)'
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: 90, height: BAR_H,
|
||||||
|
background: 'var(--bg-input)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 999,
|
||||||
|
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
gap: 4, cursor: 'pointer',
|
||||||
|
fontSize: 11, fontWeight: 500,
|
||||||
|
transition: 'background 0.15s, color 0.15s, border-color 0.15s',
|
||||||
|
}}
|
||||||
|
title="Position picken → Text tippen → Enter"
|
||||||
|
>
|
||||||
|
<Icon name="add" size={12} />
|
||||||
|
Text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Snap-Toggles (Ortho/Grid/OSnap) sind in Rhinos eigener Footer-Bar
|
{/* Snap-Toggles (Ortho/Grid/OSnap) sind in Rhinos eigener Footer-Bar
|
||||||
schon vorhanden — hier rausgenommen um Doppelung zu vermeiden. */}
|
schon vorhanden — hier rausgenommen um Doppelung zu vermeiden. */}
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,8 @@ export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name
|
|||||||
export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) }
|
export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) }
|
||||||
export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) }
|
export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) }
|
||||||
export function openAbout() { send('OPEN_ABOUT', {}) }
|
export function openAbout() { send('OPEN_ABOUT', {}) }
|
||||||
|
export function createText() { send('CREATE_TEXT', {}) }
|
||||||
|
export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) }
|
||||||
|
|
||||||
// --- Masse (in Oberleiste + Satellite-Fenster MasseSettings) ---
|
// --- Masse (in Oberleiste + Satellite-Fenster MasseSettings) ---
|
||||||
// Topbar: aktives Mass setzen + Settings-Fenster oeffnen
|
// Topbar: aktives Mass setzen + Settings-Fenster oeffnen
|
||||||
|
|||||||
Reference in New Issue
Block a user