@leavepulse/ui 0.82.0 → 0.83.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 (263) hide show
  1. package/dist/assets/LpVideoTrim-BcGs3Sh0.css +8 -0
  2. package/dist/assets/VideoFlash-VuPkC7QV.css +23 -0
  3. package/dist/chunks/{ContextMenuItems-kRVTuSZd.js → ContextMenuItems-h-apPY2d.js} +1 -1
  4. package/dist/chunks/{DropzoneLinkField-CvMUu8HQ.js → DropzoneLinkField-_Op8b_ee.js} +1 -1
  5. package/dist/chunks/{FieldLeading-CNN_5Dp1.js → FieldLeading-mOMcwNqi.js} +1 -1
  6. package/dist/chunks/{FieldWrapper-BBJPEVHr.js → FieldWrapper-DtT-AIcn.js} +1 -1
  7. package/dist/chunks/{IconCloseButton-C59_etp2.js → IconCloseButton-YmisskyZ.js} +1 -1
  8. package/dist/chunks/{LayoutCanvas-BIY74SUe.js → LayoutCanvas-BrISZEBr.js} +1 -1
  9. package/dist/chunks/{LayoutNode-CTlKlhb-.js → LayoutNode-CwMQ8ACa.js} +1 -1
  10. package/dist/chunks/{LpActionBar-Cowb-28D.js → LpActionBar-CVVC6YbC.js} +1 -1
  11. package/dist/chunks/{LpAddressInput-nTJlUQ6K.js → LpAddressInput-tiuJ-bru.js} +7 -7
  12. package/dist/chunks/{LpAlert-DhZdjekN.js → LpAlert-8-x4H_Nk.js} +1 -1
  13. package/dist/chunks/{LpAppShell-CK9hTgQ1.js → LpAppShell-BW2YMq-A.js} +4 -4
  14. package/dist/chunks/{LpAutocomplete--PaZkQoU.js → LpAutocomplete-BZODIops.js} +4 -4
  15. package/dist/chunks/{LpAvatar-DVCld_L2.js → LpAvatar-BZq4nH53.js} +1 -1
  16. package/dist/chunks/{LpBadge-DXvpxk6z.js → LpBadge-Cz0pSSUu.js} +1 -1
  17. package/dist/chunks/{LpBottomNav-COQiBNUN.js → LpBottomNav-DVdR5Mry.js} +1 -1
  18. package/dist/chunks/{LpBreadcrumbs-Dt1O5G4O.js → LpBreadcrumbs-B2jphAel.js} +1 -1
  19. package/dist/chunks/{LpCalendar-C8Xlukd2.js → LpCalendar-C0_N5LVT.js} +1 -1
  20. package/dist/chunks/{LpCheckbox-Dpybd3oV.js → LpCheckbox-CwqLNdw4.js} +2 -2
  21. package/dist/chunks/{LpCodeBlock-C1Hu5RyT.js → LpCodeBlock-Dcss6bnl.js} +2 -2
  22. package/dist/chunks/{LpCodeDisplay-BVruT3-A.js → LpCodeDisplay-CLms1zDY.js} +4 -4
  23. package/dist/chunks/{LpColorPicker-BK-NRaGK.js → LpColorPicker-CbIY450X.js} +3 -3
  24. package/dist/chunks/{LpConfirmDialog-2CQFKtqU.js → LpConfirmDialog-RmP4z6va.js} +2 -2
  25. package/dist/chunks/{LpConfirmHost-CTkSytVl.js → LpConfirmHost-DheBgRMB.js} +1 -1
  26. package/dist/chunks/{LpContextMenu-DcHPdW-t.js → LpContextMenu-DIYhlap8.js} +2 -2
  27. package/dist/chunks/{LpContextSwitcher-C_nnDbeI.js → LpContextSwitcher-fDA6ERl-.js} +2 -2
  28. package/dist/chunks/{LpCopyButton-De7_elaO.js → LpCopyButton-DSqKFGEb.js} +1 -1
  29. package/dist/chunks/{LpDataList-BjZPfQjO.js → LpDataList-f9UjSx9m.js} +2 -2
  30. package/dist/chunks/{LpDatePicker-wOrHd0fg.js → LpDatePicker-3kDQLVbg.js} +5 -5
  31. package/dist/chunks/{LpDateRangePicker-CVMvvo5t.js → LpDateRangePicker-DUPiTt0_.js} +3 -3
  32. package/dist/chunks/{LpDisclosure-CtQ63LeO.js → LpDisclosure-C5WZjhbQ.js} +1 -1
  33. package/dist/chunks/{LpDrawer-TZBPzyOM.js → LpDrawer-CkYvIGIa.js} +1 -1
  34. package/dist/chunks/{LpDropdownMenu-DL9LgkS8.js → LpDropdownMenu-zSsK2Xra.js} +6 -3
  35. package/dist/chunks/{LpEditable-DuPE2tkl.js → LpEditable-CP7Y_lKg.js} +2 -2
  36. package/dist/chunks/{LpEmptyState-W1U_tKg-.js → LpEmptyState-dsc5x-ya.js} +1 -1
  37. package/dist/chunks/{LpFieldNotice-BLKlYLwu.js → LpFieldNotice-TOTbWvgv.js} +1 -1
  38. package/dist/chunks/{LpFileTree-CEiRIpj1.js → LpFileTree-BPjnOTln.js} +4 -4
  39. package/dist/chunks/{LpFileTreeNode-C0292AU2.js → LpFileTreeNode-_-MiWmie.js} +4 -4
  40. package/dist/chunks/{LpFormField-Bhgi0idH.js → LpFormField-BJwD1iEF.js} +1 -1
  41. package/dist/chunks/{LpHintLayer-D8ZKsvsT.js → LpHintLayer-CmfGnxYu.js} +1 -1
  42. package/dist/chunks/{LpHints-DbIV-wk4.js → LpHints-C2Xvc1Fd.js} +3 -3
  43. package/dist/chunks/{LpIcon-CytpcLmS.js → LpIcon-CoOLz1oJ.js} +1 -1
  44. package/dist/chunks/{LpInfraNode-8cGoY87Q.js → LpInfraNode-CW3qsM-U.js} +2 -2
  45. package/dist/chunks/{LpInput-P2C12X7s.js → LpInput-BVWhkPuN.js} +3 -3
  46. package/dist/chunks/{LpInspector-DjfMa4tD.js → LpInspector-REmoX_es.js} +4 -4
  47. package/dist/chunks/{LpLightbox-BEQtCz0m.js → LpLightbox-C3FFTYP5.js} +65 -20
  48. package/dist/chunks/{LpLogInspector-CyxK1d-V.js → LpLogInspector-Cs8Z_fM0.js} +2 -2
  49. package/dist/chunks/{LpLogViewer-CZrc_HG7.js → LpLogViewer-DQRGe-ZK.js} +6 -6
  50. package/dist/chunks/{LpMapPicker-CbTfFcEu.js → LpMapPicker-CQnQPgKm.js} +1 -1
  51. package/dist/chunks/{LpMediaGallery-C_SwNR4y.js → LpMediaGallery-DkKqTy9P.js} +20 -8
  52. package/dist/chunks/{LpModal-B6Yt0G7E.js → LpModal-DwejR7VP.js} +1 -1
  53. package/dist/chunks/{LpModalNav-CMYWEiUs.js → LpModalNav-D1-dSe6f.js} +3 -3
  54. package/dist/chunks/{LpNotificationBell-BJKPx0X1.js → LpNotificationBell-EKLkN0sp.js} +4 -4
  55. package/dist/chunks/{LpNotificationFeed-orWISVez.js → LpNotificationFeed-Dc-yQ4NH.js} +4 -4
  56. package/dist/chunks/{LpNumberField-C5MeLsoE.js → LpNumberField-DvUjEMp-.js} +3 -3
  57. package/dist/chunks/{LpOtpInput-6SCQ64uK.js → LpOtpInput-xvDO14f4.js} +1 -1
  58. package/dist/chunks/{LpPagination-CkwbdFa9.js → LpPagination-BFZY8dFn.js} +1 -1
  59. package/dist/chunks/{LpPaletteTheme-RWgbWFOz.js → LpPaletteTheme-EBJv0Bwz.js} +2 -2
  60. package/dist/chunks/{LpPasswordInput-DJBRo5lt.js → LpPasswordInput-DRJR3fF1.js} +2 -2
  61. package/dist/chunks/{LpPhoneInput-DcmcJwos.js → LpPhoneInput-BGOOtUoW.js} +3 -3
  62. package/dist/chunks/{LpPie-DmMlhgo2.js → LpPie-Deyn8h5J.js} +2 -2
  63. package/dist/chunks/{LpPipeline-BXcp2shV.js → LpPipeline-9ChjeNh5.js} +2 -2
  64. package/dist/chunks/{LpQuota-CtEiqRg4.js → LpQuota-CNh5eF_p.js} +1 -1
  65. package/dist/chunks/{LpRadioGroup-Ubkk_eSq.js → LpRadioGroup-Br2Nl9Gr.js} +2 -2
  66. package/dist/chunks/{LpRangeCalendar-DCswVYbU.js → LpRangeCalendar-D66Y3U_w.js} +1 -1
  67. package/dist/chunks/{LpRangeSlider-DM1qy5Tp.js → LpRangeSlider-eX-fIlOw.js} +1 -1
  68. package/dist/chunks/{LpRepeater-BMo2Eabn.js → LpRepeater-0XvnlVd_.js} +2 -2
  69. package/dist/chunks/{LpSecretField-SSRGVqRc.js → LpSecretField-1RCooChq.js} +5 -5
  70. package/dist/chunks/{LpSection-C3bTxD7t.js → LpSection-B9gbJJcP.js} +1 -1
  71. package/dist/chunks/{LpSegmented-C1DF6m6A.js → LpSegmented-hIjGzGTo.js} +2 -2
  72. package/dist/chunks/{LpSelect-DvDr1VPn.js → LpSelect-DugubwTc.js} +4 -4
  73. package/dist/chunks/{LpSidebar-Bql881r6.js → LpSidebar-DevSoN8W.js} +4 -4
  74. package/dist/chunks/{LpSidebarNav-CKx2KJvr.js → LpSidebarNav-DwQQ3KXe.js} +3 -3
  75. package/dist/chunks/{LpSidebarRow-BMOL9kOG.js → LpSidebarRow-CAGgtJCS.js} +3 -3
  76. package/dist/chunks/{LpSlider-DsSMe4b5.js → LpSlider-1fHpVxpB.js} +1 -1
  77. package/dist/chunks/{LpStat-CQYYDMS6.js → LpStat-Cm9-FaWQ.js} +2 -2
  78. package/dist/chunks/{LpStatus-g5nSJfU3.js → LpStatus-CE8FSAT1.js} +1 -1
  79. package/dist/chunks/{LpStepMark-seVNOuNJ.js → LpStepMark-BWSlV69R.js} +1 -1
  80. package/dist/chunks/{LpStepper-D6qEXihZ.js → LpStepper-1D0ZA7Nx.js} +1 -1
  81. package/dist/chunks/{LpSwitch-DM5CVm31.js → LpSwitch-D_kvE0oQ.js} +1 -1
  82. package/dist/chunks/{LpTabFace-ZWwelqRU.js → LpTabFace-CpbFKi-N.js} +2 -2
  83. package/dist/chunks/{LpTableFilter-CWU9Dmk-.js → LpTableFilter-BYHyCu38.js} +2 -2
  84. package/dist/chunks/{LpTagsInput-C2qVF2ax.js → LpTagsInput-Cqm38t5N.js} +5 -5
  85. package/dist/chunks/{LpTextarea-C3EoV_xv.js → LpTextarea-DaSKBkiV.js} +1 -1
  86. package/dist/chunks/{LpThemeStudio-BIBahaIs.js → LpThemeStudio-BNdfi-Ds.js} +5 -5
  87. package/dist/chunks/{LpThemeSwitcher-D4MqFLGS.js → LpThemeSwitcher-CbC1nqBP.js} +3 -3
  88. package/dist/chunks/{LpTimeField-DPOkbvyP.js → LpTimeField-CCfPCWPT.js} +2 -2
  89. package/dist/chunks/{LpToaster-Dj1VKxFv.js → LpToaster-BhlI7h-h.js} +2 -2
  90. package/dist/chunks/{LpTopologyCanvas-Bspzwm7x.js → LpTopologyCanvas-DlKbu7E5.js} +4 -4
  91. package/dist/chunks/{LpTour-CwpVdVN-.js → LpTour-CSLerA1B.js} +1 -1
  92. package/dist/chunks/{LpTruncate-JSaIW25q.js → LpTruncate-Ct26BSYB.js} +1 -1
  93. package/dist/chunks/{LpVideoBackdrop-Ct4St4ZL.js → LpVideoBackdrop-57i8RY8g.js} +1 -2
  94. package/dist/chunks/LpVideoPlayer-DcWd9DDW.js +813 -0
  95. package/dist/chunks/{LpWindow-RVcP436Z.js → LpWindow-YnF_knSj.js} +2 -2
  96. package/dist/chunks/{LpWorkspaceSwitcher-D0WOz1Fg.js → LpWorkspaceSwitcher-B-5UU6o4.js} +3 -3
  97. package/dist/chunks/{MenuTitle-D8Umab23.js → MenuTitle-BnZbFSwq.js} +1 -1
  98. package/dist/chunks/{OptionContent-DCnX_KRK.js → OptionContent-D5e_eklA.js} +1 -1
  99. package/dist/chunks/{PickerField-ChjY27bm.js → PickerField-KpHiowNI.js} +1 -1
  100. package/dist/chunks/{RevealToggle-MaL7PEjr.js → RevealToggle-CE1hAIJB.js} +1 -1
  101. package/dist/chunks/SteadyText-CwH-kduj.js +25 -0
  102. package/dist/chunks/{TourCard-oXZSpHKb.js → TourCard-t2csiTDS.js} +1 -1
  103. package/dist/chunks/VideoThumb-Csf9bUz3.js +67 -0
  104. package/dist/chunks/VideoVolume-B4_DNmNi.js +68 -0
  105. package/dist/chunks/icons-CdSlCrle.js +59 -0
  106. package/dist/component-names.d.ts +1 -1
  107. package/dist/components/ContextMenuItems.vue.js +1 -1
  108. package/dist/components/DropzoneLinkField.vue.js +1 -1
  109. package/dist/components/FieldLeading.vue.js +1 -1
  110. package/dist/components/FieldWrapper.vue.js +1 -1
  111. package/dist/components/IconCloseButton.vue.js +1 -1
  112. package/dist/components/LayoutCanvas.vue.js +1 -1
  113. package/dist/components/LayoutNode.vue.js +1 -1
  114. package/dist/components/LpActionBar.vue.js +1 -1
  115. package/dist/components/LpAddressInput.vue.js +1 -1
  116. package/dist/components/LpAlert.vue.js +1 -1
  117. package/dist/components/LpAppShell.vue.js +1 -1
  118. package/dist/components/LpAutocomplete.vue.js +1 -1
  119. package/dist/components/LpAvatar.vue.js +1 -1
  120. package/dist/components/LpBadge.vue.js +1 -1
  121. package/dist/components/LpBottomNav.vue.js +1 -1
  122. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  123. package/dist/components/LpCalendar.vue.js +1 -1
  124. package/dist/components/LpChart.vue.js +2 -2
  125. package/dist/components/LpCheckbox.vue.js +1 -1
  126. package/dist/components/LpCodeBlock.vue.js +1 -1
  127. package/dist/components/LpCodeDisplay.vue.js +1 -1
  128. package/dist/components/LpColorPicker.vue.js +1 -1
  129. package/dist/components/LpCommandPalette.vue.js +2 -2
  130. package/dist/components/LpConfirmDialog.vue.js +1 -1
  131. package/dist/components/LpConfirmHost.vue.js +1 -1
  132. package/dist/components/LpContextMenu.vue.js +1 -1
  133. package/dist/components/LpContextSwitcher.vue.js +1 -1
  134. package/dist/components/LpCopyButton.vue.js +1 -1
  135. package/dist/components/LpDataList.vue.js +1 -1
  136. package/dist/components/LpDatePicker.vue.js +1 -1
  137. package/dist/components/LpDateRangePicker.vue.js +1 -1
  138. package/dist/components/LpDisclosure.vue.js +1 -1
  139. package/dist/components/LpDock.vue.js +1 -1
  140. package/dist/components/LpDrawer.vue.js +1 -1
  141. package/dist/components/LpDropdownMenu.vue.d.ts +5 -1
  142. package/dist/components/LpDropdownMenu.vue.js +1 -1
  143. package/dist/components/LpDropzone.vue.js +2 -2
  144. package/dist/components/LpEditable.vue.js +1 -1
  145. package/dist/components/LpEmptyState.vue.js +1 -1
  146. package/dist/components/LpFieldNotice.vue.js +1 -1
  147. package/dist/components/LpFileTree.vue.js +1 -1
  148. package/dist/components/LpFileTreeNode.vue.js +1 -1
  149. package/dist/components/LpFormField.vue.js +1 -1
  150. package/dist/components/LpHintLayer.vue.js +1 -1
  151. package/dist/components/LpHints.vue.js +1 -1
  152. package/dist/components/LpIcon.vue.js +1 -1
  153. package/dist/components/LpImageEditor.vue.js +1 -1
  154. package/dist/components/LpInfraNode.vue.js +1 -1
  155. package/dist/components/LpInput.vue.js +1 -1
  156. package/dist/components/LpInspector.vue.js +1 -1
  157. package/dist/components/LpLightbox.vue.js +1 -1
  158. package/dist/components/LpLogInspector.vue.js +1 -1
  159. package/dist/components/LpLogViewer.vue.js +1 -1
  160. package/dist/components/LpMapPicker.vue.js +1 -1
  161. package/dist/components/LpMediaGallery.vue.d.ts +1 -1
  162. package/dist/components/LpMediaGallery.vue.js +1 -1
  163. package/dist/components/LpModal.vue.js +1 -1
  164. package/dist/components/LpModalNav.vue.js +1 -1
  165. package/dist/components/LpNotificationBell.vue.js +1 -1
  166. package/dist/components/LpNotificationFeed.vue.js +1 -1
  167. package/dist/components/LpNumberField.vue.js +1 -1
  168. package/dist/components/LpOtpInput.vue.js +1 -1
  169. package/dist/components/LpPagination.vue.js +1 -1
  170. package/dist/components/LpPaletteFacts.vue.js +1 -1
  171. package/dist/components/LpPaletteHeader.vue.js +2 -2
  172. package/dist/components/LpPaletteTheme.vue.js +1 -1
  173. package/dist/components/LpPasswordInput.vue.js +1 -1
  174. package/dist/components/LpPhoneInput.vue.js +1 -1
  175. package/dist/components/LpPie.vue.js +1 -1
  176. package/dist/components/LpPipeline.vue.js +1 -1
  177. package/dist/components/LpQuota.vue.js +1 -1
  178. package/dist/components/LpRadioGroup.vue.js +1 -1
  179. package/dist/components/LpRangeCalendar.vue.js +1 -1
  180. package/dist/components/LpRangeSlider.vue.js +1 -1
  181. package/dist/components/LpRepeater.vue.js +1 -1
  182. package/dist/components/LpSecretField.vue.js +1 -1
  183. package/dist/components/LpSection.vue.js +1 -1
  184. package/dist/components/LpSegmented.vue.js +1 -1
  185. package/dist/components/LpSelect.vue.js +1 -1
  186. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  187. package/dist/components/LpSidebar.vue.js +1 -1
  188. package/dist/components/LpSidebarNav.vue.js +1 -1
  189. package/dist/components/LpSidebarRow.vue.js +1 -1
  190. package/dist/components/LpSlider.vue.js +1 -1
  191. package/dist/components/LpStat.vue.js +1 -1
  192. package/dist/components/LpStatus.vue.js +1 -1
  193. package/dist/components/LpStepMark.vue.js +1 -1
  194. package/dist/components/LpStepper.vue.js +1 -1
  195. package/dist/components/LpSwitch.vue.js +1 -1
  196. package/dist/components/LpTabFace.vue.js +1 -1
  197. package/dist/components/LpTable.vue.js +9 -9
  198. package/dist/components/LpTableFilter.vue.js +1 -1
  199. package/dist/components/LpTabs.vue.js +2 -2
  200. package/dist/components/LpTagsInput.vue.js +1 -1
  201. package/dist/components/LpTextarea.vue.js +1 -1
  202. package/dist/components/LpThemeStudio.vue.js +1 -1
  203. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  204. package/dist/components/LpTimeField.vue.js +1 -1
  205. package/dist/components/LpToaster.vue.js +1 -1
  206. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  207. package/dist/components/LpTour.vue.js +1 -1
  208. package/dist/components/LpTruncate.vue.js +1 -1
  209. package/dist/components/LpVideoBackdrop.vue.js +1 -1
  210. package/dist/components/LpVideoPlayer.vue.d.ts +111 -0
  211. package/dist/components/LpVideoPlayer.vue.js +4 -0
  212. package/dist/components/LpVideoTrim.vue.d.ts +19 -5
  213. package/dist/components/LpVideoTrim.vue.js +596 -445
  214. package/dist/components/LpWindow.vue.js +1 -1
  215. package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
  216. package/dist/components/MenuTitle.vue.js +1 -1
  217. package/dist/components/OptionContent.vue.js +1 -1
  218. package/dist/components/PickerField.vue.js +1 -1
  219. package/dist/components/RevealToggle.vue.js +1 -1
  220. package/dist/components/SteadyText.vue.d.ts +7 -0
  221. package/dist/components/SteadyText.vue.js +4 -0
  222. package/dist/components/TourCard.vue.js +1 -1
  223. package/dist/components/VideoFlash.vue.d.ts +10 -0
  224. package/dist/components/VideoFlash.vue.js +33 -0
  225. package/dist/components/VideoThumb.vue.d.ts +12 -0
  226. package/dist/components/VideoThumb.vue.js +4 -0
  227. package/dist/components/VideoVolume.vue.d.ts +15 -0
  228. package/dist/components/VideoVolume.vue.js +4 -0
  229. package/dist/components/lightbox.d.ts +13 -0
  230. package/dist/components/lightbox.js +11 -1
  231. package/dist/components/menuWhen.js +1 -1
  232. package/dist/components/videoBackdrop.d.ts +15 -2
  233. package/dist/components/videoBackdrop.js +8 -2
  234. package/dist/components/videoMotion.d.ts +28 -0
  235. package/dist/components/videoMotion.js +26 -0
  236. package/dist/components/videoTrim.d.ts +2 -9
  237. package/dist/components/videoTrim.js +0 -6
  238. package/dist/composables/useVideoMedia.d.ts +74 -0
  239. package/dist/composables/useVideoMedia.js +291 -0
  240. package/dist/icons-all.js +1 -1
  241. package/dist/index.d.ts +4 -1
  242. package/dist/index.js +173 -168
  243. package/package.json +1 -1
  244. package/src/component-names.ts +1 -0
  245. package/src/components/LpDropdownMenu.vue +4 -0
  246. package/src/components/LpLightbox.vue +61 -8
  247. package/src/components/LpMediaGallery.vue +19 -2
  248. package/src/components/LpVideoBackdrop.vue +1 -7
  249. package/src/components/LpVideoPlayer.vue +774 -0
  250. package/src/components/LpVideoTrim.vue +437 -295
  251. package/src/components/SteadyText.vue +21 -0
  252. package/src/components/VideoFlash.vue +52 -0
  253. package/src/components/VideoThumb.vue +66 -0
  254. package/src/components/VideoVolume.vue +63 -0
  255. package/src/components/lightbox.ts +23 -0
  256. package/src/components/videoBackdrop.ts +20 -3
  257. package/src/components/videoMotion.ts +38 -0
  258. package/src/components/videoTrim.ts +2 -14
  259. package/src/composables/useVideoMedia.ts +411 -0
  260. package/src/index.ts +4 -1
  261. package/src/kit-icons.ts +1 -1
  262. package/dist/assets/LpVideoTrim-Ci6IIHW0.css +0 -8
  263. package/dist/chunks/icons-BOdqDUii.js +0 -59
@@ -0,0 +1,411 @@
1
+ import { onBeforeUnmount, ref, watch, type Ref } from "vue"
2
+ import { SLOWEST_RATE } from "../components/videoBackdrop"
3
+ import { nodeWindow } from "./realm"
4
+ import { usePersisted } from "./usePersisted"
5
+ import { usePrefersReducedMotion } from "./useReducedMotion"
6
+ import { useTween } from "./useTween"
7
+
8
+ /*
9
+ * The state of a <video> as a component that draws its own controls needs it,
10
+ * shared by LpVideoPlayer and LpVideoTrim so both know the same things about
11
+ * a video the same way:
12
+ *
13
+ * - One seek at a time, the latest wins: a drag asks for a position on every
14
+ * pointer move, and WebKitGTK's pipeline stalls on a seek started inside
15
+ * another.
16
+ * - A MediaRecorder WebM carries no duration until its end has been seen: it
17
+ * is found by seeking far past it once.
18
+ * - Chromium answers a 404, or a login page served in place of an expired
19
+ * session's video, with "format not supported", the same code as a real
20
+ * codec problem; one byte of the address tells the two apart.
21
+ * - The playhead follows the picture every frame while playing, not the four
22
+ * `timeupdate`s a second.
23
+ *
24
+ * Bind `listeners` on the element with `v-on`.
25
+ */
26
+
27
+ /**
28
+ * Why there is no video to show:
29
+ * - "load": the address did not answer (network, 404, an expired session);
30
+ * - "decode": the file arrived but is damaged;
31
+ * - "unsupported": a format or codec this browser cannot play (H.265 in
32
+ * Chromium without hardware support, ProRes anywhere).
33
+ */
34
+ export type VideoErrorCause = "load" | "decode" | "unsupported"
35
+
36
+ export function mediaErrorCause(error: MediaError | null): VideoErrorCause {
37
+ if (error?.code === 3) return "decode"
38
+ if (error?.code === 4) return "unsupported"
39
+ return "load"
40
+ }
41
+
42
+ export interface UseVideoMediaOptions {
43
+ src: () => string
44
+ /** Every frame while playing, after `time` is updated. */
45
+ onFrame?: () => void
46
+ onError?: (cause: VideoErrorCause) => void
47
+ /** Ease starts and stops (true by default); false cuts in and out at once. */
48
+ ease?: () => boolean
49
+ /**
50
+ * Keep volume, mute and speed between videos on this device, shared by every
51
+ * player and editor of the kit: someone who turned the sound down turned it
52
+ * down, whichever component the next video is in. False forgets.
53
+ */
54
+ remember?: () => boolean
55
+ }
56
+
57
+ interface Sound {
58
+ volume: number
59
+ muted: boolean
60
+ rate: number
61
+ }
62
+ /*
63
+ * A kept record is used only when it is one a person could have chosen: a
64
+ * speed outside what the menus offer, or a volume outside 0..1, is a record
65
+ * written by an easing caught mid-way (a sixteenth speed and silence, once),
66
+ * and restoring it would start every video stuck.
67
+ */
68
+ const isSound = (v: unknown): v is Sound => {
69
+ if (!v || typeof v !== "object") return false
70
+ const { volume, muted, rate } = v as Sound
71
+ return typeof muted === "boolean" && typeof volume === "number" && volume >= 0 && volume <= 1 && typeof rate === "number" && rate >= 0.25 && rate <= 4
72
+ }
73
+
74
+ export function useVideoMedia(el: Ref<HTMLVideoElement | null>, options: UseVideoMediaOptions) {
75
+ const duration = ref(0)
76
+ const time = ref(0)
77
+ const playing = ref(false)
78
+ /** Playback stalled for data. */
79
+ const waiting = ref(false)
80
+ const ended = ref(false)
81
+ const muted = ref(false)
82
+ const volume = ref(1)
83
+ const rate = ref(1)
84
+ /** Buffered stretches as fractions of the duration. */
85
+ const buffered = ref<[number, number][]>([])
86
+ const failure = ref<VideoErrorCause | null>(null)
87
+ const size = ref({ width: 0, height: 0 })
88
+
89
+ const sound = usePersisted<Sound>(() => (options.remember?.() ?? true) && "lp-video:sound", { volume: 1, muted: false, rate: 1 }, { validate: isSound })
90
+ /* Applied once per address, when it first knows its duration; saved only after, so a fresh element's defaults never overwrite what was kept. */
91
+ let applied = ""
92
+
93
+ let seekBusy = false
94
+ let seekTarget: number | null = null
95
+ let probingDuration = false
96
+ let frame = 0
97
+
98
+ function seek(t: number) {
99
+ const video = el.value
100
+ const target = Math.min(Math.max(0, t), duration.value || t)
101
+ time.value = target
102
+ ended.value = false
103
+ if (!video) return
104
+ if (seekBusy) {
105
+ seekTarget = target
106
+ return
107
+ }
108
+ seekBusy = true
109
+ video.currentTime = target
110
+ }
111
+
112
+ /*
113
+ * `currentTime` moves in steps, one per decoded frame: at a few frames a
114
+ * second (a screen recording, a slideshow) a bar or a playhead driven by it
115
+ * jumps along. Between steps the position runs on the clock from the last
116
+ * one, at the playback rate; it never goes back (a step that lands a hair
117
+ * behind the estimate is held), and never runs more than a moment ahead of
118
+ * the video, so a stall shows as a stop, not as a bar that keeps going.
119
+ */
120
+ const AHEAD_LIMIT = 0.25
121
+ let lastMedia = -1
122
+ let lastAt = 0
123
+ function smoothTime(video: HTMLVideoElement, now: number): number {
124
+ const t = video.currentTime
125
+ if (t !== lastMedia) {
126
+ lastMedia = t
127
+ lastAt = now
128
+ }
129
+ const estimate = Math.min(lastMedia + ((now - lastAt) / 1000) * video.playbackRate, lastMedia + AHEAD_LIMIT, duration.value || Infinity)
130
+ return Math.max(estimate, Math.min(time.value, lastMedia + AHEAD_LIMIT))
131
+ }
132
+
133
+ function tick(now: number) {
134
+ frame = 0
135
+ const video = el.value
136
+ if (!video || !playing.value) return
137
+ if (!seekBusy) time.value = smoothTime(video, now)
138
+ options.onFrame?.()
139
+ if (playing.value) frame = nodeWindow(video).requestAnimationFrame(tick)
140
+ }
141
+ function stopTicking() {
142
+ if (frame && el.value) nodeWindow(el.value).cancelAnimationFrame(frame)
143
+ frame = 0
144
+ }
145
+
146
+ function readBuffered(video: HTMLVideoElement) {
147
+ const d = duration.value
148
+ if (!d) return
149
+ const out: [number, number][] = []
150
+ for (let i = 0; i < video.buffered.length; i++) out.push([video.buffered.start(i) / d, video.buffered.end(i) / d])
151
+ buffered.value = out
152
+ }
153
+
154
+ async function probe(src: string): Promise<VideoErrorCause> {
155
+ try {
156
+ const res = await fetch(src, { headers: { Range: "bytes=0-0" }, credentials: "same-origin" })
157
+ void res.body?.cancel()
158
+ const type = res.headers.get("content-type") ?? ""
159
+ return res.ok && !/^text\/|json|xml/.test(type) ? "unsupported" : "load"
160
+ } catch {
161
+ // Another origin without CORS: there is no telling, so the browser's word stands.
162
+ return "unsupported"
163
+ }
164
+ }
165
+
166
+ const listeners = {
167
+ loadedmetadata() {
168
+ const video = el.value
169
+ if (!video) return
170
+ size.value = { width: video.videoWidth, height: video.videoHeight }
171
+ if (applied !== options.src()) {
172
+ applied = options.src()
173
+ volume.value = sound.value.volume
174
+ rate.value = sound.value.rate
175
+ video.muted = video.muted || sound.value.muted
176
+ muted.value = video.muted
177
+ video.volume = volume.value
178
+ video.playbackRate = rate.value
179
+ }
180
+ if (Number.isFinite(video.duration)) {
181
+ duration.value = video.duration
182
+ return
183
+ }
184
+ probingDuration = true
185
+ video.currentTime = 1e101
186
+ },
187
+ durationchange() {
188
+ const video = el.value
189
+ if (!video || !Number.isFinite(video.duration) || !video.duration) return
190
+ duration.value = video.duration
191
+ if (probingDuration) {
192
+ probingDuration = false
193
+ video.currentTime = 0
194
+ }
195
+ },
196
+ seeked() {
197
+ seekBusy = false
198
+ lastMedia = -1
199
+ if (seekTarget !== null) {
200
+ const next = seekTarget
201
+ seekTarget = null
202
+ seek(next)
203
+ } else if (el.value && !playing.value) time.value = el.value.currentTime
204
+ },
205
+ play() {
206
+ lastMedia = -1
207
+ // Started from outside (media keys, the picture-in-picture window): it wants to play now.
208
+ wantsPlay.value = true
209
+ playing.value = true
210
+ ended.value = false
211
+ if (!frame && el.value) frame = nodeWindow(el.value).requestAnimationFrame(tick)
212
+ },
213
+ pause() {
214
+ wantsPlay.value = false
215
+ playing.value = false
216
+ stopTicking()
217
+ if (el.value && !seekBusy) time.value = el.value.currentTime
218
+ },
219
+ ended() {
220
+ instant = true
221
+ wantsPlay.value = false
222
+ ended.value = true
223
+ },
224
+ waiting() {
225
+ waiting.value = true
226
+ },
227
+ playing() {
228
+ waiting.value = false
229
+ },
230
+ canplay() {
231
+ waiting.value = false
232
+ },
233
+ timeupdate() {
234
+ if (el.value && !playing.value && !seekBusy) time.value = el.value.currentTime
235
+ },
236
+ progress() {
237
+ if (el.value) readBuffered(el.value)
238
+ },
239
+ volumechange() {
240
+ const video = el.value
241
+ if (!video) return
242
+ // Firefox reports itself muted below a quarter speed: during the easing that is the easing's.
243
+ if (!easing()) muted.value = video.muted
244
+ if (easing() || video.volume === written.volume) return
245
+ volume.value = video.volume
246
+ remember()
247
+ },
248
+ ratechange() {
249
+ const video = el.value
250
+ if (!video || easing() || video.playbackRate === written.rate) return
251
+ rate.value = video.playbackRate
252
+ remember()
253
+ },
254
+ async error() {
255
+ const video = el.value
256
+ if (!video?.error) return
257
+ const src = options.src()
258
+ let cause = mediaErrorCause(video.error)
259
+ if (cause === "unsupported") cause = await probe(src)
260
+ if (src !== options.src()) return
261
+ failure.value = cause
262
+ waiting.value = false
263
+ options.onError?.(cause)
264
+ },
265
+ }
266
+
267
+ /*
268
+ * Start and stop are eased, as LpVideoBackdrop's `easePlayback` is: a play
269
+ * gathers speed and sound from a standstill over a moment, a pause slows and
270
+ * quietens to one, instead of the picture and the sound cutting in and out
271
+ * in a single frame (and the sound with a click). `wantsPlay` turns at once,
272
+ * so a button's icon answers the press; the element follows. A pause for a
273
+ * seek or a drag, or at a clip's end, is `instant`: there the exact frame
274
+ * matters more than the glide. Reduced motion has no easing at all.
275
+ */
276
+ const wantsPlay = ref(false)
277
+ let instant = false
278
+ const reducedMotion = usePrefersReducedMotion()
279
+ const throttle = useTween<number>(
280
+ () => (wantsPlay.value ? 1 : 0),
281
+ (from, to, k) => from + (to - from) * k,
282
+ { enabled: () => !instant && !reducedMotion.value && (options.ease?.() ?? true) },
283
+ )
284
+ /*
285
+ * Only around playback: a paused element keeps its real rate and volume.
286
+ * Firefox silences sound below a quarter speed and reports the element as
287
+ * muted meanwhile, so a paused video held at the slowest rate came up muted
288
+ * and would not be unmuted. While the rate eases, what the element reports
289
+ * about its sound is the easing's, not a choice, and is not taken as one.
290
+ */
291
+ /* What the easing itself last wrote: the element's later report of the same value is an echo, not a choice. */
292
+ const written = { rate: Number.NaN, volume: Number.NaN }
293
+ function applyThrottle(video: HTMLVideoElement, k: number) {
294
+ written.rate = video.playbackRate = k >= 1 ? rate.value : Math.max(SLOWEST_RATE, rate.value * k)
295
+ written.volume = video.volume = k >= 1 ? volume.value : volume.value * k
296
+ }
297
+ watch(throttle, (k) => {
298
+ const video = el.value
299
+ if (!video) return
300
+ if (k === 0 && !wantsPlay.value) {
301
+ if (!video.paused) video.pause()
302
+ applyThrottle(video, 1)
303
+ instant = false
304
+ return
305
+ }
306
+ applyThrottle(video, k)
307
+ if (k === 1) instant = false
308
+ })
309
+ /*
310
+ * Whether the element's rate and volume are the easing's rather than the
311
+ * chosen ones: from the press of play until full speed, and from the press
312
+ * of pause until the stop. The element reports a change a moment after it
313
+ * is made, so the bounds are the intent, not the throttle alone: a start
314
+ * reported while the throttle still read 0 was taken for a choice of a
315
+ * sixteenth speed and silence.
316
+ */
317
+ const easing = () => (wantsPlay.value ? throttle.value < 1 : throttle.value > 0)
318
+
319
+ function remember() {
320
+ if (applied) sound.value = { volume: volume.value, muted: muted.value, rate: rate.value }
321
+ }
322
+
323
+ /** Play, quietly: a browser that refuses (autoplay with sound) just leaves it paused. */
324
+ function play() {
325
+ const video = el.value
326
+ if (!video || failure.value) return
327
+ if (ended.value) seek(0)
328
+ wantsPlay.value = true
329
+ // From a standstill the first frame already plays slow and quiet, not at full speed for one frame.
330
+ if (!instant && !reducedMotion.value && (options.ease?.() ?? true) && throttle.value === 0) applyThrottle(video, 0)
331
+ void video.play().catch(() => {
332
+ wantsPlay.value = false
333
+ })
334
+ }
335
+ /** Pause, easing to a stop unless `instant` (a seek, a drag, a clip's end, where the frame must be exact). */
336
+ function pause(options: { instant?: boolean } = {}) {
337
+ if (options.instant) instant = true
338
+ wantsPlay.value = false
339
+ }
340
+ function toggle() {
341
+ if (wantsPlay.value) pause()
342
+ else play()
343
+ }
344
+ function setMuted(value: boolean) {
345
+ if (el.value) el.value.muted = value
346
+ muted.value = value
347
+ remember()
348
+ }
349
+ function setVolume(value: number) {
350
+ const v = Math.min(1, Math.max(0, value))
351
+ volume.value = v
352
+ if (el.value) {
353
+ el.value.volume = easing() ? v * throttle.value : v
354
+ // Turning the sound up is asking to hear it.
355
+ if (v > 0 && el.value.muted) el.value.muted = false
356
+ }
357
+ // Said now, not on the element's event: a slider bound to "muted ? 0 : volume" would spring back to 0 meanwhile.
358
+ if (v > 0) muted.value = false
359
+ remember()
360
+ }
361
+ function setRate(value: number) {
362
+ rate.value = value
363
+ if (el.value) el.value.playbackRate = easing() ? Math.max(SLOWEST_RATE, value * throttle.value) : value
364
+ remember()
365
+ }
366
+
367
+ /**
368
+ * The frame on screen as a PNG, for "copy frame". Null when there is none
369
+ * yet, or when the video came from another origin without CORS and the
370
+ * browser will not let it be read back.
371
+ */
372
+ function captureFrame(): Promise<Blob | null> {
373
+ const video = el.value
374
+ if (!video?.videoWidth) return Promise.resolve(null)
375
+ const canvas = video.ownerDocument.createElement("canvas")
376
+ canvas.width = video.videoWidth
377
+ canvas.height = video.videoHeight
378
+ try {
379
+ canvas.getContext("2d")?.drawImage(video, 0, 0)
380
+ return new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), "image/png"))
381
+ } catch {
382
+ return Promise.resolve(null)
383
+ }
384
+ }
385
+
386
+ watch(options.src, () => {
387
+ duration.value = 0
388
+ time.value = 0
389
+ failure.value = null
390
+ buffered.value = []
391
+ ended.value = false
392
+ waiting.value = false
393
+ seekBusy = false
394
+ seekTarget = null
395
+ probingDuration = false
396
+ })
397
+
398
+ onBeforeUnmount(() => {
399
+ stopTicking()
400
+ const video = el.value
401
+ if (!video) return
402
+ // Stop the stream from fetching on: an element that is merely detached keeps its connection.
403
+ video.pause()
404
+ video.removeAttribute("src")
405
+ video.load()
406
+ })
407
+
408
+ return { duration, time, playing, wantsPlay, waiting, ended, muted, volume, rate, buffered, failure, size, listeners, seek, play, pause, toggle, setMuted, setVolume, setRate, captureFrame }
409
+ }
410
+
411
+ export type VideoMedia = ReturnType<typeof useVideoMedia>
package/src/index.ts CHANGED
@@ -156,7 +156,7 @@ export { default as LpInput } from "./components/LpInput.vue"
156
156
  export { default as LpLightbox } from "./components/LpLightbox.vue"
157
157
  export type { LightboxLabels } from "./components/LpLightbox.vue"
158
158
  export type { LightboxItem } from "./components/lightbox"
159
- export { fileNameOf } from "./components/lightbox"
159
+ export { fileNameOf, isVideoItem } from "./components/lightbox"
160
160
  export { default as LpLink } from "./components/LpLink.vue"
161
161
  export { default as LpLogViewer } from "./components/LpLogViewer.vue"
162
162
  export { default as LpLogInspector } from "./components/LpLogInspector.vue"
@@ -227,6 +227,9 @@ export { default as LpVideoBackdrop } from "./components/LpVideoBackdrop.vue"
227
227
  export { formatClipTime, isNativeLoop, planLoop, seamProgress, seamReached } from "./components/videoBackdrop"
228
228
  export type { LoopPlan } from "./components/videoBackdrop"
229
229
  export { default as LpVideoTrim } from "./components/LpVideoTrim.vue"
230
+ export { default as LpVideoPlayer } from "./components/LpVideoPlayer.vue"
231
+ export { useVideoMedia } from "./composables/useVideoMedia"
232
+ export type { VideoErrorCause, VideoMedia } from "./composables/useVideoMedia"
230
233
  export { dragEnd, normalizeRange as normalizeTrimRange, parseClipTime, setEnd } from "./components/videoTrim"
231
234
  export type { FrameRegion, TimeRange, VideoTrimErrorCause } from "./components/videoTrim"
232
235
  export { default as LpSparkline } from "./components/LpSparkline.vue"