@liorian/sdk 0.28.0-alpha.56010ca
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/README.md +483 -0
- package/package.json +94 -0
- package/schemas/module.schema.json +818 -0
- package/src/application/index.ts +3 -0
- package/src/application/module-instance.ts +36 -0
- package/src/application/service/auth-api-service.ts +291 -0
- package/src/application/service/auth-user.service.ts +108 -0
- package/src/application/service/index.ts +12 -0
- package/src/application/service/module-activation-api.service.ts +105 -0
- package/src/application/service/notifications-api-service.ts +38 -0
- package/src/application/service/notifications-push.service.ts +299 -0
- package/src/application/service/notifications-socket-service.ts +76 -0
- package/src/application/service/organizations-api-service.ts +104 -0
- package/src/application/service/otp-api-service.ts +11 -0
- package/src/application/service/storage-api-service.ts +77 -0
- package/src/application/service/storefront-api.service.ts +72 -0
- package/src/application/service/user-preferences-api.service.ts +57 -0
- package/src/domain/config/app.config.ts +35 -0
- package/src/domain/config/auth.config.ts +30 -0
- package/src/domain/config/index.ts +4 -0
- package/src/domain/config/logo.config.ts +17 -0
- package/src/domain/entities/activities.interface.ts +18 -0
- package/src/domain/entities/catalog.interface.ts +96 -0
- package/src/domain/entities/index.ts +13 -0
- package/src/domain/entities/media.ts +22 -0
- package/src/domain/entities/module-activation.interface.ts +173 -0
- package/src/domain/entities/module.interface.ts +101 -0
- package/src/domain/entities/notification.interface.ts +23 -0
- package/src/domain/entities/objectable.type.ts +3 -0
- package/src/domain/entities/organization.interface.ts +81 -0
- package/src/domain/entities/user-auth.interface.ts +17 -0
- package/src/domain/entities/user-phone.interface.ts +11 -0
- package/src/domain/entities/user-profile.interface.ts +23 -0
- package/src/domain/entities/user.interface.ts +79 -0
- package/src/domain/enums/coloring.enum.ts +6 -0
- package/src/domain/enums/domains.enum.ts +282 -0
- package/src/domain/enums/index.ts +8 -0
- package/src/domain/enums/module-category.enum.ts +57 -0
- package/src/domain/enums/module.enum.ts +97 -0
- package/src/domain/enums/routine.enum.ts +12 -0
- package/src/domain/enums/user-profile.enum.ts +25 -0
- package/src/domain/enums/user-status.enum.ts +7 -0
- package/src/domain/index.ts +6 -0
- package/src/domain/typing/auth-caches.ts +12 -0
- package/src/domain/typing/auth-oauth.types.ts +172 -0
- package/src/domain/typing/devise.ts +2 -0
- package/src/domain/typing/index.ts +10 -0
- package/src/domain/typing/response.ts +30 -0
- package/src/domain/typing/routine.types.ts +113 -0
- package/src/domain/typing/session.interface.ts +28 -0
- package/src/domain/typing/statisticals.ts +11 -0
- package/src/domain/typing/user-media-upload.ts +9 -0
- package/src/domain/typing/values.ts +6 -0
- package/src/index.tsx +168 -0
- package/src/infrastructure/capabilities/cache-storage/capability.ts +129 -0
- package/src/infrastructure/capabilities/cache-storage/enum.ts +5 -0
- package/src/infrastructure/capabilities/cache-storage/index.ts +4 -0
- package/src/infrastructure/capabilities/cache-storage/types.ts +13 -0
- package/src/infrastructure/capabilities/index.ts +3 -0
- package/src/infrastructure/capabilities/stackable/capability.ts +55 -0
- package/src/infrastructure/capabilities/stackable/index.ts +3 -0
- package/src/infrastructure/capabilities/stackable/types.ts +24 -0
- package/src/infrastructure/common-classname.constant.ts +4 -0
- package/src/infrastructure/data/countries.ts +208 -0
- package/src/infrastructure/data/gender.ts +5 -0
- package/src/infrastructure/data/index.ts +3 -0
- package/src/infrastructure/dataset/index.ts +3 -0
- package/src/infrastructure/dataset/sign-in.dataset.ts +21 -0
- package/src/infrastructure/dataset/sign-up.dataset.ts +64 -0
- package/src/infrastructure/hooks/index.ts +10 -0
- package/src/infrastructure/hooks/use-activity-lock-draft.ts +69 -0
- package/src/infrastructure/hooks/use-auth.ts +24 -0
- package/src/infrastructure/hooks/use-elastic-interaction.ts +180 -0
- package/src/infrastructure/hooks/use-is-desktop.ts +25 -0
- package/src/infrastructure/hooks/use-is-tauri.ts +17 -0
- package/src/infrastructure/hooks/use-mobile.ts +19 -0
- package/src/infrastructure/hooks/use-notifications.ts +128 -0
- package/src/infrastructure/hooks/use-recent-colors.ts +113 -0
- package/src/infrastructure/hooks/use-recent-modules.ts +76 -0
- package/src/infrastructure/index.tsx +11 -0
- package/src/infrastructure/library/design-system.ts +95 -0
- package/src/infrastructure/library/index.tsx +5 -0
- package/src/infrastructure/library/motion-utils.ts +77 -0
- package/src/infrastructure/library/motion.tsx +31 -0
- package/src/infrastructure/library/scroll-progress.tsx +93 -0
- package/src/infrastructure/providers/auth-guard.provider.tsx +47 -0
- package/src/infrastructure/providers/auth.provider.tsx +168 -0
- package/src/infrastructure/providers/index.tsx +8 -0
- package/src/infrastructure/providers/module-usage.provider.tsx +95 -0
- package/src/infrastructure/providers/modules-guard.provider.tsx +64 -0
- package/src/infrastructure/providers/modules-routines.provider.tsx +33 -0
- package/src/infrastructure/providers/modules-switcher.provider.tsx +25 -0
- package/src/infrastructure/providers/query.provider.tsx +27 -0
- package/src/infrastructure/routines/__tests__/routine.test.ts +368 -0
- package/src/infrastructure/routines/index.tsx +3 -0
- package/src/infrastructure/routines/routine.hook.ts +121 -0
- package/src/infrastructure/routines/routine.ts +283 -0
- package/src/infrastructure/routines/routine.tsx +283 -0
- package/src/infrastructure/stores/activity-lock.store.ts +22 -0
- package/src/infrastructure/stores/auth-user-connected.store.ts +24 -0
- package/src/infrastructure/stores/dataset.store.ts +106 -0
- package/src/infrastructure/stores/index.ts +6 -0
- package/src/infrastructure/stores/module.store.ts +266 -0
- package/src/infrastructure/stores/upload.store.ts +198 -0
- package/src/infrastructure/utilities/access-label.util.ts +158 -0
- package/src/infrastructure/utilities/activities.util.tsx +54 -0
- package/src/infrastructure/utilities/activity-lock.service.ts +263 -0
- package/src/infrastructure/utilities/activity-tracker.service.ts +205 -0
- package/src/infrastructure/utilities/api-service.ts +237 -0
- package/src/infrastructure/utilities/auth-logout.util.ts +23 -0
- package/src/infrastructure/utilities/classname.util.ts +7 -0
- package/src/infrastructure/utilities/encryption.service.ts +125 -0
- package/src/infrastructure/utilities/form-snapshot.service.ts +304 -0
- package/src/infrastructure/utilities/format.util.ts +40 -0
- package/src/infrastructure/utilities/index.tsx +23 -0
- package/src/infrastructure/utilities/media-label.service.ts +47 -0
- package/src/infrastructure/utilities/modules.ts +11 -0
- package/src/infrastructure/utilities/permission.util.ts +22 -0
- package/src/infrastructure/utilities/push-permission.util.ts +43 -0
- package/src/infrastructure/utilities/route-access.ts +38 -0
- package/src/infrastructure/utilities/socket-service.ts +123 -0
- package/src/infrastructure/utilities/sso-auth.util.ts +188 -0
- package/src/infrastructure/utilities/stringable.util.ts +16 -0
- package/src/infrastructure/utilities/strings.util.ts +18 -0
- package/src/infrastructure/utilities/tauri.util.ts +15 -0
- package/src/infrastructure/utilities/utils.ts +6 -0
- package/src/infrastructure/utilities/values.ts +5 -0
- package/src/presentation/8starlabs-ui/button.tsx +94 -0
- package/src/presentation/8starlabs-ui/flip-clock.tsx +368 -0
- package/src/presentation/8starlabs-ui/heatmap.tsx +430 -0
- package/src/presentation/8starlabs-ui/hooks/index.tsx +2 -0
- package/src/presentation/8starlabs-ui/hooks/use-mobile.ts +21 -0
- package/src/presentation/8starlabs-ui/index.tsx +20 -0
- package/src/presentation/8starlabs-ui/json-viewer.tsx +972 -0
- package/src/presentation/8starlabs-ui/marquee.tsx +134 -0
- package/src/presentation/8starlabs-ui/partition-bar.tsx +198 -0
- package/src/presentation/8starlabs-ui/scroll-fade.tsx +158 -0
- package/src/presentation/8starlabs-ui/status-indicator.tsx +86 -0
- package/src/presentation/8starlabs-ui/system-banner.tsx +50 -0
- package/src/presentation/8starlabs-ui/timeline.tsx +576 -0
- package/src/presentation/8starlabs-ui/transport-badge.tsx +249 -0
- package/src/presentation/8starlabs-ui/ui/copy-button.tsx +60 -0
- package/src/presentation/8starlabs-ui/ui/index.tsx +2 -0
- package/src/presentation/analytics-section.tsx +85 -0
- package/src/presentation/app-sidebar.tsx +234 -0
- package/src/presentation/charts/area-widget.chart-skeleton.tsx +61 -0
- package/src/presentation/charts/area-widget.chart.tsx +128 -0
- package/src/presentation/charts/chart-widgets.types.ts +81 -0
- package/src/presentation/charts/index.tsx +16 -0
- package/src/presentation/charts/line-widget.chart-skeleton.tsx +64 -0
- package/src/presentation/charts/line-widget.chart.tsx +105 -0
- package/src/presentation/charts/pie-widget.chart-skeleton.tsx +56 -0
- package/src/presentation/charts/pie-widget.chart.tsx +168 -0
- package/src/presentation/charts/radar-widget.chart-skeleton.tsx +67 -0
- package/src/presentation/charts/radar-widget.chart.tsx +80 -0
- package/src/presentation/charts/radial-stacked-widget.chart-skeleton.tsx +66 -0
- package/src/presentation/charts/radial-stacked-widget.chart.tsx +120 -0
- package/src/presentation/charts/radial-widget.chart-skeleton.tsx +61 -0
- package/src/presentation/charts/radial-widget.chart.tsx +87 -0
- package/src/presentation/charts/tooltip-widget.chart-skeleton.tsx +64 -0
- package/src/presentation/charts/tooltip-widget.chart.tsx +89 -0
- package/src/presentation/components/activity-actions.tsx +20 -0
- package/src/presentation/components/activity-container.tsx +55 -0
- package/src/presentation/components/activity-content.tsx +20 -0
- package/src/presentation/components/activity-descriptor-parser.tsx +37 -0
- package/src/presentation/components/activity-header.tsx +20 -0
- package/src/presentation/components/activity-loader.tsx +19 -0
- package/src/presentation/components/activity-title.tsx +32 -0
- package/src/presentation/components/activity.tsx +22 -0
- package/src/presentation/components/auth/index.tsx +4 -0
- package/src/presentation/components/auth/sso-callback.view.tsx +44 -0
- package/src/presentation/components/auth/sso-disconnect.view.tsx +41 -0
- package/src/presentation/components/auth/sso-sign-in.view.tsx +29 -0
- package/src/presentation/components/auth-lock-screen.tsx +309 -0
- package/src/presentation/components/auth-session.view.tsx +17 -0
- package/src/presentation/components/data-table.tsx +814 -0
- package/src/presentation/components/dynamic-icon.tsx +16 -0
- package/src/presentation/components/floating-upload.tsx +291 -0
- package/src/presentation/components/index.tsx +40 -0
- package/src/presentation/components/local-time.tsx +170 -0
- package/src/presentation/components/markdown-parser.tsx +201 -0
- package/src/presentation/components/message-scroller.tsx +131 -0
- package/src/presentation/components/module-first-access-config.view.tsx +181 -0
- package/src/presentation/components/module-first-access-pending.view.tsx +38 -0
- package/src/presentation/components/module-guard.tsx +34 -0
- package/src/presentation/components/modules-first-access.guard.tsx +88 -0
- package/src/presentation/components/modules-guard-forbidden.tsx +46 -0
- package/src/presentation/components/notification-details.modal.tsx +145 -0
- package/src/presentation/components/notification-item.tsx +127 -0
- package/src/presentation/components/notifications-popover.tsx +128 -0
- package/src/presentation/components/push-notifications.provider.tsx +65 -0
- package/src/presentation/components/upload-progress-dialog.tsx +140 -0
- package/src/presentation/components/waiting-bar.tsx +22 -0
- package/src/presentation/components/waiting-section.tsx +15 -0
- package/src/presentation/components/waiting.tsx +14 -0
- package/src/presentation/data-grid/data-grid-empty.tsx +24 -0
- package/src/presentation/data-grid/data-grid-search-engine.tsx +32 -0
- package/src/presentation/data-grid/data-grid.tsx +892 -0
- package/src/presentation/data-grid/index.tsx +4 -0
- package/src/presentation/form-screen.tsx +95 -0
- package/src/presentation/icons/custom.tsx +20 -0
- package/src/presentation/icons/glyphIcons.tsx +53 -0
- package/src/presentation/icons/icons.enum.ts +4 -0
- package/src/presentation/icons/icons.util.ts +6 -0
- package/src/presentation/icons/index.tsx +8 -0
- package/src/presentation/icons/lucide.tsx +17 -0
- package/src/presentation/icons/remix.tsx +33 -0
- package/src/presentation/icons/types.ts +27 -0
- package/src/presentation/index.tsx +45 -0
- package/src/presentation/modals/components/ModalBox.tsx +74 -0
- package/src/presentation/modals/components/ModalPortal.tsx +31 -0
- package/src/presentation/modals/components/ModalStepper.tsx +319 -0
- package/src/presentation/modals/components/ModalWrapper.tsx +65 -0
- package/src/presentation/modals/components/index.tsx +5 -0
- package/src/presentation/modals/hooks/index.ts +2 -0
- package/src/presentation/modals/hooks/useModal.ts +38 -0
- package/src/presentation/modals/index.tsx +5 -0
- package/src/presentation/modals/stores/index.ts +2 -0
- package/src/presentation/modals/stores/useModalStore.ts +90 -0
- package/src/presentation/modals/types/index.ts +2 -0
- package/src/presentation/modals/types/modal.type.ts +33 -0
- package/src/presentation/module-menubar.tsx +300 -0
- package/src/presentation/module-widget.tsx +233 -0
- package/src/presentation/nav-documents.tsx +89 -0
- package/src/presentation/nav-main.tsx +62 -0
- package/src/presentation/nav-secondary.tsx +41 -0
- package/src/presentation/nav-user.tsx +109 -0
- package/src/presentation/permission-action-badge.tsx +35 -0
- package/src/presentation/reui/index.tsx +3 -0
- package/src/presentation/reui/stepper.tsx +508 -0
- package/src/presentation/reui/timeline.tsx +226 -0
- package/src/presentation/sheets/index.tsx +2 -0
- package/src/presentation/sheets/legacy-sheet-header.tsx +0 -0
- package/src/presentation/sheets/legacy-sheet.tsx +37 -0
- package/src/presentation/size.enum.ts +19 -0
- package/src/presentation/system/index.tsx +3 -0
- package/src/presentation/system/logo.theme.tsx +21 -0
- package/src/presentation/system/types.ts +10 -0
- package/src/presentation/theme/index.tsx +6 -0
- package/src/presentation/theme/prefer-color-scheme.tsx +50 -0
- package/src/presentation/theme/switch-color-scheme.tsx +16 -0
- package/src/presentation/theme/theme-switcher-button.tsx +20 -0
- package/src/presentation/theme/theme.enum.ts +4 -0
- package/src/presentation/theme/theme.store.ts +87 -0
- package/src/presentation/themes/index.tsx +2 -0
- package/src/presentation/themes/katon/dashboard-layout.tsx +1080 -0
- package/src/presentation/themes/katon/edge-section.tsx +19 -0
- package/src/presentation/themes/katon/header-menubar.tsx +270 -0
- package/src/presentation/themes/katon/header-modules-available.tsx +44 -0
- package/src/presentation/themes/katon/header-modules-recents.tsx +52 -0
- package/src/presentation/themes/katon/header-modules-store.tsx +25 -0
- package/src/presentation/themes/katon/header-options.tsx +13 -0
- package/src/presentation/themes/katon/header-organization-select.tsx +146 -0
- package/src/presentation/themes/katon/header-routines.tsx +74 -0
- package/src/presentation/themes/katon/header-tasks-connected-user.tsx +150 -0
- package/src/presentation/themes/katon/index.tsx +16 -0
- package/src/presentation/themes/katon/main-wrapper.tsx +34 -0
- package/src/presentation/themes/katon/module-item-link.tsx +32 -0
- package/src/presentation/themes/katon/view-frame.tsx +25 -0
- package/src/presentation/themes/katon/view-helmet.tsx +53 -0
- package/src/presentation/themes/katon/view-status.tsx +32 -0
- package/src/presentation/themes/katon/view-wrapper.tsx +15 -0
- package/src/presentation/themes/katon/view.tsx +42 -0
- package/src/presentation/ui/accordion-multiselect.tsx +206 -0
- package/src/presentation/ui/accordion.tsx +81 -0
- package/src/presentation/ui/alert-dialog.tsx +199 -0
- package/src/presentation/ui/alert.tsx +76 -0
- package/src/presentation/ui/aspect-ratio.tsx +11 -0
- package/src/presentation/ui/attachment.tsx +204 -0
- package/src/presentation/ui/avatar.tsx +112 -0
- package/src/presentation/ui/badge.tsx +49 -0
- package/src/presentation/ui/breadcrumb.tsx +122 -0
- package/src/presentation/ui/bubble.tsx +125 -0
- package/src/presentation/ui/button-group.tsx +83 -0
- package/src/presentation/ui/button.tsx +87 -0
- package/src/presentation/ui/calendar.tsx +222 -0
- package/src/presentation/ui/card.tsx +106 -0
- package/src/presentation/ui/carousel.tsx +242 -0
- package/src/presentation/ui/chart.tsx +373 -0
- package/src/presentation/ui/checkbox.tsx +33 -0
- package/src/presentation/ui/clickable.tsx +58 -0
- package/src/presentation/ui/collapsible.tsx +33 -0
- package/src/presentation/ui/color-swatch-selector.tsx +81 -0
- package/src/presentation/ui/combobox.tsx +299 -0
- package/src/presentation/ui/command.tsx +195 -0
- package/src/presentation/ui/context-menu.tsx +263 -0
- package/src/presentation/ui/dialog.tsx +168 -0
- package/src/presentation/ui/direction.tsx +22 -0
- package/src/presentation/ui/drawer.tsx +134 -0
- package/src/presentation/ui/dropdown-menu.tsx +269 -0
- package/src/presentation/ui/empty.tsx +104 -0
- package/src/presentation/ui/field.tsx +238 -0
- package/src/presentation/ui/granularity-selector.tsx +58 -0
- package/src/presentation/ui/hover-card.tsx +44 -0
- package/src/presentation/ui/index.tsx +74 -0
- package/src/presentation/ui/input-group.tsx +156 -0
- package/src/presentation/ui/input-otp.tsx +87 -0
- package/src/presentation/ui/input.tsx +19 -0
- package/src/presentation/ui/item.tsx +196 -0
- package/src/presentation/ui/kpi-card.tsx +203 -0
- package/src/presentation/ui/label-selector.tsx +100 -0
- package/src/presentation/ui/label.tsx +24 -0
- package/src/presentation/ui/legacy-birth-date-input.tsx +43 -0
- package/src/presentation/ui/legacy-color-picker/index.tsx +42 -0
- package/src/presentation/ui/legacy-color-picker/legacy-color-picker.content.tsx +37 -0
- package/src/presentation/ui/legacy-color-picker/legacy-color-picker.context.tsx +42 -0
- package/src/presentation/ui/legacy-color-picker/legacy-color-picker.hex-input.tsx +97 -0
- package/src/presentation/ui/legacy-color-picker/legacy-color-picker.picker.tsx +266 -0
- package/src/presentation/ui/legacy-color-picker/legacy-color-picker.presets.ts +22 -0
- package/src/presentation/ui/legacy-color-picker/legacy-color-picker.quick-list.tsx +49 -0
- package/src/presentation/ui/legacy-color-picker/legacy-color-picker.recent.tsx +43 -0
- package/src/presentation/ui/legacy-color-picker/legacy-color-picker.swatches.tsx +111 -0
- package/src/presentation/ui/legacy-color-picker/legacy-color-picker.trigger.tsx +128 -0
- package/src/presentation/ui/legacy-color-picker/legacy-color-picker.tsx +110 -0
- package/src/presentation/ui/legacy-color-picker/legacy-color-picker.types.ts +1 -0
- package/src/presentation/ui/legacy-color-picker/legacy-color-picker.utils.ts +114 -0
- package/src/presentation/ui/legacy-country-input.tsx +171 -0
- package/src/presentation/ui/legacy-gender-input.tsx +22 -0
- package/src/presentation/ui/legacy-input.tsx +106 -0
- package/src/presentation/ui/legacy-phone-input.tsx +213 -0
- package/src/presentation/ui/legacy-select-input.tsx +114 -0
- package/src/presentation/ui/legacy-text-area.tsx +111 -0
- package/src/presentation/ui/list-items.tsx +59 -0
- package/src/presentation/ui/marker.tsx +69 -0
- package/src/presentation/ui/media-player/index.tsx +12 -0
- package/src/presentation/ui/media-player/media-player.context.tsx +36 -0
- package/src/presentation/ui/media-player/media-player.tsx +167 -0
- package/src/presentation/ui/media-player/player-controls.tsx +207 -0
- package/src/presentation/ui/media-player/players/audio-player.tsx +78 -0
- package/src/presentation/ui/media-player/players/index.tsx +4 -0
- package/src/presentation/ui/media-player/players/video-player.tsx +193 -0
- package/src/presentation/ui/media-player/players/voice-note-player.tsx +108 -0
- package/src/presentation/ui/media-player/use-media-player.hook.ts +263 -0
- package/src/presentation/ui/menubar.tsx +281 -0
- package/src/presentation/ui/message.tsx +92 -0
- package/src/presentation/ui/navigation-menu.tsx +172 -0
- package/src/presentation/ui/pagination.tsx +129 -0
- package/src/presentation/ui/popover.tsx +89 -0
- package/src/presentation/ui/progress.tsx +31 -0
- package/src/presentation/ui/resizable.tsx +50 -0
- package/src/presentation/ui/select.tsx +192 -0
- package/src/presentation/ui/separator.tsx +28 -0
- package/src/presentation/ui/sheet.tsx +147 -0
- package/src/presentation/ui/sidebar.tsx +702 -0
- package/src/presentation/ui/skeleton.tsx +13 -0
- package/src/presentation/ui/slide-to-confirm.tsx +129 -0
- package/src/presentation/ui/slider.tsx +59 -0
- package/src/presentation/ui/sonner.tsx +58 -0
- package/src/presentation/ui/switch.tsx +33 -0
- package/src/presentation/ui/table.tsx +116 -0
- package/src/presentation/ui/tabs.tsx +90 -0
- package/src/presentation/ui/text-gradient.tsx +84 -0
- package/src/presentation/ui/textarea.tsx +18 -0
- package/src/presentation/ui/timeline-steps.tsx +235 -0
- package/src/presentation/ui/toggle-group.tsx +89 -0
- package/src/presentation/ui/toggle.tsx +47 -0
- package/src/presentation/ui/tooltip.tsx +57 -0
- package/src/presentation/uploading/index.tsx +3 -0
- package/src/presentation/uploading/media-upload-field.tsx +152 -0
- package/src/presentation/uploading/storage-media.tsx +210 -0
- package/src/styles.css +403 -0
- package/src/themes.css +1038 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import {cn} from "../../../infrastructure/utilities/utils";
|
|
6
|
+
import {AudioPlayer} from "./players/audio-player";
|
|
7
|
+
import {VideoPlayer} from "./players/video-player";
|
|
8
|
+
import {VoiceNotePlayer} from "./players/voice-note-player";
|
|
9
|
+
import {MediaPlayerProvider, type MediaPlayerVariant} from "./media-player.context";
|
|
10
|
+
import {useMediaPlayer, type UseMediaPlayerOptions} from "./use-media-player.hook";
|
|
11
|
+
|
|
12
|
+
export interface MediaPlayerProps
|
|
13
|
+
extends Omit<UseMediaPlayerOptions, "src">,
|
|
14
|
+
Pick<React.ComponentProps<"div">, "className" | "id"> {
|
|
15
|
+
/** URL du média : fichier ou flux live (.m3u8 géré automatiquement). */
|
|
16
|
+
src: string;
|
|
17
|
+
/** Apparence : « voice-note » (bulle WhatsApp), « audio » (classique) ou « video » (YouTube). */
|
|
18
|
+
variant?: MediaPlayerVariant;
|
|
19
|
+
/** Titre annoncé aux lecteurs d'écran et affiché selon la variante. */
|
|
20
|
+
label?: string;
|
|
21
|
+
/** Pochette (audio) ou image d'aperçu (video). */
|
|
22
|
+
cover?: string;
|
|
23
|
+
/** Classe utilitaire pour personnaliser la couleur d'accent (progression, poignée…). */
|
|
24
|
+
accentClassName?: string;
|
|
25
|
+
/** Déclenché à la fin de la lecture (jamais en direct). */
|
|
26
|
+
onEnded?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
Lecteur multimédia unifié du design system : une seule source de logique
|
|
31
|
+
(useMediaPlayer) et trois apparences partageant la même direction
|
|
32
|
+
artistique, les mêmes commandes clavier (inspirées de YouTube) et le même
|
|
33
|
+
support des flux live HLS.
|
|
34
|
+
*/
|
|
35
|
+
export function MediaPlayer({
|
|
36
|
+
src,
|
|
37
|
+
variant = "audio",
|
|
38
|
+
label,
|
|
39
|
+
cover,
|
|
40
|
+
autoPlay = false,
|
|
41
|
+
rates,
|
|
42
|
+
live,
|
|
43
|
+
accentClassName,
|
|
44
|
+
className,
|
|
45
|
+
id,
|
|
46
|
+
onEnded,
|
|
47
|
+
}: MediaPlayerProps) {
|
|
48
|
+
const controller = useMediaPlayer({src, live, autoPlay, rates});
|
|
49
|
+
const {mediaRef, containerRef} = controller;
|
|
50
|
+
|
|
51
|
+
// Propagation optionnelle de l'événement « ended ».
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
const element = mediaRef.current;
|
|
54
|
+
if (!element || !onEnded) return;
|
|
55
|
+
element.addEventListener("ended", onEnded);
|
|
56
|
+
return () => element.removeEventListener("ended", onEnded);
|
|
57
|
+
}, [mediaRef, onEnded]);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
Raccourcis inspirés de YouTube : Espace/K lecture, ←/→ ±5 s, J/L ±10 s,
|
|
61
|
+
↑/↓ volume, M muet, F plein écran, I PiP (vidéo), 0-9 saut à x %,
|
|
62
|
+
Début/Fin. Les éléments interactifs internes gardent leurs touches
|
|
63
|
+
natives (Espace sur un bouton ne double-déclenche pas).
|
|
64
|
+
*/
|
|
65
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
66
|
+
const target = event.target as HTMLElement;
|
|
67
|
+
const interactiveTarget = Boolean(target.closest("button, a, input, textarea, select, [role='slider']"));
|
|
68
|
+
const key = event.key;
|
|
69
|
+
const lowerKey = key.toLowerCase();
|
|
70
|
+
|
|
71
|
+
switch (true) {
|
|
72
|
+
case (key === " " || lowerKey === "k") && !interactiveTarget:
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
controller.actions.togglePlay();
|
|
75
|
+
break;
|
|
76
|
+
case key === "ArrowLeft":
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
event.stopPropagation();
|
|
79
|
+
controller.actions.seekBy(-5);
|
|
80
|
+
break;
|
|
81
|
+
case key === "ArrowRight":
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
event.stopPropagation();
|
|
84
|
+
controller.actions.seekBy(5);
|
|
85
|
+
break;
|
|
86
|
+
case lowerKey === "j":
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
controller.actions.seekBy(-10);
|
|
89
|
+
break;
|
|
90
|
+
case lowerKey === "l":
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
controller.actions.seekBy(10);
|
|
93
|
+
break;
|
|
94
|
+
case key === "ArrowUp":
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
controller.actions.setVolume(controller.state.volume + 0.05);
|
|
97
|
+
break;
|
|
98
|
+
case key === "ArrowDown":
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
controller.actions.setVolume(controller.state.volume - 0.05);
|
|
101
|
+
break;
|
|
102
|
+
case lowerKey === "m":
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
controller.actions.toggleMute();
|
|
105
|
+
break;
|
|
106
|
+
case lowerKey === "f":
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
controller.actions.toggleFullscreen();
|
|
109
|
+
break;
|
|
110
|
+
case lowerKey === "i" && variant === "video":
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
void controller.actions.togglePictureInPicture();
|
|
113
|
+
break;
|
|
114
|
+
case /^[0-9]$/.test(key) && !controller.state.isLive:
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
controller.actions.jumpToFraction(Number(key) / 10);
|
|
117
|
+
break;
|
|
118
|
+
case key === "Home":
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
controller.actions.seekTo(0);
|
|
121
|
+
break;
|
|
122
|
+
case key === "End" && !controller.state.isLive:
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
controller.actions.seekTo(controller.state.duration);
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<MediaPlayerProvider value={{controller, variant, accentClassName, label}}>
|
|
133
|
+
<div
|
|
134
|
+
ref={containerRef}
|
|
135
|
+
id={id}
|
|
136
|
+
role="group"
|
|
137
|
+
aria-label={label ?? "Lecteur multimédia"}
|
|
138
|
+
tabIndex={-1}
|
|
139
|
+
data-slot="media-player"
|
|
140
|
+
data-variant={variant}
|
|
141
|
+
className={cn(
|
|
142
|
+
"outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
143
|
+
variant === "video" ? "group/player relative overflow-hidden bg-black" : "w-full",
|
|
144
|
+
className,
|
|
145
|
+
)}
|
|
146
|
+
onKeyDown={handleKeyDown}
|
|
147
|
+
>
|
|
148
|
+
{variant === "voice-note" && <VoiceNotePlayer/>}
|
|
149
|
+
{variant === "audio" && <AudioPlayer cover={cover}/>}
|
|
150
|
+
{variant === "video" && (
|
|
151
|
+
<VideoPlayer>
|
|
152
|
+
<video
|
|
153
|
+
ref={mediaRef as React.RefObject<HTMLVideoElement>}
|
|
154
|
+
poster={cover}
|
|
155
|
+
preload="metadata"
|
|
156
|
+
playsInline
|
|
157
|
+
className="size-full"
|
|
158
|
+
/>
|
|
159
|
+
</VideoPlayer>
|
|
160
|
+
)}
|
|
161
|
+
{variant !== "video" && (
|
|
162
|
+
<audio ref={mediaRef} preload="metadata" className="hidden"/>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</MediaPlayerProvider>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {Volume1Icon, Volume2Icon, VolumeXIcon} from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import {cn} from "../../../infrastructure/utilities/utils";
|
|
7
|
+
import {Slider} from "../slider";
|
|
8
|
+
import {usePlayer} from "./media-player.context";
|
|
9
|
+
|
|
10
|
+
/** Formate une durée en secondes : « 3:05 » ou « 1:02:11 ». */
|
|
11
|
+
export function formatMediaTime(totalSeconds: number): string {
|
|
12
|
+
if (!Number.isFinite(totalSeconds) || totalSeconds < 0) return "0:00";
|
|
13
|
+
const seconds = Math.floor(totalSeconds % 60);
|
|
14
|
+
const minutes = Math.floor((totalSeconds / 60) % 60);
|
|
15
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
16
|
+
const twoDigits = String(seconds).padStart(2, "0");
|
|
17
|
+
return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${twoDigits}` : `${minutes}:${twoDigits}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PlayerScrubberProps {
|
|
21
|
+
className?: string;
|
|
22
|
+
/** Épaisseur de la piste au repos ; grossit au survol/focus. */
|
|
23
|
+
thick?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
Barre de progression accessible (role="slider", ARIA complet) avec affichage
|
|
28
|
+
du tampon et poignée visible au survol — inspirée de la barre YouTube.
|
|
29
|
+
Le clavier (←/→/Home/End/0-9) est géré par le conteneur racine du lecteur.
|
|
30
|
+
*/
|
|
31
|
+
export function PlayerScrubber({className, thick = false}: PlayerScrubberProps) {
|
|
32
|
+
const {controller} = usePlayer();
|
|
33
|
+
const {currentTime, bufferedEnd, seekMin, seekMax, isLive, duration} = controller.state;
|
|
34
|
+
const {seekTo} = controller.actions;
|
|
35
|
+
const trackRef = React.useRef<HTMLDivElement | null>(null);
|
|
36
|
+
const [dragging, setDragging] = React.useState(false);
|
|
37
|
+
|
|
38
|
+
const span = Math.max(seekMax - seekMin, 0.001);
|
|
39
|
+
const fraction = isLive && !seekWindowUsable(seekMin, seekMax)
|
|
40
|
+
? 1
|
|
41
|
+
: Math.min(Math.max((currentTime - seekMin) / span, 0), 1);
|
|
42
|
+
const bufferedFraction = Number.isFinite(bufferedEnd) ? Math.min(Math.max((bufferedEnd - seekMin) / span, 0), 1) : 0;
|
|
43
|
+
|
|
44
|
+
const seekFromPointer = (clientX: number) => {
|
|
45
|
+
const track = trackRef.current;
|
|
46
|
+
if (!track) return;
|
|
47
|
+
const rect = track.getBoundingClientRect();
|
|
48
|
+
const ratio = rect.width > 0 ? Math.min(Math.max((clientX - rect.left) / rect.width, 0), 1) : 0;
|
|
49
|
+
seekTo(seekMin + ratio * span);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Direct sans fenêtre DVR : la barre est purement indicative.
|
|
53
|
+
const disabled = isLive && !seekWindowUsable(seekMin, seekMax);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
ref={trackRef}
|
|
58
|
+
role="slider"
|
|
59
|
+
tabIndex={disabled ? -1 : 0}
|
|
60
|
+
aria-label={isLive ? "Position dans le direct" : "Barre de progression"}
|
|
61
|
+
aria-valuemin={seekMin}
|
|
62
|
+
aria-valuemax={Math.round(seekMax)}
|
|
63
|
+
aria-valuenow={Math.round(currentTime)}
|
|
64
|
+
aria-valuetext={`${formatMediaTime(currentTime)} sur ${formatMediaTime(isLive ? seekMax : duration || seekMax)}`}
|
|
65
|
+
aria-disabled={disabled}
|
|
66
|
+
data-slot="player-scrubber"
|
|
67
|
+
className={cn(
|
|
68
|
+
"group/scrubber relative flex touch-none items-center py-2 select-none",
|
|
69
|
+
disabled ? "cursor-default" : "cursor-pointer",
|
|
70
|
+
className,
|
|
71
|
+
)}
|
|
72
|
+
onPointerDown={(event) => {
|
|
73
|
+
if (disabled) return;
|
|
74
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
75
|
+
setDragging(true);
|
|
76
|
+
seekFromPointer(event.clientX);
|
|
77
|
+
}}
|
|
78
|
+
onPointerMove={(event) => {
|
|
79
|
+
if (dragging) seekFromPointer(event.clientX);
|
|
80
|
+
}}
|
|
81
|
+
onPointerUp={() => setDragging(false)}
|
|
82
|
+
onPointerCancel={() => setDragging(false)}
|
|
83
|
+
>
|
|
84
|
+
<div className={cn(
|
|
85
|
+
"relative w-full overflow-hidden rounded-full bg-muted transition-[height]",
|
|
86
|
+
thick ? "h-1.5" : "h-1 group-hover/scrubber:h-1.5 group-focus-within/scrubber:h-1.5",
|
|
87
|
+
)}>
|
|
88
|
+
<div
|
|
89
|
+
data-slot="player-buffered"
|
|
90
|
+
className="absolute inset-y-0 left-0 rounded-full bg-muted-foreground/30"
|
|
91
|
+
style={{width: `${bufferedFraction * 100}%`}}
|
|
92
|
+
/>
|
|
93
|
+
<div
|
|
94
|
+
data-slot="player-played"
|
|
95
|
+
className={cn("absolute inset-y-0 left-0 rounded-full bg-primary", dragging && "bg-primary")}
|
|
96
|
+
style={{width: `${fraction * 100}%`}}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
{!disabled && (
|
|
100
|
+
<span
|
|
101
|
+
aria-hidden
|
|
102
|
+
data-slot="player-thumb"
|
|
103
|
+
className={cn(
|
|
104
|
+
"absolute top-1/2 size-3.5 -translate-x-1/2 -translate-y-1/2 scale-0 rounded-full bg-primary shadow transition-transform",
|
|
105
|
+
"group-hover/scrubber:scale-100 group-focus-within/scrubber:scale-100",
|
|
106
|
+
dragging && "scale-125",
|
|
107
|
+
)}
|
|
108
|
+
style={{left: `${fraction * 100}%`}}
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function seekWindowUsable(start: number, end: number): boolean {
|
|
116
|
+
return Number.isFinite(start) && Number.isFinite(end) && end - start > 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
Volume façon YouTube : bouton muet + curseur horizontal qui se déplie au
|
|
121
|
+
survol. Accessible via le Slider radix (flèches natives) et ↑/↓ globaux.
|
|
122
|
+
*/
|
|
123
|
+
export function PlayerVolumeControl({className}: {className?: string}) {
|
|
124
|
+
const {controller} = usePlayer();
|
|
125
|
+
const {volume, muted} = controller.state;
|
|
126
|
+
const {setVolume, toggleMute} = controller.actions;
|
|
127
|
+
|
|
128
|
+
const effectiveVolume = muted ? 0 : volume;
|
|
129
|
+
const VolumeIcon = effectiveVolume === 0 ? VolumeXIcon : effectiveVolume < 0.5 ? Volume1Icon : Volume2Icon;
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div data-slot="player-volume" className={cn("group/volume flex items-center gap-1", className)}>
|
|
133
|
+
<ButtonGhost
|
|
134
|
+
label={muted ? "Réactiver le son (M)" : "Couper le son (M)"}
|
|
135
|
+
onClick={toggleMute}
|
|
136
|
+
>
|
|
137
|
+
<VolumeIcon/>
|
|
138
|
+
</ButtonGhost>
|
|
139
|
+
<div className="w-0 overflow-hidden opacity-0 transition-all duration-200 group-hover/volume:w-20 group-hover/volume:opacity-100 group-focus-within/volume:w-20 group-focus-within/volume:opacity-100">
|
|
140
|
+
<Slider
|
|
141
|
+
aria-label="Volume"
|
|
142
|
+
value={[effectiveVolume * 100]}
|
|
143
|
+
min={0}
|
|
144
|
+
max={100}
|
|
145
|
+
step={5}
|
|
146
|
+
onValueChange={(values) => setVolume((values[0] ?? 0) / 100)}
|
|
147
|
+
className="h-1 w-20"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Bouton rond générique des barres de contrôle (cohérence entre variantes). */
|
|
155
|
+
export function ButtonGhost({
|
|
156
|
+
label,
|
|
157
|
+
className,
|
|
158
|
+
children,
|
|
159
|
+
...props
|
|
160
|
+
}: React.ComponentProps<"button"> & {label: string}) {
|
|
161
|
+
return (
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
title={label}
|
|
165
|
+
aria-label={label}
|
|
166
|
+
data-slot="player-button"
|
|
167
|
+
className={cn(
|
|
168
|
+
"inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full text-current opacity-90 outline-hidden transition hover:bg-accent hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring",
|
|
169
|
+
className,
|
|
170
|
+
)}
|
|
171
|
+
{...props}
|
|
172
|
+
>
|
|
173
|
+
{children}
|
|
174
|
+
</button>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Badge « EN DIRECT » avec point rouge pulsant (style plateformes live). */
|
|
179
|
+
export function LiveBadge({className}: {className?: string}) {
|
|
180
|
+
return (
|
|
181
|
+
<span
|
|
182
|
+
data-slot="player-live-badge"
|
|
183
|
+
className={cn("inline-flex shrink-0 items-center gap-1.5 text-xs font-semibold tracking-wide text-red-500 uppercase", className)}
|
|
184
|
+
>
|
|
185
|
+
<span className="relative flex size-2">
|
|
186
|
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"/>
|
|
187
|
+
<span className="relative inline-flex size-2 rounded-full bg-red-500"/>
|
|
188
|
+
</span>
|
|
189
|
+
Direct
|
|
190
|
+
</span>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Affichage du temps « 1:23 / 5:00 », remplacé par le badge en direct. */
|
|
195
|
+
export function TimeDisplay({className}: {className?: string}) {
|
|
196
|
+
const {controller} = usePlayer();
|
|
197
|
+
const {currentTime, duration, isLive} = controller.state;
|
|
198
|
+
|
|
199
|
+
if (isLive) return <LiveBadge className={className}/>;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<span data-slot="player-time" className={cn("shrink-0 text-xs tabular-nums select-none", className)}>
|
|
203
|
+
{formatMediaTime(currentTime)}
|
|
204
|
+
<span className="opacity-60"> / {formatMediaTime(duration)}</span>
|
|
205
|
+
</span>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import {PauseIcon, PlayIcon} from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import {cn} from "../../../../infrastructure/utilities/utils";
|
|
6
|
+
import {usePlayer} from "../media-player.context";
|
|
7
|
+
import {ButtonGhost, PlayerScrubber, PlayerVolumeControl, TimeDisplay} from "../player-controls";
|
|
8
|
+
|
|
9
|
+
interface AudioPlayerProps {
|
|
10
|
+
/** Pochette optionnelle affichée à gauche (sinon icône par défaut du consommateur). */
|
|
11
|
+
cover?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
Apparence « lecteur audio traditionnel » : pochette, transport, barre de
|
|
16
|
+
progression avec tampon, temps, volume dépliable et vitesse — la même grammaire
|
|
17
|
+
visuelle que les variantes voice-note et video.
|
|
18
|
+
*/
|
|
19
|
+
export function AudioPlayer({cover}: AudioPlayerProps) {
|
|
20
|
+
const {controller, accentClassName, label} = usePlayer();
|
|
21
|
+
const {isPlaying, isLoading, duration} = controller.state;
|
|
22
|
+
const {togglePlay} = controller.actions;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div data-slot="audio-player" className="flex items-center gap-3 rounded-xl border bg-card p-3">
|
|
26
|
+
{/* Pochette */}
|
|
27
|
+
<div className="relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-muted-foreground">
|
|
28
|
+
{cover ? (
|
|
29
|
+
<img src={cover} alt="" className="size-full object-cover"/>
|
|
30
|
+
) : (
|
|
31
|
+
<span className={cn(
|
|
32
|
+
"flex items-center justify-center transition-colors",
|
|
33
|
+
isPlaying && cn("text-primary", accentClassName),
|
|
34
|
+
)}>
|
|
35
|
+
<AudioGlyph/>
|
|
36
|
+
</span>
|
|
37
|
+
)}
|
|
38
|
+
{isLoading && isPlaying && (
|
|
39
|
+
<span aria-hidden className="absolute inset-0 flex items-center justify-center bg-background/60">
|
|
40
|
+
<span className="size-4 animate-spin rounded-full border-2 border-current border-t-transparent"/>
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{/* Transport + progression */}
|
|
46
|
+
<div className="min-w-0 flex-1">
|
|
47
|
+
{label && (
|
|
48
|
+
<p className="mb-0.5 truncate text-xs font-medium" title={label}>{label}</p>
|
|
49
|
+
)}
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
<ButtonGhost
|
|
52
|
+
label={isPlaying ? "Pause (Espace)" : "Lire (Espace)"}
|
|
53
|
+
onClick={togglePlay}
|
|
54
|
+
className={cn("size-9 shrink-0 bg-primary/10 hover:bg-primary/20", accentClassName)}
|
|
55
|
+
>
|
|
56
|
+
{isPlaying ? <PauseIcon className="size-4 fill-current"/> : <PlayIcon className="size-4 fill-current"/>}
|
|
57
|
+
</ButtonGhost>
|
|
58
|
+
<div className="min-w-0 flex-1">
|
|
59
|
+
<PlayerScrubber thick/>
|
|
60
|
+
</div>
|
|
61
|
+
<TimeDisplay className="shrink-0 text-xs tabular-nums text-muted-foreground"/>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="mt-0.5 hidden items-center justify-end gap-2 sm:flex">
|
|
64
|
+
<PlayerVolumeControl/>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Petit glyphe « onde sonore » pour la pochette par défaut. */
|
|
72
|
+
function AudioGlyph() {
|
|
73
|
+
return (
|
|
74
|
+
<svg viewBox="0 0 24 24" className="size-5 fill-none stroke-current stroke-2" aria-hidden>
|
|
75
|
+
<path d="M4 10v4M8 7v10M12 4v16M16 7v10M20 10v4" strokeLinecap="round"/>
|
|
76
|
+
</svg>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {MaximizeIcon, MinimizeIcon, PauseIcon, PictureInPicture2Icon, PlayIcon, SettingsIcon} from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import {cn} from "../../../../infrastructure/utilities/utils";
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuLabel,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "../../dropdown-menu";
|
|
14
|
+
import {usePlayer} from "../media-player.context";
|
|
15
|
+
import {ButtonGhost, PlayerScrubber, PlayerVolumeControl, TimeDisplay} from "../player-controls";
|
|
16
|
+
|
|
17
|
+
interface VideoPlayerProps {
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Délai avant masquage automatique des contrôles en lecture (YouTube ≈ 3 s). */
|
|
22
|
+
const CONTROLS_HIDE_DELAY_MS = 2700;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
Apparence « lecteur vidéo » inspirée de YouTube : vidéo plein cadre, grand
|
|
26
|
+
bouton central au repos, barre de progression fine au-dessus des contrôles,
|
|
27
|
+
contrôles sur dégradé qui se masquent automatiquement en lecture,
|
|
28
|
+
vitesse dans un menu, Picture-in-Picture et plein écran (double-clic).
|
|
29
|
+
*/
|
|
30
|
+
export function VideoPlayer({children}: VideoPlayerProps) {
|
|
31
|
+
const {controller} = usePlayer();
|
|
32
|
+
const {isPlaying, isLoading, isFullscreen, isLive, error, rate} = controller.state;
|
|
33
|
+
const {togglePlay, toggleFullscreen, togglePictureInPicture, setRate} = controller.actions;
|
|
34
|
+
|
|
35
|
+
const [controlsVisible, setControlsVisible] = React.useState(true);
|
|
36
|
+
const hideTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
37
|
+
const clickTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
38
|
+
|
|
39
|
+
/** Reprogramme le masquage : contrôles toujours visibles en pause. */
|
|
40
|
+
const scheduleHide = React.useCallback(() => {
|
|
41
|
+
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
|
|
42
|
+
setControlsVisible(true);
|
|
43
|
+
hideTimerRef.current = setTimeout(() => {
|
|
44
|
+
if (controller.state.isPlaying) setControlsVisible(false);
|
|
45
|
+
}, CONTROLS_HIDE_DELAY_MS);
|
|
46
|
+
}, [controller.state.isPlaying]);
|
|
47
|
+
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
if (!isPlaying) {
|
|
50
|
+
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
|
|
51
|
+
setControlsVisible(true);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
scheduleHide();
|
|
55
|
+
return () => {
|
|
56
|
+
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
|
|
57
|
+
};
|
|
58
|
+
}, [isPlaying, scheduleHide]);
|
|
59
|
+
|
|
60
|
+
// Clic simple = lecture/pause ; double-clic = plein écran (désambiguïsé).
|
|
61
|
+
const handleSurfaceClick = () => {
|
|
62
|
+
if (clickTimerRef.current) {
|
|
63
|
+
clearTimeout(clickTimerRef.current);
|
|
64
|
+
clickTimerRef.current = null;
|
|
65
|
+
toggleFullscreen();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
clickTimerRef.current = setTimeout(() => {
|
|
69
|
+
clickTimerRef.current = null;
|
|
70
|
+
togglePlay();
|
|
71
|
+
}, 220);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
data-slot="video-surface"
|
|
77
|
+
className={cn(
|
|
78
|
+
"group/video relative size-full select-none",
|
|
79
|
+
!controlsVisible && isPlaying && "cursor-none",
|
|
80
|
+
)}
|
|
81
|
+
onPointerMove={scheduleHide}
|
|
82
|
+
onPointerLeave={() => {
|
|
83
|
+
if (isPlaying) setControlsVisible(false);
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{children}
|
|
87
|
+
|
|
88
|
+
{/* Clic / double-clic sur la surface */}
|
|
89
|
+
<div aria-hidden className="absolute inset-0" onClick={handleSurfaceClick}/>
|
|
90
|
+
|
|
91
|
+
{/* Grand bouton central au repos */}
|
|
92
|
+
{!isPlaying && (
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
title="Lire (Espace)"
|
|
96
|
+
aria-label="Lire"
|
|
97
|
+
onClick={() => {
|
|
98
|
+
if (clickTimerRef.current) {
|
|
99
|
+
clearTimeout(clickTimerRef.current);
|
|
100
|
+
clickTimerRef.current = null;
|
|
101
|
+
}
|
|
102
|
+
togglePlay();
|
|
103
|
+
}}
|
|
104
|
+
className="absolute inset-0 z-10 flex cursor-pointer items-center justify-center outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
|
|
105
|
+
>
|
|
106
|
+
<span className="flex size-16 items-center justify-center rounded-full bg-black/60 text-white backdrop-blur transition hover:scale-105 hover:bg-black/70">
|
|
107
|
+
<PlayIcon className="size-8 fill-current"/>
|
|
108
|
+
</span>
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{/* Tampon de chargement */}
|
|
113
|
+
{isLoading && isPlaying && (
|
|
114
|
+
<div aria-hidden className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center">
|
|
115
|
+
<span className="size-10 animate-spin rounded-full border-[3px] border-white/30 border-t-white"/>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{error && (
|
|
120
|
+
<div className="absolute inset-x-0 top-3 z-20 flex justify-center">
|
|
121
|
+
<span className="rounded-md bg-red-600/90 px-3 py-1 text-xs font-medium text-white">{error}</span>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{/* Dégradé + barre de progression + contrôles */}
|
|
126
|
+
<div
|
|
127
|
+
data-slot="video-controls"
|
|
128
|
+
className={cn(
|
|
129
|
+
"absolute inset-x-0 bottom-0 z-10 bg-gradient-to-t from-black/80 via-black/40 to-transparent px-3 pt-8 pb-1.5 text-white transition-all duration-200",
|
|
130
|
+
controlsVisible ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-2 opacity-0",
|
|
131
|
+
)}
|
|
132
|
+
>
|
|
133
|
+
<PlayerScrubber className="text-white [&_[data-slot=player-buffered]]:bg-white/30"/>
|
|
134
|
+
|
|
135
|
+
<div className="flex items-center gap-1">
|
|
136
|
+
<ButtonGhost
|
|
137
|
+
label={isPlaying ? "Pause (Espace)" : "Lire (Espace)"}
|
|
138
|
+
onClick={togglePlay}
|
|
139
|
+
className="text-white hover:bg-white/15"
|
|
140
|
+
>
|
|
141
|
+
{isPlaying ? <PauseIcon className="size-5 fill-current"/> : <PlayIcon className="size-5 fill-current"/>}
|
|
142
|
+
</ButtonGhost>
|
|
143
|
+
|
|
144
|
+
<PlayerVolumeControl className="text-white [&_button]:hover:bg-white/15"/>
|
|
145
|
+
|
|
146
|
+
<TimeDisplay className="text-white/90"/>
|
|
147
|
+
|
|
148
|
+
<span className="flex-1"/>
|
|
149
|
+
|
|
150
|
+
{/* Vitesse de lecture */}
|
|
151
|
+
<DropdownMenu>
|
|
152
|
+
<DropdownMenuTrigger asChild>
|
|
153
|
+
<ButtonGhost label="Paramètres de lecture" className="text-white hover:bg-white/15">
|
|
154
|
+
<SettingsIcon className="size-4"/>
|
|
155
|
+
</ButtonGhost>
|
|
156
|
+
</DropdownMenuTrigger>
|
|
157
|
+
<DropdownMenuContent align="end" side="top" sideOffset={8}>
|
|
158
|
+
<DropdownMenuLabel>Vitesse de lecture</DropdownMenuLabel>
|
|
159
|
+
{[0.5, 0.75, 1, 1.25, 1.5, 1.75, 2].map((value) => (
|
|
160
|
+
<DropdownMenuItem
|
|
161
|
+
key={value}
|
|
162
|
+
onSelect={() => setRate(value)}
|
|
163
|
+
className={cn("justify-between", value === rate && "font-semibold")}
|
|
164
|
+
>
|
|
165
|
+
{String(value).replace(".", ",")}×
|
|
166
|
+
{value === rate && <span aria-hidden>✓</span>}
|
|
167
|
+
</DropdownMenuItem>
|
|
168
|
+
))}
|
|
169
|
+
</DropdownMenuContent>
|
|
170
|
+
</DropdownMenu>
|
|
171
|
+
|
|
172
|
+
{!isLive && (
|
|
173
|
+
<ButtonGhost
|
|
174
|
+
label="Picture-in-Picture (I)"
|
|
175
|
+
onClick={() => void togglePictureInPicture()}
|
|
176
|
+
className="hidden text-white hover:bg-white/15 sm:inline-flex"
|
|
177
|
+
>
|
|
178
|
+
<PictureInPicture2Icon className="size-4"/>
|
|
179
|
+
</ButtonGhost>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
<ButtonGhost
|
|
183
|
+
label={isFullscreen ? "Quitter le plein écran (F)" : "Plein écran (F)"}
|
|
184
|
+
onClick={toggleFullscreen}
|
|
185
|
+
className="text-white hover:bg-white/15"
|
|
186
|
+
>
|
|
187
|
+
{isFullscreen ? <MinimizeIcon className="size-4"/> : <MaximizeIcon className="size-4"/>}
|
|
188
|
+
</ButtonGhost>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|