parseet 0.2.0__py3-none-any.whl

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.
parseet/main.qml ADDED
@@ -0,0 +1,1057 @@
1
+ // main.qml
2
+ import QtQuick
3
+ import QtQuick.Controls
4
+ import QtQuick.Layouts
5
+ import QtQuick.Dialogs
6
+
7
+ ApplicationWindow {
8
+ id: root
9
+ visible: true
10
+ width: 720
11
+ height: 680
12
+ minimumWidth: 600
13
+ minimumHeight: 520
14
+ title: "Samplesheet Tools"
15
+
16
+ // ── Palette ────────────────────────────────────────────────────────────
17
+ readonly property color bg: "#0f1117"
18
+ readonly property color surface: "#181c26"
19
+ readonly property color surfaceHigh: "#1e2334"
20
+ readonly property color border: "#272d3d"
21
+ readonly property color accent: "#4f8ef7"
22
+ readonly property color accentDim: "#2a4a8a"
23
+ readonly property color textPrimary: "#e8eaf0"
24
+ readonly property color textMuted: "#6b7280"
25
+ readonly property color ok: "#34d399"
26
+ readonly property color warn: "#fbbf24"
27
+ readonly property color err: "#f87171"
28
+
29
+ color: bg
30
+ font.family: "Menlo, Consolas, monospace"
31
+
32
+ // ── Accumulated integration-file list (Create mode) ────────────────────
33
+ ListModel {
34
+ id: integrationFilesModel
35
+ }
36
+
37
+ function addIntegrationFiles(urls) {
38
+ // urls is a list of QUrl strings from the dialog
39
+ var existing = {};
40
+ for (var i = 0; i < integrationFilesModel.count; i++)
41
+ existing[integrationFilesModel.get(i).path] = true;
42
+ for (var j = 0; j < urls.length; j++) {
43
+ var p = urls[j].toString().replace(/^file:\/\//, "");
44
+ if (!existing[p]) {
45
+ integrationFilesModel.append({
46
+ path: p
47
+ });
48
+ existing[p] = true;
49
+ }
50
+ }
51
+ }
52
+
53
+ function integrationFilePaths() {
54
+ var out = [];
55
+ for (var i = 0; i < integrationFilesModel.count; i++)
56
+ out.push(integrationFilesModel.get(i).path);
57
+ return out;
58
+ }
59
+
60
+ // ── Results model ──────────────────────────────────────────────────────
61
+ ListModel {
62
+ id: resultsModel
63
+ }
64
+
65
+ // ── Backend wiring ─────────────────────────────────────────────────────
66
+ Connections {
67
+ target: backend
68
+
69
+ function onRunFinished(errors, warnings, okList) {
70
+ resultsModel.clear();
71
+ for (var i = 0; i < errors.length; i++)
72
+ resultsModel.append({
73
+ tag: "ERR",
74
+ msg: errors[i]
75
+ });
76
+ for (var i = 0; i < warnings.length; i++)
77
+ resultsModel.append({
78
+ tag: "WARN",
79
+ msg: warnings[i]
80
+ });
81
+ for (var i = 0; i < okList.length; i++)
82
+ resultsModel.append({
83
+ tag: "OK",
84
+ msg: okList[i]
85
+ });
86
+ resultsDrawer.open();
87
+ runBtn.state = "idle";
88
+ }
89
+
90
+ function onRunFailed(message) {
91
+ resultsModel.clear();
92
+ resultsModel.append({
93
+ tag: "ERR",
94
+ msg: "Unhandled exception:\n" + message
95
+ });
96
+ resultsDrawer.open();
97
+ runBtn.state = "idle";
98
+ }
99
+
100
+ function onRunStarted() {
101
+ resultsDrawer.close();
102
+ runBtn.state = "running";
103
+ }
104
+ }
105
+
106
+ // ── Helpers ────────────────────────────────────────────────────────────
107
+ function countOf(tag) {
108
+ var n = 0;
109
+ for (var i = 0; i < resultsModel.count; i++)
110
+ if (resultsModel.get(i).tag === tag)
111
+ n++;
112
+ return n;
113
+ }
114
+
115
+ function dispatchRun() {
116
+ if (modeBar.currentIndex === 0) {
117
+ if (!checkSheet.filePath || !checkFolder.filePath) {
118
+ resultsModel.clear();
119
+ resultsModel.append({
120
+ tag: "ERR",
121
+ msg: "Please select both a samplesheet and a raw-files folder."
122
+ });
123
+ resultsDrawer.open();
124
+ return;
125
+ }
126
+ backend.runCheck(checkSheet.filePath, checkFolder.filePath, checkDryRun.checked, checkIgnoreId.checked);
127
+ } else {
128
+ var files = integrationFilePaths();
129
+ if (!createSheet.filePath || files.length === 0 || !createOutdir.filePath) {
130
+ resultsModel.clear();
131
+ resultsModel.append({
132
+ tag: "ERR",
133
+ msg: "Please select a samplesheet, at least one integration file, and an output folder."
134
+ });
135
+ resultsDrawer.open();
136
+ return;
137
+ }
138
+ backend.runCreate(createSheet.filePath, files, createOutdir.filePath, radioMsdial.checked ? "msdial" : "crommy", createDryRun.checked, createIgnoreWarnings.checked);
139
+ }
140
+ }
141
+
142
+ // ══════════════════════════════════════════════════════════════════════
143
+ // Main scrollable content (forms + run button)
144
+ // ══════════════════════════════════════════════════════════════════════
145
+ ScrollView {
146
+ // Leave room at the bottom for the drawer handle when it's visible
147
+ anchors {
148
+ top: parent.top
149
+ left: parent.left
150
+ right: parent.right
151
+ bottom: resultsDrawer.top
152
+ }
153
+ contentWidth: availableWidth
154
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
155
+
156
+ ColumnLayout {
157
+ width: parent.width
158
+ spacing: 0
159
+
160
+ // ── Header ─────────────────────────────────────────────────────
161
+ Rectangle {
162
+ Layout.fillWidth: true
163
+ height: 52
164
+ color: surface
165
+ Rectangle {
166
+ width: parent.width
167
+ height: 1
168
+ anchors.bottom: parent.bottom
169
+ color: border
170
+ }
171
+
172
+ RowLayout {
173
+ anchors {
174
+ fill: parent
175
+ leftMargin: 24
176
+ rightMargin: 24
177
+ }
178
+ spacing: 10
179
+ Rectangle {
180
+ width: 8
181
+ height: 8
182
+ radius: 4
183
+ color: accent
184
+ }
185
+ Text {
186
+ text: "samplesheet"
187
+ font.pixelSize: 15
188
+ font.letterSpacing: 3
189
+ font.weight: Font.Bold
190
+ color: textPrimary
191
+ }
192
+ Text {
193
+ text: "tools"
194
+ font.pixelSize: 15
195
+ font.letterSpacing: 3
196
+ font.weight: Font.Light
197
+ color: textMuted
198
+ }
199
+ Item {
200
+ Layout.fillWidth: true
201
+ }
202
+ Text {
203
+ text: modeBar.currentIndex === 0 ? "CHECK ORDER" : "CREATE"
204
+ font.pixelSize: 11
205
+ font.letterSpacing: 2
206
+ color: accent
207
+ }
208
+ }
209
+ }
210
+
211
+ // ── Mode tab bar ───────────────────────────────────────────────
212
+ Rectangle {
213
+ Layout.fillWidth: true
214
+ height: 56
215
+ color: bg
216
+
217
+ TabBar {
218
+ id: modeBar
219
+ anchors.centerIn: parent
220
+ width: 300
221
+ height: 36
222
+ background: Rectangle {
223
+ color: surface
224
+ radius: 6
225
+ border.color: border
226
+ border.width: 1
227
+ }
228
+
229
+ TabButton {
230
+ text: "Check Order"
231
+ width: 150
232
+ height: 36
233
+ contentItem: Text {
234
+ text: parent.text
235
+ font.pixelSize: 12
236
+ font.letterSpacing: 1
237
+ color: modeBar.currentIndex === 0 ? textPrimary : textMuted
238
+ horizontalAlignment: Text.AlignHCenter
239
+ verticalAlignment: Text.AlignVCenter
240
+ }
241
+ background: Rectangle {
242
+ color: modeBar.currentIndex === 0 ? accentDim : "transparent"
243
+ radius: 5
244
+ }
245
+ }
246
+ TabButton {
247
+ text: "Create"
248
+ width: 150
249
+ height: 36
250
+ contentItem: Text {
251
+ text: parent.text
252
+ font.pixelSize: 12
253
+ font.letterSpacing: 1
254
+ color: modeBar.currentIndex === 1 ? textPrimary : textMuted
255
+ horizontalAlignment: Text.AlignHCenter
256
+ verticalAlignment: Text.AlignVCenter
257
+ }
258
+ background: Rectangle {
259
+ color: modeBar.currentIndex === 1 ? accentDim : "transparent"
260
+ radius: 5
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ // ── CHECK ORDER form ───────────────────────────────────────────
267
+ ColumnLayout {
268
+ visible: modeBar.currentIndex === 0
269
+ Layout.fillWidth: true
270
+ Layout.leftMargin: 24
271
+ Layout.rightMargin: 24
272
+ Layout.topMargin: 8
273
+ Layout.bottomMargin: 8
274
+ spacing: 14
275
+
276
+ FilePickerRow {
277
+ id: checkSheet
278
+ label: "Samplesheet"
279
+ placeholder: "Select Excel samplesheet…"
280
+ nameFilters: ["Excel files (*.xlsx *.xls)"]
281
+ }
282
+ FilePickerRow {
283
+ id: checkFolder
284
+ label: "Raw folder"
285
+ placeholder: "Select folder containing .raw files…"
286
+ selectFolder: true
287
+ }
288
+ RowLayout {
289
+ spacing: 32
290
+ Layout.topMargin: 4
291
+ OptionToggle {
292
+ id: checkDryRun
293
+ label: "Dry run"
294
+ checked: true
295
+ }
296
+ OptionToggle {
297
+ id: checkIgnoreId
298
+ label: "Ignore ID"
299
+ checked: false
300
+ }
301
+ }
302
+ Item {
303
+ height: 4
304
+ }
305
+ }
306
+
307
+ // ── CREATE form ────────────────────────────────────────────────
308
+ ColumnLayout {
309
+ visible: modeBar.currentIndex === 1
310
+ Layout.fillWidth: true
311
+ Layout.leftMargin: 24
312
+ Layout.rightMargin: 24
313
+ Layout.topMargin: 8
314
+ Layout.bottomMargin: 8
315
+ spacing: 14
316
+
317
+ FilePickerRow {
318
+ id: createSheet
319
+ label: "Samplesheet"
320
+ placeholder: "Select Excel samplesheet…"
321
+ nameFilters: ["Excel files (*.xlsx *.xls)"]
322
+ }
323
+
324
+ // ── Integration file accumulator ───────────────────────────
325
+ ColumnLayout {
326
+ Layout.fillWidth: true
327
+ spacing: 6
328
+
329
+ // Label row + "+" button
330
+ RowLayout {
331
+ spacing: 0
332
+ Layout.fillWidth: true
333
+
334
+ Text {
335
+ text: "Integration files"
336
+ font.pixelSize: 12
337
+ font.letterSpacing: 1
338
+ color: textMuted
339
+ Layout.preferredWidth: 120
340
+ }
341
+
342
+ // File count indicator
343
+ Text {
344
+ text: integrationFilesModel.count === 0 ? "no files added" : integrationFilesModel.count + " file" + (integrationFilesModel.count > 1 ? "s" : "")
345
+ font.pixelSize: 12
346
+ color: integrationFilesModel.count === 0 ? textMuted : accent
347
+ }
348
+
349
+ Item {
350
+ Layout.fillWidth: true
351
+ }
352
+
353
+ // Clear all button — only shown when there are files
354
+ Rectangle {
355
+ visible: integrationFilesModel.count > 0
356
+ width: 60
357
+ height: 26
358
+ radius: 4
359
+ color: clearAllMouse.containsMouse ? Qt.rgba(0.97, 0.44, 0.44, 0.15) : "transparent"
360
+ border.color: Qt.rgba(0.97, 0.44, 0.44, 0.35)
361
+ border.width: 1
362
+ Behavior on color {
363
+ ColorAnimation {
364
+ duration: 80
365
+ }
366
+ }
367
+ Text {
368
+ anchors.centerIn: parent
369
+ text: "clear all"
370
+ font.pixelSize: 10
371
+ font.letterSpacing: 1
372
+ color: err
373
+ }
374
+ MouseArea {
375
+ id: clearAllMouse
376
+ anchors.fill: parent
377
+ hoverEnabled: true
378
+ cursorShape: Qt.PointingHandCursor
379
+ onClicked: integrationFilesModel.clear()
380
+ }
381
+ }
382
+
383
+ Item {
384
+ width: 8
385
+ }
386
+
387
+ // "+" add button
388
+ Rectangle {
389
+ width: 26
390
+ height: 26
391
+ radius: 4
392
+ color: addBtnMouse.containsMouse ? accentDim : "transparent"
393
+ border.color: accent
394
+ border.width: 1
395
+ Behavior on color {
396
+ ColorAnimation {
397
+ duration: 80
398
+ }
399
+ }
400
+ Text {
401
+ anchors.centerIn: parent
402
+ text: "+"
403
+ font.pixelSize: 16
404
+ color: accent
405
+ }
406
+ MouseArea {
407
+ id: addBtnMouse
408
+ anchors.fill: parent
409
+ hoverEnabled: true
410
+ cursorShape: Qt.PointingHandCursor
411
+ onClicked: addFilesDlg.open()
412
+ }
413
+
414
+ FileDialog {
415
+ id: addFilesDlg
416
+ nameFilters: ["Excel / TSV files (*.xlsx *.xls *.tsv *.txt)", "All files (*)"]
417
+ fileMode: FileDialog.OpenFiles
418
+ onAccepted: addIntegrationFiles(Array.from(selectedFiles))
419
+ }
420
+ }
421
+ }
422
+
423
+ // Scrollable chip list of added files
424
+ Rectangle {
425
+ visible: integrationFilesModel.count > 0
426
+ Layout.fillWidth: true
427
+ // Grow up to ~8 chips tall, then scroll
428
+ Layout.preferredHeight: Math.min(integrationFilesModel.count, 8) * 30 + 8
429
+ color: "#12161f"
430
+ radius: 5
431
+ border.color: border
432
+ border.width: 1
433
+ clip: true
434
+
435
+ ScrollView {
436
+ anchors {
437
+ fill: parent
438
+ margins: 4
439
+ }
440
+ contentWidth: availableWidth
441
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
442
+
443
+ ColumnLayout {
444
+ width: parent.width
445
+ spacing: 2
446
+
447
+ Repeater {
448
+ model: integrationFilesModel
449
+ delegate: Rectangle {
450
+ Layout.fillWidth: true
451
+ height: 26
452
+ radius: 4
453
+ color: chipMouse.containsMouse ? surfaceHigh : "transparent"
454
+ Behavior on color {
455
+ ColorAnimation {
456
+ duration: 60
457
+ }
458
+ }
459
+
460
+ RowLayout {
461
+ anchors {
462
+ fill: parent
463
+ leftMargin: 8
464
+ rightMargin: 4
465
+ }
466
+ spacing: 6
467
+
468
+ // Coloured dot
469
+ Rectangle {
470
+ width: 5
471
+ height: 5
472
+ radius: 2.5
473
+ color: accent
474
+ }
475
+
476
+ // Filename (basename only, full path in tooltip)
477
+ Text {
478
+ Layout.fillWidth: true
479
+ text: model.path.split("/").pop()
480
+ font.pixelSize: 11
481
+ color: textPrimary
482
+ elide: Text.ElideMiddle
483
+
484
+ ToolTip.visible: chipMouse.containsMouse
485
+ ToolTip.text: model.path
486
+ ToolTip.delay: 600
487
+ }
488
+
489
+ // × remove button
490
+ Rectangle {
491
+ width: 18
492
+ height: 18
493
+ radius: 3
494
+ color: removeMouse.containsMouse ? Qt.rgba(0.97, 0.44, 0.44, 0.2) : "transparent"
495
+ Behavior on color {
496
+ ColorAnimation {
497
+ duration: 60
498
+ }
499
+ }
500
+ Text {
501
+ anchors.centerIn: parent
502
+ text: "×"
503
+ font.pixelSize: 13
504
+ color: textMuted
505
+ }
506
+ MouseArea {
507
+ id: removeMouse
508
+ anchors.fill: parent
509
+ hoverEnabled: true
510
+ cursorShape: Qt.PointingHandCursor
511
+ onClicked: integrationFilesModel.remove(index)
512
+ }
513
+ }
514
+ }
515
+
516
+ MouseArea {
517
+ id: chipMouse
518
+ anchors.fill: parent
519
+ hoverEnabled: true
520
+ acceptedButtons: Qt.NoButton
521
+ }
522
+ }
523
+ }
524
+ }
525
+ }
526
+ }
527
+ }
528
+
529
+ FilePickerRow {
530
+ id: createOutdir
531
+ label: "Output folder"
532
+ placeholder: "Select output directory…"
533
+ selectFolder: true
534
+ }
535
+
536
+ // Software selector
537
+ RowLayout {
538
+ spacing: 0
539
+ Layout.topMargin: 2
540
+
541
+ Text {
542
+ text: "Software"
543
+ font.pixelSize: 12
544
+ font.letterSpacing: 1
545
+ color: textMuted
546
+ Layout.preferredWidth: 120
547
+ }
548
+
549
+ RowLayout {
550
+ spacing: 24
551
+ RadioChip {
552
+ id: radioMsdial
553
+ label: "msdial"
554
+ checked: true
555
+ onCheckedChanged: if (checked)
556
+ radioCrommy.checked = false
557
+ }
558
+ RadioChip {
559
+ id: radioCrommy
560
+ label: "crommy"
561
+ checked: false
562
+ onCheckedChanged: if (checked)
563
+ radioMsdial.checked = false
564
+ }
565
+ }
566
+ }
567
+
568
+ RowLayout {
569
+ spacing: 32
570
+ Layout.topMargin: 4
571
+ OptionToggle {
572
+ id: createDryRun
573
+ label: "Dry run"
574
+ checked: false
575
+ }
576
+ OptionToggle {
577
+ id: createIgnoreWarnings
578
+ label: "Ignore warnings"
579
+ checked: false
580
+ }
581
+ }
582
+
583
+ Item {
584
+ height: 4
585
+ }
586
+ }
587
+
588
+ // ── Run button ─────────────────────────────────────────────────
589
+ Rectangle {
590
+ id: runBtn
591
+ Layout.fillWidth: true
592
+ Layout.leftMargin: 24
593
+ Layout.rightMargin: 24
594
+ Layout.topMargin: 4
595
+ height: 44
596
+ radius: 6
597
+ state: "idle"
598
+ color: runBtn.state === "running" ? accentDim : runBtnMouse.containsMouse ? Qt.lighter(accent, 1.15) : accent
599
+ Behavior on color {
600
+ ColorAnimation {
601
+ duration: 120
602
+ }
603
+ }
604
+
605
+ RowLayout {
606
+ anchors.centerIn: parent
607
+ spacing: 10
608
+
609
+ Rectangle {
610
+ width: 14
611
+ height: 14
612
+ radius: 7
613
+ color: "transparent"
614
+ border.color: textPrimary
615
+ border.width: 2
616
+ visible: runBtn.state === "running"
617
+ Rectangle {
618
+ width: 6
619
+ height: 6
620
+ radius: 3
621
+ color: textPrimary
622
+ anchors {
623
+ top: parent.top
624
+ left: parent.left
625
+ }
626
+ }
627
+ RotationAnimator on rotation {
628
+ running: runBtn.state === "running"
629
+ from: 0
630
+ to: 360
631
+ duration: 900
632
+ loops: Animation.Infinite
633
+ }
634
+ }
635
+ Text {
636
+ text: runBtn.state === "running" ? "Running…" : "Run"
637
+ font.pixelSize: 13
638
+ font.weight: Font.DemiBold
639
+ font.letterSpacing: 2
640
+ color: textPrimary
641
+ }
642
+ }
643
+
644
+ MouseArea {
645
+ id: runBtnMouse
646
+ anchors.fill: parent
647
+ hoverEnabled: true
648
+ cursorShape: Qt.PointingHandCursor
649
+ enabled: runBtn.state !== "running"
650
+ onClicked: dispatchRun()
651
+ }
652
+ }
653
+
654
+ Item {
655
+ height: 24
656
+ }
657
+ }
658
+ }
659
+
660
+ // ══════════════════════════════════════════════════════════════════════
661
+ // Results drawer — anchored to window bottom, always visible on resize
662
+ // ══════════════════════════════════════════════════════════════════════
663
+ Rectangle {
664
+ id: resultsDrawer
665
+
666
+ // Height: collapsed (handle only) or expanded (handle + up to 280px log)
667
+ property bool expanded: false
668
+ property int handleH: 36
669
+ property int maxLogH: 280
670
+
671
+ function open() {
672
+ expanded = true;
673
+ }
674
+ function close() {
675
+ expanded = false;
676
+ }
677
+
678
+ anchors {
679
+ left: parent.left
680
+ right: parent.right
681
+ bottom: parent.bottom
682
+ }
683
+ height: expanded ? handleH + Math.min(resultsListView.contentHeight + 32, maxLogH) : (resultsModel.count > 0 ? handleH : 0)
684
+ color: surface
685
+ border.color: border
686
+ border.width: 1
687
+ // Only round top corners
688
+ radius: 8
689
+ // Clip so the rounded top corners work
690
+ layer.enabled: true
691
+
692
+ Behavior on height {
693
+ NumberAnimation {
694
+ duration: 220
695
+ easing.type: Easing.OutCubic
696
+ }
697
+ }
698
+
699
+ // ── Drag handle / summary bar ──────────────────────────────────────
700
+ Rectangle {
701
+ id: drawerHandle
702
+ anchors {
703
+ top: parent.top
704
+ left: parent.left
705
+ right: parent.right
706
+ }
707
+ height: resultsDrawer.handleH
708
+ color: "transparent"
709
+
710
+ RowLayout {
711
+ anchors {
712
+ fill: parent
713
+ leftMargin: 16
714
+ rightMargin: 16
715
+ }
716
+ spacing: 12
717
+
718
+ // Chevron
719
+ Text {
720
+ text: resultsDrawer.expanded ? "▼" : "▲"
721
+ font.pixelSize: 10
722
+ color: textMuted
723
+ Behavior on text {}
724
+ }
725
+
726
+ Text {
727
+ text: "Results"
728
+ font.pixelSize: 11
729
+ font.letterSpacing: 2
730
+ font.weight: Font.Bold
731
+ color: textMuted
732
+ }
733
+
734
+ Item {
735
+ Layout.fillWidth: true
736
+ }
737
+
738
+ CountBadge {
739
+ count: countOf("ERR")
740
+ badgeColor: err
741
+ label: "errors"
742
+ }
743
+ CountBadge {
744
+ count: countOf("WARN")
745
+ badgeColor: warn
746
+ label: "warnings"
747
+ }
748
+ CountBadge {
749
+ count: countOf("OK")
750
+ badgeColor: ok
751
+ label: "ok"
752
+ }
753
+
754
+ // Dismiss button
755
+ Rectangle {
756
+ width: 22
757
+ height: 22
758
+ radius: 4
759
+ visible: resultsDrawer.expanded
760
+ color: dismissMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.07) : "transparent"
761
+ Behavior on color {
762
+ ColorAnimation {
763
+ duration: 80
764
+ }
765
+ }
766
+ Text {
767
+ anchors.centerIn: parent
768
+ text: "✕"
769
+ font.pixelSize: 11
770
+ color: textMuted
771
+ }
772
+ MouseArea {
773
+ id: dismissMouse
774
+ anchors.fill: parent
775
+ hoverEnabled: true
776
+ cursorShape: Qt.PointingHandCursor
777
+ onClicked: resultsDrawer.close()
778
+ }
779
+ }
780
+ }
781
+
782
+ // Divider shown only when expanded
783
+ Rectangle {
784
+ anchors {
785
+ bottom: parent.bottom
786
+ left: parent.left
787
+ right: parent.right
788
+ }
789
+ height: 1
790
+ color: border
791
+ visible: resultsDrawer.expanded
792
+ }
793
+
794
+ MouseArea {
795
+ anchors.fill: parent
796
+ cursorShape: Qt.PointingHandCursor
797
+ onClicked: resultsDrawer.expanded = !resultsDrawer.expanded
798
+ }
799
+ }
800
+
801
+ // ── Scrollable log ─────────────────────────────────────────────────
802
+ ListView {
803
+ id: resultsListView
804
+ anchors {
805
+ top: drawerHandle.bottom
806
+ left: parent.left
807
+ right: parent.right
808
+ bottom: parent.bottom
809
+ leftMargin: 12
810
+ rightMargin: 12
811
+ topMargin: 8
812
+ bottomMargin: 8
813
+ }
814
+ model: resultsModel
815
+ clip: true
816
+ spacing: 4
817
+ visible: resultsDrawer.expanded
818
+ ScrollBar.vertical: ScrollBar {
819
+ policy: ScrollBar.AsNeeded
820
+ }
821
+
822
+ delegate: RowLayout {
823
+ width: resultsListView.width - 4
824
+ spacing: 10
825
+
826
+ Rectangle {
827
+ width: 36
828
+ height: 16
829
+ radius: 3
830
+ color: model.tag === "ERR" ? Qt.rgba(0.97, 0.44, 0.44, 0.18) : model.tag === "WARN" ? Qt.rgba(0.98, 0.75, 0.14, 0.18) : Qt.rgba(0.20, 0.83, 0.60, 0.18)
831
+ Text {
832
+ anchors.centerIn: parent
833
+ text: model.tag
834
+ font.pixelSize: 9
835
+ font.weight: Font.Bold
836
+ font.letterSpacing: 1
837
+ color: model.tag === "ERR" ? err : model.tag === "WARN" ? warn : ok
838
+ }
839
+ }
840
+
841
+ Text {
842
+ text: model.msg
843
+ font.pixelSize: 12
844
+ Layout.fillWidth: true
845
+ wrapMode: Text.Wrap
846
+ color: model.tag === "ERR" ? Qt.lighter(err, 1.2) : model.tag === "WARN" ? Qt.lighter(warn, 1.1) : textPrimary
847
+ }
848
+ }
849
+ }
850
+ }
851
+
852
+ // ══════════════════════════════════════════════════════════════════════
853
+ // Inline components
854
+ // ══════════════════════════════════════════════════════════════════════
855
+
856
+ // FilePickerRow ─────────────────────────────────────────────────────────
857
+ component FilePickerRow: RowLayout {
858
+ id: pickerRoot
859
+ property string label: "File"
860
+ property string placeholder: "Select…"
861
+ property var nameFilters: []
862
+ property bool selectFolder: false
863
+ property bool selectMultiple: false
864
+ property string filePath: ""
865
+ property var filePaths: []
866
+
867
+ spacing: 0
868
+ Layout.fillWidth: true
869
+ Layout.topMargin: 2
870
+
871
+ Text {
872
+ text: pickerRoot.label
873
+ font.pixelSize: 12
874
+ font.letterSpacing: 1
875
+ color: textMuted
876
+ Layout.preferredWidth: 120
877
+ }
878
+
879
+ Rectangle {
880
+ Layout.fillWidth: true
881
+ height: 34
882
+ radius: 5
883
+ color: "#12161f"
884
+ border.color: fieldMouse.containsMouse ? accent : border
885
+ border.width: 1
886
+ Behavior on border.color {
887
+ ColorAnimation {
888
+ duration: 100
889
+ }
890
+ }
891
+
892
+ Text {
893
+ anchors {
894
+ left: parent.left
895
+ right: parent.right
896
+ verticalCenter: parent.verticalCenter
897
+ leftMargin: 10
898
+ rightMargin: 10
899
+ }
900
+ elide: Text.ElideLeft
901
+ font.pixelSize: 12
902
+ text: {
903
+ if (pickerRoot.selectMultiple) {
904
+ if (pickerRoot.filePaths.length === 0)
905
+ return pickerRoot.placeholder;
906
+ if (pickerRoot.filePaths.length === 1)
907
+ return pickerRoot.filePaths[0];
908
+ return pickerRoot.filePaths.length + " files selected";
909
+ }
910
+ return pickerRoot.filePath || pickerRoot.placeholder;
911
+ }
912
+ color: (text !== pickerRoot.placeholder) ? textPrimary : textMuted
913
+ }
914
+
915
+ MouseArea {
916
+ id: fieldMouse
917
+ anchors.fill: parent
918
+ hoverEnabled: true
919
+ cursorShape: Qt.PointingHandCursor
920
+ onClicked: {
921
+ if (pickerRoot.selectFolder)
922
+ folderDlg.open();
923
+ else if (pickerRoot.selectMultiple)
924
+ multiDlg.open();
925
+ else
926
+ singleDlg.open();
927
+ }
928
+ }
929
+ }
930
+
931
+ FileDialog {
932
+ id: singleDlg
933
+ nameFilters: pickerRoot.nameFilters
934
+ fileMode: FileDialog.OpenFile
935
+ onAccepted: pickerRoot.filePath = selectedFile.toString().replace(/^file:\/\//, "")
936
+ }
937
+ FileDialog {
938
+ id: multiDlg
939
+ nameFilters: pickerRoot.nameFilters
940
+ fileMode: FileDialog.OpenFiles
941
+ onAccepted: pickerRoot.filePaths = Array.from(selectedFiles).map(function (u) {
942
+ return u.toString().replace(/^file:\/\//, "");
943
+ })
944
+ }
945
+ FolderDialog {
946
+ id: folderDlg
947
+ onAccepted: pickerRoot.filePath = selectedFolder.toString().replace(/^file:\/\//, "")
948
+ }
949
+ }
950
+
951
+ // OptionToggle ──────────────────────────────────────────────────────────
952
+ component OptionToggle: RowLayout {
953
+ id: toggleRoot
954
+ property string label: ""
955
+ property bool checked: false
956
+ spacing: 8
957
+
958
+ Rectangle {
959
+ width: 18
960
+ height: 18
961
+ radius: 4
962
+ color: toggleRoot.checked ? accentDim : "transparent"
963
+ border.color: toggleRoot.checked ? accent : textMuted
964
+ border.width: 1
965
+ Behavior on color {
966
+ ColorAnimation {
967
+ duration: 100
968
+ }
969
+ }
970
+ Rectangle {
971
+ anchors.centerIn: parent
972
+ width: 8
973
+ height: 8
974
+ radius: 2
975
+ color: accent
976
+ visible: toggleRoot.checked
977
+ }
978
+ MouseArea {
979
+ anchors.fill: parent
980
+ cursorShape: Qt.PointingHandCursor
981
+ onClicked: toggleRoot.checked = !toggleRoot.checked
982
+ }
983
+ }
984
+ Text {
985
+ text: toggleRoot.label
986
+ font.pixelSize: 12
987
+ color: textMuted
988
+ }
989
+ }
990
+
991
+ // RadioChip ─────────────────────────────────────────────────────────────
992
+ component RadioChip: RowLayout {
993
+ id: radioRoot
994
+ property string label: ""
995
+ property bool checked: false
996
+ spacing: 8
997
+
998
+ Rectangle {
999
+ width: 16
1000
+ height: 16
1001
+ radius: 8
1002
+ color: "transparent"
1003
+ border.color: radioRoot.checked ? accent : textMuted
1004
+ border.width: 1
1005
+ Behavior on border.color {
1006
+ ColorAnimation {
1007
+ duration: 100
1008
+ }
1009
+ }
1010
+ Rectangle {
1011
+ anchors.centerIn: parent
1012
+ width: 8
1013
+ height: 8
1014
+ radius: 4
1015
+ color: accent
1016
+ visible: radioRoot.checked
1017
+ }
1018
+ MouseArea {
1019
+ anchors.fill: parent
1020
+ cursorShape: Qt.PointingHandCursor
1021
+ onClicked: radioRoot.checked = true
1022
+ }
1023
+ }
1024
+ Text {
1025
+ text: radioRoot.label
1026
+ font.pixelSize: 12
1027
+ color: textMuted
1028
+ }
1029
+ }
1030
+
1031
+ // CountBadge ────────────────────────────────────────────────────────────
1032
+ component CountBadge: RowLayout {
1033
+ property int count: 0
1034
+ property color badgeColor: ok
1035
+ property string label: ""
1036
+ spacing: 4
1037
+ Rectangle {
1038
+ width: countTxt.implicitWidth + 12
1039
+ height: 18
1040
+ radius: 9
1041
+ color: Qt.rgba(badgeColor.r, badgeColor.g, badgeColor.b, 0.15)
1042
+ Text {
1043
+ id: countTxt
1044
+ anchors.centerIn: parent
1045
+ text: count
1046
+ font.pixelSize: 11
1047
+ font.weight: Font.Bold
1048
+ color: badgeColor
1049
+ }
1050
+ }
1051
+ Text {
1052
+ text: label
1053
+ font.pixelSize: 11
1054
+ color: textMuted
1055
+ }
1056
+ }
1057
+ }