[홈패널] 비디오 플레이어 추가

This commit is contained in:
sungmin.in
2024-03-05 10:05:34 +09:00
parent 2eea50e57e
commit eaac5e73ac
99 changed files with 10666 additions and 57 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,537 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:2.500000,
bigbuckbunny0.ts
#EXTINF:2.500000,
bigbuckbunny1.ts
#EXTINF:2.500000,
bigbuckbunny2.ts
#EXTINF:2.500000,
bigbuckbunny3.ts
#EXTINF:1.916667,
bigbuckbunny4.ts
#EXTINF:2.500000,
bigbuckbunny5.ts
#EXTINF:1.375000,
bigbuckbunny6.ts
#EXTINF:2.500000,
bigbuckbunny7.ts
#EXTINF:2.500000,
bigbuckbunny8.ts
#EXTINF:2.291667,
bigbuckbunny9.ts
#EXTINF:2.500000,
bigbuckbunny10.ts
#EXTINF:2.500000,
bigbuckbunny11.ts
#EXTINF:2.500000,
bigbuckbunny12.ts
#EXTINF:2.500000,
bigbuckbunny13.ts
#EXTINF:2.500000,
bigbuckbunny14.ts
#EXTINF:2.500000,
bigbuckbunny15.ts
#EXTINF:2.500000,
bigbuckbunny16.ts
#EXTINF:2.500000,
bigbuckbunny17.ts
#EXTINF:2.500000,
bigbuckbunny18.ts
#EXTINF:2.166667,
bigbuckbunny19.ts
#EXTINF:2.500000,
bigbuckbunny20.ts
#EXTINF:2.500000,
bigbuckbunny21.ts
#EXTINF:2.500000,
bigbuckbunny22.ts
#EXTINF:2.500000,
bigbuckbunny23.ts
#EXTINF:2.500000,
bigbuckbunny24.ts
#EXTINF:2.500000,
bigbuckbunny25.ts
#EXTINF:1.083333,
bigbuckbunny26.ts
#EXTINF:2.500000,
bigbuckbunny27.ts
#EXTINF:2.500000,
bigbuckbunny28.ts
#EXTINF:2.500000,
bigbuckbunny29.ts
#EXTINF:2.166667,
bigbuckbunny30.ts
#EXTINF:2.083333,
bigbuckbunny31.ts
#EXTINF:2.500000,
bigbuckbunny32.ts
#EXTINF:1.916667,
bigbuckbunny33.ts
#EXTINF:2.500000,
bigbuckbunny34.ts
#EXTINF:2.500000,
bigbuckbunny35.ts
#EXTINF:2.500000,
bigbuckbunny36.ts
#EXTINF:2.291667,
bigbuckbunny37.ts
#EXTINF:1.666667,
bigbuckbunny38.ts
#EXTINF:2.500000,
bigbuckbunny39.ts
#EXTINF:2.500000,
bigbuckbunny40.ts
#EXTINF:2.500000,
bigbuckbunny41.ts
#EXTINF:2.500000,
bigbuckbunny42.ts
#EXTINF:2.500000,
bigbuckbunny43.ts
#EXTINF:2.500000,
bigbuckbunny44.ts
#EXTINF:2.500000,
bigbuckbunny45.ts
#EXTINF:2.500000,
bigbuckbunny46.ts
#EXTINF:1.250000,
bigbuckbunny47.ts
#EXTINF:2.500000,
bigbuckbunny48.ts
#EXTINF:2.500000,
bigbuckbunny49.ts
#EXTINF:2.500000,
bigbuckbunny50.ts
#EXTINF:2.500000,
bigbuckbunny51.ts
#EXTINF:2.500000,
bigbuckbunny52.ts
#EXTINF:1.708333,
bigbuckbunny53.ts
#EXTINF:2.500000,
bigbuckbunny54.ts
#EXTINF:2.500000,
bigbuckbunny55.ts
#EXTINF:2.500000,
bigbuckbunny56.ts
#EXTINF:2.500000,
bigbuckbunny57.ts
#EXTINF:2.500000,
bigbuckbunny58.ts
#EXTINF:2.500000,
bigbuckbunny59.ts
#EXTINF:2.500000,
bigbuckbunny60.ts
#EXTINF:2.000000,
bigbuckbunny61.ts
#EXTINF:2.500000,
bigbuckbunny62.ts
#EXTINF:2.500000,
bigbuckbunny63.ts
#EXTINF:2.500000,
bigbuckbunny64.ts
#EXTINF:2.125000,
bigbuckbunny65.ts
#EXTINF:2.500000,
bigbuckbunny66.ts
#EXTINF:2.333333,
bigbuckbunny67.ts
#EXTINF:1.458333,
bigbuckbunny68.ts
#EXTINF:2.500000,
bigbuckbunny69.ts
#EXTINF:1.416667,
bigbuckbunny70.ts
#EXTINF:1.791667,
bigbuckbunny71.ts
#EXTINF:1.541667,
bigbuckbunny72.ts
#EXTINF:2.500000,
bigbuckbunny73.ts
#EXTINF:2.500000,
bigbuckbunny74.ts
#EXTINF:1.583333,
bigbuckbunny75.ts
#EXTINF:1.625000,
bigbuckbunny76.ts
#EXTINF:2.500000,
bigbuckbunny77.ts
#EXTINF:2.500000,
bigbuckbunny78.ts
#EXTINF:2.500000,
bigbuckbunny79.ts
#EXTINF:2.500000,
bigbuckbunny80.ts
#EXTINF:1.583333,
bigbuckbunny81.ts
#EXTINF:2.500000,
bigbuckbunny82.ts
#EXTINF:2.500000,
bigbuckbunny83.ts
#EXTINF:1.375000,
bigbuckbunny84.ts
#EXTINF:1.208333,
bigbuckbunny85.ts
#EXTINF:2.500000,
bigbuckbunny86.ts
#EXTINF:2.500000,
bigbuckbunny87.ts
#EXTINF:2.250000,
bigbuckbunny88.ts
#EXTINF:1.750000,
bigbuckbunny89.ts
#EXTINF:1.541667,
bigbuckbunny90.ts
#EXTINF:1.416667,
bigbuckbunny91.ts
#EXTINF:2.500000,
bigbuckbunny92.ts
#EXTINF:2.416667,
bigbuckbunny93.ts
#EXTINF:2.500000,
bigbuckbunny94.ts
#EXTINF:2.500000,
bigbuckbunny95.ts
#EXTINF:1.708333,
bigbuckbunny96.ts
#EXTINF:2.500000,
bigbuckbunny97.ts
#EXTINF:2.500000,
bigbuckbunny98.ts
#EXTINF:2.500000,
bigbuckbunny99.ts
#EXTINF:1.916667,
bigbuckbunny100.ts
#EXTINF:2.500000,
bigbuckbunny101.ts
#EXTINF:2.500000,
bigbuckbunny102.ts
#EXTINF:2.500000,
bigbuckbunny103.ts
#EXTINF:2.500000,
bigbuckbunny104.ts
#EXTINF:2.125000,
bigbuckbunny105.ts
#EXTINF:1.458333,
bigbuckbunny106.ts
#EXTINF:2.500000,
bigbuckbunny107.ts
#EXTINF:2.500000,
bigbuckbunny108.ts
#EXTINF:2.250000,
bigbuckbunny109.ts
#EXTINF:1.250000,
bigbuckbunny110.ts
#EXTINF:1.666667,
bigbuckbunny111.ts
#EXTINF:1.666667,
bigbuckbunny112.ts
#EXTINF:1.666667,
bigbuckbunny113.ts
#EXTINF:1.666667,
bigbuckbunny114.ts
#EXTINF:2.500000,
bigbuckbunny115.ts
#EXTINF:1.666667,
bigbuckbunny116.ts
#EXTINF:1.666667,
bigbuckbunny117.ts
#EXTINF:1.666667,
bigbuckbunny118.ts
#EXTINF:2.500000,
bigbuckbunny119.ts
#EXTINF:1.666667,
bigbuckbunny120.ts
#EXTINF:2.500000,
bigbuckbunny121.ts
#EXTINF:2.500000,
bigbuckbunny122.ts
#EXTINF:2.500000,
bigbuckbunny123.ts
#EXTINF:1.875000,
bigbuckbunny124.ts
#EXTINF:1.666667,
bigbuckbunny125.ts
#EXTINF:2.500000,
bigbuckbunny126.ts
#EXTINF:2.500000,
bigbuckbunny127.ts
#EXTINF:2.500000,
bigbuckbunny128.ts
#EXTINF:2.500000,
bigbuckbunny129.ts
#EXTINF:2.500000,
bigbuckbunny130.ts
#EXTINF:1.666667,
bigbuckbunny131.ts
#EXTINF:1.583333,
bigbuckbunny132.ts
#EXTINF:1.583333,
bigbuckbunny133.ts
#EXTINF:1.541667,
bigbuckbunny134.ts
#EXTINF:2.500000,
bigbuckbunny135.ts
#EXTINF:2.458333,
bigbuckbunny136.ts
#EXTINF:2.500000,
bigbuckbunny137.ts
#EXTINF:2.500000,
bigbuckbunny138.ts
#EXTINF:2.291667,
bigbuckbunny139.ts
#EXTINF:1.916667,
bigbuckbunny140.ts
#EXTINF:2.500000,
bigbuckbunny141.ts
#EXTINF:2.291667,
bigbuckbunny142.ts
#EXTINF:2.375000,
bigbuckbunny143.ts
#EXTINF:2.125000,
bigbuckbunny144.ts
#EXTINF:2.500000,
bigbuckbunny145.ts
#EXTINF:2.500000,
bigbuckbunny146.ts
#EXTINF:2.500000,
bigbuckbunny147.ts
#EXTINF:2.500000,
bigbuckbunny148.ts
#EXTINF:2.500000,
bigbuckbunny149.ts
#EXTINF:2.500000,
bigbuckbunny150.ts
#EXTINF:1.041667,
bigbuckbunny151.ts
#EXTINF:2.500000,
bigbuckbunny152.ts
#EXTINF:2.500000,
bigbuckbunny153.ts
#EXTINF:2.500000,
bigbuckbunny154.ts
#EXTINF:2.500000,
bigbuckbunny155.ts
#EXTINF:2.500000,
bigbuckbunny156.ts
#EXTINF:2.500000,
bigbuckbunny157.ts
#EXTINF:2.500000,
bigbuckbunny158.ts
#EXTINF:2.500000,
bigbuckbunny159.ts
#EXTINF:2.500000,
bigbuckbunny160.ts
#EXTINF:2.500000,
bigbuckbunny161.ts
#EXTINF:2.500000,
bigbuckbunny162.ts
#EXTINF:2.500000,
bigbuckbunny163.ts
#EXTINF:1.375000,
bigbuckbunny164.ts
#EXTINF:1.583333,
bigbuckbunny165.ts
#EXTINF:2.500000,
bigbuckbunny166.ts
#EXTINF:2.500000,
bigbuckbunny167.ts
#EXTINF:2.500000,
bigbuckbunny168.ts
#EXTINF:2.500000,
bigbuckbunny169.ts
#EXTINF:2.500000,
bigbuckbunny170.ts
#EXTINF:2.500000,
bigbuckbunny171.ts
#EXTINF:2.500000,
bigbuckbunny172.ts
#EXTINF:2.291667,
bigbuckbunny173.ts
#EXTINF:2.000000,
bigbuckbunny174.ts
#EXTINF:1.625000,
bigbuckbunny175.ts
#EXTINF:2.500000,
bigbuckbunny176.ts
#EXTINF:1.083333,
bigbuckbunny177.ts
#EXTINF:2.500000,
bigbuckbunny178.ts
#EXTINF:2.083333,
bigbuckbunny179.ts
#EXTINF:2.500000,
bigbuckbunny180.ts
#EXTINF:1.333333,
bigbuckbunny181.ts
#EXTINF:2.291667,
bigbuckbunny182.ts
#EXTINF:1.708333,
bigbuckbunny183.ts
#EXTINF:2.500000,
bigbuckbunny184.ts
#EXTINF:1.500000,
bigbuckbunny185.ts
#EXTINF:1.208333,
bigbuckbunny186.ts
#EXTINF:2.500000,
bigbuckbunny187.ts
#EXTINF:2.041667,
bigbuckbunny188.ts
#EXTINF:2.458333,
bigbuckbunny189.ts
#EXTINF:2.083333,
bigbuckbunny190.ts
#EXTINF:1.083333,
bigbuckbunny191.ts
#EXTINF:1.791667,
bigbuckbunny192.ts
#EXTINF:1.500000,
bigbuckbunny193.ts
#EXTINF:2.500000,
bigbuckbunny194.ts
#EXTINF:2.250000,
bigbuckbunny195.ts
#EXTINF:2.500000,
bigbuckbunny196.ts
#EXTINF:2.500000,
bigbuckbunny197.ts
#EXTINF:1.625000,
bigbuckbunny198.ts
#EXTINF:1.041667,
bigbuckbunny199.ts
#EXTINF:1.541667,
bigbuckbunny200.ts
#EXTINF:2.500000,
bigbuckbunny201.ts
#EXTINF:2.500000,
bigbuckbunny202.ts
#EXTINF:2.000000,
bigbuckbunny203.ts
#EXTINF:2.500000,
bigbuckbunny204.ts
#EXTINF:2.500000,
bigbuckbunny205.ts
#EXTINF:1.541667,
bigbuckbunny206.ts
#EXTINF:2.250000,
bigbuckbunny207.ts
#EXTINF:1.333333,
bigbuckbunny208.ts
#EXTINF:2.500000,
bigbuckbunny209.ts
#EXTINF:1.833333,
bigbuckbunny210.ts
#EXTINF:2.500000,
bigbuckbunny211.ts
#EXTINF:2.500000,
bigbuckbunny212.ts
#EXTINF:1.666667,
bigbuckbunny213.ts
#EXTINF:2.500000,
bigbuckbunny214.ts
#EXTINF:2.500000,
bigbuckbunny215.ts
#EXTINF:2.291667,
bigbuckbunny216.ts
#EXTINF:1.500000,
bigbuckbunny217.ts
#EXTINF:2.166667,
bigbuckbunny218.ts
#EXTINF:2.458333,
bigbuckbunny219.ts
#EXTINF:2.500000,
bigbuckbunny220.ts
#EXTINF:1.333333,
bigbuckbunny221.ts
#EXTINF:2.500000,
bigbuckbunny222.ts
#EXTINF:2.500000,
bigbuckbunny223.ts
#EXTINF:2.333333,
bigbuckbunny224.ts
#EXTINF:2.500000,
bigbuckbunny225.ts
#EXTINF:2.500000,
bigbuckbunny226.ts
#EXTINF:2.500000,
bigbuckbunny227.ts
#EXTINF:2.500000,
bigbuckbunny228.ts
#EXTINF:2.500000,
bigbuckbunny229.ts
#EXTINF:2.500000,
bigbuckbunny230.ts
#EXTINF:2.500000,
bigbuckbunny231.ts
#EXTINF:2.500000,
bigbuckbunny232.ts
#EXTINF:2.500000,
bigbuckbunny233.ts
#EXTINF:2.500000,
bigbuckbunny234.ts
#EXTINF:2.500000,
bigbuckbunny235.ts
#EXTINF:2.500000,
bigbuckbunny236.ts
#EXTINF:2.500000,
bigbuckbunny237.ts
#EXTINF:2.500000,
bigbuckbunny238.ts
#EXTINF:2.500000,
bigbuckbunny239.ts
#EXTINF:2.500000,
bigbuckbunny240.ts
#EXTINF:2.500000,
bigbuckbunny241.ts
#EXTINF:2.500000,
bigbuckbunny242.ts
#EXTINF:2.500000,
bigbuckbunny243.ts
#EXTINF:2.500000,
bigbuckbunny244.ts
#EXTINF:2.500000,
bigbuckbunny245.ts
#EXTINF:2.500000,
bigbuckbunny246.ts
#EXTINF:2.500000,
bigbuckbunny247.ts
#EXTINF:2.500000,
bigbuckbunny248.ts
#EXTINF:2.500000,
bigbuckbunny249.ts
#EXTINF:2.500000,
bigbuckbunny250.ts
#EXTINF:2.500000,
bigbuckbunny251.ts
#EXTINF:2.500000,
bigbuckbunny252.ts
#EXTINF:2.500000,
bigbuckbunny253.ts
#EXTINF:2.500000,
bigbuckbunny254.ts
#EXTINF:2.500000,
bigbuckbunny255.ts
#EXTINF:2.500000,
bigbuckbunny256.ts
#EXTINF:2.416667,
bigbuckbunny257.ts
#EXTINF:1.041667,
bigbuckbunny258.ts
#EXTINF:2.500000,
bigbuckbunny259.ts
#EXTINF:2.500000,
bigbuckbunny260.ts
#EXTINF:2.500000,
bigbuckbunny261.ts
#EXTINF:2.500000,
bigbuckbunny262.ts
#EXTINF:2.500000,
bigbuckbunny263.ts
#EXTINF:2.500000,
bigbuckbunny264.ts
#EXTINF:0.500000,
bigbuckbunny265.ts
#EXT-X-ENDLIST

View File

@@ -0,0 +1,279 @@
WEBVTT FILE
1
00:00:02.619 --> 00:00:04.804
Hello everyone nice to meet you all again
2
00:00:04.804 --> 00:00:05.922
My name is Marina
3
00:00:05.922 --> 00:00:09.259
And today we will have a special Ashtanga yoga class
4
00:00:09.409 --> 00:00:10.827
Please if you are beginner
5
00:00:10.827 --> 00:00:13.646
Be careful with all Asnas we will do today
6
00:00:13.646 --> 00:00:15.615
and enjoy
7
00:00:21.588 --> 00:00:24.290
Okay guys and let's start with the suns rotation
8
00:00:24.290 --> 00:00:26.926
Inhale
9
00:00:26.926 --> 00:00:28.228
Exhale
10
00:00:28.228 --> 00:00:31.064
Next inhale bring your hands up
11
00:00:31.064 --> 00:00:34.751
and go down to the downfold
12
00:00:34.751 --> 00:00:36.586
Inhale look up
13
00:00:36.586 --> 00:00:38.705
Exhale jump back to the plank
14
00:00:38.705 --> 00:00:39.856
Chaturanga
15
00:00:39.856 --> 00:00:41.324
Upward dog
16
00:00:42.292 --> 00:00:46.913
and slowly come to the downward dog
17
00:00:46.913 --> 00:00:49.365
Extend your back
18
00:00:49.933 --> 00:00:53.119
and bring now up your right leg
19
00:00:53.119 --> 00:00:56.389
Move it forward and step forward
20
00:00:56.389 --> 00:00:58.108
Okay
21
00:00:58.108 --> 00:01:01.194
Bring your right leg on top of the right hand
22
00:01:01.194 --> 00:01:04.514
and try to keep the balance here
23
00:01:04.514 --> 00:01:06.132
Jump back and again
24
00:01:06.132 --> 00:01:08.451
Upward dog inhale
25
00:01:08.451 --> 00:01:10.236
Exhale downward dog
26
00:01:13.373 --> 00:01:16.593
Extend your back and now your left leg up
27
00:01:16.593 --> 00:01:22.982
Bend your knee and step forward with your left leg
28
00:01:22.982 --> 00:01:26.486
Okay bring your left hand behind your left hip
29
00:01:26.486 --> 00:01:30.023
and again bring all the weight on top of your left hand
30
00:01:30.023 --> 00:01:31.691
Try to keep the balance here
31
00:01:31.691 --> 00:01:35.545
and jump back to the upward dog
32
00:01:35.545 --> 00:01:39.649
and downward dog
33
00:01:39.649 --> 00:01:43.069
Continue breathing inhale
34
00:01:43.069 --> 00:01:45.321
and exhale
35
00:01:45.321 --> 00:01:47.090
Look forward
36
00:01:47.090 --> 00:01:49.559
Okay and step forward
37
00:01:49.559 --> 00:01:51.611
Now we'll go to Bakasana
38
00:01:51.611 --> 00:01:56.883
Here you can bring your weight to your hands
39
00:01:56.883 --> 00:02:00.887
Be careful here and if you're suppose to jump back
40
00:02:00.887 --> 00:02:04.240
again upward dog and downward dog
41
00:02:04.240 --> 00:02:05.542
Extend your back
42
00:02:05.542 --> 00:02:07.660
One more time
43
00:02:09.012 --> 00:02:10.163
Very good
44
00:02:10.163 --> 00:02:13.283
Look forward
45
00:02:13.283 --> 00:02:17.203
Place your knees on the mat
46
00:02:17.203 --> 00:02:19.389
Open your legs a bit
47
00:02:19.389 --> 00:02:22.959
and were going to Sirsana hands stand
48
00:02:22.959 --> 00:02:27.547
Please be careful here slightly moving with your legs
49
00:02:27.547 --> 00:02:30.884
Bring them very very slightly up
50
00:02:30.884 --> 00:02:32.619
Keeping balance
51
00:02:32.619 --> 00:02:37.240
But be very careful if you're trying this pose please
52
00:02:37.240 --> 00:02:38.875
Be careful with your neck
53
00:02:38.875 --> 00:02:43.096
and try to keep the weight in your hands
54
00:02:43.096 --> 00:02:47.417
Here you can do some variation like open your legs
55
00:02:47.734 --> 00:02:50.620
closing them
56
00:02:51.371 --> 00:02:53.206
But always be careful
57
00:02:53.206 --> 00:02:58.661
that your neck and shoulders are completely relaxed.
58
00:02:58.661 --> 00:03:02.649
And of course after Sirsana we'll relax
59
00:03:02.649 --> 00:03:05.885
or stay in a childs pose for a little while
60
00:03:07.637 --> 00:03:09.472
Okay look forward
61
00:03:09.472 --> 00:03:14.744
Bring your back again in the position of the downward dog
62
00:03:16.596 --> 00:03:20.233
And well jump forward
63
00:03:20.233 --> 00:03:22.669
Roll up the spine return
64
00:03:22.669 --> 00:03:25.572
Inhale and Exhale
65
00:03:25.572 --> 00:03:27.507
Namaste
66
00:03:28.558 --> 00:03:31.110
Thank you everyone for joining my class today
67
00:03:31.110 --> 00:03:33.196
I hope it was not too difficult for you
68
00:03:33.196 --> 00:03:36.132
and hope to see you in my next classes
69
00:03:36.132 --> 00:03:37.500
Good bye

View File

@@ -0,0 +1,453 @@
/**
* Item Module of contents list
*
* @module MediaItem
*/
import React, { useCallback, useEffect, useMemo } from "react";
import classNames from "classnames";
import compose from "ramda/src/compose";
import { shallowEqual, useSelector } from "react-redux";
import { Image } from "@enact/sandstone/Image";
import { Marquee, MarqueeController } from "@enact/sandstone/Marquee";
import Spottable from "@enact/spotlight/Spottable";
import { Cell, Column } from "@enact/ui/Layout";
import { VoiceControlDecorator } from "@enact/webos/speech";
import * as Config from "../../utils/Config";
// import YOUTUBE_BADGE_LOGO from "../../../assets/list/icon/icon_youtube.png";
import * as ContentType from "../../utils/Config";
import * as Utils from "../../utils/helperMethods";
import { $L } from "../../utils/helperMethods";
import SpotlightIds from "../../utils/SpotlightIds";
import css from "./MediaItem.module.less";
/**
*
* @param {itemSize} "small", "big", 'plan'
* @returns
*/
const MediaItemBase = ({
itemSize,
supportFavBtn,
favBtnFocused,
caption,
onItemClick,
forceFocus,
listspotlightid,
style,
onFavBtnFocused,
itemInfo,
playing,
selectMode,
...rest
}) => {
const contentsMyFavorites = useSelector(
(state) => state.contentsMyFavorites,
shallowEqual
);
const contentInfos = useSelector((state) => state.contentInfos);
const panelInfo = useSelector((state) => state.panels);
const updatedContentInfo = useSelector(
(state) => state.updatedContentInfo,
(newState) => {
if (!itemInfo || !newState) {
return true;
}
if (itemInfo.contentId === newState.contentId) {
Utils.jsonConcat(itemInfo, newState);
return false;
}
if (itemInfo.youtubeId && itemInfo.youtubeId === newState.youtubeId) {
Utils.jsonConcat(itemInfo, newState);
return false;
}
if (itemInfo.contentType === ContentType.PLAN) {
let matched = false;
if (itemInfo.items) {
for (let j = 0; j < itemInfo.items.length; j++) {
if (itemInfo.items[j].contentId === newState.contentId) {
Utils.jsonConcat(itemInfo.items[j], newState);
matched = true;
}
}
if (itemInfo.items && itemInfo.items[0]) {
itemInfo.thumbnailImageUrl = itemInfo.items[0].thumbnailImageUrl;
itemInfo.postImageUrl = itemInfo.items[0].postImageUrl;
}
if (matched) {
return false;
}
}
}
return true; //it's same item. Will not update.
}
);
useEffect(() => {
if (itemInfo && itemInfo.contentType === ContentType.PLAN) {
if (itemInfo.items) {
for (let j in itemInfo.items) {
if (
itemInfo.items[j].contentId &&
contentInfos[itemInfo.items[j].contentId]
) {
Utils.jsonConcat(
itemInfo.items[j],
contentInfos[itemInfo.items[j].contentId]
);
}
}
if (itemInfo.items && itemInfo.items[0]) {
itemInfo.thumbnailImageUrl = itemInfo.items[0].thumbnailImageUrl;
itemInfo.postImageUrl = itemInfo.items[0].postImageUrl;
}
}
} else if (
itemInfo &&
itemInfo.contentId &&
contentInfos[itemInfo.contentId]
) {
Utils.jsonConcat(itemInfo, contentInfos[itemInfo.contentId]);
}
}, [itemInfo, contentInfos, updatedContentInfo]);
const iamFavContent = useMemo(() => {
let ret = false;
if (listspotlightid === SpotlightIds.LIST_MYFAVORITE) {
ret = true;
} else if (
itemInfo &&
contentsMyFavorites &&
contentsMyFavorites.length > 0
) {
const id = itemInfo.contentId;
for (let index in contentsMyFavorites) {
if (id === contentsMyFavorites[index].contentId) {
ret = true;
break;
}
}
}
return ret;
}, [contentsMyFavorites, itemInfo, listspotlightid]);
const onFavMouseEnter = useCallback(() => {
if (!supportFavBtn) {
return;
}
onFavBtnFocused(true);
}, [onFavBtnFocused, supportFavBtn]);
const onFavMouseLeave = useCallback(() => {
onFavBtnFocused(false);
}, [onFavBtnFocused]);
const onClick = useCallback(
(ev) => {
console.log("onClick..", ev);
if (onItemClick) {
onItemClick(ev);
}
ev.stopPropagation();
ev.preventDefault();
},
[onItemClick]
);
let thumbnail = "",
placeholder = "";
if (itemSize !== "small" && itemInfo && itemInfo.postImageUrl) {
thumbnail = itemInfo.postImageUrl;
} else {
thumbnail = itemInfo && itemInfo.thumbnailImageUrl;
}
placeholder = itemInfo && itemInfo.thumbnailImageUrl;
const captionContainer = useCallback(
(_isYoutubeChannel, _isApp, _isSpecialButton) => {
const marqueeAlign =
_isYoutubeChannel || _isApp || _isSpecialButton ? "center" : "left";
const replaceHtmlEntitesCaption = Utils.replaceHtmlEntites(caption);
if (!itemInfo || itemInfo.contentType === ContentType.PLAN) {
return null;
}
let splitTitle = [];
if (Object.keys(itemInfo).includes("dayTitle")) {
splitTitle = replaceHtmlEntitesCaption.split(":");
}
return (
<Cell
align="center"
key="captionContainer"
className={classNames(
css.captionContainer,
_isYoutubeChannel ? css.channel : null,
_isApp ? css.app : null,
_isSpecialButton ? css.innerText : null
)}
>
<Marquee
marqueeDisabled={!forceFocus || favBtnFocused}
alignment={marqueeAlign}
key="caption"
className={css.caption}
marqueeOn={!favBtnFocused && forceFocus ? "render" : undefined}
>
{Object.keys(itemInfo).includes("dayTitle") ? (
<>
<span className={css.emphasisFont}>
{" "}
{splitTitle[0] + " :"}{" "}
</span>
{splitTitle[1]}
</>
) : (
replaceHtmlEntitesCaption
)}
</Marquee>
</Cell>
);
},
[itemInfo, caption, forceFocus, favBtnFocused]
);
const planItemDetail = useCallback(
(itemSize) => {
if (itemInfo && itemSize === "plan") {
return (
<div className={css.planItemDetail}>
<Marquee
marqueeDisabled={!forceFocus}
onClick={onClick}
className={css.planTitle}
marqueeOn={forceFocus ? "render" : undefined}
>
{Utils.replaceHtmlEntites(itemInfo.title)}
</Marquee>
{itemInfo.items &&
itemInfo.items.map((item, index) => (
<div className={css.planList} key={index}>
<div className={css.plancountcontainer}>
<div className={css.repeatImage} />
<div>{item && item.repeatCount}</div>
</div>
<Marquee
marqueeDisabled={!forceFocus}
className={css.planlisttitle}
alignment={"left"}
marqueeOn={forceFocus ? "render" : undefined}
>
{item && item.title}
</Marquee>
</div>
))}
</div>
);
} else {
return (
<>
<div
className={classNames(
css.captionContainer,
isYoutubeChannel ? css.channelPlayIcon : null,
isMoreButton ? css.moreIcon : null,
isAddPlanButton ? css.addPlanIcon : null,
playing ? css.playing : null
)}
>
<Marquee
className={css.caption}
marqueeDisabled={!forceFocus}
onClick={onClick}
marqueeOn={forceFocus ? "render" : undefined}
>
{Utils.replaceHtmlEntites(itemInfo.title)}
</Marquee>
</div>
<div className={css.repeatCountList}>
<span>{itemInfo.items.length}</span>
</div>
</>
);
}
},
[itemInfo, itemSize, forceFocus, onClick]
);
const playerRepeat = useCallback(() => {
if (itemInfo) {
return (
<div className={css.playerRepeatContainer}>
<div className={css.repeatImage} />
<div>{itemInfo.repeatCount}</div>
</div>
);
} else {
return null;
}
}, [itemInfo]);
const supportBadge = useMemo(() => {
let supported = false,
badgeStyle = {};
if (itemInfo) {
if (itemInfo.contentType === ContentType.YOUTUBE_VIDEO) {
supported = true;
// badgeStyle = { backgroundImage: `url(${YOUTUBE_BADGE_LOGO})` };
} else if (itemInfo.contentType === ContentType.CP_CONTENT) {
supported = true;
// badgeStyle = { backgroundImage: `url(${itemInfo.badgeIconUrl})` };
}
}
return { supported, badgeStyle };
}, [itemInfo]);
const isYoutubeChannel =
itemInfo && itemInfo.contentType === ContentType.YOUTUBE_CHANNEL;
const isYoutubeVideo =
itemInfo && itemInfo.contentType === ContentType.YOUTUBE_VIDEO;
const isMoreButton = itemInfo && itemInfo.contentType === ContentType.MORE;
const isAddPlanButton =
itemInfo && itemInfo.contentType === ContentType.ADD_PLAN;
const isSpecialButton = isMoreButton || isAddPlanButton;
const isApp = itemInfo && itemInfo.contentType === ContentType.APP;
const isYoutubeAppMoveIcon =
panelInfo.length > 0 && panelInfo[0].name === "moreformedlist";
const moveAppIcon =
itemInfo &&
(itemInfo.contentType === ContentType.APP ||
itemInfo.contentType === ContentType.CP_CONTENT);
const playtime =
itemInfo && itemInfo.playtime
? Utils.transSecToText(itemInfo.playtime)
: null;
const useNewTodayIcon = Config.USE_NEW_TODAY_ICON;
return (
<div
{...rest}
className={classNames(
forceFocus ? css.forceFocus : null,
css.mediaImage,
itemSize === "big"
? css.bigItem
: itemSize === "plan"
? css.planItem
: null
)}
style={style}
onClick={onClick}
>
<Column
className={classNames(
css.imageItem,
favBtnFocused ? css.favFocus : null
)}
>
<Cell
key="thumbContainer"
className={classNames(
css.thumbContainer,
isYoutubeChannel ? css.thumbContaineryoutubeChannel : null,
isApp ? css.thumbContainerApp : null,
isSpecialButton ? css.specialBtnContainer : null
)}
>
{thumbnail && (
<Image
className={classNames(
css.image,
isYoutubeChannel ? css.youtubeChannel : null,
isApp ? css.applogo : null,
isYoutubeVideo ? css.youtubeVideo : null
)}
placeholder={placeholder}
src={thumbnail}
/>
)}
{listspotlightid === SpotlightIds.LIST_TODAYTOP && (
<div
className={classNames(
css.top10Number,
css["index_" + (rest.index + 1)]
)}
/>
)}
{itemInfo && itemInfo.isToday && (
<div
className={classNames(
css.toDayStr,
useNewTodayIcon && css.newTodayStr
)}
/>
)}
{captionContainer(isYoutubeChannel, isApp, isSpecialButton)}
{!forceFocus && playtime && (
<div className={classNames(!isYoutubeChannel ? css.subtime : null)}>
{playtime}
</div>
)}
{supportBadge.supported && (
<div className={css.badgelogo} style={supportBadge.badgeStyle} />
)}
{itemInfo && (!selectMode || isAddPlanButton) && (
<div
className={classNames(
css.playIcon,
isYoutubeAppMoveIcon
? css.appIcon
: isYoutubeChannel
? css.channelPlayIcon
: null,
moveAppIcon ? css.appIcon : null,
isMoreButton ? css.moreIcon : null,
isAddPlanButton
? itemSize === "plan"
? css.addPlanIcon
: css.moreIcon
: null,
playing ? css.playing : isYoutubeVideo ? css.appIcon : null
)}
/>
)}
{itemInfo &&
itemInfo.contentType === ContentType.PLAN &&
planItemDetail(itemSize)}
{itemInfo && itemInfo.repeatCount && playerRepeat()}
</Cell>
{supportFavBtn && (
<div
isfavbtn={"true"}
className={classNames(
css.addToFavoriteBtn,
favBtnFocused ? css.focus : null
)}
onMouseEnter={onFavMouseEnter}
onMouseLeave={onFavMouseLeave}
>
<div
className={classNames(
css.favIcon,
iamFavContent ? css.removeFav : null
)}
/>
<div className={css.favText}>
{iamFavContent
? $L("Remove from Favorite").toUpperCase()
: $L("Add to Favorite").toUpperCase()}
</div>
</div>
)}
</Column>
</div>
);
};
const ImageItemDecorator = compose(
MarqueeController({ marqueeOnFocus: true }),
VoiceControlDecorator,
Spottable
);
const MediaItem = ImageItemDecorator(MediaItemBase);
export default MediaItem;

View File

@@ -0,0 +1,792 @@
@import "~@enact/sandstone/styles/mixins.less";
.noMediaText {
height: 304px;
text-align: center;
line-height: 304px;
}
.mediaImage {
position: relative;
display: inline-block;
width: 360px;
height: 304px;
margin: 0;
font-family: "LG SmartFont";
&.bigItem,
&.planItem {
height: 638px;
}
}
.hide {
visibility: hidden;
opacity: 0;
}
.imageItem {
position: absolute;
top: 0;
width: 100%;
height: 100%;
padding-top: 0px;
// will-change: transform;
.toDayStr {
position: absolute;
margin-left: -16px;
margin-top: -5px;
z-index: 10;
width: 300px;
height: 150px;
background-size: contain;
background-image: url("../../../assets/images/player/today.png");
background-repeat: no-repeat;
}
.newTodayStr {
margin-left: 209px;
margin-top: 10px;
width: 150px;
height: 75px;
background-image: url("../../../assets/images/player/new_today_Icon.png");
}
.top10Number {
position: absolute;
margin-left: -16px;
margin-top: -20px;
z-index: 10;
width: 146px;
height: 146px;
background-size: contain;
&.index_1 {
background-image: url("../../../assets/images/player/numbers/image_01.png");
}
&.index_2 {
background-image: url("../../../assets/images/player/numbers/image_02.png");
}
&.index_3 {
background-image: url("../../../assets/images/player/numbers/image_03.png");
}
&.index_4 {
background-image: url("../../../assets/images/player/numbers/image_04.png");
}
&.index_5 {
background-image: url("../../../assets/images/player/numbers/image_05.png");
}
&.index_6 {
background-image: url("../../../assets/images/player/numbers/image_06.png");
}
&.index_7 {
background-image: url("../../../assets/images/player/numbers/image_07.png");
}
&.index_8 {
background-image: url("../../../assets/images/player/numbers/image_08.png");
}
&.index_9 {
background-image: url("../../../assets/images/player/numbers/image_09.png");
}
&.index_10 {
background-image: url("../../../assets/images/player/numbers/image_10.png");
}
}
.thumbContainer {
margin: 0;
width: 100%;
height: 100%;
background-size: contain;
background-repeat: no-repeat;
background-image: url("../../../assets/images/player/frame.png");
// transition: transform 200ms ease-in-out;
&.specialBtnContainer {
background-image: initial;
//background-color: yellow;
width: 340px;
height: 304px;
//right: 50px;
background-color: rgba(143, 143, 143, 0.28);
border: 2px solid rgba(114, 111, 111, 0.292);
border-radius: 25px;
box-shadow: inset 0px 0px 20px rgba(212, 200, 200, 0.15);
margin: 14px 10px 61px 10px;
}
&.thumbContaineryoutubeChannel {
width: 360px;
height: 304px;
background-image: url("../../../assets/images/player/frame_channel.png");
}
&.thumbContainerApp {
width: 360px;
height: 304px;
background-image: url("../../../assets/images/player/fitness_banner_round.png");
}
}
.image {
position: absolute;
z-index: 5;
width: 301px; // old = 84%
height: 169px; // old = 59%
margin-left: 30px; // old = 8%
margin-top: 34px; // old = 6%
border-radius: 18px; // old = 7%
border: 0px;
/* yhcho_temp */
// border: 1px solid rgb(115, 173, 33);
transition: opacity 500ms ease-in-out;
}
.subtime {
position: absolute;
display: block;
z-index: 16;
background-color: rgba(39, 39, 39, 0.7);
width: auto;
height: 30px !important;
line-height: 35px !important;
right: 32px;
bottom: 104px;
font-size: 18px;
text-align: center;
margin: auto;
padding-bottom: 2px;
padding-left: 9px;
padding-right: 9px;
color: rgb(255, 255, 255);
transition: initial;
border-radius: 18px; // old = 7%
border: 0px;
}
.badgelogo {
z-index: 5;
bottom: 105px;
left: 38px;
width: 42px;
height: 29px;
border-radius: 0px;
border: 0px;
background-size: contain;
background-repeat: no-repeat;
position: absolute;
}
.applogo {
position: relative;
width: 148px;
height: 148px;
margin-left: 29%;
margin-top: 10%;
border: 2px solid rgb(196, 191, 205);
}
.youtubeChannel {
position: relative;
width: 148px;
height: 148px;
margin-left: 29%;
margin-top: 10%;
border: 2px solid rgb(196, 191, 205);
border-radius: 70%;
}
.youtubeVideo {
position: absolute;
z-index: 5;
width: 300px; // old = 84%
height: 168px; // old = 59%
margin-left: 31px; // old = 8%
margin-top: 35px; // old = 6%
border-radius: 18px; // old = 7%
border: 0px;
/* yhcho_temp */
// border: 1px solid rgb(115, 173, 33);
transition: opacity 500ms ease-in-out;
}
.playIcon {
opacity: 0;
width: 91px;
height: 106px;
z-index: 6;
position: absolute;
top: 21%;
left: 38%;
transform: translate(-50% -50%);
background-repeat: no-repeat;
background-size: cover;
background-image: url("../../../assets/images/player/ico-play.png");
&.channelPlayIcon {
top: 25%;
left: 38%;
width: 96px;
height: 78px;
background-image: url("../../../assets/images/player/icon/icon_channel.png");
}
&.moreIcon {
opacity: 1;
left: 39%;
width: 83px;
height: 106px;
background-image: url("../../../assets/images/player/icon/make routine_Banner.png");
}
&.addPlanIcon {
opacity: 1;
left: 34%;
width: 116px;
height: 148px;
background-image: url("../../../assets/images/player/icon/make routine_Banner.png");
}
&.playing {
opacity: 1;
background-image: url("../../../assets/images/player/ico-playing.png");
}
&.appIcon {
top: 25%;
left: 38%;
width: 96px;
height: 78px;
background-image: url("../../../assets/images/player/icon/icon_app.png");
}
}
.addToFavoriteBtn {
opacity: 0;
width: 270px;
height: 38px;
z-index: 6;
position: absolute;
display: flex;
bottom: 4px;
left: 50%;
transform: translateX(-43%);
border-radius: 20px;
/* top: initial; */
background-color: rgba(112, 104, 104, 0.7);
// background-image: url("../../../assets/images/list/btn-frame-1.png");
.favIcon {
width: 26px;
height: 25px;
margin-left: 9px;
margin-top: 5px;
background-size: contain;
background-repeat: no-repeat;
background-image: url("../../../assets/images/player/ico_favorite.png");
&.removeFav {
background-image: url("../../../assets/images/player/ico_favorite_fill.png");
}
}
.favText {
font-size: 17px;
min-width: 223px;
letter-spacing: -0.9px;
text-align: center;
color: rgb(209, 209, 209);
margin-top: 2.1px;
//margin-left: 2px;
}
&.focus {
width: 295px;
height: 41px;
border-radius: 20px;
background: linear-gradient(
50deg,
rgba(90, 111, 146, 0.6),
rgba(90, 111, 146, 0.77),
rgba(112, 77, 116, 0.77),
rgba(112, 77, 116, 0.6)
) !important;
// background-image: url("../../../assets/images/list/btn-focus.png");
top: initial;
left: 48.1%;
bottom: 4px;
.favIcon {
width: 27px;
height: 27px;
margin-left: 9px;
margin-top: 6px;
background-image: url("../../../assets/images/player/ico_favorite_focus.png");
&.removeFav {
background-image: url("../../../assets/images/player/ico_favorite_fill_focus.png");
}
}
.favText {
margin-top: 5px;
margin-left: 2px;
font-size: 19px;
text-align: center;
color: rgb(255, 255, 255);
}
}
}
.captionContainer {
position: absolute;
z-index: 15;
height: 40px !important;
width: 83%;
bottom: 57px;
left: 33px;
display: flex;
flex-direction: column;
justify-content: center;
.caption {
position: absolute;
display: block;
width: 100%;
height: 40px;
font-size: 20px;
letter-spacing: -0.5px;
color: rgb(196, 191, 205) !important;
z-index: 15;
text-overflow: ellipsis;
overflow: hidden;
justify-content: center;
white-space: nowrap;
pointer-events: none;
}
&.channel {
width: 212px;
left: 51%;
transform: translateX(-50%);
.caption {
font-size: 22px;
line-height: 27px;
text-align: center;
}
}
&.app {
width: 212px;
left: 51%;
transform: translateX(-50%);
.caption {
font-size: 22px;
line-height: 27px;
text-align: center;
}
}
&.innerText {
bottom: 30px;
.caption {
text-align: center;
}
}
}
.playerRepeatContainer {
position: absolute;
z-index: 15;
height: 55px !important;
width: 300px !important;
bottom: 101px;
left: 31px;
font-size: 16px;
line-height: 40px;
padding: 0px 0px;
margin: 0px 0px !important;
font-family: "LG SmartFont";
float: left;
text-align: left;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.555),
rgba(0, 0, 0, 0)
) !important;
border-radius: 0px 0px 18px 18px;
.repeatImage {
width: 23px;
height: 18px;
margin-left: 10px;
margin-right: 10px;
background-size: contain;
background-image: url("../../../assets/images/player/icon/icon_repeat.png");
}
> div {
display: inline-block;
vertical-align: middle;
margin-top: 17px;
text-shadow: -0.8px 0 rgb(196, 196, 196), 0 0.8px rgb(196, 196, 196),
0.8px 0 rgb(196, 196, 196), 0 -0.8px rgb(196, 196, 196);
}
}
}
.repeatCountList {
position: absolute;
bottom: 105px;
left: 35px;
z-index: 101;
background-image: url("../../../assets/images/player/icon_list.png");
background-repeat: no-repeat;
background-position: 7px 7px;
width: 68px;
height: 30px;
background-color: black;
opacity: 0.6;
border-radius: 5px;
> span {
position: absolute;
right: 10px;
top: -1px;
font-family: "LG SmartFont";
font-size: 18px;
}
}
.bigItem {
.thumbContainer {
background-image: url("../../../assets/images/player/frame-big.png");
.image {
height: 500px;
background-size: cover;
margin-top: 35px;
margin-left: 30px;
}
}
.playIcon {
top: 36%;
}
}
.planItem {
.imageItem {
padding-left: 12px;
padding-right: 12px;
}
.thumbContainer {
width: 336px;
height: 563px;
background-size: cover;
background-color: rgba(143, 143, 143, 0.28);
border: 2px solid rgba(114, 111, 111, 0.292);
border-radius: 25px;
box-shadow: inset 0px 0px 20px rgba(202, 200, 200, 0.15);
margin: 18px 0px 48px 0px;
background-image: initial;
.image {
background-size: cover;
margin-top: 15px;
margin-left: 15px;
height: 530px;
width: 306px;
border-radius: 20px;
}
&.specialBtnContainer {
background-image: initial;
//background-image: url("../../../assets/images/list/frame-big.png");
background-size: cover;
background-color: rgba(143, 143, 143, 0.28);
border: 2px solid rgba(114, 111, 111, 0.219);
border-radius: 25px;
box-shadow: inset 0px 0px 20px rgba(202, 200, 200, 0.15);
margin: 18px 0px 48px 0px;
//background-color: rgba(126, 126, 126, 0.315);
}
}
.playIcon {
top: 36%;
}
.planItemDetail {
z-index: 101;
position: absolute;
display: block;
margin-top: 212px;
margin-left: 15px;
width: 306px;
height: 334px;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
border-bottom-right-radius: 20px;
border-bottom-left-radius: 20px;
background: linear-gradient(
0deg,
rgba(15, 15, 15, 0.62),
rgba(15, 15, 15, 0.62),
rgba(15, 15, 15, 0.62),
rgba(36, 36, 36, 0)
) !important;
.planTitle {
height: 65px;
padding: 31px 10px 0px 10px;
margin-bottom: 0px;
text-align: left;
font-size: 26px;
font-family: "LG SmartFont SemiBold";
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
}
.planList {
font-size: 18px;
margin-bottom: 9px;
margin-left: 10px;
font-family: "LG SmartFont";
.plancountcontainer {
width: auto;
height: 100%;
margin-left: 10px;
margin-right: 10px;
font-size: 16px;
font-family: "LG SmartFont";
float: right;
text-align: right;
.repeatImage {
width: 23px;
height: 18px;
margin-right: 10px;
background-size: contain;
background-image: url("../../../assets/images/player/icon/icon_repeat.png");
}
> div {
display: inline-block;
vertical-align: middle;
font-size: 0.70833rem;
height: 100%;
font-family: "LG SmartFont";
}
}
.planlisttitle {
width: auto;
font-size: 17px;
font-family: "LG SmartFont";
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
pointer-events: none;
}
}
}
.innerText {
bottom: 170px !important;
.caption {
text-align: center;
font-family: "LG SmartFont";
}
}
.subtime {
opacity: 0;
}
}
.forceFocus {
// :global(.spottable):focus{
z-index: 20;
:not(.favFocus) .thumbContainer {
/*transform: scale(1.2);*/
background-image: url("../../../assets/images/player/focus-frame.png");
width: 403px; //
height: 100%; //
.image {
width: 348px; // old = 83%
height: 196px; // old = 58.5%
margin-top: 18px; // old = 8%
margin-left: 27px;
}
.badgelogo {
left: 35px;
bottom: 94px;
}
.applogo {
width: 178px;
height: 178px;
margin-left: 25.5%;
margin-top: 6%;
border: 1px solid rgb(104, 99, 116);
}
.youtubeChannel {
width: 176px;
height: 176px;
margin-left: 26%;
margin-top: 6%;
border: 1px solid rgb(104, 99, 116);
border-radius: 70%;
}
&.thumbContaineryoutubeChannel {
margin: 0px 18px;
width: 360px;
height: 304px;
background-image: url("../../../assets/images/player/focus_frame_channel.png");
}
&.thumbContainerApp {
margin: 0px 18px;
width: 360px;
height: 304px;
background-image: url("../../../assets/images/player/focus_fitness_banner_round.png");
}
&.specialBtnContainer {
background-image: initial;
border: 2px solid rgba(114, 111, 111, 0.295);
border-radius: 25px;
box-shadow: inset 0px 0px 20px rgba(24, 24, 24, 0.15);
width: 380px;
height: 304px;
margin: 1px 10px 48px 10px;
background: linear-gradient(
50deg,
rgba(90, 111, 146, 0.6),
rgba(90, 111, 146, 0.77),
rgba(112, 77, 116, 0.77),
rgba(112, 77, 116, 0.6)
) !important;
}
.playerRepeatContainer {
z-index: 15;
height: 55px !important;
width: 348px !important;
bottom: 90px;
left: 27px;
font-family: "LG SmartFont";
font-size: 17px;
}
.newTodayStr {
width: 168px;
margin-left: 235px;
margin-top: -3px;
}
}
.favFocus {
.thumbContainer {
//transform: scale(1.05);
background-image: url("../../../assets/images/player/selected_frame_test.png");
width: 408px; //
height: 100%; //
/*&.rightTransformOrigin{
transform-origin: right;
}*/
.image {
width: 328px; // old = 83%
height: 188px; // old = 58.5%
margin-top: 25px; // old = 8%
margin-left: 28px;
}
&.thumbContaineryoutubeChannel {
background-image: url("../../../assets/images/player/focus_frame_channel.png");
width: 360px; //
height: 304px; //
margin-left: 18px;
.youtubeChannel {
width: 176px;
height: 176px;
margin-left: 26%;
margin-top: 6%;
border: 1px solid rgb(104, 99, 116);
border-radius: 70%;
}
}
&.thumbContainerApp {
margin: 0px 18px;
width: 360px;
height: 304px;
background-image: url("../../../assets/images/player/focus_fitness_banner_round.png");
.applogo {
width: 178px;
height: 178px;
margin-left: 25.5%;
margin-top: 6%;
border: 1px solid rgb(104, 99, 116);
}
}
.planItemDetail {
width: 340px;
margin-top: 221px;
margin-left: 20px;
background: linear-gradient(
0deg,
rgba(15, 15, 15, 0.69),
rgba(15, 15, 15, 0.69),
rgba(15, 15, 15, 0.69),
rgba(36, 36, 36, 0)
) !important;
}
.newTodayStr {
height: 150px;
height: 75px;
margin-left: 228px;
margin-top: 3px;
}
}
}
.captionContainer {
bottom: 47px;
.caption {
color: rgb(255, 255, 255) !important;
}
}
.playIcon {
opacity: 1;
&.moreIcon {
background-color: transparent;
}
&.addPlanIcon {
background-color: transparent;
}
}
.addToFavoriteBtn {
opacity: 1;
}
&.bigItem {
:not(.favFocus) .thumbContainer {
background-image: url("../../../assets/images/player/focus_frame_big.png");
.image {
height: 527px;
margin-left: 28px;
width: 345px;
}
}
.favFocus .thumbContainer {
background-image: url("../../../assets/images/player/frame-big_btn_selected.png");
.image {
height: 520px;
margin-left: 28px;
}
}
}
&.planItem {
:not(.favFocus) .thumbContainer {
border: 2px solid rgba(114, 111, 111, 0.292);
border-radius: 25px;
box-shadow: inset 0px 0px 20px rgba(202, 200, 200, 0.15);
margin: 13px 0px 43px 0px;
width: 380px;
height: 583px;
background: linear-gradient(
50deg,
rgba(90, 111, 146, 0.6),
rgba(90, 111, 146, 0.77),
rgba(112, 77, 116, 0.77),
rgba(112, 77, 116, 0.6)
) !important;
background-image: initial;
.image {
background-size: cover;
margin-top: 15px;
margin-left: 20px;
height: 540px;
width: 340px;
border-radius: 20px;
}
.caption {
margin-top: 12px;
}
&.specialBtnContainer {
background-image: initial;
border: 2px solid rgba(114, 111, 111, 0.292);
border-radius: 25px;
box-shadow: inset 0px 0px 20px rgba(202, 200, 200, 0.15);
width: 380px;
background: linear-gradient(
50deg,
rgba(90, 111, 146, 0.6),
rgba(90, 111, 146, 0.77),
rgba(112, 77, 116, 0.77),
rgba(112, 77, 116, 0.6)
) !important;
}
.planItemDetail {
width: 340px;
margin-top: 221px;
margin-left: 20px;
background: linear-gradient(
0deg,
rgba(15, 15, 15, 0.69),
rgba(15, 15, 15, 0.69),
rgba(15, 15, 15, 0.69),
rgba(36, 36, 36, 0)
) !important;
}
}
}
.repeatCountList {
bottom: 97px;
left: 35px;
}
}
.emphasisFont {
font-weight: bold;
font-size: 24px;
}

View File

@@ -0,0 +1,26 @@
/**
* Display text that no files exist
*
* @module NoFileElement
*/
import React, {useMemo} from 'react';
import {$L} from '../../utils/common';
import css from './MediaItem.module.less';
const NoMediaElement = ({dataSize}) => {
if (dataSize === 0) {
return (
<div role="alert" className={css.noMediaText}>
{$L('No Media exist.')}
</div>
);
} else {
return null;
}
};
export default NoMediaElement;

View File

@@ -0,0 +1,6 @@
{
"main": "MediaItem.js",
"styles": [
"MediaItem.module.less"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
@itemHeight: 304px; // 284px
@itemHeightBig: 638px;
.listContainer {
width: ~"calc(100% - 62px)";
display: inline-block;
position: relative;
transition: transform 200ms ease-out;
padding-left: 62px;
.listTitlelayer {
height: 30px;
margin-top: 0px;
margin-bottom: 0px;
left: 14px;
display: inline-block;
position: relative;
.listicon {
display: inline-block;
position: relative;
width: 36px;
height: 20px;
background-size: contain;
background-repeat: no-repeat;
background-image: url("../../../assets/images/player/icon/icon_subtitle.png");
}
.listTitle {
color: rgb(255, 255, 255);
display: inline-block;
position: relative;
width: 1700px;
height: 20px;
line-height: 20px;
margin-left: 9px;
letter-spacing: -1.4px;
font-size: 28px;
}
}
.listMain {
width: 100%;
height: @itemHeight;
white-space: nowrap;
&.bigItem {
height: @itemHeightBig;
}
}
&.vertical {
width: auto;
height: auto;
padding-left: 0px;
overflow: hidden;
min-width: 1800px;
.listMain {
white-space: initial;
height: auto;
}
}
.leftRightScrollArea {
position: absolute;
background-color: blue;
top: 0px;
opacity: 0.3;
width: 100px;
height: 100%;
z-index: 21;
&.hide {
opacity: 0;
}
}
.leftScrollArea {
left: 0px;
}
.rightScrollArea {
right: 0px;
}
}

View File

@@ -0,0 +1,3 @@
{
"main": "MediaList.js"
}

View File

@@ -0,0 +1,882 @@
import ApiDecorator from '@enact/core/internal/ApiDecorator';
import Cancelable from '@enact/ui/Cancelable';
import kind from '@enact/core/kind';
import hoc from '@enact/core/hoc';
import {is} from '@enact/core/keymap';
import {on, off} from '@enact/core/dispatcher';
import Pause from '@enact/spotlight/Pause';
import Slottable from '@enact/ui/Slottable';
import Spotlight from '@enact/spotlight';
import {SpotlightContainerDecorator, spotlightDefaultClass} from '@enact/spotlight/SpotlightContainerDecorator';
import {forward} from '@enact/core/handle';
import {Job} from '@enact/core/util';
import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
import $L from '@enact/sandstone/internal/$L';
import {compareChildren} from '@enact/sandstone/internal/util';
import ActionGuide from '@enact/sandstone/ActionGuide';
import Button from '@enact/sandstone/Button';
import {countReactChildren} from './util';
import css from './MediaControls.module.less';
const OuterContainer = SpotlightContainerDecorator({
defaultElement: [
`.${spotlightDefaultClass}`
]
}, 'div');
const Container = SpotlightContainerDecorator({
enterTo: 'default-element'
}, 'div');
const MediaButton = onlyUpdateForKeys([
'children',
'className',
'disabled',
'icon',
'onClick',
'spotlightDisabled'
])(Button);
const forwardToggleMore = forward('onToggleMore');
const animationDuration = 300;
/**
* A set of components for controlling media playback and rendering additional components.
*
* @class MediaControlsBase
* @memberof sandstone/MediaPlayer
* @ui
* @private
*/
const MediaControlsBase = kind({
name: 'MediaControls',
// intentionally assigning these props to MediaControls instead of Base (which is private)
propTypes: /** @lends sandstone/MediaPlayer.MediaControls.prototype */ {
/**
* DOM id for the component.
*
* This child component `ActionGuide`'s id is generated from the id.
*
* @type {String}
* @required
* @public
*/
id: PropTypes.string.isRequired,
/**
* The `aria-label` for the action guide.
*
* @type {String}
* @public
*/
actionGuideAriaLabel: PropTypes.string,
/**
* The label for the action guide.
*
* @type {String}
* @public
*/
actionGuideLabel: PropTypes.string,
/**
* These components are placed below the action guide. Typically these will be media playlist controls.
*
* @type {Node}
* @public
*/
bottomComponents: PropTypes.node,
/**
* Jump backward [icon]{@link sandstone/Icon.Icon} name. Accepts any
* [icon]{@link sandstone/Icon.Icon} component type.
*
* @type {String}
* @default 'jumpbackward'
* @public
*/
jumpBackwardIcon: PropTypes.string,
/**
* Disables state on the media "jump" buttons; the outer pair.
*
* @type {Boolean}
* @public
*/
jumpBackwardDisabled: PropTypes.bool,
jumpForwardDisabled: PropTypes.bool,
/**
* Jump forward [icon]{@link sandstone/Icon.Icon} name. Accepts any
* [icon]{@link sandstone/Icon.Icon} component type.
*
* @type {String}
* @default 'jumpforward'
* @public
*/
jumpForwardIcon: PropTypes.string,
/**
* Disables the media buttons.
*
* @type {Boolean}
* @public
*/
mediaDisabled: PropTypes.bool,
/**
* When `true`, more components are rendered. This does not indicate the visibility of more components.
*
* @type {Boolean}
* @public
*/
moreComponentsRendered: PropTypes.bool,
/**
* The spotlight ID for the moreComponent container.
*
* @type {String}
* @public
* @default 'moreComponents'
*/
moreComponentsSpotlightId: PropTypes.string,
/**
* Removes the "jump" buttons. The buttons that skip forward or backward in the video.
*
* @type {Boolean}
* @public
*/
noJumpButtons: PropTypes.bool,
/**
* Called when cancel/back key events are fired.
*
* @type {Function}
* @public
*/
onClose: PropTypes.func,
/**
* Called when the user clicks the JumpBackward button
*
* @type {Function}
* @public
*/
onJumpBackwardButtonClick: PropTypes.func,
/**
* Called when the user clicks the JumpForward button.
*
* @type {Function}
* @public
*/
onJumpForwardButtonClick: PropTypes.func,
/**
* Called when the user presses a media control button.
*
* @type {Function}
* @public
*/
onKeyDownFromMediaButtons: PropTypes.func,
/**
* Called when the user clicks the Play button.
*
* @type {Function}
* @public
*/
onPlayButtonClick: PropTypes.func,
/**
* `true` when the video is paused.
*
* @type {Boolean}
* @public
*/
paused: PropTypes.bool,
/**
* A string which is sent to the `pause` icon of the player controls. This can be
* anything that is accepted by [Icon]{@link sandstone/Icon.Icon}. This will be temporarily replaced by
* the [playIcon]{@link sandstone/MediaPlayer.MediaControls.playIcon} when the
* [paused]{@link sandstone/MediaPlayer.MediaControls.paused} boolean is `false`.
*
* @type {String}
* @default 'pause'
* @public
*/
pauseIcon: PropTypes.string,
/**
* A string which is sent to the `play` icon of the player controls. This can be
* anything that is accepted by {@link sandstone/Icon.Icon}. This will be temporarily replaced by
* the [pauseIcon]{@link sandstone/MediaPlayer.MediaControls.pauseIcon} when the
* [paused]{@link sandstone/MediaPlayer.MediaControls.paused} boolean is `true`.
*
* @type {String}
* @default 'play'
* @public
*/
playIcon: PropTypes.string,
/**
* Disables the media "play"/"pause" button.
*
* @type {Boolean}
* @public
*/
playPauseButtonDisabled: PropTypes.bool,
/**
* When `true`, more components are visible.
*
* @type {Boolean}
* @private
*/
showMoreComponents: PropTypes.bool,
/**
* `true` controls are disabled from Spotlight.
*
* @type {Boolean}
* @public
*/
spotlightDisabled: PropTypes.bool,
/**
* The spotlight ID for the media controls container.
*
* @type {String}
* @public
* @default 'mediaControls'
*/
spotlightId: PropTypes.string,
/**
* The visibility of the component. When `false`, the component will be hidden.
*
* @type {Boolean}
* @default true
* @public
*/
visible: PropTypes.bool,
onActionGuideClick: PropTypes.func
},
defaultProps: {
jumpBackwardIcon: 'jumpbackward',
jumpForwardIcon: 'jumpforward',
moreComponentsSpotlightId: 'moreComponents',
spotlightId: 'mediaControls',
pauseIcon: 'pause',
playIcon: 'play',
visible: true
},
styles: {
css,
className: 'controlsFrame'
},
computed: {
actionGuideClassName: ({styler, showMoreComponents}) => styler.join({hidden: showMoreComponents}),
actionGuideShowing: ({bottomComponents, children}) => countReactChildren(children) || bottomComponents,
className: ({visible, styler}) => styler.append({hidden: !visible}),
moreButtonsClassName: ({styler}) => styler.join('mediaControls', 'moreButtonsComponents'),
moreComponentsRendered: ({showMoreComponents, moreComponentsRendered}) => showMoreComponents || moreComponentsRendered
},
render: ({
actionGuideAriaLabel,
actionGuideLabel,
actionGuideShowing,
children,
id,
jumpBackwardIcon,
jumpBackwardDisabled,
jumpForwardDisabled,
jumpForwardIcon,
bottomComponents,
mediaDisabled,
moreComponentsSpotlightId,
noJumpButtons,
onJumpBackwardButtonClick,
onJumpForwardButtonClick,
onKeyDownFromMediaButtons,
onPlayButtonClick,
paused,
pauseIcon,
playIcon,
playPauseButtonDisabled,
showMoreComponents,
moreComponentsRendered,
moreButtonsClassName,
actionGuideClassName,
spotlightDisabled,
spotlightId,
onActionGuideClick,
...rest
}) => {
delete rest.onClose;
delete rest.visible;
return (
<OuterContainer {...rest} id={id} spotlightId={spotlightId}>
<Container className={css.mediaControls} spotlightDisabled={spotlightDisabled} onKeyDown={onKeyDownFromMediaButtons}>
{noJumpButtons ? null : <MediaButton aria-label={$L('Previous')} backgroundOpacity="transparent" css={css} disabled={mediaDisabled || jumpBackwardDisabled} icon={jumpBackwardIcon} onClick={onJumpBackwardButtonClick} size="large" spotlightDisabled={spotlightDisabled} />}
<MediaButton aria-label={paused ? $L('Play') : $L('Pause')} className={spotlightDefaultClass} backgroundOpacity="transparent" css={css} disabled={mediaDisabled || playPauseButtonDisabled} icon={paused ? playIcon : pauseIcon} onClick={onPlayButtonClick} size="large" spotlightDisabled={spotlightDisabled} />
{noJumpButtons ? null : <MediaButton aria-label={$L('Next')} backgroundOpacity="transparent" css={css} disabled={mediaDisabled || jumpForwardDisabled} icon={jumpForwardIcon} onClick={onJumpForwardButtonClick} size="large" spotlightDisabled={spotlightDisabled} />}
</Container>
{actionGuideShowing ?
<ActionGuide id={`${id}_actionGuide`} aria-label={actionGuideAriaLabel != null ? actionGuideAriaLabel : null} css={css} className={actionGuideClassName} icon="arrowsmalldown" onClick={onActionGuideClick}>{actionGuideLabel}</ActionGuide> :
null
}
{moreComponentsRendered ?
<Container spotlightId={moreComponentsSpotlightId} className={css.moreComponents} spotlightDisabled={!showMoreComponents || spotlightDisabled}>
<Container className={moreButtonsClassName} >
{children}
</Container>
<div>
{bottomComponents}
</div>
</Container> :
null
}
</OuterContainer>
);
}
});
/**
* Media control behaviors to apply to [MediaControlsBase]{@link sandstone/MediaPlayer.MediaControlsBase}.
* Provides built-in support for showing more components and key handling for basic playback
* controls.
*
* @class MediaControlsDecorator
* @memberof sandstone/MediaPlayer
* @mixes ui/Slottable.Slottable
* @hoc
* @private
*/
const MediaControlsDecorator = hoc((config, Wrapped) => { // eslint-disable-line no-unused-vars
class MediaControlsDecoratorHOC extends React.Component {
static displayName = 'MediaControlsDecorator';
static propTypes = /** @lends sandstone/MediaPlayer.MediaControlsDecorator.prototype */ {
/**
* The label for the action guide.
*
* @type {String}
* @public
*/
actionGuideLabel: PropTypes.string,
/**
* These components are placed below the children. Typically these will be media playlist items.
*
* @type {Node}
* @public
*/
bottomComponents: PropTypes.node,
/**
* The number of milliseconds that the player will pause before firing the
* first jump event on a right or left pulse.
*
* @type {Number}
* @default 400
* @public
*/
initialJumpDelay: PropTypes.number,
/**
* The number of milliseconds that the player will throttle before firing a
* jump event on a right or left pulse.
*
* @type {Number}
* @default 200
* @public
*/
jumpDelay: PropTypes.number,
/**
* Disables the media buttons.
*
* @type {Boolean}
* @public
*/
mediaDisabled: PropTypes.bool,
/**
* Disables showing more components.
*
* @type {Boolean}
* @public
*/
moreActionDisabled: PropTypes.bool,
/**
* Setting this to `true` will disable left and right keys for seeking.
*
* @type {Boolean}
* @public
*/
no5WayJump: PropTypes.bool,
/**
* Called when media fast forwards.
*
* @type {Function}
* @public
*/
onFastForward: PropTypes.func,
/**
* Called when media jumps.
*
* @type {Function}
* @public
*/
onJump: PropTypes.func,
/**
* Called when media gets paused.
*
* @type {Function}
* @public
*/
onPause: PropTypes.func,
/**
* Called when media starts playing.
*
* @type {Function}
* @public
*/
onPlay: PropTypes.func,
/**
* Called when media rewinds.
*
* @type {Function}
* @public
*/
onRewind: PropTypes.func,
/**
* Called when the visibility of more components is changed
*
* Event payload includes:
*
* * `type` - Type of event, `'onToggleMore'`
* * `showMoreComponents` - `true` when the components are visible`
* * `liftDistance` - The distance, in pixels, the component animates
*
* @type {Function}
* @public
*/
onToggleMore: PropTypes.func,
/**
* The video pause state.
*
* @type {Boolean}
* @public
*/
paused: PropTypes.bool,
/**
* Disables state on the media "play"/"pause" button
*
* @type {Boolean}
* @public
*/
playPauseButtonDisabled: PropTypes.bool,
/**
* Disables the media playback-rate control via rewind and fast forward keys
*
* @type {Boolean}
* @public
*/
rateChangeDisabled: PropTypes.bool,
/**
* Registers the MediaControls component with an
* [ApiDecorator]{@link core/internal/ApiDecorator.ApiDecorator}.
*
* @type {Function}
* @private
*/
setApiProvider: PropTypes.func,
/**
* The visibility of the component. When `false`, the component will be hidden.
*
* @type {Boolean}
* @public
*/
visible: PropTypes.bool
};
static defaultProps = {
initialJumpDelay: 400,
jumpDelay: 200
};
constructor (props) {
super(props);
this.mediaControlsNode = null;
this.moreComponentsNode = null;
this.actionGuideHeight = 0;
this.animation = null;
this.bottomComponentsHeight = 0;
this.keyLoop = null;
this.pulsingKeyCode = null;
this.pulsing = null;
this.paused = new Pause('MediaPlayer');
this.state = {
showMoreComponents: false,
moreComponentsRendered: false
};
if (props.setApiProvider) {
props.setApiProvider(this);
}
}
static getDerivedStateFromProps (props) {
if (!props.visible) {
return {
showMoreComponents: false
};
}
return null;
}
componentDidMount () {
on('keydown', this.handleKeyDown);
on('keyup', this.handleKeyUp);
on('blur', this.handleBlur, window);
on('wheel', this.handleWheel);
}
componentDidUpdate (prevProps, prevState) {
// Need to render `moreComponents` to show it. For performance, render `moreComponents` if it is actually shown.
if (!prevState.showMoreComponents && this.state.showMoreComponents && !this.state.moreComponentsRendered) {
this.moreComponentsRenderingJob.startRafAfter();
} else if (prevState.showMoreComponents && !this.state.showMoreComponents) {
this.moreComponentsRenderingJob.stop();
}
if (!prevState.moreComponentsRendered && this.state.moreComponentsRendered ||
this.state.moreComponentsRendered && prevProps.bottomComponents !== this.props.bottomComponents ||
!compareChildren(this.props.children, prevProps.children)
) {
this.calculateMoreComponentsHeight();
}
if (this.state.showMoreComponents && !prevState.moreComponentsRendered && this.state.moreComponentsRendered ||
this.state.moreComponentsRendered && prevState.showMoreComponents !== this.state.showMoreComponents
) {
forwardToggleMore({
type: 'onToggleMore',
showMoreComponents: this.state.showMoreComponents,
liftDistance: this.bottomComponentsHeight - this.actionGuideHeight
}, this.props);
if (this.state.showMoreComponents) {
this.moreComponentsNode = this.moreComponentsNode || this.mediaControlsNode.querySelector(`.${css.moreComponents}`);
this.paused.pause();
this.animation = this.moreComponentsNode.animate([
{transform: 'none', opacity: 0, offset: 0},
{transform: `translateY(${-this.actionGuideHeight}px)`, opacity: 1, offset: 1}
], {
duration: animationDuration,
fill: 'forwards'
});
this.animation.onfinish = this.handleFinish;
this.animation.oncancel = this.handleCancel;
} else if (this.animation != null) {
this.animation.cancel();
}
}
// if media controls disabled, reset key loop
if (!prevProps.mediaDisabled && this.props.mediaDisabled) {
this.stopListeningForPulses();
this.paused.resume();
}
}
componentWillUnmount () {
off('keydown', this.handleKeyDown);
off('keyup', this.handleKeyUp);
off('blur', this.handleBlur, window);
off('wheel', this.handleWheel);
this.stopListeningForPulses();
this.moreComponentsRenderingJob.stop();
if (this.animation) {
this.animation.cancel();
}
}
moreComponentsRenderingJob = new Job(() => {
this.setState({
moreComponentsRendered: true
});
});
calculateMoreComponentsHeight = () => {
if (!this.mediaControlsNode) {
this.bottomComponentsHeight = 0;
return;
}
const bottomElement = this.mediaControlsNode.querySelector(`.${css.moreComponents}`);
this.bottomComponentsHeight = bottomElement ? bottomElement.scrollHeight : 0;
};
handleKeyDownFromMediaButtons = (ev) => {
if (is('down', ev.keyCode) && !this.state.showMoreComponents && !this.props.moreActionDisabled) {
this.showMoreComponents();
ev.stopPropagation();
}
};
handleKeyDown = (ev) => {
const {
mediaDisabled,
no5WayJump,
visible
} = this.props;
const current = Spotlight.getCurrent();
if (!no5WayJump &&
!visible &&
!mediaDisabled &&
!current &&
(is('left', ev.keyCode) || is('right', ev.keyCode))) {
this.paused.pause();
this.startListeningForPulses(ev.keyCode);
}
};
handleKeyUp = (ev) => {
const {
mediaDisabled,
no5WayJump,
rateChangeDisabled,
playPauseButtonDisabled
} = this.props;
if (mediaDisabled) return;
if (!playPauseButtonDisabled) {
if (is('play', ev.keyCode)) {
forward('onPlay', ev, this.props);
} else if (is('pause', ev.keyCode)) {
forward('onPause', ev, this.props);
}
}
if (!no5WayJump && (is('left', ev.keyCode) || is('right', ev.keyCode))) {
this.stopListeningForPulses();
this.paused.resume();
}
if (!rateChangeDisabled) {
if (is('rewind', ev.keyCode)) {
forward('onRewind', ev, this.props);
} else if (is('fastForward', ev.keyCode)) {
forward('onFastForward', ev, this.props);
}
}
};
handleBlur = () => {
this.stopListeningForPulses();
this.paused.resume();
};
handleWheel = (ev) => {
if (!this.state.showMoreComponents && this.props.visible && !this.props.moreActionDisabled && ev.deltaY > 0) {
this.showMoreComponents();
}
};
startListeningForPulses = (keyCode) => {
// Ignore new pulse calls if key code is same, otherwise start new series if we're pulsing
if (this.pulsing && keyCode !== this.pulsingKeyCode) {
this.stopListeningForPulses();
}
if (!this.pulsing) {
this.pulsingKeyCode = keyCode;
this.pulsing = true;
this.keyLoop = setTimeout(this.handlePulse, this.props.initialJumpDelay);
forward('onJump', {keyCode}, this.props);
}
};
handlePulse = () => {
forward('onJump', {keyCode: this.pulsingKeyCode}, this.props);
this.keyLoop = setTimeout(this.handlePulse, this.props.jumpDelay);
};
handlePlayButtonClick = (ev) => {
forward('onPlayButtonClick', ev, this.props);
if (this.props.paused) {
forward('onPlay', ev, this.props);
} else {
forward('onPause', ev, this.props);
}
};
stopListeningForPulses () {
this.pulsing = false;
if (this.keyLoop) {
clearTimeout(this.keyLoop);
this.keyLoop = null;
}
}
getMediaControls = (node) => {
if (!node) {
this.actionGuideHeight = 0;
return;
}
this.mediaControlsNode = ReactDOM.findDOMNode(node); // eslint-disable-line react/no-find-dom-node
const guideElement = this.mediaControlsNode.querySelector(`.${css.actionGuide}`);
this.actionGuideHeight = guideElement ? guideElement.scrollHeight : 0;
};
areMoreComponentsAvailable = () => {
return this.state.showMoreComponents;
};
showMoreComponents = () => {
this.setState({showMoreComponents: true});
};
hideMoreComponents = () => {
this.setState({showMoreComponents: false});
};
toggleMoreComponents () {
this.setState((prevState) => {
return {
showMoreComponents: !prevState.showMoreComponents
};
});
}
handleClose = (ev) => {
if (this.props.visible) {
forward('onClose', ev, this.props);
}
};
handleFinish = () => {
if (this.state.showMoreComponents) {
this.paused.resume();
if (!Spotlight.getPointerMode()) {
Spotlight.move('down');
}
}
};
handleCancel = () => {
this.paused.resume();
};
onActionGuideClick = () => {
this.showMoreComponents();
};
render () {
const props = Object.assign({}, this.props);
delete props.initialJumpDelay;
delete props.jumpDelay;
delete props.moreActionDisabled;
delete props.no5WayJump;
delete props.onFastForward;
delete props.onJump;
delete props.onPause;
delete props.onPlay;
delete props.onRewind;
delete props.onToggleMore;
delete props.rateChangeDisabled;
delete props.setApiProvider;
return (
<Wrapped
ref={this.getMediaControls}
{...props}
moreComponentsRendered={this.state.moreComponentsRendered}
onClose={this.handleClose}
onKeyDownFromMediaButtons={this.handleKeyDownFromMediaButtons}
onPlayButtonClick={this.handlePlayButtonClick}
onTransitionEnd={this.handleTransitionEnd}
showMoreComponents={this.state.showMoreComponents}
onActionGuideClick={this.onActionGuideClick}
/>
);
}
}
return Slottable({slots: ['bottomComponents']}, MediaControlsDecoratorHOC);
});
const handleCancel = (ev, {onClose}) => {
if (onClose) {
onClose(ev);
}
};
/**
* A set of components for controlling media playback and rendering additional components.
*
* This uses [Slottable]{@link ui/Slottable} to accept the custom tags, `<bottomComponents>`
* to add components to the bottom of the media controls. Any additional children will be
* rendered into the "more" controls area. Showing the additional components is handled by
* `MediaControls` when the user navigates down from the media buttons.
*
* @class MediaControls
* @memberof sandstone/MediaPlayer
* @mixes ui/Cancelable.Cancelable
* @ui
* @public
*/
const MediaControls = ApiDecorator(
{api: [
'areMoreComponentsAvailable',
'showMoreComponents',
'hideMoreComponents'
]},
MediaControlsDecorator(
Cancelable({modal: true, onCancel: handleCancel},
MediaControlsBase
)
)
);
MediaControls.defaultSlot = 'mediaControlsComponent';
export default MediaControls;
export {
MediaControlsBase,
MediaControls,
MediaControlsDecorator
};

View File

@@ -0,0 +1,69 @@
@import "~@enact/sandstone/styles/variables.less";
@import "~@enact/sandstone/styles/mixins.less";
@import "~@enact/sandstone/styles/skin.less";
.controlsFrame {
position: relative;
display: block;
&.hidden {
will-change: opacity;
opacity: 0;
}
.mediaControls {
text-align: center;
direction: ltr;
white-space: nowrap;
> *:first-child {
margin-inline-start: 0;
}
}
.actionGuide {
padding-top: 39px;
transition: opacity @sand-mediaplayer-controls-actionguide-time linear;
&.hidden {
opacity: 0;
}
}
.moreComponents {
position: absolute;
left: 0;
right: 0;
height: 0px;
opacity: 0;
.moreButtonsComponents {
> * {
margin: 0;
margin-inline-start: 114px;
&:first-child {
margin-inline-start: 0;
}
}
}
> :first-child {
margin-top: 21px;
}
}
.button {
height: 90px;
min-width: 90px;
margin: 0;
margin-inline-start: 45px;
.client {
padding: 0 9px;
}
.bg {
border-radius: 48px;
}
}
}

View File

@@ -0,0 +1,54 @@
import kind from '@enact/core/kind';
import {Knob} from '@enact/ui/Slider';
import PropTypes from 'prop-types';
import React from 'react';
/**
* Knob for the MediaSlider in {@link sandstone/MediaPlayer}.
*
* @class MediaKnob
* @memberof sandstone/MediaPlayer
* @ui
* @private
*/
const MediaKnob = kind({
name: 'MediaKnob',
propTypes: {
preview: PropTypes.bool,
previewProportion: PropTypes.number,
value: PropTypes.number
},
computed: {
style: ({style, preview, previewProportion}) => {
if (!preview) {
return style;
}
return {
...style,
'--ui-slider-proportion-end-knob': previewProportion
};
}
},
render: ({preview, previewProportion, value, ...rest}) => {
if (preview) {
value = previewProportion;
}
return (
<Knob
{...rest}
proportion={value}
value={value}
/>
);
}
});
export default MediaKnob;
export {
MediaKnob
};

View File

@@ -0,0 +1,119 @@
import kind from '@enact/core/kind';
import PropTypes from 'prop-types';
import React from 'react';
import Slider from '@enact/sandstone/Slider';
import MediaKnob from './MediaKnob';
import MediaSliderDecorator from './MediaSliderDecorator';
import css from './MediaSlider.module.less';
/**
* The base component to render a customized [Slider]{@link sandstone/Slider.Slider} for use in
* media player components such as [VideoPlayer]{@link sandstone/VideoPlayer.VideoPlayer}.
*
* @class MediaSliderBase
* @memberof sandstone/MediaPlayer
* @ui
* @private
*/
const MediaSliderBase = kind({
name: 'MediaSlider',
propTypes: /** @lends sandstone/MediaPlayer.MediaSlider.prototype */ {
/**
* When `true`, the knob will expand. Note that Slider is a controlled
* component. Changing the value would only affect pressed visual and
* not the state.
*
* @type {Boolean}
* @public
*/
forcePressed: PropTypes.bool,
/**
* Allow moving the knob via pointer or 5-way without emitting `onChange` events
*
* @type {Boolean}
* @default false
* @public
*/
preview: PropTypes.bool,
/**
* The position of the knob when in `preview` mode
*
* @type {Number}
* @public
*/
previewProportion: PropTypes.number,
/**
* The visibility of the component. When `false`, the component will be hidden.
*
* @type {Boolean}
* @default true
* @public
*/
visible: PropTypes.bool
},
defaultProps: {
preview: false,
visible: true
},
styles: {
css,
className: 'sliderFrame'
},
computed: {
className: ({styler, visible}) => styler.append({hidden: !visible}),
sliderClassName: ({styler, forcePressed}) => styler.join({
pressed: forcePressed,
mediaSlider: true
})
},
render: ({className, preview, previewProportion, sliderClassName, ...rest}) => {
delete rest.forcePressed;
delete rest.visible;
return (
<div className={className}>
<Slider
{...rest}
aria-hidden="true"
className={sliderClassName}
css={css}
knobComponent={
<MediaKnob preview={preview} previewProportion={previewProportion} />
}
max={1}
min={0}
step={0.00001}
/>
</div>
);
}
});
/**
* A customized slider suitable for use within media player components such as
* [VideoPlayer]{@link sandstone/VideoPlayer.VideoPlayer}.
*
* @class MediaSlider
* @extends sandstone/Slider.Slider
* @memberof sandstone/MediaPlayer
* @ui
* @public
*/
const MediaSlider = MediaSliderDecorator(MediaSliderBase);
export default MediaSlider;
export {
MediaSlider,
MediaSliderBase
};

View File

@@ -0,0 +1,125 @@
// MediaSlider.module.less
//
@import "~@enact/sandstone/styles/variables.less";
@import "~@enact/sandstone/styles/skin.less";
@sand-mediaplayer-slider-tap-area: 70px;
@sand-mediaplayer-slider-height: 6px;
@sand-mediaplayer-slider-knob-size: 24px;
@sand-video-player-padding-side: 42px;
.sliderFrame {
@knob-transform-active: @sand-translate-center scale(1);
@knob-transform-resting: @sand-translate-center scale(@sand-mediaplayer-slider-knob-resting-state-scale);
@slider-padding-v: ((@sand-mediaplayer-slider-tap-area - @sand-mediaplayer-slider-height) / 2);
@slider-padding-h: @sand-mediaplayer-slider-knob-size;
margin-left: 130px;
margin-right: 130px;
flex: 1 0 auto;
&.hidden {
will-change: opacity;
opacity: 0;
}
.mediaSlider {
margin: 0 @slider-padding-h;
padding: @slider-padding-v 0;
height: @sand-mediaplayer-slider-height;
// Add a tap area that extends to the edges of the screen, to make the slider more accessible
&::before {
content: "";
position: absolute;
.position(0, -(@sand-video-player-padding-side));
}
// Grow the knob when the Slider gets spotted
.focus({
&.active,
&.pressed {
.knob::before {
transform: @knob-transform-active;
opacity: 1;
}
}
});
.spottable({
&.pressed {
.knob::before {
transform: @knob-transform-active;
opacity: 1;
}
}
});
}
// Knob
.knob {
@activate-transition-function: cubic-bezier(0.15, 0.85, 0.6, 1.65);
//@slide-transition-function: cubic-bezier(0.15, 0.85, 0.53, 1.09);
//-webkit-transition: -webkit-transform @slide-transition-function 0.2s;
//transition: transform @slide-transition-function 0.2s;
&::before {
width: @sand-mediaplayer-slider-knob-size;
height: @sand-mediaplayer-slider-knob-size;
border-width: 0;
border-radius: @sand-mediaplayer-slider-knob-size;
transform: @knob-transform-resting;
opacity: 0;
will-change: transform, opacity;
-webkit-transition: -webkit-transform @activate-transition-function 0.2s, opacity ease 0.2s;
transition: transform @activate-transition-function 0.2s, opacity ease 0.2s;
}
}
&.scrubbing {
.knob {
display: block;
}
}
}
.applySkins({
.sliderFrame {
.slider {
.bar {
background-color: @sand-mediaslider-bar-bg-color;
}
.fill {
background-color: @sand-mediaslider-fill-bg-color;
}
.knob {
&::before {
background-color: @sand-mediaplayer-slider-knob-color;
}
}
.focus({
.bar {
background-color: @sand-mediaslider-bar-bg-color;
}
.fill {
background-color: @sand-mediaslider-fill-bg-color;
}
.disabled({
.bar {
opacity: @sand-mediaslider-disabled-bar-opacity;
}
});
});
.disabled({
.bar {
opacity: @sand-mediaslider-disabled-bar-opacity;
}
});
}
}
});

View File

@@ -0,0 +1,226 @@
import {adaptEvent, call, handle, forKey, forward, oneOf, preventDefault, returnsTrue, stopImmediate} from '@enact/core/handle';
import hoc from '@enact/core/hoc';
import platform from '@enact/core/platform';
import {calcProportion} from '@enact/ui/Slider/utils';
import clamp from 'ramda/src/clamp';
import PropTypes from 'prop-types';
import React from 'react';
// decrements the MediaKnob position if we're tracking
const decrement = (state) => {
if (state.tracking && state.x > 0) {
const x = Math.max(0, state.x - 0.05);
return {x};
}
return null;
};
// increments the MediaKnob position if we're tracking
const increment = (state) => {
if (state.tracking && state.x < 1) {
const x = Math.min(1, state.x + 0.05);
return {x};
}
return null;
};
const handleBlur = handle(
forward('onBlur'),
call('untrack')
);
const handleFocus = handle(
forward('onFocus'),
// extract target from the event and pass it to track()
adaptEvent(({target}) => target, call('track'))
);
const handleKeyDown = handle(
forward('onKeyDown'),
call('isTracking'),
// if tracking and the key is left/right update the preview x position
oneOf(
[forKey('left'), returnsTrue(call('decrement'))],
[forKey('right'), returnsTrue(call('increment'))]
),
// if we handled left or right, preventDefault to prevent browser scroll behavior
preventDefault,
// stopImmediate to prevent spotlight handling
stopImmediate
);
const handleKeyUp = handle(
forward('onKeyUp'),
call('isTracking'),
forKey('enter'),
// prevent sandstone/Slider from activating the knob
preventDefault,
adaptEvent(call('getEventPayload'), forward('onChange'))
);
/**
* MediaSlider for {@link sandstone/VideoPlayer}.
*
* @class MediaSliderDecorator
* @memberof sandstone/MediaPlayer
* @hoc
* @private
*/
const MediaSliderDecorator = hoc((config, Wrapped) => { // eslint-disable-line no-unused-vars
return class extends React.Component {
static displayName = 'MediaSliderDecorator';
static propTypes = {
backgroundProgress: PropTypes.number,
selection: PropTypes.arrayOf(PropTypes.number),
value: PropTypes.number
};
constructor (props) {
super(props);
this.handleMouseOver = this.handleMouseOver.bind(this);
this.handleMouseOut = this.handleMouseOut.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
if (platform.touch) {
this.handleTouchMove = this.handleTouchMove.bind(this);
}
handleBlur.bindAs(this, 'handleBlur');
handleFocus.bindAs(this, 'handleFocus');
handleKeyDown.bindAs(this, 'handleKeyDown');
handleKeyUp.bindAs(this, 'handleKeyUp');
this.state = {
maxX: 0,
minX: 0,
tracking: false,
x: 0
};
}
componentDidUpdate (prevProps, prevState) {
if (prevState.x !== this.state.x) {
forward('onKnobMove', this.getEventPayload('onKnobMove'), this.props);
}
}
getEventPayload (type) {
return {
type,
value: this.state.x,
proportion: this.state.x
};
}
track (target) {
const bounds = target.getBoundingClientRect();
this.setState({
maxX: bounds.right,
minX: bounds.left,
tracking: true,
x: this.props.value
});
}
move (clientX) {
this.setState((state) => {
const value = clamp(state.minX, state.maxX, clientX);
return {
x: calcProportion(state.minX, state.maxX, value)
};
});
}
untrack () {
this.setState({
maxX: 0,
minX: 0,
tracking: false
});
}
decrement () {
this.setState(decrement);
}
increment () {
this.setState(increment);
}
isTracking () {
return this.state.tracking;
}
handleMouseOver (ev) {
if (ev.currentTarget.contains(ev.relatedTarget)) {
return;
}
this.track(ev.currentTarget);
this.move(ev.clientX);
}
handleMouseOut (ev) {
if (ev.currentTarget.contains(ev.relatedTarget)) {
return;
}
this.untrack();
}
handleMouseMove (ev) {
this.move(ev.clientX);
}
handleTouchMove (ev) {
// ignores multi touch
this.move(ev.touches[0].clientX);
}
render () {
const {selection, ...rest} = this.props;
let {backgroundProgress} = this.props;
let progressAnchor = 0;
if (selection != null && selection.length > 0) {
// extracts both values from selection, defaulting backgroundProgress to
// progressAnchor if not defined
[progressAnchor, backgroundProgress = progressAnchor] = selection;
}
delete rest.onKnobMove;
if (platform.touch) {
rest.onTouchMove = this.handleTouchMove;
}
return (
<Wrapped
{...rest}
backgroundProgress={backgroundProgress}
onBlur={this.handleBlur}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
onMouseMove={this.handleMouseMove}
preview={this.state.tracking}
previewProportion={this.state.x}
progressAnchor={progressAnchor}
/>
);
}
};
});
export default MediaSliderDecorator;
export {
MediaSliderDecorator
};

View File

@@ -0,0 +1,112 @@
import PropTypes from 'prop-types';
import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';
import kind from '@enact/core/kind';
import React from 'react';
import {secondsToPeriod, secondsToTime} from './util';
import css from './Times.module.less';
/**
* Sandstone-styled formatted time component.
*
* @class Times
* @memberof sandstone/MediaPlayer
* @ui
* @public
*/
const TimesBase = kind({
name: 'Times',
propTypes: /** @lends sandstone/MediaPlayer.Times.prototype */ {
/**
* An instance of a Duration Formatter from i18n.
*
* Must has a `format()` method that returns a string.
*
* @type {Object}
* @required
* @public
*/
formatter: PropTypes.object.isRequired,
/**
* The current time in seconds of the video source.
*
* @type {Number}
* @default 0
* @public
*/
current: PropTypes.number,
/**
* Removes the current time.
*
* @type {Boolean}
* @public
*/
noCurrentTime: PropTypes.bool,
/**
* Removes the total time.
*
* @type {Boolean}
* @public
*/
noTotalTime: PropTypes.bool,
/**
* The total time (duration) in seconds of the loaded video source.
*
* @type {Number}
* @default 0
* @public
*/
total: PropTypes.number
},
defaultProps: {
current: 0,
total: 0
},
styles: {
css,
className: 'times'
},
computed: {
currentPeriod: ({current}) => secondsToPeriod(current),
currentReadable: ({current, formatter}) => secondsToTime(current, formatter),
noSeparator: ({noCurrentTime, noTotalTime}) => noCurrentTime || noTotalTime,
totalPeriod: ({total}) => secondsToPeriod(total),
totalReadable: ({total, formatter}) => secondsToTime(total, formatter)
},
render: ({currentPeriod, currentReadable, noCurrentTime, noSeparator, noTotalTime, totalPeriod, totalReadable, ...rest}) => {
delete rest.current;
delete rest.formatter;
delete rest.total;
return (
<div {...rest}>
{noCurrentTime ?
null :
<time className={css.currentTime} dateTime={currentPeriod}>{currentReadable}</time>
}
{noSeparator ?
null :
<span className={css.separator}>/</span>
}
{noTotalTime ?
null :
<time className={css.totalTime} dateTime={totalPeriod}>{totalReadable}</time>
}
</div>
);
}
});
const Times = onlyUpdateForKeys(['current', 'formatter', 'total'])(TimesBase);
export default Times;
export {Times, TimesBase};

View File

@@ -0,0 +1,28 @@
// Times.module.less
//
@import "~@enact/sandstone/styles/variables.less";
@import "~@enact/sandstone/styles/mixins.less";
.times {
position: absolute;
font-family: "LG SmartFont SemiBold";
width: 100%;
top: 19px;
font-size: 30px;
font-weight: bold;
letter-spacing: -1.4px;
line-height: 30px;
white-space: nowrap;
> * {
display: inline-block;
}
.separator {
padding: 0 1ex;
}
.enact-locale-rtl({
direction: ltr;
});
}

View File

@@ -0,0 +1,21 @@
/**
* Set of Sandstone-styled components to support a custom media player.
*
* @module sandstone/MediaPlayer
* @exports MediaControls
* @exports MediaSlider
* @exports secondsToTime
* @exports Times
*/
import {secondsToTime} from './util';
import MediaControls from './MediaControls';
import MediaSlider from './MediaSlider';
import Times from './Times';
export {
MediaControls,
MediaSlider,
secondsToTime,
Times
};

View File

@@ -0,0 +1,3 @@
{
"main": "index.js"
}

View File

@@ -0,0 +1,83 @@
import React from 'react';
// MediaPlayer utils.js
//
/**
* Create a time object (hour, minute, second) from an amount of seconds.
*
* @param {Number|String} value A duration of time represented in seconds
*
* @returns {Object} An object with keys {hour, minute, second} representing the duration
* seconds provided as an argument.
* @private
*/
const parseTime = (value) => {
value = parseFloat(value);
const time = {};
const hour = Math.floor(value / (60 * 60));
time.minute = Math.floor((value / 60) % 60);
time.second = Math.floor(value % 60);
if (hour) {
time.hour = hour;
}
return time;
};
/**
* Generate a time usable by <time datetime />.
*
* @param {Number|String} seconds A duration of time represented in seconds
*
* @returns {String} String formatted for use in a `datetime` field of a `<time>` tag.
* @private
*/
const secondsToPeriod = (seconds) => {
return 'P' + seconds + 'S';
};
/**
* Formats a duration in seconds into a human-readable time.
*
* @type {Function}
* @param {Number|String} seconds A duration of time represented in seconds
* @param {DurationFmt} durfmt An instance of a `ilib.DurationFmt` object from iLib configured
* to display time
* @param {Object} config Additional configuration object that includes `includeHour`
*
* @returns {String} Formatted duration string
* @memberof sandstone/MediaPlayer
* @public
*/
const secondsToTime = (seconds, durfmt, config) => {
const includeHour = config && config.includeHour;
if (durfmt) {
const parsedTime = parseTime(seconds);
const timeString = durfmt.format(parsedTime).toString();
if (includeHour && !parsedTime.hour) {
return '00:' + timeString;
} else {
return timeString;
}
}
return includeHour ? '00:00:00' : '00:00';
};
/**
* Safely count the children nodes and exclude null & undefined values for an accurate count of
* real children
*
* @param {component} children React.Component or React.PureComponent
* @returns {Number} Number of children nodes
* @private
*/
const countReactChildren = (children) => React.Children.toArray(children).filter(n => n != null).length;
export {
countReactChildren,
parseTime,
secondsToPeriod,
secondsToTime
};

View File

@@ -0,0 +1,64 @@
/**
* TPlayerFeedBackBtn
*
* @module TPlayerFeedBackBtn
*/
import css from './TPlayerFeedBackBtn.module.less';
import classNames from 'classnames';
import React, {useCallback, useEffect} from 'react';
import Spottable from '@enact/spotlight/Spottable';
const SpottableComponent = Spottable('div');
/**
*
* @param {type} left, right
* @returns
*/
const TYPES=["left", "right"];
const TPlayerFeedBackBtn = ({type="left",children, spotlightId, className, onClick, disabled, ...rest}) => {
useEffect(() => {
if(TYPES.indexOf(type) < 0 ){
console.error('TButton type error');
}
}, [type]);
const _onClick = useCallback((ev) => {
if(disabled){
ev.stopPropagation();
return;
}
if (onClick) {
onClick(ev);
}
ev.persist();
}, [ onClick, disabled]);
const _onKeyDown = useCallback((ev) => {
if(ev.key === 'ArrowLeft' && type==="left"){
_onClick(ev);
}else if(ev.key === 'ArrowRight' && type==="right"){
_onClick(ev);
}
}, [type, _onClick]);
return (
<SpottableComponent
{...rest}
className={classNames(css.tButton, type === 'right' ? css.right: null, className ? className: null, disabled ? css.disabled: null)}
onClick={_onClick}
onKeyDown={_onKeyDown}
// data-webos-voice-intent={'Select'}
// data-webos-voice-label={children}
spotlightDisabled={disabled}
spotlightId={spotlightId}
>
<div className={css.skipicon}/>
<div className={css.text}>
{children}
</div>
</SpottableComponent>
);
};
export default TPlayerFeedBackBtn;

View File

@@ -0,0 +1,74 @@
.tButton {
background-color: rgba(233, 233, 233, 0.29);
display: inline-block;
position: relative;
width: 185px;
height: 49px;
color: rgb(255, 255, 255);
border-radius: 0px 43.2px 43.2px 0px;
border: 2px solid rgb(250, 250, 250);
padding-top: 18.72px;
padding-left: 16px;
text-align: center;
font-family: "LG SmartFont SemiBold";
text-shadow: 2.4px 2.4px 16.8px rgba(0, 0, 0, 0.79);
box-shadow: 1.68px 1.68px 12px rgba(0, 0, 0, 0.28);
margin-top: 11px;
.skipicon {
display: block;
position: absolute;
top: 56%;
transform: translateY(-50%);
left: 10px;
width: 40px;
height: 60px;
background-size: contain;
background-repeat: no-repeat;
background-image: url("../../../assets/images/player/icon/icon_player_left.png");
}
&:focus {
background: linear-gradient(
50deg,
rgba(90, 111, 146, 0.6),
rgba(90, 111, 146, 0.77),
rgba(112, 77, 116, 0.77),
rgba(112, 77, 116, 0.6)
);
width: 220px;
height: 60px;
top: 0px;
left: 0px;
color: rgb(255, 255, 255);
border-radius: 0px 48px 48px 0px;
border: 2px solid rgb(250, 250, 250);
font-size: 35px;
padding-top: 30px;
padding-left: 10px;
text-align: center;
text-shadow: 2.4px 2.4px 16.8px rgba(0, 0, 0, 0.79);
box-shadow: 1.68px 1.68px 12px rgba(0, 0, 0, 0.28);
margin-top: 0px;
}
}
// 오른쪽 skip버튼
.right {
padding-left: 0px;
padding-right: 16px;
border-radius: 1.8rem 0rem 0rem 1.8rem;
&:focus {
left: initial;
right: 0px;
border-radius: 2rem 0rem 0rem 2rem;
}
.skipicon {
left: initial;
right: 10px;
background-image: url("../../../assets/images/player/icon/icon_player_right.png");
}
}
.disabled {
background-color: initial;
border: initial;
box-shadow: initial;
}

View File

@@ -0,0 +1,3 @@
{
"main": "TPlayerFeedBackBtn.js"
}

View File

@@ -0,0 +1,100 @@
import React from "react";
import PropTypes from "prop-types";
import onlyUpdateForKeys from "recompose/onlyUpdateForKeys";
import kind from "@enact/core/kind";
import { $L } from "../../utils/helperMethods";
import TPlayerFeedBackBtn from "../TPlayerFeedBackBtn";
import css from "./Feedback.module.less";
import states from "./FeedbackIcons.js";
/**
* Feedback {@link sandstone/VideoPlayer}. This displays the media's playback rate and other
* information.
*
* @class Feedback
* @memberof sandstone/VideoPlayer
* @ui
* @private
*/
const FeedbackBase = kind({
name: "Feedback",
propTypes: /** @lends sandstone/VideoPlayer.Feedback.prototype */ {
children: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/**
* Refers to one of the following possible media playback states.
* `'play'`, `'pause'`, `'rewind'`, `'fastForward'` ,
* `'jumpBackward'`, `'jumpForward'`, `'jumpToStart'`, `'jumpToEnd'`, `'stop'`.
*
* Each state understands where its related icon should be positioned, and whether it should
* respond to changes to the `visible` property.
*
* This string feeds directly into {@link sandstone/FeedbackIcon.FeedbackIcon}.
*
* @type {('play'|'pause'|'rewind'|'fastForward'|'jumpBackward'|'jumpForward'|'jumpToStart'|'jumpToEnd'|'stop')}
* @public
*/
playbackState: PropTypes.oneOf(Object.keys(states)),
/**
* If the current `playbackState` allows this component's visibility to be changed,
* this component will be hidden. If not, setting this property will have no effect.
* All `playbackState`s respond to this property except the following:
* `'rewind'`, `'fastForward'`.
*
* @type {Boolean}
* @default true
* @public
*/
visible: PropTypes.bool,
},
defaultProps: {
visible: true,
},
styles: {
css,
className: "feedback",
},
computed: {
className: ({ styler, visible }) => styler.append({ hidden: !visible }),
children: ({ children, playbackState: s }) => {
if (states[s]) {
// Working with a known state, treat `children` as playbackRate
// if (states[s].message && children !== 1) { // `1` represents a playback rate of 1:1
// return children.toString().replace(/^-/, '') + states[s].message;
// }
} else {
// Custom Message
return children;
}
},
},
render: ({ ...rest }) => {
delete rest.visible;
delete rest.playbackState;
delete rest.children;
return (
<div {...rest}>
<TPlayerFeedBackBtn disabled>{$L("10 sec")}</TPlayerFeedBackBtn>
<TPlayerFeedBackBtn disabled type={"right"}>
{$L("10 sec")}
</TPlayerFeedBackBtn>
</div>
);
},
});
const Feedback = onlyUpdateForKeys(["children", "playbackState", "visible"])(
FeedbackBase
);
export default Feedback;
export { Feedback, FeedbackBase };

View File

@@ -0,0 +1,157 @@
// Feedback.module.less
//
@import "~@enact/sandstone/styles/mixins.less";
@import "~@enact/sandstone/styles/variables.less";
@import "~@enact/sandstone/styles/skin.less";
.feedback {
width: 100%;
display: flex;
justify-content: space-between;
&.hidden {
display: none;
}
.message {
.sand-font({
font-weight: @sand-video-feedback-message-font-weight;
});
font-weight: @sand-video-feedback-message-font-weight;
}
.icon {
font-size: @sand-video-feedback-icon-font-size;
height: inherit; // Use the height and line-height from the parent element to always middle align the icon, regardless of the container's size
line-height: inherit;
vertical-align: bottom;
.applySkins({
color: inherit;
});
}
.skipbuttonB {
background-color: rgba(233, 233, 233, 0.29);
display: block;
position: absolute;
width: 7.7rem;
height: 2.05rem;
top: 0px;
left: 0px;
color: rgb(255, 255, 255);
border-radius: 0rem 1.8rem 1.8rem 0rem;
border: 2px solid rgb(250, 250, 250);
padding-top: 0.78rem;
padding-left: 16px;
text-align: center;
text-shadow: 0.1rem 0.1rem 0.7rem rgba(0, 0, 0, 0.79);
box-shadow: 0.07rem 0.07rem 0.5rem rgba(0, 0, 0, 0.28);
}
.skipiconB {
display: block;
position: absolute;
top: 10px;
left: 10px;
width: 41px;
height: 2.05rem;
background-image: url("../../../assets/images/player/icon/icon_player_left.png");
}
// 오른쪽 skip버튼
.skipbuttonP {
background-color: rgba(233, 233, 233, 0.29);
display: block;
position: absolute;
width: 7.7rem;
height: 2.05rem;
top: 0px;
right: 0px;
color: rgb(255, 255, 255);
border-radius: 1.8rem 0rem 0rem 1.8rem;
border: 2px solid rgb(250, 250, 250);
padding-top: 0.78rem;
padding-right: 16px;
text-align: center;
text-shadow: 0.1rem 0.1rem 0.7rem rgba(0, 0, 0, 0.79);
box-shadow: 0.07rem 0.07rem 0.5rem rgba(0, 0, 0, 0.28);
}
.skipiconP {
display: block;
position: absolute;
top: 10px;
right: 10px;
width: 41px;
height: 2.05rem;
background-image: url("../../../assets/images/player/icon/icon_player_right.png");
}
}
// Focus왼쪽 skip버튼
.skipbuttonFocusB {
background: linear-gradient(
50deg,
rgba(90, 111, 146, 0.6),
rgba(90, 111, 146, 0.77),
rgba(112, 77, 116, 0.77),
rgba(112, 77, 116, 0.6)
);
display: inline-block;
position: absolute;
width: 220px;
height: 57px;
top: 0px;
left: 0px;
color: rgb(255, 255, 255);
border-radius: 0rem 2rem 2rem 0rem;
border: 2px solid rgb(250, 250, 250);
font-size: 35px;
padding-top: 30px;
padding-left: 10px;
text-align: center;
text-shadow: 0.1rem 0.1rem 0.7rem rgba(0, 0, 0, 0.79);
box-shadow: 0.07rem 0.07rem 0.5rem rgba(0, 0, 0, 0.28);
}
.skipiconFocusB {
display: block;
position: absolute;
transform: scale(110%, 110%);
top: 18px;
left: 10px;
width: 40px;
height: 60px;
background-image: url("../../../assets/images/player/icon/icon_player_left.png");
}
// Focus오른쪽 skip버튼
.skipbuttonFocusP {
background: linear-gradient(
50deg,
rgba(90, 111, 146, 0.6),
rgba(90, 111, 146, 0.77),
rgba(112, 77, 116, 0.77),
rgba(112, 77, 116, 0.6)
);
display: inline-block;
position: absolute;
width: 220px;
height: 57px;
top: 0px;
right: 0px;
color: rgb(255, 255, 255);
border-radius: 2rem 0rem 0rem 2rem;
border: 2px solid rgb(250, 250, 250);
font-size: 35px;
padding-top: 30px;
padding-right: 10px;
text-align: center;
text-shadow: 0.1rem 0.1rem 0.7rem rgba(0, 0, 0, 0.79);
box-shadow: 0.07rem 0.07rem 0.5rem rgba(0, 0, 0, 0.28);
}
.skipiconFocusP {
display: block;
position: absolute;
transform: scale(110%, 110%);
top: 18px;
right: 10px;
width: 40px;
height: 60px;
background-image: url("../../../assets/images/player/icon/icon_player_right.png");
}

View File

@@ -0,0 +1,92 @@
import kind from '@enact/core/kind';
import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';
import PropTypes from 'prop-types';
import React from 'react';
import Feedback from './Feedback';
import states from './FeedbackIcons.js';
/**
* FeedbackContent {@link sandstone/VideoPlayer}. This displays the media's playback rate and other
* information.
*
* @class FeedbackContent
* @memberof sandstone/VideoPlayer
* @ui
* @private
*/
const FeedbackContentBase = kind({
name: 'FeedbackContent',
propTypes: /** @lends sandstone/VideoPlayer.Feedback.prototype */ {
/**
* If the current `playbackState` allows the feedback component's visibility to be changed,
* the feedback component will be hidden. If not, setting this property will have no effect.
* All `playbackState`s respond to this property except the following:
* `'rewind'`, `'fastForward'`.
*
* @type {Boolean}
* @default true
* @public
*/
feedbackVisible: PropTypes.bool,
/**
* Value of the feedback playback rate
*
* @type {String|Number}
* @public
*/
playbackRate: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/**
* Refers to one of the following possible media playback states.
* `'play'`, `'pause'`, `'rewind'`, `'fastForward'` ,
* `'jumpBackward'`, `'jumpForward'`, `'jumpToStart'`, `'jumpToEnd'`, `'stop'`.
*
* Each state understands where its related icon should be positioned, and whether it should
* respond to changes to the `visible` property.
*
* This string feeds directly into {@link sandstone/FeedbackIcon.FeedbackIcon}.
*
* @type {('play'|'pause'|'rewind'|'fastForward'|'jumpBackward'|'jumpForward'|'jumpToStart'|'jumpToEnd'|'stop')}
* @public
*/
playbackState: PropTypes.oneOf(Object.keys(states)),
/**
* The visibility of the component. When `false`, the component will be hidden.
*
* @type {Boolean}
* @default true
* @public
*/
visible: PropTypes.bool
},
defaultProps: {
feedbackVisible: true,
visible: true
},
render: ({children, playbackRate, playbackState, feedbackVisible, visible, ...rest}) => {
return (
<div {...rest} style={!visible ? {display: 'none'} : null}>
<Feedback
playbackState={playbackState}
visible={feedbackVisible}
>
{playbackRate}
</Feedback>
{children}
</div>
);
}
});
const FeedbackContent = onlyUpdateForKeys(['children', 'feedbackVisible', 'playbackRate', 'playbackState', 'visible'])(FeedbackContentBase);
export default FeedbackContent;
export {
FeedbackContent,
FeedbackContentBase
};

View File

@@ -0,0 +1,61 @@
import kind from '@enact/core/kind';
import PropTypes from 'prop-types';
import Skinnable from '@enact/sandstone/Skinnable';
import Icon from '@enact/sandstone/Icon';
import iconMap from './FeedbackIcons.js';
import css from './Feedback.module.less';
/**
* Feedback Icon for {@link sandstone/VideoPlayer.Feedback}.
*
* @class FeedbackIcon
* @memberof sandstone/VideoPlayer
* @ui
* @private
*/
const FeedbackIconBase = kind({
name: 'FeedbackIcon',
propTypes: /** @lends sandstone/VideoPlayer.FeedbackIcon.prototype */ {
/**
* Refers to one of the following possible media playback states.
* `'play'`, `'pause'`, `'rewind'`, `'fastForward'` ,
* `'jumpBackward'`, `'jumpForward'`, `'jumpToStart'`, `'jumpToEnd'`, `'stop'`.
*
* @type {('play'|'pause'|'rewind'|'fastForward'|'jumpBackward'|'jumpForward'|'jumpToStart'|'jumpToEnd'|'stop')}
* @public
*/
children: PropTypes.oneOf(Object.keys(iconMap))
},
styles: {
css,
className: 'icon'
},
computed: {
children: ({children}) => children && iconMap[children] && iconMap[children].icon
},
render: ({children, ...rest}) => {
if (children) {
return (
<Icon {...rest}>{children}</Icon>
);
}
return null;
}
});
const FeedbackIcon = Skinnable(FeedbackIconBase);
export default FeedbackIcon;
export {
FeedbackIcon,
FeedbackIconBase
};

View File

@@ -0,0 +1,16 @@
// Full List (Hash) of Feedback states and their icons with metadata
//
export default {
play : {icon: 'play', position: 'after', allowHide: true, message: null},
pause : {icon: 'pause', position: 'after', allowHide: false, message: null},
rewind : {icon: 'backward', position: 'before', allowHide: false, message: 'x'},
slowRewind : {icon: 'pausebackward', position: 'before', allowHide: false, message: 'x'},
fastForward : {icon: 'forward', position: 'after', allowHide: false, message: 'x'},
slowForward : {icon: 'pauseforward', position: 'after', allowHide: false, message: 'x'},
jumpBackward : {icon: 'pausejumpbackward', position: 'before', allowHide: false, message: ' '},
jumpForward : {icon: 'pausejumpforward', position: 'after', allowHide: false, message: ' '},
jumpToStart : {icon: 'jumpbackward', position: 'before', allowHide: true, message: null},
jumpToEnd : {icon: 'jumpforward', position: 'after', allowHide: true, message: null},
stop : {icon: 'stop', position: null, allowHide: true, message: null}
};

View File

@@ -0,0 +1,237 @@
import kind from '@enact/core/kind';
import ComponentOverride from '@enact/ui/ComponentOverride';
import EnactPropTypes from '@enact/core/internal/prop-types';
import PropTypes from 'prop-types';
import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';
import React from 'react';
import Image from '@enact/sandstone/Image';
import Skinnable from '@enact/sandstone/Skinnable';
import states from './FeedbackIcons.js';
import {secondsToTime} from '../MediaPlayer/';
import css from './FeedbackTooltip.module.less';
/**
* FeedbackTooltip {@link sandstone/VideoPlayer}. This displays the media's playback rate and
* time information.
*
* @class FeedbackTooltip
* @memberof sandstone/VideoPlayer
* @ui
* @private
*/
const FeedbackTooltipBase = kind({
name: 'FeedbackTooltip',
propTypes: /** @lends sandstone/VideoPlayer.FeedbackTooltip.prototype */ {
/**
* Invoke action to display or hide tooltip.
*
* @type {('focus'|'blur'|'idle')}
* @default 'idle'
*/
action: PropTypes.oneOf(['focus', 'blur', 'idle']),
/**
* Duration of the current media in seconds
*
* @type {Number}
* @default 0
* @public
*/
duration: PropTypes.number,
/**
* Instance of `NumFmt` to format the time
*
* @type {Object}
* @public
*/
formatter: PropTypes.object,
/**
* If the current `playbackState` allows this component's visibility to be changed,
* this component will be hidden. If not, setting this property will have no effect.
* All `playbackState`s respond to this property except the following:
* `'rewind'`, `'fastForward'`.
*
* @type {Boolean}
* @default false
* @public
*/
hidden: PropTypes.bool,
/**
* Part of the API required by `ui/Slider` but not used by FeedbackTooltip which only
* supports horizontal orientation
*
* @type {String}
* @private
*/
orientation: PropTypes.string,
/**
* Value of the feedback playback rate
*
* @type {String|Number}
* @public
*/
playbackRate: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/**
* Refers to one of the following possible media playback states.
* `'play'`, `'pause'`, `'rewind'`, `'fastForward'` ,
* `'jumpBackward'`, `'jumpForward'`, `'jumpToStart'`, `'jumpToEnd'`, `'stop'`.
*
* Each state understands where its related icon should be positioned, and whether it should
* respond to changes to the `visible` property.
*
* This string feeds directly into {@link sandstone/FeedbackIcon.FeedbackIcon}.
*
* @type {('play'|'pause'|'rewind'|'fastForward'|'jumpBackward'|'jumpForward'|'jumpToStart'|'jumpToEnd'|'stop')}
* @public
*/
playbackState: PropTypes.oneOf(Object.keys(states)),
/**
* This component will be used instead of the built-in version. The internal thumbnail style
* will be applied to this component. This component follows the same rules as the built-in
* version; hiding and showing according to the state of `action`.
*
* This can be a tag name as a string, a rendered DOM node, a component, or a component
* instance.
*
* @type {String|Component|Element}
* @public
*/
thumbnailComponent: EnactPropTypes.renderableOverride,
/**
* `true` if Slider knob is scrubbing.
*
* @type {Boolean}
* @public
*/
thumbnailDeactivated: PropTypes.bool,
/**
* Set a thumbnail image source to show on VideoPlayer's Slider knob. This is a standard
* {@link sandstone/Image} component so it supports all of the same options for the `src`
* property. If no `thumbnailSrc` is set, no tooltip will display.
*
* @type {String|Object}
* @public
*/
thumbnailSrc: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
/**
* Required by the interface for sandstone/Slider.tooltip but not used here
*
* @type {Boolean}
* @default true
* @public
*/
visible: PropTypes.bool
},
defaultProps: {
action: 'idle',
thumbnailDeactivated: false,
hidden: false
},
styles: {
css,
className: 'feedbackTooltip'
},
computed: {
arrowContainerClassName: ({action, styler, thumbnailComponent, thumbnailSrc}) => {
return styler.join(
'arrowContainer',
{hidden: action !== 'focus' || (!thumbnailComponent && !thumbnailSrc)}
);
},
children: ({children, duration, formatter}) => {
return secondsToTime(children * duration, formatter);
},
className: ({hidden, playbackState: s, thumbnailDeactivated, styler, action, thumbnailComponent, thumbnailSrc}) => {
return styler.append({
hidden: hidden && states[s] && states[s].allowHide,
thumbnailDeactivated,
shift: action === 'focus' && (thumbnailComponent || thumbnailSrc)
});
},
feedbackVisible: ({action, playbackState}) => {
return (action !== 'focus' || action === 'idle') && !(action === 'blur' && playbackState === 'play');
},
thumbnailComponent: ({action, thumbnailComponent, thumbnailSrc}) => {
if (action === 'focus') {
if (thumbnailComponent) {
return <ComponentOverride
component={thumbnailComponent}
className={css.thumbnail}
key="thumbnailComponent"
/>;
} else if (thumbnailSrc) {
return (
<div className={css.thumbnail} key="thumbnailComponent">
<Image src={thumbnailSrc} className={css.image} />
</div>
);
}
}
}
},
render: ({arrowContainerClassName, children, thumbnailComponent, ...rest}) => {
delete rest.action;
delete rest.duration;
delete rest.formatter;
delete rest.hidden;
delete rest.orientation;
delete rest.thumbnailDeactivated;
delete rest.thumbnailSrc;
delete rest.visible;
delete rest.playbackRate;
delete rest.playbackState;
delete rest.feedbackVisible;
delete rest.children;
return (
<div {...rest}>
<div className={css.alignmentContainer}>
{thumbnailComponent}
{/* <FeedbackContent
className={css.content}
feedbackVisible={feedbackVisible}
key="feedbackContent"
playbackRate={playbackRate}
playbackState={playbackState}
>
{children}
</FeedbackContent> */}
<div className={arrowContainerClassName}>
<div className={css.arrow} />
</div>
</div>
</div>
);
}
});
const FeedbackTooltip = onlyUpdateForKeys(
['action', 'children', 'hidden', 'playbackState', 'playbackRate', 'thumbnailComponent', 'thumbnailDeactivated', 'thumbnailSrc', 'visible']
)(
Skinnable(
FeedbackTooltipBase
)
);
FeedbackTooltip.defaultSlot = 'tooltip';
export default FeedbackTooltip;
export {
FeedbackTooltip,
FeedbackTooltipBase
};

View File

@@ -0,0 +1,116 @@
// FeedbackTooltip.module.less
//
@import "~@enact/sandstone/styles/mixins.less";
@import "~@enact/sandstone/styles/variables.less";
@import "~@enact/sandstone/styles/skin.less";
@sand-video-slider-tooltip-arrow-border-top: 12px;
@sand-video-slider-tooltip-position-bottom: 21px;
@sand-video-slider-tooltip-shift-position-bottom: @sand-video-slider-tooltip-position-bottom + @sand-video-slider-tooltip-arrow-border-top;
@sand-video-slider-tooltip-thumbnail-border-width: 3px;
@sand-video-slider-tooltip-thumbnail-width: 213px;
@sand-video-slider-tooltip-thumbnail-height: 120px;
@sand-video-slider-tooltip-font-weight: normal;
@sand-video-slider-tooltip-line-height: 36px;
@sand-video-slider-tooltip-margin-bottom: 3px;
@sand-video-slider-tooltip-arrow-border-top: 12px;
@sand-video-slider-tooltip-arrow-border-left-right: 9px;
.feedbackTooltip {
position: absolute;
bottom: @sand-video-slider-tooltip-position-bottom;
&.shift {
bottom: @sand-video-slider-tooltip-shift-position-bottom;
.content {
position: absolute;
.position(auto, 0, 0, 0);
}
}
&.hidden {
display: none;
}
.thumbnail,
.content {
pointer-events: none;
will-change: transform;
}
.thumbnail {
border-width: @sand-video-slider-tooltip-thumbnail-border-width;
border-style: solid;
border-radius: 0;
overflow: hidden;
transition: opacity 250ms;
.image {
margin: 0;
width: @sand-video-slider-tooltip-thumbnail-width;
height: @sand-video-slider-tooltip-thumbnail-height;
}
}
&.thumbnailDeactivated {
.thumbnail {
opacity: @sand-video-slider-tooltip-deactivated-thumbnail-opacity;
}
}
.content {
.sand-font({
font-size: 24px;
font-weight: @sand-video-slider-tooltip-font-weight;
});
.sand-font-number();
font-size: 24px;
font-weight: @sand-video-slider-tooltip-font-weight;
line-height: @sand-video-slider-tooltip-line-height;
white-space: nowrap;
text-align: center;
margin-bottom: @sand-video-slider-tooltip-margin-bottom;
}
.alignmentContainer {
position: relative;
right: 50%;
}
.arrowContainer {
position: absolute;
left: 0;
right: 0;
&.hidden {
display: none;
}
.arrow {
margin: auto;
width: 0;
height: 0;
border-top: @sand-video-slider-tooltip-arrow-border-top solid;
border-right: @sand-video-slider-tooltip-arrow-border-left-right solid;
border-left: @sand-video-slider-tooltip-arrow-border-left-right solid;
}
}
.applySkins({
.thumbnail {
border-color: @sand-video-slider-tooltip-thumbnail-border-color;
}
.arrow {
border-top-color: @sand-video-slider-tooltip-thumbnail-border-color;
border-right-color: transparent;
border-left-color: transparent;
}
.content {
color: @sand-light-gray;
text-shadow: @sand-mediaplayer-slider-tooltip-text-shadow;
}
});
}

View File

@@ -0,0 +1,193 @@
import React from "react";
import PropTypes from "prop-types";
import onlyUpdateForKeys from "recompose/onlyUpdateForKeys";
import EnactPropTypes from "@enact/core/internal/prop-types";
import kind from "@enact/core/kind";
import Marquee from "@enact/sandstone/Marquee";
import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator";
import ForwardRef from "@enact/ui/ForwardRef";
import { $L } from "../../utils/helperMethods";
import SpotlightIds from "../../utils/SpotlightIds";
import TButton from "../TButton/TButton";
import css from "./MediaTitle.module.less";
const Container = SpotlightContainerDecorator(
{ enterTo: null, preserveId: true },
"div"
);
/**
* MediaTitle {@link sandstone/VideoPlayer}.
*
* @class MediaTitle
* @memberof sandstone/VideoPlayer
* @ui
* @private
*/
const MediaTitleBase = kind({
name: "MediaTitle",
propTypes: /** @lends sandstone/VideoPlayer.MediaTitle.prototype */ {
/**
* DOM id for the component. Also define ids for the title and node wrapping the `children`
* in the forms `${id}_title` and `${id}_info`, respectively.
*
* @type {String}
* @required
* @public
*/
id: PropTypes.string.isRequired,
/**
* Anything supplied to `children` will be rendered. Typically this will be informational
* badges indicating aspect ratio, audio channels, etc., but it could also be a description.
*
* @type {Node}
* @public
*/
children: PropTypes.node,
backButton: PropTypes.node,
promotionTitle: PropTypes.string,
introPer: PropTypes.number,
onClickSkipIntro: PropTypes.func,
onIntroDisabled: PropTypes.func,
/**
* Forwards a reference to the MediaTitle component.
*
* @type {Object|Function}
* @private
*/
forwardRef: EnactPropTypes.ref,
/**
* Control whether the children (infoComponents) are displayed.
*
* @type {Boolean}
* @default false
* @public
*/
infoVisible: PropTypes.bool,
/**
* A title string to identify the media's title.
*
* @type {String|Node}
* @public
*/
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
/**
* Setting this to false effectively hides the entire component. Setting it to `false` after
* the control has rendered causes a fade-out transition. Setting to `true` after or during
* the transition makes the component immediately visible again, without delay or transition.
*
* @type {Boolean}
* @default true
* @public
*/
// This property uniquely defaults to true, because it doesn't make sense to have it false
// and have the control be initially invisible, and is named "visible" to match the other
// props (current and possible future). Having an `infoVisible` and a `hidden` prop seems weird.
visible: PropTypes.bool,
},
defaultProps: {
infoVisible: false,
visible: true,
},
styles: {
css,
className: "titleFrame",
},
computed: {
childrenClassName: ({ infoVisible, styler }) =>
styler.join("infoComponents", infoVisible ? "visible" : "hidden"),
className: ({ visible, styler }) =>
styler.append(visible ? "visible" : "hidden"),
titleClassName: ({ infoVisible, styler }) =>
styler.join({
title: true,
infoVisible,
}),
},
render: ({
children,
backButton,
childrenClassName,
id,
forwardRef,
title,
titleClassName,
introPer,
onClickSkipIntro,
onIntroDisabled,
promotionTitle,
...rest
}) => {
delete rest.infoVisible;
delete rest.visible;
delete rest.spotlightDown;
return (
<Container {...rest} id={id} ref={forwardRef}>
{backButton}
<Marquee
id={`${id}_title`}
className={titleClassName}
marqueeOn="render"
>
{title}
</Marquee>
{introPer >= 0 && introPer < 100 ? (
<div id={`${id}_skipIntro`} className={css.skipIntro}>
<TButton
allowClickOnPreview
spotlightId={SpotlightIds.PLAYER_SKIPINTRO}
type={"skipIntro"}
fillPer={introPer}
onClick={onClickSkipIntro}
onDisabled={onIntroDisabled}
>
{$L("SKIP INTRO")}
</TButton>
</div>
) : (
promotionTitle && (
<div id={`${id}_skipIntro`} className={css.skipIntro}>
<TButton
withMarquee
spotlightId={SpotlightIds.PLAYER_SKIPINTRO}
onClick={onClickSkipIntro}
>
{promotionTitle}
</TButton>
</div>
)
)}
<div id={`${id}_info`} className={childrenClassName}>
{" "}
{/* tabIndex={-1} */}
{children}
</div>
</Container>
);
},
});
const MediaTitle = ForwardRef(
onlyUpdateForKeys([
"children",
"title",
"infoVisible",
"visible",
"introPer",
])(MediaTitleBase)
);
export default MediaTitle;
export { MediaTitle, MediaTitleBase };

View File

@@ -0,0 +1,94 @@
// MediaTitle.module.less
//
@import "~@enact/sandstone/styles/variables.less";
@import "~@enact/sandstone/styles/mixins.less";
@import "~@enact/sandstone/styles/skin.less";
.titleFrame {
--infoComponentsOffset: 0;
@badges-present-transition: transform 500ms ease-in-out;
position: relative;
flex-grow: 1;
opacity: 1;
flex-direction: row;
display: flex;
&.hidden {
transition: opacity 1000ms ease-in-out;
opacity: 0;
}
// Title
.title {
width: auto;
transition: @badges-present-transition;
.font-kerning;
flex-grow: 1;
height: 60px;
padding: 0;
text-shadow: 0px 5px 10px rgba(0, 0, 0, 0.2);
font-size: 48px;
font-weight: bold;
line-height: 60px;
letter-spacing: -2.4px;
text-align: left;
color: rgb(255, 255, 255);
&.infoVisible {
transform: translateY(~"calc(var(--infoComponentsOffset) * -1)") translateZ(0);
}
.enact-locale-tallglyph({
// The font-size .times is the same as .title in tallglyph locales so no
// vertical adjustment is required to align their baselines
bottom: 0;
});
}
.skipIntro{
margin-right: 65px;
}
// Badges and title components
.infoComponents {
vertical-align: super;
font-size: 54px;
&.hidden {
opacity: 0;
}
&.visible {
transition: opacity 500ms ease-in-out;
}
> * {
display: inline-block;
margin: 0 12px;
}
.badgeTextIcon {
font-family: @sand-font-family-bold;
font-size: @sand-video-player-badge-text-size;
text-align: center;
white-space: nowrap;
display: inline-block;
}
.fontLgIcons {
font-family: "LG Icons";
}
}
.applySkins({
.titleFrame {
color: @sand-video-player-title-color;
.redIcon {
background-color: @sand-video-player-redicon-bg-color;
color: @sand-video-player-redicon-text-color;
}
}
})
}

View File

@@ -0,0 +1,52 @@
import React from 'react';
import kind from '@enact/core/kind';
import Touchable from '@enact/ui/Touchable';
import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';
import PropTypes from 'prop-types';
import css from './VideoPlayer.module.less';
/**
* Overlay {@link sandstone/VideoPlayer}. This covers the Video piece of the
* {@link sandstone/VideoPlayer} to prevent unnecessary VideoPlayer repaints due to mouse-moves.
* It also acts as a container for overlaid elements, like the {@link sandstone/Spinner}.
*
* @class Overlay
* @memberof sandstone/VideoPlayer
* @ui
* @private
*/
const OverlayBase = kind({
name: 'Overlay',
propTypes: /** @lends sandstone/VideoPlayer.Overlay.prototype */ {
bottomControlsVisible: PropTypes.bool,
children: PropTypes.node
},
styles: {
css,
className: 'overlay'
},
computed: {
className: ({bottomControlsVisible, styler}) => styler.append({['scrim']: bottomControlsVisible})
},
render: (props) => {
delete props.bottomControlsVisible;
return <div {...props} />;
}
});
const Overlay = onlyUpdateForKeys(['bottomControlsVisible', 'children'])(
Touchable(
OverlayBase
)
);
export default Overlay;
export {
Overlay,
OverlayBase
};

View File

@@ -0,0 +1,324 @@
import {forward} from '@enact/core/handle';
import ForwardRef from '@enact/ui/ForwardRef';
import {Media, getKeyFromSource} from '@enact/ui/Media';
import EnactPropTypes from '@enact/core/internal/prop-types';
import Slottable from '@enact/ui/Slottable';
import compose from 'ramda/src/compose';
import React from 'react';
import css from './VideoPlayer.module.less';
import PropTypes from 'prop-types';
/**
* Adds support for preloading a video source for `VideoPlayer`.
*
* @class VideoBase
* @memberof sandstone/VideoPlayer
* @ui
* @private
*/
const VideoBase = class extends React.Component {
static displayName = 'Video';
static propTypes = /** @lends sandstone/VideoPlayer.Video.prototype */ {
/**
* Video plays automatically.
*
* @type {Boolean}
* @default false
* @public
*/
autoPlay: PropTypes.bool,
/**
* Video component to use.
*
* The default (`'video'`) renders an `HTMLVideoElement`. Custom video components must have
* a similar API structure, exposing the following APIs:
*
* Properties:
* * `currentTime` {Number} - Playback index of the media in seconds
* * `duration` {Number} - Media's entire duration in seconds
* * `error` {Boolean} - `true` if video playback has errored.
* * `loading` {Boolean} - `true` if video playback is loading.
* * `paused` {Boolean} - Playing vs paused state. `true` means the media is paused
* * `playbackRate` {Number} - Current playback rate, as a number
* * `proportionLoaded` {Number} - A value between `0` and `1`
* representing the proportion of the media that has loaded
* * `proportionPlayed` {Number} - A value between `0` and `1` representing the
* proportion of the media that has already been shown
*
* Events:
* * `onLoadStart` - Called when the video starts to load
* * `onPlay` - Sent when playback of the media starts after having been paused
* * `onUpdate` - Sent when any of the properties were updated
*
* Methods:
* * `play()` - play video
* * `pause()` - pause video
* * `load()` - load video
*
* The [`source`]{@link sandstone/VideoPlayer.Video.source} property is passed to
* the video component as a child node.
*
* @type {String|Component|Element}
* @default 'video'
* @public
*/
mediaComponent: EnactPropTypes.renderableOverride,
/**
* The video source to be preloaded. Expects a `<source>` node.
*
* @type {Node}
* @public
*/
preloadSource: PropTypes.node,
/**
* Called with a reference to the active [Media]{@link ui/Media.Media} component.
*
* @type {Function}
* @private
*/
setMedia: PropTypes.func,
/**
* The video source to be played.
*
* Any children `<source>` elements will be sent directly to the `mediaComponent` as video
* sources.
*
* See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source
*
* @type {String|Node}
* @public
*/
source: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
track: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
};
static defaultProps = {
mediaComponent: 'video'
};
componentDidUpdate (prevProps) {
const {source, preloadSource} = this.props;
const {source: prevSource, preloadSource: prevPreloadSource} = prevProps;
const key = getKeyFromSource(source);
const prevKey = getKeyFromSource(prevSource);
const preloadKey = getKeyFromSource(preloadSource);
const prevPreloadKey = getKeyFromSource(prevPreloadSource);
if (this.props.setMedia !== prevProps.setMedia) {
this.clearMedia(prevProps);
this.setMedia();
}
if (source) {
if (key === prevPreloadKey && preloadKey !== prevPreloadKey) {
// if there's source and it was the preload source
// if the preloaded video didn't error, notify VideoPlayer it is ready to reset
if (this.preloadLoadStart) {
forward('onLoadStart', this.preloadLoadStart, this.props);
}
// emit onUpdate to give VideoPlayer an opportunity to updates its internal state
// since it won't receive the onLoadStart or onError event
forward('onUpdate', {type: 'onUpdate'}, this.props);
this.autoPlay();
} else if (key !== prevKey) {
// if there's source and it has changed.
this.autoPlay();
}
}
if (preloadSource && preloadKey !== prevPreloadKey) {
this.preloadLoadStart = null;
// In the case that the previous source equalled the previous preload (causing the
// preload video node to not be created) and then the preload source was changed, we
// need to guard against accessing the preloadVideo node.
if (this.preloadVideo) {
this.preloadVideo.load();
}
}
}
componentWillUnmount () {
this.clearMedia();
}
keys = ['media-1', 'media-2'];
prevSourceKey = null;
prevPreloadKey = null;
handlePreloadLoadStart = (ev) => {
// persist the event so we can cache it to re-emit when the preload becomes active
ev.persist();
this.preloadLoadStart = ev;
// prevent the from bubbling to upstream handlers
ev.stopPropagation();
};
clearMedia ({setMedia} = this.props) {
if (setMedia) {
setMedia(null);
}
}
setMedia ({setMedia} = this.props) {
if (setMedia) {
setMedia(this.video);
}
}
autoPlay () {
if (!this.props.autoPlay) return;
this.video.play();
}
setVideoRef = (node) => {
this.video = node;
this.setMedia();
};
setPreloadRef = (node) => {
if (node) {
node.load();
}
this.preloadVideo = node;
};
getKeys () {
const {source, preloadSource} = this.props;
const sourceKey = source && getKeyFromSource(source);
let preloadKey = preloadSource && getKeyFromSource(preloadSource);
// If the same source is used for both, clear the preload key to avoid rendering duplicate
// video elements.
if (sourceKey === preloadKey) {
preloadKey = null;
}
// if either the source or preload existed previously in the other "slot", swap the keys so
// the preload video becomes the active video and vice versa
if (
(sourceKey === this.prevPreloadKey && this.prevPreloadKey) ||
(preloadKey === this.prevSourceKey && this.prevSourceKey)
) {
this.keys.reverse();
}
// cache the previous keys so we know if the sources change the next time
this.prevSourceKey = sourceKey;
this.prevPreloadKey = preloadKey;
// if preload is unset, clear the key so we don't render that media node at all
return preloadKey ? this.keys : this.keys.slice(0, 1);
}
render () {
const {
preloadSource,
source,
track,
mediaComponent,
...rest
} = this.props;
delete rest.setMedia;
const [sourceKey, preloadKey] = this.getKeys();
return (
<React.Fragment>
{sourceKey ? (
<Media
{...rest}
className={css.video}
controls={false}
key={sourceKey}
mediaComponent={mediaComponent}
preload="none"
ref={this.setVideoRef}
track={React.isValidElement(track) ? track : (
<track src={track} />
)}
source={React.isValidElement(source) ? source : (
<source src={source} />
)}
/>
) : null}
{preloadKey ? (
<Media
autoPlay={false}
className={css.preloadVideo}
controls={false}
key={preloadKey}
mediaComponent={mediaComponent}
onLoadStart={this.handlePreloadLoadStart}
preload="none"
ref={this.setPreloadRef}
track={React.isValidElement(track) ? track : (
<track src={track} />
)}
source={React.isValidElement(preloadSource) ? preloadSource : (
<source src={preloadSource} />
)}
/>
) : null}
</React.Fragment>
);
}
};
const VideoDecorator = compose(
ForwardRef({prop: 'setMedia'}),
Slottable({slots: ['source', 'track','preloadSource']})
);
/**
* Provides support for more advanced video configurations for `VideoPlayer`.
*
* Custom Video Tag
*
* ```
* <VideoPlayer>
* <Video mediaComponent="custom-video-element">
* <source src="path/to/source.mp4" />
* </Video>
* </VideoPlayer>
* ```
*
* Preload Video Source
*
* ```
* <VideoPlayer>
* <Video>
* <source src="path/to/source.mp4" />
* <source src="path/to/preload-source.mp4" slot="preloadSource" />
* </Video>
* </VideoPlayer>
* ```
*
* @class Video
* @mixes ui/Slottable.Slottable
* @memberof sandstone/VideoPlayer
* @ui
* @public
*/
const Video = VideoDecorator(VideoBase);
Video.defaultSlot = 'videoComponent';
export default Video;
export {
Video
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,167 @@
// VideoPlayer.module.less
//
@import "~@enact/sandstone/styles/variables.less";
@import "~@enact/sandstone/styles/mixins.less";
@import "~@enact/sandstone/styles/skin.less";
.videoPlayer {
// Set by counting the IconButtons inside the side components.
--liftDistance: 0px;
overflow: hidden;
.video {
height: 100%;
width: 100%;
}
.preloadVideo {
display: none;
}
.overlay {
position: absolute;
.position(0);
&::after {
content: "";
position: absolute;
.position(auto, 0, 0, 0);
height: 80%;
transform-origin: bottom;
// Fancier gradient for future reference. Keeping linear-gradient as specified from Enyo.
// background-image: radial-gradient(rgba(0, 0, 0, 0) 50%, rgb(0, 0, 0) 100%);
// background-size: 170% 200%;
// background-position: bottom center;
}
}
@keyframes spin {
0% {
transform: rotate(0.25turn);
}
33% {
transform: rotate(0.5turn);
}
80% {
transform: rotate(0.95turn);
}
85% {
transform: rotate(1turn);
}
100% {
transform: rotate(1.25turn);
}
}
.spinner {
background-image: url("../../../assets/images/player/icon_loading.png");
width: 100px;
height: 100px;
position: relative;
background-size: cover;
border-radius: 20.8125rem;
overflow: hidden;
margin: 490px auto;
animation: none 1.25s linear infinite;
animation-name: spin;
// animation-play-state: paused;
}
.controlFeedbackBtnLayer {
position: absolute;
z-index: 50;
top: 506px;
left: 0;
padding: 0;
width: 100%;
height: 94px;
display: flex;
justify-content: space-between;
&.lift {
transform: translateY(~"calc(var(--liftDistance) * -1)");
transition: transform 0.3s linear;
}
}
.fullscreen {
.miniFeedback {
position: absolute;
z-index: 50;
top: 506px;
left: 0;
padding: 0;
width: 100%;
height: 94px;
-webkit-margin-end: 0px;
pointer-events: none;
}
.bottom {
position: absolute;
z-index: 100; // Value assigned as part of the VideoPlayer API so layers may be inserted in-between
bottom: 10px;
left: 0;
right: 0;
// left: @sand-video-player-padding-side;
// right: @sand-video-player-padding-side;
&.lift {
transform: translateY(~"calc(var(--liftDistance) * -1)");
transition: transform 0.3s linear;
}
&.hidden {
pointer-events: none;
.sliderContainer {
position: absolute;
left: 0;
right: 0;
}
}
.infoFrame {
display: flex;
margin-left: 64px;
}
.sliderContainer {
// display: flex;
position: relative;
align-items: center;
margin-left: 90px;
margin-right: 90px;
height: 70px;
> *:first-child {
text-align: right;
}
.enact-locale-rtl({
direction: ltr;
});
}
}
}
.controlsHandleAbove {
pointer-events: none;
position: absolute;
.position(0, 0, auto, 0);
}
// Skin colors
.applySkins({
.fullscreen {
.bottom {
background-color: @sand-video-player-bottom-bg-color;
.infoFrame {
text-shadow: @sand-video-player-title-text-shadow;
}
}
}
.overlay {
z-index: 2;
&.scrim::after {
background: @sand-video-player-scrim-gradient-color
}
}
});
}

View File

@@ -0,0 +1,3 @@
{
"main": "VideoPlayer.js"
}

View File

@@ -0,0 +1,264 @@
/* global HTMLMediaElement */
import React from 'react';
import {mount} from 'enzyme';
import Video from '../Video';
describe('VideoPlayer.Video', () => {
function getSourceNode (wrapper) {
return wrapper.find('Video').instance().video;
}
function getPreloadNode (wrapper) {
return wrapper.find('Video').instance().preloadVideo;
}
beforeEach(() => {
jest.spyOn(HTMLMediaElement.prototype, 'load').mockImplementation(() => true);
});
afterEach(() => {
HTMLMediaElement.prototype.load.mockRestore();
});
describe('changing sources', () => {
// Failures in these tests will often result in the following error. The error is misleading
// but indicates the nodes aren't reused as expected.
// TypeError: Cannot assign to read only property 'Symbol(impl)' of object '[object DOMImplementation]'
test('should use the same node when changing the `source`', () => {
const subject = mount(
<Video source="abc.mp4" />
);
const expected = getSourceNode(subject);
subject.setProps({
source: 'def.mp4'
});
const actual = getSourceNode(subject);
expect(actual).toBe(expected);
});
test('should not render `preloadSource` when not present', () => {
const subject = mount(
<Video source="abc.mp4" />
);
const expected = 1;
const actual = subject.find('Media').length;
expect(actual).toBe(expected);
});
test('should use same `source` when removing `source` and no `preload`', () => {
const subject = mount(
<Video source="abc.mp4" />
);
const expected = getSourceNode(subject);
subject.setProps({
source: undefined // eslint-disable-line no-undefined
});
const actual = getSourceNode(subject);
expect(actual).toBe(expected);
});
test('should use same `source` when adding `source` and no `preload`', () => {
const subject = mount(
<Video />
);
const expected = getSourceNode(subject);
subject.setProps({
source: 'abc.mp4'
});
const actual = getSourceNode(subject);
expect(actual).toBe(expected);
});
test('should use the same node when adding `preloadSource`', () => {
const subject = mount(
<Video source="abc.mp4" />
);
const expected = getSourceNode(subject);
subject.setProps({
preloadSource: 'def.mp4'
});
const actual = getSourceNode(subject);
expect(actual).toBe(expected);
});
test('should render `preloadSource` when added', () => {
const subject = mount(
<Video source="abc.mp4" />
);
subject.setProps({
preloadSource: 'def.mp4'
});
const expected = 2;
const actual = subject.find('Media').length;
expect(actual).toBe(expected);
});
test('should use the same node when adding `preloadSource` the same as source', () => {
const subject = mount(
<Video source="abc.mp4" />
);
const expected = getSourceNode(subject);
subject.setProps({
preloadSource: 'abc.mp4'
});
const actual = getSourceNode(subject);
expect(actual).toBe(expected);
});
test('should use the same node when changing `preloadSource`', () => {
const subject = mount(
<Video source="abc.mp4" preloadSource="def.mp4" />
);
const expected = getPreloadNode(subject);
subject.setProps({
preloadSource: 'ghi.mp4'
});
const actual = getPreloadNode(subject);
expect(actual).toBe(expected);
});
test('should swaps nodes when swapping `source` and `preloadSource`', () => {
const subject = mount(
<Video source="abc.mp4" preloadSource="def.mp4" />
);
const source = getSourceNode(subject);
const preload = getPreloadNode(subject);
subject.setProps({
source: 'def.mp4',
preloadSource: 'abc.mp4'
});
expect(getSourceNode(subject)).toBe(preload);
expect(getPreloadNode(subject)).toBe(source);
});
test('should not swap nodes on re-render after swapping `source` and `preloadSource`', () => {
const subject = mount(
<Video source="abc.mp4" preloadSource="def.mp4" />
);
const source = getSourceNode(subject);
const preload = getPreloadNode(subject);
subject.setProps({
source: 'def.mp4',
preloadSource: 'abc.mp4'
});
subject.setProps({});
expect(getSourceNode(subject)).toBe(preload);
expect(getPreloadNode(subject)).toBe(source);
});
test('should reuse preload node when moving `preloadSource` to `source`', () => {
const subject = mount(
<Video source="abc.mp4" preloadSource="def.mp4" />
);
const preload = getPreloadNode(subject);
subject.setProps({
source: 'def.mp4',
preloadSource: null
});
expect(getSourceNode(subject)).toBe(preload);
expect(getPreloadNode(subject)).toBeFalsy();
});
test('should reuse both nodes when both `preloadSource` and `source` change', () => {
const subject = mount(
<Video source="abc.mp4" preloadSource="def.mp4" />
);
const source = getSourceNode(subject);
const preload = getPreloadNode(subject);
subject.setProps({
source: 'ghi.mp4',
preloadSource: 'jkl.mp4'
});
expect(getSourceNode(subject)).toBe(source);
expect(getPreloadNode(subject)).toBe(preload);
});
test('should reuse source node over two changes', () => {
const subject = mount(
<Video source="abc.mp4" />
);
const source = getSourceNode(subject);
subject.setProps({
source: 'def.mp4'
});
expect(getSourceNode(subject)).toBe(source);
subject.setProps({
source: 'ghi.mp4'
});
expect(getSourceNode(subject)).toBe(source);
});
test('should swap nodes when preload does not exist initially', () => {
const subject = mount(
<Video source="abc.mp4" />
);
const source = getSourceNode(subject);
subject.setProps({
preloadSource: 'def.mp4'
});
const preload = getPreloadNode(subject);
subject.setProps({
source: 'def.mp4',
preloadSource: 'abc.mp4'
});
expect(getSourceNode(subject)).toBe(preload);
expect(getPreloadNode(subject)).toBe(source);
});
});
});

View File

@@ -0,0 +1,17 @@
/**
* VideoPlayerSubtitle
*
* @module VideoPlayerSubtitle
*/
import React from 'react';
import css from './VideoPlayerSubtitle.module.less'
const VideoPlayerSubtitle = ({ subtitle, ...rest }) => {
return (
<div className={css.subtitleWrap}>
<div className={css.subtitle}> {subtitle}</div>
</div>
);
};
export default VideoPlayerSubtitle;

View File

@@ -0,0 +1,16 @@
.subtitleWrap{
position: absolute;
bottom: 50px;
width: 100%;
white-space: pre-wrap;
z-index: 1;
.subtitle {
color: rgb(255, 255, 255);
text-shadow: -2.5px 0px 2px rgb(0, 0, 0), 0px 2.5px 2px rgb(0, 0, 0), 2.5px 0px 2px rgb(0, 0, 0), 0px -2.5px 2px rgb(0, 0, 0);
font-family: "LG SmartFont";
font-size: 58px;
line-height: 1.4;
background-color: transparent;
text-align: center;
}
}

View File

@@ -45,8 +45,16 @@ export const ACTIVE_POPUP = {
couponPopup: "couponPopup", couponPopup: "couponPopup",
favoritePopup: "favoritePopup", favoritePopup: "favoritePopup",
loginPopup: "loginPopup", loginPopup: "loginPopup",
logoutPopup: "logoutPopup",
}; };
export const AUTO_SCROLL_DELAY = 600; export const AUTO_SCROLL_DELAY = 600;
export const AUTO_SCROLL_GAP = 20; export const AUTO_SCROLL_GAP = 20;
export const SINGLE = "SINGLE";
export const SERIES = "SERIES";
export const YOUTUBE_VIDEO = "youtube#video";
export const YOUTUBE_CHANNEL = "youtube#channel";
export const CP_CONTENT = "CP_CONTENT";
export const APP = "APP";
export const PLAN = "PLAN";
export const ADD_PLAN = "ADD_PLAN";
export const MORE = "MORE";

View File

@@ -0,0 +1,43 @@
const KeyCode = {
POWER: 0, // power key
OK: 13, // ok key
VKB_ENTER: 13, // enter key
BACK: 461, // remote back key
LEFT: 37, // left key
UP: 38, // up key
RIGHT: 39, // right key
DOWN: 40, // down key
BACKSPACE: 8, // keyboard backspace key
REW: 412, // rewind key
STOP: 413, // stop key
PLAY: 415, // play key
FF: 417, // fastforward key
INFO: 457, // info key
PAUSE: 19, // pause key
SPACEBAR: 32, // HID keyboard spacebar
ESC: 27, // HID keyboard ESC
PREVSKIP: 424, // prev skip (only japan remote key)
NEXTSKIP: 425, // next skip (only japan remote key)
RED: 403, // RED button.
GREEN: 404, // GREEN button.
YELLOW: 405, // YELLOW button.
BLUE: 406, // BLUE button.
LIST: 1056, // LIST button
EXIT: 1001, // EXIT button
MORE_ACTION: 1062, // More Action Button
CH_UP: 33, // CHANNEL UP button
CH_DOWN: 34, // CHANNEL DOWN button
SUBTITLE: 460,
NUM_0 : 48,
NUM_1 : 49,
NUM_2 : 50,
NUM_3 : 51,
NUM_4 : 52,
NUM_5 : 53,
NUM_6 : 54,
NUM_7 : 55,
NUM_8 : 56,
NUM_9 : 57
};
export default KeyCode;

View File

@@ -5,4 +5,10 @@ export const SpotlightIds = {
// categoryPanel // categoryPanel
CATEGORY_CONTENTS_BOX: "categoryContentsBox", CATEGORY_CONTENTS_BOX: "categoryContentsBox",
SHOW_PRODUCTS_BOX: "showProductsBox", SHOW_PRODUCTS_BOX: "showProductsBox",
// video player
PLAYER_SKIPINTRO: "skipintro",
PLAYER_TITLE_LAYER: "playerTitleLayer",
PLAYER_SLIDER: "playerslider",
LIST_PLAYER: "list_player",
LIST_PLAYER2: "list_player2",
}; };

View File

@@ -1,9 +1,16 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import VideoPlayer from "@enact/sandstone/VideoPlayer";
import Spottable from "@enact/spotlight/Spottable";
import { pushPanel } from "../../../actions/panelActions";
import { panel_names } from "../../../utils/Config";
import PlayerPanel from "../../PlayerPanel/PlayerPanel";
import HomeTodayDeal from "../HomeTodayDeal/HomeTodayDeal"; import HomeTodayDeal from "../HomeTodayDeal/HomeTodayDeal";
import ImageBanner from "./ImageBannerUnit"; import ImageBanner from "./ImageBannerUnit";
import css from "./RollingUnit.module.less";
// import ViedoController from './VideoUnit';
export default function RollingUnit({ bannerData, imageType, spotlightId }) { export default function RollingUnit({ bannerData, imageType, spotlightId }) {
const rollingData = bannerData.bannerDetailInfos; const rollingData = bannerData.bannerDetailInfos;
@@ -14,6 +21,9 @@ export default function RollingUnit({ bannerData, imageType, spotlightId }) {
const [lastIndex, setLastIndex] = useState(rollingDataLength - 1); const [lastIndex, setLastIndex] = useState(rollingDataLength - 1);
const [onFocus, setOnFocus] = useState(false); const [onFocus, setOnFocus] = useState(false);
const SpottableComponent = Spottable("div");
const dispatch = useDispatch();
const getIndex = useCallback( const getIndex = useCallback(
(index) => { (index) => {
setStartIndex(index); setStartIndex(index);
@@ -28,6 +38,15 @@ export default function RollingUnit({ bannerData, imageType, spotlightId }) {
[onFocus] [onFocus]
); );
// 비디오 클릭
const handleClick = useCallback(() => {
dispatch(
pushPanel({
name: panel_names.PlayerPanel,
})
);
}, []);
useEffect(() => { useEffect(() => {
if (rollingDataLength === 1) { if (rollingDataLength === 1) {
return; return;
@@ -80,11 +99,14 @@ export default function RollingUnit({ bannerData, imageType, spotlightId }) {
/> />
) : rollingData[startIndex].shptmBanrTpNm === "LIVE" || ) : rollingData[startIndex].shptmBanrTpNm === "LIVE" ||
rollingData[startIndex].shptmBanrTpNm === "VOD" ? ( rollingData[startIndex].shptmBanrTpNm === "VOD" ? (
<> <VideoPlayer>
<p>비디오 구현 </p> <source
</> src="https://lgtv-vod-p.b-cdn.net/OEMLGTV_SCALPMEDLF1TRTPIHD_115428_800-660-8561_carbon.mp4"
type="video/mp4"
/>
</VideoPlayer>
) : rollingData[startIndex].shptmBanrTpNm === "Today's Deals" ? ( ) : rollingData[startIndex].shptmBanrTpNm === "Today's Deals" ? (
<HomeTodayDeal /* <HomeTodayDeal
imgAlt={rollingData[startIndex].tmnlImgAlt} imgAlt={rollingData[startIndex].tmnlImgAlt}
imageName={rollingData[startIndex].tmnlImgNm} imageName={rollingData[startIndex].tmnlImgNm}
imgPath={rollingData[startIndex].tmnlImgPath} imgPath={rollingData[startIndex].tmnlImgPath}
@@ -100,7 +122,15 @@ export default function RollingUnit({ bannerData, imageType, spotlightId }) {
getIndex={getIndex} getIndex={getIndex}
getFocus={getFocus} getFocus={getFocus}
spotlightId={spotlightId} spotlightId={spotlightId}
/> */
<div className={css.videoMain} onClick={handleClick}>
<VideoPlayer>
<source
src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
type="video/mp4"
/> />
</VideoPlayer>
</div>
) : null} ) : null}
</> </>
); );

View File

@@ -1,54 +1,90 @@
@import "../../../style/CommonStyle.module.less"; @import "../../../style/CommonStyle.module.less";
@import "../../../style/utils.module.less"; @import "../../../style/utils.module.less";
.container { .videoMain {
position: relative; position: absolute;
.leftBtn { width: 100%;
.position(@position: absolute, @top: 408px, @right: auto, @bottom: auto, @left: 18px); height: 100%;
.size(@w: 42px, @h: 42px); left: 0;
background-image: url("../../../../assets/icon/button_icon/btn_prev_thumb_nor.png"); top: 0;
background-size: 42px 42px; right: 0;
background-position: center center; background-color: black;
cursor: pointer; video {
z-index: 1; width: 100%;
&:focus { height: 100%;
background-image: url("../../../../assets/icon/button_icon/btn_prev_thumb_foc.png"); background-color: transparent;
} object-fit: contain;
} position: absolute;
.rightBtn { }
.position(@position: absolute, @top: 408px, @right: 18px, @bottom: auto, @left: auto); }
.size(@w: 42px, @h: 42px); .playLogBg {
background-image: url("../../../../assets/icon/button_icon/btn_next_thumb_nor.png"); width: 100%;
background-size: 42px 42px; height: 100%;
background-position: center center; position: absolute;
cursor: pointer; margin: 0;
z-index: 1; transition: opacity 500ms ease-in-out;
&:focus { &.hide {
background-image: url("../../../../assets/icon/button_icon/btn_next_thumb_foc.png"); opacity: 0;
} }
} }
&.horizontal { .hide {
.leftBtn { visibility: hidden;
.position(@position: absolute, @top: 189px, @right: auto, @bottom: auto, @left: 18px); }
.size(@w: 42px, @h: 42px); .localGuide {
background-image: url("../../../../assets/icon/button_icon/btn_prev_thumb_nor.png"); position: absolute;
background-size: 42px 42px; top: 50px;
background-position: center center; color: red;
z-index: 1; left: 50%;
&:focus { transform: translateX(-50%);
background-image: url("../../../../assets/icon/button_icon/btn_prev_thumb_foc.png"); }
}
} .exitBtnPosition {
.rightBtn { position: fixed;
.position(@position: absolute, @top: 189px, @right: 18px, @bottom: auto, @left:auto); left: 80px;
.size(@w: 42px, @h: 42px); top: 80px;
background-image: url("../../../../assets/icon/button_icon/btn_next_thumb_nor.png"); &.lt {
background-size: 42px 42px; left: initial;
background-position: center center; right: 80px;
z-index: 1; }
&:focus { }
background-image: url("../../../../assets/icon/button_icon/btn_next_thumb_foc.png"); .autoSwitch {
} display: flex;
} align-items: center;
> div:first-child {
font-family: Pretendard;
font-size: 32px;
font-weight: 500;
letter-spacing: -0.96px;
color: rgba(255, 255, 255, 0.7);
}
> div {
margin: 0;
padding: 0;
}
}
.bleNotiPopup {
width: 652px !important;
height: 450px !important;
margin-bottom: 300px !important;
> div > div > div > div > div:nth-child(2) {
padding: 30px 0 50px 0 !important;
}
.title {
font-size: 40px !important;
font-weight: bold;
letter-spacing: -0.8px;
margin-bottom: 30px;
}
.detailLimited {
width: 382px !important;
height: 67px !important;
margin: -20px auto !important;
font-size: 25px !important;
padding-top: 20px !important;
letter-spacing: -0.75px !important;
background-color: white !important;
color: #909090 !important;
line-height: 35px !important;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
.videoMain {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
right: 0;
background-color: black;
video {
width: 100%;
height: 100%;
background-color: transparent;
object-fit: fill;
position: absolute;
}
}
.playLogBg{
width: 100%;
height: 100%;
position: absolute;
margin: 0;
transition: opacity 500ms ease-in-out;
&.hide {
opacity: 0;
}
}
.hide {
visibility: hidden;
}
.localGuide{
position: absolute;
top: 50px;
color: red;
left: 50%;
transform: translateX(-50%);
}
.cameraSettingsButtonContainer {
position: absolute;
top: 150px;
left: 246px;
z-index: 1;
}

View File

@@ -0,0 +1,6 @@
{
"main": "PlayerPanel.js",
"styles": [
"PlayerPanel.module.less"
]
}