212 lines · 6.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.TaggedErrorClass<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 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")); 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 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" },          { role: "resetZoom" },          { role: "zoomIn", accelerator: "CmdOrCtrl+=" },          { role: "zoomIn", accelerator: "CmdOrCtrl+Plus", visible: false },          { role: "zoomOut" },          { 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);