@@ -200,6 +200,7 @@ const TAB_IDS = {
200200 transcript : "transcript" ,
201201 audio : "audio" ,
202202 cursor : "cursor" ,
203+ clips : "clips" ,
203204 hotkeys : "hotkeys" ,
204205} as const ;
205206
@@ -214,6 +215,7 @@ export function ConfigSidebar() {
214215 | "transcript"
215216 | "audio"
216217 | "cursor"
218+ | "clips"
217219 | "hotkeys" ,
218220 } ) ;
219221
@@ -247,6 +249,7 @@ export function ConfigSidebar() {
247249 meta ( ) . type === "multiple" && ( meta ( ) as any ) . segments [ 0 ] . cursor
248250 ) ,
249251 } ,
252+ { id : TAB_IDS . clips , icon : IconCapScissors } ,
250253 // { id: "hotkeys" as const, icon: IconCapHotkeys },
251254 ] }
252255 >
@@ -417,6 +420,7 @@ export function ConfigSidebar() {
417420 </ Field >
418421 ) }
419422 </ KTabs . Content >
423+ < ClipConfig scrollRef = { scrollRef } />
420424 < KTabs . Content value = "cursor" class = "flex flex-col gap-6" >
421425 < Field
422426 name = "Cursor"
@@ -1883,6 +1887,237 @@ function ZoomSegmentConfig(props: {
18831887 ) ;
18841888}
18851889
1890+ function ClipConfig ( props : { scrollRef : HTMLDivElement } ) {
1891+ const { project, setProject, editorState, setEditorState } =
1892+ useEditorContext ( ) ;
1893+
1894+ const SPEED_PRESETS = [
1895+ { label : "0.25x" , value : 4 } ,
1896+ { label : "0.5x" , value : 2 } ,
1897+ { label : "0.75x" , value : 1.33 } ,
1898+ { label : "1x (Normal)" , value : 1 } ,
1899+ { label : "1.25x" , value : 0.8 } ,
1900+ { label : "1.5x" , value : 0.67 } ,
1901+ { label : "2x" , value : 0.5 } ,
1902+ { label : "3x" , value : 0.33 } ,
1903+ { label : "4x" , value : 0.25 } ,
1904+ ] ;
1905+
1906+ const segments = ( ) => project . timeline ?. segments ?? [ ] ;
1907+ const selectedSegmentIndex = ( ) => editorState . selectedClipIndex ;
1908+
1909+ const hasSelectedSegment = ( ) =>
1910+ selectedSegmentIndex ( ) !== null &&
1911+ segments ( ) . length > 0 &&
1912+ typeof selectedSegmentIndex ( ) === "number" &&
1913+ selectedSegmentIndex ( ) ! >= 0 &&
1914+ selectedSegmentIndex ( ) ! < segments ( ) . length ;
1915+
1916+ const selectedSegment = ( ) =>
1917+ hasSelectedSegment ( ) ? segments ( ) [ selectedSegmentIndex ( ) ! ] : null ;
1918+
1919+ return (
1920+ < KTabs . Content value = "clips" class = "flex flex-col gap-6" >
1921+ < Field name = "Clip Settings" icon = { < IconCapScissors /> } >
1922+ < Show
1923+ when = { segments ( ) . length > 0 }
1924+ fallback = {
1925+ < div class = "text-gray-500 text-center py-4" >
1926+ No clips available. Split your recording to create clips.
1927+ </ div >
1928+ }
1929+ >
1930+ < div class = "flex flex-col gap-4" >
1931+ < Subfield name = "Select Clip" required >
1932+ < KSelect
1933+ options = { segments ( ) . map ( ( _ , index ) => ( {
1934+ label : `Clip ${ index + 1 } ` ,
1935+ value : index ,
1936+ } ) ) }
1937+ optionValue = "value"
1938+ optionTextValue = "label"
1939+ value = {
1940+ hasSelectedSegment ( )
1941+ ? {
1942+ label : `Clip ${ selectedSegmentIndex ( ) ! + 1 } ` ,
1943+ value : selectedSegmentIndex ( ) ! ,
1944+ }
1945+ : undefined
1946+ }
1947+ onChange = { ( selected ) => {
1948+ if ( selected ) {
1949+ setEditorState ( "selectedClipIndex" , selected . value ) ;
1950+ }
1951+ } }
1952+ placeholder = "Select a clip"
1953+ itemComponent = { ( props ) => (
1954+ < MenuItem < typeof KSelect . Item >
1955+ as = { KSelect . Item }
1956+ item = { props . item }
1957+ >
1958+ < KSelect . ItemLabel class = "flex-1" >
1959+ { props . item . rawValue . label }
1960+ </ KSelect . ItemLabel >
1961+ </ MenuItem >
1962+ ) }
1963+ >
1964+ < KSelect . Trigger class = "flex flex-row gap-2 items-center px-2 w-full h-8 bg-gray-200 rounded-lg transition-colors disabled:text-gray-400" >
1965+ < KSelect . Value < {
1966+ label : string ;
1967+ value : number ;
1968+ } > class = "flex-1 text-sm text-left truncate text-[--gray-500] font-normal" >
1969+ { ( state ) =>
1970+ state . selectedOption ( ) ? (
1971+ < span > { state . selectedOption ( ) . label } </ span >
1972+ ) : (
1973+ < span class = "text-gray-400" > Select clip</ span >
1974+ )
1975+ }
1976+ </ KSelect . Value >
1977+ < KSelect . Icon < ValidComponent >
1978+ as = { ( props ) => (
1979+ < IconCapChevronDown
1980+ { ...props }
1981+ class = "size-4 shrink-0 transform transition-transform ui-expanded:rotate-180 text-[--gray-500]"
1982+ />
1983+ ) }
1984+ />
1985+ </ KSelect . Trigger >
1986+ < KSelect . Portal >
1987+ < PopperContent < typeof KSelect . Content >
1988+ as = { KSelect . Content }
1989+ class = { cx ( topSlideAnimateClasses , "z-50" ) }
1990+ >
1991+ < MenuItemList < typeof KSelect . Listbox >
1992+ class = "overflow-y-auto max-h-32"
1993+ as = { KSelect . Listbox }
1994+ />
1995+ </ PopperContent >
1996+ </ KSelect . Portal >
1997+ </ KSelect >
1998+ </ Subfield >
1999+
2000+ < Show when = { hasSelectedSegment ( ) && selectedSegment ( ) } >
2001+ < Subfield name = "Playback Speed" required >
2002+ < KSelect
2003+ options = { SPEED_PRESETS }
2004+ optionValue = "value"
2005+ optionTextValue = "label"
2006+ value = {
2007+ SPEED_PRESETS . find (
2008+ ( preset ) =>
2009+ Math . abs ( preset . value - selectedSegment ( ) ! . timescale ) <
2010+ 0.01
2011+ ) || {
2012+ label : `${ ( 1 / selectedSegment ( ) ! . timescale ) . toFixed (
2013+ 2
2014+ ) } x`,
2015+ value : selectedSegment ( ) ! . timescale ,
2016+ }
2017+ }
2018+ onChange = { ( selected ) => {
2019+ if ( selected ) {
2020+ setProject (
2021+ "timeline" ,
2022+ "segments" ,
2023+ selectedSegmentIndex ( ) ! ,
2024+ "timescale" ,
2025+ selected . value
2026+ ) ;
2027+ }
2028+ } }
2029+ itemComponent = { ( props ) => (
2030+ < MenuItem < typeof KSelect . Item >
2031+ as = { KSelect . Item }
2032+ item = { props . item }
2033+ >
2034+ < KSelect . ItemLabel class = "flex-1" >
2035+ { props . item . rawValue . label }
2036+ </ KSelect . ItemLabel >
2037+ </ MenuItem >
2038+ ) }
2039+ >
2040+ < KSelect . Trigger class = "flex flex-row gap-2 items-center px-2 w-full h-8 bg-gray-200 rounded-lg transition-colors disabled:text-gray-400" >
2041+ < KSelect . Value < {
2042+ label : string ;
2043+ value : number ;
2044+ } > class = "flex-1 text-sm text-left truncate text-[--gray-500] font-normal" >
2045+ { ( state ) => < span > { state . selectedOption ( ) . label } </ span > }
2046+ </ KSelect . Value >
2047+ < KSelect . Icon < ValidComponent >
2048+ as = { ( props ) => (
2049+ < IconCapChevronDown
2050+ { ...props }
2051+ class = "size-4 shrink-0 transform transition-transform ui-expanded:rotate-180 text-[--gray-500]"
2052+ />
2053+ ) }
2054+ />
2055+ </ KSelect . Trigger >
2056+ < KSelect . Portal >
2057+ < PopperContent < typeof KSelect . Content >
2058+ as = { KSelect . Content }
2059+ class = { cx ( topSlideAnimateClasses , "z-50" ) }
2060+ >
2061+ < MenuItemList < typeof KSelect . Listbox >
2062+ class = "overflow-y-auto max-h-32"
2063+ as = { KSelect . Listbox }
2064+ />
2065+ </ PopperContent >
2066+ </ KSelect . Portal >
2067+ </ KSelect >
2068+ </ Subfield >
2069+
2070+ < Subfield name = "Custom Speed" >
2071+ < div class = "flex items-center gap-2" >
2072+ < input
2073+ type = "number"
2074+ min = "0.1"
2075+ max = "10"
2076+ step = "0.1"
2077+ value = { ( 1 / selectedSegment ( ) ! . timescale ) . toFixed ( 2 ) }
2078+ onInput = { ( e ) => {
2079+ const value = parseFloat ( e . currentTarget . value ) ;
2080+ if ( ! isNaN ( value ) && value > 0 ) {
2081+ setProject (
2082+ "timeline" ,
2083+ "segments" ,
2084+ selectedSegmentIndex ( ) ! ,
2085+ "timescale" ,
2086+ 1 / value
2087+ ) ;
2088+ }
2089+ } }
2090+ class = "w-20 px-2 py-1 rounded border border-gray-300 bg-white"
2091+ />
2092+ < span class = "text-gray-500" > x</ span >
2093+ </ div >
2094+ </ Subfield >
2095+
2096+ < div class = "mt-2" >
2097+ < p class = "text-xs text-gray-500" >
2098+ Original duration:{ " " }
2099+ { ( selectedSegment ( ) ! . end - selectedSegment ( ) ! . start ) . toFixed (
2100+ 2
2101+ ) }
2102+ s
2103+ </ p >
2104+ < p class = "text-xs text-gray-500" >
2105+ Playback duration:{ " " }
2106+ { (
2107+ ( selectedSegment ( ) ! . end - selectedSegment ( ) ! . start ) /
2108+ selectedSegment ( ) ! . timescale
2109+ ) . toFixed ( 2 ) }
2110+ s
2111+ </ p >
2112+ </ div >
2113+ </ Show >
2114+ </ div >
2115+ </ Show >
2116+ </ Field >
2117+ </ KTabs . Content >
2118+ ) ;
2119+ }
2120+
18862121function RgbInput ( props : {
18872122 value : [ number , number , number ] ;
18882123 onChange : ( value : [ number , number , number ] ) => void ;
0 commit comments