google fonts

This commit is contained in:
Siddharth
2026-02-06 21:58:07 -08:00
parent a89198ccdc
commit 05f4e74de6
7 changed files with 453 additions and 6 deletions
+6
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { LaunchWindow } from "./components/launch/LaunchWindow";
import { SourceSelector } from "./components/launch/SourceSelector";
import VideoEditor from "./components/video-editor/VideoEditor";
import { loadAllCustomFonts } from "./lib/customFonts";
export default function App() {
const [windowType, setWindowType] = useState('');
@@ -15,6 +16,11 @@ export default function App() {
document.documentElement.style.background = 'transparent';
document.getElementById('root')?.style.setProperty('background', 'transparent');
}
// Load custom fonts on app initialization
loadAllCustomFonts().catch((error) => {
console.error('Failed to load custom fonts:', error);
});
}, []);
switch (windowType) {
+2 -2
View File
@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-[9999] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-[10000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
+24
View File
@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
+23
View File
@@ -0,0 +1,23 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface LabelProps
extends React.LabelHTMLAttributes<HTMLLabelElement> {}
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
({ className, ...props }, ref) => {
return (
<label
ref={ref}
className={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...props}
/>
)
}
)
Label.displayName = "Label"
export { Label }
@@ -0,0 +1,181 @@
import { useState } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Plus } from 'lucide-react';
import { toast } from 'sonner';
import {
addCustomFont,
generateFontId,
parseFontFamilyFromImport,
isValidGoogleFontsUrl,
type CustomFont,
} from '@/lib/customFonts';
interface AddCustomFontDialogProps {
onFontAdded?: (font: CustomFont) => void;
}
export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) {
const [open, setOpen] = useState(false);
const [importUrl, setImportUrl] = useState('');
const [fontName, setFontName] = useState('');
const [loading, setLoading] = useState(false);
const handleImportUrlChange = (url: string) => {
setImportUrl(url);
// Auto-extract font name if valid Google Fonts URL
if (isValidGoogleFontsUrl(url)) {
const extracted = parseFontFamilyFromImport(url);
if (extracted && !fontName) {
setFontName(extracted);
}
}
};
const handleAdd = async () => {
// Validate inputs
if (!importUrl.trim()) {
toast.error('Please enter a Google Fonts import URL');
return;
}
if (!isValidGoogleFontsUrl(importUrl)) {
toast.error('Please enter a valid Google Fonts URL');
return;
}
if (!fontName.trim()) {
toast.error('Please enter a font name');
return;
}
setLoading(true);
try {
// Extract font family from URL
const fontFamily = parseFontFamilyFromImport(importUrl);
if (!fontFamily) {
toast.error('Could not extract font family from URL');
setLoading(false);
return;
}
// Create custom font object
const newFont: CustomFont = {
id: generateFontId(fontName),
name: fontName.trim(),
fontFamily: fontFamily,
importUrl: importUrl.trim(),
};
// Add font (this will load and verify it) - throws if it fails
await addCustomFont(newFont);
// Notify parent
if (onFontAdded) {
onFontAdded(newFont);
}
toast.success(`Font "${fontName}" added successfully`);
// Reset and close
setImportUrl('');
setFontName('');
setOpen(false);
} catch (error) {
console.error('Failed to add custom font:', error);
const errorMessage = error instanceof Error ? error.message : 'Failed to load font';
toast.error('Failed to add font', {
description: errorMessage.includes('timeout')
? 'Font took too long to load. Please check the URL and try again.'
: 'The font could not be loaded. Please verify the Google Fonts URL is correct.',
});
} finally {
setLoading(false);
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="w-full bg-white/5 border-white/10 text-slate-200 hover:bg-white/10 h-9 text-xs"
>
<Plus className="w-3 h-3 mr-1" />
Add Google Font
</Button>
</DialogTrigger>
<DialogContent className="bg-[#1a1a1c] border-white/10 text-slate-200">
<DialogHeader>
<DialogTitle>Add Google Font</DialogTitle>
<DialogDescription className="text-slate-400">
Add a custom font from Google Fonts to use in your annotations.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 mt-4">
<div className="space-y-2">
<Label htmlFor="import-url" className="text-slate-200">
Google Fonts Import URL
</Label>
<Input
id="import-url"
placeholder="https://fonts.googleapis.com/css2?family=Roboto&display=swap"
value={importUrl}
onChange={(e) => handleImportUrlChange(e.target.value)}
className="bg-white/5 border-white/10 text-slate-200"
/>
<p className="text-xs text-slate-400">
Get this from Google Fonts: Select a font Click "Get font" Copy the @import URL
</p>
</div>
<div className="space-y-2">
<Label htmlFor="font-name" className="text-slate-200">
Display Name
</Label>
<Input
id="font-name"
placeholder="My Custom Font"
value={fontName}
onChange={(e) => setFontName(e.target.value)}
className="bg-white/5 border-white/10 text-slate-200"
/>
<p className="text-xs text-slate-400">
This is how the font will appear in the font selector
</p>
</div>
<div className="flex justify-end gap-2 mt-6">
<Button
variant="outline"
onClick={() => setOpen(false)}
className="bg-white/5 border-white/10 text-slate-200 hover:bg-white/10"
>
Cancel
</Button>
<Button
onClick={handleAdd}
disabled={loading}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
{loading ? 'Adding...' : 'Add Font'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
@@ -1,4 +1,4 @@
import {useRef } from "react";
import { useRef, useState, useEffect } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Trash2, Type, Image as ImageIcon, Upload, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, ChevronDown, Info } from "lucide-react";
@@ -11,6 +11,8 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Slider } from "@/components/ui/slider";
import { cn } from "@/lib/utils";
import { getArrowComponent } from "./ArrowSvgs";
import { AddCustomFontDialog } from "./AddCustomFontDialog";
import { getCustomFonts, type CustomFont } from "@/lib/customFonts";
interface AnnotationSettingsPanelProps {
annotation: AnnotationRegion;
@@ -43,6 +45,13 @@ export function AnnotationSettingsPanel({
onDelete,
}: AnnotationSettingsPanelProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [customFonts, setCustomFonts] = useState<CustomFont[]>([]);
// Load custom fonts on mount
useEffect(() => {
setCustomFonts(getCustomFonts());
}, []);
const colorPalette = [
'#FF0000', // Red
'#FFD700', // Yellow/Gold
@@ -148,19 +157,35 @@ export function AnnotationSettingsPanel({
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-xs font-medium text-slate-200 mb-2 block">Font Style</label>
<Select
value={annotation.style.fontFamily}
<Select
value={annotation.style.fontFamily}
onValueChange={(value) => onStyleChange({ fontFamily: value })}
>
<SelectTrigger className="w-full bg-white/5 border-white/10 text-slate-200 h-9 text-xs">
<SelectValue placeholder="Select style" />
</SelectTrigger>
<SelectContent className="bg-[#1a1a1c] border-white/10 text-slate-200">
<SelectContent className="bg-[#1a1a1c] border-white/10 text-slate-200 max-h-[300px]">
{FONT_FAMILIES.map((font) => (
<SelectItem key={font.value} value={font.value} style={{ fontFamily: font.value }}>
{font.label}
</SelectItem>
))}
{customFonts.length > 0 && (
<>
<div className="px-2 py-1.5 text-[10px] font-medium text-slate-400 uppercase tracking-wider">
Custom Fonts
</div>
{customFonts.map((font) => (
<SelectItem
key={font.id}
value={font.fontFamily}
style={{ fontFamily: font.fontFamily }}
>
{font.name}
</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
</div>
@@ -184,6 +209,16 @@ export function AnnotationSettingsPanel({
</div>
</div>
{/* Add Custom Font Button */}
<div>
<AddCustomFontDialog
onFontAdded={(font) => {
setCustomFonts(getCustomFonts());
onStyleChange({ fontFamily: font.fontFamily });
}}
/>
</div>
{/* Formatting Toggles */}
<div className="flex items-center justify-between gap-2">
<ToggleGroup type="multiple" className="justify-start bg-white/5 p-1 rounded-lg border border-white/5">
+178
View File
@@ -0,0 +1,178 @@
// Google Fonts loading and management utility
export interface CustomFont {
id: string;
name: string; // Display name
fontFamily: string; // CSS font-family value
importUrl: string; // Google Fonts @import URL
}
const STORAGE_KEY = 'openscreen_custom_fonts';
const loadedFonts = new Set<string>();
// Load custom fonts from localStorage
export function getCustomFonts(): CustomFont[] {
try {
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error('Failed to load custom fonts from storage:', error);
return [];
}
}
// Save custom fonts to localStorage
export function saveCustomFonts(fonts: CustomFont[]): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(fonts));
} catch (error) {
console.error('Failed to save custom fonts to storage:', error);
}
}
// Add a new custom font (throws error if font fails to load)
export async function addCustomFont(font: CustomFont): Promise<CustomFont[]> {
const fonts = getCustomFonts();
const exists = fonts.some(f => f.id === font.id || f.fontFamily === font.fontFamily);
if (exists) {
return fonts;
}
// Try to load the font first - this will throw if it fails
await loadFont(font);
// Only add to storage if font loaded successfully
fonts.push(font);
saveCustomFonts(fonts);
return fonts;
}
// Remove a custom font
export function removeCustomFont(fontId: string): CustomFont[] {
const fonts = getCustomFonts();
const filtered = fonts.filter(f => f.id !== fontId);
saveCustomFonts(filtered);
// Remove the style element
const styleEl = document.getElementById(`custom-font-${fontId}`);
if (styleEl) {
styleEl.remove();
}
loadedFonts.delete(fontId);
return filtered;
}
// Load a Google Font into the document
export function loadFont(font: CustomFont): Promise<void> {
return new Promise((resolve, reject) => {
// Skip if already loaded
if (loadedFonts.has(font.id)) {
resolve();
return;
}
try {
const styleId = `custom-font-${font.id}`;
// Remove existing style if present
const existing = document.getElementById(styleId);
if (existing) {
existing.remove();
}
// Create style element with @import
const style = document.createElement('style');
style.id = styleId;
style.textContent = `@import url('${font.importUrl}');`;
document.head.appendChild(style);
// Wait for font to load
waitForFont(font.fontFamily)
.then(() => {
loadedFonts.add(font.id);
resolve();
})
.catch(reject);
} catch (error) {
console.error('Failed to load font:', font, error);
reject(error);
}
});
}
// Wait for a font to be available and verify it loaded
function waitForFont(fontFamily: string, timeout = 5000): Promise<void> {
return new Promise((resolve, reject) => {
// Use CSS Font Loading API if available
if ('fonts' in document) {
Promise.race([
document.fonts.load(`16px "${fontFamily}"`),
new Promise((_, rej) => setTimeout(() => rej(new Error('Font load timeout')), timeout))
])
.then(() => {
// Verify the font actually loaded by checking if it's available
const isAvailable = document.fonts.check(`16px "${fontFamily}"`);
if (isAvailable) {
resolve();
} else {
reject(new Error(`Font "${fontFamily}" failed to load`));
}
})
.catch((error) => {
reject(error);
});
} else {
// Fallback for browsers without Font Loading API
// Wait a bit and hope for the best
setTimeout(() => resolve(), 1000);
}
});
}
// Load all stored custom fonts on app initialization
export function loadAllCustomFonts(): Promise<void[]> {
const fonts = getCustomFonts();
return Promise.all(fonts.map(font => loadFont(font).catch(err => {
console.error('Failed to load custom font:', font.name, err);
})));
}
// Generate a unique ID for a font
export function generateFontId(name: string): string {
return `${name.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}`;
}
// Parse Google Fonts @import URL to extract font family name
export function parseFontFamilyFromImport(importUrl: string): string | null {
try {
// Extract from URL like: https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap
const url = new URL(importUrl);
const familyParam = url.searchParams.get('family');
if (familyParam) {
// Remove weight/style info: "Roboto:wght@400;700" -> "Roboto"
const fontName = familyParam.split(':')[0];
// Replace + with spaces: "Open+Sans" -> "Open Sans"
return fontName.replace(/\+/g, ' ');
}
return null;
} catch (error) {
console.error('Failed to parse font family from import URL:', error);
return null;
}
}
// Validate if a string looks like a Google Fonts import URL
export function isValidGoogleFontsUrl(url: string): boolean {
try {
const urlObj = new URL(url);
return urlObj.hostname === 'fonts.googleapis.com' && urlObj.searchParams.has('family');
} catch {
return false;
}
}