feat(v5): eigenes Fenster ohne Windows-Rahmen + Layout skaliert mit
Ampel / ampel (push) Successful in 1m34s
Ampel / ampel (push) Successful in 1m34s
Zwei Befunde des Commanders (01.09.2026):
1. "Momentan ist alles nach links gerueckt, nichts skaliert mit der
fenstergroesse und es ist alles gequetscht."
Ursache gefunden: <main> hatte WEDER Zentrierung NOCH Maximalbreite,
die Karten darin aber einen harten Deckel von max-w-2xl (672 px). In
einem 1280er Fenster klebte damit alles am linken Rand und rechts
stand nichts. Behoben:
* <main> und Kopfzeile teilen sich mx-auto max-w-[1600px] mit
mitwachsendem Innenabstand — der Inhalt ist mittig und nutzt die
Breite.
* Der 672-px-Deckel der Einstellungs-Spalte ist weg. Drei
Encoder-Felder stehen jetzt nebeneinander statt untereinander.
* Die Sektionswahl der Einstellungen wird im schmalen Fenster zur
Zeile ueber den Karten (lg:flex-row), statt eine 176-px-Spalte vom
ohnehin knappen Platz abzuziehen.
* Info-Kacheln der Laufwerks-Karte: zwei Spalten schmal, vier breit.
* Startgroesse 1000x700 -> 1280x820, Mindestmass 720x480 -> 860x560.
2. "waere sogar richtig cool wenn du den rahmen wegbekommst"
frame:false, und die Kopfzeile IST jetzt die Titelleiste:
Minimieren / Maximieren / Schliessen sitzen oben rechts in
Windows-Massen (46x32), gezogen wird an der Kopfzeile, Doppelklick
maximiert. Groesse aendern bleibt moeglich — Electron setzt fuer
rahmenlose Fenster unter Windows weiter WS_THICKFRAME (thickFrame,
Standard true), die Kanten ziehen also wie gewohnt.
Schliessen nimmt GENAU den Weg des alten Fensterkreuzes
(fenster.close() -> der bestehende close-Horcher versteckt nur):
Rippy lebt im Tray weiter, ein laufender Rip merkt davon nichts
(Paragraph 4.1 Punkt 3).
Die Ziehflaeche ist .ziehbar in stil.css; alles Bedienbare darin
traegt .nicht-ziehbar, sonst verschluckt der Griff den Klick.
fensterMaximiert im HauptStatus entscheidet ueber das Knopf-Symbol
und wird auch bei Doppelklick/Tastenkuerzel nachgefuehrt (maximize-
und unmaximize-Horcher).
Nachgemessen mit dem Smoke-Beweis: beweise/ui-513-uebersicht.png (kein
Rahmen, eigene Knoepfe, Inhalt ueber die volle Breite) und
beweise/ui-513-einstellungen.png (drei Encoder nebeneinander).
230 Tests gruen, Typpruefung sauber. Version 5.1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0c38754c09
commit
827910fd63
Binary file not shown.
|
After Width: | Height: | Size: 295 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 239 KiB |
@@ -1,17 +1,25 @@
|
||||
Version 5.1.2
|
||||
Version 5.1.3
|
||||
|
||||
Neu: Rippy sagt Bescheid, wenn ein Update da ist.
|
||||
Bisher hat es sich still im Hintergrund aktualisiert — jetzt steht eine
|
||||
Karte oben in der Übersicht, mit Versionsnummer und dieser Liste hier.
|
||||
In den Einstellungen gibt es dazu den Knopf "Jetzt auf Updates prüfen".
|
||||
Wer nicht warten will, startet über die Karte sofort neu.
|
||||
Das Fenster gehört jetzt Rippy.
|
||||
Der graue Windows-Rahmen ist weg. Minimieren, Maximieren und Schließen
|
||||
sitzen oben rechts im Programm selbst, gezogen wird an der Kopfzeile —
|
||||
Doppelklick darauf maximiert. Die Kanten lassen sich weiter ziehen wie
|
||||
gewohnt. Schließen versteckt Rippy weiterhin nur im Infobereich, ein
|
||||
laufender Rip merkt davon nichts.
|
||||
|
||||
Nichts klebt mehr links.
|
||||
Die Seite ist jetzt mittig und wächst mit dem Fenster mit, statt auf
|
||||
halber Breite abzubrechen. Im schmalen Fenster rutschen die Kacheln
|
||||
untereinander, statt sich zu quetschen; die Einstellungen legen ihre
|
||||
Abschnittswahl dann als Zeile über die Karten.
|
||||
|
||||
Aus 5.1.2: Rippy sagt Bescheid, wenn ein Update da ist.
|
||||
Diese Karte hier ist der Beweis — mit Versionsnummer und dieser Liste.
|
||||
In den Einstellungen gibt es den Knopf "Jetzt auf Updates prüfen".
|
||||
|
||||
Aus 5.1.1: Serien-Discs werden richtig erkannt.
|
||||
Disc 2 von "Spartacus: Gods of the Arena" wurde als der Film "Spartacus"
|
||||
von 1960 erkannt. Rippy fragt jetzt die Disc selbst: Ihr
|
||||
Inhaltsverzeichnis nennt die Folgen, und daran erkennt Rippy eine Serie,
|
||||
statt sie zu raten. Disc-Nummer und Folgenzahl stehen neben dem Titel.
|
||||
Und wenn Rippy sich unsicher ist, sagt es das jetzt, statt sich sicher
|
||||
zu irren.
|
||||
Disc 2 von "Spartacus: Gods of the Arena" galt als der Film "Spartacus"
|
||||
von 1960. Rippy fragt jetzt die Disc selbst: Ihr Inhaltsverzeichnis
|
||||
nennt die Folgen. Disc-Nummer und Folgenzahl stehen neben dem Titel.
|
||||
|
||||
Unverändert: Updates warten immer, bis kein Rip mehr läuft.
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "rippy",
|
||||
"version": "5.1.2",
|
||||
"version": "5.1.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rippy",
|
||||
"version": "5.1.2",
|
||||
"version": "5.1.3",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"electron-updater": "^6.8.9",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rippy",
|
||||
"productName": "Rippy",
|
||||
"version": "5.1.2",
|
||||
"version": "5.1.3",
|
||||
"description": "Rippy v5 — Disc-Ripping als eigenständiges Windows-Programm (KONZEPT-WINDOWS.md)",
|
||||
"main": "out/haupt/index.js",
|
||||
"author": "KrBrZ",
|
||||
|
||||
@@ -56,6 +56,11 @@ contextBridge.exposeInMainWorld('rippy', {
|
||||
autostartSetzen(an: boolean): Promise<boolean> {
|
||||
return ipcRenderer.invoke('autostart-setzen', an === true) as Promise<boolean>
|
||||
},
|
||||
/** Eigene Titelleiste (§ 4.2): minimieren | maximieren | schliessen.
|
||||
* „schliessen" versteckt nur — Rippy lebt im Tray weiter. */
|
||||
fensterSteuern(was: 'minimieren' | 'maximieren' | 'schliessen'): Promise<boolean> {
|
||||
return ipcRenderer.invoke('fenster-steuern', String(was)) as Promise<boolean>
|
||||
},
|
||||
/** „Jetzt prüfen" (§ 6.8) — das Ergebnis kommt über den Haupt-Status. */
|
||||
updatePruefen(): Promise<unknown> {
|
||||
return ipcRenderer.invoke('update-pruefen') as Promise<unknown>
|
||||
|
||||
@@ -83,6 +83,60 @@ const PHASEN_MONO: Record<RipStatus['phase'], string> = {
|
||||
|
||||
const LAUFENDE_PHASEN = ['liest-info', 'rippt', 'komprimiert', 'legt-ab']
|
||||
|
||||
// ── Eigene Fensterknöpfe (§ 4.2, seit 5.1.3) ────────────────────────────
|
||||
// Bewusst schlicht und in Windows-Maßen (46 × 32) — wer sie sucht, findet
|
||||
// sie dort, wo er sie erwartet. Sie sitzen in der Ziehfläche und müssen
|
||||
// deshalb `nicht-ziehbar` sein, sonst verschluckt der Fenstergriff den Klick.
|
||||
const IconMinimieren = (): React.JSX.Element => (
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
||||
<path d="M0 5h10" stroke="currentColor" strokeWidth="1.1" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const IconMaximieren = (): React.JSX.Element => (
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">
|
||||
<rect x="0.6" y="0.6" width="8.8" height="8.8" stroke="currentColor" strokeWidth="1.1" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const IconWiederherstellen = (): React.JSX.Element => (
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">
|
||||
<rect x="0.6" y="2.6" width="6.8" height="6.8" stroke="currentColor" strokeWidth="1.1" />
|
||||
<path d="M2.7 2.6V0.6h6.7v6.7h-2" stroke="currentColor" strokeWidth="1.1" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const IconSchliessen = (): React.JSX.Element => (
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
||||
<path d="M0.6 0.6l8.8 8.8M9.4 0.6l-8.8 8.8" stroke="currentColor" strokeWidth="1.1" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
function FensterKnopf({
|
||||
titel,
|
||||
aufKlick,
|
||||
rot = false,
|
||||
children,
|
||||
}: {
|
||||
titel: string
|
||||
aufKlick: () => void
|
||||
rot?: boolean
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={aufKlick}
|
||||
title={titel}
|
||||
aria-label={titel}
|
||||
className={`flex h-8 w-[46px] items-center justify-center text-dunst transition-colors ${
|
||||
rot ? 'hover:bg-rot hover:text-white' : 'hover:bg-white/10 hover:text-schnee'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function sicherheitsWort(confidence: number): string {
|
||||
if (confidence >= 0.9) return 'sehr wahrscheinlich'
|
||||
if (confidence >= 0.8) return 'wahrscheinlich'
|
||||
@@ -427,7 +481,9 @@ function LaufwerkKachel({
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-3 grid max-w-2xl grid-cols-4 gap-2.5">
|
||||
{/* Zwei Kacheln nebeneinander im schmalen Fenster, vier im
|
||||
breiten — vorher immer vier, also im schmalen gequetscht. */}
|
||||
<div className="mt-3 grid max-w-3xl grid-cols-2 gap-2.5 sm:grid-cols-4">
|
||||
<InfoKachel label="MEDIUM" wert={TYP_NAMEN[geraet.typ]} />
|
||||
<InfoKachel label="GRÖSSE" wert={geraet.groesseBytes > 0 ? gb(geraet.groesseBytes) : '—'} />
|
||||
<InfoKachel
|
||||
@@ -807,9 +863,12 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
|
||||
if (ordner !== null) setzen('ablage', ordner)
|
||||
}
|
||||
|
||||
// Schmales Fenster: die Sektionswahl legt sich als Zeile über die Karten.
|
||||
// Ab lg steht sie wieder als mitlaufende Spalte daneben (Commander-Fund
|
||||
// 01.09.2026: „alles gequetscht").
|
||||
return (
|
||||
<div className="flex gap-8">
|
||||
<nav className="sticky top-4 flex h-fit w-44 flex-none flex-col gap-1">
|
||||
<div className="flex flex-col gap-5 lg:flex-row lg:gap-8">
|
||||
<nav className="flex flex-row flex-wrap gap-1 lg:sticky lg:top-4 lg:h-fit lg:w-44 lg:flex-none lg:flex-col">
|
||||
{SEKTIONEN.map(([id, name]) => (
|
||||
<button
|
||||
key={id}
|
||||
@@ -825,7 +884,9 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="min-w-0 max-w-2xl flex-1 space-y-5">
|
||||
{/* Kein max-w mehr: Der 672-px-Deckel war der Grund, warum in einem
|
||||
breiten Fenster alles links klebte und rechts nichts stand. */}
|
||||
<div className="min-w-0 flex-1 space-y-5">
|
||||
<SektionsKarte
|
||||
id="ablage"
|
||||
titel="ABLAGE"
|
||||
@@ -1395,30 +1456,53 @@ export default function App() {
|
||||
<div className="buehnenlicht pointer-events-none fixed inset-0" />
|
||||
<div className="filmkorn pointer-events-none fixed inset-0" />
|
||||
|
||||
<header className="relative border-b border-white/8 px-7 py-3.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3.5">
|
||||
<IconDisc groesse={30} farbe="oklch(0.83 0.11 75)" />
|
||||
<h1 className="font-display text-[25px] font-bold tracking-tight">Rippy</h1>
|
||||
<span className="rounded-md border border-white/12 px-2 py-0.5 font-mono text-[10px] text-nebel">
|
||||
{/* Die Kopfzeile IST die Titelleiste: Sie ist die Ziehfläche des
|
||||
rahmenlosen Fensters (§ 4.2). Alles Bedienbare darin trägt
|
||||
`nicht-ziehbar`, sonst schluckt der Griff den Klick. */}
|
||||
<header className="ziehbar relative flex-none border-b border-white/8">
|
||||
<div className="mx-auto flex w-full max-w-[1600px] items-center gap-3 py-2 pl-5 pr-0 sm:pl-7">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<IconDisc groesse={28} farbe="oklch(0.83 0.11 75)" />
|
||||
<h1 className="font-display text-[22px] font-bold tracking-tight sm:text-[25px]">Rippy</h1>
|
||||
<span className="hidden rounded-md border border-white/12 px-2 py-0.5 font-mono text-[10px] text-nebel md:inline">
|
||||
v{haupt?.version ?? '…'}
|
||||
</span>
|
||||
{einRipLaeuft && (
|
||||
<span className="inline-flex items-center gap-2 rounded-md border border-rec/45 bg-rec/12 px-2.5 py-0.5 font-mono text-[10px] tracking-[0.15em] text-[oklch(0.83_0.12_50)]">
|
||||
<span className="inline-flex flex-none items-center gap-2 rounded-md border border-rec/45 bg-rec/12 px-2.5 py-0.5 font-mono text-[10px] tracking-[0.15em] text-[oklch(0.83_0.12_50)]">
|
||||
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-rec shadow-[0_0_9px_oklch(0.72_0.17_45/0.9)]" />
|
||||
REC
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<nav className="flex gap-1.5">
|
||||
<nav className="nicht-ziehbar ml-auto flex flex-wrap justify-end gap-1.5">
|
||||
{knopf('uebersicht', 'Übersicht')}
|
||||
{knopf('einstellungen', 'Einstellungen')}
|
||||
{knopf('bibliothek', `Bibliothek${verbindung.bibliothek.length > 0 ? ` (${verbindung.bibliothek.length})` : ''}`)}
|
||||
</nav>
|
||||
<div className="nicht-ziehbar ml-1 flex flex-none items-center self-start">
|
||||
<FensterKnopf titel="Minimieren" aufKlick={() => void window.rippy.fensterSteuern('minimieren')}>
|
||||
<IconMinimieren />
|
||||
</FensterKnopf>
|
||||
<FensterKnopf
|
||||
titel={haupt?.fensterMaximiert === true ? 'Wiederherstellen' : 'Maximieren'}
|
||||
aufKlick={() => void window.rippy.fensterSteuern('maximieren')}
|
||||
>
|
||||
{haupt?.fensterMaximiert === true ? <IconWiederherstellen /> : <IconMaximieren />}
|
||||
</FensterKnopf>
|
||||
{/* Schließen versteckt nur — Rippy lebt im Tray weiter, ein
|
||||
laufender Rip merkt davon nichts (§ 4.1 Punkt 3). */}
|
||||
<FensterKnopf
|
||||
titel="In den Infobereich (Rippy läuft weiter)"
|
||||
rot
|
||||
aufKlick={() => void window.rippy.fensterSteuern('schliessen')}
|
||||
>
|
||||
<IconSchliessen />
|
||||
</FensterKnopf>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="relative flex-1 space-y-7 p-7">
|
||||
<main className="relative mx-auto w-full max-w-[1600px] flex-1 space-y-6 px-5 py-6 sm:px-7 sm:py-7">
|
||||
{setupOffen && <ErstEinrichtung verbindung={verbindung} />}
|
||||
{!setupOffen && bereich === 'uebersicht' && (
|
||||
<>
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ declare global {
|
||||
autostartSetzen(an: boolean): Promise<boolean>
|
||||
updatePruefen(): Promise<unknown>
|
||||
updateInstallieren(): Promise<{ ok: boolean; grund: string }>
|
||||
fensterSteuern(was: 'minimieren' | 'maximieren' | 'schliessen'): Promise<boolean>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,18 @@
|
||||
box-shadow: 0 0 8px oklch(0.8 0.12 70 / 0.45);
|
||||
}
|
||||
|
||||
/* Eigene Titelleiste (§ 4.2): Das Fenster hat seit 5.1.3 keinen
|
||||
Windows-Rahmen mehr (frame:false) — gezogen wird an der Kopfzeile.
|
||||
`drag` macht eine Fläche zum Fenstergriff; ALLES Bedienbare darin
|
||||
braucht `nicht-ziehbar`, sonst verschluckt der Griff den Klick.
|
||||
Doppelklick auf den Griff maximiert, das macht Windows von selbst. */
|
||||
.ziehbar {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
.nicht-ziehbar {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* Dezente Scrollbalken — helle Standardbalken zerschneiden die Bühne. */
|
||||
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||
::-webkit-scrollbar-thumb { background: oklch(1 0 0 / 0.12); border-radius: 999px; border: 2px solid transparent; background-clip: content-box; }
|
||||
|
||||
@@ -191,6 +191,9 @@ export interface HauptStatus {
|
||||
autostart: boolean
|
||||
/** Selbst-Update (§ 6.8). */
|
||||
update: UpdateStand
|
||||
/** Für die eigene Titelleiste (§ 4.2, seit 5.1.3): Ist das Fenster
|
||||
* gerade maximiert? Entscheidet nur, welches Symbol der Knopf zeigt. */
|
||||
fensterMaximiert: boolean
|
||||
}
|
||||
|
||||
/** Der Stand des Selbst-Updates (§ 6.8) — EINE Definition, die sich Haupt
|
||||
|
||||
@@ -7,12 +7,21 @@ import { join } from 'node:path'
|
||||
|
||||
export function fensterErstellen(): BrowserWindow {
|
||||
const fenster = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 700,
|
||||
minWidth: 720,
|
||||
minHeight: 480,
|
||||
width: 1280,
|
||||
height: 820,
|
||||
minWidth: 860,
|
||||
minHeight: 560,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
// Eigene Titelleiste (Commander-Wunsch 01.09.2026): Der Windows-Rahmen
|
||||
// ist weg, Minimieren/Maximieren/Schließen sitzen im UI und gezogen
|
||||
// wird an der Kopfzeile (.ziehbar in fenster/stil.css).
|
||||
//
|
||||
// Wichtig: GRÖSSE ÄNDERN bleibt möglich. Electron setzt für rahmenlose
|
||||
// Fenster unter Windows weiterhin WS_THICKFRAME (Option `thickFrame`,
|
||||
// Standard true) — die Kanten lassen sich also weiter ziehen, und
|
||||
// Doppelklick auf die Ziehfläche maximiert wie gewohnt.
|
||||
frame: false,
|
||||
// Dunkler Grund SOFORT — sonst blitzt beim Start ein weißes Rechteck,
|
||||
// bevor React lädt. Das warme Bühnen-Schwarz des Kino-Mixes
|
||||
// (oklch(0.13 0.012 60), siehe fenster/stil.css).
|
||||
|
||||
@@ -176,6 +176,9 @@ function start(): void {
|
||||
kern: kernVerwaltung.zustand(),
|
||||
autostart: autostartAktiv(),
|
||||
update: updateStand(),
|
||||
// Für die eigene Titelleiste: Das Knopf-Symbol muss wissen, ob das
|
||||
// Fenster gerade maximiert ist.
|
||||
fensterMaximiert: fenster !== null && !fenster.isDestroyed() && fenster.isMaximized(),
|
||||
}
|
||||
fenster.webContents.send('haupt-status', status)
|
||||
}
|
||||
@@ -189,6 +192,11 @@ function start(): void {
|
||||
statusSenden()
|
||||
smokePruefen()
|
||||
})
|
||||
// Die eigene Titelleiste zeichnet ihr Maximieren-Symbol nach diesem
|
||||
// Zustand — auch wenn per Doppelklick oder Windows-Tastenkürzel
|
||||
// maximiert wird, nicht nur über unseren Knopf.
|
||||
fenster.on('maximize', () => statusSenden())
|
||||
fenster.on('unmaximize', () => statusSenden())
|
||||
// § 4.1 Punkt 3: Fenster zu, Rip läuft weiter — Rippy lebt im Tray.
|
||||
fenster.on('close', (ereignis) => {
|
||||
if (beendenGewollt) return
|
||||
@@ -223,6 +231,26 @@ function start(): void {
|
||||
}
|
||||
})
|
||||
|
||||
// Eigene Titelleiste (§ 4.2): Das Fenster hat keinen Windows-Rahmen
|
||||
// mehr, die drei Knöpfe sitzen im UI und melden sich hier.
|
||||
ipcMain.handle('fenster-steuern', (_ereignis, was: unknown) => {
|
||||
if (fenster === null || fenster.isDestroyed()) return false
|
||||
if (was === 'minimieren') {
|
||||
fenster.minimize()
|
||||
} else if (was === 'maximieren') {
|
||||
if (fenster.isMaximized()) fenster.unmaximize()
|
||||
else fenster.maximize()
|
||||
} else if (was === 'schliessen') {
|
||||
// Genau der Weg des alten Fensterkreuzes: Der close-Horcher weiter
|
||||
// unten versteckt nur — Rippy lebt im Tray weiter (§ 4.1 Punkt 3),
|
||||
// ein laufender Rip darf davon nichts merken.
|
||||
fenster.close()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return fenster.isDestroyed() ? false : fenster.isMaximized()
|
||||
})
|
||||
|
||||
// Der Knopf „Jetzt prüfen" (§ 6.8). Der Stand meldet sich über
|
||||
// beiAenderung von selbst ans Fenster — hier gibt es nichts zurück.
|
||||
ipcMain.handle('update-pruefen', () => {
|
||||
|
||||
Reference in New Issue
Block a user