Custom signal-based runtime (no i18n dependency): shared src/shared/i18n dictionaries, ui I18nService.t() reactive by construction so language switches live, and a backend i18n.ts for native menus + thrown errors. Every user-facing string keyed across templates, toasts, dialogs, file kinds, columns, macOS menu/context menus, and errors. Language picker in Options (auto-detects OS locale, falls back to English); switching relabels the webview and native chrome instantly. English/Spanish/French/ German fully drafted; Chinese/Japanese/Basque drafted, flagged for native review. Docs updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
130 lines
4.3 KiB
TypeScript
130 lines
4.3 KiB
TypeScript
import { Component, computed, inject, input, signal } from '@angular/core';
|
|
import { I18nService } from '../i18n.service';
|
|
import { DialogsService, type ChmodRequest } from '../dialogs.service';
|
|
import { FocusService } from '../focus.service';
|
|
|
|
const ROLES = ['roleOwner', 'roleGroup', 'roleOthers'] as const;
|
|
const PERMS = ['r', 'w', 'x'] as const;
|
|
|
|
@Component({
|
|
selector: 'app-chmod-dialog',
|
|
template: `
|
|
<div class="fixed inset-0 z-40 grid place-items-center bg-black/40" (mousedown)="cancel()">
|
|
<div
|
|
class="w-80 rounded-lg border border-border bg-card p-4 text-card-foreground shadow-xl"
|
|
(mousedown)="$event.stopPropagation()"
|
|
(keydown.escape)="cancel()"
|
|
>
|
|
<h2 class="text-sm font-semibold">{{ i18n.t('dialogs.chmodTitle') }}</h2>
|
|
<p class="mt-1 truncate text-xs text-muted-foreground">{{ targetLabel() }}</p>
|
|
|
|
<table class="mt-3 w-full text-xs">
|
|
<thead>
|
|
<tr class="text-muted-foreground">
|
|
<th class="pb-1 text-left font-normal"></th>
|
|
<th class="pb-1 font-normal">{{ i18n.t('dialogs.read') }}</th>
|
|
<th class="pb-1 font-normal">{{ i18n.t('dialogs.write') }}</th>
|
|
<th class="pb-1 font-normal">{{ i18n.t('dialogs.execute') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (role of roles; track role; let r = $index) {
|
|
<tr>
|
|
<td class="py-1 pr-2 text-muted-foreground">{{ i18n.t('dialogs.' + role) }}</td>
|
|
@for (perm of perms; track perm; let p = $index) {
|
|
<td class="py-1 text-center">
|
|
<input
|
|
type="checkbox"
|
|
[checked]="hasBit(r, p)"
|
|
(change)="toggleBit(r, p)"
|
|
class="size-3.5 accent-primary"
|
|
/>
|
|
</td>
|
|
}
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="mt-3 flex items-center gap-2">
|
|
<label class="text-xs text-muted-foreground">{{ i18n.t('dialogs.octal') }}</label>
|
|
<input
|
|
[value]="octal()"
|
|
(input)="setOctal($any($event.target).value)"
|
|
maxlength="4"
|
|
spellcheck="false"
|
|
class="h-7 w-20 rounded-md border border-input bg-transparent px-2 font-mono text-xs outline-none focus:ring-2 focus:ring-ring"
|
|
/>
|
|
</div>
|
|
|
|
<div class="mt-4 flex justify-end gap-2">
|
|
<button
|
|
(click)="cancel()"
|
|
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
|
>
|
|
{{ i18n.t('common.cancel') }}
|
|
</button>
|
|
<button
|
|
(click)="submit()"
|
|
class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
|
>
|
|
{{ i18n.t('common.apply') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
})
|
|
export class ChmodDialog {
|
|
readonly i18n = inject(I18nService);
|
|
readonly request = input.required<ChmodRequest>();
|
|
|
|
readonly roles = ROLES;
|
|
readonly perms = PERMS;
|
|
|
|
private readonly dialogs = inject(DialogsService);
|
|
private readonly focus = inject(FocusService);
|
|
|
|
// mode is initialized lazily from the request on first read
|
|
private readonly modeOverride = signal<number | null>(null);
|
|
readonly mode = computed(() => this.modeOverride() ?? this.request().initialMode & 0o777);
|
|
readonly octal = computed(() => this.mode().toString(8).padStart(3, '0'));
|
|
|
|
targetLabel(): string {
|
|
const targets = this.request().targets;
|
|
return targets.length === 1
|
|
? targets[0]
|
|
: this.i18n.t('dialogs.chmodItems', { count: targets.length });
|
|
}
|
|
|
|
hasBit(role: number, perm: number): boolean {
|
|
return !!(this.mode() & (0o400 >> (role * 3 + perm)));
|
|
}
|
|
|
|
toggleBit(role: number, perm: number): void {
|
|
this.modeOverride.set(this.mode() ^ (0o400 >> (role * 3 + perm)));
|
|
}
|
|
|
|
setOctal(text: string): void {
|
|
if (/^[0-7]{1,4}$/.test(text)) {
|
|
this.modeOverride.set(parseInt(text, 8) & 0o777);
|
|
}
|
|
}
|
|
|
|
submit(): void {
|
|
const req = this.request();
|
|
const mode = this.mode();
|
|
this.close();
|
|
req.onConfirm(mode);
|
|
}
|
|
|
|
cancel(): void {
|
|
this.close();
|
|
}
|
|
|
|
private close(): void {
|
|
this.dialogs.chmod.set(null);
|
|
this.focus.bump();
|
|
}
|
|
}
|