@codingfactory/mediables-vue 2.0.1
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.
- package/dist/PixiFrameExporter-BTU38EVl.cjs +2 -0
- package/dist/PixiFrameExporter-BTU38EVl.cjs.map +1 -0
- package/dist/PixiFrameExporter-Bb3QNWP-.js +199 -0
- package/dist/PixiFrameExporter-Bb3QNWP-.js.map +1 -0
- package/dist/adapters/MediablesAdapter.d.ts +19 -0
- package/dist/adapters/SpatieAdapter.d.ts +18 -0
- package/dist/adapters/index.d.ts +18 -0
- package/dist/components/AdminMediaBrowser.vue.d.ts +11 -0
- package/dist/components/AdminMediaBrowserExample.vue.d.ts +2 -0
- package/dist/components/AdminMediaBulkActionsToolbar.vue.d.ts +2 -0
- package/dist/components/AdminMediaGrid.vue.d.ts +17 -0
- package/dist/components/AdminMediaListItem.vue.d.ts +10 -0
- package/dist/components/AdminMediaManager.vue.d.ts +25 -0
- package/dist/components/AdminMediaUploader.vue.d.ts +11 -0
- package/dist/components/AlbumBrowser.vue.d.ts +17 -0
- package/dist/components/AlbumManager.vue.d.ts +16 -0
- package/dist/components/AlbumMediaGrid.vue.d.ts +28 -0
- package/dist/components/AlbumTree.vue.d.ts +35 -0
- package/dist/components/BulkActionsToolbar.vue.d.ts +2 -0
- package/dist/components/ConversionProgressIndicator.vue.d.ts +2 -0
- package/dist/components/Editor.vue.d.ts +29 -0
- package/dist/components/ExampleGridAndList.vue.d.ts +2 -0
- package/dist/components/ExampleUsage.vue.d.ts +2 -0
- package/dist/components/Grid.vue.d.ts +2 -0
- package/dist/components/ImageEditor/ImageEditor.vue.d.ts +3 -0
- package/dist/components/ImageEditorModal.vue.d.ts +16 -0
- package/dist/components/ImagePicker.vue.d.ts +32 -0
- package/dist/components/ImageUploadZone.vue.d.ts +7 -0
- package/dist/components/Item.vue.d.ts +2 -0
- package/dist/components/Library.vue.d.ts +2 -0
- package/dist/components/ManagedMediaGallery.vue.d.ts +12 -0
- package/dist/components/MediaAttacher.vue.d.ts +21 -0
- package/dist/components/MediaBrowser.vue.d.ts +2 -0
- package/dist/components/MediaCard.vue.d.ts +2 -0
- package/dist/components/MediaFilters.vue.d.ts +2 -0
- package/dist/components/MediaGrid.vue.d.ts +31 -0
- package/dist/components/MediaInfoEditor.vue.d.ts +7 -0
- package/dist/components/MediaManager.vue.d.ts +2 -0
- package/dist/components/MediaUploadWithProgress.vue.d.ts +2 -0
- package/dist/components/MediaUploader.vue.d.ts +2 -0
- package/dist/components/MediaWorkspace.vue.d.ts +19 -0
- package/dist/components/Modal.vue.d.ts +2 -0
- package/dist/components/ModelMediaManager.vue.d.ts +80 -0
- package/dist/components/Pagination.vue.d.ts +2 -0
- package/dist/components/Search.vue.d.ts +2 -0
- package/dist/components/VideoEditorSimple.vue.d.ts +2 -0
- package/dist/components/VideoExportPanel.vue.d.ts +2 -0
- package/dist/components/VideoTimeline.vue.d.ts +2 -0
- package/dist/components/VideoTimelineSimple.vue.d.ts +2 -0
- package/dist/components/VideoToolsPanel.vue.d.ts +2 -0
- package/dist/components/albums/AlbumTreeNode.vue.d.ts +23 -0
- package/dist/components/attachment/MediaAttachment.vue.d.ts +23 -0
- package/dist/components/attachment/index.d.ts +4 -0
- package/dist/components/collection/MediaCollection.vue.d.ts +27 -0
- package/dist/components/collection/MediaCollectionDropzone.vue.d.ts +18 -0
- package/dist/components/collection/MediaCollectionItem.vue.d.ts +2 -0
- package/dist/components/collection/index.d.ts +6 -0
- package/dist/components/form/MediaHiddenFields.vue.d.ts +2 -0
- package/dist/components/form/index.d.ts +4 -0
- package/dist/components/image/ImageEditor.vue.d.ts +2 -0
- package/dist/components/image/ImageItem.vue.d.ts +2 -0
- package/dist/components/renderless/MediaAttachmentProvider.vue.d.ts +12 -0
- package/dist/components/renderless/MediaCollectionProvider.vue.d.ts +12 -0
- package/dist/components/renderless/index.d.ts +7 -0
- package/dist/components/timeline/TimeRuler.vue.d.ts +2 -0
- package/dist/components/timeline/VideoTrack.vue.d.ts +2 -0
- package/dist/components/tools/VideoFiltersPanel.vue.d.ts +7 -0
- package/dist/components/tools/VideoTextPanel.vue.d.ts +2 -0
- package/dist/components/video/AudioTrackManager.vue.d.ts +2 -0
- package/dist/components/video/EditorControls.vue.d.ts +2 -0
- package/dist/components/video/ExportPanel.vue.d.ts +2 -0
- package/dist/components/video/FilterSelector.vue.d.ts +2 -0
- package/dist/components/video/LiveStreamManager.vue.d.ts +2 -0
- package/dist/components/video/StreamCredentials.vue.d.ts +2 -0
- package/dist/components/video/StreamStatus.vue.d.ts +2 -0
- package/dist/components/video/TextOverlayPanel.vue.d.ts +2 -0
- package/dist/components/video/ThumbnailPicker.vue.d.ts +2 -0
- package/dist/components/video/TimelineClip.vue.d.ts +2 -0
- package/dist/components/video/TimelineControls.vue.d.ts +2 -0
- package/dist/components/video/TransitionSelector.vue.d.ts +2 -0
- package/dist/components/video/VideoControls.vue.d.ts +2 -0
- package/dist/components/video/VideoEditor.vue.d.ts +8 -0
- package/dist/components/video/VideoEditorDialog.vue.d.ts +12 -0
- package/dist/components/video/VideoFilterCarousel.vue.d.ts +2 -0
- package/dist/components/video/VideoFilterPreview.vue.d.ts +18 -0
- package/dist/components/video/VideoPlayer.vue.d.ts +2 -0
- package/dist/components/video/VideoPreview.vue.d.ts +3 -0
- package/dist/components/video/VideoPreviewCSS.vue.d.ts +2 -0
- package/dist/components/video/VideoPreviewEngine.vue.d.ts +3 -0
- package/dist/components/video/VideoTimeline.vue.d.ts +2 -0
- package/dist/components/video/VideoUploadProgress.vue.d.ts +2 -0
- package/dist/components/video/VideoUploader.vue.d.ts +29 -0
- package/dist/components/video/index.d.ts +19 -0
- package/dist/composables/useAccordion.d.ts +138 -0
- package/dist/composables/useAlbumDragDrop.d.ts +24 -0
- package/dist/composables/useAlbums.d.ts +17 -0
- package/dist/composables/useFloatingPills.d.ts +111 -0
- package/dist/composables/useHaptic.d.ts +10 -0
- package/dist/composables/useImageEditorModal.d.ts +277 -0
- package/dist/composables/useLiveStream.d.ts +66 -0
- package/dist/composables/useMediaAttachment.d.ts +105 -0
- package/dist/composables/useMediaCollection.d.ts +122 -0
- package/dist/composables/useMediaConversionProgress.d.ts +31 -0
- package/dist/composables/useMediaDragSort.d.ts +56 -0
- package/dist/composables/useMediaSelection.d.ts +27 -0
- package/dist/composables/useMediaUploadQueue.d.ts +61 -0
- package/dist/composables/useMediaValidation.d.ts +59 -0
- package/dist/composables/useRadialMenu.d.ts +116 -0
- package/dist/composables/useSanctumClient.d.ts +31 -0
- package/dist/composables/useTheme.d.ts +7 -0
- package/dist/composables/useToast.d.ts +25 -0
- package/dist/composables/useVideoEditor.d.ts +127 -0
- package/dist/composables/useVideoFilters.d.ts +176 -0
- package/dist/composables/useVideoPlayer.d.ts +50 -0
- package/dist/composables/useVideoUpload.d.ts +134 -0
- package/dist/filters/controlMapping.d.ts +31 -0
- package/dist/filters/css-registry.d.ts +83 -0
- package/dist/filters/definitions/adjustment.d.ts +2 -0
- package/dist/filters/definitions/adjustmentAdvanced.d.ts +2 -0
- package/dist/filters/definitions/advancedBloom.d.ts +2 -0
- package/dist/filters/definitions/alpha.d.ts +2 -0
- package/dist/filters/definitions/ascii.d.ts +2 -0
- package/dist/filters/definitions/backdropBlur.d.ts +2 -0
- package/dist/filters/definitions/bevel.d.ts +2 -0
- package/dist/filters/definitions/bloom.d.ts +2 -0
- package/dist/filters/definitions/blur.d.ts +2 -0
- package/dist/filters/definitions/bulgePinch.d.ts +2 -0
- package/dist/filters/definitions/colorGradient.d.ts +2 -0
- package/dist/filters/definitions/colorMap.d.ts +2 -0
- package/dist/filters/definitions/colorMatrix.d.ts +2 -0
- package/dist/filters/definitions/colorOverlay.d.ts +2 -0
- package/dist/filters/definitions/colorReplace.d.ts +2 -0
- package/dist/filters/definitions/convolution.d.ts +2 -0
- package/dist/filters/definitions/crossHatch.d.ts +2 -0
- package/dist/filters/definitions/crt.d.ts +2 -0
- package/dist/filters/definitions/displacement.d.ts +2 -0
- package/dist/filters/definitions/dot.d.ts +2 -0
- package/dist/filters/definitions/dropShadow.d.ts +2 -0
- package/dist/filters/definitions/emboss.d.ts +2 -0
- package/dist/filters/definitions/glitch.d.ts +2 -0
- package/dist/filters/definitions/glow.d.ts +2 -0
- package/dist/filters/definitions/godray.d.ts +2 -0
- package/dist/filters/definitions/grayscale.d.ts +2 -0
- package/dist/filters/definitions/hslAdjustment.d.ts +2 -0
- package/dist/filters/definitions/kawaseBlur.d.ts +2 -0
- package/dist/filters/definitions/lightmap.d.ts +2 -0
- package/dist/filters/definitions/motionBlur.d.ts +2 -0
- package/dist/filters/definitions/multiColorReplace.d.ts +2 -0
- package/dist/filters/definitions/noise.d.ts +2 -0
- package/dist/filters/definitions/oldFilm.d.ts +2 -0
- package/dist/filters/definitions/outline.d.ts +2 -0
- package/dist/filters/definitions/pixelate.d.ts +2 -0
- package/dist/filters/definitions/radialBlur.d.ts +2 -0
- package/dist/filters/definitions/reflection.d.ts +2 -0
- package/dist/filters/definitions/rgbSplit.d.ts +2 -0
- package/dist/filters/definitions/shockwave.d.ts +2 -0
- package/dist/filters/definitions/simplexNoise.d.ts +2 -0
- package/dist/filters/definitions/tiltShift.d.ts +2 -0
- package/dist/filters/definitions/twist.d.ts +2 -0
- package/dist/filters/definitions/vignette.d.ts +2 -0
- package/dist/filters/definitions/zoomBlur.d.ts +2 -0
- package/dist/filters/factory.d.ts +38 -0
- package/dist/filters/filters/controlMapping.d.ts +31 -0
- package/dist/filters/filters/definitions/adjustment.d.ts +2 -0
- package/dist/filters/filters/definitions/adjustmentAdvanced.d.ts +2 -0
- package/dist/filters/filters/definitions/advancedBloom.d.ts +2 -0
- package/dist/filters/filters/definitions/alpha.d.ts +2 -0
- package/dist/filters/filters/definitions/ascii.d.ts +2 -0
- package/dist/filters/filters/definitions/backdropBlur.d.ts +2 -0
- package/dist/filters/filters/definitions/bevel.d.ts +2 -0
- package/dist/filters/filters/definitions/bloom.d.ts +2 -0
- package/dist/filters/filters/definitions/blur.d.ts +2 -0
- package/dist/filters/filters/definitions/bulgePinch.d.ts +2 -0
- package/dist/filters/filters/definitions/colorGradient.d.ts +2 -0
- package/dist/filters/filters/definitions/colorMap.d.ts +2 -0
- package/dist/filters/filters/definitions/colorMatrix.d.ts +2 -0
- package/dist/filters/filters/definitions/colorOverlay.d.ts +2 -0
- package/dist/filters/filters/definitions/colorReplace.d.ts +2 -0
- package/dist/filters/filters/definitions/convolution.d.ts +2 -0
- package/dist/filters/filters/definitions/crossHatch.d.ts +2 -0
- package/dist/filters/filters/definitions/crt.d.ts +2 -0
- package/dist/filters/filters/definitions/displacement.d.ts +2 -0
- package/dist/filters/filters/definitions/dot.d.ts +2 -0
- package/dist/filters/filters/definitions/dropShadow.d.ts +2 -0
- package/dist/filters/filters/definitions/emboss.d.ts +2 -0
- package/dist/filters/filters/definitions/glitch.d.ts +2 -0
- package/dist/filters/filters/definitions/glow.d.ts +2 -0
- package/dist/filters/filters/definitions/godray.d.ts +2 -0
- package/dist/filters/filters/definitions/grayscale.d.ts +2 -0
- package/dist/filters/filters/definitions/hslAdjustment.d.ts +2 -0
- package/dist/filters/filters/definitions/kawaseBlur.d.ts +2 -0
- package/dist/filters/filters/definitions/lightmap.d.ts +2 -0
- package/dist/filters/filters/definitions/motionBlur.d.ts +2 -0
- package/dist/filters/filters/definitions/multiColorReplace.d.ts +2 -0
- package/dist/filters/filters/definitions/noise.d.ts +2 -0
- package/dist/filters/filters/definitions/oldFilm.d.ts +2 -0
- package/dist/filters/filters/definitions/outline.d.ts +2 -0
- package/dist/filters/filters/definitions/pixelate.d.ts +2 -0
- package/dist/filters/filters/definitions/radialBlur.d.ts +2 -0
- package/dist/filters/filters/definitions/reflection.d.ts +2 -0
- package/dist/filters/filters/definitions/rgbSplit.d.ts +2 -0
- package/dist/filters/filters/definitions/shockwave.d.ts +2 -0
- package/dist/filters/filters/definitions/simplexNoise.d.ts +2 -0
- package/dist/filters/filters/definitions/tiltShift.d.ts +2 -0
- package/dist/filters/filters/definitions/twist.d.ts +2 -0
- package/dist/filters/filters/definitions/vignette.d.ts +2 -0
- package/dist/filters/filters/definitions/zoomBlur.d.ts +2 -0
- package/dist/filters/filters/factory.d.ts +36 -0
- package/dist/filters/filters/index.d.ts +93 -0
- package/dist/filters/filters/registry.d.ts +89 -0
- package/dist/filters/index.d.ts +93 -0
- package/dist/filters/registry.d.ts +89 -0
- package/dist/filters/video-compatible.d.ts +77 -0
- package/dist/filters/video-css-filters.d.ts +153 -0
- package/dist/index-6yUGA--H.cjs +42 -0
- package/dist/index-6yUGA--H.cjs.map +1 -0
- package/dist/index-CcGWfCCV.js +7799 -0
- package/dist/index-CcGWfCCV.js.map +1 -0
- package/dist/index-DTUgsw7J.cjs +76 -0
- package/dist/index-DTUgsw7J.cjs.map +1 -0
- package/dist/index-VrUG0lmk.js +28655 -0
- package/dist/index-VrUG0lmk.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/js/workers/material-color-extractor.js +215 -0
- package/dist/mediables-vanilla.cjs +2 -0
- package/dist/mediables-vanilla.cjs.map +1 -0
- package/dist/mediables-vanilla.mjs +12 -0
- package/dist/mediables-vanilla.mjs.map +1 -0
- package/dist/mediables-vue.cjs +2 -0
- package/dist/mediables-vue.cjs.map +1 -0
- package/dist/mediables-vue.mjs +67 -0
- package/dist/mediables-vue.mjs.map +1 -0
- package/dist/render-page/assets/index-hBfvGPpt.js +48933 -0
- package/dist/render-page/index.html +18 -0
- package/dist/services/VideoJobClient.d.ts +79 -0
- package/dist/stores/albumStore.d.ts +4 -0
- package/dist/stores/mediaVariantStore.d.ts +1 -0
- package/dist/stores/useAdminMediaStore.d.ts +16 -0
- package/dist/stores/useMediaStore.d.ts +25 -0
- package/dist/stores/useVideoStore.d.ts +21 -0
- package/dist/stores/variantPollStore.d.ts +5 -0
- package/dist/stores/video.d.ts +1 -0
- package/dist/style.css +1 -0
- package/dist/types/adapter.d.ts +181 -0
- package/dist/types/album.d.ts +28 -0
- package/dist/types/api.d.ts +88 -0
- package/dist/types/collection.d.ts +306 -0
- package/dist/types/editor.d.ts +172 -0
- package/dist/types/image.d.ts +210 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/media.d.ts +107 -0
- package/dist/types/types/address.d.ts +66 -0
- package/dist/types/types/admin/intelligent-po.d.ts +47 -0
- package/dist/types/types/admin/products/index.d.ts +17 -0
- package/dist/types/types/admin/purchase-order.d.ts +50 -0
- package/dist/types/types/admin/receipt.d.ts +86 -0
- package/dist/types/types/admin/vendor.d.ts +61 -0
- package/dist/types/types/ai.d.ts +63 -0
- package/dist/types/types/aiActions.d.ts +42 -0
- package/dist/types/types/aiDesigner.d.ts +77 -0
- package/dist/types/types/api-errors.d.ts +6 -0
- package/dist/types/types/api.d.ts +109 -0
- package/dist/types/types/bundle.d.ts +131 -0
- package/dist/types/types/bundles/analytics.d.ts +64 -0
- package/dist/types/types/bundles.d.ts +108 -0
- package/dist/types/types/cart.d.ts +81 -0
- package/dist/types/types/checkout.d.ts +40 -0
- package/dist/types/types/component-config.d.ts +26 -0
- package/dist/types/types/components.d.ts +32 -0
- package/dist/types/types/content.d.ts +138 -0
- package/dist/types/types/coupon.d.ts +32 -0
- package/dist/types/types/customer-product-history.d.ts +210 -0
- package/dist/types/types/drag-contracts.d.ts +40 -0
- package/dist/types/types/drag-drop.d.ts +19 -0
- package/dist/types/types/editor.d.ts +127 -0
- package/dist/types/types/errors.d.ts +36 -0
- package/dist/types/types/feedback.d.ts +122 -0
- package/dist/types/types/image.d.ts +210 -0
- package/dist/types/types/index.d.ts +62 -0
- package/dist/types/types/instagram.d.ts +86 -0
- package/dist/types/types/ionic-components.d.ts +152 -0
- package/dist/types/types/layout.d.ts +127 -0
- package/dist/types/types/media-gateway.d.ts +34 -0
- package/dist/types/types/media.d.ts +178 -0
- package/dist/types/types/notifications.d.ts +123 -0
- package/dist/types/types/order-management.d.ts +245 -0
- package/dist/types/types/pageBuilder/block.d.ts +34 -0
- package/dist/types/types/pageBuilder/blocks.d.ts +82 -0
- package/dist/types/types/pageBuilder/cache.d.ts +33 -0
- package/dist/types/types/pageBuilder/editor.d.ts +15 -0
- package/dist/types/types/pageBuilder/field.d.ts +11 -0
- package/dist/types/types/pageBuilder/index.d.ts +24 -0
- package/dist/types/types/pageBuilder/revisions.d.ts +40 -0
- package/dist/types/types/pageBuilder/templates.d.ts +62 -0
- package/dist/types/types/pattern.d.ts +40 -0
- package/dist/types/types/payment.d.ts +21 -0
- package/dist/types/types/payments.d.ts +10 -0
- package/dist/types/types/pipeline.d.ts +12 -0
- package/dist/types/types/pixi/filter-args.d.ts +274 -0
- package/dist/types/types/pixi/filters-extended.d.ts +157 -0
- package/dist/types/types/pixi/filters.d.ts +38 -0
- package/dist/types/types/preview.d.ts +36 -0
- package/dist/types/types/pricing.d.ts +31 -0
- package/dist/types/types/quickbooks.d.ts +43 -0
- package/dist/types/types/receipt.d.ts +121 -0
- package/dist/types/types/rewards.d.ts +110 -0
- package/dist/types/types/saved-cart.d.ts +51 -0
- package/dist/types/types/settings.d.ts +63 -0
- package/dist/types/types/shipment.d.ts +62 -0
- package/dist/types/types/shipping.d.ts +98 -0
- package/dist/types/types/sidebar-variations.d.ts +226 -0
- package/dist/types/types/slots.d.ts +2 -0
- package/dist/types/types/specification-types.d.ts +70 -0
- package/dist/types/types/specifications.d.ts +163 -0
- package/dist/types/types/store.d.ts +64 -0
- package/dist/types/types/template.d.ts +8 -0
- package/dist/types/types/user.d.ts +47 -0
- package/dist/types/types/variant-groups.d.ts +158 -0
- package/dist/types/types/wishlist.d.ts +73 -0
- package/dist/types/types/workflow-wizard.d.ts +12 -0
- package/dist/types/video.d.ts +449 -0
- package/dist/utils/category-tree-constants.d.ts +42 -0
- package/dist/utils/cookies.d.ts +3 -0
- package/dist/utils/crypto-polyfill.d.ts +4 -0
- package/dist/utils/datetime.d.ts +43 -0
- package/dist/utils/debounce.d.ts +10 -0
- package/dist/utils/debugConsole.d.ts +69 -0
- package/dist/utils/editor/argHelpers.d.ts +6 -0
- package/dist/utils/formatters.d.ts +105 -0
- package/dist/utils/isPresignedAwsUrl.d.ts +67 -0
- package/dist/utils/media-helpers.d.ts +34 -0
- package/dist/utils/normalisePricing.d.ts +11 -0
- package/dist/utils/recipe-generator.d.ts +34 -0
- package/dist/utils/string.d.ts +29 -0
- package/dist/utils/unwrapApiResponse.d.ts +5 -0
- package/dist/utils/uuid.d.ts +30 -0
- package/dist/utils/validators.d.ts +28 -0
- package/dist/utils/video-export.d.ts +60 -0
- package/dist/v3-ionic-1-demo.html +440 -0
- package/dist/video-engine/VideoEngine.d.ts +267 -0
- package/dist/video-engine/adapters/AudioManager.d.ts +106 -0
- package/dist/video-engine/adapters/CSSFilterAdapter.d.ts +106 -0
- package/dist/video-engine/adapters/ExportManager.d.ts +88 -0
- package/dist/video-engine/adapters/FilterBridge.d.ts +96 -0
- package/dist/video-engine/adapters/MediablesCompositionAdapter.d.ts +56 -0
- package/dist/video-engine/adapters/PixiFrameExporter.d.ts +52 -0
- package/dist/video-engine/adapters/RenderQueue.d.ts +119 -0
- package/dist/video-engine/adapters/TextOverlayManager.d.ts +93 -0
- package/dist/video-engine/adapters/TimelineAdapter.d.ts +58 -0
- package/dist/video-engine/adapters/TransitionManager.d.ts +76 -0
- package/dist/video-engine/adapters/WebCodecsExport.d.ts +36 -0
- package/dist/video-engine/compositions/examples/example.d.ts +2 -0
- package/dist/video-engine/filters/CSSFilterSystem.d.ts +213 -0
- package/dist/video-engine/index.d.ts +14 -0
- package/dist/video-engine/presets/ExportPresets.d.ts +70 -0
- package/dist/video-engine/types/index.d.ts +96 -0
- package/dist/video-engine/types.d.ts +1 -0
- package/dist/video-engine/utils/EventEmitter.d.ts +12 -0
- package/dist/video-engine/utils/MediaSourceResolver.d.ts +9 -0
- package/dist/video-engine/utils/error-reporter.d.ts +159 -0
- package/dist/video-engine/utils/keyboard-shortcuts.d.ts +120 -0
- package/dist/video-engine/utils/pixi-video-fallback.d.ts +2 -0
- package/docs/video-subsystem/README.md +490 -0
- package/docs/video-subsystem/api-reference.md +747 -0
- package/docs/video-subsystem/component-examples.md +1477 -0
- package/docs/video-subsystem/integration-guide.md +1021 -0
- package/package.json +102 -0
|
@@ -0,0 +1,1477 @@
|
|
|
1
|
+
# Video Component Usage Examples
|
|
2
|
+
|
|
3
|
+
This guide provides practical examples of using the Mediables video components in your Vue application.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Basic Examples](#basic-examples)
|
|
8
|
+
2. [Advanced Examples](#advanced-examples)
|
|
9
|
+
3. [Integration Patterns](#integration-patterns)
|
|
10
|
+
4. [Mobile Optimizations](#mobile-optimizations)
|
|
11
|
+
5. [Error Handling](#error-handling)
|
|
12
|
+
6. [Performance Tips](#performance-tips)
|
|
13
|
+
|
|
14
|
+
## Basic Examples
|
|
15
|
+
|
|
16
|
+
### Simple Video Upload
|
|
17
|
+
|
|
18
|
+
```vue
|
|
19
|
+
<template>
|
|
20
|
+
<div class="video-upload-container">
|
|
21
|
+
<VideoUploader
|
|
22
|
+
@uploaded="onVideoUploaded"
|
|
23
|
+
@error="onUploadError"
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<div v-if="uploadedVideo" class="mt-4">
|
|
27
|
+
<p>Video uploaded successfully!</p>
|
|
28
|
+
<VideoPlayer :media="uploadedVideo" />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { ref } from 'vue'
|
|
35
|
+
import { VideoUploader, VideoPlayer } from '@mediables/vue'
|
|
36
|
+
|
|
37
|
+
const uploadedVideo = ref(null)
|
|
38
|
+
|
|
39
|
+
function onVideoUploaded(media) {
|
|
40
|
+
uploadedVideo.value = media
|
|
41
|
+
console.log('Video ready:', media.uuid)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function onUploadError(error) {
|
|
45
|
+
console.error('Upload failed:', error)
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Basic Video Player
|
|
51
|
+
|
|
52
|
+
```vue
|
|
53
|
+
<template>
|
|
54
|
+
<VideoPlayer
|
|
55
|
+
:playback-url="videoUrl"
|
|
56
|
+
:controls="true"
|
|
57
|
+
:autoplay="false"
|
|
58
|
+
:muted="false"
|
|
59
|
+
/>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<script setup lang="ts">
|
|
63
|
+
import { VideoPlayer } from '@mediables/vue'
|
|
64
|
+
|
|
65
|
+
const videoUrl = 'https://your-cdn.b-cdn.net/your-library-id/play_video-guid-abc123/playlist.m3u8'
|
|
66
|
+
</script>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Simple Video Editor
|
|
70
|
+
|
|
71
|
+
```vue
|
|
72
|
+
<template>
|
|
73
|
+
<VideoEditor
|
|
74
|
+
:media-uuid="videoId"
|
|
75
|
+
@export="handleExport"
|
|
76
|
+
/>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<script setup lang="ts">
|
|
80
|
+
import { VideoEditor } from '@mediables/vue'
|
|
81
|
+
|
|
82
|
+
const videoId = 'your-video-uuid-here'
|
|
83
|
+
|
|
84
|
+
async function handleExport(recipe) {
|
|
85
|
+
console.log('Exporting with recipe:', recipe)
|
|
86
|
+
// Send recipe to backend for processing
|
|
87
|
+
}
|
|
88
|
+
</script>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Advanced Examples
|
|
92
|
+
|
|
93
|
+
### Upload with Progress and Validation
|
|
94
|
+
|
|
95
|
+
```vue
|
|
96
|
+
<template>
|
|
97
|
+
<div class="advanced-upload">
|
|
98
|
+
<VideoUploader
|
|
99
|
+
:multiple="true"
|
|
100
|
+
:max-size="maxFileSize"
|
|
101
|
+
:accepted-formats="acceptedFormats"
|
|
102
|
+
:collection="'project-videos'"
|
|
103
|
+
@before-upload="validateUpload"
|
|
104
|
+
@progress="updateProgress"
|
|
105
|
+
@uploaded="handleUploaded"
|
|
106
|
+
@error="handleError"
|
|
107
|
+
>
|
|
108
|
+
<template #trigger>
|
|
109
|
+
<div class="custom-upload-button">
|
|
110
|
+
<IonIcon :icon="cloudUploadOutline" />
|
|
111
|
+
<span>Drop videos here or click to browse</span>
|
|
112
|
+
<small>MP4, MOV, WebM up to 2GB</small>
|
|
113
|
+
</div>
|
|
114
|
+
</template>
|
|
115
|
+
</VideoUploader>
|
|
116
|
+
|
|
117
|
+
<!-- Progress indicators -->
|
|
118
|
+
<div v-if="uploads.length > 0" class="upload-queue mt-4">
|
|
119
|
+
<div v-for="upload in uploads" :key="upload.id" class="upload-item">
|
|
120
|
+
<div class="flex items-center justify-between">
|
|
121
|
+
<span>{{ upload.filename }}</span>
|
|
122
|
+
<span>{{ upload.progress }}%</span>
|
|
123
|
+
</div>
|
|
124
|
+
<IonProgressBar :value="upload.progress / 100" />
|
|
125
|
+
<button v-if="!upload.completed" @click="cancelUpload(upload.id)">
|
|
126
|
+
Cancel
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
132
|
+
|
|
133
|
+
<script setup lang="ts">
|
|
134
|
+
import { ref, reactive } from 'vue'
|
|
135
|
+
import { IonIcon, IonProgressBar, useIonToast } from '@ionic/vue'
|
|
136
|
+
import { cloudUploadOutline } from 'ionicons/icons'
|
|
137
|
+
import { VideoUploader } from '@mediables/vue'
|
|
138
|
+
|
|
139
|
+
const [presentToast] = useIonToast()
|
|
140
|
+
|
|
141
|
+
const maxFileSize = 2 * 1024 * 1024 * 1024 // 2GB
|
|
142
|
+
const acceptedFormats = ['video/mp4', 'video/quicktime', 'video/webm']
|
|
143
|
+
const uploads = reactive([])
|
|
144
|
+
|
|
145
|
+
function validateUpload(file) {
|
|
146
|
+
// Custom validation
|
|
147
|
+
if (file.name.includes('temp')) {
|
|
148
|
+
presentToast({
|
|
149
|
+
message: 'Temporary files not allowed',
|
|
150
|
+
color: 'warning'
|
|
151
|
+
})
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check duration (if available)
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
const video = document.createElement('video')
|
|
158
|
+
video.preload = 'metadata'
|
|
159
|
+
|
|
160
|
+
video.onloadedmetadata = () => {
|
|
161
|
+
if (video.duration > 600) { // 10 minutes max
|
|
162
|
+
presentToast({
|
|
163
|
+
message: 'Video must be less than 10 minutes',
|
|
164
|
+
color: 'warning'
|
|
165
|
+
})
|
|
166
|
+
resolve(false)
|
|
167
|
+
} else {
|
|
168
|
+
resolve(true)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
video.src = URL.createObjectURL(file)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function updateProgress(data) {
|
|
177
|
+
const upload = uploads.find(u => u.id === data.uploadId)
|
|
178
|
+
if (upload) {
|
|
179
|
+
upload.progress = data.progress
|
|
180
|
+
} else {
|
|
181
|
+
uploads.push({
|
|
182
|
+
id: data.uploadId,
|
|
183
|
+
filename: data.filename,
|
|
184
|
+
progress: data.progress,
|
|
185
|
+
completed: false
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function handleUploaded(media) {
|
|
191
|
+
const upload = uploads.find(u => u.id === media.upload_id)
|
|
192
|
+
if (upload) {
|
|
193
|
+
upload.completed = true
|
|
194
|
+
upload.progress = 100
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function handleError(error) {
|
|
199
|
+
presentToast({
|
|
200
|
+
message: `Upload failed: ${error.message}`,
|
|
201
|
+
color: 'danger',
|
|
202
|
+
duration: 5000
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function cancelUpload(uploadId) {
|
|
207
|
+
// Implementation depends on your upload mechanism
|
|
208
|
+
const index = uploads.findIndex(u => u.id === uploadId)
|
|
209
|
+
if (index !== -1) {
|
|
210
|
+
uploads.splice(index, 1)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
</script>
|
|
214
|
+
|
|
215
|
+
<style scoped>
|
|
216
|
+
.custom-upload-button {
|
|
217
|
+
border: 2px dashed var(--ion-color-medium);
|
|
218
|
+
border-radius: 8px;
|
|
219
|
+
padding: 3rem;
|
|
220
|
+
text-align: center;
|
|
221
|
+
cursor: pointer;
|
|
222
|
+
transition: all 0.3s;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.custom-upload-button:hover {
|
|
226
|
+
border-color: var(--ion-color-primary);
|
|
227
|
+
background: var(--ion-color-primary-tint);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.upload-item {
|
|
231
|
+
padding: 1rem;
|
|
232
|
+
border: 1px solid var(--ion-color-light);
|
|
233
|
+
border-radius: 4px;
|
|
234
|
+
margin-bottom: 0.5rem;
|
|
235
|
+
}
|
|
236
|
+
</style>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Player with Custom Controls
|
|
240
|
+
|
|
241
|
+
```vue
|
|
242
|
+
<template>
|
|
243
|
+
<div class="custom-player">
|
|
244
|
+
<VideoPlayer
|
|
245
|
+
ref="playerRef"
|
|
246
|
+
:media="media"
|
|
247
|
+
:controls="false"
|
|
248
|
+
@play="isPlaying = true"
|
|
249
|
+
@pause="isPlaying = false"
|
|
250
|
+
@timeupdate="updateTime"
|
|
251
|
+
@ended="handleEnded"
|
|
252
|
+
/>
|
|
253
|
+
|
|
254
|
+
<!-- Custom control bar -->
|
|
255
|
+
<div class="controls">
|
|
256
|
+
<button @click="togglePlay">
|
|
257
|
+
<IonIcon :icon="isPlaying ? pauseOutline : playOutline" />
|
|
258
|
+
</button>
|
|
259
|
+
|
|
260
|
+
<div class="time-display">
|
|
261
|
+
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<input
|
|
265
|
+
type="range"
|
|
266
|
+
:value="currentTime"
|
|
267
|
+
:max="duration"
|
|
268
|
+
@input="seek($event.target.value)"
|
|
269
|
+
class="seek-bar"
|
|
270
|
+
/>
|
|
271
|
+
|
|
272
|
+
<button @click="toggleMute">
|
|
273
|
+
<IonIcon :icon="isMuted ? volumeMuteOutline : volumeHighOutline" />
|
|
274
|
+
</button>
|
|
275
|
+
|
|
276
|
+
<button @click="toggleFullscreen">
|
|
277
|
+
<IonIcon :icon="expandOutline" />
|
|
278
|
+
</button>
|
|
279
|
+
|
|
280
|
+
<select @change="changeQuality($event.target.value)">
|
|
281
|
+
<option value="auto">Auto</option>
|
|
282
|
+
<option value="1080p">1080p</option>
|
|
283
|
+
<option value="720p">720p</option>
|
|
284
|
+
<option value="480p">480p</option>
|
|
285
|
+
</select>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<!-- Overlay for loading/buffering -->
|
|
289
|
+
<div v-if="isBuffering" class="overlay">
|
|
290
|
+
<IonSpinner />
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
</template>
|
|
294
|
+
|
|
295
|
+
<script setup lang="ts">
|
|
296
|
+
import { ref, computed } from 'vue'
|
|
297
|
+
import { IonIcon, IonSpinner } from '@ionic/vue'
|
|
298
|
+
import {
|
|
299
|
+
playOutline,
|
|
300
|
+
pauseOutline,
|
|
301
|
+
volumeHighOutline,
|
|
302
|
+
volumeMuteOutline,
|
|
303
|
+
expandOutline
|
|
304
|
+
} from 'ionicons/icons'
|
|
305
|
+
import { VideoPlayer } from '@mediables/vue'
|
|
306
|
+
|
|
307
|
+
const props = defineProps({
|
|
308
|
+
media: Object
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
const playerRef = ref()
|
|
312
|
+
const isPlaying = ref(false)
|
|
313
|
+
const isMuted = ref(false)
|
|
314
|
+
const isBuffering = ref(false)
|
|
315
|
+
const currentTime = ref(0)
|
|
316
|
+
const duration = ref(0)
|
|
317
|
+
|
|
318
|
+
function togglePlay() {
|
|
319
|
+
if (isPlaying.value) {
|
|
320
|
+
playerRef.value.pause()
|
|
321
|
+
} else {
|
|
322
|
+
playerRef.value.play()
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function seek(time) {
|
|
327
|
+
playerRef.value.seek(Number(time))
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function toggleMute() {
|
|
331
|
+
isMuted.value = !isMuted.value
|
|
332
|
+
playerRef.value.setMuted(isMuted.value)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function toggleFullscreen() {
|
|
336
|
+
playerRef.value.requestFullscreen()
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function changeQuality(quality) {
|
|
340
|
+
playerRef.value.setQuality(quality)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function updateTime(event) {
|
|
344
|
+
currentTime.value = event.currentTime
|
|
345
|
+
duration.value = event.duration
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function handleEnded() {
|
|
349
|
+
isPlaying.value = false
|
|
350
|
+
// Maybe show related videos or replay button
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function formatTime(seconds) {
|
|
354
|
+
const mins = Math.floor(seconds / 60)
|
|
355
|
+
const secs = Math.floor(seconds % 60)
|
|
356
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`
|
|
357
|
+
}
|
|
358
|
+
</script>
|
|
359
|
+
|
|
360
|
+
<style scoped>
|
|
361
|
+
.custom-player {
|
|
362
|
+
position: relative;
|
|
363
|
+
background: black;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.controls {
|
|
367
|
+
position: absolute;
|
|
368
|
+
bottom: 0;
|
|
369
|
+
left: 0;
|
|
370
|
+
right: 0;
|
|
371
|
+
display: flex;
|
|
372
|
+
align-items: center;
|
|
373
|
+
gap: 1rem;
|
|
374
|
+
padding: 1rem;
|
|
375
|
+
background: linear-gradient(transparent, rgba(0,0,0,0.8));
|
|
376
|
+
color: white;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.seek-bar {
|
|
380
|
+
flex: 1;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.overlay {
|
|
384
|
+
position: absolute;
|
|
385
|
+
inset: 0;
|
|
386
|
+
display: flex;
|
|
387
|
+
align-items: center;
|
|
388
|
+
justify-content: center;
|
|
389
|
+
background: rgba(0,0,0,0.5);
|
|
390
|
+
}
|
|
391
|
+
</style>
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Advanced Video Editor with Timeline
|
|
395
|
+
|
|
396
|
+
```vue
|
|
397
|
+
<template>
|
|
398
|
+
<div class="advanced-editor">
|
|
399
|
+
<VideoEditor
|
|
400
|
+
ref="editorRef"
|
|
401
|
+
:media-uuid="videoId"
|
|
402
|
+
:initial-recipe="savedRecipe"
|
|
403
|
+
@change="handleChange"
|
|
404
|
+
@export="handleExport"
|
|
405
|
+
>
|
|
406
|
+
<template #toolbar>
|
|
407
|
+
<div class="editor-toolbar">
|
|
408
|
+
<button @click="undo" :disabled="!canUndo">
|
|
409
|
+
<IonIcon :icon="arrowUndoOutline" />
|
|
410
|
+
Undo
|
|
411
|
+
</button>
|
|
412
|
+
|
|
413
|
+
<button @click="redo" :disabled="!canRedo">
|
|
414
|
+
<IonIcon :icon="arrowRedoOutline" />
|
|
415
|
+
Redo
|
|
416
|
+
</button>
|
|
417
|
+
|
|
418
|
+
<div class="separator" />
|
|
419
|
+
|
|
420
|
+
<button @click="splitAtPlayhead">
|
|
421
|
+
<IonIcon :icon="cutOutline" />
|
|
422
|
+
Split
|
|
423
|
+
</button>
|
|
424
|
+
|
|
425
|
+
<button @click="deleteSelected" :disabled="!hasSelection">
|
|
426
|
+
<IonIcon :icon="trashOutline" />
|
|
427
|
+
Delete
|
|
428
|
+
</button>
|
|
429
|
+
|
|
430
|
+
<div class="separator" />
|
|
431
|
+
|
|
432
|
+
<button @click="addTextOverlay">
|
|
433
|
+
<IonIcon :icon="textOutline" />
|
|
434
|
+
Add Text
|
|
435
|
+
</button>
|
|
436
|
+
|
|
437
|
+
<button @click="addTransition">
|
|
438
|
+
<IonIcon :icon="shuffleOutline" />
|
|
439
|
+
Transition
|
|
440
|
+
</button>
|
|
441
|
+
</div>
|
|
442
|
+
</template>
|
|
443
|
+
|
|
444
|
+
<template #sidebar>
|
|
445
|
+
<div class="editor-sidebar">
|
|
446
|
+
<!-- Filter presets -->
|
|
447
|
+
<div class="section">
|
|
448
|
+
<h3>Quick Filters</h3>
|
|
449
|
+
<div class="filter-grid">
|
|
450
|
+
<button
|
|
451
|
+
v-for="preset in filterPresets"
|
|
452
|
+
:key="preset.id"
|
|
453
|
+
@click="applyPreset(preset)"
|
|
454
|
+
class="filter-preset"
|
|
455
|
+
>
|
|
456
|
+
<img :src="preset.thumbnail" :alt="preset.name" />
|
|
457
|
+
<span>{{ preset.name }}</span>
|
|
458
|
+
</button>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<!-- Audio controls -->
|
|
463
|
+
<div class="section">
|
|
464
|
+
<h3>Audio</h3>
|
|
465
|
+
<label>
|
|
466
|
+
Volume
|
|
467
|
+
<input
|
|
468
|
+
type="range"
|
|
469
|
+
min="0"
|
|
470
|
+
max="200"
|
|
471
|
+
v-model="audioVolume"
|
|
472
|
+
@change="updateAudio"
|
|
473
|
+
/>
|
|
474
|
+
<span>{{ audioVolume }}%</span>
|
|
475
|
+
</label>
|
|
476
|
+
|
|
477
|
+
<label>
|
|
478
|
+
<input type="checkbox" v-model="removeAudio" />
|
|
479
|
+
Remove audio
|
|
480
|
+
</label>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
<!-- Export settings -->
|
|
484
|
+
<div class="section">
|
|
485
|
+
<h3>Export Settings</h3>
|
|
486
|
+
<select v-model="exportQuality">
|
|
487
|
+
<option value="2160p">4K (2160p)</option>
|
|
488
|
+
<option value="1080p">Full HD (1080p)</option>
|
|
489
|
+
<option value="720p">HD (720p)</option>
|
|
490
|
+
<option value="480p">SD (480p)</option>
|
|
491
|
+
</select>
|
|
492
|
+
|
|
493
|
+
<select v-model="exportFormat">
|
|
494
|
+
<option value="mp4">MP4 (H.264)</option>
|
|
495
|
+
<option value="webm">WebM (VP9)</option>
|
|
496
|
+
</select>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
</template>
|
|
500
|
+
</VideoEditor>
|
|
501
|
+
|
|
502
|
+
<!-- Export progress modal -->
|
|
503
|
+
<IonModal :is-open="exporting">
|
|
504
|
+
<IonHeader>
|
|
505
|
+
<IonToolbar>
|
|
506
|
+
<IonTitle>Exporting Video</IonTitle>
|
|
507
|
+
</IonToolbar>
|
|
508
|
+
</IonHeader>
|
|
509
|
+
|
|
510
|
+
<IonContent>
|
|
511
|
+
<div class="export-progress">
|
|
512
|
+
<IonProgressBar :value="exportProgress / 100" />
|
|
513
|
+
<p>{{ exportProgress }}% complete</p>
|
|
514
|
+
<p class="text-sm">{{ exportStatus }}</p>
|
|
515
|
+
</div>
|
|
516
|
+
</IonContent>
|
|
517
|
+
</IonModal>
|
|
518
|
+
</div>
|
|
519
|
+
</template>
|
|
520
|
+
|
|
521
|
+
<script setup lang="ts">
|
|
522
|
+
import { ref, reactive, computed } from 'vue'
|
|
523
|
+
import {
|
|
524
|
+
IonIcon,
|
|
525
|
+
IonModal,
|
|
526
|
+
IonHeader,
|
|
527
|
+
IonToolbar,
|
|
528
|
+
IonTitle,
|
|
529
|
+
IonContent,
|
|
530
|
+
IonProgressBar
|
|
531
|
+
} from '@ionic/vue'
|
|
532
|
+
import {
|
|
533
|
+
arrowUndoOutline,
|
|
534
|
+
arrowRedoOutline,
|
|
535
|
+
cutOutline,
|
|
536
|
+
trashOutline,
|
|
537
|
+
textOutline,
|
|
538
|
+
shuffleOutline
|
|
539
|
+
} from 'ionicons/icons'
|
|
540
|
+
import { VideoEditor } from '@mediables/vue'
|
|
541
|
+
import { useVideoEditorStore } from '@/stores/videoEditor'
|
|
542
|
+
|
|
543
|
+
const props = defineProps({
|
|
544
|
+
videoId: String,
|
|
545
|
+
savedRecipe: Object
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
const editorRef = ref()
|
|
549
|
+
const editorStore = useVideoEditorStore()
|
|
550
|
+
|
|
551
|
+
const canUndo = computed(() => editorStore.canUndo)
|
|
552
|
+
const canRedo = computed(() => editorStore.canRedo)
|
|
553
|
+
const hasSelection = computed(() => editorStore.hasSelection)
|
|
554
|
+
|
|
555
|
+
const audioVolume = ref(100)
|
|
556
|
+
const removeAudio = ref(false)
|
|
557
|
+
const exportQuality = ref('1080p')
|
|
558
|
+
const exportFormat = ref('mp4')
|
|
559
|
+
const exporting = ref(false)
|
|
560
|
+
const exportProgress = ref(0)
|
|
561
|
+
const exportStatus = ref('')
|
|
562
|
+
|
|
563
|
+
const filterPresets = [
|
|
564
|
+
{ id: 'vintage', name: 'Vintage', thumbnail: '/filters/vintage.jpg' },
|
|
565
|
+
{ id: 'cinematic', name: 'Cinematic', thumbnail: '/filters/cinematic.jpg' },
|
|
566
|
+
{ id: 'vibrant', name: 'Vibrant', thumbnail: '/filters/vibrant.jpg' },
|
|
567
|
+
{ id: 'noir', name: 'Noir', thumbnail: '/filters/noir.jpg' }
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
function undo() {
|
|
571
|
+
editorRef.value.undo()
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function redo() {
|
|
575
|
+
editorRef.value.redo()
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function splitAtPlayhead() {
|
|
579
|
+
editorRef.value.splitAtPlayhead()
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function deleteSelected() {
|
|
583
|
+
editorRef.value.deleteSelected()
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function addTextOverlay() {
|
|
587
|
+
editorRef.value.addLayer({
|
|
588
|
+
type: 'text',
|
|
589
|
+
content: 'Your text here',
|
|
590
|
+
position: { x: 50, y: 50 },
|
|
591
|
+
style: {
|
|
592
|
+
fontSize: 24,
|
|
593
|
+
color: '#ffffff',
|
|
594
|
+
fontFamily: 'Arial'
|
|
595
|
+
}
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function addTransition() {
|
|
600
|
+
editorRef.value.addTransition({
|
|
601
|
+
type: 'crossfade',
|
|
602
|
+
duration: 1
|
|
603
|
+
})
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function applyPreset(preset) {
|
|
607
|
+
const filters = getPresetFilters(preset.id)
|
|
608
|
+
editorRef.value.applyFilters(filters)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function updateAudio() {
|
|
612
|
+
editorRef.value.setAudioVolume(audioVolume.value / 100)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function handleChange(state) {
|
|
616
|
+
editorStore.updateState(state)
|
|
617
|
+
// Auto-save draft every 30 seconds
|
|
618
|
+
editorStore.saveDraft()
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function handleExport(recipe) {
|
|
622
|
+
exporting.value = true
|
|
623
|
+
exportProgress.value = 0
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
// Add export settings to recipe
|
|
627
|
+
const fullRecipe = {
|
|
628
|
+
...recipe,
|
|
629
|
+
audio: {
|
|
630
|
+
...recipe.audio,
|
|
631
|
+
volume: removeAudio.value ? 0 : audioVolume.value / 100
|
|
632
|
+
},
|
|
633
|
+
output: {
|
|
634
|
+
format: exportFormat.value,
|
|
635
|
+
quality: exportQuality.value,
|
|
636
|
+
codec: exportFormat.value === 'mp4' ? 'h264' : 'vp9'
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const response = await api.post(`/video/${props.videoId}/export`, {
|
|
641
|
+
recipe: fullRecipe
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
// Poll for progress
|
|
645
|
+
const jobId = response.data.job_id
|
|
646
|
+
const interval = setInterval(async () => {
|
|
647
|
+
const status = await api.get(`/video/exports/${jobId}`)
|
|
648
|
+
|
|
649
|
+
exportProgress.value = status.data.progress
|
|
650
|
+
exportStatus.value = status.data.status
|
|
651
|
+
|
|
652
|
+
if (status.data.status === 'completed') {
|
|
653
|
+
clearInterval(interval)
|
|
654
|
+
exporting.value = false
|
|
655
|
+
// Navigate to result
|
|
656
|
+
router.push(`/video/export/${jobId}/complete`)
|
|
657
|
+
} else if (status.data.status === 'failed') {
|
|
658
|
+
clearInterval(interval)
|
|
659
|
+
exporting.value = false
|
|
660
|
+
// Show error
|
|
661
|
+
}
|
|
662
|
+
}, 2000)
|
|
663
|
+
|
|
664
|
+
} catch (error) {
|
|
665
|
+
exporting.value = false
|
|
666
|
+
console.error('Export failed:', error)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function getPresetFilters(presetId) {
|
|
671
|
+
const presets = {
|
|
672
|
+
vintage: [
|
|
673
|
+
{ id: 'sepia', intensity: 0.3 },
|
|
674
|
+
{ id: 'vignette', intensity: 0.5 }
|
|
675
|
+
],
|
|
676
|
+
cinematic: [
|
|
677
|
+
{ id: 'contrast', params: { value: 1.2 } },
|
|
678
|
+
{ id: 'saturation', params: { value: 0.8 } }
|
|
679
|
+
],
|
|
680
|
+
vibrant: [
|
|
681
|
+
{ id: 'saturation', params: { value: 1.5 } },
|
|
682
|
+
{ id: 'brightness', params: { value: 0.1 } }
|
|
683
|
+
],
|
|
684
|
+
noir: [
|
|
685
|
+
{ id: 'grayscale', intensity: 1 },
|
|
686
|
+
{ id: 'contrast', params: { value: 1.3 } }
|
|
687
|
+
]
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return presets[presetId] || []
|
|
691
|
+
}
|
|
692
|
+
</script>
|
|
693
|
+
|
|
694
|
+
<style scoped>
|
|
695
|
+
.advanced-editor {
|
|
696
|
+
height: 100vh;
|
|
697
|
+
display: flex;
|
|
698
|
+
flex-direction: column;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.editor-toolbar {
|
|
702
|
+
display: flex;
|
|
703
|
+
align-items: center;
|
|
704
|
+
gap: 0.5rem;
|
|
705
|
+
padding: 0.5rem;
|
|
706
|
+
background: var(--ion-color-light);
|
|
707
|
+
border-bottom: 1px solid var(--ion-color-medium);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.separator {
|
|
711
|
+
width: 1px;
|
|
712
|
+
height: 24px;
|
|
713
|
+
background: var(--ion-color-medium);
|
|
714
|
+
margin: 0 0.5rem;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.editor-sidebar {
|
|
718
|
+
width: 300px;
|
|
719
|
+
padding: 1rem;
|
|
720
|
+
background: var(--ion-color-light-shade);
|
|
721
|
+
overflow-y: auto;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.section {
|
|
725
|
+
margin-bottom: 2rem;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.section h3 {
|
|
729
|
+
font-size: 1rem;
|
|
730
|
+
font-weight: 600;
|
|
731
|
+
margin-bottom: 0.5rem;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.filter-grid {
|
|
735
|
+
display: grid;
|
|
736
|
+
grid-template-columns: repeat(2, 1fr);
|
|
737
|
+
gap: 0.5rem;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.filter-preset {
|
|
741
|
+
display: flex;
|
|
742
|
+
flex-direction: column;
|
|
743
|
+
align-items: center;
|
|
744
|
+
padding: 0.5rem;
|
|
745
|
+
border: 1px solid var(--ion-color-medium);
|
|
746
|
+
border-radius: 4px;
|
|
747
|
+
cursor: pointer;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.filter-preset img {
|
|
751
|
+
width: 60px;
|
|
752
|
+
height: 60px;
|
|
753
|
+
object-fit: cover;
|
|
754
|
+
border-radius: 4px;
|
|
755
|
+
margin-bottom: 0.25rem;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.export-progress {
|
|
759
|
+
padding: 2rem;
|
|
760
|
+
text-align: center;
|
|
761
|
+
}
|
|
762
|
+
</style>
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
## Integration Patterns
|
|
766
|
+
|
|
767
|
+
### With Pinia Store
|
|
768
|
+
|
|
769
|
+
```typescript
|
|
770
|
+
// stores/video.ts
|
|
771
|
+
import { defineStore } from 'pinia'
|
|
772
|
+
import { ref, computed } from 'vue'
|
|
773
|
+
import api from '@/services/api'
|
|
774
|
+
|
|
775
|
+
export const useVideoStore = defineStore('video', () => {
|
|
776
|
+
const videos = ref([])
|
|
777
|
+
const currentVideo = ref(null)
|
|
778
|
+
const isUploading = ref(false)
|
|
779
|
+
const uploadProgress = ref(0)
|
|
780
|
+
|
|
781
|
+
const sortedVideos = computed(() => {
|
|
782
|
+
return [...videos.value].sort((a, b) =>
|
|
783
|
+
new Date(b.created_at) - new Date(a.created_at)
|
|
784
|
+
)
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
async function fetchVideos() {
|
|
788
|
+
const response = await api.get('/videos')
|
|
789
|
+
videos.value = response.data.data
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async function uploadVideo(file) {
|
|
793
|
+
isUploading.value = true
|
|
794
|
+
uploadProgress.value = 0
|
|
795
|
+
|
|
796
|
+
try {
|
|
797
|
+
// Request upload URL
|
|
798
|
+
const uploadResponse = await api.post('/video/upload', {
|
|
799
|
+
filename: file.name,
|
|
800
|
+
size: file.size,
|
|
801
|
+
mime_type: file.type
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
// Upload to provider
|
|
805
|
+
const xhr = new XMLHttpRequest()
|
|
806
|
+
|
|
807
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
808
|
+
if (e.lengthComputable) {
|
|
809
|
+
uploadProgress.value = Math.round((e.loaded / e.total) * 100)
|
|
810
|
+
}
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
return new Promise((resolve, reject) => {
|
|
814
|
+
xhr.onload = () => {
|
|
815
|
+
if (xhr.status === 200) {
|
|
816
|
+
const media = { uuid: uploadResponse.data.media_uuid }
|
|
817
|
+
videos.value.unshift(media)
|
|
818
|
+
resolve(media)
|
|
819
|
+
} else {
|
|
820
|
+
reject(new Error('Upload failed'))
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
xhr.onerror = () => reject(new Error('Upload failed'))
|
|
825
|
+
|
|
826
|
+
xhr.open('PUT', uploadResponse.data.upload_url)
|
|
827
|
+
xhr.send(file)
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
} finally {
|
|
831
|
+
isUploading.value = false
|
|
832
|
+
uploadProgress.value = 0
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function deleteVideo(uuid) {
|
|
837
|
+
await api.delete(`/videos/${uuid}`)
|
|
838
|
+
const index = videos.value.findIndex(v => v.uuid === uuid)
|
|
839
|
+
if (index !== -1) {
|
|
840
|
+
videos.value.splice(index, 1)
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return {
|
|
845
|
+
videos,
|
|
846
|
+
currentVideo,
|
|
847
|
+
isUploading,
|
|
848
|
+
uploadProgress,
|
|
849
|
+
sortedVideos,
|
|
850
|
+
fetchVideos,
|
|
851
|
+
uploadVideo,
|
|
852
|
+
deleteVideo
|
|
853
|
+
}
|
|
854
|
+
})
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
### With Vue Router
|
|
858
|
+
|
|
859
|
+
```typescript
|
|
860
|
+
// router/video-routes.ts
|
|
861
|
+
export const videoRoutes = [
|
|
862
|
+
{
|
|
863
|
+
path: '/videos',
|
|
864
|
+
component: () => import('@/views/VideoList.vue'),
|
|
865
|
+
meta: { requiresAuth: true }
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
path: '/videos/upload',
|
|
869
|
+
component: () => import('@/views/VideoUpload.vue'),
|
|
870
|
+
meta: { requiresAuth: true }
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
path: '/videos/:uuid',
|
|
874
|
+
component: () => import('@/views/VideoPlayer.vue'),
|
|
875
|
+
props: true
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
path: '/videos/:uuid/edit',
|
|
879
|
+
component: () => import('@/views/VideoEditor.vue'),
|
|
880
|
+
props: true,
|
|
881
|
+
meta: { requiresAuth: true }
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
path: '/live',
|
|
885
|
+
component: () => import('@/views/LiveStream.vue'),
|
|
886
|
+
meta: { requiresAuth: true }
|
|
887
|
+
}
|
|
888
|
+
]
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
## Mobile Optimizations
|
|
892
|
+
|
|
893
|
+
### Responsive Video Player
|
|
894
|
+
|
|
895
|
+
```vue
|
|
896
|
+
<template>
|
|
897
|
+
<div class="mobile-player" :class="{ 'fullscreen': isFullscreen }">
|
|
898
|
+
<VideoPlayer
|
|
899
|
+
ref="player"
|
|
900
|
+
:media="media"
|
|
901
|
+
:controls="!showCustomControls"
|
|
902
|
+
@click="toggleControls"
|
|
903
|
+
@touchstart="handleTouchStart"
|
|
904
|
+
@touchend="handleTouchEnd"
|
|
905
|
+
/>
|
|
906
|
+
|
|
907
|
+
<!-- Mobile gesture overlay -->
|
|
908
|
+
<div v-if="showCustomControls" class="gesture-overlay">
|
|
909
|
+
<!-- Tap zones for seek -->
|
|
910
|
+
<div class="tap-zone left" @click="seekBackward">
|
|
911
|
+
<transition name="feedback">
|
|
912
|
+
<div v-if="showSeekFeedback === 'back'" class="seek-feedback">
|
|
913
|
+
-10s
|
|
914
|
+
</div>
|
|
915
|
+
</transition>
|
|
916
|
+
</div>
|
|
917
|
+
|
|
918
|
+
<div class="tap-zone center" @click="togglePlay">
|
|
919
|
+
<transition name="fade">
|
|
920
|
+
<IonIcon
|
|
921
|
+
v-if="showPlayPause"
|
|
922
|
+
:icon="isPlaying ? pauseCircleOutline : playCircleOutline"
|
|
923
|
+
class="play-icon"
|
|
924
|
+
/>
|
|
925
|
+
</transition>
|
|
926
|
+
</div>
|
|
927
|
+
|
|
928
|
+
<div class="tap-zone right" @click="seekForward">
|
|
929
|
+
<transition name="feedback">
|
|
930
|
+
<div v-if="showSeekFeedback === 'forward'" class="seek-feedback">
|
|
931
|
+
+10s
|
|
932
|
+
</div>
|
|
933
|
+
</transition>
|
|
934
|
+
</div>
|
|
935
|
+
</div>
|
|
936
|
+
|
|
937
|
+
<!-- Mobile controls (bottom) -->
|
|
938
|
+
<transition name="slide-up">
|
|
939
|
+
<div v-if="showCustomControls" class="mobile-controls">
|
|
940
|
+
<div class="progress-bar" @click="seekToPosition">
|
|
941
|
+
<div class="progress-fill" :style="{ width: progressPercent + '%' }" />
|
|
942
|
+
<div class="progress-thumb" :style="{ left: progressPercent + '%' }" />
|
|
943
|
+
</div>
|
|
944
|
+
|
|
945
|
+
<div class="control-buttons">
|
|
946
|
+
<button @click="togglePlay">
|
|
947
|
+
<IonIcon :icon="isPlaying ? pauseOutline : playOutline" />
|
|
948
|
+
</button>
|
|
949
|
+
|
|
950
|
+
<span class="time">
|
|
951
|
+
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
|
952
|
+
</span>
|
|
953
|
+
|
|
954
|
+
<button @click="toggleFullscreen">
|
|
955
|
+
<IonIcon :icon="isFullscreen ? contractOutline : expandOutline" />
|
|
956
|
+
</button>
|
|
957
|
+
</div>
|
|
958
|
+
</div>
|
|
959
|
+
</transition>
|
|
960
|
+
</div>
|
|
961
|
+
</template>
|
|
962
|
+
|
|
963
|
+
<script setup lang="ts">
|
|
964
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
965
|
+
import { IonIcon } from '@ionic/vue'
|
|
966
|
+
import {
|
|
967
|
+
playOutline,
|
|
968
|
+
pauseOutline,
|
|
969
|
+
playCircleOutline,
|
|
970
|
+
pauseCircleOutline,
|
|
971
|
+
expandOutline,
|
|
972
|
+
contractOutline
|
|
973
|
+
} from 'ionicons/icons'
|
|
974
|
+
import { VideoPlayer } from '@mediables/vue'
|
|
975
|
+
import { Haptics, ImpactStyle } from '@capacitor/haptics'
|
|
976
|
+
|
|
977
|
+
const props = defineProps({
|
|
978
|
+
media: Object
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
const player = ref()
|
|
982
|
+
const isPlaying = ref(false)
|
|
983
|
+
const isFullscreen = ref(false)
|
|
984
|
+
const showCustomControls = ref(true)
|
|
985
|
+
const showPlayPause = ref(false)
|
|
986
|
+
const showSeekFeedback = ref(null)
|
|
987
|
+
const currentTime = ref(0)
|
|
988
|
+
const duration = ref(0)
|
|
989
|
+
|
|
990
|
+
const progressPercent = computed(() => {
|
|
991
|
+
if (!duration.value) return 0
|
|
992
|
+
return (currentTime.value / duration.value) * 100
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
let controlsTimeout = null
|
|
996
|
+
|
|
997
|
+
function toggleControls() {
|
|
998
|
+
showCustomControls.value = !showCustomControls.value
|
|
999
|
+
resetControlsTimeout()
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function resetControlsTimeout() {
|
|
1003
|
+
clearTimeout(controlsTimeout)
|
|
1004
|
+
controlsTimeout = setTimeout(() => {
|
|
1005
|
+
if (isPlaying.value) {
|
|
1006
|
+
showCustomControls.value = false
|
|
1007
|
+
}
|
|
1008
|
+
}, 3000)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function togglePlay() {
|
|
1012
|
+
if (isPlaying.value) {
|
|
1013
|
+
player.value.pause()
|
|
1014
|
+
} else {
|
|
1015
|
+
player.value.play()
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
showPlayPause.value = true
|
|
1019
|
+
setTimeout(() => {
|
|
1020
|
+
showPlayPause.value = false
|
|
1021
|
+
}, 600)
|
|
1022
|
+
|
|
1023
|
+
// Haptic feedback
|
|
1024
|
+
Haptics.impact({ style: ImpactStyle.Light })
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function seekForward() {
|
|
1028
|
+
player.value.seek(currentTime.value + 10)
|
|
1029
|
+
showSeekFeedback.value = 'forward'
|
|
1030
|
+
setTimeout(() => {
|
|
1031
|
+
showSeekFeedback.value = null
|
|
1032
|
+
}, 600)
|
|
1033
|
+
|
|
1034
|
+
Haptics.impact({ style: ImpactStyle.Light })
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function seekBackward() {
|
|
1038
|
+
player.value.seek(Math.max(0, currentTime.value - 10))
|
|
1039
|
+
showSeekFeedback.value = 'back'
|
|
1040
|
+
setTimeout(() => {
|
|
1041
|
+
showSeekFeedback.value = null
|
|
1042
|
+
}, 600)
|
|
1043
|
+
|
|
1044
|
+
Haptics.impact({ style: ImpactStyle.Light })
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function seekToPosition(event) {
|
|
1048
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
1049
|
+
const percent = (event.clientX - rect.left) / rect.width
|
|
1050
|
+
player.value.seek(duration.value * percent)
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function toggleFullscreen() {
|
|
1054
|
+
if (isFullscreen.value) {
|
|
1055
|
+
document.exitFullscreen()
|
|
1056
|
+
} else {
|
|
1057
|
+
player.value.$el.requestFullscreen()
|
|
1058
|
+
}
|
|
1059
|
+
isFullscreen.value = !isFullscreen.value
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Gesture handling
|
|
1063
|
+
let touchStartX = 0
|
|
1064
|
+
let touchStartTime = 0
|
|
1065
|
+
|
|
1066
|
+
function handleTouchStart(event) {
|
|
1067
|
+
touchStartX = event.touches[0].clientX
|
|
1068
|
+
touchStartTime = currentTime.value
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function handleTouchEnd(event) {
|
|
1072
|
+
const touchEndX = event.changedTouches[0].clientX
|
|
1073
|
+
const deltaX = touchEndX - touchStartX
|
|
1074
|
+
|
|
1075
|
+
// Horizontal swipe for seek
|
|
1076
|
+
if (Math.abs(deltaX) > 50) {
|
|
1077
|
+
const seekAmount = (deltaX / window.innerWidth) * 30 // Max 30s seek
|
|
1078
|
+
player.value.seek(touchStartTime + seekAmount)
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function formatTime(seconds) {
|
|
1083
|
+
const mins = Math.floor(seconds / 60)
|
|
1084
|
+
const secs = Math.floor(seconds % 60)
|
|
1085
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Clean up on unmount
|
|
1089
|
+
onUnmounted(() => {
|
|
1090
|
+
clearTimeout(controlsTimeout)
|
|
1091
|
+
})
|
|
1092
|
+
</script>
|
|
1093
|
+
|
|
1094
|
+
<style scoped>
|
|
1095
|
+
.mobile-player {
|
|
1096
|
+
position: relative;
|
|
1097
|
+
width: 100%;
|
|
1098
|
+
background: black;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
.mobile-player.fullscreen {
|
|
1102
|
+
position: fixed;
|
|
1103
|
+
inset: 0;
|
|
1104
|
+
z-index: 9999;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.gesture-overlay {
|
|
1108
|
+
position: absolute;
|
|
1109
|
+
inset: 0;
|
|
1110
|
+
display: flex;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.tap-zone {
|
|
1114
|
+
flex: 1;
|
|
1115
|
+
display: flex;
|
|
1116
|
+
align-items: center;
|
|
1117
|
+
justify-content: center;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
.tap-zone.left,
|
|
1121
|
+
.tap-zone.right {
|
|
1122
|
+
flex: 0.3;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
.seek-feedback {
|
|
1126
|
+
padding: 0.5rem 1rem;
|
|
1127
|
+
background: rgba(0,0,0,0.7);
|
|
1128
|
+
color: white;
|
|
1129
|
+
border-radius: 20px;
|
|
1130
|
+
font-weight: bold;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
.play-icon {
|
|
1134
|
+
font-size: 64px;
|
|
1135
|
+
color: white;
|
|
1136
|
+
opacity: 0.9;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
.mobile-controls {
|
|
1140
|
+
position: absolute;
|
|
1141
|
+
bottom: 0;
|
|
1142
|
+
left: 0;
|
|
1143
|
+
right: 0;
|
|
1144
|
+
padding: 1rem;
|
|
1145
|
+
background: linear-gradient(transparent, rgba(0,0,0,0.9));
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
.progress-bar {
|
|
1149
|
+
position: relative;
|
|
1150
|
+
height: 4px;
|
|
1151
|
+
background: rgba(255,255,255,0.3);
|
|
1152
|
+
border-radius: 2px;
|
|
1153
|
+
margin-bottom: 1rem;
|
|
1154
|
+
cursor: pointer;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.progress-fill {
|
|
1158
|
+
position: absolute;
|
|
1159
|
+
top: 0;
|
|
1160
|
+
left: 0;
|
|
1161
|
+
height: 100%;
|
|
1162
|
+
background: var(--ion-color-primary);
|
|
1163
|
+
border-radius: 2px;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.progress-thumb {
|
|
1167
|
+
position: absolute;
|
|
1168
|
+
top: -6px;
|
|
1169
|
+
width: 16px;
|
|
1170
|
+
height: 16px;
|
|
1171
|
+
background: white;
|
|
1172
|
+
border-radius: 50%;
|
|
1173
|
+
transform: translateX(-50%);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.control-buttons {
|
|
1177
|
+
display: flex;
|
|
1178
|
+
align-items: center;
|
|
1179
|
+
justify-content: space-between;
|
|
1180
|
+
color: white;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.control-buttons button {
|
|
1184
|
+
background: none;
|
|
1185
|
+
border: none;
|
|
1186
|
+
color: white;
|
|
1187
|
+
font-size: 24px;
|
|
1188
|
+
padding: 0.5rem;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
.time {
|
|
1192
|
+
font-size: 14px;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/* Animations */
|
|
1196
|
+
.fade-enter-active,
|
|
1197
|
+
.fade-leave-active {
|
|
1198
|
+
transition: opacity 0.3s;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
.fade-enter-from,
|
|
1202
|
+
.fade-leave-to {
|
|
1203
|
+
opacity: 0;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
.feedback-enter-active {
|
|
1207
|
+
animation: bounce 0.6s;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
@keyframes bounce {
|
|
1211
|
+
0%, 100% { transform: scale(1); }
|
|
1212
|
+
50% { transform: scale(1.2); }
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
.slide-up-enter-active,
|
|
1216
|
+
.slide-up-leave-active {
|
|
1217
|
+
transition: transform 0.3s;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
.slide-up-enter-from,
|
|
1221
|
+
.slide-up-leave-to {
|
|
1222
|
+
transform: translateY(100%);
|
|
1223
|
+
}
|
|
1224
|
+
</style>
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
## Error Handling
|
|
1228
|
+
|
|
1229
|
+
### Comprehensive Error Management
|
|
1230
|
+
|
|
1231
|
+
```vue
|
|
1232
|
+
<template>
|
|
1233
|
+
<div class="video-container">
|
|
1234
|
+
<VideoUploader
|
|
1235
|
+
v-if="!hasError"
|
|
1236
|
+
@uploaded="handleUploaded"
|
|
1237
|
+
@error="handleError"
|
|
1238
|
+
/>
|
|
1239
|
+
|
|
1240
|
+
<!-- Error display -->
|
|
1241
|
+
<IonCard v-if="hasError" color="danger">
|
|
1242
|
+
<IonCardHeader>
|
|
1243
|
+
<IonCardTitle>Upload Error</IonCardTitle>
|
|
1244
|
+
</IonCardHeader>
|
|
1245
|
+
<IonCardContent>
|
|
1246
|
+
<p>{{ errorMessage }}</p>
|
|
1247
|
+
|
|
1248
|
+
<div v-if="errorDetails" class="error-details">
|
|
1249
|
+
<h4>Details:</h4>
|
|
1250
|
+
<ul>
|
|
1251
|
+
<li v-for="(detail, key) in errorDetails" :key="key">
|
|
1252
|
+
<strong>{{ key }}:</strong> {{ detail }}
|
|
1253
|
+
</li>
|
|
1254
|
+
</ul>
|
|
1255
|
+
</div>
|
|
1256
|
+
|
|
1257
|
+
<IonButton @click="retry" fill="outline" color="light">
|
|
1258
|
+
Try Again
|
|
1259
|
+
</IonButton>
|
|
1260
|
+
</IonCardContent>
|
|
1261
|
+
</IonCard>
|
|
1262
|
+
</div>
|
|
1263
|
+
</template>
|
|
1264
|
+
|
|
1265
|
+
<script setup lang="ts">
|
|
1266
|
+
import { ref, computed } from 'vue'
|
|
1267
|
+
import {
|
|
1268
|
+
IonCard,
|
|
1269
|
+
IonCardHeader,
|
|
1270
|
+
IonCardTitle,
|
|
1271
|
+
IonCardContent,
|
|
1272
|
+
IonButton
|
|
1273
|
+
} from '@ionic/vue'
|
|
1274
|
+
import { VideoUploader } from '@mediables/vue'
|
|
1275
|
+
|
|
1276
|
+
const error = ref(null)
|
|
1277
|
+
const hasError = computed(() => !!error.value)
|
|
1278
|
+
const errorMessage = computed(() => {
|
|
1279
|
+
if (!error.value) return ''
|
|
1280
|
+
|
|
1281
|
+
// Map common errors to user-friendly messages
|
|
1282
|
+
const errorMap = {
|
|
1283
|
+
'UPLOAD_TOO_LARGE': 'Video file is too large. Maximum size is 5GB.',
|
|
1284
|
+
'UNSUPPORTED_FORMAT': 'Video format not supported. Please use MP4, MOV, or WebM.',
|
|
1285
|
+
'NETWORK_ERROR': 'Network connection lost. Please check your internet and try again.',
|
|
1286
|
+
'QUOTA_EXCEEDED': 'Storage quota exceeded. Please delete some videos first.',
|
|
1287
|
+
'PROCESSING_FAILED': 'Video processing failed. The file may be corrupted.',
|
|
1288
|
+
'UNAUTHORIZED': 'Session expired. Please log in again.',
|
|
1289
|
+
'RATE_LIMITED': 'Too many uploads. Please wait a moment and try again.'
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
return errorMap[error.value.code] || error.value.message || 'An unexpected error occurred'
|
|
1293
|
+
})
|
|
1294
|
+
|
|
1295
|
+
const errorDetails = computed(() => {
|
|
1296
|
+
if (!error.value?.details) return null
|
|
1297
|
+
return error.value.details
|
|
1298
|
+
})
|
|
1299
|
+
|
|
1300
|
+
function handleUploaded(media) {
|
|
1301
|
+
console.log('Upload successful:', media)
|
|
1302
|
+
error.value = null
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function handleError(uploadError) {
|
|
1306
|
+
console.error('Upload error:', uploadError)
|
|
1307
|
+
|
|
1308
|
+
// Parse error response
|
|
1309
|
+
if (uploadError.response?.data?.error) {
|
|
1310
|
+
error.value = uploadError.response.data.error
|
|
1311
|
+
} else if (uploadError.code === 'ERR_NETWORK') {
|
|
1312
|
+
error.value = {
|
|
1313
|
+
code: 'NETWORK_ERROR',
|
|
1314
|
+
message: 'Network connection failed'
|
|
1315
|
+
}
|
|
1316
|
+
} else {
|
|
1317
|
+
error.value = {
|
|
1318
|
+
code: 'UNKNOWN',
|
|
1319
|
+
message: uploadError.message || 'Upload failed',
|
|
1320
|
+
details: {
|
|
1321
|
+
status: uploadError.response?.status,
|
|
1322
|
+
statusText: uploadError.response?.statusText
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Log to error tracking service
|
|
1328
|
+
if (window.Sentry) {
|
|
1329
|
+
window.Sentry.captureException(uploadError, {
|
|
1330
|
+
contexts: {
|
|
1331
|
+
upload: {
|
|
1332
|
+
error_code: error.value.code,
|
|
1333
|
+
error_message: error.value.message
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
})
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function retry() {
|
|
1341
|
+
error.value = null
|
|
1342
|
+
}
|
|
1343
|
+
</script>
|
|
1344
|
+
|
|
1345
|
+
<style scoped>
|
|
1346
|
+
.error-details {
|
|
1347
|
+
margin-top: 1rem;
|
|
1348
|
+
padding: 1rem;
|
|
1349
|
+
background: rgba(0,0,0,0.1);
|
|
1350
|
+
border-radius: 4px;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
.error-details ul {
|
|
1354
|
+
list-style: none;
|
|
1355
|
+
padding: 0;
|
|
1356
|
+
margin: 0.5rem 0 0 0;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
.error-details li {
|
|
1360
|
+
padding: 0.25rem 0;
|
|
1361
|
+
}
|
|
1362
|
+
</style>
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
## Performance Tips
|
|
1366
|
+
|
|
1367
|
+
### 1. Lazy Load Components
|
|
1368
|
+
|
|
1369
|
+
```typescript
|
|
1370
|
+
// Only load video components when needed
|
|
1371
|
+
const VideoEditor = defineAsyncComponent(() =>
|
|
1372
|
+
import('@mediables/vue').then(m => m.VideoEditor)
|
|
1373
|
+
)
|
|
1374
|
+
|
|
1375
|
+
const VideoPlayer = defineAsyncComponent(() =>
|
|
1376
|
+
import('@mediables/vue').then(m => m.VideoPlayer)
|
|
1377
|
+
)
|
|
1378
|
+
```
|
|
1379
|
+
|
|
1380
|
+
### 2. Optimize Thumbnails
|
|
1381
|
+
|
|
1382
|
+
```vue
|
|
1383
|
+
<script setup>
|
|
1384
|
+
// Generate multiple thumbnail sizes
|
|
1385
|
+
const thumbnailSizes = {
|
|
1386
|
+
small: { width: 320, height: 180 },
|
|
1387
|
+
medium: { width: 640, height: 360 },
|
|
1388
|
+
large: { width: 1280, height: 720 }
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function getThumbnailUrl(media, size = 'medium') {
|
|
1392
|
+
const { width, height } = thumbnailSizes[size]
|
|
1393
|
+
return `${media.thumbnail_base}?w=${width}&h=${height}&fit=cover`
|
|
1394
|
+
}
|
|
1395
|
+
</script>
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
### 3. Implement Virtual Scrolling
|
|
1399
|
+
|
|
1400
|
+
```vue
|
|
1401
|
+
<template>
|
|
1402
|
+
<RecycleScroller
|
|
1403
|
+
class="video-list"
|
|
1404
|
+
:items="videos"
|
|
1405
|
+
:item-size="120"
|
|
1406
|
+
key-field="uuid"
|
|
1407
|
+
v-slot="{ item }"
|
|
1408
|
+
>
|
|
1409
|
+
<VideoListItem :video="item" />
|
|
1410
|
+
</RecycleScroller>
|
|
1411
|
+
</template>
|
|
1412
|
+
|
|
1413
|
+
<script setup>
|
|
1414
|
+
import { RecycleScroller } from 'vue-virtual-scroller'
|
|
1415
|
+
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
|
1416
|
+
</script>
|
|
1417
|
+
```
|
|
1418
|
+
|
|
1419
|
+
### 4. Cache Playback URLs
|
|
1420
|
+
|
|
1421
|
+
```typescript
|
|
1422
|
+
// Use a simple LRU cache for playback URLs
|
|
1423
|
+
import lru from 'tiny-lru'
|
|
1424
|
+
|
|
1425
|
+
const playbackCache = lru(100, 3600000) // 100 items, 1 hour TTL
|
|
1426
|
+
|
|
1427
|
+
async function getPlaybackUrl(uuid) {
|
|
1428
|
+
const cached = playbackCache.get(uuid)
|
|
1429
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
1430
|
+
return cached.url
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const response = await api.get(`/video/${uuid}/playback`)
|
|
1434
|
+
playbackCache.set(uuid, {
|
|
1435
|
+
url: response.data.playback_url,
|
|
1436
|
+
expiresAt: new Date(response.data.token_expires_at).getTime()
|
|
1437
|
+
})
|
|
1438
|
+
|
|
1439
|
+
return response.data.playback_url
|
|
1440
|
+
}
|
|
1441
|
+
```
|
|
1442
|
+
|
|
1443
|
+
### 5. Debounce Editor Operations
|
|
1444
|
+
|
|
1445
|
+
```typescript
|
|
1446
|
+
import { debounce } from 'lodash-es'
|
|
1447
|
+
|
|
1448
|
+
const saveRecipe = debounce(async (recipe) => {
|
|
1449
|
+
await api.post('/video/draft', { recipe })
|
|
1450
|
+
}, 1000)
|
|
1451
|
+
|
|
1452
|
+
const updateTimeline = debounce((changes) => {
|
|
1453
|
+
editorStore.updateTimeline(changes)
|
|
1454
|
+
}, 100)
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
## Best Practices
|
|
1458
|
+
|
|
1459
|
+
1. **Always handle loading states** - Show spinners or skeletons
|
|
1460
|
+
2. **Provide feedback** - Acknowledge user actions immediately
|
|
1461
|
+
3. **Test on real devices** - Especially for mobile gestures
|
|
1462
|
+
4. **Monitor performance** - Track video load times and errors
|
|
1463
|
+
5. **Implement retry logic** - For network failures
|
|
1464
|
+
6. **Use appropriate video formats** - MP4 for compatibility
|
|
1465
|
+
7. **Optimize for mobile data** - Offer quality settings
|
|
1466
|
+
8. **Cache aggressively** - But respect token expiry
|
|
1467
|
+
9. **Handle edge cases** - No network, low storage, etc.
|
|
1468
|
+
10. **Accessibility** - Captions, keyboard controls, ARIA labels
|
|
1469
|
+
|
|
1470
|
+
## Support
|
|
1471
|
+
|
|
1472
|
+
For more examples and support:
|
|
1473
|
+
|
|
1474
|
+
- **GitHub Examples**: https://github.com/mediables/examples
|
|
1475
|
+
- **CodeSandbox Demos**: https://codesandbox.io/u/mediables
|
|
1476
|
+
- **Video Tutorials**: https://youtube.com/@mediables
|
|
1477
|
+
- **Discord Community**: https://discord.gg/mediables
|