♻️ Refactor code
This commit is contained in:
251
frontend/src/views/editor/Editor.vue
Normal file
251
frontend/src/views/editor/Editor.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<script setup lang="ts">
|
||||
import {onBeforeUnmount, onMounted, ref, watch} from 'vue';
|
||||
import {EditorState, Extension} from '@codemirror/state';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {useEditorStore} from '@/stores/editorStore';
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useDocumentStore} from '@/stores/documentStore';
|
||||
import {useLogStore} from '@/stores/logStore';
|
||||
import {createBasicSetup} from './extensions/basicSetup';
|
||||
import {
|
||||
createStatsUpdateExtension,
|
||||
createWheelZoomHandler,
|
||||
getTabExtensions,
|
||||
updateStats,
|
||||
updateTabConfig,
|
||||
createAutoSavePlugin,
|
||||
createSaveShortcutPlugin,
|
||||
createFontExtensionFromBackend,
|
||||
updateFontConfig,
|
||||
} from './extensions';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { DocumentService } from '../../../bindings/voidraft/internal/services';
|
||||
import Toolbar from '@/components/toolbar/Toolbar.vue';
|
||||
|
||||
const editorStore = useEditorStore();
|
||||
const configStore = useConfigStore();
|
||||
const documentStore = useDocumentStore();
|
||||
const logStore = useLogStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
initialDoc: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const editorElement = ref<HTMLElement | null>(null);
|
||||
const editorCreated = ref(false);
|
||||
let isDestroying = false;
|
||||
|
||||
// 创建编辑器
|
||||
const createEditor = async () => {
|
||||
if (!editorElement.value || editorCreated.value) return;
|
||||
editorCreated.value = true;
|
||||
|
||||
// 加载文档内容
|
||||
await documentStore.initialize();
|
||||
const docContent = documentStore.documentContent || props.initialDoc;
|
||||
|
||||
// 获取基本扩展
|
||||
const basicExtensions = createBasicSetup();
|
||||
|
||||
// 获取Tab相关扩展
|
||||
const tabExtensions = getTabExtensions(
|
||||
configStore.config.tabSize,
|
||||
configStore.config.enableTabIndent,
|
||||
configStore.config.tabType
|
||||
);
|
||||
|
||||
// 创建字体扩展
|
||||
const fontExtension = createFontExtensionFromBackend({
|
||||
fontFamily: configStore.config.fontFamily,
|
||||
fontSize: configStore.config.fontSize,
|
||||
lineHeight: configStore.config.lineHeight,
|
||||
fontWeight: configStore.config.fontWeight
|
||||
});
|
||||
|
||||
// 创建统计信息更新扩展
|
||||
const statsExtension = createStatsUpdateExtension(
|
||||
editorStore.updateDocumentStats
|
||||
);
|
||||
|
||||
// 创建保存快捷键插件
|
||||
const saveShortcutPlugin = createSaveShortcutPlugin(() => {
|
||||
if (editorStore.editorView) {
|
||||
handleManualSave();
|
||||
}
|
||||
});
|
||||
|
||||
// 创建自动保存插件
|
||||
const autoSavePlugin = createAutoSavePlugin({
|
||||
debounceDelay: 300, // 300毫秒的输入防抖
|
||||
onSave: (success) => {
|
||||
if (success) {
|
||||
documentStore.lastSaved = new Date();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 组合所有扩展
|
||||
const extensions: Extension[] = [
|
||||
...basicExtensions,
|
||||
...tabExtensions,
|
||||
fontExtension,
|
||||
statsExtension,
|
||||
saveShortcutPlugin,
|
||||
autoSavePlugin
|
||||
];
|
||||
|
||||
// 创建编辑器状态
|
||||
const state = EditorState.create({
|
||||
doc: docContent,
|
||||
extensions
|
||||
});
|
||||
|
||||
// 创建编辑器视图
|
||||
const view = new EditorView({
|
||||
state,
|
||||
parent: editorElement.value
|
||||
});
|
||||
|
||||
// 将编辑器实例保存到store
|
||||
editorStore.setEditorView(view);
|
||||
|
||||
// 应用初始字体大小
|
||||
editorStore.applyFontSize();
|
||||
|
||||
// 立即更新统计信息
|
||||
updateStats(view, editorStore.updateDocumentStats);
|
||||
|
||||
};
|
||||
|
||||
// 创建滚轮事件处理器
|
||||
const handleWheel = createWheelZoomHandler(
|
||||
configStore.increaseFontSize,
|
||||
configStore.decreaseFontSize
|
||||
);
|
||||
|
||||
// 手动保存文档
|
||||
const handleManualSave = async () => {
|
||||
if (!editorStore.editorView || isDestroying) return;
|
||||
|
||||
const view = editorStore.editorView as EditorView;
|
||||
const content = view.state.doc.toString();
|
||||
|
||||
// 先更新内容
|
||||
await DocumentService.UpdateActiveDocumentContent(content);
|
||||
// 然后调用强制保存方法(不再传递content参数)
|
||||
const success = await documentStore.forceSaveDocument();
|
||||
if (success) {
|
||||
logStore.info(t('document.manualSaveSuccess'));
|
||||
}
|
||||
};
|
||||
|
||||
// 重新配置编辑器(仅在必要时)
|
||||
const reconfigureTabSettings = () => {
|
||||
if (!editorStore.editorView) return;
|
||||
updateTabConfig(
|
||||
editorStore.editorView as EditorView,
|
||||
configStore.config.tabSize,
|
||||
configStore.config.enableTabIndent,
|
||||
configStore.config.tabType
|
||||
);
|
||||
};
|
||||
|
||||
// 重新配置字体设置
|
||||
const reconfigureFontSettings = () => {
|
||||
if (!editorStore.editorView) return;
|
||||
updateFontConfig(editorStore.editorView as EditorView, {
|
||||
fontFamily: configStore.config.fontFamily,
|
||||
fontSize: configStore.config.fontSize,
|
||||
lineHeight: configStore.config.lineHeight,
|
||||
fontWeight: configStore.config.fontWeight
|
||||
});
|
||||
};
|
||||
|
||||
// 监听Tab设置变化
|
||||
watch(() => configStore.config.tabSize, reconfigureTabSettings);
|
||||
watch(() => configStore.config.enableTabIndent, reconfigureTabSettings);
|
||||
watch(() => configStore.config.tabType, reconfigureTabSettings);
|
||||
|
||||
// 监听字体大小变化
|
||||
watch(() => configStore.config.fontSize, () => {
|
||||
reconfigureFontSettings();
|
||||
editorStore.applyFontSize();
|
||||
});
|
||||
|
||||
// 监听字体族变化
|
||||
watch(() => configStore.config.fontFamily, reconfigureFontSettings);
|
||||
|
||||
// 监听字体粗细变化
|
||||
watch(() => configStore.config.fontWeight, reconfigureFontSettings);
|
||||
|
||||
// 监听行高变化
|
||||
watch(() => configStore.config.lineHeight, reconfigureFontSettings);
|
||||
|
||||
onMounted(() => {
|
||||
// 创建编辑器
|
||||
createEditor();
|
||||
|
||||
// 添加滚轮事件监听
|
||||
if (editorElement.value) {
|
||||
editorElement.value.addEventListener('wheel', handleWheel, {passive: false});
|
||||
}
|
||||
|
||||
// 确保统计信息已更新
|
||||
if (editorStore.editorView) {
|
||||
setTimeout(() => {
|
||||
updateStats(editorStore.editorView as EditorView, editorStore.updateDocumentStats);
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isDestroying = true;
|
||||
|
||||
// 移除滚轮事件监听
|
||||
if (editorElement.value) {
|
||||
editorElement.value.removeEventListener('wheel', handleWheel);
|
||||
}
|
||||
|
||||
// 直接销毁编辑器
|
||||
if (editorStore.editorView) {
|
||||
editorStore.editorView.destroy();
|
||||
editorStore.setEditorView(null);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="editor-container">
|
||||
<div ref="editorElement" class="editor"></div>
|
||||
<Toolbar />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.editor-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.editor {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.cm-editor) {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.cm-scroller) {
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
99
frontend/src/views/editor/extensions/autoSaveExtension.ts
Normal file
99
frontend/src/views/editor/extensions/autoSaveExtension.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
||||
import { DocumentService } from '../../../../bindings/voidraft/internal/services';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
|
||||
// 定义自动保存配置选项
|
||||
export interface AutoSaveOptions {
|
||||
// 保存回调
|
||||
onSave?: (success: boolean) => void;
|
||||
// 内容变更延迟传递(毫秒)- 输入时不会立即发送,有一个小延迟,避免频繁调用后端
|
||||
debounceDelay?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自动保存插件
|
||||
* @param options 配置选项
|
||||
* @returns EditorView.Plugin
|
||||
*/
|
||||
export function createAutoSavePlugin(options: AutoSaveOptions = {}) {
|
||||
const {
|
||||
onSave = () => {},
|
||||
debounceDelay = 1000 // 默认1000ms延迟,原为300ms
|
||||
} = options;
|
||||
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
private isActive: boolean = true;
|
||||
private isSaving: boolean = false;
|
||||
private readonly contentUpdateFn: (view: EditorView) => void;
|
||||
|
||||
constructor(private view: EditorView) {
|
||||
// 创建内容更新函数,简单传递内容给后端
|
||||
this.contentUpdateFn = this.createDebouncedUpdateFn(debounceDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建防抖的内容更新函数
|
||||
*/
|
||||
private createDebouncedUpdateFn(delay: number): (view: EditorView) => void {
|
||||
// 使用VueUse的防抖函数创建一个新函数
|
||||
return useDebounceFn(async (view: EditorView) => {
|
||||
// 如果插件已不活跃或正在保存中,不发送
|
||||
if (!this.isActive || this.isSaving) return;
|
||||
|
||||
this.isSaving = true;
|
||||
const content = view.state.doc.toString();
|
||||
|
||||
try {
|
||||
// 简单将内容传递给后端,让后端处理保存策略
|
||||
await DocumentService.UpdateActiveDocumentContent(content);
|
||||
onSave(true);
|
||||
} catch (err) {
|
||||
console.error('Failed to update document content:', err);
|
||||
onSave(false);
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
// 如果内容没有变化,直接返回
|
||||
if (!update.docChanged) return;
|
||||
|
||||
// 调用防抖函数
|
||||
this.contentUpdateFn(this.view);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// 标记插件不再活跃
|
||||
this.isActive = false;
|
||||
|
||||
// 直接发送最终内容
|
||||
const content = this.view.state.doc.toString();
|
||||
DocumentService.UpdateActiveDocumentContent(content)
|
||||
.then(() => console.log('Successfully sent final content on destroy'))
|
||||
.catch(err => console.error('Failed to send content on destroy:', err));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建处理保存快捷键的插件
|
||||
* @param onSave 保存回调
|
||||
* @returns EditorView.Plugin
|
||||
*/
|
||||
export function createSaveShortcutPlugin(onSave: () => void) {
|
||||
return EditorView.domEventHandlers({
|
||||
keydown: (event) => {
|
||||
// Ctrl+S / Cmd+S
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||
event.preventDefault();
|
||||
onSave();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
75
frontend/src/views/editor/extensions/basicSetup.ts
Normal file
75
frontend/src/views/editor/extensions/basicSetup.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import {Extension} from '@codemirror/state';
|
||||
import {
|
||||
crosshairCursor,
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
EditorView,
|
||||
highlightActiveLine,
|
||||
highlightActiveLineGutter,
|
||||
highlightSpecialChars,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
} from '@codemirror/view';
|
||||
import {
|
||||
bracketMatching,
|
||||
defaultHighlightStyle,
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
indentOnInput,
|
||||
syntaxHighlighting,
|
||||
} from '@codemirror/language';
|
||||
import {defaultKeymap, history, historyKeymap,} from '@codemirror/commands';
|
||||
import {highlightSelectionMatches, searchKeymap} from '@codemirror/search';
|
||||
import {autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete';
|
||||
import {lintKeymap} from '@codemirror/lint';
|
||||
import {customHighlightActiveLine, defaultDark} from '@/views/editor/theme/default-dark';
|
||||
|
||||
// 基本编辑器设置,包含常用扩展
|
||||
export const createBasicSetup = (): Extension[] => {
|
||||
return [
|
||||
// 主题相关
|
||||
defaultDark,
|
||||
|
||||
// 基础UI
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
dropCursor(),
|
||||
EditorView.lineWrapping,
|
||||
|
||||
// 历史记录
|
||||
history(),
|
||||
|
||||
// 代码折叠
|
||||
foldGutter(),
|
||||
|
||||
// 选择与高亮
|
||||
drawSelection(),
|
||||
customHighlightActiveLine,
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
|
||||
// 缩进和编辑辅助
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
|
||||
// 自动完成
|
||||
autocompletion(),
|
||||
|
||||
// 键盘映射
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap
|
||||
]),
|
||||
];
|
||||
};
|
111
frontend/src/views/editor/extensions/fontExtension.ts
Normal file
111
frontend/src/views/editor/extensions/fontExtension.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { Extension, Compartment } from '@codemirror/state';
|
||||
|
||||
// 字体配置接口
|
||||
export interface FontConfig {
|
||||
fontFamily: string;
|
||||
fontSize?: number;
|
||||
lineHeight?: number;
|
||||
fontWeight?: string;
|
||||
}
|
||||
|
||||
// 创建字体配置compartment
|
||||
export const fontCompartment = new Compartment();
|
||||
|
||||
// 默认字体配置
|
||||
export const DEFAULT_FONT_CONFIG: FontConfig = {
|
||||
fontFamily: '"HarmonyOS Sans SC", "HarmonyOS Sans", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif',
|
||||
fontSize: 14,
|
||||
lineHeight: 1.5,
|
||||
fontWeight: 'normal'
|
||||
};
|
||||
|
||||
// 从后端配置创建字体配置
|
||||
export function createFontConfigFromBackend(backendConfig: {
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
lineHeight?: number;
|
||||
fontWeight?: string;
|
||||
}): FontConfig {
|
||||
return {
|
||||
fontFamily: backendConfig.fontFamily || DEFAULT_FONT_CONFIG.fontFamily,
|
||||
fontSize: backendConfig.fontSize || DEFAULT_FONT_CONFIG.fontSize,
|
||||
lineHeight: backendConfig.lineHeight || DEFAULT_FONT_CONFIG.lineHeight,
|
||||
fontWeight: backendConfig.fontWeight || DEFAULT_FONT_CONFIG.fontWeight,
|
||||
};
|
||||
}
|
||||
|
||||
// 创建字体样式扩展
|
||||
export function createFontExtension(config: Partial<FontConfig> = {}): Extension {
|
||||
const fontConfig = { ...DEFAULT_FONT_CONFIG, ...config };
|
||||
|
||||
const styles: Record<string, any> = {
|
||||
'&': {
|
||||
fontFamily: fontConfig.fontFamily,
|
||||
...(fontConfig.fontSize && { fontSize: `${fontConfig.fontSize}px` }),
|
||||
...(fontConfig.lineHeight && { lineHeight: fontConfig.lineHeight.toString() }),
|
||||
...(fontConfig.fontWeight && { fontWeight: fontConfig.fontWeight }),
|
||||
},
|
||||
'.cm-content': {
|
||||
fontFamily: fontConfig.fontFamily,
|
||||
...(fontConfig.fontSize && { fontSize: `${fontConfig.fontSize}px` }),
|
||||
...(fontConfig.lineHeight && { lineHeight: fontConfig.lineHeight.toString() }),
|
||||
...(fontConfig.fontWeight && { fontWeight: fontConfig.fontWeight }),
|
||||
},
|
||||
'.cm-editor': {
|
||||
fontFamily: fontConfig.fontFamily,
|
||||
},
|
||||
'.cm-scroller': {
|
||||
fontFamily: fontConfig.fontFamily,
|
||||
},
|
||||
'.cm-gutters': {
|
||||
fontFamily: fontConfig.fontFamily,
|
||||
...(fontConfig.fontSize && { fontSize: `${fontConfig.fontSize}px` }),
|
||||
},
|
||||
'.cm-lineNumbers': {
|
||||
fontFamily: fontConfig.fontFamily,
|
||||
...(fontConfig.fontSize && { fontSize: `${Math.max(10, fontConfig.fontSize - 1)}px` }),
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
fontFamily: fontConfig.fontFamily,
|
||||
...(fontConfig.fontSize && { fontSize: `${Math.max(12, fontConfig.fontSize - 1)}px` }),
|
||||
},
|
||||
'.cm-completionLabel': {
|
||||
fontFamily: fontConfig.fontFamily,
|
||||
},
|
||||
'.cm-completionDetail': {
|
||||
fontFamily: fontConfig.fontFamily,
|
||||
}
|
||||
};
|
||||
|
||||
return EditorView.theme(styles);
|
||||
}
|
||||
|
||||
// 创建响应式字体大小扩展
|
||||
export function createResponsiveFontExtension(baseFontSize: number = 14): Extension {
|
||||
return fontCompartment.of(createFontExtension({
|
||||
fontSize: baseFontSize,
|
||||
lineHeight: 1.5
|
||||
}));
|
||||
}
|
||||
|
||||
// 从后端配置创建字体扩展
|
||||
export function createFontExtensionFromBackend(backendConfig: {
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
lineHeight?: number;
|
||||
fontWeight?: string;
|
||||
}): Extension {
|
||||
const fontConfig = createFontConfigFromBackend(backendConfig);
|
||||
return fontCompartment.of(createFontExtension(fontConfig));
|
||||
}
|
||||
|
||||
// 动态更新字体配置
|
||||
export function updateFontConfig(view: EditorView, config: Partial<FontConfig>): void {
|
||||
const newFontExtension = createFontExtension(config);
|
||||
|
||||
// 使用compartment重新配置字体扩展
|
||||
view.dispatch({
|
||||
effects: fontCompartment.reconfigure(newFontExtension)
|
||||
});
|
||||
}
|
6
frontend/src/views/editor/extensions/index.ts
Normal file
6
frontend/src/views/editor/extensions/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// 统一导出所有扩展
|
||||
export * from './tabExtension';
|
||||
export * from './wheelZoomExtension';
|
||||
export * from './statsExtension';
|
||||
export * from './autoSaveExtension';
|
||||
export * from './fontExtension';
|
42
frontend/src/views/editor/extensions/statsExtension.ts
Normal file
42
frontend/src/views/editor/extensions/statsExtension.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {Extension} from '@codemirror/state';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {DocumentStats} from '@/types/editor';
|
||||
|
||||
// 更新编辑器文档统计信息
|
||||
export const updateStats = (
|
||||
view: EditorView,
|
||||
updateDocumentStats: (stats: DocumentStats) => void
|
||||
) => {
|
||||
if (!view) return;
|
||||
|
||||
const state = view.state;
|
||||
const doc = state.doc;
|
||||
const text = doc.toString();
|
||||
|
||||
// 计算选中的字符数
|
||||
let selectedChars = 0;
|
||||
const selections = state.selection;
|
||||
if (selections) {
|
||||
for (let i = 0; i < selections.ranges.length; i++) {
|
||||
const range = selections.ranges[i];
|
||||
selectedChars += range.to - range.from;
|
||||
}
|
||||
}
|
||||
|
||||
updateDocumentStats({
|
||||
lines: doc.lines,
|
||||
characters: text.length,
|
||||
selectedCharacters: selectedChars
|
||||
});
|
||||
};
|
||||
|
||||
// 创建统计信息更新监听器扩展
|
||||
export const createStatsUpdateExtension = (
|
||||
updateDocumentStats: (stats: DocumentStats) => void
|
||||
): Extension => {
|
||||
return EditorView.updateListener.of(update => {
|
||||
if (update.docChanged || update.selectionSet) {
|
||||
updateStats(update.view, updateDocumentStats);
|
||||
}
|
||||
});
|
||||
};
|
78
frontend/src/views/editor/extensions/tabExtension.ts
Normal file
78
frontend/src/views/editor/extensions/tabExtension.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {Compartment, Extension} from '@codemirror/state';
|
||||
import {EditorView, keymap} from '@codemirror/view';
|
||||
import {indentSelection} from '@codemirror/commands';
|
||||
import {indentUnit} from '@codemirror/language';
|
||||
import {TabType} from '../../../../bindings/voidraft/internal/models/models';
|
||||
|
||||
// Tab设置相关的compartment
|
||||
export const tabSizeCompartment = new Compartment();
|
||||
export const tabKeyCompartment = new Compartment();
|
||||
|
||||
// 自定义Tab键处理函数
|
||||
export const tabHandler = (view: EditorView, tabSize: number, tabType: TabType): boolean => {
|
||||
// 如果有选中文本,使用indentSelection
|
||||
if (!view.state.selection.main.empty) {
|
||||
return indentSelection(view);
|
||||
}
|
||||
|
||||
// 根据tabType创建缩进字符
|
||||
const indent = tabType === 'spaces' ? ' '.repeat(tabSize) : '\t';
|
||||
|
||||
// 在光标位置插入缩进字符
|
||||
const {state, dispatch} = view;
|
||||
dispatch(state.update(state.replaceSelection(indent), {scrollIntoView: true}));
|
||||
return true;
|
||||
};
|
||||
|
||||
// 获取Tab相关的扩展
|
||||
export const getTabExtensions = (tabSize: number, enableTabIndent: boolean, tabType: TabType): Extension[] => {
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
// 根据tabType设置缩进单位
|
||||
const indentStr = tabType === 'spaces' ? ' '.repeat(tabSize) : '\t';
|
||||
extensions.push(tabSizeCompartment.of(indentUnit.of(indentStr)));
|
||||
|
||||
// 如果启用了Tab缩进,添加自定义Tab键映射
|
||||
if (enableTabIndent) {
|
||||
extensions.push(
|
||||
tabKeyCompartment.of(
|
||||
keymap.of([{
|
||||
key: "Tab",
|
||||
run: (view) => tabHandler(view, tabSize, tabType)
|
||||
}])
|
||||
)
|
||||
);
|
||||
} else {
|
||||
extensions.push(tabKeyCompartment.of([]));
|
||||
}
|
||||
|
||||
return extensions;
|
||||
};
|
||||
|
||||
// 更新Tab配置
|
||||
export const updateTabConfig = (
|
||||
view: EditorView | null,
|
||||
tabSize: number,
|
||||
enableTabIndent: boolean,
|
||||
tabType: TabType
|
||||
) => {
|
||||
if (!view) return;
|
||||
|
||||
// 根据tabType更新indentUnit配置
|
||||
const indentStr = tabType === 'spaces' ? ' '.repeat(tabSize) : '\t';
|
||||
view.dispatch({
|
||||
effects: tabSizeCompartment.reconfigure(indentUnit.of(indentStr))
|
||||
});
|
||||
|
||||
// 更新Tab键映射
|
||||
const tabKeymap = enableTabIndent
|
||||
? keymap.of([{
|
||||
key: "Tab",
|
||||
run: (view) => tabHandler(view, tabSize, tabType)
|
||||
}])
|
||||
: [];
|
||||
|
||||
view.dispatch({
|
||||
effects: tabKeyCompartment.reconfigure(tabKeymap)
|
||||
});
|
||||
};
|
22
frontend/src/views/editor/extensions/wheelZoomExtension.ts
Normal file
22
frontend/src/views/editor/extensions/wheelZoomExtension.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// 处理滚轮缩放字体的事件处理函数
|
||||
export const createWheelZoomHandler = (
|
||||
increaseFontSize: () => void,
|
||||
decreaseFontSize: () => void
|
||||
) => {
|
||||
return (event: WheelEvent) => {
|
||||
// 检查是否按住了Ctrl键
|
||||
if (event.ctrlKey) {
|
||||
// 阻止默认行为(防止页面缩放)
|
||||
event.preventDefault();
|
||||
|
||||
// 根据滚轮方向增大或减小字体
|
||||
if (event.deltaY < 0) {
|
||||
// 向上滚动,增大字体
|
||||
increaseFontSize();
|
||||
} else {
|
||||
// 向下滚动,减小字体
|
||||
decreaseFontSize();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
128
frontend/src/views/editor/theme/aura.ts
Normal file
128
frontend/src/views/editor/theme/aura.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'aura',
|
||||
dark: true,
|
||||
background: '#21202e',
|
||||
foreground: '#edecee',
|
||||
selection: '#A198EC7F',
|
||||
cursor: '#a277ff',
|
||||
dropdownBackground: '#21202e',
|
||||
dropdownBorder: '#3b334b',
|
||||
activeLine: '#4d4b6622',
|
||||
lineNumber: '#a394f033',
|
||||
lineNumberActive: '#cdccce',
|
||||
matchingBracket: '#a394f033',
|
||||
keyword: '#a277ff',
|
||||
storage: '#a277ff',
|
||||
variable: '#edecee',
|
||||
parameter: '#edecee',
|
||||
function: '#ffca85',
|
||||
string: '#61ffca',
|
||||
constant: '#61ffca',
|
||||
type: '#82e2ff',
|
||||
class: '#82e2ff',
|
||||
number: '#61ffca',
|
||||
comment: '#6d6d6d',
|
||||
heading: '#a277ff',
|
||||
invalid: '#ff6767',
|
||||
regexp: '#61ffca',
|
||||
}
|
||||
|
||||
export const auraTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const auraHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const aura: Extension = [
|
||||
auraTheme,
|
||||
syntaxHighlighting(auraHighlightStyle),
|
||||
]
|
143
frontend/src/views/editor/theme/default-dark.ts
Normal file
143
frontend/src/views/editor/theme/default-dark.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'base-dark',
|
||||
dark: true,
|
||||
background: '#252B37',
|
||||
foreground: '#9BB586',
|
||||
selection: '#3381c1',
|
||||
selectionMatch: '#1A58887F',
|
||||
cursor: '#F8F8F2',
|
||||
dropdownBackground: '#282A36',
|
||||
dropdownBorder: '#191A21',
|
||||
activeLine: '#2E333F99',
|
||||
lineNumber: '#676d7c',
|
||||
lineNumberActive: '#F8F8F2',
|
||||
lineNumberBackground: '#212731',
|
||||
matchingBracket: '#44475A',
|
||||
keyword: '#FF79C6',
|
||||
storage: '#FF79C6',
|
||||
variable: '#F8F8F2',
|
||||
parameter: '#F8F8F2',
|
||||
function: '#50FA7B',
|
||||
string: '#F1FA8C',
|
||||
constant: '#BD93F9',
|
||||
type: '#8BE9FD',
|
||||
class: '#8BE9FD',
|
||||
number: '#BD93F9',
|
||||
comment: '#6272A4',
|
||||
heading: '#BD93F9',
|
||||
invalid: '#FF5555',
|
||||
regexp: '#F1FA8C',
|
||||
}
|
||||
|
||||
export const draculaTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
|
||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
|
||||
backgroundColor: `${config.selection} !important`
|
||||
},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
|
||||
'.cm-selectionMatch': {
|
||||
backgroundColor: `${config.selectionMatch} !important`,
|
||||
borderRadius: '2px'
|
||||
},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.lineNumberBackground,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const draculaHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const customHighlightActiveLine = EditorView.theme({
|
||||
'.cm-activeLine': {
|
||||
backgroundColor: config.activeLine,
|
||||
}
|
||||
})
|
||||
|
||||
export const defaultDark: Extension = [
|
||||
draculaTheme,
|
||||
syntaxHighlighting(draculaHighlightStyle),
|
||||
]
|
128
frontend/src/views/editor/theme/dracula.ts
Normal file
128
frontend/src/views/editor/theme/dracula.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'dracula',
|
||||
dark: true,
|
||||
background: '#282A36',
|
||||
foreground: '#F8F8F2',
|
||||
selection: '#44475A',
|
||||
cursor: '#F8F8F2',
|
||||
dropdownBackground: '#282A36',
|
||||
dropdownBorder: '#191A21',
|
||||
activeLine: '#53576c22',
|
||||
lineNumber: '#6272A4',
|
||||
lineNumberActive: '#F8F8F2',
|
||||
matchingBracket: '#44475A',
|
||||
keyword: '#FF79C6',
|
||||
storage: '#FF79C6',
|
||||
variable: '#F8F8F2',
|
||||
parameter: '#F8F8F2',
|
||||
function: '#50FA7B',
|
||||
string: '#F1FA8C',
|
||||
constant: '#BD93F9',
|
||||
type: '#8BE9FD',
|
||||
class: '#8BE9FD',
|
||||
number: '#BD93F9',
|
||||
comment: '#6272A4',
|
||||
heading: '#BD93F9',
|
||||
invalid: '#FF5555',
|
||||
regexp: '#F1FA8C',
|
||||
}
|
||||
|
||||
export const draculaTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const draculaHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const dracula: Extension = [
|
||||
draculaTheme,
|
||||
syntaxHighlighting(draculaHighlightStyle),
|
||||
]
|
128
frontend/src/views/editor/theme/github-dark.ts
Normal file
128
frontend/src/views/editor/theme/github-dark.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'githubDark',
|
||||
dark: true,
|
||||
background: '#24292e',
|
||||
foreground: '#d1d5da',
|
||||
selection: '#3392FF44',
|
||||
cursor: '#c8e1ff',
|
||||
dropdownBackground: '#24292e',
|
||||
dropdownBorder: '#1b1f23',
|
||||
activeLine: '#4d566022',
|
||||
lineNumber: '#444d56',
|
||||
lineNumberActive: '#e1e4e8',
|
||||
matchingBracket: '#17E5E650',
|
||||
keyword: '#f97583',
|
||||
storage: '#f97583',
|
||||
variable: '#ffab70',
|
||||
parameter: '#e1e4e8',
|
||||
function: '#79b8ff',
|
||||
string: '#9ecbff',
|
||||
constant: '#79b8ff',
|
||||
type: '#79b8ff',
|
||||
class: '#b392f0',
|
||||
number: '#79b8ff',
|
||||
comment: '#6a737d',
|
||||
heading: '#79b8ff',
|
||||
invalid: '#f97583',
|
||||
regexp: '#9ecbff',
|
||||
}
|
||||
|
||||
export const githubDarkTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const githubDarkHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const githubDark: Extension = [
|
||||
githubDarkTheme,
|
||||
syntaxHighlighting(githubDarkHighlightStyle),
|
||||
]
|
128
frontend/src/views/editor/theme/github-light.ts
Normal file
128
frontend/src/views/editor/theme/github-light.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'githubLight',
|
||||
dark: false,
|
||||
background: '#fff',
|
||||
foreground: '#444d56',
|
||||
selection: '#0366d625',
|
||||
cursor: '#044289',
|
||||
dropdownBackground: '#fff',
|
||||
dropdownBorder: '#e1e4e8',
|
||||
activeLine: '#c6c6c622',
|
||||
lineNumber: '#1b1f234d',
|
||||
lineNumberActive: '#24292e',
|
||||
matchingBracket: '#34d05840',
|
||||
keyword: '#d73a49',
|
||||
storage: '#d73a49',
|
||||
variable: '#e36209',
|
||||
parameter: '#24292e',
|
||||
function: '#005cc5',
|
||||
string: '#032f62',
|
||||
constant: '#005cc5',
|
||||
type: '#005cc5',
|
||||
class: '#6f42c1',
|
||||
number: '#005cc5',
|
||||
comment: '#6a737d',
|
||||
heading: '#005cc5',
|
||||
invalid: '#cb2431',
|
||||
regexp: '#032f62',
|
||||
}
|
||||
|
||||
export const githubLightTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const githubLightHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const githubLight: Extension = [
|
||||
githubLightTheme,
|
||||
syntaxHighlighting(githubLightHighlightStyle),
|
||||
]
|
128
frontend/src/views/editor/theme/material-dark.ts
Normal file
128
frontend/src/views/editor/theme/material-dark.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'materialDark',
|
||||
dark: true,
|
||||
background: '#263238',
|
||||
foreground: '#EEFFFF',
|
||||
selection: '#80CBC420',
|
||||
cursor: '#FFCC00',
|
||||
dropdownBackground: '#263238',
|
||||
dropdownBorder: '#FFFFFF10',
|
||||
activeLine: '#4c616c22',
|
||||
lineNumber: '#37474F',
|
||||
lineNumberActive: '#607a86',
|
||||
matchingBracket: '#263238',
|
||||
keyword: '#C792EA',
|
||||
storage: '#C792EA',
|
||||
variable: '#EEFFFF',
|
||||
parameter: '#EEFFFF',
|
||||
function: '#82AAFF',
|
||||
string: '#C3E88D',
|
||||
constant: '#F78C6C',
|
||||
type: '#B2CCD6',
|
||||
class: '#FFCB6B',
|
||||
number: '#F78C6C',
|
||||
comment: '#546E7A',
|
||||
heading: '#C3E88D',
|
||||
invalid: '#FF5370',
|
||||
regexp: '#89DDFF',
|
||||
}
|
||||
|
||||
export const materialDarkTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const materialDarkHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const materialDark: Extension = [
|
||||
materialDarkTheme,
|
||||
syntaxHighlighting(materialDarkHighlightStyle),
|
||||
]
|
128
frontend/src/views/editor/theme/material-light.ts
Normal file
128
frontend/src/views/editor/theme/material-light.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'materialLight',
|
||||
dark: false,
|
||||
background: '#FAFAFA',
|
||||
foreground: '#90A4AE',
|
||||
selection: '#80CBC440',
|
||||
cursor: '#272727',
|
||||
dropdownBackground: '#FAFAFA',
|
||||
dropdownBorder: '#00000010',
|
||||
activeLine: '#c2c2c222',
|
||||
lineNumber: '#CFD8DC',
|
||||
lineNumberActive: '#7E939E',
|
||||
matchingBracket: '#FAFAFA',
|
||||
keyword: '#7C4DFF',
|
||||
storage: '#7C4DFF',
|
||||
variable: '#90A4AE',
|
||||
parameter: '#90A4AE',
|
||||
function: '#6182B8',
|
||||
string: '#91B859',
|
||||
constant: '#F76D47',
|
||||
type: '#8796B0',
|
||||
class: '#FFB62C',
|
||||
number: '#F76D47',
|
||||
comment: '#90A4AE',
|
||||
heading: '#91B859',
|
||||
invalid: '#E53935',
|
||||
regexp: '#39ADB5',
|
||||
}
|
||||
|
||||
export const materialLightTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const materialLightHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const materialLight: Extension = [
|
||||
materialLightTheme,
|
||||
syntaxHighlighting(materialLightHighlightStyle),
|
||||
]
|
128
frontend/src/views/editor/theme/solarized-dark.ts
Normal file
128
frontend/src/views/editor/theme/solarized-dark.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'solarizedDark',
|
||||
dark: true,
|
||||
background: '#002B36',
|
||||
foreground: '#93A1A1',
|
||||
selection: '#274642',
|
||||
cursor: '#D30102',
|
||||
dropdownBackground: '#002B36',
|
||||
dropdownBorder: '#2AA19899',
|
||||
activeLine: '#005b7022',
|
||||
lineNumber: '#93A1A1',
|
||||
lineNumberActive: '#949494',
|
||||
matchingBracket: '#073642',
|
||||
keyword: '#859900',
|
||||
storage: '#93A1A1',
|
||||
variable: '#268BD2',
|
||||
parameter: '#268BD2',
|
||||
function: '#268BD2',
|
||||
string: '#2AA198',
|
||||
constant: '#CB4B16',
|
||||
type: '#CB4B16',
|
||||
class: '#CB4B16',
|
||||
number: '#D33682',
|
||||
comment: '#586E75',
|
||||
heading: '#268BD2',
|
||||
invalid: '#DC322F',
|
||||
regexp: '#DC322F',
|
||||
}
|
||||
|
||||
export const solarizedDarkTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const solarizedDarkHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const solarizedDark: Extension = [
|
||||
solarizedDarkTheme,
|
||||
syntaxHighlighting(solarizedDarkHighlightStyle),
|
||||
]
|
128
frontend/src/views/editor/theme/solarized-light.ts
Normal file
128
frontend/src/views/editor/theme/solarized-light.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'solarizedLight',
|
||||
dark: false,
|
||||
background: '#FDF6E3',
|
||||
foreground: '#586E75',
|
||||
selection: '#EEE8D5',
|
||||
cursor: '#657B83',
|
||||
dropdownBackground: '#FDF6E3',
|
||||
dropdownBorder: '#D3AF86',
|
||||
activeLine: '#d5bd5c22',
|
||||
lineNumber: '#586E75',
|
||||
lineNumberActive: '#567983',
|
||||
matchingBracket: '#EEE8D5',
|
||||
keyword: '#859900',
|
||||
storage: '#586E75',
|
||||
variable: '#268BD2',
|
||||
parameter: '#268BD2',
|
||||
function: '#268BD2',
|
||||
string: '#2AA198',
|
||||
constant: '#CB4B16',
|
||||
type: '#CB4B16',
|
||||
class: '#CB4B16',
|
||||
number: '#D33682',
|
||||
comment: '#93A1A1',
|
||||
heading: '#268BD2',
|
||||
invalid: '#DC322F',
|
||||
regexp: '#DC322F',
|
||||
}
|
||||
|
||||
export const solarizedLightTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const solarizedLightHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const solarizedLight: Extension = [
|
||||
solarizedLightTheme,
|
||||
syntaxHighlighting(solarizedLightHighlightStyle),
|
||||
]
|
128
frontend/src/views/editor/theme/tokyo-night-day.ts
Normal file
128
frontend/src/views/editor/theme/tokyo-night-day.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'tokyoNightDay',
|
||||
dark: false,
|
||||
background: '#e1e2e7',
|
||||
foreground: '#6a6f8e',
|
||||
selection: '#8591b840',
|
||||
cursor: '#3760bf',
|
||||
dropdownBackground: '#e1e2e7',
|
||||
dropdownBorder: '#6a6f8e',
|
||||
activeLine: '#a7aaba22',
|
||||
lineNumber: '#b3b6cd',
|
||||
lineNumberActive: '#68709a',
|
||||
matchingBracket: '#e9e9ec',
|
||||
keyword: '#9854f1',
|
||||
storage: '#9854f1',
|
||||
variable: '#3760bf',
|
||||
parameter: '#3760bf',
|
||||
function: '#2e7de9',
|
||||
string: '#587539',
|
||||
constant: '#9854f1',
|
||||
type: '#07879d',
|
||||
class: '#3760bf',
|
||||
number: '#b15c00',
|
||||
comment: '#9da3c2',
|
||||
heading: '#006a83',
|
||||
invalid: '#ff3e64',
|
||||
regexp: '#2e5857',
|
||||
}
|
||||
|
||||
export const tokyoNightDayTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const tokyoNightDayHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const tokyoNightDay: Extension = [
|
||||
tokyoNightDayTheme,
|
||||
syntaxHighlighting(tokyoNightDayHighlightStyle),
|
||||
]
|
128
frontend/src/views/editor/theme/tokyo-night-storm.ts
Normal file
128
frontend/src/views/editor/theme/tokyo-night-storm.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'tokyoNightStorm',
|
||||
dark: true,
|
||||
background: '#24283b',
|
||||
foreground: '#7982a9',
|
||||
selection: '#6f7bb630',
|
||||
cursor: '#c0caf5',
|
||||
dropdownBackground: '#24283b',
|
||||
dropdownBorder: '#7982a9',
|
||||
activeLine: '#4d547722',
|
||||
lineNumber: '#3b4261',
|
||||
lineNumberActive: '#737aa2',
|
||||
matchingBracket: '#1f2335',
|
||||
keyword: '#bb9af7',
|
||||
storage: '#bb9af7',
|
||||
variable: '#c0caf5',
|
||||
parameter: '#c0caf5',
|
||||
function: '#7aa2f7',
|
||||
string: '#9ece6a',
|
||||
constant: '#bb9af7',
|
||||
type: '#2ac3de',
|
||||
class: '#c0caf5',
|
||||
number: '#ff9e64',
|
||||
comment: '#565f89',
|
||||
heading: '#89ddff',
|
||||
invalid: '#ff5370',
|
||||
regexp: '#b4f9f8',
|
||||
}
|
||||
|
||||
export const tokyoNightStormTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const tokyoNightStormHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const tokyoNightStorm: Extension = [
|
||||
tokyoNightStormTheme,
|
||||
syntaxHighlighting(tokyoNightStormHighlightStyle),
|
||||
]
|
128
frontend/src/views/editor/theme/tokyo-night.ts
Normal file
128
frontend/src/views/editor/theme/tokyo-night.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'tokyoNight',
|
||||
dark: true,
|
||||
background: '#1a1b26',
|
||||
foreground: '#787c99',
|
||||
selection: '#515c7e40',
|
||||
cursor: '#c0caf5',
|
||||
dropdownBackground: '#1a1b26',
|
||||
dropdownBorder: '#787c99',
|
||||
activeLine: '#43455c22',
|
||||
lineNumber: '#363b54',
|
||||
lineNumberActive: '#737aa2',
|
||||
matchingBracket: '#16161e',
|
||||
keyword: '#bb9af7',
|
||||
storage: '#bb9af7',
|
||||
variable: '#c0caf5',
|
||||
parameter: '#c0caf5',
|
||||
function: '#7aa2f7',
|
||||
string: '#9ece6a',
|
||||
constant: '#bb9af7',
|
||||
type: '#0db9d7',
|
||||
class: '#c0caf5',
|
||||
number: '#ff9e64',
|
||||
comment: '#444b6a',
|
||||
heading: '#89ddff',
|
||||
invalid: '#ff5370',
|
||||
regexp: '#b4f9f8',
|
||||
}
|
||||
|
||||
export const tokyoNightTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const tokyoNightHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const tokyoNight: Extension = [
|
||||
tokyoNightTheme,
|
||||
syntaxHighlighting(tokyoNightHighlightStyle),
|
||||
]
|
242
frontend/src/views/settings/Settings.vue
Normal file
242
frontend/src/views/settings/Settings.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<script setup lang="ts">
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import MemoryMonitor from '@/components/monitor/MemoryMonitor.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 导航配置
|
||||
const navItems = [
|
||||
{ id: 'general', icon: '⚙️', route: '/settings/general' },
|
||||
{ id: 'editing', icon: '✏️', route: '/settings/editing' },
|
||||
{ id: 'appearance', icon: '🎨', route: '/settings/appearance' },
|
||||
{ id: 'keyBindings', icon: '⌨️', route: '/settings/key-bindings' },
|
||||
{ id: 'updates', icon: '🔄', route: '/settings/updates' }
|
||||
];
|
||||
|
||||
const activeNavItem = ref(route.path.split('/').pop() || 'general');
|
||||
|
||||
// 处理导航点击
|
||||
const handleNavClick = (item: typeof navItems[0]) => {
|
||||
activeNavItem.value = item.id;
|
||||
router.push(item.route);
|
||||
};
|
||||
|
||||
// 返回编辑器
|
||||
const goBackToEditor = () => {
|
||||
router.push('/');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<div class="settings-sidebar">
|
||||
<div class="settings-header">
|
||||
<div class="header-content">
|
||||
<button class="back-button" @click="goBackToEditor" :title="t('settings.backToEditor')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<h1>{{ t('settings.title') }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-nav">
|
||||
<div
|
||||
v-for="item in navItems"
|
||||
:key="item.id"
|
||||
class="nav-item"
|
||||
:class="{ active: activeNavItem === item.id }"
|
||||
@click="handleNavClick(item)"
|
||||
>
|
||||
<span class="nav-icon">{{ item.icon }}</span>
|
||||
<span class="nav-text">{{ t(`settings.${item.id}`) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-footer">
|
||||
<div class="memory-info-section">
|
||||
<div class="section-title">{{ t('settings.systemInfo') }}</div>
|
||||
<MemoryMonitor />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-content">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
|
||||
.settings-sidebar {
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
background-color: #333333;
|
||||
border-right: 1px solid #444444;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.settings-header {
|
||||
padding: 20px 16px;
|
||||
border-bottom: 1px solid #444444;
|
||||
background-color: #2d2d2d;
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.back-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #a0a0a0;
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #e0e0e0;
|
||||
background-color: #3a3a3a;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-nav {
|
||||
flex: 1;
|
||||
padding: 10px 0;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #333333;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #555555;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: #3a3a3a;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #3c3c3c;
|
||||
border-left-color: #4a9eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
margin-right: 10px;
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-footer {
|
||||
padding: 12px 16px 16px 16px;
|
||||
border-top: 1px solid #444444;
|
||||
background-color: #2d2d2d;
|
||||
|
||||
.memory-info-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.section-title {
|
||||
font-size: 10px;
|
||||
color: #777777;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
background-color: #2a2a2a;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #555555;
|
||||
border-radius: 5px;
|
||||
border: 2px solid #2a2a2a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义变量
|
||||
:root {
|
||||
--border-color: #444444;
|
||||
--hover-color: #3a3a3a;
|
||||
--active-bg: #3c3c3c;
|
||||
--accent-color: #4a9eff;
|
||||
--bg-primary: #2a2a2a;
|
||||
--bg-secondary: #333333;
|
||||
--text-primary: #e0e0e0;
|
||||
--text-secondary: #a0a0a0;
|
||||
--text-muted: #777777;
|
||||
}
|
||||
</style>
|
60
frontend/src/views/settings/components/SettingItem.vue
Normal file
60
frontend/src/views/settings/components/SettingItem.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string;
|
||||
description?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-title">{{ title }}</div>
|
||||
<div v-if="description" class="setting-description">{{ description }}</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.setting-item {
|
||||
display: flex;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #444444;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
flex: 1;
|
||||
padding-right: 20px;
|
||||
|
||||
.setting-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 12px;
|
||||
color: #a0a0a0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-control {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
38
frontend/src/views/settings/components/SettingSection.vue
Normal file
38
frontend/src/views/settings/components/SettingSection.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setting-section">
|
||||
<h2 class="section-title">{{ title }}</h2>
|
||||
<div class="section-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.setting-section {
|
||||
margin-bottom: 30px;
|
||||
background-color: #333333;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
padding: 12px 16px;
|
||||
background-color: #3d3d3d;
|
||||
color: #ffffff;
|
||||
border-bottom: 1px solid #444444;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
53
frontend/src/views/settings/components/ToggleSwitch.vue
Normal file
53
frontend/src/views/settings/components/ToggleSwitch.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>();
|
||||
|
||||
const toggle = () => {
|
||||
emit('update:modelValue', !props.modelValue);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toggle-switch" :class="{ active: modelValue }" @click="toggle">
|
||||
<div class="toggle-handle"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.toggle-switch {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
background-color: #555555;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&.active {
|
||||
background-color: #4a9eff;
|
||||
|
||||
.toggle-handle {
|
||||
transform: translateX(20px);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-handle {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
</style>
|
235
frontend/src/views/settings/pages/AppearancePage.vue
Normal file
235
frontend/src/views/settings/pages/AppearancePage.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<script setup lang="ts">
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import { LanguageType } from '../../../../bindings/voidraft/internal/models/models';
|
||||
|
||||
const { t } = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// 语言选项
|
||||
const languageOptions = [
|
||||
{ value: LanguageType.LangZhCN, label: t('languages.zh-CN') },
|
||||
{ value: LanguageType.LangEnUS, label: t('languages.en-US') },
|
||||
];
|
||||
|
||||
// 更新语言设置
|
||||
const updateLanguage = async (event: Event) => {
|
||||
const select = event.target as HTMLSelectElement;
|
||||
const selectedLanguage = select.value as LanguageType;
|
||||
|
||||
try {
|
||||
// 使用 configStore 的语言设置方法
|
||||
await configStore.setLanguage(selectedLanguage);
|
||||
} catch (error) {
|
||||
console.error('Failed to update language:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 主题选择(未实际实现,仅界面展示)
|
||||
const themeOptions = [
|
||||
{ id: 'dark', name: '深色', color: '#2a2a2a' },
|
||||
{ id: 'darker', name: '暗黑', color: '#1a1a1a' },
|
||||
{ id: 'light', name: '浅色', color: '#f5f5f5' },
|
||||
{ id: 'blue', name: '蓝调', color: '#1e3a5f' },
|
||||
];
|
||||
|
||||
const selectedTheme = ref('dark');
|
||||
|
||||
const selectTheme = (themeId: string) => {
|
||||
selectedTheme.value = themeId;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<SettingSection :title="t('settings.language')">
|
||||
<SettingItem :title="t('settings.language')" :description="t('settings.restartRequired')">
|
||||
<select class="select-input" :value="configStore.config.language" @change="updateLanguage">
|
||||
<option v-for="option in languageOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</SettingItem>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection :title="t('settings.appearance')">
|
||||
<div class="theme-selector">
|
||||
<div class="selector-label">主题</div>
|
||||
<div class="theme-options">
|
||||
<div
|
||||
v-for="theme in themeOptions"
|
||||
:key="theme.id"
|
||||
class="theme-option"
|
||||
:class="{ active: selectedTheme === theme.id }"
|
||||
@click="selectTheme(theme.id)"
|
||||
>
|
||||
<div class="color-preview" :style="{ backgroundColor: theme.color }"></div>
|
||||
<div class="theme-name">{{ theme.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-preview">
|
||||
<div class="preview-header">
|
||||
<div class="preview-title">预览</div>
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<div class="preview-line"><span class="line-number">1</span><span class="keyword">function</span> <span class="function">example</span>() {</div>
|
||||
<div class="preview-line"><span class="line-number">2</span> <span class="keyword">const</span> greeting = <span class="string">"Hello, World!"</span>;</div>
|
||||
<div class="preview-line"><span class="line-number">3</span> <span class="function">console.log</span>(greeting);</div>
|
||||
<div class="preview-line"><span class="line-number">4</span>}</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.select-input {
|
||||
min-width: 150px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
background-color: #3a3a3a;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23e0e0e0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 16px;
|
||||
padding-right: 30px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
option {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-selector {
|
||||
padding: 15px 16px;
|
||||
|
||||
.selector-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 15px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.theme-options {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.theme-option {
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.color-preview {
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.theme-name {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
color: #c0c0c0;
|
||||
}
|
||||
|
||||
&:hover .color-preview {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
&.active .color-preview {
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
&.active .theme-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor-preview {
|
||||
margin: 20px 16px;
|
||||
background-color: #252525;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.preview-header {
|
||||
padding: 10px 16px;
|
||||
background-color: #353535;
|
||||
border-bottom: 1px solid #444444;
|
||||
|
||||
.preview-title {
|
||||
font-size: 13px;
|
||||
color: #b0b0b0;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
padding: 12px 0;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
|
||||
.preview-line {
|
||||
padding: 3px 16px;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.line-number {
|
||||
color: #707070;
|
||||
display: inline-block;
|
||||
width: 25px;
|
||||
margin-right: 15px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.keyword {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.function {
|
||||
color: #dcdcaa;
|
||||
}
|
||||
|
||||
.string {
|
||||
color: #ce9178;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.coming-soon-placeholder {
|
||||
padding: 20px;
|
||||
background-color: #333333;
|
||||
border-radius: 6px;
|
||||
color: #a0a0a0;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
451
frontend/src/views/settings/pages/EditingPage.vue
Normal file
451
frontend/src/views/settings/pages/EditingPage.vue
Normal file
@@ -0,0 +1,451 @@
|
||||
<script setup lang="ts">
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { FONT_OPTIONS } from '@/stores/configStore';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||
import { TabType } from '../../../../bindings/voidraft/internal/models/models';
|
||||
|
||||
const { t } = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// 确保配置已加载
|
||||
onMounted(async () => {
|
||||
if (!configStore.configLoaded) {
|
||||
await configStore.loadConfig();
|
||||
}
|
||||
});
|
||||
|
||||
// 字体选择选项
|
||||
const fontFamilyOptions = FONT_OPTIONS;
|
||||
const currentFontFamily = computed(() => configStore.config.fontFamily);
|
||||
|
||||
// 字体选择
|
||||
const handleFontFamilyChange = async (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
const fontFamily = target.value;
|
||||
if (fontFamily) {
|
||||
try {
|
||||
await configStore.setFontFamily(fontFamily);
|
||||
} catch (error) {
|
||||
console.error('Failed to set font family:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 字体粗细选项
|
||||
const fontWeightOptions = [
|
||||
{ value: '100', label: '极细 (100)' },
|
||||
{ value: '200', label: '超细 (200)' },
|
||||
{ value: '300', label: '细 (300)' },
|
||||
{ value: 'normal', label: '正常 (400)' },
|
||||
{ value: '500', label: '中等 (500)' },
|
||||
{ value: '600', label: '半粗 (600)' },
|
||||
{ value: 'bold', label: '粗体 (700)' },
|
||||
{ value: '800', label: '超粗 (800)' },
|
||||
{ value: '900', label: '极粗 (900)' }
|
||||
];
|
||||
|
||||
// 字体粗细选择
|
||||
const handleFontWeightChange = async (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
try {
|
||||
await configStore.setFontWeight(target.value);
|
||||
} catch (error) {
|
||||
console.error('Failed to set font weight:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 行高控制
|
||||
const increaseLineHeight = async () => {
|
||||
const newLineHeight = Math.min(3.0, configStore.config.lineHeight + 0.1);
|
||||
try {
|
||||
await configStore.setLineHeight(Math.round(newLineHeight * 10) / 10);
|
||||
} catch (error) {
|
||||
console.error('Failed to increase line height:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const decreaseLineHeight = async () => {
|
||||
const newLineHeight = Math.max(1.0, configStore.config.lineHeight - 0.1);
|
||||
try {
|
||||
await configStore.setLineHeight(Math.round(newLineHeight * 10) / 10);
|
||||
} catch (error) {
|
||||
console.error('Failed to decrease line height:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 字体大小控制
|
||||
const increaseFontSize = async () => {
|
||||
try {
|
||||
await configStore.increaseFontSize();
|
||||
} catch (error) {
|
||||
console.error('Failed to increase font size:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const decreaseFontSize = async () => {
|
||||
try {
|
||||
await configStore.decreaseFontSize();
|
||||
} catch (error) {
|
||||
console.error('Failed to decrease font size:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Tab类型切换
|
||||
const tabTypeText = computed(() => {
|
||||
return configStore.config.tabType === TabType.TabTypeSpaces
|
||||
? t('settings.spaces')
|
||||
: t('settings.tabs');
|
||||
});
|
||||
|
||||
// Tab大小增减
|
||||
const increaseTabSize = async () => {
|
||||
try {
|
||||
await configStore.increaseTabSize();
|
||||
} catch (error) {
|
||||
console.error('Failed to increase tab size:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const decreaseTabSize = async () => {
|
||||
try {
|
||||
await configStore.decreaseTabSize();
|
||||
} catch (error) {
|
||||
console.error('Failed to decrease tab size:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Tab相关操作
|
||||
const handleToggleTabIndent = async () => {
|
||||
try {
|
||||
await configStore.toggleTabIndent();
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle tab indent:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleTabType = async () => {
|
||||
try {
|
||||
await configStore.toggleTabType();
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle tab type:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<SettingSection :title="t('settings.fontSettings')">
|
||||
<SettingItem
|
||||
:title="t('settings.fontFamily')"
|
||||
:description="t('settings.fontFamilyDescription')"
|
||||
>
|
||||
<select
|
||||
class="font-family-select"
|
||||
:value="currentFontFamily"
|
||||
@change="handleFontFamilyChange"
|
||||
>
|
||||
<option
|
||||
v-for="option in fontFamilyOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
:title="t('settings.fontSize')"
|
||||
:description="t('settings.fontSizeDescription')"
|
||||
>
|
||||
<div class="number-control">
|
||||
<button @click="decreaseFontSize" class="control-button">-</button>
|
||||
<span>{{ configStore.config.fontSize }}px</span>
|
||||
<button @click="increaseFontSize" class="control-button">+</button>
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
:title="t('settings.fontWeight')"
|
||||
:description="t('settings.fontWeightDescription')"
|
||||
>
|
||||
<select
|
||||
class="font-weight-select"
|
||||
:value="configStore.config.fontWeight"
|
||||
@change="handleFontWeightChange"
|
||||
>
|
||||
<option
|
||||
v-for="option in fontWeightOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
:title="t('settings.lineHeight')"
|
||||
:description="t('settings.lineHeightDescription')"
|
||||
>
|
||||
<div class="number-control">
|
||||
<button @click="decreaseLineHeight" class="control-button">-</button>
|
||||
<span>{{ configStore.config.lineHeight.toFixed(1) }}</span>
|
||||
<button @click="increaseLineHeight" class="control-button">+</button>
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
<div class="font-preview" :style="{
|
||||
fontSize: `${configStore.config.fontSize}px`,
|
||||
fontFamily: configStore.config.fontFamily,
|
||||
fontWeight: configStore.config.fontWeight,
|
||||
lineHeight: configStore.config.lineHeight
|
||||
}">
|
||||
<div class="preview-label">字体预览</div>
|
||||
<div class="preview-text">
|
||||
<span>function example() {</span>
|
||||
<span class="indent">console.log("Hello, 世界!");</span>
|
||||
<span class="indent">const message = "鸿蒙字体测试";</span>
|
||||
<span>}</span>
|
||||
</div>
|
||||
</div>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection :title="t('settings.tabSettings')">
|
||||
<SettingItem :title="t('settings.tabSize')">
|
||||
<div class="number-control">
|
||||
<button @click="decreaseTabSize" class="control-button" :disabled="configStore.config.tabSize <= configStore.tabSize.min">-</button>
|
||||
<span>{{ configStore.config.tabSize }}</span>
|
||||
<button @click="increaseTabSize" class="control-button" :disabled="configStore.config.tabSize >= configStore.tabSize.max">+</button>
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem :title="t('settings.tabType')">
|
||||
<button class="tab-type-toggle" @click="handleToggleTabType">
|
||||
{{ tabTypeText }}
|
||||
</button>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem :title="t('settings.enableTabIndent')">
|
||||
<ToggleSwitch
|
||||
v-model="configStore.config.enableTabIndent"
|
||||
@update:modelValue="handleToggleTabIndent"
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection :title="t('settings.saveOptions')">
|
||||
<SettingItem :title="t('settings.autoSaveDelay')" :description="'单位:毫秒'">
|
||||
<input
|
||||
type="number"
|
||||
class="number-input"
|
||||
disabled
|
||||
:value="5000"
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem :title="t('settings.changeThreshold')" :description="'变更字符超过此阈值时触发保存'">
|
||||
<input
|
||||
type="number"
|
||||
class="number-input"
|
||||
disabled
|
||||
:value="500"
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem :title="t('settings.minSaveInterval')" :description="'单位:毫秒'">
|
||||
<input
|
||||
type="number"
|
||||
class="number-input"
|
||||
disabled
|
||||
:value="1000"
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.number-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.control-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #444444;
|
||||
border-color: #666666;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #e0e0e0;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.font-size-preview {
|
||||
margin: 15px 0 5px 20px;
|
||||
padding: 15px;
|
||||
background-color: #252525;
|
||||
border: 1px solid #444444;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
|
||||
.preview-label {
|
||||
font-size: 12px;
|
||||
color: #888888;
|
||||
margin-bottom: 8px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
line-height: 1.4;
|
||||
color: #d0d0d0;
|
||||
}
|
||||
|
||||
.indent {
|
||||
padding-left: 20px;
|
||||
color: #4a9eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.font-preview {
|
||||
margin: 15px 0 5px 20px;
|
||||
padding: 15px;
|
||||
background-color: #252525;
|
||||
border: 1px solid #444444;
|
||||
border-radius: 4px;
|
||||
|
||||
.preview-label {
|
||||
font-size: 12px;
|
||||
color: #888888;
|
||||
margin-bottom: 8px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
color: #d0d0d0;
|
||||
}
|
||||
|
||||
.indent {
|
||||
padding-left: 20px;
|
||||
color: #4a9eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.font-family-select,
|
||||
.font-weight-select {
|
||||
min-width: 180px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
background-color: #3a3a3a;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #666666;
|
||||
}
|
||||
|
||||
option {
|
||||
background-color: #3a3a3a;
|
||||
color: #e0e0e0;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-type-toggle {
|
||||
min-width: 100px;
|
||||
padding: 8px 15px;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #444444;
|
||||
border-color: #666666;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
|
||||
.number-input {
|
||||
width: 100px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
background-color: #3a3a3a;
|
||||
color: #a0a0a0;
|
||||
font-size: 13px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
289
frontend/src/views/settings/pages/GeneralPage.vue
Normal file
289
frontend/src/views/settings/pages/GeneralPage.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<script setup lang="ts">
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// 选择的键盘修饰键
|
||||
const selectedModifiers = ref({
|
||||
ctrl: false,
|
||||
shift: false,
|
||||
alt: true,
|
||||
altgr: false,
|
||||
win: false
|
||||
});
|
||||
|
||||
// 选择的键
|
||||
const selectedKey = ref('X');
|
||||
|
||||
// 可选键列表
|
||||
const keyOptions = [
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
||||
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'
|
||||
];
|
||||
|
||||
// 更新选择的键
|
||||
const updateSelectedKey = (event: Event) => {
|
||||
const select = event.target as HTMLSelectElement;
|
||||
selectedKey.value = select.value;
|
||||
};
|
||||
|
||||
// 切换修饰键
|
||||
const toggleModifier = (key: keyof typeof selectedModifiers.value) => {
|
||||
selectedModifiers.value[key] = !selectedModifiers.value[key];
|
||||
};
|
||||
|
||||
// 重置设置
|
||||
const resetSettings = async () => {
|
||||
if (confirm(t('settings.confirmReset'))) {
|
||||
await configStore.resetConfig();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<SettingSection :title="t('settings.globalHotkey')">
|
||||
<SettingItem :title="t('settings.enableGlobalHotkey')">
|
||||
<ToggleSwitch v-model="configStore.config.alwaysOnTop" /> <!-- 此处使用alwaysOnTop作为示例 -->
|
||||
</SettingItem>
|
||||
|
||||
<div class="hotkey-selector">
|
||||
<div class="hotkey-modifiers">
|
||||
<label class="modifier-label" :class="{ active: selectedModifiers.ctrl }">
|
||||
<input type="checkbox" v-model="selectedModifiers.ctrl" class="hidden-checkbox">
|
||||
<span class="modifier-key">Ctrl</span>
|
||||
</label>
|
||||
<label class="modifier-label" :class="{ active: selectedModifiers.shift }">
|
||||
<input type="checkbox" v-model="selectedModifiers.shift" class="hidden-checkbox">
|
||||
<span class="modifier-key">Shift</span>
|
||||
</label>
|
||||
<label class="modifier-label" :class="{ active: selectedModifiers.alt }">
|
||||
<input type="checkbox" v-model="selectedModifiers.alt" class="hidden-checkbox">
|
||||
<span class="modifier-key">Alt</span>
|
||||
</label>
|
||||
<label class="modifier-label" :class="{ active: selectedModifiers.altgr }">
|
||||
<input type="checkbox" v-model="selectedModifiers.altgr" class="hidden-checkbox">
|
||||
<span class="modifier-key">AltGr</span>
|
||||
</label>
|
||||
<label class="modifier-label" :class="{ active: selectedModifiers.win }">
|
||||
<input type="checkbox" v-model="selectedModifiers.win" class="hidden-checkbox">
|
||||
<span class="modifier-key">Win</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<select class="key-select" v-model="selectedKey">
|
||||
<option v-for="key in keyOptions" :key="key" :value="key">{{ key }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection :title="t('settings.window')">
|
||||
<SettingItem :title="t('settings.showInSystemTray')">
|
||||
<ToggleSwitch v-model="configStore.config.alwaysOnTop" /> <!-- 需要后端实现 -->
|
||||
</SettingItem>
|
||||
<SettingItem :title="t('settings.alwaysOnTop')">
|
||||
<ToggleSwitch v-model="configStore.config.alwaysOnTop" @update:modelValue="configStore.toggleAlwaysOnTop" />
|
||||
</SettingItem>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection :title="t('settings.bufferFiles')">
|
||||
<SettingItem :title="t('settings.useCustomLocation')">
|
||||
<ToggleSwitch v-model="configStore.config.alwaysOnTop" /> <!-- 需要后端实现 -->
|
||||
</SettingItem>
|
||||
<div class="directory-selector">
|
||||
<div class="path-display">{{ configStore.config.alwaysOnTop ? 'C:/Custom/Path' : 'Default Location' }}</div>
|
||||
<button class="select-button">{{ t('settings.selectDirectory') }}</button>
|
||||
</div>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection :title="t('settings.dangerZone')">
|
||||
<div class="danger-zone">
|
||||
<div class="reset-section">
|
||||
<div class="reset-info">
|
||||
<h4>{{ t('settings.resetAllSettings') }}</h4>
|
||||
<p>{{ t('settings.resetDescription') }}</p>
|
||||
</div>
|
||||
<button class="reset-button" @click="resetSettings">
|
||||
{{ t('settings.reset') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</SettingSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.hotkey-selector {
|
||||
padding: 15px 0 5px 20px;
|
||||
|
||||
.hotkey-modifiers {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.modifier-label {
|
||||
cursor: pointer;
|
||||
|
||||
.hidden-checkbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modifier-key {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
background-color: #444444;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
color: #b0b0b0;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
}
|
||||
|
||||
&.active .modifier-key {
|
||||
background-color: #2c5a9e;
|
||||
color: #ffffff;
|
||||
border-color: #3a6db1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.key-select {
|
||||
min-width: 80px;
|
||||
padding: 8px 12px;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23e0e0e0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 16px;
|
||||
padding-right: 30px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
option {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.directory-selector {
|
||||
margin-top: 10px;
|
||||
padding-left: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.path-display {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
color: #b0b0b0;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.select-button {
|
||||
padding: 8px 12px;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: #444444;
|
||||
border-color: #666666;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.danger-zone {
|
||||
padding: 20px;
|
||||
background-color: rgba(220, 53, 69, 0.05);
|
||||
border: 1px solid rgba(220, 53, 69, 0.2);
|
||||
border-radius: 6px;
|
||||
margin-top: 10px;
|
||||
|
||||
.reset-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
|
||||
.reset-info {
|
||||
flex: 1;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 6px 0;
|
||||
color: #ff6b6b;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #b0b0b0;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
padding: 8px 16px;
|
||||
background-color: #dc3545;
|
||||
border: 1px solid #c82333;
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: #c82333;
|
||||
border-color: #bd2130;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
219
frontend/src/views/settings/pages/KeyBindingsPage.vue
Normal file
219
frontend/src/views/settings/pages/KeyBindingsPage.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface KeyBinding {
|
||||
id: string;
|
||||
name: string;
|
||||
keys: string[];
|
||||
isEditing: boolean;
|
||||
}
|
||||
|
||||
// 示例快捷键列表(仅用于界面展示)
|
||||
const keyBindings = ref<KeyBinding[]>([
|
||||
{ id: 'save', name: '保存文档', keys: ['Ctrl', 'S'], isEditing: false },
|
||||
{ id: 'new', name: '新建文档', keys: ['Ctrl', 'N'], isEditing: false },
|
||||
{ id: 'open', name: '打开文档', keys: ['Ctrl', 'O'], isEditing: false },
|
||||
{ id: 'find', name: '查找', keys: ['Ctrl', 'F'], isEditing: false },
|
||||
{ id: 'replace', name: '替换', keys: ['Ctrl', 'H'], isEditing: false },
|
||||
]);
|
||||
|
||||
// 切换编辑状态
|
||||
const toggleEdit = (binding: KeyBinding) => {
|
||||
// 先关闭其他所有编辑中的项
|
||||
keyBindings.value.forEach(item => {
|
||||
if (item.id !== binding.id) {
|
||||
item.isEditing = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 切换当前项
|
||||
binding.isEditing = !binding.isEditing;
|
||||
};
|
||||
|
||||
// 编辑模式下按键事件处理
|
||||
const handleKeyDown = (event: KeyboardEvent, binding: KeyBinding) => {
|
||||
if (!binding.isEditing) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const newKeys: string[] = [];
|
||||
if (event.ctrlKey) newKeys.push('Ctrl');
|
||||
if (event.shiftKey) newKeys.push('Shift');
|
||||
if (event.altKey) newKeys.push('Alt');
|
||||
|
||||
// 获取按键
|
||||
let keyName = event.key;
|
||||
if (keyName === ' ') keyName = 'Space';
|
||||
if (keyName.length === 1) keyName = keyName.toUpperCase();
|
||||
|
||||
// 如果有修饰键,就添加主键
|
||||
if (event.ctrlKey || event.shiftKey || event.altKey) {
|
||||
if (!['Control', 'Shift', 'Alt'].includes(keyName)) {
|
||||
newKeys.push(keyName);
|
||||
}
|
||||
} else {
|
||||
// 没有修饰键,直接使用主键
|
||||
newKeys.push(keyName);
|
||||
}
|
||||
|
||||
// 唯一按键,不增加空字段
|
||||
if (newKeys.length > 0) {
|
||||
binding.keys = [...new Set(newKeys)];
|
||||
}
|
||||
|
||||
// 完成编辑
|
||||
binding.isEditing = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<SettingSection :title="t('settings.keyBindings')">
|
||||
<div class="key-bindings-container">
|
||||
<div class="key-bindings-header">
|
||||
<div class="command-col">命令</div>
|
||||
<div class="keybinding-col">快捷键</div>
|
||||
<div class="action-col">操作</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="binding in keyBindings"
|
||||
:key="binding.id"
|
||||
class="key-binding-row"
|
||||
:class="{ 'is-editing': binding.isEditing }"
|
||||
@keydown="(e) => handleKeyDown(e, binding)"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="command-col">{{ binding.name }}</div>
|
||||
<div class="keybinding-col" :class="{ 'is-editing': binding.isEditing }">
|
||||
<template v-if="binding.isEditing">
|
||||
按下快捷键...
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
v-for="(key, index) in binding.keys"
|
||||
:key="index"
|
||||
class="key-badge"
|
||||
>
|
||||
{{ key }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="action-col">
|
||||
<button
|
||||
class="edit-button"
|
||||
@click="toggleEdit(binding)"
|
||||
>
|
||||
{{ binding.isEditing ? '取消' : '编辑' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.key-bindings-container {
|
||||
padding: 10px 16px;
|
||||
|
||||
.key-bindings-header {
|
||||
display: flex;
|
||||
padding: 0 0 10px 0;
|
||||
border-bottom: 1px solid #444444;
|
||||
color: #a0a0a0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.key-binding-row {
|
||||
display: flex;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
&.is-editing {
|
||||
background-color: rgba(74, 158, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.command-col {
|
||||
flex: 1;
|
||||
padding-right: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.keybinding-col {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
padding: 0 10px;
|
||||
|
||||
&.is-editing {
|
||||
font-style: italic;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.key-badge {
|
||||
background-color: #3a3a3a;
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
border: 1px solid #555555;
|
||||
}
|
||||
}
|
||||
|
||||
.action-col {
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
|
||||
.edit-button {
|
||||
padding: 5px 10px;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #444444;
|
||||
border-color: #666666;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.coming-soon-placeholder {
|
||||
padding: 20px;
|
||||
background-color: #333333;
|
||||
border-radius: 6px;
|
||||
color: #a0a0a0;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
223
frontend/src/views/settings/pages/UpdatesPage.vue
Normal file
223
frontend/src/views/settings/pages/UpdatesPage.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// 模拟版本数据
|
||||
const currentVersion = ref('1.0.0');
|
||||
const isCheckingForUpdates = ref(false);
|
||||
const updateAvailable = ref(false);
|
||||
const latestVersion = ref('1.1.0');
|
||||
const updateNotes = ref([
|
||||
'优化编辑器性能',
|
||||
'新增自动保存功能',
|
||||
'修复多个界面显示问题',
|
||||
'添加更多编辑器主题'
|
||||
]);
|
||||
|
||||
// 自动检查更新选项
|
||||
const autoCheckUpdates = ref(true);
|
||||
|
||||
// 模拟检查更新
|
||||
const checkForUpdates = () => {
|
||||
isCheckingForUpdates.value = true;
|
||||
|
||||
// 模拟网络请求延迟
|
||||
setTimeout(() => {
|
||||
isCheckingForUpdates.value = false;
|
||||
updateAvailable.value = true;
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
// 模拟下载更新
|
||||
const downloadUpdate = () => {
|
||||
// 在实际应用中这里会调用后端API下载更新
|
||||
alert('开始下载更新...');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<SettingSection :title="t('settings.updates')">
|
||||
<div class="update-info">
|
||||
<div class="version-info">
|
||||
<div class="current-version">
|
||||
<span class="label">当前版本:</span>
|
||||
<span class="version">{{ currentVersion }}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="check-button"
|
||||
@click="checkForUpdates"
|
||||
:disabled="isCheckingForUpdates"
|
||||
>
|
||||
<span v-if="isCheckingForUpdates" class="loading-spinner"></span>
|
||||
{{ isCheckingForUpdates ? '检查中...' : '检查更新' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="updateAvailable" class="update-available">
|
||||
<div class="update-header">
|
||||
<div class="update-title">发现新版本: {{ latestVersion }}</div>
|
||||
<button class="download-button" @click="downloadUpdate">
|
||||
下载更新
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="update-notes">
|
||||
<div class="notes-title">更新内容:</div>
|
||||
<ul class="notes-list">
|
||||
<li v-for="(note, index) in updateNotes" :key="index">
|
||||
{{ note }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingItem title="自动检查更新" description="启动应用时自动检查更新">
|
||||
<ToggleSwitch v-model="autoCheckUpdates" />
|
||||
</SettingItem>
|
||||
</SettingSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.update-info {
|
||||
padding: 15px 16px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.version-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.current-version {
|
||||
font-size: 14px;
|
||||
|
||||
.label {
|
||||
color: #a0a0a0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.version {
|
||||
color: #e0e0e0;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.check-button {
|
||||
padding: 8px 16px;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #444444;
|
||||
border-color: #666666;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #e0e0e0;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.update-available {
|
||||
background-color: #2c3847;
|
||||
border: 1px solid #3a4a5c;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
|
||||
.update-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.update-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
padding: 8px 16px;
|
||||
background-color: #2c5a9e;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #3867a9;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.update-notes {
|
||||
.notes-title {
|
||||
font-size: 13px;
|
||||
color: #b0b0b0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.notes-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
|
||||
li {
|
||||
font-size: 13px;
|
||||
color: #d0d0d0;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user