@ramathibodi/nuxt-commons 0.1.74 → 0.1.75

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.
Files changed (96) hide show
  1. package/README.md +115 -115
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Alert.vue +58 -58
  4. package/dist/runtime/components/BarcodeReader.vue +130 -130
  5. package/dist/runtime/components/ExportCSV.vue +110 -110
  6. package/dist/runtime/components/FileBtn.vue +79 -79
  7. package/dist/runtime/components/ImportCSV.vue +151 -151
  8. package/dist/runtime/components/MrzReader.vue +168 -168
  9. package/dist/runtime/components/SplitterPanel.vue +67 -67
  10. package/dist/runtime/components/TabsGroup.vue +39 -39
  11. package/dist/runtime/components/TextBarcode.vue +66 -66
  12. package/dist/runtime/components/device/IdCardButton.vue +95 -95
  13. package/dist/runtime/components/device/IdCardWebSocket.vue +207 -207
  14. package/dist/runtime/components/device/Scanner.vue +350 -350
  15. package/dist/runtime/components/dialog/Confirm.vue +112 -112
  16. package/dist/runtime/components/dialog/Host.vue +88 -88
  17. package/dist/runtime/components/dialog/Index.vue +84 -84
  18. package/dist/runtime/components/dialog/Loading.vue +51 -51
  19. package/dist/runtime/components/dialog/default/Confirm.vue +112 -112
  20. package/dist/runtime/components/dialog/default/Loading.vue +60 -60
  21. package/dist/runtime/components/dialog/default/Notify.vue +82 -82
  22. package/dist/runtime/components/dialog/default/Printing.vue +46 -46
  23. package/dist/runtime/components/dialog/default/VerifyUser.vue +144 -144
  24. package/dist/runtime/components/document/Form.vue +50 -50
  25. package/dist/runtime/components/document/TemplateBuilder.vue +536 -536
  26. package/dist/runtime/components/form/ActionPad.vue +156 -156
  27. package/dist/runtime/components/form/Birthdate.vue +116 -116
  28. package/dist/runtime/components/form/CheckboxGroup.vue +99 -99
  29. package/dist/runtime/components/form/CodeEditor.vue +45 -45
  30. package/dist/runtime/components/form/Date.vue +270 -270
  31. package/dist/runtime/components/form/DateTime.vue +220 -220
  32. package/dist/runtime/components/form/Dialog.vue +178 -178
  33. package/dist/runtime/components/form/EditPad.vue +157 -157
  34. package/dist/runtime/components/form/File.vue +295 -295
  35. package/dist/runtime/components/form/Hidden.vue +44 -44
  36. package/dist/runtime/components/form/Iterator.vue +538 -538
  37. package/dist/runtime/components/form/Login.vue +143 -143
  38. package/dist/runtime/components/form/Pad.vue +399 -399
  39. package/dist/runtime/components/form/SignPad.vue +226 -226
  40. package/dist/runtime/components/form/System.vue +34 -34
  41. package/dist/runtime/components/form/Table.vue +391 -391
  42. package/dist/runtime/components/form/TableData.vue +236 -236
  43. package/dist/runtime/components/form/Time.vue +177 -177
  44. package/dist/runtime/components/form/images/Capture.vue +245 -245
  45. package/dist/runtime/components/form/images/Edit.vue +133 -133
  46. package/dist/runtime/components/form/images/Field.vue +331 -331
  47. package/dist/runtime/components/form/images/Pad.vue +54 -54
  48. package/dist/runtime/components/label/Date.vue +37 -37
  49. package/dist/runtime/components/label/DateAgo.vue +102 -102
  50. package/dist/runtime/components/label/DateCount.vue +152 -152
  51. package/dist/runtime/components/label/Field.vue +111 -111
  52. package/dist/runtime/components/label/FormatMoney.vue +37 -37
  53. package/dist/runtime/components/label/Mask.vue +46 -46
  54. package/dist/runtime/components/label/Object.vue +21 -21
  55. package/dist/runtime/components/master/Autocomplete.vue +89 -89
  56. package/dist/runtime/components/master/Combobox.vue +88 -88
  57. package/dist/runtime/components/master/RadioGroup.vue +90 -90
  58. package/dist/runtime/components/master/Select.vue +70 -70
  59. package/dist/runtime/components/master/label.vue +55 -55
  60. package/dist/runtime/components/model/Autocomplete.vue +91 -91
  61. package/dist/runtime/components/model/Combobox.vue +90 -90
  62. package/dist/runtime/components/model/Pad.vue +114 -114
  63. package/dist/runtime/components/model/Select.vue +78 -84
  64. package/dist/runtime/components/model/Table.vue +370 -370
  65. package/dist/runtime/components/model/iterator.vue +497 -497
  66. package/dist/runtime/components/model/label.vue +58 -58
  67. package/dist/runtime/components/pdf/Print.vue +75 -75
  68. package/dist/runtime/components/pdf/View.vue +146 -146
  69. package/dist/runtime/composables/dialog.d.ts +1 -1
  70. package/dist/runtime/composables/graphql.d.ts +1 -1
  71. package/dist/runtime/composables/graphqlModel.d.ts +9 -9
  72. package/dist/runtime/composables/graphqlModelItem.d.ts +7 -7
  73. package/dist/runtime/composables/graphqlModelOperation.d.ts +6 -6
  74. package/dist/runtime/composables/userPermission.d.ts +1 -1
  75. package/dist/runtime/labs/Calendar.vue +99 -99
  76. package/dist/runtime/labs/form/EditMobile.vue +152 -152
  77. package/dist/runtime/labs/form/TextFieldMask.vue +43 -43
  78. package/dist/runtime/plugins/clientConfig.d.ts +1 -1
  79. package/dist/runtime/plugins/default.d.ts +1 -1
  80. package/dist/runtime/plugins/dialogManager.d.ts +1 -1
  81. package/dist/runtime/plugins/permission.d.ts +1 -1
  82. package/dist/runtime/types/alert.d.ts +11 -11
  83. package/dist/runtime/types/clientConfig.d.ts +13 -13
  84. package/dist/runtime/types/dialogManager.d.ts +35 -35
  85. package/dist/runtime/types/formDialog.d.ts +5 -5
  86. package/dist/runtime/types/graphqlOperation.d.ts +23 -23
  87. package/dist/runtime/types/menu.d.ts +31 -31
  88. package/dist/runtime/types/modules.d.ts +7 -7
  89. package/dist/runtime/types/permission.d.ts +13 -13
  90. package/package.json +131 -131
  91. package/scripts/enrich-vue-docs-from-ai.mjs +197 -197
  92. package/scripts/generate-ai-summary.mjs +321 -321
  93. package/scripts/generate-composables-md.mjs +129 -129
  94. package/scripts/postInstall.cjs +70 -70
  95. package/templates/.codegen/codegen.ts +32 -32
  96. package/templates/.codegen/plugin-schema-object.js +161 -161
@@ -1,350 +1,350 @@
1
- <script lang="ts" setup>
2
- /**
3
- * DeviceScanner bridges UI actions with host-agent hardware/device operations and emits runtime scan/read results.
4
- * This doc block is consumed by vue-docgen for generated API documentation.
5
- */
6
- import { computed, ref, watch, reactive } from 'vue'
7
- import type { Base64File } from '../../composables/assetFile'
8
- import { useAlert } from '../../composables/alert'
9
- import {
10
- useHostAgent,
11
- PAPER_SOURCE,
12
- BIT_DEPTH,
13
- type ScanRequest,
14
- type ScanResult,
15
- } from '../../composables/hostAgent'
16
-
17
- const alert = useAlert()
18
- const host = useHostAgent()
19
-
20
- /**
21
- * Custom events emitted by DeviceScanner.
22
- * Parents can listen to these events to react to user actions and internal state changes.
23
- */
24
- const emit = defineEmits<{
25
- (e: 'scan', files: Base64File[]): void
26
- }>()
27
-
28
- interface Props {
29
- feeder?: boolean // Uses ADF feeder mode instead of flatbed scanning.
30
- duplex?: boolean // Scans both sides of pages when feeder mode supports duplex.
31
- dpi?: number // Scan resolution in DPI for image quality and file size tradeoff.
32
- quality?: number // Compression/quality value used when exporting scanned images.
33
- /** UI string: 'color' | 'grey' | 'bw' */
34
- color?: string // Vuetify color name applied to the visual element.
35
- maxSize?: number // Maximum allowed output size (MB) before upload is blocked.
36
- }
37
-
38
- /**
39
- * Public props accepted by DeviceScanner.
40
- * Document each prop field with intent, defaults, and side effects for clear generated docs.
41
- */
42
- const props = withDefaults(defineProps<Props>(), {
43
- feeder: false,
44
- duplex: false,
45
- dpi: 200,
46
- quality: 80,
47
- color: 'color',
48
- maxSize: 5,
49
- })
50
-
51
- /**
52
- * ✅ Model directly matches HostAgent ScanRequest
53
- */
54
- const scannerOptions = ref<ScanRequest>({
55
- dpi: props.dpi,
56
- quality: props.quality,
57
- paperSource: props.duplex
58
- ? PAPER_SOURCE.Duplex
59
- : props.feeder
60
- ? PAPER_SOURCE.Feeder
61
- : PAPER_SOURCE.Flatbed,
62
- bitDepth:
63
- props.color === 'bw'
64
- ? BIT_DEPTH.BlackAndWhite
65
- : props.color === 'grey'
66
- ? BIT_DEPTH.Grayscale
67
- : BIT_DEPTH.Color,
68
- })
69
-
70
- watch(() => props.dpi, () => {
71
- if (props.dpi) scannerOptions.value.dpi = props.dpi
72
- })
73
- watch(() => props.quality, () => {
74
- if (props.quality) scannerOptions.value.quality = props.quality
75
- })
76
-
77
- /**
78
- * Mapping
79
- * - Flatbed => feeder=false, duplex=false
80
- * - Feeder => feeder=true, duplex=false
81
- * - Duplex => feeder=true, duplex=true
82
- */
83
-
84
- // duplex is true only when paperSource is Duplex
85
- const uiDuplex = computed<boolean>({
86
- get: () => scannerOptions.value.paperSource === PAPER_SOURCE.Duplex,
87
- set(v) {
88
- if (v) {
89
- // duplex ON => MUST be feeder => paperSource Duplex
90
- scannerOptions.value.paperSource = PAPER_SOURCE.Duplex
91
- } else {
92
- // duplex OFF => if currently duplex, fall back to Feeder (because feeder is still on)
93
- // user can still later turn feeder off to go Flatbed
94
- if (scannerOptions.value.paperSource === PAPER_SOURCE.Duplex) {
95
- scannerOptions.value.paperSource = PAPER_SOURCE.Feeder
96
- }
97
- // if already Feeder/Flatbed, do nothing
98
- }
99
- },
100
- })
101
-
102
- // feeder is true when paperSource is Feeder or Duplex
103
- const uiFeeder = computed<boolean>({
104
- get: () =>
105
- scannerOptions.value.paperSource === PAPER_SOURCE.Feeder ||
106
- scannerOptions.value.paperSource === PAPER_SOURCE.Duplex,
107
- set(v) {
108
- if (v) {
109
- // feeder ON => keep duplex state if duplex already on, else go Feeder
110
- scannerOptions.value.paperSource = uiDuplex.value ? PAPER_SOURCE.Duplex : PAPER_SOURCE.Feeder
111
- } else {
112
- // feeder OFF => duplex must be OFF too => go Flatbed
113
- scannerOptions.value.paperSource = PAPER_SOURCE.Flatbed
114
- }
115
- },
116
- })
117
-
118
-
119
- const uiColor = computed<string>({
120
- get() {
121
- const b = scannerOptions.value.bitDepth
122
- if (b === BIT_DEPTH.BlackAndWhite) return 'bw'
123
- if (b === BIT_DEPTH.Grayscale) return 'grey'
124
- return 'color'
125
- },
126
- set(v) {
127
- const vv = (v || 'color').toLowerCase()
128
- scannerOptions.value.bitDepth =
129
- vv === 'bw'
130
- ? BIT_DEPTH.BlackAndWhite
131
- : vv === 'grey' || vv === 'gray'
132
- ? BIT_DEPTH.Grayscale
133
- : BIT_DEPTH.Color
134
- },
135
- })
136
-
137
- watch(() => props.duplex, () => {
138
- uiDuplex.value = props.duplex
139
- })
140
- watch(() => props.feeder, () => {
141
- uiFeeder.value = props.feeder
142
- })
143
- watch(() => props.color, () => {
144
- if (props.color) uiColor.value = props.color
145
- })
146
-
147
- function scanResultsToBase64Files(results: ScanResult[]): Base64File[] {
148
- return results.map((r, idx) => ({
149
- fileName: `scan_${idx + 1}.png`,
150
- base64String: r.base64String.startsWith('data:')
151
- ? r.base64String
152
- : `data:image/png;base64,${r.base64String}`,
153
- }))
154
- }
155
-
156
- const isScanning = ref(false)
157
-
158
- async function performScan() {
159
- try {
160
- isScanning.value = true
161
- const results = await host.scan(scannerOptions.value)
162
- emit('scan', scanResultsToBase64Files(results || []))
163
- } catch (e: any) {
164
- const msg = e?.data?.detail || e?.data?.title || e?.message || 'Scan failed'
165
- alert?.addAlert({ message: msg, alertType: 'error' })
166
- } finally {
167
- isScanning.value = false
168
- }
169
- }
170
-
171
- /**
172
- * ✅ Upload helpers (restored)
173
- */
174
- function fileToBase64(file: File) {
175
- const maxSize = props.maxSize * 1048576
176
-
177
- return new Promise<Base64File>((resolve, reject) => {
178
- if (file.size > maxSize) reject(`File (${file.name}) size exceeds the ${props.maxSize} MB limit.`)
179
-
180
- const reader = new FileReader()
181
- reader.onload = (event) => {
182
- resolve({ fileName: file.name, base64String: event.target?.result as string })
183
- }
184
- reader.onerror = reject
185
- reader.readAsDataURL(file)
186
- })
187
- }
188
-
189
- function performFileUpload(files: File | File[] | undefined) {
190
- if (!files) return
191
-
192
- const allFiles = Array.isArray(files) ? files : [files]
193
- const base64Promises = allFiles.map(fileToBase64)
194
-
195
- Promise.all(base64Promises)
196
- .then((base64Strings) => emit('scan', base64Strings))
197
- .catch((error) => alert?.addAlert({ message: String(error), alertType: 'error' }))
198
- }
199
-
200
- /**
201
- * Slot props grouping (so you can do ops.performScan etc.)
202
- */
203
- const scannerOptionsProps = computed(() => scannerOptions.value)
204
- const configHelper = reactive({
205
- get feeder() { return uiFeeder.value },
206
- set feeder(v: boolean) { uiFeeder.value = v },
207
-
208
- get duplex() { return uiDuplex.value },
209
- set duplex(v: boolean) { uiDuplex.value = v },
210
-
211
- get color() { return uiColor.value },
212
- set color(v: string) { uiColor.value = v },
213
- })
214
- const operation = { performScan, performFileUpload, fileToBase64}
215
- </script>
216
-
217
- <template>
218
- <div class="scanner">
219
- <slot :scannerOptions="scannerOptionsProps" :configHelper="configHelper" :operation="operation" :isScanning="isScanning">
220
- <v-card>
221
- <v-card-text>
222
- <form-pad v-model="scannerOptions">
223
- <template #default>
224
- <!-- Upload -->
225
- <v-row>
226
- <v-col cols="12">
227
- <p>Upload a New Files</p>
228
- </v-col>
229
-
230
- <v-col cols="12">
231
- <file-btn
232
- @update:modelValue="performFileUpload"
233
- block
234
- multiple
235
- variant="tonal"
236
- rounded="xl"
237
- >
238
- <v-icon>mdi mdi-tray-arrow-up</v-icon>
239
- Upload file
240
- </file-btn>
241
- </v-col>
242
- </v-row>
243
-
244
- <!-- Divider -->
245
- <v-row>
246
- <v-col cols="12" style="text-align: center;">
247
- <p>Or</p>
248
- </v-col>
249
- </v-row>
250
-
251
- <!-- Scan title -->
252
- <v-row>
253
- <v-col cols="12">
254
- <p>Scan a New Files</p>
255
- </v-col>
256
- </v-row>
257
-
258
- <!-- Scan controls -->
259
- <v-row>
260
- <v-col cols="12" class="py-0">
261
- <v-switch
262
- color="primary"
263
- v-model="uiFeeder"
264
- label="Enable feeder"
265
- hide-details
266
- density="compact"
267
- />
268
- </v-col>
269
-
270
- <v-col cols="12" class="py-0">
271
- <v-switch
272
- color="primary"
273
- v-model="uiDuplex"
274
- label="Enable duplex"
275
- hide-details
276
- density="compact"
277
- />
278
- </v-col>
279
-
280
- <v-col cols="12">
281
- <p>choose color mode</p>
282
-
283
- <v-btn-toggle
284
- v-model="uiColor"
285
- variant="tonal"
286
- mandatory
287
- >
288
- <v-btn value="color" prepend-icon="mdi mdi-palette-outline">
289
- <span>Color</span>
290
- </v-btn>
291
-
292
- <v-btn value="grey" prepend-icon="mdi mdi-circle">
293
- <span>Grayscale</span>
294
- </v-btn>
295
-
296
- <v-btn value="bw" prepend-icon="mdi mdi-circle-half-full">
297
- <span>Black&amp;White</span>
298
- </v-btn>
299
- </v-btn-toggle>
300
- </v-col>
301
-
302
- <v-col cols="12">
303
- <v-slider
304
- v-model="scannerOptions.dpi"
305
- :max="200"
306
- :min="100"
307
- :ticks="{ 100: 'Low', 150: 'Standard', 200: 'High' }"
308
- show-ticks="always"
309
- step="50"
310
- tick-size="4"
311
- label="Resolution"
312
- hide-details
313
- />
314
- </v-col>
315
-
316
- <v-col cols="12">
317
- <v-slider
318
- v-model="scannerOptions.quality"
319
- :max="100"
320
- :min="60"
321
- :ticks="{ 60: 'Low', 80: 'Standard', 100: 'High' }"
322
- show-ticks="always"
323
- step="5"
324
- tick-size="4"
325
- label="Quality"
326
- hide-details
327
- />
328
- </v-col>
329
-
330
- <v-col cols="12">
331
- <v-btn
332
- @click="performScan"
333
- :loading="isScanning"
334
- :disabled="isScanning"
335
- block
336
- variant="tonal"
337
- rounded="xl"
338
- >
339
- <v-icon>mdi mdi-scanner</v-icon>
340
- Start Scanning
341
- </v-btn>
342
- </v-col>
343
- </v-row>
344
- </template>
345
- </form-pad>
346
- </v-card-text>
347
- </v-card>
348
- </slot>
349
- </div>
350
- </template>
1
+ <script lang="ts" setup>
2
+ /**
3
+ * DeviceScanner bridges UI actions with host-agent hardware/device operations and emits runtime scan/read results.
4
+ * This doc block is consumed by vue-docgen for generated API documentation.
5
+ */
6
+ import { computed, ref, watch, reactive } from 'vue'
7
+ import type { Base64File } from '../../composables/assetFile'
8
+ import { useAlert } from '../../composables/alert'
9
+ import {
10
+ useHostAgent,
11
+ PAPER_SOURCE,
12
+ BIT_DEPTH,
13
+ type ScanRequest,
14
+ type ScanResult,
15
+ } from '../../composables/hostAgent'
16
+
17
+ const alert = useAlert()
18
+ const host = useHostAgent()
19
+
20
+ /**
21
+ * Custom events emitted by DeviceScanner.
22
+ * Parents can listen to these events to react to user actions and internal state changes.
23
+ */
24
+ const emit = defineEmits<{
25
+ (e: 'scan', files: Base64File[]): void
26
+ }>()
27
+
28
+ interface Props {
29
+ feeder?: boolean // Uses ADF feeder mode instead of flatbed scanning.
30
+ duplex?: boolean // Scans both sides of pages when feeder mode supports duplex.
31
+ dpi?: number // Scan resolution in DPI for image quality and file size tradeoff.
32
+ quality?: number // Compression/quality value used when exporting scanned images.
33
+ /** UI string: 'color' | 'grey' | 'bw' */
34
+ color?: string // Vuetify color name applied to the visual element.
35
+ maxSize?: number // Maximum allowed output size (MB) before upload is blocked.
36
+ }
37
+
38
+ /**
39
+ * Public props accepted by DeviceScanner.
40
+ * Document each prop field with intent, defaults, and side effects for clear generated docs.
41
+ */
42
+ const props = withDefaults(defineProps<Props>(), {
43
+ feeder: false,
44
+ duplex: false,
45
+ dpi: 200,
46
+ quality: 80,
47
+ color: 'color',
48
+ maxSize: 5,
49
+ })
50
+
51
+ /**
52
+ * ✅ Model directly matches HostAgent ScanRequest
53
+ */
54
+ const scannerOptions = ref<ScanRequest>({
55
+ dpi: props.dpi,
56
+ quality: props.quality,
57
+ paperSource: props.duplex
58
+ ? PAPER_SOURCE.Duplex
59
+ : props.feeder
60
+ ? PAPER_SOURCE.Feeder
61
+ : PAPER_SOURCE.Flatbed,
62
+ bitDepth:
63
+ props.color === 'bw'
64
+ ? BIT_DEPTH.BlackAndWhite
65
+ : props.color === 'grey'
66
+ ? BIT_DEPTH.Grayscale
67
+ : BIT_DEPTH.Color,
68
+ })
69
+
70
+ watch(() => props.dpi, () => {
71
+ if (props.dpi) scannerOptions.value.dpi = props.dpi
72
+ })
73
+ watch(() => props.quality, () => {
74
+ if (props.quality) scannerOptions.value.quality = props.quality
75
+ })
76
+
77
+ /**
78
+ * Mapping
79
+ * - Flatbed => feeder=false, duplex=false
80
+ * - Feeder => feeder=true, duplex=false
81
+ * - Duplex => feeder=true, duplex=true
82
+ */
83
+
84
+ // duplex is true only when paperSource is Duplex
85
+ const uiDuplex = computed<boolean>({
86
+ get: () => scannerOptions.value.paperSource === PAPER_SOURCE.Duplex,
87
+ set(v) {
88
+ if (v) {
89
+ // duplex ON => MUST be feeder => paperSource Duplex
90
+ scannerOptions.value.paperSource = PAPER_SOURCE.Duplex
91
+ } else {
92
+ // duplex OFF => if currently duplex, fall back to Feeder (because feeder is still on)
93
+ // user can still later turn feeder off to go Flatbed
94
+ if (scannerOptions.value.paperSource === PAPER_SOURCE.Duplex) {
95
+ scannerOptions.value.paperSource = PAPER_SOURCE.Feeder
96
+ }
97
+ // if already Feeder/Flatbed, do nothing
98
+ }
99
+ },
100
+ })
101
+
102
+ // feeder is true when paperSource is Feeder or Duplex
103
+ const uiFeeder = computed<boolean>({
104
+ get: () =>
105
+ scannerOptions.value.paperSource === PAPER_SOURCE.Feeder ||
106
+ scannerOptions.value.paperSource === PAPER_SOURCE.Duplex,
107
+ set(v) {
108
+ if (v) {
109
+ // feeder ON => keep duplex state if duplex already on, else go Feeder
110
+ scannerOptions.value.paperSource = uiDuplex.value ? PAPER_SOURCE.Duplex : PAPER_SOURCE.Feeder
111
+ } else {
112
+ // feeder OFF => duplex must be OFF too => go Flatbed
113
+ scannerOptions.value.paperSource = PAPER_SOURCE.Flatbed
114
+ }
115
+ },
116
+ })
117
+
118
+
119
+ const uiColor = computed<string>({
120
+ get() {
121
+ const b = scannerOptions.value.bitDepth
122
+ if (b === BIT_DEPTH.BlackAndWhite) return 'bw'
123
+ if (b === BIT_DEPTH.Grayscale) return 'grey'
124
+ return 'color'
125
+ },
126
+ set(v) {
127
+ const vv = (v || 'color').toLowerCase()
128
+ scannerOptions.value.bitDepth =
129
+ vv === 'bw'
130
+ ? BIT_DEPTH.BlackAndWhite
131
+ : vv === 'grey' || vv === 'gray'
132
+ ? BIT_DEPTH.Grayscale
133
+ : BIT_DEPTH.Color
134
+ },
135
+ })
136
+
137
+ watch(() => props.duplex, () => {
138
+ uiDuplex.value = props.duplex
139
+ })
140
+ watch(() => props.feeder, () => {
141
+ uiFeeder.value = props.feeder
142
+ })
143
+ watch(() => props.color, () => {
144
+ if (props.color) uiColor.value = props.color
145
+ })
146
+
147
+ function scanResultsToBase64Files(results: ScanResult[]): Base64File[] {
148
+ return results.map((r, idx) => ({
149
+ fileName: `scan_${idx + 1}.png`,
150
+ base64String: r.base64String.startsWith('data:')
151
+ ? r.base64String
152
+ : `data:image/png;base64,${r.base64String}`,
153
+ }))
154
+ }
155
+
156
+ const isScanning = ref(false)
157
+
158
+ async function performScan() {
159
+ try {
160
+ isScanning.value = true
161
+ const results = await host.scan(scannerOptions.value)
162
+ emit('scan', scanResultsToBase64Files(results || []))
163
+ } catch (e: any) {
164
+ const msg = e?.data?.detail || e?.data?.title || e?.message || 'Scan failed'
165
+ alert?.addAlert({ message: msg, alertType: 'error' })
166
+ } finally {
167
+ isScanning.value = false
168
+ }
169
+ }
170
+
171
+ /**
172
+ * ✅ Upload helpers (restored)
173
+ */
174
+ function fileToBase64(file: File) {
175
+ const maxSize = props.maxSize * 1048576
176
+
177
+ return new Promise<Base64File>((resolve, reject) => {
178
+ if (file.size > maxSize) reject(`File (${file.name}) size exceeds the ${props.maxSize} MB limit.`)
179
+
180
+ const reader = new FileReader()
181
+ reader.onload = (event) => {
182
+ resolve({ fileName: file.name, base64String: event.target?.result as string })
183
+ }
184
+ reader.onerror = reject
185
+ reader.readAsDataURL(file)
186
+ })
187
+ }
188
+
189
+ function performFileUpload(files: File | File[] | undefined) {
190
+ if (!files) return
191
+
192
+ const allFiles = Array.isArray(files) ? files : [files]
193
+ const base64Promises = allFiles.map(fileToBase64)
194
+
195
+ Promise.all(base64Promises)
196
+ .then((base64Strings) => emit('scan', base64Strings))
197
+ .catch((error) => alert?.addAlert({ message: String(error), alertType: 'error' }))
198
+ }
199
+
200
+ /**
201
+ * Slot props grouping (so you can do ops.performScan etc.)
202
+ */
203
+ const scannerOptionsProps = computed(() => scannerOptions.value)
204
+ const configHelper = reactive({
205
+ get feeder() { return uiFeeder.value },
206
+ set feeder(v: boolean) { uiFeeder.value = v },
207
+
208
+ get duplex() { return uiDuplex.value },
209
+ set duplex(v: boolean) { uiDuplex.value = v },
210
+
211
+ get color() { return uiColor.value },
212
+ set color(v: string) { uiColor.value = v },
213
+ })
214
+ const operation = { performScan, performFileUpload, fileToBase64}
215
+ </script>
216
+
217
+ <template>
218
+ <div class="scanner">
219
+ <slot :scannerOptions="scannerOptionsProps" :configHelper="configHelper" :operation="operation" :isScanning="isScanning">
220
+ <v-card>
221
+ <v-card-text>
222
+ <form-pad v-model="scannerOptions">
223
+ <template #default>
224
+ <!-- Upload -->
225
+ <v-row>
226
+ <v-col cols="12">
227
+ <p>Upload a New Files</p>
228
+ </v-col>
229
+
230
+ <v-col cols="12">
231
+ <file-btn
232
+ @update:modelValue="performFileUpload"
233
+ block
234
+ multiple
235
+ variant="tonal"
236
+ rounded="xl"
237
+ >
238
+ <v-icon>mdi mdi-tray-arrow-up</v-icon>
239
+ Upload file
240
+ </file-btn>
241
+ </v-col>
242
+ </v-row>
243
+
244
+ <!-- Divider -->
245
+ <v-row>
246
+ <v-col cols="12" style="text-align: center;">
247
+ <p>Or</p>
248
+ </v-col>
249
+ </v-row>
250
+
251
+ <!-- Scan title -->
252
+ <v-row>
253
+ <v-col cols="12">
254
+ <p>Scan a New Files</p>
255
+ </v-col>
256
+ </v-row>
257
+
258
+ <!-- Scan controls -->
259
+ <v-row>
260
+ <v-col cols="12" class="py-0">
261
+ <v-switch
262
+ color="primary"
263
+ v-model="uiFeeder"
264
+ label="Enable feeder"
265
+ hide-details
266
+ density="compact"
267
+ />
268
+ </v-col>
269
+
270
+ <v-col cols="12" class="py-0">
271
+ <v-switch
272
+ color="primary"
273
+ v-model="uiDuplex"
274
+ label="Enable duplex"
275
+ hide-details
276
+ density="compact"
277
+ />
278
+ </v-col>
279
+
280
+ <v-col cols="12">
281
+ <p>choose color mode</p>
282
+
283
+ <v-btn-toggle
284
+ v-model="uiColor"
285
+ variant="tonal"
286
+ mandatory
287
+ >
288
+ <v-btn value="color" prepend-icon="mdi mdi-palette-outline">
289
+ <span>Color</span>
290
+ </v-btn>
291
+
292
+ <v-btn value="grey" prepend-icon="mdi mdi-circle">
293
+ <span>Grayscale</span>
294
+ </v-btn>
295
+
296
+ <v-btn value="bw" prepend-icon="mdi mdi-circle-half-full">
297
+ <span>Black&amp;White</span>
298
+ </v-btn>
299
+ </v-btn-toggle>
300
+ </v-col>
301
+
302
+ <v-col cols="12">
303
+ <v-slider
304
+ v-model="scannerOptions.dpi"
305
+ :max="200"
306
+ :min="100"
307
+ :ticks="{ 100: 'Low', 150: 'Standard', 200: 'High' }"
308
+ show-ticks="always"
309
+ step="50"
310
+ tick-size="4"
311
+ label="Resolution"
312
+ hide-details
313
+ />
314
+ </v-col>
315
+
316
+ <v-col cols="12">
317
+ <v-slider
318
+ v-model="scannerOptions.quality"
319
+ :max="100"
320
+ :min="60"
321
+ :ticks="{ 60: 'Low', 80: 'Standard', 100: 'High' }"
322
+ show-ticks="always"
323
+ step="5"
324
+ tick-size="4"
325
+ label="Quality"
326
+ hide-details
327
+ />
328
+ </v-col>
329
+
330
+ <v-col cols="12">
331
+ <v-btn
332
+ @click="performScan"
333
+ :loading="isScanning"
334
+ :disabled="isScanning"
335
+ block
336
+ variant="tonal"
337
+ rounded="xl"
338
+ >
339
+ <v-icon>mdi mdi-scanner</v-icon>
340
+ Start Scanning
341
+ </v-btn>
342
+ </v-col>
343
+ </v-row>
344
+ </template>
345
+ </form-pad>
346
+ </v-card-text>
347
+ </v-card>
348
+ </slot>
349
+ </div>
350
+ </template>