| ... | @@ -2,11 +2,80 @@ import * as Context from "effect/Context"; | ... | @@ -2,11 +2,80 @@ import * as Context from "effect/Context"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; | 3 | import * as Layer from "effect/Layer"; |
| 4 | import * as Option from "effect/Option"; | 4 | import * as Option from "effect/Option"; |
| | 5 | import * as Schema from "effect/Schema"; |
| 5 | | 6 | |
| 6 | import * as Electron from "electron"; | 7 | import * as Electron from "electron"; |
| 7 | | 8 | |
| 8 | const CONFIRM_BUTTON_INDEX = 1; | 9 | const CONFIRM_BUTTON_INDEX = 1; |
| 9 | | 10 | |
| | 11 | export class ElectronDialogPickFolderError extends Schema.TaggedErrorClass<ElectronDialogPickFolderError>()( |
| | 12 | "ElectronDialogPickFolderError", |
| | 13 | { |
| | 14 | ownerWindowId: Schema.NullOr(Schema.Number), |
| | 15 | defaultPath: Schema.NullOr(Schema.String), |
| | 16 | cause: Schema.Defect(), |
| | 17 | }, |
| | 18 | ) { |
| | 19 | override get message(): string { |
| | 20 | const owner = this.ownerWindowId === null ? "the application" : `window ${this.ownerWindowId}`; |
| | 21 | const defaultPath = this.defaultPath === null ? "no default path" : this.defaultPath; |
| | 22 | return `Failed to open the Electron folder picker for ${owner} with ${defaultPath}.`; |
| | 23 | } |
| | 24 | } |
| | 25 | |
| | 26 | export class ElectronDialogConfirmError extends Schema.TaggedErrorClass<ElectronDialogConfirmError>()( |
| | 27 | "ElectronDialogConfirmError", |
| | 28 | { |
| | 29 | ownerWindowId: Schema.NullOr(Schema.Number), |
| | 30 | promptLength: Schema.Number, |
| | 31 | cause: Schema.Defect(), |
| | 32 | }, |
| | 33 | ) { |
| | 34 | override get message(): string { |
| | 35 | const owner = this.ownerWindowId === null ? "the application" : `window ${this.ownerWindowId}`; |
| | 36 | return `Failed to open an Electron confirmation dialog for ${owner} with a ${this.promptLength}-character prompt.`; |
| | 37 | } |
| | 38 | } |
| | 39 | |
| | 40 | export class ElectronDialogShowMessageBoxError extends Schema.TaggedErrorClass<ElectronDialogShowMessageBoxError>()( |
| | 41 | "ElectronDialogShowMessageBoxError", |
| | 42 | { |
| | 43 | type: Schema.NullOr(Schema.Literals(["none", "info", "error", "question", "warning"])), |
| | 44 | titleLength: Schema.NullOr(Schema.Number), |
| | 45 | messageLength: Schema.Number, |
| | 46 | detailLength: Schema.NullOr(Schema.Number), |
| | 47 | buttonCount: Schema.Number, |
| | 48 | cause: Schema.Defect(), |
| | 49 | }, |
| | 50 | ) { |
| | 51 | override get message(): string { |
| | 52 | const type = this.type === null ? "untyped" : this.type; |
| | 53 | return `Failed to show the Electron ${type} message box with ${this.buttonCount} buttons.`; |
| | 54 | } |
| | 55 | } |
| | 56 | |
| | 57 | export class ElectronDialogShowErrorBoxError extends Schema.TaggedErrorClass<ElectronDialogShowErrorBoxError>()( |
| | 58 | "ElectronDialogShowErrorBoxError", |
| | 59 | { |
| | 60 | titleLength: Schema.Number, |
| | 61 | contentLength: Schema.Number, |
| | 62 | cause: Schema.Defect(), |
| | 63 | }, |
| | 64 | ) { |
| | 65 | override get message(): string { |
| | 66 | return `Failed to show the Electron error box with a ${this.titleLength}-character title and ${this.contentLength}-character content.`; |
| | 67 | } |
| | 68 | } |
| | 69 | |
| | 70 | export const ElectronDialogError = Schema.Union([ |
| | 71 | ElectronDialogPickFolderError, |
| | 72 | ElectronDialogConfirmError, |
| | 73 | ElectronDialogShowMessageBoxError, |
| | 74 | ElectronDialogShowErrorBoxError, |
| | 75 | ]); |
| | 76 | export type ElectronDialogError = typeof ElectronDialogError.Type; |
| | 77 | export const isElectronDialogError = Schema.is(ElectronDialogError); |
| | 78 | |
| 10 | export interface ElectronDialogPickFolderInput { | 79 | export interface ElectronDialogPickFolderInput { |
| 11 | readonly owner: Option.Option<Electron.BrowserWindow>; | 80 | readonly owner: Option.Option<Electron.BrowserWindow>; |
| 12 | readonly defaultPath: Option.Option<string>; | 81 | readonly defaultPath: Option.Option<string>; |
| ... | @@ -22,17 +91,24 @@ export class ElectronDialog extends Context.Service< | ... | @@ -22,17 +91,24 @@ export class ElectronDialog extends Context.Service< |
| 22 | { | 91 | { |
| 23 | readonly pickFolder: ( | 92 | readonly pickFolder: ( |
| 24 | input: ElectronDialogPickFolderInput, | 93 | input: ElectronDialogPickFolderInput, |
| 25 | ) => Effect.Effect<Option.Option<string>>; | 94 | ) => Effect.Effect<Option.Option<string>, ElectronDialogPickFolderError>; |
| 26 | readonly confirm: (input: ElectronDialogConfirmInput) => Effect.Effect<boolean>; | 95 | readonly confirm: ( |
| | 96 | input: ElectronDialogConfirmInput, |
| | 97 | ) => Effect.Effect<boolean, ElectronDialogConfirmError>; |
| 27 | readonly showMessageBox: ( | 98 | readonly showMessageBox: ( |
| 28 | options: Electron.MessageBoxOptions, | 99 | options: Electron.MessageBoxOptions, |
| 29 | ) => Effect.Effect<Electron.MessageBoxReturnValue>; | 100 | ) => Effect.Effect<Electron.MessageBoxReturnValue, ElectronDialogShowMessageBoxError>; |
| 30 | readonly showErrorBox: (title: string, content: string) => Effect.Effect<void>; | 101 | readonly showErrorBox: (title: string, content: string) => Effect.Effect<void>; |
| 31 | } | 102 | } |
| 32 | >()("@t3tools/desktop/electron/ElectronDialog") {} | 103 | >()("@t3tools/desktop/electron/ElectronDialog") {} |
| 33 | | 104 | |
| 34 | export const make = ElectronDialog.of({ | 105 | export const make = ElectronDialog.of({ |
| 35 | pickFolder: Effect.fn("desktop.electron.dialog.pickFolder")(function* (input) { | 106 | pickFolder: Effect.fn("desktop.electron.dialog.pickFolder")(function* (input) { |
| | 107 | const ownerWindowId = Option.match(input.owner, { |
| | 108 | onNone: () => null, |
| | 109 | onSome: (owner) => owner.id, |
| | 110 | }); |
| | 111 | const defaultPath = Option.getOrNull(input.defaultPath); |
| 36 | const openDialogOptions: Electron.OpenDialogOptions = Option.match(input.defaultPath, { | 112 | const openDialogOptions: Electron.OpenDialogOptions = Option.match(input.defaultPath, { |
| 37 | onNone: () => ({ | 113 | onNone: () => ({ |
| 38 | properties: ["openDirectory", "createDirectory"], | 114 | properties: ["openDirectory", "createDirectory"], |
| ... | @@ -42,10 +118,18 @@ export const make = ElectronDialog.of({ | ... | @@ -42,10 +118,18 @@ export const make = ElectronDialog.of({ |
| 42 | defaultPath, | 118 | defaultPath, |
| 43 | }), | 119 | }), |
| 44 | }); | 120 | }); |
| 45 | const result = yield* Option.match(input.owner, { | 121 | const result = yield* Effect.tryPromise({ |
| 46 | onNone: () => Effect.promise(() => Electron.dialog.showOpenDialog(openDialogOptions)), | 122 | try: () => |
| 47 | onSome: (owner) => | 123 | Option.match(input.owner, { |
| 48 | Effect.promise(() => Electron.dialog.showOpenDialog(owner, openDialogOptions)), | 124 | onNone: () => Electron.dialog.showOpenDialog(openDialogOptions), |
| | 125 | onSome: (owner) => Electron.dialog.showOpenDialog(owner, openDialogOptions), |
| | 126 | }), |
| | 127 | catch: (cause) => |
| | 128 | new ElectronDialogPickFolderError({ |
| | 129 | ownerWindowId, |
| | 130 | defaultPath, |
| | 131 | cause, |
| | 132 | }), |
| 49 | }); | 133 | }); |
| 50 | | 134 | |
| 51 | if (result.canceled) { | 135 | if (result.canceled) { |
| ... | @@ -67,17 +151,48 @@ export const make = ElectronDialog.of({ | ... | @@ -67,17 +151,48 @@ export const make = ElectronDialog.of({ |
| 67 | noLink: true, | 151 | noLink: true, |
| 68 | message: normalizedMessage, | 152 | message: normalizedMessage, |
| 69 | }; | 153 | }; |
| 70 | const result = yield* Option.match(input.owner, { | 154 | const ownerWindowId = Option.match(input.owner, { |
| 71 | onNone: () => Effect.promise(() => Electron.dialog.showMessageBox(options)), | 155 | onNone: () => null, |
| 72 | onSome: (owner) => Effect.promise(() => Electron.dialog.showMessageBox(owner, options)), | 156 | onSome: (owner) => owner.id, |
| | 157 | }); |
| | 158 | const result = yield* Effect.tryPromise({ |
| | 159 | try: () => |
| | 160 | Option.match(input.owner, { |
| | 161 | onNone: () => Electron.dialog.showMessageBox(options), |
| | 162 | onSome: (owner) => Electron.dialog.showMessageBox(owner, options), |
| | 163 | }), |
| | 164 | catch: (cause) => |
| | 165 | new ElectronDialogConfirmError({ |
| | 166 | ownerWindowId, |
| | 167 | promptLength: normalizedMessage.length, |
| | 168 | cause, |
| | 169 | }), |
| 73 | }); | 170 | }); |
| 74 | return result.response === CONFIRM_BUTTON_INDEX; | 171 | return result.response === CONFIRM_BUTTON_INDEX; |
| 75 | }), | 172 | }), |
| 76 | showMessageBox: (options) => Effect.promise(() => Electron.dialog.showMessageBox(options)), | 173 | showMessageBox: (options) => |
| 77 | showErrorBox: (title, content) => | 174 | Effect.tryPromise({ |
| 78 | Effect.sync(() => { | 175 | try: () => Electron.dialog.showMessageBox(options), |
| 79 | Electron.dialog.showErrorBox(title, content); | 176 | catch: (cause) => |
| | 177 | new ElectronDialogShowMessageBoxError({ |
| | 178 | type: options.type ?? null, |
| | 179 | titleLength: options.title?.length ?? null, |
| | 180 | messageLength: options.message.length, |
| | 181 | detailLength: options.detail?.length ?? null, |
| | 182 | buttonCount: options.buttons?.length ?? 0, |
| | 183 | cause, |
| | 184 | }), |
| 80 | }), | 185 | }), |
| | 186 | showErrorBox: (title, content) => |
| | 187 | Effect.try({ |
| | 188 | try: () => Electron.dialog.showErrorBox(title, content), |
| | 189 | catch: (cause) => |
| | 190 | new ElectronDialogShowErrorBoxError({ |
| | 191 | titleLength: title.length, |
| | 192 | contentLength: content.length, |
| | 193 | cause, |
| | 194 | }), |
| | 195 | }).pipe(Effect.orDie), |
| 81 | }); | 196 | }); |
| 82 | | 197 | |
| 83 | export const layer = Layer.succeed(ElectronDialog, make); | 198 | export const layer = Layer.succeed(ElectronDialog, make); |