flea-qml 0.0.0-stage → 0.1.0

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 (625) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +478 -2
  3. package/THIRD-PARTY-NOTICES.md +29 -0
  4. package/demos/App.java +5 -0
  5. package/demos/Dockerfile +4 -0
  6. package/demos/Main.kt +3 -0
  7. package/demos/Program.cs +1 -0
  8. package/demos/README.md +12 -0
  9. package/demos/app.dart +3 -0
  10. package/demos/app.deb +0 -0
  11. package/demos/app.js +1 -0
  12. package/demos/backup.tar +0 -0
  13. package/demos/budget.xlsx +0 -0
  14. package/demos/build.sh +2 -0
  15. package/demos/bundle.zip +0 -0
  16. package/demos/clip.mp4 +0 -0
  17. package/demos/data.sqlite +0 -0
  18. package/demos/disk.iso +0 -0
  19. package/demos/engine.cpp +5 -0
  20. package/demos/firmware.bin +0 -0
  21. package/demos/flake.nix +4 -0
  22. package/demos/folder/inside.txt +1 -0
  23. package/demos/font.ttf +0 -0
  24. package/demos/index.html +5 -0
  25. package/demos/index.php +2 -0
  26. package/demos/init.lua +1 -0
  27. package/demos/letter.docx +0 -0
  28. package/demos/lib.rs +3 -0
  29. package/demos/main.c +6 -0
  30. package/demos/main.h +3 -0
  31. package/demos/math-and-diagrams.md +200 -0
  32. package/demos/notes.tar.gz +0 -0
  33. package/demos/notes.txt +3 -0
  34. package/demos/photo.png +0 -0
  35. package/demos/program +2 -0
  36. package/demos/report.pdf +32 -0
  37. package/demos/script.py +1 -0
  38. package/demos/server.go +7 -0
  39. package/demos/settings.json +4 -0
  40. package/demos/slides.pptx +0 -0
  41. package/demos/song.mp3 +0 -0
  42. package/demos/style.css +4 -0
  43. package/demos/tool.rb +1 -0
  44. package/demos/types.ts +2 -0
  45. package/docs/pele.md +23 -0
  46. package/docs/protocol.md +3420 -0
  47. package/docs/temml.md +63 -0
  48. package/index.js +107 -0
  49. package/package.json +34 -4
  50. package/src/archive.js +402 -0
  51. package/src/archiveops.js +697 -0
  52. package/src/backend.js +369 -0
  53. package/src/bulkrename.js +95 -0
  54. package/src/clip-host.js +76 -0
  55. package/src/clip.js +462 -0
  56. package/src/code.js +168 -0
  57. package/src/compare.js +74 -0
  58. package/src/create.js +128 -0
  59. package/src/dirsize.js +147 -0
  60. package/src/figure-svg.js +530 -0
  61. package/src/figure-thread.js +23 -0
  62. package/src/figure.js +132 -0
  63. package/src/find.js +55 -0
  64. package/src/flea.js +39 -0
  65. package/src/fsinfo.js +39 -0
  66. package/src/highlight.js +141 -0
  67. package/src/html.js +52 -0
  68. package/src/kinds.js +89 -0
  69. package/src/listing.js +130 -0
  70. package/src/markdown-html.js +317 -0
  71. package/src/mathml.js +186 -0
  72. package/src/menu.js +180 -0
  73. package/src/meta.js +129 -0
  74. package/src/office.js +810 -0
  75. package/src/order.js +100 -0
  76. package/src/peek.js +49 -0
  77. package/src/pele-wrapper.js +695 -0
  78. package/src/permissions.js +180 -0
  79. package/src/places.js +313 -0
  80. package/src/preview-language.js +10 -0
  81. package/src/transfer.js +669 -0
  82. package/src/trash-record.js +41 -0
  83. package/src/trash.js +210 -0
  84. package/src/trashbrowse.js +326 -0
  85. package/src/uistate.js +393 -0
  86. package/src/uistore.js +168 -0
  87. package/src/vendor/LICENSES/Apache-2.0.txt +202 -0
  88. package/src/vendor/LICENSES/MIT-katex-2015.txt +27 -0
  89. package/src/vendor/LICENSES/MIT-katex.txt +21 -0
  90. package/src/vendor/LICENSES/MIT-mermaid.txt +21 -0
  91. package/src/vendor/LICENSES/MIT-pele.txt +40 -0
  92. package/src/vendor/LICENSES/MIT-perseus.txt +18 -0
  93. package/src/vendor/LICENSES/MIT-temml.txt +21 -0
  94. package/src/vendor/pele.js +145 -0
  95. package/src/vendor/temml.mjs +14708 -0
  96. package/src/watch.js +98 -0
  97. package/src/wayland/client.js +109 -0
  98. package/src/wayland/protocol.js +55 -0
  99. package/src/wayland/protocols/ext-data-control-v1.xml +276 -0
  100. package/src/wayland/protocols/wayland.xml +3248 -0
  101. package/src/wayland/protocols/wlr-data-control-unstable-v1.xml +278 -0
  102. package/src/wayland/resource-io.js +6 -0
  103. package/src/wayland/socket-discovery.js +26 -0
  104. package/src/wayland/transport-posix.js +120 -0
  105. package/src/wayland/transport-win32.js +11 -0
  106. package/src/wayland/transport.js +3 -0
  107. package/src/wayland/win32-resources.js +80 -0
  108. package/src/wayland/win32-wire.js +62 -0
  109. package/test/archive.test.js +80 -0
  110. package/test/bench-markdown-scroll.mjs +190 -0
  111. package/test/clip-host.test.js +113 -0
  112. package/test/code-preview.test.js +35 -0
  113. package/test/compare-ui-paths.test.js +24 -0
  114. package/test/compare.test.js +54 -0
  115. package/test/delete-trash.test.js +67 -0
  116. package/test/dirsize.test.js +138 -0
  117. package/test/file-ops.test.js +241 -0
  118. package/test/find-contents.test.js +161 -0
  119. package/test/find-input.test.js +66 -0
  120. package/test/fixtures/pele-parity.json +1006 -0
  121. package/test/highlight-launcher.test.js +35 -0
  122. package/test/html-preview.test.js +22 -0
  123. package/test/input.test.js +15 -0
  124. package/test/keymap.test.js +26 -0
  125. package/test/language-icons.test.js +36 -0
  126. package/test/launcher.test.js +51 -0
  127. package/test/manual-code-scroll.mjs +28 -0
  128. package/test/manual-column-scroll.mjs +24 -0
  129. package/test/manual-compare-ui.mjs +20 -0
  130. package/test/manual-find-status.mjs +37 -0
  131. package/test/manual-html-security.mjs +23 -0
  132. package/test/manual-html-ui.mjs +30 -0
  133. package/test/manual-syntax-preview.mjs +26 -0
  134. package/test/manual-trash-ui.mjs +86 -0
  135. package/test/markdown-diagrams.test.js +93 -0
  136. package/test/markdown-html.test.js +105 -0
  137. package/test/mathml.test.js +92 -0
  138. package/test/menu-transfer.test.js +57 -0
  139. package/test/office-preview.test.js +154 -0
  140. package/test/pele-parity.test.js +28 -0
  141. package/test/places.test.js +134 -0
  142. package/test/preview-keys.test.js +60 -0
  143. package/test/preview-language.test.js +22 -0
  144. package/test/status-filename.test.js +20 -0
  145. package/test/transfer-replace-merge.test.js +187 -0
  146. package/test/trash-address.test.js +23 -0
  147. package/test/trash-view.test.js +37 -0
  148. package/test/trashbrowse.test.js +135 -0
  149. package/ui/AboutFacts.qml +78 -0
  150. package/ui/ArchiveCards.qml +68 -0
  151. package/ui/ArchiveCompressDialog.qml +378 -0
  152. package/ui/ArchivePasswordDialog.qml +257 -0
  153. package/ui/Backend.qml +771 -0
  154. package/ui/BulkOldNames.qml +47 -0
  155. package/ui/BulkRenameDialog.qml +380 -0
  156. package/ui/CardFrame.qml +67 -0
  157. package/ui/CardScroll.qml +79 -0
  158. package/ui/CheckBox.qml +73 -0
  159. package/ui/ChromeBar.qml +360 -0
  160. package/ui/ChromeButton.qml +131 -0
  161. package/ui/ChromeModes.qml +43 -0
  162. package/ui/CodeLines.qml +256 -0
  163. package/ui/CodeRefusal.qml +52 -0
  164. package/ui/CollideConfirm.qml +147 -0
  165. package/ui/CollideHost.qml +167 -0
  166. package/ui/ColumnPane.qml +523 -0
  167. package/ui/ColumnRow.qml +380 -0
  168. package/ui/ColumnsArea.qml +594 -0
  169. package/ui/ColumnsLanding.qml +147 -0
  170. package/ui/CompareView.qml +321 -0
  171. package/ui/ContextMenu.qml +743 -0
  172. package/ui/ConvertDialog.qml +456 -0
  173. package/ui/CountedWorker.qml +7 -0
  174. package/ui/Crumb.qml +64 -0
  175. package/ui/CrumbDrop.qml +25 -0
  176. package/ui/CsvTable.qml +394 -0
  177. package/ui/CursorMark.qml +41 -0
  178. package/ui/DefaultClaim.qml +130 -0
  179. package/ui/DeviceLock.qml +47 -0
  180. package/ui/DeviceMounts.qml +540 -0
  181. package/ui/DialogButton.qml +150 -0
  182. package/ui/DialogField.qml +147 -0
  183. package/ui/DialogHeader.qml +31 -0
  184. package/ui/DialogScrim.qml +20 -0
  185. package/ui/DialogTitle.qml +37 -0
  186. package/ui/Divider.qml +9 -0
  187. package/ui/DropInto.qml +126 -0
  188. package/ui/DropTarget.qml +35 -0
  189. package/ui/DropboxMark.qml +60 -0
  190. package/ui/EmptyState.qml +150 -0
  191. package/ui/EscapeHint.qml +11 -0
  192. package/ui/FactsTable.qml +54 -0
  193. package/ui/FastScrollHandler.qml +476 -0
  194. package/ui/FatNamesConfirm.qml +157 -0
  195. package/ui/Favourites.qml +143 -0
  196. package/ui/FigureService.qml +120 -0
  197. package/ui/FileDrag.qml +267 -0
  198. package/ui/FilterStrip.qml +133 -0
  199. package/ui/FitMetrics.qml +22 -0
  200. package/ui/FlashDriveRows.qml +137 -0
  201. package/ui/FlashHost.qml +72 -0
  202. package/ui/FlashImageRow.qml +104 -0
  203. package/ui/FlashRunner.qml +119 -0
  204. package/ui/FlashStepRow.qml +172 -0
  205. package/ui/FlashStrip.qml +38 -0
  206. package/ui/FlashView.qml +397 -0
  207. package/ui/FleaMark.qml +88 -0
  208. package/ui/FlyoutFade.qml +23 -0
  209. package/ui/FontPane.qml +170 -0
  210. package/ui/Glass.qml +114 -0
  211. package/ui/GlassTagger.qml +59 -0
  212. package/ui/Glyph.qml +64 -0
  213. package/ui/GlyphSolid.qml +22 -0
  214. package/ui/GridArea.qml +664 -0
  215. package/ui/GridTile.qml +325 -0
  216. package/ui/GvfsBridge.qml +164 -0
  217. package/ui/Header.qml +433 -0
  218. package/ui/HoverTimers.qml +39 -0
  219. package/ui/HtmlClip.qml +30 -0
  220. package/ui/HtmlPane.qml +63 -0
  221. package/ui/HyprlandMark.qml +45 -0
  222. package/ui/Ipc.qml +1005 -0
  223. package/ui/IpcControls.qml +7 -0
  224. package/ui/IpcMenuState.qml +48 -0
  225. package/ui/IpcPreviewState.qml +185 -0
  226. package/ui/JumpPath.qml +51 -0
  227. package/ui/KeymapSheet.qml +561 -0
  228. package/ui/KeymapSheetResult.qml +89 -0
  229. package/ui/KindStack.qml +40 -0
  230. package/ui/LangMark.qml +51 -0
  231. package/ui/LangMarkLoader.qml +26 -0
  232. package/ui/List.qml +480 -0
  233. package/ui/LoadingState.qml +54 -0
  234. package/ui/LocalSend.qml +56 -0
  235. package/ui/LocalSendMark.qml +73 -0
  236. package/ui/MarkdownBlockView.qml +311 -0
  237. package/ui/MarkdownBlocks.qml +35 -0
  238. package/ui/MarkdownFigure.qml +254 -0
  239. package/ui/MarkdownImages.qml +72 -0
  240. package/ui/MarkdownList.qml +81 -0
  241. package/ui/MarkdownMathsText.qml +97 -0
  242. package/ui/MarkdownPane.qml +83 -0
  243. package/ui/MarkdownPictures.qml +41 -0
  244. package/ui/MarkdownQuote.qml +52 -0
  245. package/ui/MarkdownRemote.qml +82 -0
  246. package/ui/MarkdownSource.qml +155 -0
  247. package/ui/MarkdownTable.qml +222 -0
  248. package/ui/MarkdownTableScroll.qml +64 -0
  249. package/ui/MarkdownTableWheel.qml +126 -0
  250. package/ui/MarkdownText.qml +87 -0
  251. package/ui/MarkdownWorker.js +132 -0
  252. package/ui/MatchText.qml +99 -0
  253. package/ui/MediaSession.qml +38 -0
  254. package/ui/MediaSound.qml +26 -0
  255. package/ui/MediaStrip.qml +191 -0
  256. package/ui/MediaTrack.qml +36 -0
  257. package/ui/MediaVolume.qml +54 -0
  258. package/ui/MenuActionDialog.qml +396 -0
  259. package/ui/MenuEdgeFade.qml +24 -0
  260. package/ui/MenuHost.qml +227 -0
  261. package/ui/MenuRow.qml +351 -0
  262. package/ui/MountListing.qml +100 -0
  263. package/ui/NetworkDialog.qml +435 -0
  264. package/ui/NetworkForm.qml +426 -0
  265. package/ui/NetworkMounts.qml +913 -0
  266. package/ui/NetworkPlaces.qml +127 -0
  267. package/ui/OpenWithDialog.qml +613 -0
  268. package/ui/Opener.qml +115 -0
  269. package/ui/Pane.qml +1296 -0
  270. package/ui/PaneMenuActions.qml +527 -0
  271. package/ui/PanePath.qml +90 -0
  272. package/ui/PaneRail.qml +239 -0
  273. package/ui/PaneStates.qml +88 -0
  274. package/ui/PaneSwap.qml +339 -0
  275. package/ui/PaneWire.qml +605 -0
  276. package/ui/ParkedWait.qml +9 -0
  277. package/ui/PathEntry.qml +186 -0
  278. package/ui/PathJump.qml +400 -0
  279. package/ui/PdfViewer.qml +297 -0
  280. package/ui/PermissionsDialog.qml +658 -0
  281. package/ui/PermissionsHeadings.qml +31 -0
  282. package/ui/PhoneMounts.qml +67 -0
  283. package/ui/PickerAnswers.qml +75 -0
  284. package/ui/PickerBox.qml +26 -0
  285. package/ui/PickerChrome.qml +295 -0
  286. package/ui/PickerFilter.qml +97 -0
  287. package/ui/PickerGrid.qml +308 -0
  288. package/ui/PickerHeader.qml +28 -0
  289. package/ui/PickerLifecycle.qml +27 -0
  290. package/ui/PickerList.qml +218 -0
  291. package/ui/PickerListing.qml +149 -0
  292. package/ui/PickerPathBar.qml +127 -0
  293. package/ui/PickerPlaces.qml +189 -0
  294. package/ui/PickerRecent.qml +92 -0
  295. package/ui/PickerSave.qml +101 -0
  296. package/ui/PickerSelection.qml +155 -0
  297. package/ui/PickerWindow.qml +846 -0
  298. package/ui/PowerSectorsReader.qml +10 -0
  299. package/ui/PrebuildDriver.qml +146 -0
  300. package/ui/Preview.qml +1004 -0
  301. package/ui/PreviewAnimation.qml +24 -0
  302. package/ui/PreviewArchive.qml +66 -0
  303. package/ui/PreviewColumn.qml +551 -0
  304. package/ui/PreviewEdge.qml +63 -0
  305. package/ui/PreviewFacts.qml +30 -0
  306. package/ui/PreviewFactsLine.qml +34 -0
  307. package/ui/PreviewHtml.qml +233 -0
  308. package/ui/PreviewImage.qml +253 -0
  309. package/ui/PreviewImagePan.qml +13 -0
  310. package/ui/PreviewImageSharp.qml +32 -0
  311. package/ui/PreviewLines.qml +106 -0
  312. package/ui/PreviewMarkdown.qml +706 -0
  313. package/ui/PreviewMedia.qml +250 -0
  314. package/ui/PreviewPdf.qml +482 -0
  315. package/ui/PreviewScroll.qml +201 -0
  316. package/ui/PreviewSwap.qml +335 -0
  317. package/ui/PreviewText.qml +149 -0
  318. package/ui/ProtocolChip.qml +18 -0
  319. package/ui/QuickLookArchive.qml +38 -0
  320. package/ui/QuickLookBar.qml +103 -0
  321. package/ui/QuickLookPrepare.qml +355 -0
  322. package/ui/QuickLookSwap.qml +10 -0
  323. package/ui/QuickLookWarm.qml +65 -0
  324. package/ui/RailDrop.qml +155 -0
  325. package/ui/RailHeading.qml +19 -0
  326. package/ui/RailPlaces.qml +38 -0
  327. package/ui/RailTrashRow.qml +67 -0
  328. package/ui/RenameField.qml +353 -0
  329. package/ui/RendererRetry.qml +11 -0
  330. package/ui/ResizeHandle.qml +52 -0
  331. package/ui/Reveal.qml +54 -0
  332. package/ui/Row.qml +476 -0
  333. package/ui/RowDrag.qml +213 -0
  334. package/ui/RowDropTarget.qml +27 -0
  335. package/ui/RunCard.qml +286 -0
  336. package/ui/Scripts.qml +65 -0
  337. package/ui/SearchStrip.qml +232 -0
  338. package/ui/SelectionBand.qml +140 -0
  339. package/ui/SelectionPreview.qml +370 -0
  340. package/ui/SettingsFavourite.qml +122 -0
  341. package/ui/SettingsGroup.qml +96 -0
  342. package/ui/SettingsHint.qml +12 -0
  343. package/ui/SettingsMetrics.qml +29 -0
  344. package/ui/SettingsPane.qml +178 -0
  345. package/ui/SettingsPanel.qml +547 -0
  346. package/ui/SettingsRail.qml +128 -0
  347. package/ui/SettingsRow.qml +409 -0
  348. package/ui/SettingsRuler.qml +85 -0
  349. package/ui/SettingsSegment.qml +85 -0
  350. package/ui/ShareBrowser.qml +113 -0
  351. package/ui/ShareLink.qml +67 -0
  352. package/ui/ShelfPins.qml +101 -0
  353. package/ui/Sidebar.qml +732 -0
  354. package/ui/SidebarRow.qml +358 -0
  355. package/ui/Spinner.qml +60 -0
  356. package/ui/StableModel.qml +89 -0
  357. package/ui/StateMessage.qml +84 -0
  358. package/ui/StatusBar.qml +512 -0
  359. package/ui/StatusFileName.qml +27 -0
  360. package/ui/TabBar.qml +673 -0
  361. package/ui/TabDragGeometry.qml +60 -0
  362. package/ui/TagMenuMark.qml +52 -0
  363. package/ui/TagRailSection.qml +53 -0
  364. package/ui/TagSlot.qml +40 -0
  365. package/ui/TagState.qml +150 -0
  366. package/ui/TagSwatch.qml +28 -0
  367. package/ui/Taildrop.qml +86 -0
  368. package/ui/TailscaleMark.qml +50 -0
  369. package/ui/Theme.qml +460 -0
  370. package/ui/ThemeColors.qml +43 -0
  371. package/ui/TransferCard.qml +239 -0
  372. package/ui/TrashConfirm.qml +129 -0
  373. package/ui/TrashDropHandler.qml +11 -0
  374. package/ui/TrashHost.qml +202 -0
  375. package/ui/TrashMonitor.qml +30 -0
  376. package/ui/TrashToolbar.qml +19 -0
  377. package/ui/TrashView.qml +492 -0
  378. package/ui/UnlockDialog.qml +450 -0
  379. package/ui/UnlockHost.qml +206 -0
  380. package/ui/UpdateCheck.qml +97 -0
  381. package/ui/UserDirs.qml +25 -0
  382. package/ui/ViewState.qml +565 -0
  383. package/ui/ViewportScrollBar.qml +195 -0
  384. package/ui/ViewportScrollBars.qml +25 -0
  385. package/ui/WebPreview.qml +33 -0
  386. package/ui/WebPreviewView.qml +26 -0
  387. package/ui/WindowBody.qml +827 -0
  388. package/ui/WindowDrag.qml +35 -0
  389. package/ui/boot/Commons/AudioNodes.qml +50 -0
  390. package/ui/boot/Commons/AudioNodesModel.js +96 -0
  391. package/ui/boot/Commons/Border.qml +243 -0
  392. package/ui/boot/Commons/BorderGeometry.js +373 -0
  393. package/ui/boot/Commons/Color.qml +258 -0
  394. package/ui/boot/Commons/I18n.qml +118 -0
  395. package/ui/boot/Commons/InputDevices.qml +112 -0
  396. package/ui/boot/Commons/InputMethodState.qml +38 -0
  397. package/ui/boot/Commons/IpcRegistry.qml +77 -0
  398. package/ui/boot/Commons/Numbers.js +19 -0
  399. package/ui/boot/Commons/Plural.js +91 -0
  400. package/ui/boot/Commons/ShellIpc.qml +12 -0
  401. package/ui/boot/Commons/Style.qml +561 -0
  402. package/ui/boot/Commons/Translations.qml +100 -0
  403. package/ui/boot/Commons/UntypedInput.qml +152 -0
  404. package/ui/boot/Commons/Util.qml +188 -0
  405. package/ui/boot/Commons/qmldir +13 -0
  406. package/ui/boot/LICENSE-omarchy +20 -0
  407. package/ui/boot/Ui/BackgroundMedia.qml +49 -0
  408. package/ui/boot/Ui/BarIconButton.qml +106 -0
  409. package/ui/boot/Ui/BarIconContent.qml +38 -0
  410. package/ui/boot/Ui/BarIndicator.qml +53 -0
  411. package/ui/boot/Ui/BarWidget.qml +50 -0
  412. package/ui/boot/Ui/BorderOverlay.qml +54 -0
  413. package/ui/boot/Ui/BorderSurface.qml +40 -0
  414. package/ui/boot/Ui/Button.qml +224 -0
  415. package/ui/boot/Ui/ButtonGroup.qml +131 -0
  416. package/ui/boot/Ui/ConfirmDialog.qml +134 -0
  417. package/ui/boot/Ui/CursorSurface.qml +42 -0
  418. package/ui/boot/Ui/Dropdown.qml +245 -0
  419. package/ui/boot/Ui/FilterInput.qml +26 -0
  420. package/ui/boot/Ui/IconContentBounds.qml +141 -0
  421. package/ui/boot/Ui/KeyboardPanel.qml +453 -0
  422. package/ui/boot/Ui/MultiSelect.qml +624 -0
  423. package/ui/boot/Ui/NumberField.qml +86 -0
  424. package/ui/boot/Ui/OpticalGlyph.qml +68 -0
  425. package/ui/boot/Ui/OverlayWindow.qml +102 -0
  426. package/ui/boot/Ui/Panel.qml +60 -0
  427. package/ui/boot/Ui/PanelActionButton.qml +105 -0
  428. package/ui/boot/Ui/PanelController.qml +16 -0
  429. package/ui/boot/Ui/PanelHero.qml +112 -0
  430. package/ui/boot/Ui/PanelKeyCatcher.qml +100 -0
  431. package/ui/boot/Ui/PanelSectionHeader.qml +31 -0
  432. package/ui/boot/Ui/PanelSeparator.qml +19 -0
  433. package/ui/boot/Ui/PanelSlider.qml +156 -0
  434. package/ui/boot/Ui/PanelToolTip.qml +50 -0
  435. package/ui/boot/Ui/PluginBarApi.qml +89 -0
  436. package/ui/boot/Ui/PointerMoveGate.qml +54 -0
  437. package/ui/boot/Ui/PopupCard.qml +181 -0
  438. package/ui/boot/Ui/ScreenMoveRemap.qml +43 -0
  439. package/ui/boot/Ui/SearchableDropdown.qml +354 -0
  440. package/ui/boot/Ui/SizedImage.qml +125 -0
  441. package/ui/boot/Ui/SpeedTestOverlay.qml +412 -0
  442. package/ui/boot/Ui/TextField.qml +58 -0
  443. package/ui/boot/Ui/Toggle.qml +118 -0
  444. package/ui/boot/Ui/ToggleSwitch.qml +115 -0
  445. package/ui/boot/Ui/WidgetButton.qml +172 -0
  446. package/ui/boot/Ui/qmldir +41 -0
  447. package/ui/boot/fleatab.qml +50 -0
  448. package/ui/boot/picker.qml +39 -0
  449. package/ui/boot/shell.qml +122 -0
  450. package/ui/boot/tabtearoff.qml +51 -0
  451. package/ui/js/Anchor.js +379 -0
  452. package/ui/js/AnchorHold.js +162 -0
  453. package/ui/js/Archive.js +157 -0
  454. package/ui/js/ArchiveCard.js +44 -0
  455. package/ui/js/ArchiveOps.js +44 -0
  456. package/ui/js/BackReturn.js +118 -0
  457. package/ui/js/BulkRename.js +100 -0
  458. package/ui/js/Buttons.js +54 -0
  459. package/ui/js/ChromeKeys.js +116 -0
  460. package/ui/js/ClipMarks.js +73 -0
  461. package/ui/js/ClipSelection.js +67 -0
  462. package/ui/js/Clipboard.js +179 -0
  463. package/ui/js/Cloud.js +97 -0
  464. package/ui/js/CodeRuns.js +72 -0
  465. package/ui/js/Collide.js +212 -0
  466. package/ui/js/ColumnFit.js +28 -0
  467. package/ui/js/ColumnMenu.js +111 -0
  468. package/ui/js/Columns.js +298 -0
  469. package/ui/js/ColumnsCarry.js +103 -0
  470. package/ui/js/Compare.js +74 -0
  471. package/ui/js/Contrast.js +157 -0
  472. package/ui/js/ControlReader.js +42 -0
  473. package/ui/js/Convert.js +52 -0
  474. package/ui/js/CopyAs.js +52 -0
  475. package/ui/js/Crumbs.js +178 -0
  476. package/ui/js/Csv.js +231 -0
  477. package/ui/js/Density.js +44 -0
  478. package/ui/js/Devices.js +440 -0
  479. package/ui/js/DirSizes.js +180 -0
  480. package/ui/js/Drag.js +364 -0
  481. package/ui/js/DragOut.js +166 -0
  482. package/ui/js/DropDev.js +52 -0
  483. package/ui/js/Dropbox.js +41 -0
  484. package/ui/js/Eject.js +217 -0
  485. package/ui/js/Errors.js +188 -0
  486. package/ui/js/ExtThumbs.js +115 -0
  487. package/ui/js/Facts.js +402 -0
  488. package/ui/js/FigureDate.js +14 -0
  489. package/ui/js/Filter.js +285 -0
  490. package/ui/js/Find.js +46 -0
  491. package/ui/js/Flash.js +184 -0
  492. package/ui/js/FlashCard.js +79 -0
  493. package/ui/js/FlashEndings.js +125 -0
  494. package/ui/js/FlashPane.js +116 -0
  495. package/ui/js/FleaBin.js +12 -0
  496. package/ui/js/Focus.js +502 -0
  497. package/ui/js/FolderSorts.js +108 -0
  498. package/ui/js/FolderViews.js +44 -0
  499. package/ui/js/Format.js +305 -0
  500. package/ui/js/Fuzzy.js +141 -0
  501. package/ui/js/GlassTag.js +72 -0
  502. package/ui/js/Grid.js +33 -0
  503. package/ui/js/GridGeometry.js +90 -0
  504. package/ui/js/GridNames.js +153 -0
  505. package/ui/js/GvfsBridge.js +200 -0
  506. package/ui/js/HostClipboard.js +29 -0
  507. package/ui/js/Icons.js +218 -0
  508. package/ui/js/Input.js +50 -0
  509. package/ui/js/Jump.js +200 -0
  510. package/ui/js/KeyName.js +64 -0
  511. package/ui/js/Keymap.js +401 -0
  512. package/ui/js/Kinds.js +164 -0
  513. package/ui/js/LICENSES/ISC-lucide.txt +43 -0
  514. package/ui/js/LangMarkPaths.js +47 -0
  515. package/ui/js/LangMarks.js +120 -0
  516. package/ui/js/Lists.js +11 -0
  517. package/ui/js/LocalSend.js +39 -0
  518. package/ui/js/LockedMenu.js +63 -0
  519. package/ui/js/MakeDefault.js +186 -0
  520. package/ui/js/MarkBox.js +7 -0
  521. package/ui/js/Markdown.js +119 -0
  522. package/ui/js/MarkdownLists.js +86 -0
  523. package/ui/js/MarkdownMaths.js +58 -0
  524. package/ui/js/MarkdownPictures.js +32 -0
  525. package/ui/js/MarkdownPrepared.js +139 -0
  526. package/ui/js/MarkdownTableFit.js +34 -0
  527. package/ui/js/Marks.js +148 -0
  528. package/ui/js/Marquee.js +96 -0
  529. package/ui/js/Match.js +93 -0
  530. package/ui/js/Material.js +109 -0
  531. package/ui/js/MdBlocks.js +437 -0
  532. package/ui/js/MdBreak.js +71 -0
  533. package/ui/js/MdChunks.js +70 -0
  534. package/ui/js/MdContainer.js +137 -0
  535. package/ui/js/MdDocument.js +337 -0
  536. package/ui/js/MdEmph.js +97 -0
  537. package/ui/js/MdEntity.js +108 -0
  538. package/ui/js/MdEntityTable.js +223 -0
  539. package/ui/js/MdEscape.js +59 -0
  540. package/ui/js/MdFront.js +37 -0
  541. package/ui/js/MdHold.js +32 -0
  542. package/ui/js/MdHtml.js +307 -0
  543. package/ui/js/MdHtmlBlock.js +290 -0
  544. package/ui/js/MdHtmlHeading.js +53 -0
  545. package/ui/js/MdHtmlImage.js +179 -0
  546. package/ui/js/MdInline.js +154 -0
  547. package/ui/js/MdItems.js +212 -0
  548. package/ui/js/MdLeaf.js +309 -0
  549. package/ui/js/MdLink.js +196 -0
  550. package/ui/js/MdMath.js +67 -0
  551. package/ui/js/MdParagraphs.js +138 -0
  552. package/ui/js/MdRefs.js +232 -0
  553. package/ui/js/MdResolve.js +226 -0
  554. package/ui/js/MdRun.js +300 -0
  555. package/ui/js/MdUrl.js +226 -0
  556. package/ui/js/Menu.js +514 -0
  557. package/ui/js/MenuContext.js +85 -0
  558. package/ui/js/MenuFit.js +13 -0
  559. package/ui/js/MenuRefresh.js +74 -0
  560. package/ui/js/MenuWheel.js +35 -0
  561. package/ui/js/Messages.js +186 -0
  562. package/ui/js/Motion.js +16 -0
  563. package/ui/js/Mounts.js +320 -0
  564. package/ui/js/MouseNav.js +13 -0
  565. package/ui/js/Nav.js +329 -0
  566. package/ui/js/NetFs.js +26 -0
  567. package/ui/js/OpenWith.js +83 -0
  568. package/ui/js/Ops.js +585 -0
  569. package/ui/js/Palette.js +33 -0
  570. package/ui/js/PathBar.js +260 -0
  571. package/ui/js/Permissions.js +179 -0
  572. package/ui/js/Phones.js +112 -0
  573. package/ui/js/Picker.js +353 -0
  574. package/ui/js/PlaceMenu.js +96 -0
  575. package/ui/js/Places.js +265 -0
  576. package/ui/js/PreviewAsk.js +21 -0
  577. package/ui/js/PreviewKeys.js +155 -0
  578. package/ui/js/PreviewSettle.js +29 -0
  579. package/ui/js/PreviewSwap.js +101 -0
  580. package/ui/js/PropertyFacts.js +144 -0
  581. package/ui/js/Protocols.js +296 -0
  582. package/ui/js/QuickLookKinds.js +27 -0
  583. package/ui/js/QuickLookRect.js +24 -0
  584. package/ui/js/RailDrop.js +51 -0
  585. package/ui/js/RailKeys.js +87 -0
  586. package/ui/js/RailMenu.js +133 -0
  587. package/ui/js/Recent.js +135 -0
  588. package/ui/js/RecentMode.js +156 -0
  589. package/ui/js/Reload.js +60 -0
  590. package/ui/js/Renderer.js +9 -0
  591. package/ui/js/RunFile.js +143 -0
  592. package/ui/js/Scripts.js +28 -0
  593. package/ui/js/Scroll.js +304 -0
  594. package/ui/js/ScrollOff.js +69 -0
  595. package/ui/js/Search.js +478 -0
  596. package/ui/js/Selection.js +97 -0
  597. package/ui/js/Settings.js +476 -0
  598. package/ui/js/SettingsAbout.js +97 -0
  599. package/ui/js/SettingsShelf.js +52 -0
  600. package/ui/js/SheetKeys.js +42 -0
  601. package/ui/js/SheetQuery.js +281 -0
  602. package/ui/js/ShelfPile.js +27 -0
  603. package/ui/js/SlowClick.js +53 -0
  604. package/ui/js/SlowOp.js +6 -0
  605. package/ui/js/Sort.js +134 -0
  606. package/ui/js/Startup.js +63 -0
  607. package/ui/js/StateWords.js +17 -0
  608. package/ui/js/Status.js +238 -0
  609. package/ui/js/Swap.js +166 -0
  610. package/ui/js/TabMove.js +39 -0
  611. package/ui/js/Tabs.js +736 -0
  612. package/ui/js/Tags.js +134 -0
  613. package/ui/js/Taildrop.js +128 -0
  614. package/ui/js/Tap.js +174 -0
  615. package/ui/js/TextSize.js +107 -0
  616. package/ui/js/Thumbs.js +158 -0
  617. package/ui/js/Transfer.js +172 -0
  618. package/ui/js/Trash.js +81 -0
  619. package/ui/js/TrashDates.js +47 -0
  620. package/ui/js/TrashDrop.js +64 -0
  621. package/ui/js/UiState.js +438 -0
  622. package/ui/js/Unlock.js +152 -0
  623. package/ui/js/Update.js +130 -0
  624. package/ui/js/UserDirs.js +56 -0
  625. package/ui/qmldir +228 -0
package/ui/Preview.qml ADDED
@@ -0,0 +1,1004 @@
1
+ import QtQuick
2
+ import qs.Commons
3
+ import "." as Flea
4
+ import "js/Icons.js" as Icons
5
+ import "js/Kinds.js" as Kinds
6
+ import "js/Search.js" as Search
7
+ import "js/Thumbs.js" as Thumbs
8
+ import "js/ExtThumbs.js" as ExtThumbs
9
+ import "js/MarkdownPrepared.js" as Prepared
10
+ import "js/Motion.js" as Motion
11
+ import "js/PreviewSettle.js" as PreviewSettle
12
+ import "js/PreviewKeys.js" as PreviewKeys
13
+ import "js/PreviewSwap.js" as PreviewSwap
14
+ import "js/Swap.js" as Swap
15
+ import "js/QuickLookRect.js" as QuickLookRect
16
+
17
+ // The overlay lives inside the Flea window, Finder's Quick Look shape: a second window breaks omarchy-drive focus flea and every test that narrows on it.
18
+ Item {
19
+ id: root
20
+ anchors.fill: parent
21
+ // active flips instantly, so the IPC read never races the close fade; visible outlives it until surface's own opacity finishes.
22
+ visible: root.active || surface.opacity > 0
23
+ z: 1
24
+
25
+ property bool active: false
26
+ // shell.qml wires the pane in: the archive pane asks the backend about the cursor row and nothing else here reads it.
27
+ property var pane: null
28
+ property string path: ""
29
+ property string iconName: ""
30
+ property string kindName: ""
31
+ property real size: 0
32
+ property string kind: ""
33
+ readonly property bool isMedia: root.kind === "audio" || root.kind === "video"
34
+ readonly property bool isPdf: root.kind === "pdf"
35
+ readonly property bool isImage: root.kind === "image"
36
+ readonly property bool isArchive: root.kind === "archive"
37
+ readonly property bool isFont: root.kind === "font"
38
+ readonly property bool fontFailed: root.isFont && fontLoader.item !== null && fontLoader.item.failed
39
+ // RenderedPreviews: a Markdown file keeps the text kind and draws its own bar and pane.
40
+ readonly property bool isMarkdown: root.kind === "text" && root.size <= ExtThumbs.textLimit(root.pane ? root.pane.storageClass : "") && Kinds.isMarkdown(root.path)
41
+ // An office document is converted by the backend, so it needs local storage and has its own size gate.
42
+ readonly property bool isOffice: root.kind === "text" && root.size <= Kinds.OFFICE_LIMIT && Kinds.isOfficeDocument(root.path)
43
+ && !(root.pane && (root.pane.storageClass === "network" || root.pane.storageClass === "phone"))
44
+ readonly property bool isHtml: root.isOffice || (root.kind === "text" && root.size <= ExtThumbs.textLimit(root.pane ? root.pane.storageClass : "") && Kinds.isHtml(root.path))
45
+ readonly property bool isDocument: root.isMarkdown || root.isHtml
46
+ // Code and plain text share ui/PreviewText.qml; every text kind draws the Quick Look bar.
47
+ readonly property bool isCodeText: root.kind === "text" && !root.isDocument
48
+ readonly property bool hasBar: root.kind === "text"
49
+ // Compare shows two files in this overlay: path is the first and comparePath the second, and one diff is asked per open.
50
+ readonly property bool isCompare: root.kind === "compare"
51
+ property string comparePath: ""
52
+ // r flips the open document Quick Look to Source; close() forgets it, and a move to another file keeps it.
53
+ property bool markdownSource: false
54
+ // The file whose first Markdown read may block the key, decided from its listing row before the path moves; "" when none may.
55
+ property string inlineMarkdownPath: ""
56
+ // Tab put the keyboard on the bar's close mark, Markdown or code; a move off the text kinds or a close lets it go.
57
+ property bool markdownCloseFocus: false
58
+ readonly property bool markdownCloseFocused: root.active && root.hasBar && root.markdownCloseFocus
59
+ onHasBarChanged: if (!root.hasBar) root.markdownCloseFocus = false
60
+ // The backend's meta answer for the open archive, null until it lands; archiveRow is the row it was asked for.
61
+ property var archiveMeta: null
62
+ property int archiveRow: -1
63
+ // MediaPdf rule 6's fourth fact: Qt carries no sample-rate key at all (QMediaMetaData::Key, Qt 6.11), so the number is the backend probe's, asked the way an archive's is.
64
+ property int mediaRate: 0
65
+ property int mediaRow: -1
66
+ readonly property bool archiveFailed: root.isArchive && root.archiveMeta !== null && root.archiveMeta.archiveFailed === true
67
+ // For ui/Ipc.qml: the item drawing this kind's content, whether a player exists, and what the text and archive panes hold.
68
+ function surfaceItem() {
69
+ if (root.isImage) return imageLoader.item
70
+ if (root.isMedia) return mediaLoader.item
71
+ if (root.isPdf) return pdfLoader.item
72
+ if (root.isArchive) return archivePane
73
+ if (root.isCompare) return compareLoader.item
74
+ if (root.kind === "text") return root.isDocument
75
+ ? (markdownLoader.item ? markdownLoader.item.bodyItem : null) : textPane.bodyItem
76
+ return null
77
+ }
78
+ function pageBy(direction) {
79
+ if (root.isPdf) { root.turnPage(direction); return }
80
+ var view = null
81
+ if (root.isDocument) view = root.markdownShown ? root.markdownShown.viewport : null
82
+ else if (root.kind === "text") view = textPane.list
83
+ else if (root.isCompare && compareLoader.item) view = compareLoader.item.viewport
84
+ PreviewKeys.pageViewport(view, direction)
85
+ }
86
+ function mediaLoaded() { return mediaLoader.item !== null }
87
+ function textShown() { return root.isDocument
88
+ ? (markdownLoader.item ? markdownLoader.item.rawText : "") : textPane.shownText() }
89
+ function markdownView() { return root.markdownShown ? root.markdownShown.shownView : "" }
90
+ function markdownCloseState() { return JSON.stringify(root.markdownShown ? root.markdownShown.closeState() : {}) }
91
+ function markdownEndGap() { return root.markdownShown ? root.markdownShown.endGap() : -1 }
92
+ function markdownScrollY() { return root.markdownShown ? Math.round(root.markdownShown.scrollY) : -1 }
93
+ function archiveNames() { return root.archiveMeta && root.archiveMeta.names ? root.archiveMeta.names.map(function (e) { return e.n }).join("|") : "" }
94
+ readonly property bool pdfExpanded: root.isPdf && pdfLoader.item !== null && pdfLoader.item.expanded
95
+ // The PDF surface, null with no document loaded: ui/Ipc.qml answers "" for that, so an unmeasured state never reads as a value.
96
+ readonly property var pdfItem: pdfLoader.item
97
+ // What the strip actually draws: shell.qml's IPC reads this rather than re-deriving the visible: expression.
98
+ readonly property alias stripVisible: mediaStrip.visible
99
+ // The strip's own mute mark and the flag it draws from, so a test reads and clicks what is there.
100
+ readonly property var muteMark: mediaStrip.muteItem
101
+ readonly property bool muted: Flea.MediaSound.muted
102
+ // fleaWindow.itemRect needs the real Item, the same seam rowCentre already reads through pane.
103
+ readonly property var seekSlider: mediaStrip.seekItem
104
+ readonly property var volumeSlider: mediaStrip.volumeItem
105
+ readonly property string status: {
106
+ if (!root.active) return ""
107
+ // A fresh media item still carries its empty path until onLoaded binds it; its
108
+ // "stopped" there is not a whole preview, so the swap never releases on it.
109
+ if (root.isMedia) return (mediaLoader.item && mediaLoader.item.path === root.path) ? mediaLoader.item.status : "loading"
110
+ if (root.isPdf) return (pdfLoader.item && pdfLoader.item.failed) ? "This file could not be read." : "pdf"
111
+ if (root.isImage) return imageLoader.item ? imageLoader.item.status : "loading"
112
+ if (root.isArchive) return root.archiveMeta === null ? "loading" : (root.archiveFailed ? "This archive could not be read." : "archive")
113
+ if (root.isCompare) return compareLoader.item ? compareLoader.item.status : "loading"
114
+ if (root.kind === "text") return root.isDocument
115
+ ? (markdownLoader.item ? markdownLoader.item.status : "loading") : textPane.status
116
+ if (root.isFont) return fontLoader.item ? fontLoader.item.status : "loading"
117
+ return "This file cannot be previewed."
118
+ }
119
+ // The swap's answer: nothing loading, and a PDF with a page on screen or refused.
120
+ // An interim cache thumbnail shown counts as whole, never as a half-built frame.
121
+ readonly property bool lookReady: !root.active || PreviewSwap.lookReady(root.status, root.isPdf,
122
+ root.pdfItem !== null && root.pdfItem.shownPage >= 0 && root.pdfItem.opened === root.path, root.pdfItem !== null && root.pdfItem.failed,
123
+ root.interimShown, (root.isDocument && markdownLoader.item !== null && markdownLoader.item.firstScreen)
124
+ || (root.isCodeText && textPane.firstScreen)) && !root.sizeWait
125
+ // The cached thumbnail held at open, drawn under the full decode at the final rect; the stamp retires a stale one.
126
+ property string interimThumb: ""
127
+ property string interimStamp: ""
128
+ // The interim's rect off the upright original, null while its pixels are unknown: no
129
+ // interim until then. A cache file is already upright, so no box swap applies here.
130
+ readonly property var interimBox: PreviewSwap.interimRect(panes.width, panes.height - (root.isRawFile ? Theme.chromeHeight : 0),
131
+ root.imageW, root.imageH, root.imageOrient)
132
+ // True once the interim is on screen at the final's rect; lookReady releases on this.
133
+ readonly property bool interimVisible: root.isImage && root.interimThumb.length > 0
134
+ && root.path === root.interimStamp && root.interimBox !== null
135
+ readonly property bool interimShown: root.interimVisible && imageLoader.item !== null
136
+ && imageLoader.item.interimReady === true
137
+ // The original's pixels, off the shown row's meta reply; 0 until it lands.
138
+ property int imageW: 0
139
+ property int imageH: 0
140
+ property int imageOrient: 1
141
+ property int imageFrames: 0
142
+ property int imageRow: -1
143
+ // One meta ask per show; onRowsChanged never re-asks behind it.
144
+ property bool imageAsked: false
145
+ // The row whose thumbnail may stand in for an image that cannot decode and had none at open, -1 when none is owed; the sentence waits while it is set.
146
+ property int thumbRow: -1
147
+ // One thumbnail ask per show, sent only once the final decode has failed.
148
+ property bool thumbAsked: false
149
+ // A listing started while the thumbnail is owed, so its map and row indices are the old listing's.
150
+ property bool thumbRelist: false
151
+ readonly property bool imageUndecodable: root.isImage && imageLoader.item !== null && imageLoader.item.pictureFailed
152
+ onImageUndecodableChanged: root.settleThumb()
153
+ // A camera RAW is drawn through the copy of its embedded JPEG the meta reply names; null until it lands or when the RAW holds none.
154
+ readonly property bool isRawFile: root.isImage && Kinds.isRaw(root.path)
155
+ property var rawCopy: null
156
+ property bool rawAnswered: false
157
+ // See ui/js/QuickLookRect.js: shapeNext follows the file shown and its size reply, shape is what the card draws, taken once no swap holds a picture.
158
+ property var shapeNext: ({ kind: "", known: false, w: 0, h: 0, orient: 1 })
159
+ property var shape: root.shapeNext
160
+ readonly property var rectTokens: ({ fraction: Theme.preview.fraction, edge: Theme.preview.edge, bar: Theme.chromeHeight })
161
+ readonly property var rect: QuickLookRect.surface(root.shape, root.width, root.height, root.pdfExpanded, root.rectTokens)
162
+ readonly property var nextRect: QuickLookRect.surface(root.shapeNext, root.width, root.height, root.pdfExpanded, root.rectTokens)
163
+ readonly property bool held: root.swap !== null && (root.swap.holding || root.swap.capturing)
164
+ // Plain, as a binding on its own releases loops: set by show(), cleared by the reply, an early reveal or the swap's release a turn later.
165
+ property bool sizeWait: false
166
+ onHeldChanged: {
167
+ root.commitShape()
168
+ if (!root.held) Qt.callLater(function () { root.sizeWait = false })
169
+ }
170
+ function commitShape() { if (!root.held) root.shape = root.shapeNext }
171
+ // A size reply counts until the card shows or the swap lets go of its picture; a later one leaves the card alone.
172
+ function sizeArrived(w, h, orient, known) {
173
+ if (!root.revealed || root.held) {
174
+ root.shapeNext = { kind: root.kind, known: known, w: w, h: h, orient: orient }
175
+ root.commitShape()
176
+ }
177
+ root.sizeWait = false
178
+ }
179
+ function swapState() {
180
+ if (root.swap) return root.swap.describe()
181
+ return { holding: false, capturing: false, fellBack: false, holds: 0, fallbacks: 0,
182
+ bursts: 0, heldFrames: 0, midFrames: 0, loadingFrames: 0, last: { ms: 0, end: "" } }
183
+ }
184
+ // The 0.3.6 swap additions, null until the first open builds them below; every reader guards it.
185
+ readonly property var swap: swapLoader.item
186
+ readonly property bool swapBuilt: swapLoader.active
187
+ // The memory suite asserts both Markdown loader items are null without a Markdown file.
188
+ readonly property var markdownItem: markdownLoader.item
189
+ // The open Markdown document, null unless Quick Look shows one; the guarded readers share this null.
190
+ readonly property var markdownShown: root.active && root.isDocument ? markdownLoader.item : null
191
+ // Exposes the eager panes for the live swap gate, so it can pin the wiring.
192
+ readonly property var panesItem: panes
193
+ // Synchronous: a local source: URL answers item on the same call that sets active.
194
+ function ensureSwap() {
195
+ if (!swapLoader.active)
196
+ swapLoader.active = true
197
+ return swapLoader.item
198
+ }
199
+
200
+ property string pendingPath: ""
201
+ property string pendingIcon: ""
202
+ property string pendingKind: ""
203
+ property int pendingSize: 0
204
+ property string pendingThumb: ""
205
+ // The row open/follow captured, carried through load/show like the thumb.
206
+ property int pendingImageRow: -1
207
+ // Last distinct follow target, so a held key cannot reload mid-burst: only idleness loads at once.
208
+ property double lastMoveAt: 0
209
+ property string lastMoveKey: ""
210
+ // The settle idiom Pane's own thumbnail request reuses: a held j/k costs zero reloads until the cursor rests.
211
+ readonly property int followSettleMs: 120
212
+ // The same dim ui/SettingsPanel.qml lays over the listing.
213
+ readonly property real groundOpacity: 0.5
214
+ // The card and dim open once the content can draw, or early over the loading state, so no frame shows an empty card.
215
+ readonly property bool revealed: root.landed || root.revealedEarly
216
+ // Set by the cap, or by a press or the wheel during the hold.
217
+ property bool revealedEarly: false
218
+ // When the early loading state first drew, 0 while none stands; content replaces it only after Swap.MIN_SHOW_MS.
219
+ property double loadingSince: 0
220
+ // True once this open's content first drew; later moves are the swap's to count.
221
+ property bool landed: false
222
+ readonly property bool revealedOpen: root.active && root.revealed
223
+ // The hold: the card and dim draw nothing, a reopen inside the close fade included, and a move takes no picture.
224
+ readonly property bool unrevealed: root.active && !root.revealed
225
+ // An early reveal shows the loading state at once, for every kind, until this open's content lands.
226
+ readonly property bool loadingEarly: root.revealedEarly && !root.landed
227
+ // Frames drawn with the card showing during the hold; tests/ui.sh lookopen and tests/quicklook-hold.sh read them.
228
+ property int lookEmptyFrames: 0
229
+ property int revealCaps: 0
230
+ // The cap's start stamp and the sequence number of show()'s return, ordered by openSeq so tests/quicklook-firstkind.qml reads arm-before-show or after with no clock.
231
+ property var revealCapStart: null
232
+ property int openSeq: 0
233
+ property int shownSeq: 0
234
+ // The kinds whose Loader item this window has built once, so a repeat of one is a warm open.
235
+ property var builtKinds: ({})
236
+ function openState() {
237
+ return { revealed: root.revealed, revealedEarly: root.revealedEarly, landed: root.landed, lookEmptyFrames: root.lookEmptyFrames, revealCaps: root.revealCaps, revealCapStart: root.revealCapStart, shownSeq: root.shownSeq }
238
+ }
239
+ // A move still settling lands nothing, and followSettle reruns this once it stops, so what reveals is the cursor's own file.
240
+ // Past the cap, content that is ready waits out the loading state's minimum, so it never blinks for under 100 ms.
241
+ function revealIfReady() {
242
+ if (!root.active || !root.lookReady || followSettle.running)
243
+ return
244
+ var left = root.loadingEarly ? Swap.showLeft(root.loadingSince, Date.now()) : 0
245
+ if (left > 0) {
246
+ minShow.interval = left
247
+ minShow.restart()
248
+ } else {
249
+ root.landed = true
250
+ }
251
+ }
252
+ function showEarly() {
253
+ root.loadingSince = Date.now()
254
+ root.revealedEarly = true
255
+ root.sizeWait = false
256
+ }
257
+ function revealNow() {
258
+ root.revealIfReady()
259
+ if (root.unrevealed)
260
+ root.showEarly()
261
+ }
262
+ // The ready test runs after the open's own turn, because lookReady still reads the closed overlay inside this handler.
263
+ onActiveChanged: {
264
+ root.revealedEarly = false
265
+ root.loadingSince = 0
266
+ root.landed = false
267
+ minShow.stop()
268
+ if (!root.active)
269
+ revealCap.stop()
270
+ else
271
+ Qt.callLater(root.revealIfReady)
272
+ }
273
+ onLookReadyChanged: root.revealIfReady()
274
+ // Armed by load(): before show() for a warm kind, after it for a kind's first build, beside the swap's own cap.
275
+ Timer {
276
+ id: revealCap
277
+ objectName: "revealCap"
278
+ repeat: false
279
+ onTriggered: {
280
+ if (!root.unrevealed)
281
+ return
282
+ root.revealCaps += 1
283
+ root.showEarly()
284
+ }
285
+ }
286
+ // The early loading state has stood its minimum, so ready content lands now.
287
+ Timer {
288
+ id: minShow
289
+ repeat: false
290
+ onTriggered: root.revealIfReady()
291
+ }
292
+ // afterAnimating runs just before each frame is synchronised, so it sees a frame that draws the card or dim with neither content nor the loading state.
293
+ Connections {
294
+ target: root.active && !root.landed && !root.loadingEarly && (surface.opacity > 0 || lookDim.opacity > 0) ? root.Window.window : null
295
+ function onAfterAnimating() { root.lookEmptyFrames += 1 }
296
+ }
297
+ // One fade for the card and dim, so the hold can cut a close fade still running.
298
+ property real fade: root.revealedOpen ? 1 : 0
299
+ Behavior on fade {
300
+ enabled: !Theme.reducedMotion
301
+ NumberAnimation { duration: root.active ? Motion.durMs.open : Motion.durMs.close; easing.type: Easing.OutCubic }
302
+ }
303
+
304
+ // Read through to PreviewMedia so this file never imports QtMultimedia, and 0 before the loader has an item.
305
+ readonly property int position: (root.isMedia && mediaLoader.item) ? mediaLoader.item.position : 0
306
+ readonly property int duration: (root.isMedia && mediaLoader.item) ? mediaLoader.item.duration : 0
307
+
308
+ // Shown on open, hidden stripHideMs after the last reveal, video only: audio has nothing else to look at.
309
+ property bool stripShown: true
310
+ // StatusBar.messageMs, the OEM's own transient interval, which Sidebar's unmount arm reuses for the same reason.
311
+ property int stripHideMs: 4000
312
+
313
+ function revealStrip() {
314
+ root.stripShown = true
315
+ stripHideTimer.restart()
316
+ }
317
+
318
+ function togglePlay() { if (root.isMedia && mediaLoader.item) mediaLoader.item.togglePlay() }
319
+
320
+ // MediaMute rule 3: one session flag, so the column's strip and this one always agree.
321
+ function toggleMute() { if (root.isMedia) Flea.MediaSound.toggle() }
322
+
323
+ // Absolute seek in ms, clamped by PreviewMedia's own seekTo; the slider's onReleased calls this directly.
324
+ function seekTo(ms) { if (root.isMedia && mediaLoader.item) mediaLoader.item.seekTo(ms) }
325
+
326
+ // Relative seek in ms, Left/Right's own shape; seekTo does the clamping.
327
+ function seek(deltaMs) {
328
+ root.seekTo(root.position + deltaMs)
329
+ }
330
+
331
+ // The PDF viewer's three actions come through this file, so ui/js/Focus.js never learns a Loader item answers them.
332
+ function turnPage(delta) { if (root.isPdf && pdfLoader.item) pdfLoader.item.turn(delta) }
333
+
334
+ function zoomBy(steps) { if (root.isPdf && pdfLoader.item) pdfLoader.item.zoomBy(steps); else if (root.isImage && imageLoader.item) imageLoader.item.zoomBy(steps); else if (root.isHtml && root.markdownShown) root.markdownShown.zoomBy(steps) }
335
+ function zoomFit() { if (root.isImage && imageLoader.item) imageLoader.item.zoomFit(); else if (root.isHtml && root.markdownShown) root.markdownShown.zoomFit() }
336
+
337
+ function toggleExpand() { if (root.isPdf && pdfLoader.item) pdfLoader.item.toggleExpand() }
338
+
339
+ // Space opens on the cursor row; this is immediate, follow() below is the held-key j/k path.
340
+ function open(newPath, newIcon, newSize, newKind, newThumb) {
341
+ followSettle.stop()
342
+ root.lastMoveKey = newPath + "\n" + newIcon + "\n" + newSize + "\n" + newKind
343
+ root.lastMoveAt = Date.now()
344
+ root.load(newPath, newIcon, newSize, newKind, newThumb, root.pane ? root.pane.cursorIndex : -1)
345
+ }
346
+
347
+ // The Compare overlay for two regular files; iconName is the first file's, which picks the bar's mark.
348
+ function openCompare(pathA, pathB, newIcon) {
349
+ followSettle.stop()
350
+ root.comparePath = pathB
351
+ root.iconName = newIcon || ""
352
+ root.path = pathA
353
+ root.size = 0
354
+ root.kindName = ""
355
+ root.kind = "compare"
356
+ root.active = true
357
+ }
358
+
359
+ // y: the patch goes to the clipboard; any other kind has none.
360
+ function copyPatch() { if (root.isCompare && compareLoader.item) compareLoader.item.copyPatch() }
361
+
362
+ // Document Quick Looks open rendered; r flips only the open one.
363
+ function toggleMarkdownView() {
364
+ if (root.isDocument) root.markdownSource = !root.markdownSource
365
+ }
366
+
367
+ function toggleMarkdownClose() {
368
+ if (root.hasBar) root.markdownCloseFocus = !root.markdownCloseFocus
369
+ }
370
+
371
+ // w wraps the open code or text Quick Look, and a Markdown Source; ui.json keeps the choice for the next one.
372
+ function toggleWrap() {
373
+ if (root.isCodeText || (root.isDocument && root.markdownShown && root.markdownShown.shownView === "source")) ViewState.changeLeaf("preview", { wrap: !ViewState.previewWrap })
374
+ }
375
+
376
+ // The picture is taken now, so the settled load below changes the panes under it.
377
+ function follow(newPath, newIcon, newSize, newKind, newThumb) {
378
+ var key = newPath + "\n" + newIcon + "\n" + newSize + "\n" + newKind
379
+ // Pending covers a repeat; a settled revisit of the shown target stays put, anything else reloads.
380
+ var now = Date.now()
381
+ var decision = PreviewSettle.plan(now, root.lastMoveAt, root.followSettleMs, key, root.lastMoveKey, followSettle.running, root.unrested(key))
382
+ if (decision === "same") return
383
+ if (key === root.lastMoveKey && root.isShown(newPath, newIcon, newSize, newKind)) return
384
+ root.lastMoveKey = key
385
+ root.lastMoveAt = now
386
+ root.pendingPath = newPath
387
+ root.pendingIcon = newIcon
388
+ root.pendingSize = newSize
389
+ root.pendingKind = newKind
390
+ root.pendingThumb = newThumb || ""
391
+ root.pendingImageRow = root.pane ? root.pane.cursorIndex : -1
392
+ // During the hold a move takes no picture, and a burst still trails so it asks nothing per row.
393
+ if (root.revealedOpen) {
394
+ var held = root.ensureSwap()
395
+ if (held)
396
+ held.hold(null, newPath)
397
+ }
398
+ if (decision === "now") {
399
+ followSettle.stop()
400
+ root.load(newPath, newIcon, newSize, newKind, newThumb, root.pendingImageRow)
401
+ // Armed once the load's own work returns, work a starved loop can stretch past the settle.
402
+ rested.restart()
403
+ } else {
404
+ followSettle.restart()
405
+ }
406
+ }
407
+
408
+ // Whether the overlay already shows this target: the duplicate test the key clock cannot make.
409
+ function isShown(newPath, newIcon, newSize, newKind) {
410
+ return root.active && newPath === root.path && newIcon === root.iconName
411
+ && newSize === root.size && newKind === root.kindName
412
+ }
413
+
414
+ // Dropping the loader's source is what stops playback: media dies with the loader.
415
+ function close() {
416
+ if (root.swap) root.swap.cancel()
417
+ root.revealCapStart = null
418
+ followSettle.stop()
419
+ root.lastMoveKey = ""
420
+ root.pendingThumb = ""
421
+ root.pendingImageRow = -1
422
+ root.interimThumb = ""
423
+ root.interimStamp = ""
424
+ root.imageW = 0
425
+ root.imageH = 0
426
+ root.imageOrient = 1
427
+ root.imageFrames = 0
428
+ root.imageRow = -1
429
+ root.imageAsked = false
430
+ root.thumbRow = -1
431
+ root.thumbAsked = false
432
+ root.thumbRelist = false
433
+ root.rawCopy = null
434
+ root.rawAnswered = false
435
+ stripHideTimer.stop()
436
+ root.active = false
437
+ root.kind = ""
438
+ root.markdownSource = false
439
+ root.markdownCloseFocus = false
440
+ root.inlineMarkdownPath = ""
441
+ root.comparePath = ""
442
+ mediaLoader.source = ""
443
+ pdfLoader.source = ""
444
+ imageLoader.source = ""
445
+ root.archiveMeta = null
446
+ root.archiveRow = -1
447
+ root.mediaRate = 0
448
+ root.mediaRow = -1
449
+ if (root.pane) root.pane.listArea.forceActiveFocus()
450
+ }
451
+
452
+ // Under the held picture when a move took one; Space's own open has none and reveals once its content can draw.
453
+ // The swap starts after its mutation: show runs at once or waits for the capture, start runs with it.
454
+ function load(newPath, newIcon, newSize, newKind, newThumb, newRow) {
455
+ var isPdf = Kinds.quickLookKind(newIcon, newPath) === Kinds.PDF
456
+ var swapItem = root.ensureSwap()
457
+ var show = function () {
458
+ // A warm kind counts from the key, before show() builds the Loader items; a kind's first build (a PDF's compile above all) is outside the cap, which starts once show() returns; a move during the hold keeps the open's deadline.
459
+ var kindKey = root.kindFor(newPath, newIcon, newThumb, newRow)
460
+ var arm = (!root.active || !root.revealed) && !revealCap.running
461
+ var warm = root.builtKinds[kindKey] === true
462
+ if (arm && warm) root.armRevealCap(isPdf)
463
+ root.show(newPath, newIcon, newSize, newKind, newThumb, newRow)
464
+ if (swapItem) swapItem.start(isPdf)
465
+ root.shownSeq = ++root.openSeq
466
+ root.builtKinds[kindKey] = true
467
+ if (arm && !warm) root.armRevealCap(isPdf)
468
+ else if (!arm && !revealCap.running) root.revealCapStart = null
469
+ }
470
+ if (swapItem && (swapItem.holding || swapItem.capturing))
471
+ swapItem.hold(show, newPath, true)
472
+ else
473
+ show()
474
+ }
475
+
476
+ // The row at index while its name under the listing's base is path, else null: how this file identifies a row.
477
+ function namedRow(index, path) {
478
+ if (!root.pane || index < 0) return null
479
+ var row = root.pane.rowFor(index)
480
+ return row !== null && root.pane.join(Search.baseOf(root.pane), row.n) === path ? row : null
481
+ }
482
+
483
+ function kindFor(newPath, newIcon, newThumb, newRow) {
484
+ var row = root.namedRow(newRow, newPath)
485
+ return Kinds.quickLookKind(newIcon, newPath, (row !== null && row.t === true) || (newThumb || "").length > 0)
486
+ }
487
+
488
+ function armRevealCap(isPdf) {
489
+ revealCap.interval = PreviewSwap.capMs(isPdf)
490
+ revealCap.start()
491
+ root.revealCapStart = { seq: ++root.openSeq }
492
+ }
493
+
494
+ function show(newPath, newIcon, newSize, newKind, newThumb, newRow) {
495
+ var row = root.namedRow(newRow, newPath)
496
+ root.inlineMarkdownPath = row !== null && Prepared.readsInline(row, root.pane.storageClass, root.pane.storageKnown) ? newPath : ""
497
+ var kind = root.kindFor(newPath, newIcon, newThumb, newRow)
498
+ // Gate the live image reader before its path moves, so a follow cannot decode at the previous card size while its dimensions are owed.
499
+ root.sizeWait = kind === "image" && root.pane !== null && newRow >= 0
500
+ root.kind = kind
501
+ // A pane of another kind goes before the path moves, or it tries to open a file it cannot draw: an image
502
+ // pane handed a video logged "Unsupported image format" on every move from a picture to a clip.
503
+ if (!root.isMedia) mediaLoader.source = ""
504
+ if (!root.isPdf) pdfLoader.source = ""
505
+ if (!root.isImage) imageLoader.source = ""
506
+ // The interim holds the path it was read for; a stale one never outlives a move.
507
+ root.interimThumb = newThumb || ""
508
+ root.interimStamp = newPath
509
+ // The meta row is the one captured at open/follow, never the cursor now.
510
+ root.imageRow = root.isImage && newRow !== undefined ? newRow : -1
511
+ root.imageW = 0
512
+ root.imageH = 0
513
+ root.imageOrient = 1
514
+ root.imageFrames = 0
515
+ root.imageAsked = false
516
+ root.thumbAsked = false
517
+ root.thumbRelist = false
518
+ root.rawCopy = null
519
+ root.rawAnswered = false
520
+ root.shapeNext = { kind: root.kind, known: false, w: 0, h: 0, orient: 1 }
521
+ root.commitShape()
522
+ root.path = newPath
523
+ root.thumbRow = root.thumbOwed(newRow)
524
+ root.iconName = newIcon
525
+ root.size = newSize
526
+ // MediaPdf rule 6: the overlay is the bigger surface, so it says at least what the column says, and the kind is the caller's because the backend named it for that row.
527
+ root.kindName = newKind || ""
528
+ root.active = true
529
+ mediaLoader.source = root.isMedia ? "PreviewMedia.qml" : ""
530
+ pdfLoader.source = root.isPdf ? "PdfViewer.qml" : ""
531
+ imageLoader.source = root.isImage ? "PreviewImage.qml" : ""
532
+ root.askArchive()
533
+ root.askMedia()
534
+ root.askImage()
535
+ root.sizeWait = root.isImage && root.imageAsked && root.imageRow >= 0
536
+ root.revealStrip()
537
+ root.maybePrefetch()
538
+ }
539
+
540
+ // Bounded prefetch: the next row's cache entry only, once per rest, never a decode and never a meta.
541
+ function maybePrefetch() {
542
+ if (!root.active || !root.pane || followSettle.running) return
543
+ if (!ViewState.previewAutomatic) return
544
+ var next = root.pane.cursorIndex + 1
545
+ if (root.pane.thumbState.file[next] !== undefined) return
546
+ var row = root.pane.rowFor(next)
547
+ if (!row || row.d || row.t !== true || !root.thumbClear(row)) return
548
+ var work = { ask: [next], drop: [], cacheOnly: true }
549
+ root.pane.thumbState = Thumbs.applied(root.pane.thumbState, work)
550
+ root.pane.backend.thumb(work.ask, true)
551
+ }
552
+
553
+ // Whether a thumbnail ask may go out for this row here: the listing settled, the class known, no manual hold, and the row allowed.
554
+ function thumbClear(row) {
555
+ if (!root.pane || root.pane.listInFlight || !root.pane.storageKnown) return false
556
+ if (ExtThumbs.manualHold(root.pane.storageClass, ViewState.preview)) return false
557
+ return Thumbs.allowed(row, ViewState.thumbnailMode)
558
+ }
559
+
560
+ // One row, only while an archive is the thing open: the same no-sweep rule the column follows.
561
+ function askArchive() {
562
+ root.archiveMeta = null
563
+ root.archiveRow = root.isArchive && root.pane ? root.pane.cursorIndex : -1
564
+ if (root.archiveRow >= 0)
565
+ root.pane.backend.askMeta(root.archiveRow, false, false, true)
566
+ }
567
+
568
+ // The same one row, for the one fact the transport under the overlay cannot report.
569
+ function askMedia() {
570
+ root.mediaRate = 0
571
+ root.mediaRow = root.isMedia && root.pane ? root.pane.cursorIndex : -1
572
+ if (root.mediaRow >= 0)
573
+ root.pane.backend.askMeta(root.mediaRow, false, true, false)
574
+ }
575
+
576
+ // The original's pixels, which size the interim and the card; once per show, never while a held key is still bursting.
577
+ function askImage() {
578
+ if (!root.isImage || !root.pane) return
579
+ if (root.imageRow < 0 || root.imageAsked || followSettle.running) return
580
+ // An insert or a re-sort between capture and ask moves another file to the index,
581
+ // so the row must still name the shown file before anything is asked for it.
582
+ root.resolveImageRow()
583
+ root.imageAsked = true
584
+ if (root.imageRow < 0) return
585
+ root.pane.backend.askMeta(root.imageRow, false, false, false)
586
+ }
587
+
588
+ function imageRowShown() {
589
+ return root.namedRow(root.imageRow, root.path) !== null
590
+ }
591
+
592
+ // The captured index drifted onto another file: take the cursor when it names the
593
+ // shown file instead, else drop the interim for this show with no ask.
594
+ function resolveImageRow() {
595
+ if (root.imageRowShown()) return
596
+ var at = root.pane ? root.pane.cursorIndex : -1
597
+ root.imageRow = root.namedRow(at, root.path) !== null ? at : -1
598
+ }
599
+
600
+ // A re-list clears the owed row's entry: take the cursor when it names the shown file, else drop the owed thumbnail with no ask.
601
+ function resolveThumbRow() {
602
+ if (root.namedRow(root.thumbRow, root.path) !== null) return
603
+ var at = root.pane ? root.pane.cursorIndex : -1
604
+ root.thumbRow = root.namedRow(at, root.path) !== null ? at : -1
605
+ }
606
+
607
+ // The row whose thumbnail may stand in if this image cannot decode: only with none held, clear to ask, and not refused.
608
+ function thumbOwed(newRow) {
609
+ if (!root.isImage || root.interimThumb.length > 0) return -1
610
+ var at = newRow !== undefined ? newRow : -1
611
+ if (!root.thumbClear(root.namedRow(at, root.path)) || Thumbs.refused(root.pane.thumbState, at)) return -1
612
+ return at
613
+ }
614
+
615
+ // After a failed final decode: ask the one row's thumbnail, then bind its reply to the shown file; a refusal or a forgotten ask ends on the sentence.
616
+ function settleThumb() {
617
+ if (root.thumbRow < 0 || !root.imageUndecodable || root.pane.listInFlight) return
618
+ // A re-list cleared the map under the owed row: find the shown file again and ask once on the new listing, or end on the sentence when it is gone.
619
+ if (root.thumbRelist) {
620
+ root.thumbRelist = false
621
+ root.resolveThumbRow()
622
+ root.thumbAsked = false
623
+ if (root.thumbRow < 0) return
624
+ }
625
+ // An insert or a re-sort since the capture moves another file to the index, so the row must still name the shown file.
626
+ var at = root.namedRow(root.thumbRow, root.path) !== null ? root.thumbRow : -1
627
+ var state = root.pane.thumbState
628
+ var file = at >= 0 ? Thumbs.fileFor(state, at) : ""
629
+ // Nothing more is owed once a name landed, the row was refused, or our own ask was forgotten.
630
+ if (at < 0 || file.length > 0 || Thumbs.refused(state, at) || (root.thumbAsked && state.file[at] === undefined)) {
631
+ if (file.length > 0) root.interimThumb = file
632
+ root.thumbRow = -1
633
+ return
634
+ }
635
+ if (root.thumbAsked || Thumbs.pending(state.file[at])) return
636
+ root.thumbAsked = true
637
+ var work = { ask: [at], drop: [], cacheOnly: false }
638
+ root.pane.thumbState = Thumbs.applied(state, work)
639
+ root.pane.backend.thumb(work.ask, false)
640
+ }
641
+
642
+ Connections {
643
+ target: root.pane ? root.pane.backend : null
644
+ function onMeta(row, w, h, orient, durationMs, sampleRate, entries, unpacked, archiveFailed, names, lines, partial, linesFailed, target, targetDir, owner) {
645
+ if (root.isArchive && row === root.archiveRow)
646
+ root.archiveMeta = { entries: entries, unpacked: unpacked, archiveFailed: archiveFailed, names: names }
647
+ if (root.isMedia && row === root.mediaRow)
648
+ root.mediaRate = sampleRate
649
+ if (root.isImage && !root.isRawFile && row === root.imageRow && root.imageRowShown()) {
650
+ root.imageW = w
651
+ root.imageH = h
652
+ root.imageOrient = orient
653
+ root.sizeArrived(w, h, orient, true)
654
+ }
655
+ }
656
+ // The RAW's own pixels are not what is drawn, so its size and turn come from the copy and not from the file's header.
657
+ function onMetaResult(message) {
658
+ if (!root.isRawFile || message.row !== root.imageRow || !root.imageRowShown()) return
659
+ root.rawCopy = message.raw || null
660
+ root.rawAnswered = true
661
+ if (root.rawCopy === null) {
662
+ root.sizeArrived(0, 0, 1, false)
663
+ return
664
+ }
665
+ root.imageW = root.rawCopy.w
666
+ root.imageH = root.rawCopy.h
667
+ root.imageOrient = root.rawCopy.orient
668
+ root.sizeArrived(root.rawCopy.w, root.rawCopy.h, root.rawCopy.orient, true)
669
+ }
670
+ }
671
+
672
+ Connections {
673
+ target: root.pane ? root.pane.backend : null
674
+ function onMetaResult(message) {
675
+ if (root.isImage && message.row === root.imageRow && root.imageRowShown()) root.imageFrames = message.frames || 0
676
+ }
677
+ }
678
+
679
+ // A meta asked across a listing change is answered with silence, so the rows landing re-asks it, the way ui/ColumnsArea.qml does.
680
+ Connections {
681
+ target: root.pane
682
+ function onRowsChanged() {
683
+ if (root.active && root.isArchive && root.archiveMeta === null) root.askArchive()
684
+ if (root.active && root.isMedia && root.mediaRate === 0) root.askMedia()
685
+ if (root.active && root.isImage && root.imageW === 0 && !root.rawAnswered) root.askImage()
686
+ }
687
+ // The cursor and the rows are final once the listing lands, so an owed row is found again there.
688
+ function onListInFlightChanged() {
689
+ if (!root.pane) return
690
+ if (root.pane.listInFlight) root.thumbRelist = root.thumbRelist || root.thumbRow >= 0
691
+ else root.settleThumb()
692
+ }
693
+ function onThumbStateChanged() { root.settleThumb() }
694
+ }
695
+
696
+ // The cursor has not rested since its last move: an earlier row's move still trailing, or a load made at once whose rest has not run out.
697
+ function unrested(key) { return (followSettle.running && root.lastMoveKey !== key) || rested.running }
698
+ Timer {
699
+ id: followSettle
700
+ objectName: "followSettle"
701
+ interval: root.followSettleMs
702
+ repeat: false
703
+ onTriggered: root.load(root.pendingPath, root.pendingIcon, root.pendingSize, root.pendingKind, root.pendingThumb, root.pendingImageRow)
704
+ onRunningChanged: if (!followSettle.running) Qt.callLater(root.revealIfReady)
705
+ }
706
+ // A single-shot Timer runs until its posted trigger is handled, so a repeat a starved loop serves first has not rested and trails.
707
+ Timer { id: rested; interval: root.followSettleMs }
708
+
709
+ Timer {
710
+ id: stripHideTimer
711
+ interval: root.stripHideMs
712
+ repeat: false
713
+ // A held seek or volume press is contact too, so the strip waits for the release before it goes.
714
+ onTriggered: {
715
+ if (mediaStrip.scrubbing || (mediaStrip.volumeItem && mediaStrip.volumeItem.pressing)) stripHideTimer.restart()
716
+ else root.stripShown = false
717
+ }
718
+ }
719
+
720
+ MouseArea {
721
+ anchors.fill: parent
722
+ // A shield that outlives the overlay swallows the first click after it and freezes row hover for the window.
723
+ enabled: root.active
724
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
725
+ hoverEnabled: true
726
+ // A click behind the overlay would silently move the cursor or open the listing's menu on it.
727
+ // A left click outside the surface closes, a rule of 2026-09-11, and a right click is only swallowed.
728
+ onClicked: function (mouse) {
729
+ if (mouse.button === Qt.LeftButton && !surface.contains(surface.mapFromItem(root, mouse.x, mouse.y)))
730
+ root.close()
731
+ }
732
+ onPositionChanged: root.revealStrip()
733
+ // The ground takes the wheel, so a scroll over the overlay never reaches the listing behind it.
734
+ onWheel: function (wheel) { wheel.accepted = true }
735
+ }
736
+
737
+ // PdfViewer.html and MediaPlayer.html draw a pane with its own edge: on the surface colour alone the inset vanished into the listing behind it.
738
+ Rectangle {
739
+ id: lookDim
740
+ anchors.fill: parent
741
+ color: Theme.color.background
742
+ opacity: root.unrevealed ? 0 : root.fade * root.groundOpacity
743
+ }
744
+
745
+ Rectangle {
746
+ id: surface
747
+ x: Theme.cardOrigin(parent.width, width)
748
+ y: Theme.cardOrigin(parent.height, height) + surface.rise
749
+ border.width: Theme.spacing.hairline
750
+ border.color: Theme.color.mutedFrame
751
+ // Open rises into place and close only fades, faster, because the translation is enabled: root.active.
752
+ property real rise: root.revealedOpen ? 0 : Motion.translateUpPx
753
+ opacity: root.unrevealed ? 0 : root.fade
754
+ // Tall kinds take the window's height less the edge, and Expand fills the window, which is the whole of the canvas's "expand fills the window".
755
+ width: Theme.cardSpan(root.rect.w, parent.width)
756
+ height: Theme.cardSpan(root.rect.h, parent.height)
757
+ color: Theme.color.surface
758
+ // Mirrors hyprland decoration:rounding; media fills the surface and keeps square corners, a visible corner only shows on text and audio panes.
759
+ radius: Style.cornerRadius
760
+
761
+ Behavior on rise {
762
+ enabled: root.active && !Theme.reducedMotion
763
+ NumberAnimation { duration: Motion.durMs.open; easing.type: Easing.OutCubic }
764
+ }
765
+
766
+ // Every kind's pane, eager the way 0.3.5 drew them: the first Space decodes and never
767
+ // builds. Moving to the next file holds this one's picture through the swap below until that one is whole.
768
+ Item {
769
+ id: panes
770
+ // The size the next card has, so a preview built under a held picture is whole at the size it lands on.
771
+ width: Theme.cardSpan(root.nextRect.w, root.width)
772
+ height: Theme.cardSpan(root.nextRect.h, root.height)
773
+
774
+ Flea.PreviewText {
775
+ id: textPane
776
+ anchors.fill: parent
777
+ anchors.margins: Theme.spacing.hairline
778
+ active: root.isCodeText
779
+ path: root.path
780
+ size: root.size
781
+ backend: root.pane ? root.pane.backend : null
782
+ markName: Icons.glyphFor(root.iconName)
783
+ // The column's rule: code gets the gutter, plain text none.
784
+ numbered: Kinds.kindState(root.iconName) === Kinds.CODE || Kinds.isCode(root.path)
785
+ wrap: ViewState.previewWrap
786
+ closeFocused: root.markdownCloseFocused
787
+ onCloseRequested: root.close()
788
+ // ExtThumbs: Quick Look reads at most the first 256 KiB on network and phone storage.
789
+ maxBytes: ExtThumbs.textLimit(root.pane ? root.pane.storageClass : "")
790
+ truncate: root.pane ? (root.pane.storageClass === "network" || root.pane.storageClass === "phone") : false
791
+ }
792
+
793
+ // A file-path Loader, never an inline Component, so a window that never shows Markdown never compiles it.
794
+ Loader {
795
+ id: markdownLoader
796
+ anchors.fill: parent
797
+ // The pane lies inside the surface's own frame, as the column's does; media fills it on purpose.
798
+ anchors.margins: Theme.spacing.hairline
799
+ active: root.isDocument
800
+ source: root.isHtml ? "HtmlPane.qml" : "MarkdownPane.qml"
801
+ onLoaded: {
802
+ item.blockPath = Qt.binding(function () { return root.inlineMarkdownPath })
803
+ item.iconName = Qt.binding(function () { return root.iconName })
804
+ item.path = Qt.binding(function () { return root.path })
805
+ item.size = Qt.binding(function () { return root.size })
806
+ item.view = Qt.binding(function () { return root.markdownSource ? "source" : "rendered" })
807
+ item.maxBytes = Qt.binding(function () {
808
+ return root.isOffice ? Kinds.OFFICE_LIMIT : ExtThumbs.textLimit(root.pane ? root.pane.storageClass : "")
809
+ })
810
+ item.truncate = Qt.binding(function () {
811
+ return root.pane ? (root.pane.storageClass === "network"
812
+ || root.pane.storageClass === "phone") : false
813
+ })
814
+ item.active = Qt.binding(function () { return root.isDocument })
815
+ item.closeFocused = Qt.binding(function () { return root.markdownCloseFocused })
816
+ item.backend = Qt.binding(function () { return root.pane ? root.pane.backend : null })
817
+ item.wrap = Qt.binding(function () { return ViewState.previewWrap })
818
+ item.closeRequested.connect(root.close)
819
+ }
820
+ }
821
+
822
+ Loader {
823
+ id: fontLoader
824
+ anchors.fill: parent
825
+ anchors.margins: Theme.spacing.hairline
826
+ active: root.isFont
827
+ source: "FontPane.qml"
828
+ onLoaded: {
829
+ item.path = Qt.binding(function () { return root.path })
830
+ item.size = Qt.binding(function () { return root.size })
831
+ item.backend = Qt.binding(function () { return root.pane ? root.pane.backend : null })
832
+ item.active = Qt.binding(function () { return root.isFont })
833
+ item.closeRequested.connect(root.close)
834
+ }
835
+ }
836
+
837
+ // A file-path Loader, so a window that never compares compiles none of it; the item asks its one diff on load.
838
+ Loader {
839
+ id: compareLoader
840
+ anchors.fill: parent
841
+ active: root.isCompare
842
+ source: "CompareView.qml"
843
+ onLoaded: {
844
+ item.backend = Qt.binding(function () { return root.pane ? root.pane.backend : null })
845
+ item.opener = Qt.binding(function () { return root.pane ? root.pane.opener : null })
846
+ item.pathA = Qt.binding(function () { return root.path })
847
+ item.pathB = Qt.binding(function () { return root.comparePath })
848
+ item.iconName = Qt.binding(function () { return root.iconName })
849
+ item.active = Qt.binding(function () { return root.active })
850
+ item.closeRequested.connect(root.close)
851
+ }
852
+ }
853
+
854
+ Loader {
855
+ id: mediaLoader
856
+ anchors.fill: parent
857
+ onLoaded: {
858
+ item.path = Qt.binding(function () { return root.path })
859
+ item.kind = Qt.binding(function () { return root.kind })
860
+ item.size = Qt.binding(function () { return root.size })
861
+ item.kindName = Qt.binding(function () { return root.kindName })
862
+ item.rate = Qt.binding(function () { return root.mediaRate })
863
+ }
864
+ }
865
+
866
+ // source rather than sourceComponent, so a file is decoded only while an image is open and its texture goes with the item.
867
+ Loader {
868
+ id: imageLoader
869
+ anchors.fill: parent
870
+ // With no thumbnail to show, built once the size reply lands or the cap passes, so the picture decodes once at the card's final size.
871
+ active: !root.sizeWait || root.interimThumb.length > 0
872
+ onLoaded: {
873
+ item.path = Qt.binding(function () { return root.path })
874
+ item.origW = Qt.binding(function () { return root.imageW })
875
+ item.origH = Qt.binding(function () { return root.imageH })
876
+ item.thumbPending = Qt.binding(function () { return root.thumbRow >= 0 })
877
+ item.frames = Qt.binding(function () { return root.imageFrames })
878
+ item.origOrient = Qt.binding(function () { return root.imageOrient })
879
+ item.embedded = Qt.binding(function () { return root.rawCopy !== null ? root.rawCopy.file : "" })
880
+ item.embeddedAnswered = Qt.binding(function () { return root.rawAnswered })
881
+ item.interimThumb = Qt.binding(function () { return root.interimThumb })
882
+ item.interimVisible = Qt.binding(function () { return root.interimVisible })
883
+ item.interimX = Qt.binding(function () { return root.interimBox !== null ? root.interimBox.x : 0 })
884
+ item.interimY = Qt.binding(function () { return root.interimBox !== null ? root.interimBox.y : 0 })
885
+ item.interimWidth = Qt.binding(function () { return root.interimBox !== null ? root.interimBox.w : 0 })
886
+ item.interimHeight = Qt.binding(function () { return root.interimBox !== null ? root.interimBox.h : 0 })
887
+ }
888
+ }
889
+
890
+ // The canvas's PdfViewer, source not sourceComponent, so QtQuick.Pdf loads on the first PDF and never for a folder without one.
891
+ Loader {
892
+ id: pdfLoader
893
+ anchors.fill: parent
894
+ onLoaded: {
895
+ item.path = Qt.binding(function () { return root.path })
896
+ item.active = true
897
+ // A document on a hangable class loads from the backend's fetched copy.
898
+ item.backend = Qt.binding(function () { return root.pane ? root.pane.backend : null })
899
+ item.fetchFirst = Qt.binding(function () { return root.pane ? ExtThumbs.present(root.pane.storageClass) : false })
900
+ item.viewerSlot = "quicklook"
901
+ item.forceActiveFocus()
902
+ }
903
+ }
904
+
905
+ Connections {
906
+ target: pdfLoader.item
907
+ function onClosed() { root.close() }
908
+ function onPageAspectChanged() { root.sizeArrived(pdfLoader.item.pageAspect, 1, 1, pdfLoader.item.pageAspect > 0) }
909
+ }
910
+
911
+ // The canvas's Archive tile at Quick Look size, its own component so this file stays inside its ceiling.
912
+ Flea.QuickLookArchive {
913
+ id: archivePane
914
+ anchors.fill: parent
915
+ anchors.margins: Theme.spacing.rowPaddingX
916
+ visible: root.isArchive && root.archiveMeta !== null && !root.archiveFailed
917
+ path: root.path
918
+ meta: root.archiveMeta
919
+ }
920
+
921
+ // Declined, or an archive whose index could not be read: a mark over the sentence, never a bare surface.
922
+ Column {
923
+ anchors.centerIn: parent
924
+ width: parent.width - 2 * Theme.spacing.rowPaddingX
925
+ spacing: Theme.spacing.gap
926
+ visible: false // Unavailable non-text previews remain blank.
927
+
928
+ Flea.Glyph {
929
+ anchors.horizontalCenter: parent.horizontalCenter
930
+ // The overlay declining is a pane state standing alone, which States.dc.html draws at 40.
931
+ maxSize: Theme.stateMarkSize
932
+ width: Theme.stateMarkSize
933
+ height: Theme.stateMarkSize
934
+ name: root.archiveFailed ? "alert" : "file"
935
+ color: root.archiveFailed ? Theme.color.error : Theme.color.mutedFrame
936
+ }
937
+
938
+ Text {
939
+ width: parent.width
940
+ horizontalAlignment: Text.AlignHCenter
941
+ text: root.status
942
+ color: root.archiveFailed ? Theme.color.foreground : Theme.color.muted
943
+ font.family: Theme.font.family
944
+ font.pixelSize: Theme.font.body
945
+ textFormat: Text.PlainText
946
+ wrapMode: Text.Wrap
947
+ }
948
+ }
949
+
950
+ // Media still buffering or an image still decoding shows the crawl; LoadingState's hold-off keeps a fast local open from flashing it.
951
+ Flea.LoadingState {
952
+ anchors.fill: parent
953
+ visible: root.loadingEarly || (root.isMedia || root.isImage || root.isArchive || root.isFont) && root.status === "loading"
954
+ heldOff: root.loadingEarly || root.swap !== null && root.swap.fellBack
955
+ }
956
+
957
+ // MediaStrip unframed: quiet over the video, permanent on audio, and the column draws the framed form of the same file.
958
+ Flea.MediaStrip {
959
+ id: mediaStrip
960
+ visible: root.isMedia && (root.kind === "audio" || root.stripShown)
961
+ anchors.left: parent.left
962
+ anchors.right: parent.right
963
+ anchors.bottom: parent.bottom
964
+ framed: false
965
+ playing: root.status === "playing"
966
+ position: root.position
967
+ duration: root.duration
968
+ onToggled: root.togglePlay()
969
+ onSeeked: function (ms) { root.seekTo(ms) }
970
+ onTouched: root.revealStrip()
971
+ }
972
+ }
973
+
974
+ // The swap wrapper waits for the first open, so launch pays no Loader, compile or picture.
975
+ Loader {
976
+ id: swapLoader
977
+ anchors.fill: parent
978
+ active: false
979
+ source: "QuickLookSwap.qml"
980
+ onLoaded: {
981
+ // The surface's own fill, inside its hairline, so the live border and rounded corners show round the picture.
982
+ item.panesSource = panes
983
+ item.ready = Qt.binding(function () { return root.lookReady })
984
+ item.ground = Qt.binding(function () { return surface.color })
985
+ item.groundInset = Qt.binding(function () { return surface.border.width })
986
+ item.groundRadius = Qt.binding(function () { return Math.max(0, surface.radius - surface.border.width) })
987
+ }
988
+ }
989
+ }
990
+
991
+ // During the hold a press or the wheel reveals at once and reaches nothing unseen, so no click closes a card that never drew.
992
+ MouseArea {
993
+ id: holdShield
994
+ anchors.fill: parent
995
+ // Held through its own press, so the release after the reveal clicks nothing either.
996
+ enabled: root.unrevealed || holdShield.pressed
997
+ acceptedButtons: Qt.AllButtons
998
+ onPressed: root.revealNow()
999
+ onWheel: function (wheel) {
1000
+ wheel.accepted = true
1001
+ root.revealNow()
1002
+ }
1003
+ }
1004
+ }