dazscript-framework 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -299,6 +299,17 @@ Custom action icons are selected from `action(...)` metadata and sibling image f
299
299
 
300
300
  `scriptname.action.png` is the installed custom action icon. Daz Studio uses the same action icon for menu and toolbar placements. `scriptname.png` is the preferred script/content icon fallback. `scriptname.dsa.png` is a legacy fallback kept for older projects and will be removed in a future breaking release.
301
301
 
302
+ Setup dialog header assets are optional and are discovered beside `src/Setup.dsa.ts`:
303
+
304
+ 1. `src/Setup.header.png`
305
+ 2. `src/Setup.tip.png`
306
+ 3. `src/Setup.png`
307
+ 4. `src/Setup.dsa.png` legacy fallback
308
+
309
+ Header text can be placed in `src/Setup.header.html`, `src/Setup.header.md`, or `src/Setup.header.txt`, with `src/Setup.html`, `src/Setup.md`, and `src/Setup.txt` as script-named fallbacks. The installer generator embeds that text into `Setup.dsa.ts`, so Daz Studio does not need to read the text file at setup time. The setup dialog renders the header body with `DzTextBrowser` rich text support; no Markdown conversion is performed. The image remains a deployed PNG asset and is resolved relative to the generated setup script at runtime.
310
+
311
+ The same layout is available to custom dialogs through `add.header({ imagePath, html, text, height, imageWidth }).build()`. Use `html` for rich text, or `text` for escaped plain text.
312
+
302
313
  This replaces the older `Install.dsa.ts` / `Uninstall.dsa.ts` pattern. The installer generator removes those legacy files if they exist.
303
314
 
304
315
  ### Setup Keyboard Shortcuts
@@ -276,6 +276,49 @@ function toPosix(filePath) {
276
276
  return filePath.replace(/\\/g, '/');
277
277
  }
278
278
 
279
+ function resolveSetupHeaderImage(workdir) {
280
+ const candidates = [
281
+ path.join(workdir, 'src', 'Setup.header.png'),
282
+ path.join(workdir, 'src', 'Setup.tip.png'),
283
+ path.join(workdir, 'src', 'Setup.png'),
284
+ path.join(workdir, 'src', 'Setup.dsa.png'),
285
+ ];
286
+ const match = candidates.find((candidate) => fs.existsSync(candidate));
287
+ return match ? `./${path.basename(match)}` : undefined;
288
+ }
289
+
290
+ function resolveSetupHeaderTextFile(workdir) {
291
+ const candidates = [
292
+ path.join(workdir, 'src', 'Setup.header.html'),
293
+ path.join(workdir, 'src', 'Setup.header.md'),
294
+ path.join(workdir, 'src', 'Setup.header.txt'),
295
+ path.join(workdir, 'src', 'Setup.html'),
296
+ path.join(workdir, 'src', 'Setup.md'),
297
+ path.join(workdir, 'src', 'Setup.txt'),
298
+ ];
299
+
300
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null;
301
+ }
302
+
303
+ function loadSetupHeader(workdir) {
304
+ const header = {};
305
+ const imagePath = resolveSetupHeaderImage(workdir);
306
+ const textFile = resolveSetupHeaderTextFile(workdir);
307
+
308
+ if (imagePath) {
309
+ header.headerImagePath = imagePath;
310
+ }
311
+
312
+ if (textFile) {
313
+ const text = fs.readFileSync(textFile, 'utf8').trim();
314
+ if (text) {
315
+ header.headerText = text;
316
+ }
317
+ }
318
+
319
+ return header;
320
+ }
321
+
279
322
  function resolveShortcutFile(workdir, config) {
280
323
  const configuredPath =
281
324
  config.keyboardShortcutsPath ||
@@ -340,6 +383,7 @@ function generateInstallerFiles(workdir, options) {
340
383
  settingsPath,
341
384
  bundleName,
342
385
  shortcutBackupPath: `${appDataPath}/Installer/keyboard-shortcuts-backup.json`,
386
+ ...loadSetupHeader(workdir),
343
387
  };
344
388
  if (shortcutData.shortcuts && shortcutData.shortcuts.length > 0) {
345
389
  setupOptions.shortcuts = shortcutData.shortcuts;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dazscript-framework",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "author": "Freddy Diaz",
5
5
  "license": "MPL-2.0",
6
6
  "description": "",
@@ -0,0 +1,86 @@
1
+ import LayoutBuilder from './layout-builder'
2
+ import { createWidget } from './widget-builder'
3
+ import { WidgetBuilderContext } from './widgets-builder'
4
+ import { getScriptPath } from '@dsf/helpers/script-helper'
5
+
6
+ export type DialogHeaderOptions = {
7
+ image?: Pixmap
8
+ imagePath?: string
9
+ imageWidth?: number
10
+ height?: number
11
+ html?: string
12
+ text?: string
13
+ }
14
+
15
+ const escapeHtml = (value: string): string =>
16
+ value
17
+ .replace(/&/g, '&')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ .replace(/"/g, '&quot;')
21
+ .replace(/'/g, '&#39;')
22
+ .replace(/\r?\n/g, '<br>')
23
+
24
+ const resolveImagePath = (filePath: string): string => {
25
+ const value = String(filePath ?? '').replace(/\\/g, '/')
26
+ if (!value) return ''
27
+ if (value.indexOf(':') >= 0 || value.charAt(0) === '/') return value
28
+
29
+ return `${getScriptPath()}/${value.replace(/^\.\//, '')}`
30
+ }
31
+
32
+ export class DialogHeaderBuilder {
33
+ constructor(
34
+ private readonly context: WidgetBuilderContext,
35
+ private readonly options: DialogHeaderOptions
36
+ ) { }
37
+
38
+ build(): DzWidget {
39
+ const height = this.options.height ?? 96
40
+ const imageWidth = this.options.imageWidth ?? height
41
+ const html = this.options.html ?? (
42
+ typeof this.options.text === 'string'
43
+ ? escapeHtml(this.options.text)
44
+ : ''
45
+ )
46
+
47
+ return createWidget(this.context).build(DzWidget, (container) => {
48
+ container.setFixedHeight(height)
49
+
50
+ LayoutBuilder
51
+ .create(this.context)
52
+ .parent(container)
53
+ .direction('horizontal')
54
+ .build(() => {
55
+ this.buildImage(height, imageWidth)
56
+ this.buildText(html, height)
57
+ })
58
+ })
59
+ }
60
+
61
+ private buildImage(height: number, imageWidth: number): void {
62
+ const pixmap = this.options.image ?? (
63
+ this.options.imagePath ? new Pixmap(resolveImagePath(this.options.imagePath)) : null
64
+ )
65
+ if (!pixmap) return
66
+
67
+ createWidget(this.context).build(DzLabel, (label) => {
68
+ label.pixmap = pixmap
69
+ label.scaledContents = true
70
+ label.setFixedWidth(imageWidth)
71
+ label.setFixedHeight(height)
72
+ })
73
+ }
74
+
75
+ private buildText(html: string, height: number): void {
76
+ if (!html) return
77
+
78
+ createWidget(this.context).build(DzTextBrowser, (textBrowser) => {
79
+ textBrowser.html = html
80
+ textBrowser.readOnly = true
81
+ textBrowser.lineWrapMode = DzTextEdit.WidgetWidth
82
+ textBrowser.wordWrapMode = DzTextEdit.WordWrap
83
+ textBrowser.setFixedHeight(height)
84
+ })
85
+ }
86
+ }
@@ -4,6 +4,7 @@ import CheckBoxBuilder from './checkbox-builder'
4
4
  import ColorPickerBuilder from './color-picker-builder'
5
5
  import { ComboBoxBuilder } from './combo-box-builder'
6
6
  import { ComboEditBuilder } from './combo-edit-builder'
7
+ import { DialogHeaderBuilder, DialogHeaderOptions } from './dialog-header-builder'
7
8
  import GroupBoxBuilder from './groupbox-builder'
8
9
  import LabelBuilder from './label-builder'
9
10
  import LayoutBuilder, { LayoutOrientation } from './layout-builder'
@@ -63,6 +64,10 @@ export class WidgetsBuilder {
63
64
  return new LabelBuilder(this.context).text(text)
64
65
  }
65
66
 
67
+ header(options: DialogHeaderOptions): DialogHeaderBuilder {
68
+ return new DialogHeaderBuilder(this.context, options)
69
+ }
70
+
66
71
  button(text?: string): ButtonBuilder {
67
72
  return new ButtonBuilder(this.context).text(text)
68
73
  }
@@ -13,6 +13,7 @@ import { promptKeyboardShortcut } from '@dsf/shared/set-keyboard-shortcut'
13
13
  import { readFromFile, saveToFile } from './file-helper'
14
14
  import { getCanonicalInstallerEntry, toActionKey } from './custom-action-installer-entries'
15
15
  import { canResetSetupShortcut, getDisplayedSetupShortcut, resetSetupShortcut, setSetupShortcut, updateShortcutOverrideState } from './custom-action-installer-shortcuts'
16
+ import { getScriptPath } from './script-helper'
16
17
 
17
18
  type InstallerEntry = {
18
19
  action: CustomAction
@@ -31,6 +32,10 @@ type InstallerEntry = {
31
32
  type SetupDialogOptions = {
32
33
  settingsPath: string
33
34
  bundleName?: string
35
+ headerImagePath?: string
36
+ headerImageWidth?: number
37
+ headerHeight?: number
38
+ headerText?: string
34
39
  shortcuts?: ActionAccelerator[]
35
40
  shortcutsSourcePath?: string
36
41
  shortcutBackupPath?: string
@@ -96,6 +101,17 @@ const getSetupDialogOptions = (options: string | SetupDialogOptions): SetupDialo
96
101
  const getShortcutBackupPath = (options: SetupDialogOptions): string =>
97
102
  `${App.getAppDataPath()}/${options.shortcutBackupPath ?? `${options.settingsPath}/keyboard-shortcuts-backup.json`}`
98
103
 
104
+ const hasSetupHeader = (options: SetupDialogOptions): boolean =>
105
+ Boolean(options.headerImagePath || options.headerText)
106
+
107
+ const resolveSetupAssetPath = (filePath: string | null | undefined): string => {
108
+ const value = String(filePath ?? '').replace(/\\/g, '/')
109
+ if (!value) return ''
110
+ if (value.indexOf(':') >= 0 || value.charAt(0) === '/') return value
111
+
112
+ return `${getScriptPath()}/${value.replace(/^\.\//, '')}`
113
+ }
114
+
99
115
  const buildEntries = (actions: CustomAction[]): InstallerEntry[] => {
100
116
  return actions
101
117
  .filter((action) => Boolean(action.menuPath) || Boolean(action.toolbar))
@@ -180,9 +196,9 @@ class InstallerSelectionDialog extends BasicDialog {
180
196
  constructor(
181
197
  private readonly entries: InstallerEntry[],
182
198
  private readonly shortcutEntries: ShortcutEntry[],
183
- bundleName?: string
199
+ private readonly options: SetupDialogOptions
184
200
  ) {
185
- super(bundleName ? `${bundleName} Setup` : 'Setup Scripts', 'dsfSetupScripts')
201
+ super(options.bundleName ? `${options.bundleName} Setup` : 'Setup Scripts', 'dsfSetupScripts')
186
202
  this.items$ = new Observable(entries.map(toTreeNode))
187
203
  this.shortcutItems$ = new Observable(shortcutEntries.map(toShortcutTreeNode))
188
204
  }
@@ -214,9 +230,13 @@ class InstallerSelectionDialog extends BasicDialog {
214
230
  private buildScriptsTab(): void {
215
231
  const add = this.add
216
232
 
217
- add.label('Choose which scripts to install, then use the columns to review their shortcut, menu, and toolbar targets.')
218
- .wordWrap()
219
- .build()
233
+ if (hasSetupHeader(this.options)) {
234
+ this.buildHeader()
235
+ } else {
236
+ add.label('Choose which scripts to install, then use the columns to review their shortcut, menu, and toolbar targets.')
237
+ .wordWrap()
238
+ .build()
239
+ }
220
240
  add.group('Search').horizontal().build(() => {
221
241
  add.edit()
222
242
  .value(this.keywords$)
@@ -285,6 +305,20 @@ class InstallerSelectionDialog extends BasicDialog {
285
305
  })
286
306
  }
287
307
 
308
+ private buildHeader(): void {
309
+ const add = this.add
310
+ const height = this.options.headerHeight ?? 96
311
+ const imageWidth = this.options.headerImageWidth ?? height
312
+ const headerText = String(this.options.headerText ?? 'Choose which scripts to install, then use the columns to review their shortcut, menu, and toolbar targets.')
313
+
314
+ add.header({
315
+ imagePath: resolveSetupAssetPath(this.options.headerImagePath),
316
+ imageWidth,
317
+ height,
318
+ html: headerText
319
+ }).build()
320
+ }
321
+
288
322
  private buildShortcutsTab(): void {
289
323
  const add = this.add
290
324
 
@@ -559,7 +593,7 @@ class InstallerSelectionDialog extends BasicDialog {
559
593
  const runDialog = (actions: CustomAction[], options: SetupDialogOptions): SetupSelection | null => {
560
594
  const entries = buildEntries(actions)
561
595
  const shortcutEntries = buildShortcutEntries(options.shortcuts)
562
- const dialog = new InstallerSelectionDialog(entries, shortcutEntries, options.bundleName)
596
+ const dialog = new InstallerSelectionDialog(entries, shortcutEntries, options)
563
597
  return dialog.ok() ? dialog.getSelections() : null
564
598
  }
565
599
 
@@ -29,6 +29,10 @@ const writePng = (projectDir: string, fileName: string): void => {
29
29
  fs.writeFileSync(path.join(projectDir, 'src', fileName), 'png')
30
30
  }
31
31
 
32
+ const writeText = (projectDir: string, fileName: string, content: string): void => {
33
+ fs.writeFileSync(path.join(projectDir, 'src', fileName), content)
34
+ }
35
+
32
36
  const generateSetup = (projectDir: string): string => {
33
37
  const previousCwd = process.cwd()
34
38
  process.chdir(projectDir)
@@ -103,3 +107,83 @@ describe('install generator action icons', () => {
103
107
  expect(setup).not.toContain('"icon": "custom-icon.dsa.png"')
104
108
  })
105
109
  })
110
+
111
+ describe('install generator setup header', () => {
112
+ it('uses the explicit setup header image before the generic setup image', () => {
113
+ const projectDir = makeProject()
114
+ writeScript(projectDir, 'render-tools')
115
+ writePng(projectDir, 'Setup.header.png')
116
+ writePng(projectDir, 'Setup.tip.png')
117
+ writePng(projectDir, 'Setup.png')
118
+
119
+ const setup = generateSetup(projectDir)
120
+
121
+ expect(setup).toContain('"headerImagePath":"./Setup.header.png"')
122
+ expect(setup).not.toContain('"headerImagePath":"./Setup.tip.png"')
123
+ expect(setup).not.toContain('"headerImagePath":"./Setup.png"')
124
+ })
125
+
126
+ it('uses the setup tip image before the generic setup image', () => {
127
+ const projectDir = makeProject()
128
+ writeScript(projectDir, 'render-tools')
129
+ writePng(projectDir, 'Setup.tip.png')
130
+ writePng(projectDir, 'Setup.png')
131
+
132
+ const setup = generateSetup(projectDir)
133
+
134
+ expect(setup).toContain('"headerImagePath":"./Setup.tip.png"')
135
+ expect(setup).not.toContain('"headerImagePath":"./Setup.png"')
136
+ })
137
+
138
+ it('falls back to the generic setup image when no explicit header image exists', () => {
139
+ const projectDir = makeProject()
140
+ writeScript(projectDir, 'render-tools')
141
+ writePng(projectDir, 'Setup.png')
142
+
143
+ const setup = generateSetup(projectDir)
144
+
145
+ expect(setup).toContain('"headerImagePath":"./Setup.png"')
146
+ })
147
+
148
+ it('falls back to the setup script image when no header image exists', () => {
149
+ const projectDir = makeProject()
150
+ writeScript(projectDir, 'render-tools')
151
+ writePng(projectDir, 'Setup.dsa.png')
152
+
153
+ const setup = generateSetup(projectDir)
154
+
155
+ expect(setup).toContain('"headerImagePath":"./Setup.dsa.png"')
156
+ })
157
+
158
+ it('embeds setup header markdown text into generated setup options', () => {
159
+ const projectDir = makeProject()
160
+ writeScript(projectDir, 'render-tools')
161
+ writeText(projectDir, 'Setup.header.md', 'Header line\n\nSecond line\n')
162
+
163
+ const setup = generateSetup(projectDir)
164
+
165
+ expect(setup).toContain('"headerText":"Header line\\n\\nSecond line"')
166
+ })
167
+
168
+ it('prefers setup header html over markdown text', () => {
169
+ const projectDir = makeProject()
170
+ writeScript(projectDir, 'render-tools')
171
+ writeText(projectDir, 'Setup.header.html', '<h2>HTML Header</h2>')
172
+ writeText(projectDir, 'Setup.header.md', '# Markdown Header')
173
+
174
+ const setup = generateSetup(projectDir)
175
+
176
+ expect(setup).toContain('"headerText":"<h2>HTML Header</h2>"')
177
+ expect(setup).not.toContain('# Markdown Header')
178
+ })
179
+
180
+ it('falls back to setup script markdown when no header text file exists', () => {
181
+ const projectDir = makeProject()
182
+ writeScript(projectDir, 'render-tools')
183
+ writeText(projectDir, 'Setup.md', 'Setup markdown fallback')
184
+
185
+ const setup = generateSetup(projectDir)
186
+
187
+ expect(setup).toContain('"headerText":"Setup markdown fallback"')
188
+ })
189
+ })