feat: 添加自定义终端html背景的功能
This commit is contained in:
@@ -136,7 +136,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div ref="modalRootRef" class="fixed inset-0 z-[1000]" @click.self="closeCustomizer">
|
||||
<div ref="dialogContentRef" class="bg-background text-foreground rounded-lg shadow-lg w-full h-full md:w-[90%] md:max-w-[800px] md:h-[85vh] md:max-h-[700px] flex flex-col overflow-hidden">
|
||||
<div ref="dialogContentRef" class="bg-background text-foreground rounded-lg shadow-[0px_0px_15px_rgb(0_0_0_/_0.15)] w-full h-full md:w-[90%] md:max-w-[800px] md:h-[85vh] md:max-h-[700px] flex flex-col overflow-hidden">
|
||||
<header ref="headerRef" class="flex justify-between items-center px-4 py-3 border-b border-border bg-header flex-shrink-0">
|
||||
<h2 class="m-0 text-lg md:text-xl text-foreground">{{ t('styleCustomizer.title') }}</h2>
|
||||
<button @click="closeCustomizer" class="bg-transparent border-none text-2xl md:text-3xl leading-none cursor-pointer text-text-secondary px-2 py-1 rounded hover:text-foreground hover:bg-black/10">×</button>
|
||||
|
||||
+470
-47
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { ref, onMounted, watch, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAppearanceStore } from '../../stores/appearance.store';
|
||||
import { useUiNotificationsStore } from '../../stores/uiNotifications.store';
|
||||
@@ -8,28 +8,81 @@ import { storeToRefs } from 'pinia';
|
||||
const { t } = useI18n();
|
||||
const appearanceStore = useAppearanceStore();
|
||||
const notificationsStore = useUiNotificationsStore();
|
||||
|
||||
// Existing state for background image and overlay
|
||||
const {
|
||||
terminalBackgroundImage,
|
||||
isTerminalBackgroundEnabled,
|
||||
currentTerminalBackgroundOverlayOpacity,
|
||||
terminalCustomHTML,
|
||||
// terminalCustomHTML, // This will be replaced by preset logic
|
||||
// HTML Preset related state from store
|
||||
localHtmlPresets,
|
||||
remoteHtmlPresets,
|
||||
remoteHtmlPresetsRepositoryUrl,
|
||||
activeHtmlPresetTab, // This is the ref from the store
|
||||
isLoadingHtmlPresets,
|
||||
htmlPresetError,
|
||||
} = storeToRefs(appearanceStore);
|
||||
|
||||
// Actions from store
|
||||
const {
|
||||
fetchLocalHtmlPresets,
|
||||
getLocalHtmlPresetContent,
|
||||
createLocalHtmlPreset,
|
||||
updateLocalHtmlPreset,
|
||||
deleteLocalHtmlPreset,
|
||||
fetchRemoteHtmlPresetsRepositoryUrl,
|
||||
updateRemoteHtmlPresetsRepositoryUrl,
|
||||
fetchRemoteHtmlPresets,
|
||||
getRemoteHtmlPresetContent,
|
||||
applyHtmlPreset,
|
||||
} = appearanceStore;
|
||||
|
||||
|
||||
const localTerminalBackgroundEnabled = ref(true);
|
||||
const editableTerminalBackgroundOverlayOpacity = ref(0.5);
|
||||
const localTerminalCustomHTML = ref('');
|
||||
// const localTerminalCustomHTML = ref(''); // Replaced by preset editing
|
||||
|
||||
const terminalBgFileInput = ref<HTMLInputElement | null>(null);
|
||||
const uploadError = ref<string | null>(null);
|
||||
|
||||
// Component's internal active tab, synced with store's activeHtmlPresetTab
|
||||
const currentActiveTab = ref<'local' | 'remote'>('local');
|
||||
|
||||
// State for local preset editing/creating
|
||||
const showPresetEditor = ref(false);
|
||||
const editingPreset = ref<{ name: string, content: string } | null>(null); // For editing existing
|
||||
const newPresetName = ref('');
|
||||
const newPresetContent = ref('');
|
||||
|
||||
// State for remote presets
|
||||
const localRemoteHtmlPresetsRepositoryUrl = ref('');
|
||||
const localHtmlSearchTerm = ref('');
|
||||
const remoteHtmlSearchTerm = ref('');
|
||||
|
||||
|
||||
const initializeEditableState = () => {
|
||||
localTerminalBackgroundEnabled.value = isTerminalBackgroundEnabled.value;
|
||||
editableTerminalBackgroundOverlayOpacity.value = currentTerminalBackgroundOverlayOpacity.value;
|
||||
localTerminalCustomHTML.value = terminalCustomHTML.value || '';
|
||||
// localTerminalCustomHTML.value = terminalCustomHTML.value || ''; // Replaced
|
||||
uploadError.value = null;
|
||||
currentActiveTab.value = activeHtmlPresetTab.value; // Sync with store state
|
||||
localRemoteHtmlPresetsRepositoryUrl.value = remoteHtmlPresetsRepositoryUrl.value || 'https://github.com/Heavrnl/nexus-terminal/tree/main/doc/custom_html_theme';
|
||||
};
|
||||
|
||||
onMounted(initializeEditableState);
|
||||
onMounted(async () => {
|
||||
initializeEditableState();
|
||||
await fetchLocalHtmlPresets();
|
||||
// Fetch remote URL if not already set, or always? Per plan, store initializes it.
|
||||
// If store's remoteHtmlPresetsRepositoryUrl is null, then fetch it.
|
||||
if (!remoteHtmlPresetsRepositoryUrl.value) {
|
||||
await fetchRemoteHtmlPresetsRepositoryUrl();
|
||||
}
|
||||
// If a URL exists, fetch remote presets
|
||||
if (remoteHtmlPresetsRepositoryUrl.value) {
|
||||
await fetchRemoteHtmlPresets();
|
||||
}
|
||||
});
|
||||
|
||||
watch(isTerminalBackgroundEnabled, (newValue) => {
|
||||
if (localTerminalBackgroundEnabled.value !== newValue) {
|
||||
@@ -43,11 +96,13 @@ watch(currentTerminalBackgroundOverlayOpacity, (newValue) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Watch store's activeHtmlPresetTab and update local component state
|
||||
watch(activeHtmlPresetTab, (newTab) => {
|
||||
currentActiveTab.value = newTab;
|
||||
});
|
||||
|
||||
watch(terminalCustomHTML, (newValue) => {
|
||||
if (localTerminalCustomHTML.value !== (newValue || '')) {
|
||||
localTerminalCustomHTML.value = newValue || '';
|
||||
}
|
||||
watch(remoteHtmlPresetsRepositoryUrl, (newUrl) => {
|
||||
localRemoteHtmlPresetsRepositoryUrl.value = newUrl || '';
|
||||
});
|
||||
|
||||
const handleTriggerTerminalBgUpload = () => {
|
||||
@@ -55,7 +110,6 @@ const handleTriggerTerminalBgUpload = () => {
|
||||
terminalBgFileInput.value?.click();
|
||||
};
|
||||
|
||||
|
||||
const handleTerminalBgUpload = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files[0]) {
|
||||
@@ -67,7 +121,7 @@ const handleTerminalBgUpload = async (event: Event) => {
|
||||
} catch (error: any) {
|
||||
const determinedErrorMessage = error.message || t('styleCustomizer.uploadFailed');
|
||||
uploadError.value = determinedErrorMessage;
|
||||
notificationsStore.addNotification({ type: 'error', message: determinedErrorMessage }); // 显示错误通知
|
||||
notificationsStore.addNotification({ type: 'error', message: determinedErrorMessage });
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
@@ -83,22 +137,18 @@ const handleRemoveTerminalBg = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理终端背景启用/禁用切换
|
||||
const handleToggleTerminalBackground = async () => {
|
||||
const newValue = !localTerminalBackgroundEnabled.value; // 先计算新值
|
||||
localTerminalBackgroundEnabled.value = newValue; // 立即更新本地 UI
|
||||
const newValue = !localTerminalBackgroundEnabled.value;
|
||||
localTerminalBackgroundEnabled.value = newValue;
|
||||
try {
|
||||
await appearanceStore.setTerminalBackgroundEnabled(newValue);
|
||||
// 成功后不需要提示,UI 已更新
|
||||
} catch (error: any) {
|
||||
console.error("更新终端背景启用状态失败:", error);
|
||||
// 失败时回滚本地状态
|
||||
localTerminalBackgroundEnabled.value = !newValue;
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.errorToggleTerminalBg', { message: error.message }) });
|
||||
}
|
||||
};
|
||||
|
||||
// 保存终端背景蒙版透明度
|
||||
const handleSaveTerminalBackgroundOverlayOpacity = async () => {
|
||||
try {
|
||||
const opacity = Number(editableTerminalBackgroundOverlayOpacity.value);
|
||||
@@ -114,30 +164,223 @@ const handleSaveTerminalBackgroundOverlayOpacity = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- HTML Preset Functions ---
|
||||
const switchTab = (tab: 'local' | 'remote') => {
|
||||
appearanceStore.activeHtmlPresetTab = tab; // Update store, which will update currentActiveTab via watcher
|
||||
};
|
||||
|
||||
const handleSaveCustomHTML = async () => {
|
||||
const handleApplyPreset = async (htmlContent: string) => {
|
||||
try {
|
||||
await appearanceStore.setTerminalCustomHTML(localTerminalCustomHTML.value);
|
||||
notificationsStore.addNotification({ type: 'success', message: t('styleCustomizer.customTerminalHTMLSaved') });
|
||||
await applyHtmlPreset(htmlContent);
|
||||
notificationsStore.addNotification({ type: 'success', message: t('styleCustomizer.htmlPresetApplied') });
|
||||
} catch (error: any) {
|
||||
console.error("保存自定义终端 HTML 失败:", error);
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.customTerminalHTMLSaveFailed', { message: error.message }) });
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.htmlPresetApplyFailed', { message: error.message }) });
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetCustomHtml = async () => {
|
||||
try {
|
||||
await applyHtmlPreset(''); // Apply empty HTML to reset
|
||||
notificationsStore.addNotification({ type: 'success', message: t('styleCustomizer.customHtmlResetSuccess', '自定义 HTML 已重置。') });
|
||||
} catch (error: any) {
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.customHtmlResetFailed', { message: error.message }) });
|
||||
}
|
||||
};
|
||||
|
||||
// Local preset functions
|
||||
const openNewPresetEditor = () => {
|
||||
editingPreset.value = null;
|
||||
newPresetName.value = '';
|
||||
newPresetContent.value = '';
|
||||
showPresetEditor.value = true;
|
||||
};
|
||||
|
||||
const openEditPresetEditor = (preset: { name: string, content: string }) => { // This is for editing CUSTOM themes
|
||||
editingPreset.value = { ...preset };
|
||||
newPresetName.value = preset.name.replace(/\.html$/, ''); // Remove .html for editing
|
||||
newPresetContent.value = preset.content;
|
||||
showPresetEditor.value = true;
|
||||
};
|
||||
|
||||
// New function to handle "Edit" for a preset theme, which means creating a new custom theme based on it
|
||||
const handleEditPresetAsNew = async (preset: { name: string, type: 'preset' | 'custom' }) => {
|
||||
if (preset.type !== 'preset') {
|
||||
console.warn("handleEditPresetAsNew called with a non-preset theme. This should not happen.");
|
||||
// Fallback to regular edit if it's somehow a custom theme
|
||||
const content = await getLocalHtmlPresetContent(preset.name);
|
||||
openEditPresetEditor({ name: preset.name, content });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await getLocalHtmlPresetContent(preset.name); // Get content of the preset
|
||||
editingPreset.value = null; // Important: we are creating a NEW theme
|
||||
newPresetName.value = `${preset.name.replace(/\.html$/, '')}(1)`; // Default new name
|
||||
newPresetContent.value = content;
|
||||
showPresetEditor.value = true;
|
||||
} catch (e: any) {
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.errorFetchingPresetContentForCopy', { message: e.message, name: preset.name }) });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSaveLocalPreset = async () => {
|
||||
const desiredBaseName = newPresetName.value.trim(); // Name without .html, from input
|
||||
const content = newPresetContent.value.trim();
|
||||
|
||||
if (!desiredBaseName) {
|
||||
// It's recommended to add this key to your i18n files, e.g., "Preset name cannot be empty."
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.errorPresetNameRequired', '预设名称不能为空。') });
|
||||
return;
|
||||
}
|
||||
if (!content) {
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.errorPresetContentRequired') });
|
||||
return;
|
||||
}
|
||||
|
||||
const finalNewFullName = desiredBaseName.endsWith('.html') ? desiredBaseName : `${desiredBaseName}.html`;
|
||||
|
||||
if (editingPreset.value) { // Editing existing
|
||||
const originalFullName = editingPreset.value.name; // Original name with .html
|
||||
|
||||
if (finalNewFullName === originalFullName) { // Name hasn't changed, only content might have
|
||||
try {
|
||||
await updateLocalHtmlPreset(originalFullName, content);
|
||||
notificationsStore.addNotification({ type: 'success', message: t('styleCustomizer.localPresetUpdated') });
|
||||
showPresetEditor.value = false;
|
||||
} catch (error: any) {
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.localPresetUpdateFailed', { message: error.message }) });
|
||||
}
|
||||
} else { // Name has changed: "Rename" by creating new and deleting old
|
||||
try {
|
||||
// Attempt to create the new preset first. If this name already exists, createLocalHtmlPreset should throw an error.
|
||||
await createLocalHtmlPreset(finalNewFullName, content);
|
||||
// If creation was successful, delete the old preset
|
||||
await deleteLocalHtmlPreset(originalFullName);
|
||||
// It's recommended to add this key to your i18n files, e.g., "Local preset '{oldName}' has been renamed to '{newName}'."
|
||||
notificationsStore.addNotification({ type: 'success', message: t('styleCustomizer.localPresetRenamed', { oldName: originalFullName.replace(/\.html$/, ''), newName: desiredBaseName }) });
|
||||
showPresetEditor.value = false;
|
||||
} catch (error: any) {
|
||||
// It's recommended to add this key to your i18n files, e.g., "Failed to rename local preset: {message}"
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.localPresetRenameFailed', { message: error.message }) });
|
||||
}
|
||||
}
|
||||
} else { // Creating new
|
||||
// Validation for new name and content already happened above
|
||||
try {
|
||||
await createLocalHtmlPreset(finalNewFullName, content);
|
||||
notificationsStore.addNotification({ type: 'success', message: t('styleCustomizer.localPresetCreated') });
|
||||
showPresetEditor.value = false;
|
||||
newPresetName.value = ''; // Clear fields for next new preset
|
||||
newPresetContent.value = '';
|
||||
} catch (error: any) {
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.localPresetCreateFailed', { message: error.message }) });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteLocalPreset = async (name: string) => {
|
||||
// name here is the full filename with .html
|
||||
const displayName = name.replace(/\.html$/, '');
|
||||
if (confirm(t('styleCustomizer.confirmDeletePreset', { name: displayName }))) {
|
||||
try {
|
||||
await deleteLocalHtmlPreset(name);
|
||||
notificationsStore.addNotification({ type: 'success', message: t('styleCustomizer.localPresetDeleted') });
|
||||
} catch (error: any) {
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.localPresetDeleteFailed', { message: error.message }) });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Remote preset functions
|
||||
const handleSaveRemoteRepositoryUrl = async () => {
|
||||
// Allow saving an empty URL to disable remote presets
|
||||
try {
|
||||
await updateRemoteHtmlPresetsRepositoryUrl(localRemoteHtmlPresetsRepositoryUrl.value);
|
||||
notificationsStore.addNotification({ type: 'success', message: t('styleCustomizer.remoteUrlSaved') });
|
||||
// Optionally fetch presets immediately after saving new URL
|
||||
await fetchRemoteHtmlPresets(localRemoteHtmlPresetsRepositoryUrl.value);
|
||||
} catch (error: any) {
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.remoteUrlSaveFailed', { message: error.message }) });
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadRemotePresets = async () => {
|
||||
if (!remoteHtmlPresetsRepositoryUrl.value) {
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.errorSetRemoteUrlFirst') });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await fetchRemoteHtmlPresets();
|
||||
if (!htmlPresetError.value) {
|
||||
notificationsStore.addNotification({ type: 'success', message: t('styleCustomizer.remotePresetsLoaded') });
|
||||
}
|
||||
} catch (error: any) { // This catch might not be needed if store handles errors
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.remotePresetsLoadFailed', { message: error.message }) });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Placeholder for applying a local theme (needs to fetch content first)
|
||||
const applyLocalPreset = async (presetName: string) => {
|
||||
try {
|
||||
const content = await getLocalHtmlPresetContent(presetName);
|
||||
await handleApplyPreset(content);
|
||||
} catch (error: any) {
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.localPresetApplyFailed', { message: error.message }) });
|
||||
}
|
||||
};
|
||||
|
||||
// Placeholder for applying a remote theme (needs to fetch content first)
|
||||
const applyRemotePreset = async (downloadUrl?: string) => {
|
||||
if (!downloadUrl) {
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.errorMissingDownloadUrl') });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const content = await getRemoteHtmlPresetContent(downloadUrl);
|
||||
await handleApplyPreset(content);
|
||||
} catch (error: any) {
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.remotePresetApplyFailed', { message: error.message }) });
|
||||
}
|
||||
};
|
||||
|
||||
const filteredLocalHtmlPresets = computed(() => {
|
||||
const searchTerm = localHtmlSearchTerm.value.toLowerCase().trim();
|
||||
let presets = [...localHtmlPresets.value]; // Make a copy to sort
|
||||
if (searchTerm) {
|
||||
presets = presets.filter(preset => preset.name.replace(/\.html$/, '').toLowerCase().includes(searchTerm));
|
||||
}
|
||||
// Sort by name
|
||||
presets.sort((a, b) => a.name.replace(/\.html$/, '').localeCompare(b.name.replace(/\.html$/, '')));
|
||||
return presets;
|
||||
});
|
||||
|
||||
const filteredRemoteHtmlPresets = computed(() => {
|
||||
const searchTerm = remoteHtmlSearchTerm.value.toLowerCase().trim();
|
||||
let presets = [...remoteHtmlPresets.value]; // Make a copy to sort
|
||||
if (searchTerm) {
|
||||
presets = presets.filter(preset => preset.name.replace(/\.html$/, '').toLowerCase().includes(searchTerm));
|
||||
}
|
||||
presets.sort((a, b) => a.name.replace(/\.html$/, '').localeCompare(b.name.replace(/\.html$/, '')));
|
||||
return presets;
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<h3 class="mt-0 border-b border-border pb-2 mb-4 text-lg font-semibold text-foreground">{{ t('styleCustomizer.backgroundSettings') }}</h3>
|
||||
|
||||
<!-- Tab Switcher -->
|
||||
|
||||
|
||||
<hr class="my-4 md:my-8 border-border">
|
||||
|
||||
<!-- 终端背景 -->
|
||||
<!-- Existing Terminal Background Image Settings -->
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="m-0 text-base font-semibold text-foreground">{{ t('styleCustomizer.terminalBackground') }}</h4>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="handleToggleTerminalBackground"
|
||||
@@ -158,10 +401,8 @@ const handleSaveCustomHTML = async () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="localTerminalBackgroundEnabled">
|
||||
<div class="w-full h-[100px] md:h-[150px] border border-dashed border-border mb-2 flex justify-center items-center text-text-secondary bg-cover bg-center bg-no-repeat rounded bg-header relative overflow-hidden" :style="{ backgroundImage: terminalBackgroundImage ? `url(${terminalBackgroundImage})` : 'none' }">
|
||||
<!-- 实时预览蒙版 -->
|
||||
<div
|
||||
v-if="terminalBackgroundImage"
|
||||
class="absolute inset-0"
|
||||
@@ -175,7 +416,6 @@ const handleSaveCustomHTML = async () => {
|
||||
<input type="file" ref="terminalBgFileInput" @change="handleTerminalBgUpload" accept="image/*" class="hidden" />
|
||||
</div>
|
||||
|
||||
<!-- 终端背景蒙版透明度控制 -->
|
||||
<div class="mt-4 pt-4 border-t border-border/50">
|
||||
<label for="terminalBgOverlayOpacity" class="block text-sm font-medium text-foreground mb-1">{{ t('styleCustomizer.terminalBgOverlayOpacity', '终端背景蒙版透明度:') }}</label>
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -192,29 +432,212 @@ const handleSaveCustomHTML = async () => {
|
||||
<button @click="handleSaveTerminalBackgroundOverlayOpacity" class="px-3 py-1.5 text-sm border border-border rounded bg-header hover:bg-border transition duration-200 ease-in-out whitespace-nowrap">{{ t('common.save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自定义终端背景 HTML -->
|
||||
<div class="mt-4 pt-4 border-t border-border/50">
|
||||
<label for="terminalCustomHTML" class="block text-sm font-medium text-foreground mb-1">{{ t('styleCustomizer.customTerminalHTML', '自定义终端背景 HTML') }}</label>
|
||||
<textarea
|
||||
id="terminalCustomHTML"
|
||||
v-model="localTerminalCustomHTML"
|
||||
rows="10"
|
||||
class="w-full p-2 border border-border rounded bg-input text-foreground focus:ring-primary focus:border-primary"
|
||||
:placeholder="t('styleCustomizer.customTerminalHTMLPlaceholder', '例如:<h1>Hello</h1>')"
|
||||
></textarea>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<button
|
||||
@click="handleSaveCustomHTML"
|
||||
class="px-3 py-1.5 text-sm border border-border rounded bg-header hover:bg-border transition duration-200 ease-in-out whitespace-nowrap"
|
||||
>
|
||||
{{ t('common.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Old custom HTML textarea is removed from here -->
|
||||
</div>
|
||||
<div v-else class="p-4 text-center text-text-secondary italic border border-dashed border-border/50 rounded-md">
|
||||
{{ t('styleCustomizer.terminalBgDisabled', '终端背景功能已禁用。') }}
|
||||
</div>
|
||||
|
||||
<hr class="my-6 border-border">
|
||||
|
||||
<!-- Tab Switcher for HTML Background Themes -->
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<h4 class="mt-0 text-base font-semibold text-foreground">{{ t('styleCustomizer.htmlBackgroundThemes') }}</h4>
|
||||
<button
|
||||
@click="handleResetCustomHtml"
|
||||
type="button"
|
||||
class="p-1.5 text-xs rounded text-foreground hover:bg-border transition-colors duration-150 focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
>
|
||||
<i class="fa-solid fa-rotate-left"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4 flex border-b border-border">
|
||||
<button
|
||||
@click="switchTab('local')"
|
||||
:class="['px-4 py-2 -mb-px border-b-2 transition-colors duration-150', currentActiveTab === 'local' ? 'border-primary text-primary font-semibold' : 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-text-secondary hover:text-foreground']"
|
||||
>
|
||||
{{ t('styleCustomizer.localThemes') }}
|
||||
</button>
|
||||
<button
|
||||
@click="switchTab('remote')"
|
||||
:class="['px-4 py-2 -mb-px border-b-2 transition-colors duration-150', currentActiveTab === 'remote' ? 'border-primary text-primary font-semibold' : 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-text-secondary hover:text-foreground']"
|
||||
>
|
||||
{{ t('styleCustomizer.remoteThemes') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content based on active tab -->
|
||||
<div v-if="currentActiveTab === 'local'">
|
||||
<!-- Flex container for search and new preset button -->
|
||||
<div class="mb-4 flex items-center gap-4">
|
||||
<input
|
||||
type="text"
|
||||
v-model="localHtmlSearchTerm"
|
||||
:placeholder="t('styleCustomizer.searchLocalThemesPlaceholder', '搜索本地主题...')"
|
||||
class="flex-grow border border-border px-[0.7rem] py-2 rounded text-sm bg-background text-foreground box-border transition duration-200 ease-in-out focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
/>
|
||||
<button @click="openNewPresetEditor" class="px-3 py-1.5 text-sm border border-border rounded bg-header hover:bg-border transition duration-200 ease-in-out whitespace-nowrap flex-shrink-0">
|
||||
{{ t('styleCustomizer.addNewTheme') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingHtmlPresets" class="text-center p-4 text-text-secondary">
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
<ul v-else-if="filteredLocalHtmlPresets.length > 0" class="list-none p-0 mt-4 max-h-[200px] md:max-h-[280px] overflow-y-auto border border-border rounded bg-background">
|
||||
<li v-for="(preset, index) in filteredLocalHtmlPresets" :key="preset.name"
|
||||
:class="[
|
||||
'block md:grid md:grid-cols-[1fr_auto] items-center px-3 py-2.5 text-sm md:text-[0.95rem] transition-colors duration-200 ease-in-out gap-2',
|
||||
index < filteredLocalHtmlPresets.length - 1 ? 'border-b border-border' : '',
|
||||
'hover:bg-header'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center gap-2 md:col-start-1 md:col-end-2 overflow-hidden text-ellipsis whitespace-nowrap mb-2 md:mb-0">
|
||||
<span class="text-foreground font-medium" :title="preset.name.replace(/\.html$/, '')">{{ preset.name.replace(/\.html$/, '') }}</span>
|
||||
<span v-if="preset.type === 'preset'" class="px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-800 dark:bg-blue-700 dark:text-blue-200">
|
||||
{{ t('styleCustomizer.presetTag', '预设') }}
|
||||
</span>
|
||||
<span v-else-if="preset.type === 'custom'" class="px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-700 dark:text-green-200">
|
||||
{{ t('styleCustomizer.customTag', '自定义') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex md:col-start-2 md:col-end-3 flex-shrink-0 gap-2 justify-start md:justify-end flex-wrap">
|
||||
<button @click="applyLocalPreset(preset.name)"
|
||||
:title="t('styleCustomizer.applyThemeTooltip', 'Apply this theme')"
|
||||
class="px-3 py-1.5 text-xs md:text-sm border rounded transition-colors duration-200 ease-in-out whitespace-nowrap border-border bg-header text-foreground hover:bg-border hover:border-text-secondary">
|
||||
{{ t('common.apply') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="preset.type === 'custom'"
|
||||
@click="async () => {
|
||||
try {
|
||||
const content = await getLocalHtmlPresetContent(preset.name);
|
||||
openEditPresetEditor({ name: preset.name, content });
|
||||
} catch (e: any) {
|
||||
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.errorFetchingPresetContentForEdit', { message: e.message }) });
|
||||
}
|
||||
}"
|
||||
:title="t('common.edit')"
|
||||
class="px-3 py-1.5 text-xs md:text-sm border rounded transition-colors duration-200 ease-in-out whitespace-nowrap border-border bg-header text-foreground hover:bg-border hover:border-text-secondary">
|
||||
{{ t('common.edit') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="preset.type === 'preset'"
|
||||
@click="handleEditPresetAsNew(preset)"
|
||||
:title="t('styleCustomizer.editAsNewTooltip', '编辑为新自定义主题')"
|
||||
class="px-3 py-1.5 text-xs md:text-sm border rounded transition-colors duration-200 ease-in-out whitespace-nowrap border-border bg-header text-foreground hover:bg-border hover:border-text-secondary">
|
||||
{{ t('common.edit') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="preset.type === 'custom'"
|
||||
@click="handleDeleteLocalPreset(preset.name)"
|
||||
:title="t('common.delete')"
|
||||
class="px-3 py-1.5 text-xs md:text-sm border rounded transition-colors duration-200 ease-in-out whitespace-nowrap bg-error/10 text-error border-error/30 hover:bg-error/20">
|
||||
{{ t('common.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else-if="htmlPresetError" class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
|
||||
{{ htmlPresetError }}
|
||||
</div>
|
||||
<div v-else class="text-center p-4 text-text-secondary italic border border-dashed border-border rounded-md">
|
||||
{{ localHtmlSearchTerm ? t('styleCustomizer.noMatchingLocalPresetsFound', '未找到匹配的本地主题') : t('styleCustomizer.noLocalPresetsFound') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentActiveTab === 'remote'">
|
||||
<!-- URL Input and Buttons Container -->
|
||||
<div class="mb-4">
|
||||
<label for="remoteRepoUrl" class="block text-sm font-medium text-foreground mb-1">{{ t('styleCustomizer.remoteHtmlPresetsRepositoryUrl') }}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
id="remoteRepoUrl"
|
||||
v-model="localRemoteHtmlPresetsRepositoryUrl"
|
||||
class="flex-grow p-2 border border-border rounded bg-input text-foreground focus:ring-primary focus:border-primary"
|
||||
:placeholder="t('styleCustomizer.remoteRepoUrlPlaceholder', 'https://github.com/Heavrnl/nexus-terminal/tree/main/doc/custom_html_theme')"
|
||||
/>
|
||||
<button @click="handleSaveRemoteRepositoryUrl" class="px-3 py-1.5 text-sm border border-border rounded bg-header hover:bg-border transition duration-200 ease-in-out whitespace-nowrap flex-shrink-0">
|
||||
{{ t('common.save') }}
|
||||
</button>
|
||||
<button @click="handleLoadRemotePresets" :disabled="!remoteHtmlPresetsRepositoryUrl || isLoadingHtmlPresets" class="px-3 py-1.5 text-sm border border-border rounded bg-header hover:bg-border transition duration-200 ease-in-out whitespace-nowrap disabled:opacity-50 flex-shrink-0">
|
||||
{{ isLoadingHtmlPresets && currentActiveTab === 'remote' ? t('common.loading') : t('styleCustomizer.loadRemoteThemes') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Box Container -->
|
||||
<div class="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
v-model="remoteHtmlSearchTerm"
|
||||
:placeholder="t('styleCustomizer.searchRemoteThemesPlaceholder', '搜索远程主题...')"
|
||||
class="border border-border px-[0.7rem] py-2 rounded text-sm bg-background text-foreground w-full box-border transition duration-200 ease-in-out focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingHtmlPresets && currentActiveTab === 'remote'" class="text-center p-4 text-text-secondary">
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
<div v-else-if="htmlPresetError && currentActiveTab === 'remote'" class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
|
||||
{{ htmlPresetError }}
|
||||
</div>
|
||||
<div v-else-if="!remoteHtmlPresetsRepositoryUrl" class="text-center p-4 text-text-secondary italic border border-dashed border-border rounded-md">
|
||||
{{ t('styleCustomizer.pleaseSetRemoteUrl') }}
|
||||
</div>
|
||||
<ul v-else-if="filteredRemoteHtmlPresets.length > 0 && remoteHtmlPresetsRepositoryUrl" class="list-none p-0 mt-4 max-h-[200px] md:max-h-[280px] overflow-y-auto border border-border rounded bg-background">
|
||||
<li v-for="(preset, index) in filteredRemoteHtmlPresets" :key="preset.name"
|
||||
:class="[
|
||||
'block md:grid md:grid-cols-[1fr_auto] items-center px-3 py-2.5 text-sm md:text-[0.95rem] transition-colors duration-200 ease-in-out gap-2',
|
||||
index < filteredRemoteHtmlPresets.length - 1 ? 'border-b border-border' : '',
|
||||
'hover:bg-header'
|
||||
]"
|
||||
>
|
||||
<span class="block md:col-start-1 md:col-end-2 overflow-hidden text-ellipsis whitespace-nowrap mb-2 md:mb-0 text-foreground font-medium" :title="preset.name.replace(/\.html$/, '')">{{ preset.name.replace(/\.html$/, '') }}</span>
|
||||
<div class="flex md:col-start-2 md:col-end-3 flex-shrink-0 gap-2 justify-start md:justify-end flex-wrap">
|
||||
<button @click="applyRemotePreset(preset.downloadUrl)" :disabled="!preset.downloadUrl"
|
||||
:title="t('styleCustomizer.applyThemeTooltip', 'Apply this theme')"
|
||||
class="px-3 py-1.5 text-xs md:text-sm border rounded transition-colors duration-200 ease-in-out whitespace-nowrap border-border bg-header text-foreground hover:bg-border hover:border-text-secondary disabled:opacity-50">
|
||||
{{ t('common.apply') }}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else-if="remoteHtmlPresetsRepositoryUrl" class="text-center p-4 text-text-secondary italic border border-dashed border-border rounded-md">
|
||||
{{ remoteHtmlSearchTerm ? t('styleCustomizer.noMatchingRemotePresetsFound', '未找到匹配的远程主题') : t('styleCustomizer.noRemotePresetsFound') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Preset Editor (Modal or Inline) - Simplified for now -->
|
||||
<div v-if="showPresetEditor" class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50" @click.self="showPresetEditor = false">
|
||||
<div class="bg-background p-6 rounded-lg shadow-xl w-full max-w-lg">
|
||||
<div class="mb-4">
|
||||
<label for="presetName" class="block text-sm font-medium text-foreground mb-1">{{ t('styleCustomizer.presetName') }}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="presetName"
|
||||
v-model="newPresetName"
|
||||
class="w-full p-2 border border-border rounded bg-input text-foreground focus:ring-primary focus:border-primary"
|
||||
:placeholder="t('styleCustomizer.presetNamePlaceholder', 'my-theme')"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="presetContent" class="block text-sm font-medium text-foreground mb-1">{{ t('styleCustomizer.presetContent') }}</label>
|
||||
<textarea
|
||||
id="presetContent"
|
||||
v-model="newPresetContent"
|
||||
rows="10"
|
||||
class="w-full p-2 border border-border rounded bg-input text-foreground focus:ring-primary focus:border-primary"
|
||||
:placeholder="t('styleCustomizer.customTerminalHTMLPlaceholder', '例如:<h1>Hello</h1>')"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button @click="showPresetEditor = false" class="px-4 py-2 text-sm border border-border rounded bg-header hover:bg-border transition">{{ t('common.cancel') }}</button>
|
||||
<button @click="handleSaveLocalPreset" class="px-4 py-2 text-sm rounded bg-primary text-primary-foreground hover:bg-primary/90 transition">{{ t('common.save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -99,7 +99,55 @@
|
||||
"terminalBgOverlayOpacityDesc": "Controls the opacity of the black overlay on top of the background image. 0 is fully transparent, 1 is fully opaque.",
|
||||
"errorInvalidOpacityValue": "Invalid opacity value. Must be between 0 and 1.",
|
||||
"terminalBgOverlayOpacitySaved": "Terminal background overlay opacity saved.",
|
||||
"terminalBgOverlayOpacitySaveFailed": "Failed to save terminal background overlay opacity: {message}"
|
||||
"terminalBgOverlayOpacitySaveFailed": "Failed to save terminal background overlay opacity: {message}",
|
||||
"terminalBgDisabled": "Terminal background feature is disabled.",
|
||||
"htmlBackgroundThemes": "HTML Background Themes",
|
||||
"localThemes": "Local Themes",
|
||||
"remoteThemes": "Remote Themes",
|
||||
"newLocalPreset": "New Local Theme",
|
||||
"noLocalPresetsFound": "No local HTML themes found.",
|
||||
"errorFetchingPresetContentForEdit": "Failed to fetch theme content for editing: {message}",
|
||||
"remoteHtmlPresetsRepositoryUrl": "Remote HTML Themes Repository URL",
|
||||
"remoteRepoUrlPlaceholder": "e.g., https://github.com/user/repo/tree/main/themes",
|
||||
"saveUrl": "Save URL",
|
||||
"loadRemoteThemes": "refresh",
|
||||
"pleaseSetRemoteUrl": "Please set the remote HTML themes repository URL first.",
|
||||
"noRemotePresetsFound": "No HTML themes found in the remote repository, or the URL is invalid.",
|
||||
"editLocalPreset": "Edit Local Theme",
|
||||
"presetName": "Theme Name",
|
||||
"presetNamePlaceholder": "e.g., my-theme.html",
|
||||
"presetContent": "Theme Content",
|
||||
"customTerminalHTMLPlaceholder": "e.g., <h1>Hello</h1>",
|
||||
"errorToggleTerminalBg": "Failed to update terminal background enabled state: {message}",
|
||||
"htmlPresetApplied": "HTML theme applied.",
|
||||
"htmlPresetApplyFailed": "Failed to apply HTML theme: {message}",
|
||||
"errorPresetContentRequired": "Theme content cannot be empty.",
|
||||
"localPresetUpdated": "Local HTML theme updated.",
|
||||
"localPresetUpdateFailed": "Failed to update local HTML theme: {message}",
|
||||
"errorPresetNameAndContentRequired": "Theme name and content cannot be empty.",
|
||||
"localPresetCreated": "Local HTML theme created.",
|
||||
"localPresetCreateFailed": "Failed to create local HTML theme: {message}",
|
||||
"confirmDeletePreset": "Are you sure you want to delete the HTML theme \"{name}\"?",
|
||||
"localPresetDeleted": "Local HTML theme deleted.",
|
||||
"localPresetDeleteFailed": "Failed to delete local HTML theme: {message}",
|
||||
"errorRemoteUrlRequired": "Remote HTML themes repository URL cannot be empty.",
|
||||
"remoteUrlSaved": "Remote HTML themes repository URL saved.",
|
||||
"remoteUrlSaveFailed": "Failed to save remote HTML themes repository URL: {message}",
|
||||
"errorSetRemoteUrlFirst": "Please set and save the remote HTML themes repository URL first.",
|
||||
"remotePresetsLoaded": "Remote HTML themes list loaded.",
|
||||
"remotePresetsLoadFailed": "Failed to load remote HTML themes list: {message}",
|
||||
"localPresetApplyFailed": "Failed to apply local HTML theme: {message}",
|
||||
"errorPresetNameRequired": "Preset name cannot be empty.",
|
||||
"errorMissingDownloadUrl": "Remote theme download URL is missing.",
|
||||
"remotePresetApplyFailed": "Failed to apply remote HTML theme: {message}",
|
||||
"customHtmlResetSuccess":"Custom HTML has been reset.",
|
||||
"searchLocalThemesPlaceholder": "Search local themes...",
|
||||
"searchRemoteThemesPlaceholder": "Search remote themes...",
|
||||
"noMatchingLocalPresetsFound": "No matching local themes found",
|
||||
"noMatchingRemotePresetsFound": "No matching remote themes found",
|
||||
"editAsNewTooltip": "Edit as new custom theme",
|
||||
"presetTag": "Preset",
|
||||
"customTag": "Custom"
|
||||
},
|
||||
"login": {
|
||||
"title": "User Login",
|
||||
@@ -977,6 +1025,7 @@
|
||||
"testMessageUnsaved": "Test triggered for unsaved {channelType} configuration"
|
||||
},
|
||||
"common": {
|
||||
"apply": "Apply",
|
||||
"loading": "Loading...",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"clearTerminal": "ターミナルをクリア"
|
||||
},
|
||||
"common": {
|
||||
"apply": "適用",
|
||||
"all": "すべて",
|
||||
"cancel": "キャンセル",
|
||||
"close": "閉じる",
|
||||
@@ -1285,7 +1286,56 @@
|
||||
"terminalBgOverlayOpacityDesc": "背景画像上の黒いオーバーレイの不透明度を制御します。0は完全に透明、1は完全に不透明です。",
|
||||
"errorInvalidOpacityValue": "無効な不透明度の値です。0から1の間でなければなりません。",
|
||||
"terminalBgOverlayOpacitySaved": "ターミナル背景オーバーレイの不透明度が保存されました。",
|
||||
"terminalBgOverlayOpacitySaveFailed": "ターミナル背景オーバーレイの不透明度の保存に失敗しました: {message}"
|
||||
"terminalBgOverlayOpacitySaveFailed": "ターミナル背景オーバーレイの不透明度の保存に失敗しました: {message}",
|
||||
"terminalBgDisabled": "ターミナルの背景機能は無効です。",
|
||||
"htmlBackgroundThemes": "HTML背景テーマ",
|
||||
"localThemes": "ローカルテーマ",
|
||||
"remoteThemes": "リモートテーマ",
|
||||
"newLocalPreset": "新しいローカルテーマを作成",
|
||||
"noLocalPresetsFound": "ローカルHTMLテーマが見つかりません。",
|
||||
"errorFetchingPresetContentForEdit": "編集のためにテーマコンテンツの取得に失敗しました: {message}",
|
||||
"remoteHtmlPresetsRepositoryUrl": "リモートHTMLテーマリポジトリURL",
|
||||
"remoteRepoUrlPlaceholder": "例: https://github.com/user/repo/tree/main/themes",
|
||||
"saveUrl": "URLを保存",
|
||||
"loadRemoteThemes": "リフレッシュ",
|
||||
"pleaseSetRemoteUrl": "最初にリモートHTMLテーマリポジトリのURLを設定してください。",
|
||||
"noRemotePresetsFound": "リモートリポジトリにHTMLテーマが見つからないか、URLが無効です。",
|
||||
"editLocalPreset": "ローカルテーマを編集",
|
||||
"presetName": "テーマ名",
|
||||
"presetNamePlaceholder": "例: my-theme.html",
|
||||
"presetContent": "テーマコンテンツ",
|
||||
"customTerminalHTMLPlaceholder": "例: <h1>こんにちは</h1>",
|
||||
"errorToggleTerminalBg": "ターミナルの背景有効状態の更新に失敗しました: {message}",
|
||||
"htmlPresetApplied": "HTMLテーマが適用されました。",
|
||||
"htmlPresetApplyFailed": "HTMLテーマの適用に失敗しました: {message}",
|
||||
"errorPresetContentRequired": "テーマのコンテンツは空にできません。",
|
||||
"localPresetUpdated": "ローカルHTMLテーマが更新されました。",
|
||||
"localPresetUpdateFailed": "ローカルHTMLテーマの更新に失敗しました: {message}",
|
||||
"errorPresetNameAndContentRequired": "テーマ名とコンテンツは空にできません。",
|
||||
"localPresetCreated": "ローカルHTMLテーマが作成されました。",
|
||||
"localPresetCreateFailed": "ローカルHTMLテーマの作成に失敗しました: {message}",
|
||||
"confirmDeletePreset": "HTMLテーマ「{name}」を削除してもよろしいですか?",
|
||||
"localPresetDeleted": "ローカルHTMLテーマが削除されました。",
|
||||
"localPresetDeleteFailed": "ローカルHTMLテーマの削除に失敗しました: {message}",
|
||||
"errorRemoteUrlRequired": "リモートHTMLテーマリポジトリのURLは空にできません。",
|
||||
"remoteUrlSaved": "リモートHTMLテーマリポジトリのURLが保存されました。",
|
||||
"remoteUrlSaveFailed": "リモートHTMLテーマリポジトリのURLの保存に失敗しました: {message}",
|
||||
"errorSetRemoteUrlFirst": "最初にリモートHTMLテーマリポジトリのURLを設定して保存してください。",
|
||||
"remotePresetsLoaded": "リモートHTMLテーマリストが読み込まれました。",
|
||||
"remotePresetsLoadFailed": "リモートHTMLテーマリストの読み込みに失敗しました: {message}",
|
||||
"localPresetApplyFailed": "ローカルHTMLテーマの適用に失敗しました: {message}",
|
||||
"errorMissingDownloadUrl": "リモートテーマのダウンロードURLがありません。",
|
||||
"errorPresetNameRequired": "プリセット名を空にすることはできません。",
|
||||
"remotePresetApplyFailed": "リモートHTMLテーマの適用に失敗しました: {message}",
|
||||
"customHtmlResetSuccess":"カスタムHTMLがリセットされました。",
|
||||
"searchLocalThemesPlaceholder": "ローカルテーマを検索...",
|
||||
"searchRemoteThemesPlaceholder": "リモートテーマを検索...",
|
||||
"noMatchingLocalPresetsFound": "一致するローカルテーマが見つかりませんでした",
|
||||
"noMatchingRemotePresetsFound": "一致するリモートテーマが見つかりませんでした",
|
||||
"editAsNewTooltip": "新しいカスタムテーマとして編集",
|
||||
"presetTag": "プリセット",
|
||||
"customTag": "カスタム"
|
||||
|
||||
},
|
||||
"tags": {
|
||||
"addTag": "新しいタグを追加",
|
||||
|
||||
@@ -98,7 +98,57 @@
|
||||
"terminalBgOverlayOpacityDesc": "控制背景图片上方黑色蒙版的透明度。0为完全透明,1为完全不透明。",
|
||||
"errorInvalidOpacityValue": "无效的透明度值,必须在0到1之间",
|
||||
"terminalBgOverlayOpacitySaved": "终端背景蒙版透明度已保存",
|
||||
"terminalBgOverlayOpacitySaveFailed": "终端背景蒙版透明度保存失败: {message}"
|
||||
"terminalBgOverlayOpacitySaveFailed": "终端背景蒙版透明度保存失败: {message}",
|
||||
"terminalBgDisabled": "终端背景功能已禁用。",
|
||||
"htmlBackgroundThemes": "HTML 背景主题",
|
||||
"localThemes": "本地主题",
|
||||
"remoteThemes": "远程主题",
|
||||
"newLocalPreset": "新建本地主题",
|
||||
"noLocalPresetsFound": "未找到本地 HTML 主题。",
|
||||
"errorFetchingPresetContentForEdit": "获取主题内容以供编辑失败: {message}",
|
||||
"remoteHtmlPresetsRepositoryUrl": "远程 HTML 主题仓库链接",
|
||||
"remoteRepoUrlPlaceholder": "例如:https://github.com/user/repo/tree/main/themes",
|
||||
"saveUrl": "保存链接",
|
||||
"loadRemoteThemes": "刷新",
|
||||
"pleaseSetRemoteUrl": "请先设置远程 HTML 主题仓库链接。",
|
||||
"noRemotePresetsFound": "远程仓库中未找到 HTML 主题,或链接无效。",
|
||||
"editLocalPreset": "编辑本地主题",
|
||||
"presetName": "主题名称",
|
||||
"presetNamePlaceholder": "例如:my-theme.html",
|
||||
"presetContent": "主题内容",
|
||||
"customTerminalHTMLPlaceholder": "例如:<h1>Hello</h1>",
|
||||
"errorToggleTerminalBg": "更新终端背景启用状态失败: {message}",
|
||||
"htmlPresetApplied": "HTML 主题已应用。",
|
||||
"htmlPresetApplyFailed": "应用 HTML 主题失败: {message}",
|
||||
"errorPresetContentRequired": "主题内容不能为空。",
|
||||
"localPresetUpdated": "本地 HTML 主题已更新。",
|
||||
"localPresetUpdateFailed": "更新本地 HTML 主题失败: {message}",
|
||||
"errorPresetNameAndContentRequired": "主题名称和内容均不能为空。",
|
||||
"localPresetCreated": "本地 HTML 主题已创建。",
|
||||
"localPresetCreateFailed": "创建本地 HTML 主题失败: {message}",
|
||||
"confirmDeletePreset": "确定要删除 HTML 主题 \"{name}\" 吗?",
|
||||
"localPresetDeleted": "本地 HTML 主题已删除。",
|
||||
"localPresetDeleteFailed": "删除本地 HTML 主题失败: {message}",
|
||||
"errorRemoteUrlRequired": "远程 HTML 主题仓库链接不能为空。",
|
||||
"remoteUrlSaved": "远程 HTML 主题仓库链接已保存。",
|
||||
"remoteUrlSaveFailed": "保存远程 HTML 主题仓库链接失败: {message}",
|
||||
"errorSetRemoteUrlFirst": "请先设置并保存远程 HTML 主题仓库链接。",
|
||||
"remotePresetsLoaded": "远程 HTML 主题列表已加载。",
|
||||
"remotePresetsLoadFailed": "加载远程 HTML 主题列表失败: {message}",
|
||||
"localPresetApplyFailed": "应用本地 HTML 主题失败: {message}",
|
||||
"errorMissingDownloadUrl": "远程主题下载链接缺失。",
|
||||
"remotePresetApplyFailed": "应用远程 HTML 主题失败: {message}",
|
||||
"errorPresetNameRequired": "预设名称不能为空。",
|
||||
"localPresetRenamed": "本地预设 “{oldName}” 已成功重命名为 “{newName}”。",
|
||||
"localPresetRenameFailed": "重命名本地预设失败: {message}",
|
||||
"searchLocalThemesPlaceholder": "搜索本地主题...",
|
||||
"searchRemoteThemesPlaceholder": "搜索远程主题...",
|
||||
"noMatchingLocalPresetsFound": "未找到匹配的本地主题",
|
||||
"noMatchingRemotePresetsFound": "未找到匹配的远程主题",
|
||||
"editAsNewTooltip": "编辑为新自定义主题",
|
||||
"presetTag": "预设",
|
||||
"customTag": "自定义",
|
||||
"customHtmlResetSuccess":"自定义 HTML 已重置。"
|
||||
},
|
||||
"login": {
|
||||
"title": "用户登录",
|
||||
@@ -977,6 +1027,7 @@
|
||||
"testMessageUnsaved": "为未保存的 {channelType} 配置触发的测试"
|
||||
},
|
||||
"common": {
|
||||
"apply": "应用",
|
||||
"loading": "加载中...",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
|
||||
@@ -29,7 +29,15 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
||||
// Appearance Settings State
|
||||
const appearanceSettings = ref<Partial<AppearanceSettings>>({}); // 从 API 获取的原始设置
|
||||
const allTerminalThemes = ref<TerminalTheme[]>([]); // 重命名: 存储从后端获取的所有主题
|
||||
|
||||
|
||||
// HTML Presets State
|
||||
const localHtmlPresets = ref<Array<{ name: string, type: 'preset' | 'custom' }>>([]); // Updated type
|
||||
const remoteHtmlPresets = ref<Array<{ name: string, downloadUrl?: string }>>([]);
|
||||
const remoteHtmlPresetsRepositoryUrl = ref<string | null>(null);
|
||||
const activeHtmlPresetTab = ref<'local' | 'remote'>('local');
|
||||
const isLoadingHtmlPresets = ref(false);
|
||||
const htmlPresetError = ref<string | null>(null);
|
||||
|
||||
// State for theme preview
|
||||
const isPreviewingTerminalTheme = ref(false);
|
||||
const previewTerminalThemeData = ref<ITheme | null>(null);
|
||||
@@ -153,7 +161,11 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
||||
]);
|
||||
appearanceSettings.value = settingsResponse.data;
|
||||
allTerminalThemes.value = themesResponse.data; // 更新 allTerminalThemes
|
||||
|
||||
|
||||
// Initialize remoteHtmlPresetsRepositoryUrl from loaded settings
|
||||
// Assuming backend returns it as part of AppearanceSettings
|
||||
remoteHtmlPresetsRepositoryUrl.value = appearanceSettings.value.remoteHtmlPresetsUrl || null;
|
||||
|
||||
// 应用加载的 UI 主题
|
||||
applyUiTheme(currentUiTheme.value);
|
||||
// 应用背景
|
||||
@@ -569,7 +581,146 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
||||
isPreviewingTerminalTheme.value = false;
|
||||
console.log('[AppearanceStore] Stopped terminal theme preview.');
|
||||
}
|
||||
|
||||
// --- HTML Preset Actions ---
|
||||
async function fetchLocalHtmlPresets() {
|
||||
isLoadingHtmlPresets.value = true;
|
||||
htmlPresetError.value = null;
|
||||
try {
|
||||
// Updated to expect type information from the backend
|
||||
const response = await apiClient.get<Array<{ name: string, type: 'preset' | 'custom' }>>('/appearance/html-presets/local');
|
||||
localHtmlPresets.value = response.data;
|
||||
} catch (err: any) {
|
||||
console.error('获取本地 HTML 主题列表失败:', err);
|
||||
htmlPresetError.value = err.response?.data?.message || err.message || '获取本地 HTML 主题列表失败';
|
||||
localHtmlPresets.value = [];
|
||||
} finally {
|
||||
isLoadingHtmlPresets.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getLocalHtmlPresetContent(name: string): Promise<string> {
|
||||
try {
|
||||
const response = await apiClient.get<string>(`/appearance/html-presets/local/${name}`, { transformResponse: (res) => res }); // Expecting plain text
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
console.error(`获取本地 HTML 主题 '${name}' 内容失败:`, err);
|
||||
throw new Error(err.response?.data?.message || err.message || `获取主题 '${name}' 内容失败`);
|
||||
}
|
||||
}
|
||||
|
||||
async function createLocalHtmlPreset(name: string, content: string) {
|
||||
try {
|
||||
await apiClient.post('/appearance/html-presets/local', { name, content });
|
||||
await fetchLocalHtmlPresets(); // Refresh list
|
||||
} catch (err: any) {
|
||||
console.error('创建本地 HTML 主题失败:', err);
|
||||
throw new Error(err.response?.data?.message || err.message || '创建本地 HTML 主题失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateLocalHtmlPreset(name: string, content: string) {
|
||||
try {
|
||||
await apiClient.put(`/appearance/html-presets/local/${name}`, { content });
|
||||
// Optionally refresh list or update item if content is stored locally too
|
||||
} catch (err: any) {
|
||||
console.error(`更新本地 HTML 主题 '${name}' 失败:`, err);
|
||||
throw new Error(err.response?.data?.message || err.message || `更新主题 '${name}' 失败`);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteLocalHtmlPreset(name: string) {
|
||||
try {
|
||||
await apiClient.delete(`/appearance/html-presets/local/${name}`);
|
||||
await fetchLocalHtmlPresets(); // Refresh list
|
||||
} catch (err: any) {
|
||||
console.error(`删除本地 HTML 主题 '${name}' 失败:`, err);
|
||||
throw new Error(err.response?.data?.message || err.message || `删除主题 '${name}' 失败`);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRemoteHtmlPresetsRepositoryUrl() {
|
||||
isLoadingHtmlPresets.value = true; // Use main loading or a specific one
|
||||
htmlPresetError.value = null;
|
||||
try {
|
||||
const response = await apiClient.get<{ url: string | null }>('/appearance/html-presets/remote/repository-url');
|
||||
remoteHtmlPresetsRepositoryUrl.value = response.data.url;
|
||||
// Also update in appearanceSettings to persist if this API also saves
|
||||
if (appearanceSettings.value && response.data.url !== undefined) {
|
||||
// This assumes the backend GET doesn't modify, so we only update local store state from GET
|
||||
// If this GET /is/ meant to also fetch latest from settings and that's the source of truth,
|
||||
// then updateAppearanceSettings might not be needed here.
|
||||
// The plan says "或直接从 settings.value 读取并更新到 remoteHtmlPresetsRepositoryUrl state"
|
||||
// and for update: "成功后更新 store state".
|
||||
// So this action fetches and updates the store's reactive ref.
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('获取远程 HTML 主题仓库链接失败:', err);
|
||||
htmlPresetError.value = err.response?.data?.message || err.message || '获取远程仓库链接失败';
|
||||
} finally {
|
||||
isLoadingHtmlPresets.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRemoteHtmlPresetsRepositoryUrl(url: string) {
|
||||
try {
|
||||
await apiClient.put('/appearance/html-presets/remote/repository-url', { url });
|
||||
remoteHtmlPresetsRepositoryUrl.value = url; // Update local state on success
|
||||
// Persist this change in the main appearance settings object as well if needed
|
||||
await updateAppearanceSettings({ remoteHtmlPresetsUrl: url });
|
||||
} catch (err: any) {
|
||||
console.error('更新远程 HTML 主题仓库链接失败:', err);
|
||||
throw new Error(err.response?.data?.message || err.message || '更新远程仓库链接失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRemoteHtmlPresets(repoUrlParam?: string) {
|
||||
isLoadingHtmlPresets.value = true;
|
||||
htmlPresetError.value = null;
|
||||
const urlToFetch = repoUrlParam || remoteHtmlPresetsRepositoryUrl.value;
|
||||
if (!urlToFetch) {
|
||||
htmlPresetError.value = '远程仓库链接未设置';
|
||||
isLoadingHtmlPresets.value = false;
|
||||
remoteHtmlPresets.value = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// The API is GET /api/v1/appearance/html-presets/remote/list
|
||||
// It might take repoUrl as a query param if not using the saved one.
|
||||
// The plan states: `repoUrl` (可选, 如果不提供则使用已保存的链接)
|
||||
// So, if repoUrlParam is provided, it should be sent.
|
||||
const params: { repoUrl?: string } = {};
|
||||
if (repoUrlParam) { // Only send if explicitly passed to this action
|
||||
params.repoUrl = repoUrlParam;
|
||||
}
|
||||
|
||||
const response = await apiClient.get<Array<{ name: string, downloadUrl?: string }>>('/appearance/html-presets/remote/list', { params });
|
||||
remoteHtmlPresets.value = response.data;
|
||||
} catch (err: any) {
|
||||
console.error('获取远程 HTML 主题列表失败:', err);
|
||||
htmlPresetError.value = err.response?.data?.message || err.message || '获取远程主题列表失败';
|
||||
remoteHtmlPresets.value = [];
|
||||
} finally {
|
||||
isLoadingHtmlPresets.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getRemoteHtmlPresetContent(fileUrl: string): Promise<string> {
|
||||
try {
|
||||
// Expecting plain text response
|
||||
const response = await apiClient.get<string>(`/appearance/html-presets/remote/content`, { params: { fileUrl }, transformResponse: (res) => res });
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
console.error(`获取远程 HTML 主题内容 (URL: ${fileUrl}) 失败:`, err);
|
||||
throw new Error(err.response?.data?.message || err.message || '获取远程主题内容失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function applyHtmlPreset(htmlContent: string) {
|
||||
// This action internally calls setTerminalCustomHTML
|
||||
await setTerminalCustomHTML(htmlContent);
|
||||
}
|
||||
|
||||
// --- Helper Functions ---
|
||||
/**
|
||||
* 将 UI 主题 (CSS 变量) 应用到文档根元素。
|
||||
@@ -706,5 +857,23 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
||||
// Visibility control
|
||||
isStyleCustomizerVisible,
|
||||
toggleStyleCustomizer,
|
||||
|
||||
// HTML Presets State & Actions
|
||||
localHtmlPresets,
|
||||
remoteHtmlPresets,
|
||||
remoteHtmlPresetsRepositoryUrl,
|
||||
activeHtmlPresetTab,
|
||||
isLoadingHtmlPresets,
|
||||
htmlPresetError,
|
||||
fetchLocalHtmlPresets,
|
||||
getLocalHtmlPresetContent,
|
||||
createLocalHtmlPreset,
|
||||
updateLocalHtmlPreset,
|
||||
deleteLocalHtmlPreset,
|
||||
fetchRemoteHtmlPresetsRepositoryUrl,
|
||||
updateRemoteHtmlPresetsRepositoryUrl,
|
||||
fetchRemoteHtmlPresets,
|
||||
getRemoteHtmlPresetContent,
|
||||
applyHtmlPreset,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface AppearanceSettings {
|
||||
terminalBackgroundEnabled?: boolean; // 终端背景是否启用
|
||||
terminalBackgroundOverlayOpacity?: number; // 终端背景蒙版透明度 (0-1)
|
||||
terminal_custom_html?: string | null; // 终端自定义 HTML
|
||||
remoteHtmlPresetsUrl?: string | null; // 远程 HTML 主题仓库链接
|
||||
}
|
||||
|
||||
// 前端用于更新外观设置的数据结构 (对应 API 请求体)
|
||||
|
||||
Reference in New Issue
Block a user