Skip to content

Commit b91fa9c

Browse files
committed
Add YouTubePlayer and LocalVideoGuide components; update StreamVolumeControl story
1 parent 6e37b9d commit b91fa9c

5 files changed

Lines changed: 718 additions & 929 deletions

File tree

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import React, { useRef, useEffect, useState } from 'react';
2+
import styled from 'styled-components';
3+
import StreamVolumeControl from './StreamVolumeControl';
4+
5+
interface LocalVideoGuideProps {
6+
videoSources: string | string[];
7+
initialVolume?: number;
8+
showPercentage?: boolean;
9+
showGuide?: boolean;
10+
}
11+
12+
13+
/**
14+
* 本地影片
15+
* @param videoSources
16+
* @param initialVolume
17+
* @param showPercentage
18+
* @param showGuide
19+
*/
20+
const LocalVideoGuide: React.FC<LocalVideoGuideProps> = ({
21+
videoSources,
22+
initialVolume = 60,
23+
showPercentage = true,
24+
}) => {
25+
const videoRef = useRef<HTMLVideoElement>(null);
26+
const [isPlayerReady, setIsPlayerReady] = useState(false);
27+
const [hasError, setHasError] = useState(false);
28+
29+
const handleVolumeChange = (volume: number, isMuted: boolean) => {
30+
console.log('本地 Video 音量調整:', { volume, isMuted });
31+
32+
if (!videoRef.current || !isPlayerReady) {
33+
console.warn('本地 Video 播放器尚未準備就緒');
34+
return;
35+
}
36+
37+
try {
38+
if (isMuted) {
39+
videoRef.current.muted = true;
40+
console.log('本地 Video 已靜音');
41+
} else {
42+
videoRef.current.muted = false;
43+
videoRef.current.volume = volume / 100;
44+
console.log(`本地 Video 音量設置為 ${volume}%`);
45+
}
46+
} catch (error) {
47+
console.error('本地 Video 音量控制錯誤:', error);
48+
}
49+
};
50+
51+
const sources = Array.isArray(videoSources) ? videoSources : [videoSources];
52+
53+
return (
54+
<Container>
55+
56+
<PlayerContainer>
57+
<VideoWrapper>
58+
<Video
59+
ref={videoRef}
60+
width="100%"
61+
height="100%"
62+
controls
63+
onLoadedData={() => {
64+
console.log('本地 Video 載入完成');
65+
setIsPlayerReady(true);
66+
setHasError(false);
67+
if (videoRef.current) {
68+
videoRef.current.volume = initialVolume / 100;
69+
}
70+
}}
71+
onError={(e) => {
72+
console.error('本地 Video 載入錯誤:', e);
73+
setHasError(true);
74+
setIsPlayerReady(false);
75+
}}
76+
>
77+
{sources.map((src, index) => (
78+
<source key={index} src={src} type="video/mp4" />
79+
))}
80+
您的瀏覽器不支援 video 標籤。
81+
</Video>
82+
83+
{!hasError && (
84+
<VideoStatusBadge>
85+
📁 本地 MP4 {isPlayerReady ? '播放中' : '載入中'}
86+
</VideoStatusBadge>
87+
)}
88+
89+
{hasError && (
90+
<ErrorOverlay>
91+
<div className="error-icon"></div>
92+
<div className="error-title">影片載入失敗</div>
93+
<div className="error-message">
94+
1. 檢查網路連接<br/>
95+
2. 將 MP4 文件放入 public/ 資料夾<br/>
96+
3. 使用 src="/your-video.mp4"
97+
</div>
98+
</ErrorOverlay>
99+
)}
100+
</VideoWrapper>
101+
102+
<ControlsWrapper>
103+
<ControlsTitle>🎵 本地影片音量控制</ControlsTitle>
104+
105+
<StreamVolumeControl
106+
initialVolume={initialVolume}
107+
showPercentage={showPercentage}
108+
onVolumeChange={handleVolumeChange}
109+
/>
110+
111+
<TipsBox>
112+
<p>💡 使用提示</p>
113+
<p>
114+
調整滑動條控制音量<br/>
115+
點擊音量圖標進行靜音/取消靜音
116+
</p>
117+
</TipsBox>
118+
</ControlsWrapper>
119+
</PlayerContainer>
120+
</Container>
121+
);
122+
};
123+
124+
export default LocalVideoGuide;
125+
126+
127+
128+
129+
const Container = styled.div`
130+
padding: 20px;
131+
`;
132+
133+
const GuideSection = styled.div`
134+
margin-bottom: 20px;
135+
padding: 20px;
136+
background: #f8f9fa;
137+
border-radius: 12px;
138+
border: 2px solid #dee2e6;
139+
`;
140+
141+
const GuideTitle = styled.h4`
142+
margin: 0 0 16px 0;
143+
color: #495057;
144+
`;
145+
146+
const GuideStep = styled.p`
147+
margin: 0 0 8px 0;
148+
font-size: 14px;
149+
color: #6c757d;
150+
151+
&:last-child {
152+
margin: 0;
153+
font-size: 12px;
154+
color: #868e96;
155+
}
156+
157+
code {
158+
background: #e9ecef;
159+
padding: 2px 6px;
160+
border-radius: 4px;
161+
}
162+
`;
163+
164+
const TipBox = styled.div`
165+
padding: 12px;
166+
background: #fff3cd;
167+
border-radius: 6px;
168+
border: 1px solid #ffeaa7;
169+
margin-bottom: 8px;
170+
171+
p {
172+
margin: 0;
173+
font-size: 12px;
174+
color: #856404;
175+
}
176+
`;
177+
178+
const ExampleBox = styled.div`
179+
padding: 12px;
180+
background: #d1ecf1;
181+
border-radius: 6px;
182+
border: 1px solid #bee5eb;
183+
184+
p {
185+
margin: 0 0 6px 0;
186+
font-size: 12px;
187+
color: #0c5460;
188+
font-weight: 500;
189+
190+
&:last-child {
191+
margin: 0;
192+
}
193+
}
194+
195+
code {
196+
display: block;
197+
background: #e9ecef;
198+
padding: 8px;
199+
border-radius: 4px;
200+
font-size: 11px;
201+
color: #495057;
202+
}
203+
`;
204+
205+
const PlayerContainer = styled.div`
206+
display: flex;
207+
gap: 20px;
208+
align-items: flex-start;
209+
padding: 20px;
210+
background: #f8f9fa;
211+
border-radius: 16px;
212+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
213+
`;
214+
215+
const VideoWrapper = styled.div`
216+
flex: 1;
217+
background: #000;
218+
border-radius: 12px;
219+
overflow: hidden;
220+
position: relative;
221+
aspect-ratio: 16/9;
222+
max-height: 500px;
223+
`;
224+
225+
const Video = styled.video`
226+
width: 100%;
227+
height: 100%;
228+
object-fit: cover;
229+
`;
230+
231+
const VideoStatusBadge = styled.div`
232+
position: absolute;
233+
top: 10px;
234+
left: 10px;
235+
background: rgba(0, 0, 0, 0.8);
236+
color: white;
237+
padding: 6px 12px;
238+
border-radius: 6px;
239+
font-size: 14px;
240+
font-weight: 500;
241+
z-index: 10;
242+
`;
243+
244+
const ErrorOverlay = styled.div`
245+
position: absolute;
246+
top: 50%;
247+
left: 50%;
248+
transform: translate(-50%, -50%);
249+
color: #ff6b6b;
250+
font-size: 16px;
251+
text-align: center;
252+
z-index: 5;
253+
background: rgba(0, 0, 0, 0.9);
254+
padding: 24px;
255+
border-radius: 12px;
256+
max-width: 300px;
257+
258+
.error-icon {
259+
margin-bottom: 12px;
260+
}
261+
262+
.error-title {
263+
margin-bottom: 8px;
264+
}
265+
266+
.error-message {
267+
font-size: 12px;
268+
color: #ffcccc;
269+
line-height: 1.4;
270+
}
271+
`;
272+
273+
const ControlsWrapper = styled.div`
274+
display: flex;
275+
flex-direction: column;
276+
gap: 16px;
277+
align-items: center;
278+
padding: 20px;
279+
background: white;
280+
border-radius: 12px;
281+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
282+
min-width: 300px;
283+
`;
284+
285+
const ControlsTitle = styled.h3`
286+
margin: 0 0 16px 0;
287+
font-size: 18px;
288+
color: #333;
289+
text-align: center;
290+
`;
291+
292+
const TipsBox = styled.div`
293+
margin-top: 16px;
294+
padding: 12px;
295+
background: #f0f8ff;
296+
border-radius: 8px;
297+
font-size: 14px;
298+
color: #666;
299+
text-align: center;
300+
border: 1px solid #e1ecf4;
301+
302+
p {
303+
margin: 0 0 8px 0;
304+
font-weight: 500;
305+
306+
&:last-child {
307+
margin: 0;
308+
font-size: 12px;
309+
}
310+
}
311+
`;

0 commit comments

Comments
 (0)