@@ -19,7 +19,9 @@ import {
19
19
Button ,
20
20
Fade ,
21
21
IconButton ,
22
+ Paper ,
22
23
Popover ,
24
+ Popper ,
23
25
Slider ,
24
26
Theme ,
25
27
Typography ,
@@ -268,34 +270,84 @@ function Watch() {
268
270
// . (period): Forward 1 frame
269
271
useEffect ( ( ) => {
270
272
const handleKeyDown = ( e : KeyboardEvent ) => {
271
- if ( e . key === " " && player . current ) {
272
- setPlaying ( ( state ) => ! state ) ;
273
- }
274
- if ( e . key === "ArrowLeft" && player . current ) {
275
- player . current . seekTo ( player . current . getCurrentTime ( ) - 10 ) ;
276
- }
277
- if ( e . key === "ArrowRight" && player . current ) {
278
- player . current . seekTo ( player . current . getCurrentTime ( ) + 10 ) ;
279
- }
280
- if ( e . key === "ArrowUp" && player . current ) {
281
- setVolume ( ( state ) => Math . min ( state + 5 , 100 ) ) ;
282
- }
283
- if ( e . key === "ArrowDown" && player . current ) {
284
- setVolume ( ( state ) => Math . max ( state - 5 , 0 ) ) ;
285
- }
286
- if ( e . key === "," && player . current ) {
287
- player . current . seekTo ( player . current . getCurrentTime ( ) - 0.04 ) ;
288
- }
289
- if ( e . key === "." && player . current ) {
290
- player . current . seekTo ( player . current . getCurrentTime ( ) + 0.04 ) ;
291
- }
273
+ const actions : { [ key : string ] : ( ) => void } = {
274
+ " " : ( ) => setPlaying ( ( state ) => ! state ) ,
275
+ k : ( ) => setPlaying ( ( state ) => ! state ) ,
276
+ j : ( ) => player . current ?. seekTo ( player . current . getCurrentTime ( ) - 10 ) ,
277
+ l : ( ) => player . current ?. seekTo ( player . current . getCurrentTime ( ) + 10 ) ,
278
+ s : ( ) => {
279
+ if ( ! metadata || ! player . current ) return ;
280
+ // if there is a marker like credits skip it
281
+ const time = player . current . getCurrentTime ( ) ;
282
+ for ( const marker of metadata . Marker ?? [ ] ) {
283
+ if (
284
+ ! (
285
+ marker . startTimeOffset / 1000 <= time &&
286
+ marker . endTimeOffset / 1000 >= time
287
+ )
288
+ )
289
+ continue ;
290
+
291
+ switch ( marker . type ) {
292
+ case "credits" :
293
+ {
294
+ if ( ! marker . final ) {
295
+ player . current . seekTo ( marker . endTimeOffset / 1000 + 1 ) ;
296
+ return ;
297
+ }
298
+
299
+ if ( metadata . type === "movie" )
300
+ return navigate (
301
+ `/browse/${ metadata . librarySectionID } ?${ queryBuilder ( {
302
+ mid : metadata . ratingKey ,
303
+ } ) } `
304
+ ) ;
305
+
306
+ if ( ! playQueue ) return ;
307
+ const next = playQueue [ 1 ] ;
308
+ if ( ! next )
309
+ return navigate (
310
+ `/browse/${ metadata . librarySectionID } ?${ queryBuilder ( {
311
+ mid : metadata . grandparentRatingKey ,
312
+ pid : metadata . parentRatingKey ,
313
+ iid : metadata . ratingKey ,
314
+ } ) } `
315
+ ) ;
316
+
317
+ navigate ( `/watch/${ next . ratingKey } ` ) ;
318
+ }
319
+ break ;
320
+ case "intro" :
321
+ player . current . seekTo ( marker . endTimeOffset / 1000 + 1 ) ;
322
+ break ;
323
+ }
324
+ }
325
+ } ,
326
+ f : ( ) => {
327
+ if ( ! document . fullscreenElement ) {
328
+ document . documentElement . requestFullscreen ( ) ;
329
+ } else document . exitFullscreen ( ) ;
330
+ } ,
331
+ ArrowLeft : ( ) =>
332
+ player . current ?. seekTo ( player . current . getCurrentTime ( ) - 10 ) ,
333
+ ArrowRight : ( ) =>
334
+ player . current ?. seekTo ( player . current . getCurrentTime ( ) + 10 ) ,
335
+ ArrowUp : ( ) => setVolume ( ( state ) => Math . min ( state + 5 , 100 ) ) ,
336
+ ArrowDown : ( ) => setVolume ( ( state ) => Math . max ( state - 5 , 0 ) ) ,
337
+ "," : ( ) =>
338
+ player . current ?. seekTo ( player . current . getCurrentTime ( ) - 0.04 ) ,
339
+ "." : ( ) =>
340
+ player . current ?. seekTo ( player . current . getCurrentTime ( ) + 0.04 ) ,
341
+ } ;
342
+
343
+ if ( actions [ e . key ] ) actions [ e . key ] ( ) ;
292
344
} ;
293
345
294
346
document . addEventListener ( "keydown" , handleKeyDown ) ;
295
347
return ( ) => {
296
348
document . removeEventListener ( "keydown" , handleKeyDown ) ;
297
349
} ;
298
- } , [ ] ) ;
350
+ } , [ metadata , navigate , playQueue ] ) ;
299
351
300
352
return (
301
353
< >
@@ -475,7 +527,7 @@ function Watch() {
475
527
sx = { {
476
528
fontSize : "1vw" ,
477
529
color : "#FFF" ,
478
- mt : "-0.75vw " ,
530
+ mt : "-0.70vw " ,
479
531
} }
480
532
>
481
533
{ showmetadata ?. childCount &&
@@ -487,7 +539,7 @@ function Watch() {
487
539
fontSize : "1vw" ,
488
540
fontWeight : "bold" ,
489
541
color : "#FFF" ,
490
- mt : "10px " ,
542
+ mt : "6px " ,
491
543
} }
492
544
>
493
545
{ metadata ?. title } : EP. { metadata ?. index }
@@ -499,7 +551,7 @@ function Watch() {
499
551
flexDirection : "row" ,
500
552
alignItems : "center" ,
501
553
justifyContent : "flex-start" ,
502
- mt : 0.5 ,
554
+ mt : "2px" ,
503
555
gap : 1 ,
504
556
} }
505
557
>
@@ -565,6 +617,7 @@ function Watch() {
565
617
sx = { {
566
618
fontSize : "0.75vw" ,
567
619
color : "#FFF" ,
620
+ mt : "2px" ,
568
621
} }
569
622
>
570
623
{ metadata ?. summary }
@@ -1450,15 +1503,7 @@ function Watch() {
1450
1503
) }
1451
1504
</ IconButton >
1452
1505
1453
- { playQueue && playQueue [ 1 ] && (
1454
- < IconButton
1455
- onClick = { ( ) => {
1456
- navigate ( `/watch/${ playQueue [ 1 ] . ratingKey } ` ) ;
1457
- } }
1458
- >
1459
- < SkipNext fontSize = "large" />
1460
- </ IconButton >
1461
- ) }
1506
+ { playQueue && < NextEPButton queue = { playQueue } /> }
1462
1507
</ Box >
1463
1508
1464
1509
{ metadata . type === "movie" && (
@@ -1628,6 +1673,7 @@ function Watch() {
1628
1673
if ( showError ) return ;
1629
1674
1630
1675
// filter out links from the error messages
1676
+ if ( ! err . error ) return ;
1631
1677
const message = err . error . message . replace (
1632
1678
/ h t t p s ? : \/ \/ [ ^ \s ] + / g,
1633
1679
"Media"
@@ -1682,6 +1728,131 @@ function Watch() {
1682
1728
}
1683
1729
1684
1730
export default Watch ;
1731
+
1732
+ function NextEPButton ( { queue } : { queue ?: Plex . Metadata [ ] } ) {
1733
+ const navigate = useNavigate ( ) ;
1734
+
1735
+ const [ anchorEl , setAnchorEl ] = useState < null | HTMLElement > ( null ) ;
1736
+
1737
+ if ( ! queue ) return < > </ > ;
1738
+
1739
+ return (
1740
+ < >
1741
+ < Popper
1742
+ open = { Boolean ( anchorEl ) }
1743
+ anchorEl = { anchorEl }
1744
+ placement = "top-start"
1745
+ transition
1746
+ sx = { { zIndex : 10000 } }
1747
+ modifiers = { [
1748
+ {
1749
+ name : "offset" ,
1750
+ options : {
1751
+ offset : [ 0 , 10 ] ,
1752
+ } ,
1753
+ } ,
1754
+ ] }
1755
+ >
1756
+ { ( { TransitionProps } ) => (
1757
+ < Fade { ...TransitionProps } timeout = { 350 } >
1758
+ < Paper
1759
+ sx = { {
1760
+ width : "575px" ,
1761
+ height : "160px" ,
1762
+ overflow : "hidden" ,
1763
+ background : "#101010" ,
1764
+
1765
+ display : "flex" ,
1766
+ flexDirection : "row" ,
1767
+ alignItems : "flex-start" ,
1768
+ justifyContent : "flex-start" ,
1769
+ } }
1770
+ >
1771
+ < img
1772
+ src = { `${ getTranscodeImageURL (
1773
+ `${ queue [ 1 ] . thumb } ?X-Plex-Token=${ localStorage . getItem (
1774
+ "accessToken"
1775
+ ) } `,
1776
+ 500 ,
1777
+ 500
1778
+ ) } `}
1779
+ alt = ""
1780
+ style = { {
1781
+ height : "100%" ,
1782
+ aspectRatio : "16/9" ,
1783
+ width : "auto" ,
1784
+ } }
1785
+ />
1786
+
1787
+ < Box
1788
+ sx = { {
1789
+ width : "100%" ,
1790
+ height : "100%" ,
1791
+ display : "flex" ,
1792
+ flexDirection : "column" ,
1793
+ alignItems : "flex-start" ,
1794
+ justifyContent : "flex-start" ,
1795
+ p : 2 ,
1796
+ } }
1797
+ >
1798
+ < Typography
1799
+ sx = { {
1800
+ fontSize : "12px" ,
1801
+ fontWeight : "700" ,
1802
+ letterSpacing : "0.15em" ,
1803
+ color : "#e6a104" ,
1804
+ textTransform : "uppercase" ,
1805
+ } }
1806
+ >
1807
+ { queue [ 1 ] . type } { " " }
1808
+ { queue [ 1 ] . type === "episode" && queue [ 1 ] . index }
1809
+ </ Typography >
1810
+ < Typography
1811
+ sx = { {
1812
+ fontSize : "14px" ,
1813
+ fontWeight : "bold" ,
1814
+ color : "#FFF" ,
1815
+ } }
1816
+ >
1817
+ { queue [ 1 ] . title }
1818
+ </ Typography >
1819
+
1820
+ < Typography
1821
+ sx = { {
1822
+ mt : "2px" ,
1823
+ fontSize : "10px" ,
1824
+ color : "#FFF" ,
1825
+
1826
+ // max 5 lines
1827
+ display : "-webkit-box" ,
1828
+ WebkitLineClamp : 5 ,
1829
+ WebkitBoxOrient : "vertical" ,
1830
+ overflow : "hidden" ,
1831
+ textOverflow : "ellipsis" ,
1832
+ } }
1833
+ >
1834
+ { queue [ 1 ] . summary }
1835
+ </ Typography >
1836
+ </ Box >
1837
+ </ Paper >
1838
+ </ Fade >
1839
+ ) }
1840
+ </ Popper >
1841
+ { queue && queue [ 1 ] && (
1842
+ < IconButton
1843
+ onClick = { ( ) => {
1844
+ navigate ( `/watch/${ queue [ 1 ] . ratingKey } ` ) ;
1845
+ } }
1846
+ onMouseEnter = { ( e ) => setAnchorEl ( e . currentTarget ) }
1847
+ onMouseLeave = { ( ) => setAnchorEl ( null ) }
1848
+ >
1849
+ < SkipNext fontSize = "large" />
1850
+ </ IconButton >
1851
+ ) }
1852
+ </ >
1853
+ ) ;
1854
+ }
1855
+
1685
1856
function TuneSettingTab (
1686
1857
theme : Theme ,
1687
1858
setTunePage : React . Dispatch < React . SetStateAction < number > > ,
0 commit comments