feat(ui): refresh branding and account identity copying

Replace every active application icon asset with the new high-resolution artwork, including transparent PNG, complete Windows ICO sizes, and the documentation screenshot.

Turn populated username and email cells into non-selectable copy actions backed by validated native Electron clipboard IPC while keeping empty cells and row interactions unchanged.
This commit is contained in:
Sucukdeluxe
2026-08-14 21:56:25 +02:00
parent b80209a10f
commit 3974853634
13 changed files with 124 additions and 2 deletions
+18
View File
@@ -37,6 +37,7 @@ function validatePlainObject(value: unknown, name: string): Record<string, unkno
}
const IMPORT_QUEUE_MAX_BYTES = 10 * 1024 * 1024;
const CLIPBOARD_WRITE_MAX_BYTES = 4096;
const RENAME_PACKAGE_MAX_CHARS = 240;
const RESETTABLE_PROVIDER_KEYS = new Set<DebridProvider>([
"realdebrid",
@@ -611,6 +612,23 @@ function registerIpcHandlers(): void {
updateClipboardWatcher();
return next;
});
handleTrusted(IPC_CHANNELS.WRITE_CLIPBOARD_TEXT, (_event: IpcMainInvokeEvent, rawText: unknown) => {
const text = validateString(rawText, "text");
const bytes = Buffer.byteLength(text, "utf8");
if (!text.trim()) {
throw new Error("text darf nicht leer sein");
}
if (bytes > CLIPBOARD_WRITE_MAX_BYTES) {
throw new Error(`text ist zu groß (max ${CLIPBOARD_WRITE_MAX_BYTES} Bytes)`);
}
try {
clipboard.writeText(text);
return true;
} catch (error) {
logger.warn(`Zwischenablage-Schreibfehler: bytes=${bytes}, error=${String(error)}`);
throw error;
}
});
handleTrusted(IPC_CHANNELS.PICK_FOLDER, async () => {
const options = {
properties: ["openDirectory", "createDirectory"] as Array<"openDirectory" | "createDirectory">
+1
View File
@@ -65,6 +65,7 @@ const api: ElectronApi = {
exportQueue: (): Promise<{ saved: boolean }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_QUEUE),
importQueue: (json: string): Promise<{ addedPackages: number; addedLinks: number }> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_QUEUE, json),
toggleClipboard: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.TOGGLE_CLIPBOARD),
writeClipboardText: (text: string): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.WRITE_CLIPBOARD_TEXT, text),
pickFolder: (): Promise<string | null> => ipcRenderer.invoke(IPC_CHANNELS.PICK_FOLDER),
pickContainers: (): Promise<string[]> => ipcRenderer.invoke(IPC_CHANNELS.PICK_CONTAINERS),
getSessionStats: (): Promise<SessionStats> => ipcRenderer.invoke(IPC_CHANNELS.GET_SESSION_STATS),
+5
View File
@@ -4922,6 +4922,11 @@ export function App(): ReactElement {
const row = accountRowBindings.get(rowId);
if (row) setAccountContextMenu({ x, y, rowId });
},
onCopyIdentity: (label, value) => {
void window.rd.writeClipboardText(value)
.then(() => showToast(`${label} kopiert`))
.catch(() => showToast("Kopieren fehlgeschlagen"));
},
onAdd: openCreateAccountDialog,
onRemoveSelected: () => {
const row = selectedAccountViewId ? accountRowBindings.get(selectedAccountViewId) : null;
@@ -74,6 +74,7 @@ export interface AccountWorkspaceActions {
onToggleEnabled: (rowId: string) => void;
onEdit: (rowId: string) => void;
onContextMenu: (rowId: string, x: number, y: number) => void;
onCopyIdentity: (label: "Benutzername" | "E-Mail", value: string) => void;
onAdd: () => void;
onRemoveSelected: () => void;
onCheckAll: () => void;
@@ -91,6 +92,36 @@ export interface AccountWorkspaceActions {
onRoutingAdd?: (hosterId: string) => void;
}
function renderAccountIdentityCell({
className,
label,
value,
onCopy
}: {
className: string;
label: "Benutzername" | "E-Mail";
value: string;
onCopy: (label: "Benutzername" | "E-Mail", value: string) => void;
}): ReactElement {
const copyable = value.trim() !== "" && value !== "—";
return (
<span className={className} role="cell" title={copyable ? `${value}\nKlicken zum Kopieren` : value}>
{copyable ? (
<button
aria-label={`${label} kopieren`}
className="settings-account-copy-button"
onClick={(event) => {
event.stopPropagation();
onCopy(label, value);
}}
onDoubleClick={(event) => event.stopPropagation()}
type="button"
>{value}</button>
) : value}
</span>
);
}
export interface AccountWorkspaceProps {
model: AccountWorkspaceViewModel;
actions: AccountWorkspaceActions;
@@ -240,8 +271,8 @@ function AccountRow({
<span className={`settings-account-status-badge is-${row.status.tone}`}>{row.status.text}</span>
</span>
<span className="settings-account-traffic" role="cell">{row.traffic}</span>
<span className="settings-account-username settings-copyable" role="cell" title={row.username}>{row.username}</span>
<span className="settings-account-email settings-copyable" role="cell" title={row.email}>{row.email}</span>
{renderAccountIdentityCell({ className: "settings-account-username", label: "Benutzername", onCopy: actions.onCopyIdentity, value: row.username })}
{renderAccountIdentityCell({ className: "settings-account-email", label: "E-Mail", onCopy: actions.onCopyIdentity, value: row.email })}
<span className="settings-account-expires" role="cell">{row.expires}</span>
<span className="settings-account-credential" role="cell">{row.credential}</span>
<span className="settings-account-column-actions" role="cell">
+21
View File
@@ -732,6 +732,27 @@
white-space: nowrap;
}
.settings-account-copy-button {
display: block;
width: 100%;
padding: 0;
overflow: hidden;
border: 0;
background: transparent;
color: inherit;
cursor: copy;
font: inherit;
text-align: left;
text-overflow: ellipsis;
user-select: none;
white-space: nowrap;
}
.settings-account-copy-button:hover,
.settings-account-copy-button:focus-visible {
color: var(--ui-accent);
}
.settings-account-action-button {
display: grid;
width: 30px;
+1
View File
@@ -35,6 +35,7 @@ export const IPC_CHANNELS = {
STATE_UPDATE: "state:update",
CLIPBOARD_DETECTED: "clipboard:detected",
TOGGLE_CLIPBOARD: "clipboard:toggle",
WRITE_CLIPBOARD_TEXT: "clipboard:write-text",
GET_SESSION_STATS: "stats:get-session-stats",
RESET_SESSION_STATS: "stats:reset-session",
RESET_DOWNLOAD_STATS: "stats:reset-download",
+1
View File
@@ -62,6 +62,7 @@ export interface ElectronApi {
exportQueue: () => Promise<{ saved: boolean }>;
importQueue: (json: string) => Promise<{ addedPackages: number; addedLinks: number }>;
toggleClipboard: () => Promise<boolean>;
writeClipboardText: (text: string) => Promise<boolean>;
pickFolder: () => Promise<string | null>;
pickContainers: () => Promise<string[]>;
getSessionStats: () => Promise<SessionStats>;