234 lines · 7.8 KB · TypeScript
RawDownload
import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Schema from "effect/Schema"; import type * as Electron from "electron"; import { makeComponentLogger } from "../app/DesktopObservability.ts";import * as ElectronApp from "../electron/ElectronApp.ts";import * as ElectronDialog from "../electron/ElectronDialog.ts";import * as ElectronMenu from "../electron/ElectronMenu.ts";import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";import * as DesktopUpdates from "../updates/DesktopUpdates.ts";import * as DesktopWindow from "./DesktopWindow.ts"; export class DesktopApplicationMenuActionError extends Schema.TaggedError<DesktopApplicationMenuActionError>()(  "DesktopApplicationMenuActionError",  {    action: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Desktop menu action "${this.action}" failed.`;  }} export class DesktopApplicationMenu extends Context.Service<  DesktopApplicationMenu,  {    readonly configure: Effect.Effect<void>;  }>()("@t3tools/desktop/window/DesktopApplicationMenu") {} type DesktopApplicationMenuRuntimeServices =  | DesktopUpdates.DesktopUpdates  | DesktopWindow.DesktopWindow  | ElectronDialog.ElectronDialog; const { logInfo: logUpdaterInfo } = makeComponentLogger("desktop-updater"); const { logError: logMenuError } = makeComponentLogger("desktop-menu"); const dispatchMenuAction = Effect.fn("desktop.menu.dispatchMenuAction")(function* (  action: string,): Effect.fn.Return<void, DesktopWindow.DesktopWindowError, DesktopWindow.DesktopWindow> {  const desktopWindow = yield* DesktopWindow.DesktopWindow;  yield* desktopWindow.dispatchMenuAction(action);}); const zoomMainWindow = Effect.fn("desktop.menu.zoomMainWindow")(function* (  direction: DesktopWindow.MainWindowZoomDirection,): Effect.fn.Return<void, never, DesktopWindow.DesktopWindow> {  const desktopWindow = yield* DesktopWindow.DesktopWindow;  yield* desktopWindow.zoomMain(direction);}); const checkForUpdatesFromMenu = Effect.gen(function* () {  const updates = yield* DesktopUpdates.DesktopUpdates;  const electronDialog = yield* ElectronDialog.ElectronDialog;  const result = yield* updates.check("menu");  const updateState = result.state;   if (updateState.status === "up-to-date") {    yield* electronDialog.showMessageBox({      type: "info",      title: "You're up to date!",      message: `T3 Code ${updateState.currentVersion} is currently the newest version available.`,      buttons: ["OK"],    });  } else if (updateState.status === "error") {    yield* electronDialog.showMessageBox({      type: "warning",      title: "Update check failed",      message: "Could not check for updates.",      detail: updateState.message ?? "An unknown error occurred. Please try again later.",      buttons: ["OK"],    });  }}).pipe(Effect.withSpan("desktop.menu.checkForUpdates")); const handleCheckForUpdatesMenuClick = Effect.gen(function* () {  const updates = yield* DesktopUpdates.DesktopUpdates;  const electronDialog = yield* ElectronDialog.ElectronDialog;  const disabledReason = yield* updates.disabledReason;  if (Option.isSome(disabledReason)) {    yield* logUpdaterInfo("manual update check requested, but updates are disabled", {      disabledReason: disabledReason.value,    });    yield* electronDialog.showMessageBox({      type: "info",      title: "Updates unavailable",      message: "Automatic updates are not available right now.",      detail: disabledReason.value,      buttons: ["OK"],    });    return;  }   const desktopWindow = yield* DesktopWindow.DesktopWindow;  yield* desktopWindow.ensureMain;  yield* checkForUpdatesFromMenu;}).pipe(Effect.withSpan("desktop.menu.handleCheckForUpdatesClick")); /** @public Service construction is part of the canonical Effect module API. */export const make = Effect.gen(function* () {  const electronApp = yield* ElectronApp.ElectronApp;  const electronMenu = yield* ElectronMenu.ElectronMenu;  const environment = yield* DesktopEnvironment.DesktopEnvironment;  const appName = yield* electronApp.name;  const context = yield* Effect.context<DesktopApplicationMenuRuntimeServices>();  const runPromise = Effect.runPromiseWith(context);   const runMenuEffect = <E>(    action: string,    effect: Effect.Effect<void, E, DesktopApplicationMenuRuntimeServices>,  ) => {    void runPromise(      effect.pipe(        Effect.annotateLogs({ action }),        Effect.withSpan("desktop.menu.action"),        Effect.catchCause((cause) => {          const error = new DesktopApplicationMenuActionError({ action, cause });          return logMenuError(error.message, { error });        }),      ),    );  };   const configure = Effect.gen(function* () {    const checkForUpdatesClick = () => {      runMenuEffect("check-for-updates", handleCheckForUpdatesMenuClick);    };    const settingsClick = () => {      runMenuEffect("open-settings", dispatchMenuAction("open-settings"));    };    const zoomClick = (direction: DesktopWindow.MainWindowZoomDirection) => () => {      runMenuEffect(`zoom-${direction}`, zoomMainWindow(direction));    };    const template: Electron.MenuItemConstructorOptions[] = [];     if (environment.platform === "darwin") {      template.push({        label: appName,        submenu: [          { role: "about" },          {            label: "Check for Updates...",            click: checkForUpdatesClick,          },          { type: "separator" },          {            label: "Settings...",            accelerator: "CmdOrCtrl+,",            click: settingsClick,          },          { type: "separator" },          { role: "services" },          { type: "separator" },          { role: "hide" },          { role: "hideOthers" },          { role: "unhide" },          { type: "separator" },          { role: "quit" },        ],      });    }     template.push(      {        label: "File",        submenu: [          ...(environment.platform === "darwin"            ? []            : [                {                  label: "Settings...",                  accelerator: "CmdOrCtrl+,",                  click: settingsClick,                },                { type: "separator" as const },              ]),          { role: environment.platform === "darwin" ? "close" : "quit" },        ],      },      { role: "editMenu" },      {        label: "View",        submenu: [          { role: "reload" },          { role: "forceReload" },          { role: "toggleDevTools" },          { type: "separator" },          /*            Not the zoom roles: those act on the focused webContents, so with            an embedded preview WebContentsView focused they zoom the guest            page and the app UI appears stuck. These always zoom the main            window (see DesktopWindow.zoomMain).          */          { label: "Actual Size", accelerator: "CmdOrCtrl+0", click: zoomClick("reset") },          { label: "Zoom In", accelerator: "CmdOrCtrl+=", click: zoomClick("in") },          {            label: "Zoom In",            accelerator: "CmdOrCtrl+Plus",            visible: false,            click: zoomClick("in"),          },          { label: "Zoom Out", accelerator: "CmdOrCtrl+-", click: zoomClick("out") },          { type: "separator" },          { role: "togglefullscreen" },        ],      },      { role: "windowMenu" },      {        role: "help",        submenu: [          {            label: "Check for Updates...",            click: checkForUpdatesClick,          },        ],      },    );     yield* electronMenu.setApplicationMenu(template);  }).pipe(Effect.withSpan("desktop.menu.configure"));   return DesktopApplicationMenu.of({    configure,  });}); export const layer = Layer.effect(DesktopApplicationMenu, make);