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