The main feature (copy selection to the other panel, F5) is now labeled "Send" in the toolbar button, tooltips, and context menu across all 7 locales. Internal identifiers (copyToOtherPanel, ctx-copy, i18n keys) and secondary strings (toasts, status bar, errors) are unchanged. The context-menu entry moves ahead of Open/Edit as the first row item. README, marketing site, and the Windows QA checklist updated to match. Co-Authored-By: Claude <noreply@anthropic.com>
129 lines
4.1 KiB
TypeScript
129 lines
4.1 KiB
TypeScript
import { ContextMenu, Utils, type ApplicationMenuItemConfig } from "electrobun/bun";
|
|
import type { ColumnKey } from "../shared/types";
|
|
import { t } from "./i18n";
|
|
|
|
/** Optional list columns, in display order (Name is fixed). */
|
|
const COLUMN_KEYS: ColumnKey[] = ["size", "mtime", "kind", "rights", "owner", "group"];
|
|
|
|
// PORTABILITY: shell-integration wording per OS (resolved at show() for locale)
|
|
const IS_WIN = process.platform === "win32";
|
|
const revealLabel = (): string =>
|
|
t(
|
|
process.platform === "darwin"
|
|
? "ctx.revealFinder"
|
|
: IS_WIN
|
|
? "ctx.revealExplorer"
|
|
: "ctx.revealFileManager",
|
|
);
|
|
const trashLabel = (): string => t(IS_WIN ? "ctx.moveToRecycleBin" : "ctx.moveToTrash");
|
|
|
|
export interface RowMenuContext {
|
|
path: string;
|
|
name: string;
|
|
type: "file" | "dir" | "symlink";
|
|
isLocal: boolean;
|
|
hasEntry: boolean; // false = right-click on empty panel space
|
|
}
|
|
|
|
/**
|
|
* Native right-click menus for the file panels. Local-only items run directly
|
|
* here via Utils (reveal, open with default app, copy path); everything else
|
|
* forwards to the webview through the menuAction channel — the UI kept its own
|
|
* context (which panel/row was clicked) when it requested the menu.
|
|
*/
|
|
export class PanelContextMenu {
|
|
private context: RowMenuContext | null = null;
|
|
/** Forwards actions the UI must handle (ctx-open, ctx-delete, ...). */
|
|
onAction: (action: string) => void = () => {};
|
|
|
|
constructor() {
|
|
ContextMenu.on("context-menu-clicked", (event) => {
|
|
const action = (event as { data?: { action?: string } }).data?.action;
|
|
if (!action) return;
|
|
// column toggles carry no row context
|
|
if (action.startsWith("col:")) {
|
|
this.onAction(action);
|
|
return;
|
|
}
|
|
const ctx = this.context;
|
|
if (!ctx) return;
|
|
switch (action) {
|
|
case "ctx-reveal":
|
|
Utils.showItemInFolder(ctx.path);
|
|
break;
|
|
case "ctx-open-default":
|
|
Utils.openPath(ctx.path);
|
|
break;
|
|
case "ctx-copy-path":
|
|
Utils.clipboardWriteText(ctx.path);
|
|
break;
|
|
default:
|
|
if (action.startsWith("ctx-")) this.onAction(action);
|
|
}
|
|
});
|
|
}
|
|
|
|
show(context: RowMenuContext): void {
|
|
this.context = context;
|
|
ContextMenu.showContextMenu(this.build(context));
|
|
}
|
|
|
|
/** Column picker for the list headers: checkable toggles per column. */
|
|
showColumns(visible: ColumnKey[]): void {
|
|
ContextMenu.showContextMenu(
|
|
COLUMN_KEYS.map((key) => ({
|
|
label: t(`columns.${key}`),
|
|
action: `col:${key}`,
|
|
checked: visible.includes(key),
|
|
})),
|
|
);
|
|
}
|
|
|
|
private build(ctx: RowMenuContext): ApplicationMenuItemConfig[] {
|
|
const items: ApplicationMenuItemConfig[] = [];
|
|
|
|
if (!ctx.hasEntry || ctx.name === "..") {
|
|
// empty space (or the parent row): folder-level actions
|
|
items.push({ label: t("ctx.newFolder"), action: "ctx-new-folder" });
|
|
items.push({ label: t("ctx.newFile"), action: "ctx-new-file" });
|
|
items.push({ type: "separator" });
|
|
items.push({ label: t("ctx.refresh"), action: "ctx-refresh" });
|
|
if (ctx.isLocal) {
|
|
items.push({ type: "separator" });
|
|
items.push({ label: revealLabel(), action: "ctx-reveal" });
|
|
}
|
|
return items;
|
|
}
|
|
|
|
items.push({ label: t("ctx.copyToOther"), action: "ctx-copy" });
|
|
if (ctx.type === "dir") {
|
|
items.push({ label: t("ctx.open"), action: "ctx-open" });
|
|
} else {
|
|
if (ctx.isLocal) {
|
|
items.push({ label: t("ctx.open"), action: "ctx-open-default", tooltip: t("ctx.openDefault") });
|
|
}
|
|
items.push({ label: t("ctx.edit"), action: "ctx-edit", accelerator: "e" });
|
|
}
|
|
items.push({ label: t("ctx.rename"), action: "ctx-rename" });
|
|
// chmod on Windows local files only flips the read-only bit — hide the lie
|
|
if (!(ctx.isLocal && IS_WIN)) {
|
|
items.push({ label: t("ctx.permissions"), action: "ctx-chmod" });
|
|
}
|
|
items.push({ type: "separator" });
|
|
items.push({ label: t("ctx.copyPath"), action: "ctx-copy-path" });
|
|
if (ctx.isLocal) {
|
|
items.push({ label: revealLabel(), action: "ctx-reveal" });
|
|
}
|
|
items.push({ type: "separator" });
|
|
if (ctx.isLocal) {
|
|
items.push({
|
|
label: trashLabel(),
|
|
action: "ctx-trash",
|
|
...(process.platform === "darwin" ? { tooltip: t("ctx.trashTooltip") } : {}),
|
|
});
|
|
}
|
|
items.push({ label: t("ctx.delete"), action: "ctx-delete" });
|
|
return items;
|
|
}
|
|
}
|