[홈패널] 비디오 플레이어 추가
|
After Width: | Height: | Size: 4.9 KiB |
BIN
com.twin.app.shoptime/assets/images/player/focus-frame.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 32 KiB |
BIN
com.twin.app.shoptime/assets/images/player/focus_frame_big.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 104 KiB |
BIN
com.twin.app.shoptime/assets/images/player/frame-big.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 24 KiB |
BIN
com.twin.app.shoptime/assets/images/player/frame.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
com.twin.app.shoptime/assets/images/player/frame_channel.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
com.twin.app.shoptime/assets/images/player/ico-play.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
com.twin.app.shoptime/assets/images/player/ico-playing.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
com.twin.app.shoptime/assets/images/player/ico_favorite.png
Normal file
|
After Width: | Height: | Size: 581 B |
BIN
com.twin.app.shoptime/assets/images/player/ico_favorite_fill.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 613 B |
BIN
com.twin.app.shoptime/assets/images/player/icon/icon_app.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
BIN
com.twin.app.shoptime/assets/images/player/icon/icon_calory.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
com.twin.app.shoptime/assets/images/player/icon/icon_cardio.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
BIN
com.twin.app.shoptime/assets/images/player/icon/icon_channel.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
BIN
com.twin.app.shoptime/assets/images/player/icon/icon_mat.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
com.twin.app.shoptime/assets/images/player/icon/icon_more.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
BIN
com.twin.app.shoptime/assets/images/player/icon/icon_repeat.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
BIN
com.twin.app.shoptime/assets/images/player/icon/icon_youtube.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
BIN
com.twin.app.shoptime/assets/images/player/icon_list.png
Normal file
|
After Width: | Height: | Size: 153 B |
BIN
com.twin.app.shoptime/assets/images/player/icon_loading.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
com.twin.app.shoptime/assets/images/player/new_today_Icon.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
com.twin.app.shoptime/assets/images/player/numbers/image_01.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
com.twin.app.shoptime/assets/images/player/numbers/image_02.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
com.twin.app.shoptime/assets/images/player/numbers/image_03.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
com.twin.app.shoptime/assets/images/player/numbers/image_04.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
com.twin.app.shoptime/assets/images/player/numbers/image_05.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
com.twin.app.shoptime/assets/images/player/numbers/image_06.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
com.twin.app.shoptime/assets/images/player/numbers/image_07.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
com.twin.app.shoptime/assets/images/player/numbers/image_08.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
com.twin.app.shoptime/assets/images/player/numbers/image_09.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
com.twin.app.shoptime/assets/images/player/numbers/image_10.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
com.twin.app.shoptime/assets/images/player/today.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
537
com.twin.app.shoptime/assets/mock/bigbuckbunny.m3u8
Normal 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
|
||||
279
com.twin.app.shoptime/assets/mock/video.vtt
Normal 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 sun’s 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 we’re 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 child’s 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 we’ll 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
|
||||
|
||||
|
||||
453
com.twin.app.shoptime/src/components/MediaItem/MediaItem.js
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"main": "MediaItem.js",
|
||||
"styles": [
|
||||
"MediaItem.module.less"
|
||||
]
|
||||
}
|
||||
1009
com.twin.app.shoptime/src/components/MediaList/MediaList.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"main": "MediaList.js"
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
119
com.twin.app.shoptime/src/components/MediaPlayer/MediaSlider.js
Normal 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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
112
com.twin.app.shoptime/src/components/MediaPlayer/Times.js
Normal 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};
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
21
com.twin.app.shoptime/src/components/MediaPlayer/index.js
Normal 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
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"main": "index.js"
|
||||
}
|
||||
83
com.twin.app.shoptime/src/components/MediaPlayer/util.js
Normal 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
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"main": "TPlayerFeedBackBtn.js"
|
||||
}
|
||||
100
com.twin.app.shoptime/src/components/VideoPlayer/Feedback.js
Normal 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 };
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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}
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
193
com.twin.app.shoptime/src/components/VideoPlayer/MediaTitle.js
Normal 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 };
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
52
com.twin.app.shoptime/src/components/VideoPlayer/Overlay.js
Normal 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
|
||||
};
|
||||
324
com.twin.app.shoptime/src/components/VideoPlayer/Video.js
Normal 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
|
||||
};
|
||||
2428
com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js
Normal 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
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"main": "VideoPlayer.js"
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,16 @@ export const ACTIVE_POPUP = {
|
||||
couponPopup: "couponPopup",
|
||||
favoritePopup: "favoritePopup",
|
||||
loginPopup: "loginPopup",
|
||||
logoutPopup: "logoutPopup",
|
||||
};
|
||||
|
||||
export const AUTO_SCROLL_DELAY = 600;
|
||||
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";
|
||||
|
||||
43
com.twin.app.shoptime/src/utils/KeyCode.js
Normal 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;
|
||||
@@ -5,4 +5,10 @@ export const SpotlightIds = {
|
||||
// categoryPanel
|
||||
CATEGORY_CONTENTS_BOX: "categoryContentsBox",
|
||||
SHOW_PRODUCTS_BOX: "showProductsBox",
|
||||
// video player
|
||||
PLAYER_SKIPINTRO: "skipintro",
|
||||
PLAYER_TITLE_LAYER: "playerTitleLayer",
|
||||
PLAYER_SLIDER: "playerslider",
|
||||
LIST_PLAYER: "list_player",
|
||||
LIST_PLAYER2: "list_player2",
|
||||
};
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
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 ImageBanner from "./ImageBannerUnit";
|
||||
|
||||
// import ViedoController from './VideoUnit';
|
||||
import css from "./RollingUnit.module.less";
|
||||
|
||||
export default function RollingUnit({ bannerData, imageType, spotlightId }) {
|
||||
const rollingData = bannerData.bannerDetailInfos;
|
||||
@@ -14,6 +21,9 @@ export default function RollingUnit({ bannerData, imageType, spotlightId }) {
|
||||
const [lastIndex, setLastIndex] = useState(rollingDataLength - 1);
|
||||
const [onFocus, setOnFocus] = useState(false);
|
||||
|
||||
const SpottableComponent = Spottable("div");
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getIndex = useCallback(
|
||||
(index) => {
|
||||
setStartIndex(index);
|
||||
@@ -28,6 +38,15 @@ export default function RollingUnit({ bannerData, imageType, spotlightId }) {
|
||||
[onFocus]
|
||||
);
|
||||
|
||||
// 비디오 클릭
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(
|
||||
pushPanel({
|
||||
name: panel_names.PlayerPanel,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (rollingDataLength === 1) {
|
||||
return;
|
||||
@@ -80,11 +99,14 @@ export default function RollingUnit({ bannerData, imageType, spotlightId }) {
|
||||
/>
|
||||
) : rollingData[startIndex].shptmBanrTpNm === "LIVE" ||
|
||||
rollingData[startIndex].shptmBanrTpNm === "VOD" ? (
|
||||
<>
|
||||
<p>비디오 구현 중</p>
|
||||
</>
|
||||
<VideoPlayer>
|
||||
<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" ? (
|
||||
<HomeTodayDeal
|
||||
/* <HomeTodayDeal
|
||||
imgAlt={rollingData[startIndex].tmnlImgAlt}
|
||||
imageName={rollingData[startIndex].tmnlImgNm}
|
||||
imgPath={rollingData[startIndex].tmnlImgPath}
|
||||
@@ -100,7 +122,15 @@ export default function RollingUnit({ bannerData, imageType, spotlightId }) {
|
||||
getIndex={getIndex}
|
||||
getFocus={getFocus}
|
||||
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}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,54 +1,90 @@
|
||||
@import "../../../style/CommonStyle.module.less";
|
||||
@import "../../../style/utils.module.less";
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
.leftBtn {
|
||||
.position(@position: absolute, @top: 408px, @right: auto, @bottom: auto, @left: 18px);
|
||||
.size(@w: 42px, @h: 42px);
|
||||
background-image: url("../../../../assets/icon/button_icon/btn_prev_thumb_nor.png");
|
||||
background-size: 42px 42px;
|
||||
background-position: center center;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
&:focus {
|
||||
background-image: url("../../../../assets/icon/button_icon/btn_prev_thumb_foc.png");
|
||||
}
|
||||
}
|
||||
.rightBtn {
|
||||
.position(@position: absolute, @top: 408px, @right: 18px, @bottom: auto, @left: auto);
|
||||
.size(@w: 42px, @h: 42px);
|
||||
background-image: url("../../../../assets/icon/button_icon/btn_next_thumb_nor.png");
|
||||
background-size: 42px 42px;
|
||||
background-position: center center;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
&:focus {
|
||||
background-image: url("../../../../assets/icon/button_icon/btn_next_thumb_foc.png");
|
||||
}
|
||||
}
|
||||
&.horizontal {
|
||||
.leftBtn {
|
||||
.position(@position: absolute, @top: 189px, @right: auto, @bottom: auto, @left: 18px);
|
||||
.size(@w: 42px, @h: 42px);
|
||||
background-image: url("../../../../assets/icon/button_icon/btn_prev_thumb_nor.png");
|
||||
background-size: 42px 42px;
|
||||
background-position: center center;
|
||||
z-index: 1;
|
||||
&:focus {
|
||||
background-image: url("../../../../assets/icon/button_icon/btn_prev_thumb_foc.png");
|
||||
}
|
||||
}
|
||||
.rightBtn {
|
||||
.position(@position: absolute, @top: 189px, @right: 18px, @bottom: auto, @left:auto);
|
||||
.size(@w: 42px, @h: 42px);
|
||||
background-image: url("../../../../assets/icon/button_icon/btn_next_thumb_nor.png");
|
||||
background-size: 42px 42px;
|
||||
background-position: center center;
|
||||
z-index: 1;
|
||||
&:focus {
|
||||
background-image: url("../../../../assets/icon/button_icon/btn_next_thumb_foc.png");
|
||||
}
|
||||
}
|
||||
.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: contain;
|
||||
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%);
|
||||
}
|
||||
|
||||
.exitBtnPosition {
|
||||
position: fixed;
|
||||
left: 80px;
|
||||
top: 80px;
|
||||
&.lt {
|
||||
left: initial;
|
||||
right: 80px;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
1057
com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.js
Normal 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;
|
||||
}
|
||||
6
com.twin.app.shoptime/src/views/PlayerPanel/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"main": "PlayerPanel.js",
|
||||
"styles": [
|
||||
"PlayerPanel.module.less"
|
||||
]
|
||||
}
|
||||