windows/components/Taskbar.vue

308 lines
7.1 KiB
Vue
Raw Normal View History

2025-09-23 16:43:57 +00:00
<script setup lang="ts">
2025-09-25 02:10:57 +00:00
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
2025-09-23 16:43:57 +00:00
import { storeToRefs } from 'pinia';
import { useWindowsStore } from '../stores/windows';
2025-09-25 05:38:59 +00:00
import { useAppsStore } from '../stores/apps';
2025-09-23 16:43:57 +00:00
import { useUIStore } from '../stores/ui';
2025-09-25 02:10:57 +00:00
import { useI18n } from 'vue-i18n';
2025-09-23 16:43:57 +00:00
2025-09-25 02:56:23 +00:00
const { t, locale, setLocale } = useI18n();
2025-09-23 16:43:57 +00:00
const windowsStore = useWindowsStore();
2025-09-25 05:38:59 +00:00
const appsStore = useAppsStore();
2025-09-23 16:43:57 +00:00
const uiStore = useUIStore();
const { windows } = storeToRefs(windowsStore);
2025-09-25 05:38:59 +00:00
const { appInstances } = storeToRefs(appsStore);
2025-09-23 16:43:57 +00:00
const { focusWindow } = windowsStore;
2025-09-25 05:38:59 +00:00
const { focusAppInstance } = appsStore;
2025-09-23 16:43:57 +00:00
const { toggleStartMenu } = uiStore;
2025-09-24 09:45:36 +00:00
// --- Datetime Logic ---
const dateString = ref('');
const timeString = ref('');
let timer: number;
const updateTime = () => {
const now = new Date();
2025-09-25 02:10:57 +00:00
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 });
2025-09-24 09:45:36 +00:00
};
2025-09-25 02:10:57 +00:00
watch(locale, updateTime);
2025-09-24 09:45:36 +00:00
onMounted(() => {
updateTime();
timer = window.setInterval(updateTime, 1000);
});
2025-09-25 02:56:23 +00:00
// --- Language Switcher Logic ---
const isLanguageMenuOpen = ref(false);
const languageSwitcherWrapper = ref<HTMLElement | null>(null);
const availableLanguages = computed(() => [
{ key: 'en', label: 'English', display: 'EN' },
2025-09-25 05:38:59 +00:00
{ key: 'zh', label: '繁體中文', display: '注' },
2025-09-25 02:10:57 +00:00
]);
2025-09-24 09:45:36 +00:00
2025-09-25 02:56:23 +00:00
const currentLanguageDisplay = computed(() => {
const current = availableLanguages.value.find(lang => lang.key === locale.value);
2025-09-25 05:38:59 +00:00
return current?.display || '注';
2025-09-25 02:56:23 +00:00
});
function toggleLanguageMenu() {
isLanguageMenuOpen.value = !isLanguageMenuOpen.value;
2025-09-24 09:45:36 +00:00
}
2025-09-25 02:56:23 +00:00
function selectLanguage(lang: 'en' | 'zh') {
setLocale(lang);
isLanguageMenuOpen.value = false;
2025-09-24 09:45:36 +00:00
}
const handleClickOutside = (event: MouseEvent) => {
2025-09-25 02:56:23 +00:00
if (languageSwitcherWrapper.value && !languageSwitcherWrapper.value.contains(event.target as Node)) {
isLanguageMenuOpen.value = false;
2025-09-24 09:45:36 +00:00
}
};
2025-09-25 02:56:23 +00:00
watch(isLanguageMenuOpen, (isOpen) => {
2025-09-24 09:45:36 +00:00
if (isOpen) {
document.addEventListener('click', handleClickOutside);
} else {
document.removeEventListener('click', handleClickOutside);
}
});
onUnmounted(() => {
clearInterval(timer);
document.removeEventListener('click', handleClickOutside);
});
2025-09-23 16:43:57 +00:00
function handleTaskbarButtonClick(windowId: string) {
2025-09-25 05:38:59 +00:00
// Try to focus app instance first, then regular window
const appInstance = appsStore.getAppInstanceById(windowId);
if (appInstance) {
focusAppInstance(windowId);
} else {
focusWindow(windowId);
}
2025-09-23 16:43:57 +00:00
}
2025-09-25 05:38:59 +00:00
// 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,
2025-09-25 09:21:54 +00:00
title: appInfo ? t(`apps.${appInfo.name}`) : instance.title,
2025-09-25 05:38:59 +00:00
isActive: instance.isFocused,
type: 'app',
icon: appInfo?.icon
});
});
return items;
});
2025-09-23 16:43:57 +00:00
</script>
<template>
<div class="taskbar">
<button @click="toggleStartMenu" class="start-button">🚀</button>
<div class="window-list">
<button
2025-09-25 05:38:59 +00:00
v-for="item in taskbarItems"
:key="item.id"
2025-09-23 16:43:57 +00:00
class="taskbar-item"
2025-09-25 05:38:59 +00:00
:class="{ 'is-active': item.isActive }"
@click="handleTaskbarButtonClick(item.id)"
2025-09-23 16:43:57 +00:00
>
2025-09-25 05:38:59 +00:00
<span v-if="item.icon" class="taskbar-icon">{{ item.icon }}</span>
<span class="taskbar-title">{{ item.title }}</span>
2025-09-23 16:43:57 +00:00
</button>
</div>
2025-09-24 09:45:36 +00:00
<div class="taskbar-right-controls">
2025-09-25 02:56:23 +00:00
<div class="language-switcher-wrapper" ref="languageSwitcherWrapper">
<button @click="toggleLanguageMenu" class="language-switcher">
{{ currentLanguageDisplay }}
2025-09-24 09:45:36 +00:00
</button>
2025-09-25 02:56:23 +00:00
<div v-if="isLanguageMenuOpen" class="language-menu">
2025-09-24 09:45:36 +00:00
<ul>
2025-09-25 02:56:23 +00:00
<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>
2025-09-24 09:45:36 +00:00
</li>
</ul>
</div>
</div>
<div class="datetime">
<span>{{ dateString }}</span>
<span>{{ timeString }}</span>
</div>
</div>
2025-09-23 16:43:57 +00:00
</div>
</template>
<style scoped>
.taskbar {
position: fixed;
2025-09-24 09:45:36 +00:00
top: 0;
2025-09-23 16:43:57 +00:00
left: 0;
right: 0;
2025-09-24 09:45:36 +00:00
height: 22px;
2025-09-23 16:43:57 +00:00
background-color: var(--taskbar-background);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
2025-09-24 09:45:36 +00:00
padding: 0 8px;
2025-09-23 16:43:57 +00:00
z-index: var(--z-taskbar);
2025-09-24 09:45:36 +00:00
color: var(--taskbar-item-text-color);
font-size: 12px;
2025-09-23 16:43:57 +00:00
}
.start-button {
background: none;
border: none;
2025-09-24 09:45:36 +00:00
font-size: 14px;
2025-09-23 16:43:57 +00:00
cursor: pointer;
2025-09-24 09:45:36 +00:00
padding: 0 8px;
color: inherit;
margin-right: 4px;
line-height: 22px;
2025-09-23 16:43:57 +00:00
}
.window-list {
2025-09-24 09:45:36 +00:00
flex: 1 1 0;
min-width: 0;
2025-09-23 16:43:57 +00:00
display: flex;
2025-09-24 09:45:36 +00:00
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;
2025-09-23 16:43:57 +00:00
}
.taskbar-item {
background-color: var(--taskbar-item-background);
2025-09-24 09:45:36 +00:00
color: inherit;
2025-09-23 16:43:57 +00:00
border: 1px solid var(--taskbar-item-border-color);
2025-09-24 09:45:36 +00:00
border-radius: 4px;
padding: 2px 6px;
font-size: 12px;
2025-09-23 16:43:57 +00:00
cursor: pointer;
transition: background-color 0.2s ease;
2025-09-24 09:45:36 +00:00
white-space: nowrap;
2025-09-25 05:38:59 +00:00
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;
2025-09-23 16:43:57 +00:00
}
.taskbar-item:hover {
background-color: var(--taskbar-item-background-hover);
}
.taskbar-item.is-active {
background-color: var(--taskbar-item-background-active);
font-weight: bold;
}
2025-09-24 09:45:36 +00:00
.taskbar-right-controls {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0; /* Prevent right controls from shrinking */
}
2025-09-25 02:56:23 +00:00
.language-switcher-wrapper {
2025-09-24 09:45:36 +00:00
position: relative;
}
2025-09-25 02:56:23 +00:00
.language-switcher {
2025-09-24 09:45:36 +00:00
background: none;
border: none;
color: inherit;
font-size: 12px;
font-weight: 500;
cursor: pointer;
padding: 0 4px;
2025-09-25 02:56:23 +00:00
min-width: 20px;
text-align: center;
2025-09-24 09:45:36 +00:00
}
.datetime {
display: flex;
align-items: center;
gap: 8px;
padding: 0 4px;
}
2025-09-25 02:56:23 +00:00
.language-menu {
2025-09-24 09:45:36 +00:00
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;
}
2025-09-25 02:56:23 +00:00
.language-menu ul {
2025-09-24 09:45:36 +00:00
list-style: none;
padding: 0;
margin: 0;
}
2025-09-25 02:56:23 +00:00
.language-menu li {
2025-09-24 09:45:36 +00:00
display: flex;
align-items: center;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.15s ease-in-out;
}
2025-09-25 02:56:23 +00:00
.language-menu li:hover {
2025-09-24 09:45:36 +00:00
background-color: var(--taskbar-item-background-hover);
}
.checkmark {
width: 16px;
text-align: center;
margin-right: 4px;
}
2025-09-23 16:43:57 +00:00
</style>