Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 208 additions & 11 deletions apps/desktop/src/routes/editor/ConfigSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ import {
audioEnhancementStore,
generalSettingsStore,
} from "~/store";
import {
defaultKeyboardSettings,
KEYCAP_STYLE_OPTIONS,
KEYCAP_THEME_COLORS,
KEYCAP_THEME_OPTIONS,
type KeyboardSettings,
type KeycapStyle,
type KeycapTheme,
} from "~/store/keyboard";
import {
createSelectedOrganization,
getOrganizationBrandColorSwatches,
Expand Down Expand Up @@ -111,7 +120,8 @@ import {
} from "./context";
import { GradientEditor } from "./GradientEditor";
import { ImageSegmentConfig } from "./image-segment-config";
import { KeyboardTab } from "./KeyboardTab";
import { KeyboardTab } from "./keyboard-tab";
import { KeycapPreviewCluster, parseShortcutKeys } from "./keycap-renderer";
import {
encodeMaskEffect,
getMaskEffect,
Expand Down Expand Up @@ -625,16 +635,18 @@ function ConfigSidebarContent() {
});
};

type ConfigSidebarTab =
| "background"
| "camera"
| "transcript"
| "audio"
| "cursor"
| "keyboard"
| "hotkeys"
| "captions";

const [state, setState] = createStore({
selectedTab: "background" as
| "background"
| "camera"
| "transcript"
| "audio"
| "cursor"
| "keyboard"
| "hotkeys"
| "captions",
selectedTab: "background" as ConfigSidebarTab,
});

// Clip selection is a timeline-only affordance (highlight, Delete key,
Expand Down Expand Up @@ -684,6 +696,9 @@ function ConfigSidebarContent() {
? undefined
: state.selectedTab
}
onChange={(v) => {
if (v) setState("selectedTab", v as ConfigSidebarTab);
}}
class="flex overflow-hidden z-10 flex-col flex-1 min-h-0 max-w-104 rounded-xl shrink-0 bg-ed-card shadow-ed-card"
>
<KTabs.List class="flex sticky top-0 z-60 flex-row justify-around items-center px-2.5 h-[46px] border-b border-ed-line shrink-0 bg-ed-card">
Expand Down Expand Up @@ -3735,7 +3750,7 @@ function KeyboardSegmentConfig(props: {
segmentIndex: number;
segment: KeyboardTrackSegment;
}) {
const { setProject } = useEditorContext();
const { project, setProject } = useEditorContext();

const updateSegment = (fn: (segment: KeyboardTrackSegment) => void) => {
setProject(
Expand All @@ -3749,8 +3764,70 @@ function KeyboardSegmentConfig(props: {
);
};

const getSetting = <K extends keyof KeyboardSettings>(
key: K,
): NonNullable<KeyboardSettings[K]> => {
const settings = project?.keyboard?.settings;
if (settings && key in settings) {
return (settings as Record<string, unknown>)[
key as string
] as NonNullable<KeyboardSettings[K]>;
}
return defaultKeyboardSettings[key] as NonNullable<KeyboardSettings[K]>;
};

const updateSetting = <K extends keyof KeyboardSettings>(
key: K,
value: KeyboardSettings[K],
) => {
if (!project?.keyboard) {
setProject("keyboard", {
settings: { ...defaultKeyboardSettings, [key]: value },
});
return;
}
setProject("keyboard", "settings", key, value);
};

const updateSettings = (partial: Partial<KeyboardSettings>) => {
if (!project?.keyboard) {
setProject("keyboard", {
settings: { ...defaultKeyboardSettings, ...partial },
});
return;
}
for (const [key, value] of Object.entries(partial)) {
setProject("keyboard", "settings", key as keyof KeyboardSettings, value);
}
};

const previewKeys = createMemo(() => {
const text = props.segment.displayText || "";
if (!text) return ["⌨"];
return parseShortcutKeys(text);
});

return (
<div class="space-y-4">
<div class="flex flex-col items-center justify-center p-3 rounded-xl bg-ed-ctl/50 border border-ed-line overflow-hidden">
<div class="text-[10px] font-semibold text-ed-text-3 mb-2.5 tracking-wider uppercase">
Keycap Preview
</div>
<KeycapPreviewCluster
keys={previewKeys()}
style={getSetting("style")}
theme={getSetting("theme")}
showChassis={getSetting("showChassis")}
use3D={getSetting("keycapMode")}
scale={0.9}
customTextColor={props.segment.colorOverride ?? getSetting("color")}
customBgColor={
props.segment.backgroundColorOverride ??
getSetting("backgroundColor")
}
/>
</div>

<Section name={`Keyboard ${props.segmentIndex + 1}`}>
<Input
type="text"
Expand All @@ -3762,6 +3839,126 @@ function KeyboardSegmentConfig(props: {
}
/>
</Section>

<Section name="Keycap Design">
<div class="flex flex-col gap-2">
<Field name="Keycap Style" inline>
<KSelect<KeycapStyle>
options={KEYCAP_STYLE_OPTIONS.map((s) => s.value)}
value={getSetting("style")}
onChange={(value) => {
if (value === null) return;
updateSetting("style", value);
}}
itemComponent={(p) => (
<MenuItem<typeof KSelect.Item> as={KSelect.Item} item={p.item}>
<KSelect.ItemLabel class="flex-1">
{
KEYCAP_STYLE_OPTIONS.find(
(s) => s.value === p.item.rawValue,
)?.label
}
</KSelect.ItemLabel>
</MenuItem>
)}
>
<KSelect.Trigger class="flex flex-row gap-1.5 items-center px-2 h-7 max-w-full rounded-[7px] text-[13px] transition-colors outline-hidden bg-ed-ctl text-ed-text-1 hover:bg-ed-ctl-hover focus-visible:ring-1 focus-visible:ring-ed-accent">
<KSelect.Value<KeycapStyle> class="truncate">
{(state) =>
KEYCAP_STYLE_OPTIONS.find(
(s) => s.value === state.selectedOption(),
)?.label
}
</KSelect.Value>
<KSelect.Icon>
<IconCapChevronDown class="shrink-0 size-3.5 text-ed-text-3" />
</KSelect.Icon>
</KSelect.Trigger>
<KSelect.Portal>
<PopperContent<typeof KSelect.Content>
as={KSelect.Content}
class={topSlideAnimateClasses}
>
<MenuItemList<typeof KSelect.Listbox>
class="overflow-y-auto max-h-48"
as={KSelect.Listbox}
/>
</PopperContent>
</KSelect.Portal>
</KSelect>
</Field>

<Field name="Color Theme" inline>
<KSelect<KeycapTheme>
options={KEYCAP_THEME_OPTIONS.map((t) => t.value)}
value={getSetting("theme")}
onChange={(value) => {
if (value === null) return;
const themeColors = KEYCAP_THEME_COLORS[value];
updateSettings({
theme: value,
...(themeColors
? {
color: themeColors.text,
backgroundColor: themeColors.background,
}
: {}),
});
}}
itemComponent={(p) => (
<MenuItem<typeof KSelect.Item> as={KSelect.Item} item={p.item}>
<KSelect.ItemLabel class="flex-1">
{
KEYCAP_THEME_OPTIONS.find(
(t) => t.value === p.item.rawValue,
)?.label
}
</KSelect.ItemLabel>
</MenuItem>
)}
>
<KSelect.Trigger class="flex flex-row gap-1.5 items-center px-2 h-7 max-w-full rounded-[7px] text-[13px] transition-colors outline-hidden bg-ed-ctl text-ed-text-1 hover:bg-ed-ctl-hover focus-visible:ring-1 focus-visible:ring-ed-accent">
<KSelect.Value<KeycapTheme> class="truncate">
{(state) =>
KEYCAP_THEME_OPTIONS.find(
(t) => t.value === state.selectedOption(),
)?.label
}
</KSelect.Value>
<KSelect.Icon>
<IconCapChevronDown class="shrink-0 size-3.5 text-ed-text-3" />
</KSelect.Icon>
</KSelect.Trigger>
<KSelect.Portal>
<PopperContent<typeof KSelect.Content>
as={KSelect.Content}
class={topSlideAnimateClasses}
>
<MenuItemList<typeof KSelect.Listbox>
class="overflow-y-auto max-h-48"
as={KSelect.Listbox}
/>
</PopperContent>
</KSelect.Portal>
</KSelect>
</Field>

<Field name="3D Keycaps Mode" inline>
<Toggle
checked={getSetting("keycapMode")}
onChange={(checked) => updateSetting("keycapMode", checked)}
/>
</Field>

<Field name="Floating Pod Chassis" inline>
<Toggle
checked={getSetting("showChassis")}
onChange={(checked) => updateSetting("showChassis", checked)}
/>
</Field>
</div>
</Section>

<Field name="Timing">
<div class="rounded-xl bg-ed-card-2 p-3 space-y-3">
<div class="grid grid-cols-[1fr_auto_1fr] gap-2 items-start">
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/routes/editor/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { createCaptionTrackSegments } from "./captions";
import { type EditorPreviewQuality, FPS, useEditorContext } from "./context";
import { FrameButton } from "./FrameButton";
import { ImageOverlay } from "./image-overlay";
import { KeyboardOverlay } from "./keyboard-overlay";
import { MaskOverlay } from "./MaskOverlay";
import { PerformanceOverlay } from "./PerformanceOverlay";
import { usePreparingEditor } from "./preparing-editor-context";
Expand Down Expand Up @@ -785,6 +786,7 @@ function PreviewCanvas(props: {
<TextOverlay size={size()} />
</div>
<CaptionOverlay size={size()} />
<KeyboardOverlay size={size()} />
<SplitScreenOverlay size={size()} />
<SnapGuidesOverlay size={size()} />
<PerformanceOverlay size={size()} />
Expand Down
86 changes: 86 additions & 0 deletions apps/desktop/src/routes/editor/keyboard-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { createMemo, Show } from "solid-js";
Comment thread
emirhangungormez marked this conversation as resolved.
import {
defaultKeyboardSettings,
type KeycapStyle,
type KeycapTheme,
} from "~/store/keyboard";
import { useEditorContext } from "./context";
import { KeycapPreviewCluster, parseShortcutKeys } from "./keycap-renderer";

type KeyboardOverlayProps = {
size: { width: number; height: number };
};

export function KeyboardOverlay(props: KeyboardOverlayProps) {
const { project, editorState } = useEditorContext();

const currentAbsoluteTime = () =>
editorState.previewTime ?? editorState.playbackTime ?? 0;

const settings = createMemo(() => ({
...defaultKeyboardSettings,
...project.keyboard?.settings,
}));

const isTrackEnabled = () => editorState.timeline.tracks.keyboard ?? true;

const activeSegment = createMemo(() => {
if (!settings().enabled || !isTrackEnabled()) return null;
const time = currentAbsoluteTime();
const segments = project.timeline?.keyboardSegments ?? [];
return segments.find((seg) => time >= seg.start && time <= seg.end) ?? null;
});

const parsedKeys = createMemo(() => {
const seg = activeSegment();
if (!seg) return [];
return parseShortcutKeys(seg.displayText || "");
});

const positionClasses = createMemo(() => {
const pos = settings().position || "bottom-center";
switch (pos) {
case "top-left":
return "items-start justify-start pt-14 pl-12";
case "top-center":
case "top":
return "items-start justify-center pt-14";
case "top-right":
return "items-start justify-end pt-14 pr-12";
case "bottom-left":
return "items-end justify-start pb-32 pl-12";
case "bottom-right":
return "items-end justify-end pb-32 pr-12";
default:
return "items-end justify-center pb-32";
}
});

return (
<Show when={activeSegment()}>
<div
class={`absolute inset-0 pointer-events-none flex ${positionClasses()} z-30 transition-all duration-150 ease-out`}
style={{
width: `${props.size.width}px`,
height: `${props.size.height}px`,
}}
>
<div class="animate-in fade-in zoom-in-95 duration-100">
<KeycapPreviewCluster
keys={parsedKeys()}
style={(settings().style || "pbt") as KeycapStyle}
theme={(settings().theme || "white") as KeycapTheme}
showChassis={settings().showChassis ?? true}
use3D={settings().keycapMode ?? true}
scale={Math.min(1.2, Math.max(0.7, props.size.width / 1200))}
customTextColor={activeSegment()?.colorOverride ?? settings().color}
customBgColor={
activeSegment()?.backgroundColorOverride ??
settings().backgroundColor
}
/>
Comment thread
emirhangungormez marked this conversation as resolved.
</div>
</div>
</Show>
);
}
Loading