308 lines
7.1 KiB
Vue
308 lines
7.1 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
|
import { storeToRefs } from 'pinia';
|
|
import { useWindowsStore } from '../stores/windows';
|
|
import { useAppsStore } from '../stores/apps';
|
|
import { useUIStore } from '../stores/ui';
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
const { t, locale, setLocale } = useI18n();
|
|
const windowsStore = useWindowsStore();
|
|
const appsStore = useAppsStore();
|
|
const uiStore = useUIStore();
|
|
|
|
const { windows } = storeToRefs(windowsStore);
|
|
const { appInstances } = storeToRefs(appsStore);
|
|
const { focusWindow } = windowsStore;
|
|
const { focusAppInstance } = appsStore;
|
|
const { toggleStartMenu } = uiStore;
|
|
|
|
// --- Datetime Logic ---
|
|
const dateString = ref('');
|
|
const timeString = ref('');
|
|
let timer: number;
|
|
|
|
const updateTime = () => {
|
|
const now = new Date();
|
|
dateString.value = now.toLocaleDateString(locale.value, { weekday: 'short', month: 'short', day: 'numeric' });
|
|
timeString.value = now.toLocaleTimeString(locale.value, { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
};
|
|
|
|
watch(locale, updateTime);
|
|
|
|
onMounted(() => {
|
|
updateTime();
|
|
timer = window.setInterval(updateTime, 1000);
|
|
});
|
|
|
|
// --- Language Switcher Logic ---
|
|
const isLanguageMenuOpen = ref(false);
|
|
const languageSwitcherWrapper = ref<HTMLElement | null>(null);
|
|
const availableLanguages = computed(() => [
|
|
{ key: 'en', label: 'English', display: 'EN' },
|
|
{ key: 'zh', label: '繁體中文', display: '注' },
|
|
]);
|
|
|
|
const currentLanguageDisplay = computed(() => {
|
|
const current = availableLanguages.value.find(lang => lang.key === locale.value);
|
|
return current?.display || '注';
|
|
});
|
|
|
|
function toggleLanguageMenu() {
|
|
isLanguageMenuOpen.value = !isLanguageMenuOpen.value;
|
|
}
|
|
|
|
function selectLanguage(lang: 'en' | 'zh') {
|
|
setLocale(lang);
|
|
isLanguageMenuOpen.value = false;
|
|
}
|
|
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (languageSwitcherWrapper.value && !languageSwitcherWrapper.value.contains(event.target as Node)) {
|
|
isLanguageMenuOpen.value = false;
|
|
}
|
|
};
|
|
|
|
watch(isLanguageMenuOpen, (isOpen) => {
|
|
if (isOpen) {
|
|
document.addEventListener('click', handleClickOutside);
|
|
} else {
|
|
document.removeEventListener('click', handleClickOutside);
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
clearInterval(timer);
|
|
document.removeEventListener('click', handleClickOutside);
|
|
});
|
|
|
|
function handleTaskbarButtonClick(windowId: string) {
|
|
// Try to focus app instance first, then regular window
|
|
const appInstance = appsStore.getAppInstanceById(windowId);
|
|
if (appInstance) {
|
|
focusAppInstance(windowId);
|
|
} else {
|
|
focusWindow(windowId);
|
|
}
|
|
}
|
|
|
|
// Combined taskbar items (windows + app instances)
|
|
const taskbarItems = computed(() => {
|
|
const items = [];
|
|
|
|
// Add regular windows
|
|
windows.value.forEach(window => {
|
|
items.push({
|
|
id: window.id,
|
|
title: window.title,
|
|
isActive: window.isFocused,
|
|
type: 'window'
|
|
});
|
|
});
|
|
|
|
// Add app instances
|
|
appInstances.value.forEach(instance => {
|
|
const appInfo = appsStore.getAppById(instance.appId);
|
|
items.push({
|
|
id: instance.id,
|
|
title: instance.title,
|
|
isActive: instance.isFocused,
|
|
type: 'app',
|
|
icon: appInfo?.icon
|
|
});
|
|
});
|
|
|
|
return items;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="taskbar">
|
|
<button @click="toggleStartMenu" class="start-button">🚀</button>
|
|
<div class="window-list">
|
|
<button
|
|
v-for="item in taskbarItems"
|
|
:key="item.id"
|
|
class="taskbar-item"
|
|
:class="{ 'is-active': item.isActive }"
|
|
@click="handleTaskbarButtonClick(item.id)"
|
|
>
|
|
<span v-if="item.icon" class="taskbar-icon">{{ item.icon }}</span>
|
|
<span class="taskbar-title">{{ item.title }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="taskbar-right-controls">
|
|
<div class="language-switcher-wrapper" ref="languageSwitcherWrapper">
|
|
<button @click="toggleLanguageMenu" class="language-switcher">
|
|
{{ currentLanguageDisplay }}
|
|
</button>
|
|
<div v-if="isLanguageMenuOpen" class="language-menu">
|
|
<ul>
|
|
<li v-for="lang in availableLanguages" :key="lang.key" @click="selectLanguage(lang.key as 'en' | 'zh')">
|
|
<span class="checkmark" :style="{ visibility: locale === lang.key ? 'visible' : 'hidden' }">✓</span>
|
|
<span>{{ lang.label }}</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="datetime">
|
|
<span>{{ dateString }}</span>
|
|
<span>{{ timeString }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.taskbar {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 22px;
|
|
background-color: var(--taskbar-background);
|
|
backdrop-filter: blur(10px);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 8px;
|
|
z-index: var(--z-taskbar);
|
|
color: var(--taskbar-item-text-color);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.start-button {
|
|
background: none;
|
|
border: none;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
padding: 0 8px;
|
|
color: inherit;
|
|
margin-right: 4px;
|
|
line-height: 22px;
|
|
}
|
|
|
|
.window-list {
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
display: flex;
|
|
gap: 4px;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
/* Subtle scrollbar styling */
|
|
.window-list::-webkit-scrollbar {
|
|
height: 2px;
|
|
}
|
|
|
|
.window-list::-webkit-scrollbar-thumb {
|
|
background-color: rgba(255, 255, 255, 0.3);
|
|
border-radius: 1px;
|
|
}
|
|
|
|
.taskbar-item {
|
|
background-color: var(--taskbar-item-background);
|
|
color: inherit;
|
|
border: 1px solid var(--taskbar-item-border-color);
|
|
border-radius: 4px;
|
|
padding: 2px 6px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s ease;
|
|
white-space: nowrap;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.taskbar-icon {
|
|
font-size: 10px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.taskbar-title {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.taskbar-item:hover {
|
|
background-color: var(--taskbar-item-background-hover);
|
|
}
|
|
|
|
.taskbar-item.is-active {
|
|
background-color: var(--taskbar-item-background-active);
|
|
font-weight: bold;
|
|
}
|
|
|
|
.taskbar-right-controls {
|
|
margin-left: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0; /* Prevent right controls from shrinking */
|
|
}
|
|
|
|
.language-switcher-wrapper {
|
|
position: relative;
|
|
}
|
|
|
|
.language-switcher {
|
|
background: none;
|
|
border: none;
|
|
color: inherit;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
padding: 0 4px;
|
|
min-width: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.datetime {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.language-menu {
|
|
position: absolute;
|
|
top: calc(100% + 4px);
|
|
right: 0;
|
|
width: 160px;
|
|
background-color: var(--start-menu-background);
|
|
border: 1px solid var(--start-menu-border-color);
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
padding: 4px;
|
|
z-index: 10001;
|
|
}
|
|
|
|
.language-menu ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.language-menu li {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: background-color 0.15s ease-in-out;
|
|
}
|
|
|
|
.language-menu li:hover {
|
|
background-color: var(--taskbar-item-background-hover);
|
|
}
|
|
|
|
.checkmark {
|
|
width: 16px;
|
|
text-align: center;
|
|
margin-right: 4px;
|
|
}
|
|
</style>
|