@mazees/mark 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +209 -0
- package/bin/mark.js +27 -0
- package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
- package/out/renderer/assets/index-G2fADWO-.js +4768 -0
- package/out/renderer/assets/index-f3mVse3c.css +1 -0
- package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
- package/out/renderer/favicon.ico +0 -0
- package/out/renderer/icon-256.png +0 -0
- package/out/renderer/icon.svg +1 -0
- package/out/renderer/index.html +25 -0
- package/out/renderer/manifest.json +25 -0
- package/out/renderer/robot-simulator.html +1640 -0
- package/package.json +106 -0
- package/src/cli/index.js +332 -0
- package/src/cli/monitor.js +413 -0
- package/src/cli/theme.js +347 -0
- package/src/launcher/MarkTray.cs +227 -0
- package/src/main/browser-agent.js +1132 -0
- package/src/main/git-service.js +90 -0
- package/src/main/google/google-calendar.js +69 -0
- package/src/main/google/google-drive.js +263 -0
- package/src/main/google/google-gmail.js +131 -0
- package/src/main/google/google-service.js +241 -0
- package/src/main/node-tools.js +23 -0
- package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
- package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
- package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
- package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
- package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
- package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
- package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
- package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
- package/src/main/plugins/plugin-loader.js +306 -0
- package/src/main/skills/skill-manager.js +265 -0
- package/src/main/syntax-validator.js +455 -0
- package/src/main/task-daemon.js +117 -0
- package/src/main/telegram/telegram-service.js +835 -0
- package/src/main/tools/browser-tools.js +516 -0
- package/src/main/tools/file-tools.js +1126 -0
- package/src/main/tools/git-tools.js +79 -0
- package/src/main/tools/google-tools.js +566 -0
- package/src/main/tools/index.js +36 -0
- package/src/main/tools/media-tools.js +42 -0
- package/src/main/tools/system-tools.js +295 -0
- package/src/main/tools/task-tools.js +73 -0
- package/src/main/tools/telegram-tools.js +101 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/public/favicon.ico +0 -0
- package/src/renderer/public/icon-256.png +0 -0
- package/src/renderer/public/icon.svg +1 -0
- package/src/renderer/public/manifest.json +25 -0
- package/src/renderer/public/robot-simulator.html +1640 -0
- package/src/renderer/src/App.jsx +304 -0
- package/src/renderer/src/api/ai/awareness.js +294 -0
- package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
- package/src/renderer/src/api/ai/contextCompactor.js +129 -0
- package/src/renderer/src/api/ai/contextManager.js +1130 -0
- package/src/renderer/src/api/ai/core.js +167 -0
- package/src/renderer/src/api/ai/index.js +4 -0
- package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
- package/src/renderer/src/api/ai/persona.js +115 -0
- package/src/renderer/src/api/ai/planning.js +317 -0
- package/src/renderer/src/api/ai/relationship.js +177 -0
- package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
- package/src/renderer/src/api/ai/tools.js +167 -0
- package/src/renderer/src/api/ai/utils.js +359 -0
- package/src/renderer/src/api/db.js +1013 -0
- package/src/renderer/src/api/oramaStore.js +592 -0
- package/src/renderer/src/api/ragPipeline.js +87 -0
- package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
- package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
- package/src/renderer/src/api/subagent/subagentStore.js +173 -0
- package/src/renderer/src/api/taskExecutor.js +18 -0
- package/src/renderer/src/api/taskStore.js +315 -0
- package/src/renderer/src/api/tools/core-tools.js +707 -0
- package/src/renderer/src/api/tools/index.js +151 -0
- package/src/renderer/src/api/turnPairMigrator.js +200 -0
- package/src/renderer/src/api/vectorMemory.js +136 -0
- package/src/renderer/src/api/wakeWord.js +101 -0
- package/src/renderer/src/api/web-bridge.js +885 -0
- package/src/renderer/src/api/webSpeech.js +178 -0
- package/src/renderer/src/api/workspaceRag.js +76 -0
- package/src/renderer/src/assets/electron.svg +10 -0
- package/src/renderer/src/assets/icon.svg +1 -0
- package/src/renderer/src/assets/main.css +589 -0
- package/src/renderer/src/assets/music-cover.png +0 -0
- package/src/renderer/src/assets/wavy-lines.svg +25 -0
- package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
- package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
- package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
- package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
- package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
- package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
- package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
- package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
- package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
- package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
- package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
- package/src/renderer/src/components/Chat/index.js +11 -0
- package/src/renderer/src/components/ChatList.jsx +434 -0
- package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
- package/src/renderer/src/components/Versions.jsx +15 -0
- package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
- package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
- package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
- package/src/renderer/src/components/config/CameraSection.jsx +100 -0
- package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
- package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
- package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
- package/src/renderer/src/components/config/index.js +5 -0
- package/src/renderer/src/components/core/Avatar.jsx +2197 -0
- package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
- package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
- package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
- package/src/renderer/src/components/core/HoloCard.jsx +64 -0
- package/src/renderer/src/components/core/InputBar.jsx +1346 -0
- package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
- package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
- package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
- package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
- package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
- package/src/renderer/src/components/core/native-skills.js +43 -0
- package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
- package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
- package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
- package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
- package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
- package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
- package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
- package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
- package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
- package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
- package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
- package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
- package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
- package/src/renderer/src/contexts/ChatContext.jsx +61 -0
- package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
- package/src/renderer/src/data/faqData.js +202 -0
- package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
- package/src/renderer/src/hooks/agent/index.js +4 -0
- package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
- package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
- package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
- package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
- package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
- package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
- package/src/renderer/src/hooks/useAwareness.js +289 -0
- package/src/renderer/src/hooks/useChatArchiver.js +52 -0
- package/src/renderer/src/hooks/useConfirm.jsx +68 -0
- package/src/renderer/src/hooks/useMarkAgent.js +454 -0
- package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
- package/src/renderer/src/hooks/useVAD.js +511 -0
- package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
- package/src/renderer/src/main.jsx +12 -0
- package/src/renderer/src/pages/ChatStudio.jsx +922 -0
- package/src/renderer/src/pages/Configuration.jsx +360 -0
- package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
- package/src/renderer/src/pages/Guidebook.jsx +917 -0
- package/src/renderer/src/pages/Knowledge.jsx +241 -0
- package/src/renderer/src/pages/MarkHome.jsx +491 -0
- package/src/renderer/src/pages/NeuralCore.jsx +786 -0
- package/src/renderer/src/pages/Plugins.jsx +630 -0
- package/src/renderer/src/pages/SkillEditor.jsx +380 -0
- package/src/renderer/src/pages/Skills.jsx +219 -0
- package/src/renderer/src/pages/Subagents.jsx +342 -0
- package/src/renderer/src/pages/TelegramBot.jsx +343 -0
- package/src/renderer/src/utils/chatContentParser.js +203 -0
- package/src/renderer/src/utils/toolClusters.js +201 -0
- package/src/server/bin/sha3_wasm_bg.wasm +0 -0
- package/src/server/config-manager.js +57 -0
- package/src/server/index.js +195 -0
- package/src/server/launcher.js +87 -0
- package/src/server/memory/db-store.js +758 -0
- package/src/server/memory/orama-store.js +177 -0
- package/src/server/memory/vector-engine.js +109 -0
- package/src/server/routes/ai.routes.js +280 -0
- package/src/server/routes/awareness.routes.js +62 -0
- package/src/server/routes/chat.routes.js +61 -0
- package/src/server/routes/config.routes.js +41 -0
- package/src/server/routes/index.js +35 -0
- package/src/server/routes/integrations.routes.js +116 -0
- package/src/server/routes/memory.routes.js +256 -0
- package/src/server/routes/plugins.routes.js +72 -0
- package/src/server/routes/skills.routes.js +139 -0
- package/src/server/routes/subagents.routes.js +70 -0
- package/src/server/routes/tasks.routes.js +175 -0
- package/src/server/routes/workspace.routes.js +127 -0
- package/src/server/services/ai/ai-utils.js +161 -0
- package/src/server/services/ai/openai-provider.js +757 -0
- package/src/server/services/ai/web-provider.js +729 -0
- package/src/server/services/ai-bridge.js +46 -0
- package/src/server/services/deepseek-web.js +956 -0
- package/src/server/services/gemini-web.js +170 -0
- package/src/server/services/notification-service.js +91 -0
- package/src/server/services/updater.js +125 -0
- package/src/server/tools/awareness-tracker.js +292 -0
- package/src/server/tools/group-tools.js +1164 -0
- package/src/server/tools/media-tools.js +244 -0
- package/src/server/tools/pc-agent.js +327 -0
- package/src/server/tools/screen-service.js +51 -0
- package/src/server/ws-hub.js +168 -0
- package/src/shared/parsers/mark-tag-parser.js +275 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { useYoutubeMusic } from '../contexts/YoutubeMusicContext'
|
|
3
|
+
import {
|
|
4
|
+
Play,
|
|
5
|
+
Pause,
|
|
6
|
+
SkipForward,
|
|
7
|
+
SkipBack,
|
|
8
|
+
Volume,
|
|
9
|
+
Volume2,
|
|
10
|
+
VolumeX,
|
|
11
|
+
ListMusic,
|
|
12
|
+
ChevronDown,
|
|
13
|
+
Music,
|
|
14
|
+
ImageIcon,
|
|
15
|
+
Tv,
|
|
16
|
+
X
|
|
17
|
+
} from 'lucide-react'
|
|
18
|
+
|
|
19
|
+
function formatTime(seconds) {
|
|
20
|
+
if (!seconds || isNaN(seconds)) return '0:00'
|
|
21
|
+
const mins = Math.floor(seconds / 60)
|
|
22
|
+
const secs = Math.floor(seconds % 60)
|
|
23
|
+
return `${mins}:${secs < 10 ? '0' : ''}${secs}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const YoutubeMusicPlayer = () => {
|
|
27
|
+
const {
|
|
28
|
+
currentTrack,
|
|
29
|
+
queue,
|
|
30
|
+
isPlaying,
|
|
31
|
+
isMuted,
|
|
32
|
+
volume,
|
|
33
|
+
currentTime,
|
|
34
|
+
duration,
|
|
35
|
+
viewMode,
|
|
36
|
+
isPlayerOpen,
|
|
37
|
+
containerId,
|
|
38
|
+
setIsPlayerOpen,
|
|
39
|
+
togglePlayer,
|
|
40
|
+
setViewMode,
|
|
41
|
+
playTrack,
|
|
42
|
+
playPause,
|
|
43
|
+
nextTrack,
|
|
44
|
+
prevTrack,
|
|
45
|
+
seekTo,
|
|
46
|
+
setVolume,
|
|
47
|
+
toggleMute
|
|
48
|
+
} = useYoutubeMusic()
|
|
49
|
+
|
|
50
|
+
const [isSeeking, setIsSeeking] = useState(false)
|
|
51
|
+
const [seekValue, setSeekValue] = useState(0)
|
|
52
|
+
const [showQueue, setShowQueue] = useState(false)
|
|
53
|
+
|
|
54
|
+
// Web Bridge / IPC command synchronization
|
|
55
|
+
const playTrackRef = useRef(playTrack)
|
|
56
|
+
const nextTrackRef = useRef(nextTrack)
|
|
57
|
+
const prevTrackRef = useRef(prevTrack)
|
|
58
|
+
const playPauseRef = useRef(playPause)
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
playTrackRef.current = playTrack
|
|
62
|
+
nextTrackRef.current = nextTrack
|
|
63
|
+
prevTrackRef.current = prevTrack
|
|
64
|
+
playPauseRef.current = playPause
|
|
65
|
+
}, [playTrack, nextTrack, prevTrack, playPause])
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (window.api?.onExecuteMusicCommand) {
|
|
69
|
+
window.api.onExecuteMusicCommand((command, payload) => {
|
|
70
|
+
if (command === 'play' && payload) {
|
|
71
|
+
if (typeof payload === 'object') {
|
|
72
|
+
playTrackRef.current(payload)
|
|
73
|
+
} else {
|
|
74
|
+
window.api.searchMusic(payload).then((res) => {
|
|
75
|
+
if (res && res.length > 0) {
|
|
76
|
+
playTrackRef.current(res[0], res)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
} else if (command === 'next') nextTrackRef.current()
|
|
81
|
+
else if (command === 'prev') prevTrackRef.current()
|
|
82
|
+
else if (command === 'toggle') playPauseRef.current()
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}, [])
|
|
86
|
+
|
|
87
|
+
const currentSeek = isSeeking ? seekValue : currentTime
|
|
88
|
+
const progressRatio =
|
|
89
|
+
duration > 0 ? Math.min(100, Math.max(0, (currentSeek / duration) * 100)) : 0
|
|
90
|
+
const remainingTime = duration > currentSeek ? duration - currentSeek : 0
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className="fixed bottom-5 right-5 z-120 flex flex-col items-end gap-2 select-none pointer-events-none">
|
|
94
|
+
{/* Player Main Card */}
|
|
95
|
+
<div
|
|
96
|
+
className={`
|
|
97
|
+
transition-all duration-300 ease-out origin-bottom-right
|
|
98
|
+
${
|
|
99
|
+
isPlayerOpen
|
|
100
|
+
? 'opacity-100 scale-100 translate-y-0 pointer-events-auto visible'
|
|
101
|
+
: 'opacity-0 scale-95 translate-y-3 pointer-events-none invisible'
|
|
102
|
+
}
|
|
103
|
+
`}
|
|
104
|
+
style={{ pointerEvents: isPlayerOpen ? 'auto' : 'none' }}
|
|
105
|
+
>
|
|
106
|
+
<div className="relative w-80 sm:w-84 rounded-3xl overflow-hidden shadow-[0_25px_60px_-15px_rgba(0,0,0,0.9)] border border-white/10 bg-[#0d0d0e]/95 backdrop-blur-2xl text-white flex flex-col p-4">
|
|
107
|
+
{/* Subtle Ambient Red Glow Background */}
|
|
108
|
+
{currentTrack.thumbnail && (
|
|
109
|
+
<div
|
|
110
|
+
className="absolute inset-0 bg-cover bg-center blur-3xl opacity-20 pointer-events-none scale-150 transition-opacity duration-700"
|
|
111
|
+
style={{ backgroundImage: `url(${currentTrack.thumbnail})` }}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{/* Top Bar (Dismiss handle & View Mode Toggle) */}
|
|
116
|
+
<div className="relative z-10 flex items-center justify-between pb-3">
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => setIsPlayerOpen(false)}
|
|
119
|
+
className="w-8 h-8 rounded-full flex items-center justify-center text-zinc-400 hover:text-white hover:bg-white/10 active:scale-95 transition-all cursor-pointer"
|
|
120
|
+
title="Tutup Player"
|
|
121
|
+
>
|
|
122
|
+
<ChevronDown size={18} />
|
|
123
|
+
</button>
|
|
124
|
+
|
|
125
|
+
{/* Video vs Cover Pill Switcher */}
|
|
126
|
+
<div className="flex items-center bg-white/10 p-0.5 rounded-full border border-white/5 backdrop-blur-md">
|
|
127
|
+
<button
|
|
128
|
+
onClick={() => setViewMode('thumbnail')}
|
|
129
|
+
className={`px-2.5 py-1 rounded-full text-[11px] font-medium transition-all flex items-center gap-1.5 cursor-pointer ${
|
|
130
|
+
viewMode === 'thumbnail'
|
|
131
|
+
? 'bg-red-600 text-white shadow-sm'
|
|
132
|
+
: 'text-zinc-400 hover:text-zinc-200'
|
|
133
|
+
}`}
|
|
134
|
+
>
|
|
135
|
+
<ImageIcon size={12} />
|
|
136
|
+
<span>Cover</span>
|
|
137
|
+
</button>
|
|
138
|
+
<button
|
|
139
|
+
onClick={() => setViewMode('video')}
|
|
140
|
+
className={`px-2.5 py-1 rounded-full text-[11px] font-medium transition-all flex items-center gap-1.5 cursor-pointer ${
|
|
141
|
+
viewMode === 'video'
|
|
142
|
+
? 'bg-red-600 text-white shadow-sm'
|
|
143
|
+
: 'text-zinc-400 hover:text-zinc-200'
|
|
144
|
+
}`}
|
|
145
|
+
>
|
|
146
|
+
<Tv size={12} />
|
|
147
|
+
<span>Video</span>
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Queue Toggle Button */}
|
|
152
|
+
<button
|
|
153
|
+
onClick={() => setShowQueue(!showQueue)}
|
|
154
|
+
className={`w-8 h-8 rounded-full flex items-center justify-center transition-all cursor-pointer ${
|
|
155
|
+
showQueue
|
|
156
|
+
? 'bg-red-600 text-white shadow-sm shadow-red-600/50'
|
|
157
|
+
: 'text-zinc-400 hover:text-white hover:bg-white/10'
|
|
158
|
+
}`}
|
|
159
|
+
title="Daftar Antrean"
|
|
160
|
+
>
|
|
161
|
+
<ListMusic size={17} />
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Media Display Screen (Artwork / Single Unified Embedded Video) */}
|
|
166
|
+
<div className="relative z-10 w-full aspect-square rounded-2xl overflow-hidden shadow-2xl shadow-black/80 bg-black/60 border border-white/5 group my-1 flex items-center justify-center">
|
|
167
|
+
{/* SATU-SATUNYA IFRAME PLAYER ASLI (Selalu ter-mount, hanya opacity/z-index yang berganti) */}
|
|
168
|
+
<div
|
|
169
|
+
className={`absolute inset-0 w-full h-full transition-opacity duration-200 ${
|
|
170
|
+
isPlayerOpen && viewMode === 'video'
|
|
171
|
+
? 'opacity-100 pointer-events-auto z-20'
|
|
172
|
+
: 'opacity-0 pointer-events-none z-0'
|
|
173
|
+
}`}
|
|
174
|
+
>
|
|
175
|
+
<div
|
|
176
|
+
id={containerId}
|
|
177
|
+
className="w-full h-full"
|
|
178
|
+
style={{ pointerEvents: isPlayerOpen && viewMode === 'video' ? 'auto' : 'none' }}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Cover / Artwork View (Tampil saat viewMode === 'thumbnail') */}
|
|
183
|
+
<div
|
|
184
|
+
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-200 ${
|
|
185
|
+
viewMode === 'thumbnail'
|
|
186
|
+
? 'opacity-100 pointer-events-auto z-10'
|
|
187
|
+
: 'opacity-0 pointer-events-none z-0'
|
|
188
|
+
}`}
|
|
189
|
+
>
|
|
190
|
+
{currentTrack.thumbnail ? (
|
|
191
|
+
<>
|
|
192
|
+
<img
|
|
193
|
+
src={currentTrack.thumbnail}
|
|
194
|
+
alt={currentTrack.title}
|
|
195
|
+
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
|
196
|
+
/>
|
|
197
|
+
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-transparent to-black/20 pointer-events-none" />
|
|
198
|
+
</>
|
|
199
|
+
) : (
|
|
200
|
+
<div className="flex flex-col items-center gap-2 text-zinc-600">
|
|
201
|
+
<Music size={40} strokeWidth={1.5} />
|
|
202
|
+
<span className="text-xs font-mono">Belum ada lagu</span>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{/* Soundwave Animation Badge */}
|
|
207
|
+
{isPlaying && (
|
|
208
|
+
<div className="absolute bottom-3 right-3 flex items-end gap-0.5 px-2 py-1.5 rounded-full bg-black/60 backdrop-blur-md border border-white/10 shadow-lg">
|
|
209
|
+
<span className="w-0.5 bg-red-500 rounded-full animate-[pulse_0.6s_ease-in-out_infinite] h-2.5" />
|
|
210
|
+
<span className="w-0.5 bg-red-500 rounded-full animate-[pulse_0.9s_ease-in-out_infinite] h-4" />
|
|
211
|
+
<span className="w-0.5 bg-red-500 rounded-full animate-[pulse_0.4s_ease-in-out_infinite] h-2" />
|
|
212
|
+
<span className="w-0.5 bg-red-500 rounded-full animate-[pulse_0.7s_ease-in-out_infinite] h-3.5" />
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Queue List Overlay Panel */}
|
|
219
|
+
{showQueue && queue && queue.length > 0 && (
|
|
220
|
+
<div className="relative z-20 my-2 max-h-36 overflow-y-auto no-scrollbar rounded-2xl bg-black/80 border border-white/10 p-1.5 flex flex-col gap-0.5 text-xs backdrop-blur-xl">
|
|
221
|
+
{queue.map((item, idx) => {
|
|
222
|
+
const isCurrent = (item.videoId || item.id) === currentTrack.videoId
|
|
223
|
+
return (
|
|
224
|
+
<div
|
|
225
|
+
key={idx}
|
|
226
|
+
onClick={() => playTrack(item)}
|
|
227
|
+
className={`flex items-center justify-between px-3 py-2 rounded-xl cursor-pointer transition-colors ${
|
|
228
|
+
isCurrent
|
|
229
|
+
? 'bg-red-600/20 text-red-400 font-medium'
|
|
230
|
+
: 'hover:bg-white/5 text-zinc-400 hover:text-zinc-200'
|
|
231
|
+
}`}
|
|
232
|
+
>
|
|
233
|
+
<div className="flex items-center gap-2.5 truncate">
|
|
234
|
+
<span className="text-[11px] text-zinc-500 font-mono w-4">{idx + 1}</span>
|
|
235
|
+
<span className="truncate text-xs">{item.title}</span>
|
|
236
|
+
</div>
|
|
237
|
+
<span className="text-[10px] text-zinc-500 font-mono shrink-0 ml-2">
|
|
238
|
+
{item.timestamp || ''}
|
|
239
|
+
</span>
|
|
240
|
+
</div>
|
|
241
|
+
)
|
|
242
|
+
})}
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{/* Song Meta (Title & Artist) */}
|
|
247
|
+
<div className="relative z-10 mt-3 mb-2 flex items-center justify-between">
|
|
248
|
+
<div className="flex flex-col min-w-0 pr-2">
|
|
249
|
+
<span
|
|
250
|
+
className="font-semibold text-base text-white truncate tracking-tight"
|
|
251
|
+
title={currentTrack.title}
|
|
252
|
+
>
|
|
253
|
+
{currentTrack.title || 'Tidak ada lagu aktif'}
|
|
254
|
+
</span>
|
|
255
|
+
<span className="text-xs text-zinc-400 truncate mt-0.5" title={currentTrack.artist}>
|
|
256
|
+
{currentTrack.artist || 'Pilih lagu untuk memulai'}
|
|
257
|
+
</span>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{/* Progress Scrubber (Apple Music Style) */}
|
|
262
|
+
<div className="relative z-10 flex flex-col gap-1 my-1">
|
|
263
|
+
<div className="relative flex items-center group">
|
|
264
|
+
<input
|
|
265
|
+
type="range"
|
|
266
|
+
min={0}
|
|
267
|
+
max={duration || 100}
|
|
268
|
+
value={currentSeek}
|
|
269
|
+
onMouseDown={() => setIsSeeking(true)}
|
|
270
|
+
onChange={(e) => setSeekValue(Number(e.target.value))}
|
|
271
|
+
onMouseUp={(e) => {
|
|
272
|
+
setIsSeeking(false)
|
|
273
|
+
seekTo(Number(e.target.value))
|
|
274
|
+
}}
|
|
275
|
+
className="w-full h-1 bg-white/15 rounded-full appearance-none cursor-pointer accent-red-500 focus:outline-none transition-all group-hover:h-1.5"
|
|
276
|
+
style={{
|
|
277
|
+
background: `linear-gradient(to right, #ef4444 ${progressRatio}%, rgba(255, 255, 255, 0.15) ${progressRatio}%)`
|
|
278
|
+
}}
|
|
279
|
+
/>
|
|
280
|
+
</div>
|
|
281
|
+
<div className="flex justify-between text-[11px] font-mono text-zinc-400">
|
|
282
|
+
<span>{formatTime(currentSeek)}</span>
|
|
283
|
+
<span>{duration > 0 ? `-${formatTime(remainingTime)}` : '0:00'}</span>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{/* Main Playback Controls Cluster */}
|
|
288
|
+
<div className="relative z-10 flex items-center justify-center gap-6 py-2">
|
|
289
|
+
<button
|
|
290
|
+
onClick={prevTrack}
|
|
291
|
+
className="w-10 h-10 rounded-full flex items-center justify-center text-zinc-300 hover:text-white hover:bg-white/5 active:scale-90 transition-all cursor-pointer"
|
|
292
|
+
title="Sebelumnya"
|
|
293
|
+
>
|
|
294
|
+
<SkipBack size={22} fill="currentColor" />
|
|
295
|
+
</button>
|
|
296
|
+
|
|
297
|
+
<button
|
|
298
|
+
onClick={playPause}
|
|
299
|
+
className="w-13 h-13 rounded-full bg-white text-black flex items-center justify-center hover:scale-105 active:scale-95 transition-all shadow-xl shadow-white/10 cursor-pointer"
|
|
300
|
+
title={isPlaying ? 'Pause' : 'Play'}
|
|
301
|
+
>
|
|
302
|
+
{isPlaying ? (
|
|
303
|
+
<Pause size={22} fill="currentColor" />
|
|
304
|
+
) : (
|
|
305
|
+
<Play size={22} fill="currentColor" className="ml-0.5" />
|
|
306
|
+
)}
|
|
307
|
+
</button>
|
|
308
|
+
|
|
309
|
+
<button
|
|
310
|
+
onClick={nextTrack}
|
|
311
|
+
className="w-10 h-10 rounded-full flex items-center justify-center text-zinc-300 hover:text-white hover:bg-white/5 active:scale-90 transition-all cursor-pointer"
|
|
312
|
+
title="Berikutnya"
|
|
313
|
+
>
|
|
314
|
+
<SkipForward size={22} fill="currentColor" />
|
|
315
|
+
</button>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
{/* Volume Control Bar */}
|
|
319
|
+
<div className="relative z-10 flex items-center gap-2 px-1 pt-1 pb-0.5">
|
|
320
|
+
<button
|
|
321
|
+
onClick={toggleMute}
|
|
322
|
+
className="text-zinc-400 hover:text-white transition-colors cursor-pointer"
|
|
323
|
+
title={isMuted ? 'Unmute' : 'Mute'}
|
|
324
|
+
>
|
|
325
|
+
{isMuted || volume === 0 ? <VolumeX size={14} /> : <Volume size={14} />}
|
|
326
|
+
</button>
|
|
327
|
+
|
|
328
|
+
<input
|
|
329
|
+
type="range"
|
|
330
|
+
min="0"
|
|
331
|
+
max="100"
|
|
332
|
+
value={isMuted ? 0 : volume}
|
|
333
|
+
onChange={(e) => setVolume(Number(e.target.value))}
|
|
334
|
+
className="w-full h-1 bg-white/15 rounded-full appearance-none cursor-pointer accent-white focus:outline-none"
|
|
335
|
+
style={{
|
|
336
|
+
background: `linear-gradient(to right, rgba(255, 255, 255, 0.8) ${isMuted ? 0 : volume}%, rgba(255, 255, 255, 0.15) ${isMuted ? 0 : volume}%)`
|
|
337
|
+
}}
|
|
338
|
+
/>
|
|
339
|
+
|
|
340
|
+
<Volume2 size={14} className="text-zinc-400 shrink-0" />
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
{/* Floating Mini Action Button */}
|
|
346
|
+
<button
|
|
347
|
+
onClick={togglePlayer}
|
|
348
|
+
className={`
|
|
349
|
+
group relative w-10 h-10 rounded-full flex items-center justify-center pointer-events-auto
|
|
350
|
+
shadow-lg shadow-black/80 border border-white/10
|
|
351
|
+
transition-all duration-200 ease-out cursor-pointer
|
|
352
|
+
hover:scale-105 active:scale-95
|
|
353
|
+
${
|
|
354
|
+
isPlayerOpen
|
|
355
|
+
? 'bg-white/15 text-white hover:bg-white/20'
|
|
356
|
+
: 'bg-[#121214] text-white hover:border-red-500/40'
|
|
357
|
+
}
|
|
358
|
+
`}
|
|
359
|
+
title={isPlayerOpen ? 'Tutup' : 'YouTube Music'}
|
|
360
|
+
>
|
|
361
|
+
{isPlaying && !isPlayerOpen && (
|
|
362
|
+
<span className="absolute inset-0 rounded-full bg-red-600/30 animate-ping pointer-events-none" />
|
|
363
|
+
)}
|
|
364
|
+
|
|
365
|
+
{isPlayerOpen ? (
|
|
366
|
+
<X size={15} strokeWidth={2} />
|
|
367
|
+
) : (
|
|
368
|
+
<Music size={16} className={isPlaying ? 'text-red-500 animate-pulse' : 'text-zinc-300'} />
|
|
369
|
+
)}
|
|
370
|
+
</button>
|
|
371
|
+
</div>
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export default YoutubeMusicPlayer
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useCallback } from 'react'
|
|
2
|
+
import { Camera } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
export const CameraPreview = ({
|
|
5
|
+
isOpen,
|
|
6
|
+
onCapture,
|
|
7
|
+
onClose,
|
|
8
|
+
deviceId = null,
|
|
9
|
+
countdown = 5,
|
|
10
|
+
isAutonomous = false
|
|
11
|
+
}) => {
|
|
12
|
+
const videoRef = useRef(null)
|
|
13
|
+
const streamRef = useRef(null)
|
|
14
|
+
const canvasRef = useRef(null)
|
|
15
|
+
const [timeLeft, setTimeLeft] = useState(countdown)
|
|
16
|
+
const [error, setError] = useState(null)
|
|
17
|
+
const [isReady, setIsReady] = useState(false)
|
|
18
|
+
const hasCapturedRef = useRef(false)
|
|
19
|
+
|
|
20
|
+
const stopStream = useCallback(() => {
|
|
21
|
+
if (streamRef.current) {
|
|
22
|
+
streamRef.current.getTracks().forEach((track) => track.stop())
|
|
23
|
+
streamRef.current = null
|
|
24
|
+
}
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
const captureFrame = useCallback(() => {
|
|
28
|
+
console.log('[CameraPreview] captureFrame called. hasCaptured:', hasCapturedRef.current, 'video:', !!videoRef.current, 'canvas:', !!canvasRef.current)
|
|
29
|
+
if (hasCapturedRef.current || !videoRef.current || !canvasRef.current) {
|
|
30
|
+
console.log('[CameraPreview] captureFrame aborted!')
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
hasCapturedRef.current = true
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const video = videoRef.current
|
|
37
|
+
const canvas = canvasRef.current
|
|
38
|
+
const ctx = canvas.getContext('2d')
|
|
39
|
+
|
|
40
|
+
// Maintain aspect ratio, max 1280x720
|
|
41
|
+
let width = video.videoWidth || 1280
|
|
42
|
+
let height = video.videoHeight || 720
|
|
43
|
+
|
|
44
|
+
if (width > 1280 || height > 720) {
|
|
45
|
+
const ratio = Math.min(1280 / width, 720 / height)
|
|
46
|
+
width = Math.floor(width * ratio)
|
|
47
|
+
height = Math.floor(height * ratio)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
canvas.width = width
|
|
51
|
+
canvas.height = height
|
|
52
|
+
ctx.drawImage(video, 0, 0, width, height)
|
|
53
|
+
|
|
54
|
+
// Convert to base64 JPEG 70%
|
|
55
|
+
const base64Data = canvas.toDataURL('image/jpeg', 0.7)
|
|
56
|
+
console.log('[CameraPreview] Frame captured, size:', Math.round(base64Data.length / 1024), 'KB')
|
|
57
|
+
stopStream()
|
|
58
|
+
onCapture(base64Data)
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error('[CameraPreview] Error during captureFrame:', e)
|
|
61
|
+
stopStream()
|
|
62
|
+
onCapture(null)
|
|
63
|
+
}
|
|
64
|
+
}, [onCapture, stopStream])
|
|
65
|
+
|
|
66
|
+
// Main effect: open/close camera
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!isOpen) {
|
|
69
|
+
setIsReady(false)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let isMounted = true
|
|
74
|
+
hasCapturedRef.current = false
|
|
75
|
+
setTimeLeft(countdown)
|
|
76
|
+
setError(null)
|
|
77
|
+
setIsReady(false)
|
|
78
|
+
|
|
79
|
+
const initCamera = async () => {
|
|
80
|
+
try {
|
|
81
|
+
console.log('[CameraPreview] Requesting camera access..., deviceId:', deviceId)
|
|
82
|
+
|
|
83
|
+
// Start with simple constraints first, then try specific deviceId
|
|
84
|
+
const constraints = {
|
|
85
|
+
video: deviceId && deviceId !== 'default'
|
|
86
|
+
? { deviceId: { exact: deviceId } }
|
|
87
|
+
: true,
|
|
88
|
+
audio: false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log('[CameraPreview] getUserMedia constraints:', JSON.stringify(constraints))
|
|
92
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints)
|
|
93
|
+
console.log('[CameraPreview] Got stream:', stream.getVideoTracks().length, 'video tracks')
|
|
94
|
+
|
|
95
|
+
if (!isMounted) {
|
|
96
|
+
stream.getTracks().forEach(t => t.stop())
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
streamRef.current = stream
|
|
101
|
+
|
|
102
|
+
if (videoRef.current) {
|
|
103
|
+
videoRef.current.srcObject = stream
|
|
104
|
+
|
|
105
|
+
// Wait for video to actually start playing
|
|
106
|
+
videoRef.current.onloadedmetadata = () => {
|
|
107
|
+
console.log('[CameraPreview] Video metadata loaded, dimensions:', videoRef.current.videoWidth, 'x', videoRef.current.videoHeight)
|
|
108
|
+
if (!isMounted) return
|
|
109
|
+
|
|
110
|
+
videoRef.current.play().then(() => {
|
|
111
|
+
console.log('[CameraPreview] Video playing. isAutonomous prop value:', isAutonomous)
|
|
112
|
+
if (!isMounted) return
|
|
113
|
+
setIsReady(true)
|
|
114
|
+
|
|
115
|
+
if (isAutonomous) {
|
|
116
|
+
// Wait a bit for auto-focus, then snap instantly
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
console.log('[CameraPreview] setTimeout 800ms finished. isMounted:', isMounted, 'hasCaptured:', hasCapturedRef.current)
|
|
119
|
+
if (isMounted && !hasCapturedRef.current) captureFrame()
|
|
120
|
+
}, 800)
|
|
121
|
+
}
|
|
122
|
+
}).catch(err => {
|
|
123
|
+
console.error('[CameraPreview] Play error:', err)
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error('[CameraPreview] Camera access error:', err.name, err.message)
|
|
129
|
+
if (isMounted) {
|
|
130
|
+
setError(err.message)
|
|
131
|
+
stopStream()
|
|
132
|
+
// Delay onClose slightly so error is visible
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
if (isMounted) onClose()
|
|
135
|
+
}, 2000)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
initCamera()
|
|
141
|
+
|
|
142
|
+
return () => {
|
|
143
|
+
isMounted = false
|
|
144
|
+
stopStream()
|
|
145
|
+
}
|
|
146
|
+
}, [isOpen]) // Minimal deps - only re-run when isOpen changes
|
|
147
|
+
|
|
148
|
+
// Countdown timer (only when camera is ready and not autonomous)
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!isOpen || !isReady || isAutonomous || error || hasCapturedRef.current) return
|
|
151
|
+
|
|
152
|
+
if (timeLeft <= 0) {
|
|
153
|
+
captureFrame()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const timer = setTimeout(() => {
|
|
158
|
+
setTimeLeft(prev => prev - 1)
|
|
159
|
+
}, 1000)
|
|
160
|
+
|
|
161
|
+
return () => clearTimeout(timer)
|
|
162
|
+
}, [isOpen, isReady, isAutonomous, timeLeft, error, captureFrame])
|
|
163
|
+
|
|
164
|
+
// Don't render UI if closed or autonomous mode, but keep video for autonomous
|
|
165
|
+
if (!isOpen) return null
|
|
166
|
+
if (isAutonomous) {
|
|
167
|
+
return (
|
|
168
|
+
<div className="fixed bottom-0 right-0 w-1 h-1 opacity-0 pointer-events-none overflow-hidden">
|
|
169
|
+
<video ref={videoRef} autoPlay playsInline muted className="w-full h-full" />
|
|
170
|
+
<canvas ref={canvasRef} className="w-full h-full" />
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<div className="fixed bottom-24 right-6 z-50 animate-[slideUpFadeIn_0.3s_ease-out]">
|
|
177
|
+
<div className="w-[320px] rounded-2xl border border-white/10 bg-base-100/70 backdrop-blur-xl shadow-2xl overflow-hidden flex flex-col">
|
|
178
|
+
{/* Header */}
|
|
179
|
+
<div className="px-4 py-3 flex items-center justify-between border-b border-white/5 bg-base-200/50">
|
|
180
|
+
<div className="flex items-center gap-2">
|
|
181
|
+
<Camera className="w-4 h-4 text-primary" />
|
|
182
|
+
<span className="font-semibold text-sm">Mark Glance</span>
|
|
183
|
+
</div>
|
|
184
|
+
{error ? (
|
|
185
|
+
<div className="badge badge-sm border-0 bg-error/20 text-error font-bold">Error</div>
|
|
186
|
+
) : !isReady ? (
|
|
187
|
+
<span className="loading loading-dots loading-xs text-primary"></span>
|
|
188
|
+
) : (
|
|
189
|
+
<div className={`badge badge-sm border-0 font-bold ${
|
|
190
|
+
timeLeft > 3 ? 'bg-primary/20 text-primary' :
|
|
191
|
+
timeLeft > 1 ? 'bg-warning/20 text-warning' :
|
|
192
|
+
'bg-error/20 text-error'
|
|
193
|
+
}`}>
|
|
194
|
+
{timeLeft}s
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Video Container */}
|
|
200
|
+
<div className="relative bg-black w-full aspect-video">
|
|
201
|
+
{error ? (
|
|
202
|
+
<div className="absolute inset-0 flex items-center justify-center text-error/70 text-xs p-4 text-center">
|
|
203
|
+
⚠️ {error}
|
|
204
|
+
</div>
|
|
205
|
+
) : (
|
|
206
|
+
<>
|
|
207
|
+
<video
|
|
208
|
+
ref={videoRef}
|
|
209
|
+
autoPlay
|
|
210
|
+
playsInline
|
|
211
|
+
muted
|
|
212
|
+
className="w-full h-full object-cover"
|
|
213
|
+
/>
|
|
214
|
+
<canvas ref={canvasRef} className="hidden" />
|
|
215
|
+
</>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{/* Flash Effect on Capture */}
|
|
219
|
+
{timeLeft <= 0 && isReady && !error && (
|
|
220
|
+
<div className="absolute inset-0 bg-white/80 animate-pulse pointer-events-none" />
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{/* Progress Bar */}
|
|
225
|
+
{isReady && !error && (
|
|
226
|
+
<div className="h-1 w-full bg-base-300/50">
|
|
227
|
+
<div
|
|
228
|
+
className="h-full bg-gradient-to-r from-primary to-secondary transition-all duration-1000 ease-linear"
|
|
229
|
+
style={{ width: `${(timeLeft / countdown) * 100}%` }}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
)
|
|
236
|
+
}
|