Skip to content

Instantly share code, notes, and snippets.

@polooner
Created January 29, 2024 20:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save polooner/4adc157bbc6b9c51b442f47f1dfaba80 to your computer and use it in GitHub Desktop.
Save polooner/4adc157bbc6b9c51b442f47f1dfaba80 to your computer and use it in GitHub Desktop.
import { useMakeKlips } from '@/lib/hooks/use-makeklips';
import { createTimeframe } from '@/lib/utils';
import { MakeKlipsRefType } from '@/types/makeklips';
import {
ChangeEvent,
KeyboardEvent,
MouseEvent,
SyntheticEvent,
forwardRef,
useEffect,
useRef,
useState,
} from 'react';
import { KeyFrame } from '@/types';
interface CaptionContainerProps {
keyFrames: KeyFrame[];
}
//TODO: transform frames to timestamps, use createTimeframe()
export const Captions = forwardRef<MakeKlipsRefType, CaptionContainerProps>(
({ keyFrames, ...rest }, ref) => {
//TODO: use the global hook to handle word change
const { getKeyFrames } = useMakeKlips();
// setKeyFrames(keyFrames);
const frames = getKeyFrames();
return (
//TODO: set height to only be the height of player, scroll overflow
<div
{...rest}
ref={ref}
className='flex-1 gap-2 overflow-scroll flex flex-col bg-zinc-950 text-white p-4 overflow-y-auto max-h-full'
>
{frames
.sort((a, b) => a.timeFrom - b.timeFrom) // Sort keyFrames by timeFrom property
.map((keyFrame, index) => (
<CaptionBox
key={index}
id={keyFrame.id}
timeFrom={keyFrame.timeFrom}
timeTo={keyFrame.timeTo}
text={keyFrame.text}
{...keyFrame.data}
/>
))}
</div>
);
}
);
Captions.displayName = 'Captions';
function CaptionBox({ id, timeFrom, timeTo, text }: KeyFrame) {
console.log(text);
const [value, setValue] = useState(text);
const textareaRef = useRef<HTMLTextAreaElement>(null);
// const [cursorPosition, setCursorPosition] = useState(0);
const { getKeyFrames, updateKeyFrame, setKeyFrames } = useMakeKlips();
// const handleCursorChange = (e: KeyboardEvent) => {
// const input = textareaRef.current;
// if (input) {
// const cursorPosition = input.selectionStart;
// const textBeforeCursor = input.value.substring(0, cursorPosition);
// const lineCount = textBeforeCursor.split('\n').length;
// setCursorPosition(lineCount);
// console.log(cursorPosition);
// }
// };
//TODO: on backspace, pop the sentence and append to first if last character is the newline sequence (\r\n)
//TODO: on enter, sentence into another array element
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
//FIXME: text and other elements are a render behind
console.log(e.target.value);
setValue(e.target.value);
console.log(e.target.value.includes('\n\n'));
// if pushed a double new line, just cut at the end of double \n and create a new keyframe
if (e.target.value.includes('\n\n')) {
// can create an empty string. should this be prevented and alerted to the user?
//FIXME: we see a new object in console but no new CaptionBox appears
const valueCutIntoArray = e.target.value.split('\n\n');
setValue(valueCutIntoArray[0]);
updateKeyFrame(String(id), {
text: valueCutIntoArray[0],
timeFrom,
timeTo,
id,
});
//TODO: calculate %
//TODO: set timeFrom to receive the amount of frames corresponding the the % that the text is of the full text
//TODO: prevent user from infinitely pressing Enter
console.log(valueCutIntoArray[0].length + valueCutIntoArray[1].length);
setKeyFrames((prevKeyFrames) => {
return prevKeyFrames.concat({
id: prevKeyFrames.length + 1,
text: valueCutIntoArray[1],
timeFrom: 160,
timeTo: 180,
});
});
console.log(valueCutIntoArray);
} else {
updateKeyFrame(String(id), {
text: value,
timeFrom,
timeTo,
id,
});
}
const frames = getKeyFrames();
console.log(frames);
};
return (
<div className='p-4 hover:bg-zinc-900 w-full rounded-2xl flex justify-between items-center !bg-zinc-900 flex-row'>
<div style={{ direction: 'ltr' }} className='w-full'>
<span
suppressHydrationWarning
className='text-xs px-2 font-sans block text-slate-500 font-display'
>
{timeFrom}-{timeTo}
</span>
{/* TODO: stretch textarea infinitely */}
<textarea
ref={textareaRef}
className='resize-none w-full text-white bg-transparent overflow-hidden p-2 font-inherit'
value={value}
onChange={(e) => handleChange(e)}
// onKeyUp={handleCursorChange}
// onClick={handleCursorChange}
// onSelect={handleCursorChange}
/>
</div>
</div>
);
}
export default Captions;
'use client';
import { useRef, type ReactNode } from 'react';
import { type StoreApi } from 'zustand';
import { UseBoundStoreWithEqualityFn } from 'zustand/traditional';
import { Provider } from '../lib/contexts/MKStoreContext';
import { createMKStore } from '../lib/store';
import { KeyFrame, MakeKlipsState } from '@/types';
export function MakeKlipsProvider({
children,
initialKeyFrames,
}: {
children: ReactNode;
initialKeyFrames: KeyFrame[];
}) {
const storeRef = useRef<UseBoundStoreWithEqualityFn<
StoreApi<MakeKlipsState>
> | null>(null);
if (!storeRef.current) {
storeRef.current = createMKStore({
keyFrames: initialKeyFrames,
});
}
return <Provider value={storeRef.current}>{children}</Provider>;
}
import { useCallback, useMemo, useRef } from 'react';
import { useStoreApi } from './use-store';
import type { MakeKlipsInstance, Instance, KeyFrame } from '../../types';
/**
* Hook for accessing the captions instance.
*
*
* @returns ClipsInstance
*/
export function useMakeKlips<
KeyFrameType extends KeyFrame = KeyFrame
>(): ClipsInstance<KeyFrameType> {
const store = useStoreApi();
const getKeyFrames = useCallback<Instance.GetKeyFrames<KeyFrameType>>(() => {
return store.getState().keyFrames.map((k) => ({ ...k })) as KeyFrameType[];
}, []);
const getKeyFrame = useCallback<Instance.GetKeyFrame<KeyFrameType>>((id) => {
return store.getState().frameLookup.get(id) as KeyFrameType;
}, []);
// this is used to handle multiple syncronous setNodes calls
const setKeyFramesData = useRef<KeyFrame[]>();
const setKeyFramesTimeout = useRef<ReturnType<typeof setTimeout>>();
const setKeyFrames = useCallback<Instance.SetKeyFrames<KeyFrameType>>(
(payload) => {
const {
keyFrames = [],
setKeyFrames,
} = store.getState();
const nextKeyFrames =
typeof payload === 'function'
? payload((setKeyFramesData.current as KeyFrameType[]) || keyFrames)
: payload;
setKeyFramesData.current = nextKeyFrames;
if (setKeyFramesTimeout.current) {
clearTimeout(setKeyFramesTimeout.current);
}
setKeyFramesTimeout.current = setTimeout(() => {
setKeyFrames(nextKeyFrames);
setKeyFramesData.current = undefined;
}, 0);
},
[]
);
const addKeyFrames = useCallback<Instance.AddKeyFrames<KeyFrame>>(
(payload) => {
const keyFrames = Array.isArray(payload) ? payload : [payload];
const {
keyFrames: currentKeyFrames,
setKeyFrames,
} = store.getState();
const nextKeyFrames = [...currentKeyFrames, ...keyFrames];
setKeyFrames(nextKeyFrames);
},
[]
);
const toObject = useCallback<Instance.ToObject<KeyFrameType>>(() => {
const { keyFrames = [] } = store.getState();
return {
keyFrames: keyFrames.map((k) => ({ ...k })) as KeyFrameType[],
};
}, []);
const updateKeyFrame = useCallback<Instance.UpdateKeyFrame<KeyFrameType>>(
(id, keyFrameUpdate, options = { replace: true }) => {
setKeyFrames((prevKeyFrames: KeyFrameType[]) =>
prevKeyFrames.map((keyFrame) => {
if (String(keyFrame.id) === id) {
const nextKeyFrame =
typeof keyFrameUpdate === 'function'
? (keyFrame as KeyFrameType)
: keyFrameUpdate;
return options.replace
? (nextKeyFrame as KeyFrameType)
: { ...keyFrame, ...nextKeyFrame };
}
return keyFrame;
})
);
},
[setKeyFrames]
);
return useMemo(() => {
return {
getKeyFrames,
getKeyFrame,
setKeyFrames,
addKeyFrames,
toObject,
updateKeyFrame,
// deleteElements,
// getIntersectingKeyFrames,
// isKeyFrameIntersecting,
// updateKeyFrameData,
};
}, [
getKeyFrames,
getKeyFrame,
setKeyFrames,
addKeyFrames,
toObject,
updateKeyFrame,
// deleteElements,
// getIntersectingKeyFrames,
// isKeyFrameIntersecting,
// updateKeyFrameData,
]);
}
import { useContext, useMemo } from 'react';
import { useStoreWithEqualityFn as useZustandStore } from 'zustand/traditional';
import type { StoreApi } from 'zustand';
import StoreContext from '../contexts/MKStoreContext';
import type { MakeKlipsState } from '@/types/store';
type ExtractState = StoreApi<CaptionsState> extends {
getState: () => infer T;
}
? T
: never;
/**
* Hook for accessing the internal store.
*
* @public
* @param selector
* @param equalityFn
* @returns The selected state slice
*/
function useStore<StateSlice = ExtractState>(
selector: (state: MakeKlipsState) => StateSlice,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean
) {
const store = useContext(StoreContext);
//TODO: implement ErrorMessages and pass them into the Error
if (store === null) {
throw new Error('Store is null in useStore');
}
return useZustandStore(store, selector, equalityFn);
}
const useStoreApi = () => {
const store = useContext(StoreContext);
//TODO: implement ErrorMessages and pass them into the Error
if (store === null) {
throw new Error('Store is null in useStoreApi');
}
return useMemo(
() => ({
getState: store.getState,
setState: store.setState,
subscribe: store.subscribe,
destroy: store.destroy,
}),
[store]
);
};
export { useStore, useStoreApi };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment