| ... | ... | @@ -2,11 +2,80 @@ import * as Context from "effect/Context"; |
| 2 | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | import * as Layer from "effect/Layer"; |
| 4 | 4 | import * as Option from "effect/Option"; |
| 5 | import * as Schema from "effect/Schema"; |
| 5 | 6 | |
| 6 | 7 | import * as Electron from "electron"; |
| 7 | 8 | |
| 8 | 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 | 79 | export interface ElectronDialogPickFolderInput { |
| 11 | 80 | readonly owner: Option.Option<Electron.BrowserWindow>; |
| 12 | 81 | readonly defaultPath: Option.Option<string>; |
| ... | ... | @@ -22,17 +91,24 @@ export class ElectronDialog extends Context.Service< |
| 22 | 91 | { |
| 23 | 92 | readonly pickFolder: ( |
| 24 | 93 | input: ElectronDialogPickFolderInput, |
| 25 | | ) => Effect.Effect<Option.Option<string>>; |
| 26 | | readonly confirm: (input: ElectronDialogConfirmInput) => Effect.Effect<boolean>; |
| 94 | ) => Effect.Effect<Option.Option<string>, ElectronDialogPickFolderError>; |
| 95 | readonly confirm: ( |
| 96 | input: ElectronDialogConfirmInput, |
| 97 | ) => Effect.Effect<boolean, ElectronDialogConfirmError>; |
| 27 | 98 | readonly showMessageBox: ( |
| 28 | 99 | options: Electron.MessageBoxOptions, |
| 29 | | ) => Effect.Effect<Electron.MessageBoxReturnValue>; |
| 100 | ) => Effect.Effect<Electron.MessageBoxReturnValue, ElectronDialogShowMessageBoxError>; |
| 30 | 101 | readonly showErrorBox: (title: string, content: string) => Effect.Effect<void>; |
| 31 | 102 | } |
| 32 | 103 | >()("@t3tools/desktop/electron/ElectronDialog") {} |
| 33 | 104 | |
| 34 | 105 | export const make = ElectronDialog.of({ |
| 35 | 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 | 112 | const openDialogOptions: Electron.OpenDialogOptions = Option.match(input.defaultPath, { |
| 37 | 113 | onNone: () => ({ |
| 38 | 114 | properties: ["openDirectory", "createDirectory"], |
| ... | ... | @@ -42,10 +118,18 @@ export const make = ElectronDialog.of({ |
| 42 | 118 | defaultPath, |
| 43 | 119 | }), |
| 44 | 120 | }); |
| 45 | | const result = yield* Option.match(input.owner, { |
| 46 | | onNone: () => Effect.promise(() => Electron.dialog.showOpenDialog(openDialogOptions)), |
| 47 | | onSome: (owner) => |
| 48 | | Effect.promise(() => Electron.dialog.showOpenDialog(owner, openDialogOptions)), |
| 121 | const result = yield* Effect.tryPromise({ |
| 122 | try: () => |
| 123 | Option.match(input.owner, { |
| 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 | 135 | if (result.canceled) { |
| ... | ... | @@ -67,17 +151,48 @@ export const make = ElectronDialog.of({ |
| 67 | 151 | noLink: true, |
| 68 | 152 | message: normalizedMessage, |
| 69 | 153 | }; |
| 70 | | const result = yield* Option.match(input.owner, { |
| 71 | | onNone: () => Effect.promise(() => Electron.dialog.showMessageBox(options)), |
| 72 | | onSome: (owner) => Effect.promise(() => Electron.dialog.showMessageBox(owner, options)), |
| 154 | const ownerWindowId = Option.match(input.owner, { |
| 155 | onNone: () => null, |
| 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 | 171 | return result.response === CONFIRM_BUTTON_INDEX; |
| 75 | 172 | }), |
| 76 | | showMessageBox: (options) => Effect.promise(() => Electron.dialog.showMessageBox(options)), |
| 77 | | showErrorBox: (title, content) => |
| 78 | | Effect.sync(() => { |
| 79 | | Electron.dialog.showErrorBox(title, content); |
| 173 | showMessageBox: (options) => |
| 174 | Effect.tryPromise({ |
| 175 | try: () => Electron.dialog.showMessageBox(options), |
| 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 | 198 | export const layer = Layer.succeed(ElectronDialog, make); |