Initial Commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export const userStore = writable<User | null>(null);
|
||||
|
||||
export async function fetchUser() {
|
||||
try {
|
||||
const res = await fetch('/api/auth/me');
|
||||
if (res.ok) {
|
||||
const user = await res.json();
|
||||
userStore.set(user);
|
||||
} else {
|
||||
userStore.set(null);
|
||||
}
|
||||
} catch {
|
||||
userStore.set(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const themeStore = writable('Catppuccin');
|
||||
export const darkModeStore = writable(true);
|
||||
export const connectionStatus = writable('connecting');
|
||||
export const editorViewStore = writable<any>(null);
|
||||
export const documentZoomStore = writable(100);
|
||||
|
||||
export interface AwarenessUser {
|
||||
clientId: number;
|
||||
name: string;
|
||||
color: string;
|
||||
colorLight: string;
|
||||
isLocal?: boolean;
|
||||
}
|
||||
|
||||
export const connectedUsers = writable<AwarenessUser[]>([]);
|
||||
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedTheme = localStorage.getItem('editor-theme');
|
||||
const savedDark = localStorage.getItem('editor-dark-mode');
|
||||
const savedZoom = localStorage.getItem('editor-document-zoom');
|
||||
|
||||
if (savedTheme) themeStore.set(savedTheme);
|
||||
if (savedDark !== null) darkModeStore.set(savedDark === 'true');
|
||||
if (savedZoom !== null) documentZoomStore.set(parseInt(savedZoom, 10));
|
||||
|
||||
themeStore.subscribe(value => localStorage.setItem('editor-theme', value));
|
||||
darkModeStore.subscribe(value => {
|
||||
localStorage.setItem('editor-dark-mode', value.toString());
|
||||
if (value) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
});
|
||||
documentZoomStore.subscribe(value => localStorage.setItem('editor-document-zoom', value.toString()));
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
|
||||
export interface ThemeColors {
|
||||
background: string;
|
||||
text: string;
|
||||
selection: string;
|
||||
cursor: string;
|
||||
keyword: string;
|
||||
string: string;
|
||||
number: string;
|
||||
comment: string;
|
||||
variable: string;
|
||||
function: string;
|
||||
}
|
||||
|
||||
export interface ThemeConfig {
|
||||
icon: string;
|
||||
dark: ThemeColors;
|
||||
light: ThemeColors;
|
||||
}
|
||||
|
||||
export const themes: Record<string, ThemeConfig> = {
|
||||
Cerberus: {
|
||||
icon: "mdi:dog",
|
||||
dark: {
|
||||
background: "#171717", text: "#f5f5f5", selection: "#262626", cursor: "#f5f5f5",
|
||||
keyword: "#e879f9", string: "#2dd4bf", number: "#fbbf24", comment: "#737373", variable: "#f5f5f5", function: "#818cf8"
|
||||
},
|
||||
light: {
|
||||
background: "#ffffff", text: "#171717", selection: "#f5f5f5", cursor: "#171717",
|
||||
keyword: "#c026d3", string: "#0d9488", number: "#d97706", comment: "#525252", variable: "#171717", function: "#4f46e5"
|
||||
}
|
||||
},
|
||||
Catppuccin: {
|
||||
icon: "mdi:cat",
|
||||
dark: {
|
||||
background: "#1e1e2e", text: "#cdd6f4", selection: "#313244", cursor: "#f5e0dc",
|
||||
keyword: "#cba6f7", string: "#a6e3a1", number: "#fab387", comment: "#6c7086", variable: "#cdd6f4", function: "#89b4fa"
|
||||
},
|
||||
light: {
|
||||
background: "#eff1f5", text: "#4c4f69", selection: "#e6e9ef", cursor: "#dc8a78",
|
||||
keyword: "#8839ef", string: "#40a02b", number: "#fe640b", comment: "#9ca0b0", variable: "#4c4f69", function: "#1e66f5"
|
||||
}
|
||||
},
|
||||
"Arch Linux": {
|
||||
icon: "mdi:penguin",
|
||||
dark: {
|
||||
background: "#0d1117", text: "#c9d1d9", selection: "#21262d", cursor: "#c9d1d9",
|
||||
keyword: "#bc8cff", string: "#3fb950", number: "#ffa657", comment: "#6e7681", variable: "#c9d1d9", function: "#1793d1"
|
||||
},
|
||||
light: {
|
||||
background: "#ffffff", text: "#24292f", selection: "#f6f8fa", cursor: "#24292f",
|
||||
keyword: "#8250df", string: "#1a7f37", number: "#bc4c00", comment: "#6e7781", variable: "#24292f", function: "#1793d1"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function getThemeExtension(themeName: keyof typeof themes, isDark: boolean) {
|
||||
const colors = themes[themeName][isDark ? 'dark' : 'light'];
|
||||
|
||||
const theme = EditorView.theme({
|
||||
"&": {
|
||||
color: colors.text,
|
||||
backgroundColor: colors.background,
|
||||
height: "100%",
|
||||
fontSize: "14px"
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: colors.cursor
|
||||
},
|
||||
".cm-cursor, .cm-dropCursor": { borderLeftColor: colors.cursor },
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: colors.selection },
|
||||
".cm-panels": { backgroundColor: colors.background, color: colors.text },
|
||||
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
|
||||
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
|
||||
".cm-searchMatch": {
|
||||
backgroundColor: "#72a1ff59",
|
||||
outline: "1px solid #457dff"
|
||||
},
|
||||
".cm-searchMatch.cm-searchMatch-selected": {
|
||||
backgroundColor: "#6199ff2f"
|
||||
},
|
||||
".cm-activeLine": { backgroundColor: colors.selection },
|
||||
".cm-selectionMatch": { backgroundColor: "#aafe661a" },
|
||||
"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": {
|
||||
backgroundColor: "#bad0f847"
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: colors.background,
|
||||
color: colors.comment,
|
||||
border: "none"
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: colors.selection
|
||||
},
|
||||
".cm-foldPlaceholder": {
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
color: "#ddd"
|
||||
},
|
||||
".cm-tooltip": {
|
||||
border: "none",
|
||||
backgroundColor: colors.background
|
||||
},
|
||||
".cm-tooltip .cm-tooltip-arrow:before": {
|
||||
borderTopColor: "transparent",
|
||||
borderBottomColor: "transparent"
|
||||
},
|
||||
".cm-tooltip .cm-tooltip-arrow:after": {
|
||||
borderTopColor: colors.background,
|
||||
borderBottomColor: colors.background
|
||||
},
|
||||
".cm-tooltip-autocomplete": {
|
||||
"& > ul > li[aria-selected]": {
|
||||
backgroundColor: colors.selection,
|
||||
color: colors.text
|
||||
}
|
||||
}
|
||||
}, { dark: isDark });
|
||||
|
||||
const highlightStyle = HighlightStyle.define([
|
||||
{ tag: t.keyword, color: colors.keyword },
|
||||
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: colors.variable },
|
||||
{ tag: [t.function(t.variableName), t.labelName], color: colors.function },
|
||||
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: colors.function },
|
||||
{ tag: [t.definition(t.name), t.separator], color: colors.variable },
|
||||
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: colors.number },
|
||||
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: colors.keyword },
|
||||
{ tag: [t.meta, t.comment], color: colors.comment },
|
||||
{ tag: t.strong, fontWeight: "bold" },
|
||||
{ tag: t.emphasis, fontStyle: "italic" },
|
||||
{ tag: t.strikethrough, textDecoration: "line-through" },
|
||||
{ tag: t.link, color: colors.comment, textDecoration: "underline" },
|
||||
{ tag: t.heading, fontWeight: "bold", color: colors.function },
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: colors.number },
|
||||
{ tag: [t.processingInstruction, t.string, t.inserted], color: colors.string },
|
||||
{ tag: t.invalid, color: "#ff0000" },
|
||||
]);
|
||||
|
||||
return [theme, syntaxHighlighting(highlightStyle)];
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export interface Diagnostic {
|
||||
message: string;
|
||||
severity: string;
|
||||
}
|
||||
|
||||
export interface CompileResponse {
|
||||
svgs: string[] | null;
|
||||
errors: Diagnostic[] | null;
|
||||
}
|
||||
|
||||
export async function compileTypst(text: string, document_id?: string): Promise<CompileResponse> {
|
||||
const res = await fetch('/api/compile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text, document_id }),
|
||||
});
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export function exportTypst(text: string, format: 'pdf' | 'png' | 'svg', title: string = 'document', document_id?: string) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = `/api/export/${format}`;
|
||||
form.target = '_blank';
|
||||
|
||||
|
||||
|
||||
|
||||
return fetch(`/api/export/${format}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text, document_id }),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error('Export failed');
|
||||
return res.blob();
|
||||
})
|
||||
.then((blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${title}.${format}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import * as Y from 'yjs';
|
||||
import { WebsocketProvider } from 'y-websocket';
|
||||
import { get } from 'svelte/store';
|
||||
import { userStore } from './auth';
|
||||
import { connectionStatus, connectedUsers } from './store';
|
||||
import type { AwarenessUser } from './store';
|
||||
|
||||
export let doc: Y.Doc | null = null;
|
||||
export let text: Y.Text | null = null;
|
||||
export let provider: WebsocketProvider | null = null;
|
||||
|
||||
const userColors = [
|
||||
'#30bced', '#6eeb83', '#ffbc42', '#ecd444', '#ee6352',
|
||||
'#9ac2c9', '#8acb88', '#1be7ff', '#ff0054', '#9e0059'
|
||||
];
|
||||
|
||||
export function initYjs(docId: string) {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
|
||||
if (provider) {
|
||||
provider.disconnect();
|
||||
provider = null;
|
||||
}
|
||||
|
||||
doc = new Y.Doc();
|
||||
text = doc.getText('typst');
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host;
|
||||
|
||||
connectionStatus.set('connecting');
|
||||
|
||||
provider = new WebsocketProvider(
|
||||
`${protocol}//${host}/yjs`,
|
||||
docId,
|
||||
doc
|
||||
);
|
||||
|
||||
const user = get(userStore);
|
||||
const color = userColors[Math.floor(Math.random() * userColors.length)];
|
||||
|
||||
provider.awareness.setLocalStateField('user', {
|
||||
name: user?.username || 'Anonymous',
|
||||
color: color,
|
||||
colorLight: color + '33'
|
||||
});
|
||||
|
||||
provider.on('status', (event: { status: string }) => {
|
||||
connectionStatus.set(event.status);
|
||||
console.log(`Yjs connection status for ${docId}:`, event.status);
|
||||
});
|
||||
|
||||
provider.awareness.on('change', () => {
|
||||
if (!provider) return;
|
||||
const states = provider.awareness.getStates();
|
||||
const localId = provider.awareness.clientID;
|
||||
|
||||
|
||||
|
||||
const uniqueUsers = new Map<string, AwarenessUser>();
|
||||
|
||||
states.forEach((state, clientId) => {
|
||||
if (state.user) {
|
||||
const isLocal = clientId === localId;
|
||||
const userObj = {
|
||||
clientId,
|
||||
...state.user,
|
||||
isLocal
|
||||
};
|
||||
|
||||
if (isLocal) {
|
||||
|
||||
uniqueUsers.set(state.user.name, userObj);
|
||||
} else if (!uniqueUsers.has(state.user.name) || !uniqueUsers.get(state.user.name)!.isLocal) {
|
||||
|
||||
|
||||
uniqueUsers.set(state.user.name, userObj);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connectedUsers.set(Array.from(uniqueUsers.values()));
|
||||
});
|
||||
}
|
||||
|
||||
export function cleanupYjs() {
|
||||
if (provider) {
|
||||
provider.disconnect();
|
||||
provider = null;
|
||||
}
|
||||
doc = null;
|
||||
text = null;
|
||||
connectionStatus.set('disconnected');
|
||||
connectedUsers.set([]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user