pi sills
This commit is contained in:
@@ -27,6 +27,7 @@ export const VideoRenderer = ({ src, fileName, fallbackSrc }: VideoRendererProps
|
||||
useEffect(() => {
|
||||
const v = videoRef.current;
|
||||
if (!v) return;
|
||||
let blobUrl: string | null = null;
|
||||
const onLoaded = () => {
|
||||
setDuration(v.duration);
|
||||
setLoaded(true);
|
||||
@@ -35,13 +36,33 @@ export const VideoRenderer = ({ src, fileName, fallbackSrc }: VideoRendererProps
|
||||
const onPlay = () => setPlaying(true);
|
||||
const onPause = () => setPlaying(false);
|
||||
const onEnded = () => setPlaying(false);
|
||||
let fetching = false;
|
||||
let fetchDone = false;
|
||||
const onError = () => {
|
||||
if (fallbackSrc && v.src !== fallbackSrc) {
|
||||
v.src = fallbackSrc;
|
||||
v.load();
|
||||
} else {
|
||||
if (fetching) return;
|
||||
if (fetchDone) {
|
||||
setError(true);
|
||||
return;
|
||||
}
|
||||
fetching = true;
|
||||
const fetchUrl = fallbackSrc || src;
|
||||
fetch(fetchUrl)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error();
|
||||
return res.blob();
|
||||
})
|
||||
.then((blob) => {
|
||||
fetching = false;
|
||||
fetchDone = true;
|
||||
blobUrl = URL.createObjectURL(blob);
|
||||
v.src = blobUrl;
|
||||
v.load();
|
||||
})
|
||||
.catch(() => {
|
||||
fetching = false;
|
||||
fetchDone = true;
|
||||
setError(true);
|
||||
});
|
||||
};
|
||||
v.addEventListener('loadedmetadata', onLoaded);
|
||||
v.addEventListener('timeupdate', onTime);
|
||||
@@ -56,6 +77,7 @@ export const VideoRenderer = ({ src, fileName, fallbackSrc }: VideoRendererProps
|
||||
v.removeEventListener('pause', onPause);
|
||||
v.removeEventListener('ended', onEnded);
|
||||
v.removeEventListener('error', onError);
|
||||
if (blobUrl) URL.revokeObjectURL(blobUrl);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -125,7 +147,7 @@ export const VideoRenderer = ({ src, fileName, fallbackSrc }: VideoRendererProps
|
||||
if (playing) setShowControls(false);
|
||||
}}
|
||||
>
|
||||
<video ref={videoRef} src={src} preload="metadata" className="max-w-full max-h-full" onClick={togglePlay} />
|
||||
<video ref={videoRef} src={src} preload="metadata" playsInline className="max-w-full max-h-full" onClick={togglePlay} />
|
||||
|
||||
{loaded && !playing && (
|
||||
<button onClick={togglePlay} className="absolute inset-0 flex items-center justify-center cursor-pointer">
|
||||
|
||||
@@ -19,6 +19,7 @@ type PanelSlotProps = {
|
||||
registry: AppRegistry;
|
||||
components?: PanelComponents;
|
||||
interactive: boolean;
|
||||
locked: boolean;
|
||||
noHeader: boolean;
|
||||
isLastPanel: boolean;
|
||||
onSetApp: (panelId: string, appType: string | null) => void;
|
||||
@@ -164,6 +165,46 @@ const TrafficLights = ({ panelId, isLastPanel, onRemove, onClearApp }: { panelId
|
||||
);
|
||||
};
|
||||
|
||||
const MaximizeButton = ({ panelId }: { panelId: string }) => {
|
||||
const { maximizedPanelId, setMaximizedPanelId } = useWorkspace();
|
||||
const isMaximized = maximizedPanelId === panelId;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 shrink-0 ml-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMaximizedPanelId(isMaximized ? null : panelId)}
|
||||
className="group/btn h-3 w-3 rounded-full bg-[#28c840] hover:brightness-90 transition-all cursor-pointer flex items-center justify-center"
|
||||
title={isMaximized ? 'Restore' : 'Maximize'}
|
||||
>
|
||||
{isMaximized ? (
|
||||
<Minus className="h-2 w-2 text-[#006500] opacity-0 group-hover/btn:opacity-100 transition-opacity" strokeWidth={3} />
|
||||
) : (
|
||||
<svg viewBox="0 0 10 10" className="h-1.5 w-1.5 text-[#006500] opacity-0 group-hover/btn:opacity-100 transition-opacity">
|
||||
<path d="M0 3.5L5 0L10 3.5V10H0Z" fill="currentColor" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MaximizeContextMenu = ({ panelId, children }: { panelId: string; children: React.ReactNode }) => {
|
||||
const { maximizedPanelId, setMaximizedPanelId } = useWorkspace();
|
||||
const isMaximized = maximizedPanelId === panelId;
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem onClick={() => setMaximizedPanelId(isMaximized ? null : panelId)}>
|
||||
{isMaximized ? 'Restore' : 'Maximize'}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: drag-to-reposition needs work (visual feedback, edge cases)
|
||||
// const DragHandle = ({ panelId }: { panelId: string }) => {
|
||||
// const { setDragSourceId, dragSourceId } = useWorkspace();
|
||||
@@ -182,7 +223,7 @@ const TrafficLights = ({ panelId, isLastPanel, onRemove, onClearApp }: { panelId
|
||||
// return <div className="absolute inset-0 z-20 rounded-lg bg-duck-dark/10 pointer-events-none" />;
|
||||
// };
|
||||
|
||||
export const PanelSlot = ({ panel, registry, components, interactive, noHeader, isLastPanel, onSetApp, onSplit, onRemove }: PanelSlotProps) => {
|
||||
export const PanelSlot = ({ panel, registry, components, interactive, locked, noHeader, isLastPanel, onSetApp, onSplit, onRemove }: PanelSlotProps) => {
|
||||
const { maximizedPanelId, transitioningPanelId, isMobile, onMobileBack } = useWorkspace();
|
||||
const isMaximized = maximizedPanelId === panel.id;
|
||||
|
||||
@@ -199,14 +240,18 @@ export const PanelSlot = ({ panel, registry, components, interactive, noHeader,
|
||||
const onClose = panelEntry?.onClose;
|
||||
|
||||
const contextMenu = interactive
|
||||
? (content: React.ReactNode) => (
|
||||
<PanelContextMenu panelId={panel.id} hasApp={!!AppComponent} isLastPanel={isLastPanel} onSplit={onSplit} onRemove={onRemove} onClearApp={() => onSetApp(panel.id, null)}>
|
||||
{content}
|
||||
</PanelContextMenu>
|
||||
)
|
||||
? locked
|
||||
? (content: React.ReactNode) => (
|
||||
<MaximizeContextMenu panelId={panel.id}>{content}</MaximizeContextMenu>
|
||||
)
|
||||
: (content: React.ReactNode) => (
|
||||
<PanelContextMenu panelId={panel.id} hasApp={!!AppComponent} isLastPanel={isLastPanel} onSplit={onSplit} onRemove={onRemove} onClearApp={() => onSetApp(panel.id, null)}>
|
||||
{content}
|
||||
</PanelContextMenu>
|
||||
)
|
||||
: (content: React.ReactNode) => <>{content}</>;
|
||||
|
||||
const overlays = interactive ? (
|
||||
const overlays = interactive && !locked ? (
|
||||
<>
|
||||
<SwapOverlay panelId={panel.id} />
|
||||
<SwapSourceIndicator panelId={panel.id} />
|
||||
@@ -214,7 +259,7 @@ export const PanelSlot = ({ panel, registry, components, interactive, noHeader,
|
||||
) : null;
|
||||
|
||||
if (!AppComponent) {
|
||||
if (!interactive) {
|
||||
if (!interactive || locked) {
|
||||
return (
|
||||
<div className="h-full w-full p-1">
|
||||
<div className="h-full w-full rounded-lg border-3 border-duck-teal/50" />
|
||||
@@ -254,7 +299,11 @@ export const PanelSlot = ({ panel, registry, components, interactive, noHeader,
|
||||
const ResolvedHeader = HeaderComponent ?? DefaultHeader;
|
||||
|
||||
const trafficLights = interactive && !isMobile ? (
|
||||
<TrafficLights panelId={panel.id} isLastPanel={isLastPanel} onRemove={onRemove} onClearApp={() => onSetApp(panel.id, null)} />
|
||||
locked ? (
|
||||
<MaximizeButton panelId={panel.id} />
|
||||
) : (
|
||||
<TrafficLights panelId={panel.id} isLastPanel={isLastPanel} onRemove={onRemove} onClearApp={() => onSetApp(panel.id, null)} />
|
||||
)
|
||||
) : null;
|
||||
|
||||
const mobileBackButton = isMobile && onMobileBack ? (
|
||||
@@ -285,9 +334,13 @@ export const PanelSlot = ({ panel, registry, components, interactive, noHeader,
|
||||
);
|
||||
|
||||
const headerBar = interactive ? (
|
||||
<PanelContextMenu panelId={panel.id} hasApp={!!AppComponent} isLastPanel={isLastPanel} onSplit={onSplit} onRemove={onRemove} onClearApp={() => onSetApp(panel.id, null)}>
|
||||
{headerContent}
|
||||
</PanelContextMenu>
|
||||
locked ? (
|
||||
<MaximizeContextMenu panelId={panel.id}>{headerContent}</MaximizeContextMenu>
|
||||
) : (
|
||||
<PanelContextMenu panelId={panel.id} hasApp={!!AppComponent} isLastPanel={isLastPanel} onSplit={onSplit} onRemove={onRemove} onClearApp={() => onSetApp(panel.id, null)}>
|
||||
{headerContent}
|
||||
</PanelContextMenu>
|
||||
)
|
||||
) : headerContent;
|
||||
|
||||
const body = (
|
||||
|
||||
@@ -9,6 +9,7 @@ type WorkspaceRendererProps = {
|
||||
registry: AppRegistry;
|
||||
components?: PanelComponents;
|
||||
interactive?: boolean;
|
||||
locked?: boolean;
|
||||
noHeader?: boolean;
|
||||
isMobile?: boolean;
|
||||
mobilePanelId?: string;
|
||||
@@ -23,6 +24,7 @@ export const WorkspaceRenderer = ({
|
||||
registry,
|
||||
components,
|
||||
interactive = false,
|
||||
locked = false,
|
||||
noHeader = false,
|
||||
isMobile = false,
|
||||
mobilePanelId,
|
||||
@@ -40,6 +42,7 @@ export const WorkspaceRenderer = ({
|
||||
registry={registry}
|
||||
components={components}
|
||||
interactive={interactive}
|
||||
locked={locked}
|
||||
noHeader={noHeader}
|
||||
isMobile={isMobile}
|
||||
mobilePanelId={mobilePanelId}
|
||||
@@ -58,6 +61,7 @@ type LayoutNodeRendererProps = {
|
||||
registry: AppRegistry;
|
||||
components?: PanelComponents;
|
||||
interactive: boolean;
|
||||
locked: boolean;
|
||||
noHeader: boolean;
|
||||
isMobile: boolean;
|
||||
mobilePanelId?: string;
|
||||
@@ -91,6 +95,7 @@ const LayoutNodeRenderer = ({
|
||||
registry,
|
||||
components,
|
||||
interactive,
|
||||
locked,
|
||||
noHeader,
|
||||
isMobile,
|
||||
mobilePanelId,
|
||||
@@ -125,6 +130,7 @@ const LayoutNodeRenderer = ({
|
||||
registry={registry}
|
||||
components={components}
|
||||
interactive={interactive}
|
||||
locked={locked}
|
||||
noHeader={noHeader}
|
||||
isLastPanel={totalPanels <= 1}
|
||||
onSetApp={onSetApp}
|
||||
@@ -144,6 +150,7 @@ const LayoutNodeRenderer = ({
|
||||
registry={registry}
|
||||
components={components}
|
||||
interactive={interactive}
|
||||
locked={locked}
|
||||
noHeader={noHeader}
|
||||
isMobile={isMobile}
|
||||
mobilePanelId={mobilePanelId}
|
||||
@@ -171,6 +178,7 @@ const LayoutNodeRenderer = ({
|
||||
registry={registry}
|
||||
components={components}
|
||||
interactive={interactive}
|
||||
locked={locked}
|
||||
noHeader={noHeader}
|
||||
isMobile={isMobile}
|
||||
mobilePanelId={mobilePanelId}
|
||||
@@ -198,6 +206,7 @@ const LayoutNodeRenderer = ({
|
||||
registry={registry}
|
||||
components={components}
|
||||
interactive={interactive}
|
||||
locked={locked}
|
||||
noHeader={noHeader}
|
||||
isMobile={isMobile}
|
||||
mobilePanelId={mobilePanelId}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useAppRegistry } from '../../AppRegistry/useAppRegistry';
|
||||
|
||||
type WorkspaceViewProps = {
|
||||
workspace: WorkspaceState;
|
||||
locked?: boolean;
|
||||
cwd?: string;
|
||||
root?: string;
|
||||
initialFilePath?: string;
|
||||
@@ -24,7 +25,7 @@ type WorkspaceViewProps = {
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export const WorkspaceView = ({ workspace, cwd = '~', root, initialFilePath, defaultFileSort, components, ephemeral, mobilePanelId, onMobilePanelChange }: WorkspaceViewProps) => {
|
||||
export const WorkspaceView = ({ workspace, locked, cwd = '~', root, initialFilePath, defaultFileSort, components, ephemeral, mobilePanelId, onMobilePanelChange }: WorkspaceViewProps) => {
|
||||
const { registry } = useAppRegistry();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@@ -178,11 +179,12 @@ export const WorkspaceView = ({ workspace, cwd = '~', root, initialFilePath, def
|
||||
registry={registry}
|
||||
components={components}
|
||||
interactive
|
||||
locked={locked}
|
||||
isMobile={isMobile}
|
||||
mobilePanelId={mobilePanelId}
|
||||
onSetApp={handleSetApp}
|
||||
onSplit={handleSplit}
|
||||
onRemove={handleRemove}
|
||||
onSetApp={locked ? noop : handleSetApp}
|
||||
onSplit={locked ? noop : handleSplit}
|
||||
onRemove={locked ? noop : handleRemove}
|
||||
onResized={handleResized}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
|
||||
@@ -15,6 +15,7 @@ const mergeWithDefaults = (saved: Partial<UserSettings>): UserSettings => ({
|
||||
tasks: { ...DEFAULT_SETTINGS.tasks, ...saved.tasks },
|
||||
appearance: { ...DEFAULT_SETTINGS.appearance, ...saved.appearance },
|
||||
languages: { ...DEFAULT_SETTINGS.languages, ...saved.languages },
|
||||
onboarding: { ...DEFAULT_SETTINGS.onboarding, ...saved.onboarding },
|
||||
});
|
||||
|
||||
export const useSettings = () => {
|
||||
@@ -72,6 +73,9 @@ export type UserSettings = {
|
||||
default: string;
|
||||
translateTo: string;
|
||||
};
|
||||
onboarding: {
|
||||
complete: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type UserState = Record<string, unknown>;
|
||||
@@ -103,4 +107,7 @@ export const DEFAULT_SETTINGS: UserSettings = {
|
||||
default: 'en',
|
||||
translateTo: 'en',
|
||||
},
|
||||
onboarding: {
|
||||
complete: false,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user