fix: language
This commit is contained in:
parent
33835376ba
commit
cfa76cab17
314
app/app.vue
314
app/app.vue
|
@ -2,76 +2,76 @@
|
||||||
import Taskbar from '~/components/Taskbar.vue';
|
import Taskbar from '~/components/Taskbar.vue';
|
||||||
import AboutMeWindow from '~/components/AboutMeWindow.vue';
|
import AboutMeWindow from '~/components/AboutMeWindow.vue';
|
||||||
import TerminalWindow from '~/components/TerminalWindow.vue';
|
import TerminalWindow from '~/components/TerminalWindow.vue';
|
||||||
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue';
|
import WinampWindow from '~/components/WinampWindow.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { ref, reactive, onMounted, computed, provide, onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
// --- Data Definitions ---
|
||||||
|
|
||||||
// Reactive array to hold all window states
|
// Reactive array to hold all window states
|
||||||
const windows = reactive([]);
|
const windows = reactive([]);
|
||||||
|
|
||||||
// Keep track of the highest z-index to ensure new/focused windows are on top
|
// Keep track of the highest z-index
|
||||||
const currentZIndex = ref(100);
|
const currentZIndex = ref(100);
|
||||||
|
|
||||||
// Function to generate a unique ID for each window instance
|
// Desktop icons container position
|
||||||
const generateUniqueId = () => {
|
const iconsContainerX = ref(0);
|
||||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
const iconsContainerY = ref(0);
|
||||||
};
|
|
||||||
|
|
||||||
// Load window states from localStorage on mount
|
// Dragging state for desktop icons container
|
||||||
onMounted(() => {
|
const isIconsContainerDragging = ref(false);
|
||||||
const savedWindows = localStorage.getItem('windows');
|
const iconsContainerStartX = ref(0);
|
||||||
if (savedWindows) {
|
const iconsContainerStartY = ref(0);
|
||||||
const parsedWindows = JSON.parse(savedWindows);
|
const initialIconsContainerX = ref(0);
|
||||||
parsedWindows.forEach(win => {
|
const initialIconsContainerY = ref(0);
|
||||||
// Ensure z-index is properly managed on load
|
|
||||||
if (win.zIndex > currentZIndex.value) {
|
|
||||||
currentZIndex.value = win.zIndex;
|
|
||||||
}
|
|
||||||
windows.push(win);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save window states to localStorage whenever windows array changes
|
// Define Start Menu Items with icons and actions
|
||||||
const saveWindowsToLocalStorage = () => {
|
const startMenuItems = ref([
|
||||||
localStorage.setItem('windows', JSON.stringify(windows));
|
{
|
||||||
};
|
labelKey: 'terminal',
|
||||||
|
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGQ9Ik0yMiAxNlY4QzIyIDYuODk1NDMgMjEuMTA0NiA2IDIwIDZIMTRMMTIgNEg0QzIuODk1NDMgNCAyIDQuODk1NDMgMiA2VjE4QzIgMTkuMTA0NiAyLjg5NTQzIDIwIDQgMjBIMjBDMjEuMTA0NiAyMCAyMiAxOS4xMDQ2IDIyIDE4VjE2WiIgZmlsbD0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0iTTggMTJMMTAgMTQuNUwxMiAxMkwxMCA5LjVMOCAxMloiIGZpbGw9IndoaXRlIi8+CiAgICA8cGF0aCBkPSJNMTAgMTZIMTRWMThIMTBWMTZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4=',
|
||||||
|
action: 'openTerminal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: 'about_me',
|
||||||
|
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIiBmaWxsPSIjRkZGRjAwIi8+CiAgICA8cGF0aCBkPSJNMTEgN0gxM1Y5SDExVjdaTTExIDExSDEzVjE3SDExVjExWiIgZmlsbD0iYmxhY2siLz4KPC9zdmc+Cg==',
|
||||||
|
action: 'openAboutMe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: 'close_all_windows',
|
||||||
|
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGQ9Ik0xOC41IDUuNUw1LjUgMTguNU01LjUgNS41TDE4LjUgMTguNSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=',
|
||||||
|
action: 'closeAllWindows',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// --- Window Management Functions ---
|
||||||
|
|
||||||
|
const generateUniqueId = () => Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||||
|
|
||||||
|
const saveWindowsToLocalStorage = () => localStorage.setItem('windows', JSON.stringify(windows));
|
||||||
|
|
||||||
// Function to open a new window or bring an existing one to front
|
|
||||||
const openWindow = (type, titleKey, initialWidth = 600, initialHeight = 400) => {
|
const openWindow = (type, titleKey, initialWidth = 600, initialHeight = 400) => {
|
||||||
const existingWindow = windows.find(w => w.type === type);
|
const existingWindow = windows.find(w => w.type === type);
|
||||||
if (existingWindow) {
|
if (existingWindow) {
|
||||||
bringWindowToFront(existingWindow.id);
|
bringWindowToFront(existingWindow.id);
|
||||||
if (existingWindow.isMinimized) {
|
if (existingWindow.isMinimized) restoreWindow(existingWindow.id);
|
||||||
restoreWindow(existingWindow.id);
|
existingWindow.isVisible = true;
|
||||||
}
|
|
||||||
existingWindow.isVisible = true; // Ensure it's visible if it was closed
|
|
||||||
} else {
|
} else {
|
||||||
currentZIndex.value++;
|
currentZIndex.value++;
|
||||||
// Calculate initial position to center the window within the desktop content area
|
|
||||||
const desktopWidth = window.innerWidth;
|
const desktopWidth = window.innerWidth;
|
||||||
const desktopHeight = window.innerHeight - 40; // Minus taskbar height
|
const desktopHeight = window.innerHeight - 40;
|
||||||
const initialX = (desktopWidth - initialWidth) / 2;
|
const initialX = (desktopWidth - initialWidth) / 2;
|
||||||
const initialY = (desktopHeight - initialHeight) / 2 + 40; // Offset by taskbar height
|
const initialY = (desktopHeight - initialHeight) / 2 + 40;
|
||||||
|
|
||||||
const newWindow = {
|
windows.push({
|
||||||
id: generateUniqueId(),
|
id: generateUniqueId(),
|
||||||
type,
|
type, title: titleKey, isVisible: true, isMinimized: false,
|
||||||
title: titleKey, // Store the translation key here
|
x: initialX, y: initialY, width: initialWidth, height: initialHeight,
|
||||||
isVisible: true,
|
|
||||||
isMinimized: false,
|
|
||||||
x: initialX,
|
|
||||||
y: initialY,
|
|
||||||
width: initialWidth,
|
|
||||||
height: initialHeight,
|
|
||||||
zIndex: currentZIndex.value,
|
zIndex: currentZIndex.value,
|
||||||
};
|
});
|
||||||
windows.push(newWindow);
|
|
||||||
saveWindowsToLocalStorage();
|
|
||||||
}
|
}
|
||||||
|
saveWindowsToLocalStorage();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to close a window
|
|
||||||
const closeWindow = (id) => {
|
const closeWindow = (id) => {
|
||||||
const index = windows.findIndex(w => w.id === id);
|
const index = windows.findIndex(w => w.id === id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
|
@ -80,138 +80,256 @@ const closeWindow = (id) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to close all windows
|
|
||||||
const closeAllWindows = () => {
|
const closeAllWindows = () => {
|
||||||
windows.splice(0, windows.length); // Clear the array
|
windows.length = 0;
|
||||||
saveWindowsToLocalStorage();
|
saveWindowsToLocalStorage();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to minimize a window
|
|
||||||
const minimizeWindow = (id) => {
|
const minimizeWindow = (id) => {
|
||||||
const windowToMinimize = windows.find(w => w.id === id);
|
const win = windows.find(w => w.id === id);
|
||||||
if (windowToMinimize) {
|
if (win) {
|
||||||
windowToMinimize.isMinimized = true;
|
console.log(`Minimizing window ${id}. Before: isMinimized=${win.isMinimized}`);
|
||||||
|
win.isMinimized = true;
|
||||||
|
console.log(`Minimizing window ${id}. After: isMinimized=${win.isMinimized}`);
|
||||||
saveWindowsToLocalStorage();
|
saveWindowsToLocalStorage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to restore a minimized window
|
|
||||||
const restoreWindow = (id) => {
|
const restoreWindow = (id) => {
|
||||||
const windowToRestore = windows.find(w => w.id === id);
|
const win = windows.find(w => w.id === id);
|
||||||
if (windowToRestore) {
|
if (win) {
|
||||||
windowToRestore.isMinimized = false;
|
console.log(`Restoring window ${id}. Before: isMinimized=${win.isMinimized}`);
|
||||||
|
win.isMinimized = false;
|
||||||
|
console.log(`Restoring window ${id}. After: isMinimized=${win.isMinimized}`);
|
||||||
bringWindowToFront(id);
|
bringWindowToFront(id);
|
||||||
saveWindowsToLocalStorage();
|
saveWindowsToLocalStorage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to bring a window to the front (update z-index)
|
|
||||||
const bringWindowToFront = (id) => {
|
const bringWindowToFront = (id) => {
|
||||||
const windowToFront = windows.find(w => w.id === id);
|
const win = windows.find(w => w.id === id);
|
||||||
if (windowToFront) {
|
if (win) {
|
||||||
currentZIndex.value++;
|
currentZIndex.value++;
|
||||||
windowToFront.zIndex = currentZIndex.value;
|
win.zIndex = currentZIndex.value;
|
||||||
|
console.log(`Bringing window ${id} to front. New zIndex: ${win.zIndex}`);
|
||||||
saveWindowsToLocalStorage();
|
saveWindowsToLocalStorage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to update window position (for dragging)
|
const updateWindowPosition = (id, { x, y }) => {
|
||||||
const updateWindowPosition = (id, newX, newY) => {
|
const win = windows.find(w => w.id === id);
|
||||||
const windowToUpdate = windows.find(w => w.id === id);
|
if (win) {
|
||||||
if (windowToUpdate) {
|
win.x = x;
|
||||||
windowToUpdate.x = newX;
|
win.y = y;
|
||||||
windowToUpdate.y = newY;
|
|
||||||
saveWindowsToLocalStorage();
|
saveWindowsToLocalStorage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle opening About Me window from Taskbar
|
const updateWindowDimensions = (id, { width, height }) => {
|
||||||
const openAboutMeWindow = () => {
|
const win = windows.find(w => w.id === id);
|
||||||
openWindow('about-me', 'about_me'); // Pass the translation key
|
if (win) {
|
||||||
|
win.width = width;
|
||||||
|
win.height = height;
|
||||||
|
saveWindowsToLocalStorage();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openTerminalWindow = () => {
|
// --- Action Handlers ---
|
||||||
openWindow('terminal', 'terminal');
|
|
||||||
|
const openAboutMeWindow = () => openWindow('about-me', 'about_me');
|
||||||
|
const openTerminalWindow = () => openWindow('terminal', 'terminal');
|
||||||
|
const openWinampWindow = () => {
|
||||||
|
console.log('Opening Winamp window via provide/inject.');
|
||||||
|
openWindow('winamp', 'Winamp', 400, 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Computed property for minimized windows
|
// Provide the openWinampWindow function
|
||||||
const minimizedWindows = computed(() => {
|
provide('openWinamp', openWinampWindow);
|
||||||
return windows.filter(win => win.isMinimized);
|
|
||||||
|
const menuActions = {
|
||||||
|
openTerminal: openTerminalWindow,
|
||||||
|
openAboutMe: openAboutMeWindow,
|
||||||
|
closeAllWindows: closeAllWindows,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuAction = (action) => {
|
||||||
|
const func = menuActions[action];
|
||||||
|
if (func) func();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Desktop Icons Container Dragging Functions ---
|
||||||
|
const handleIconsContainerMouseDown = (e) => {
|
||||||
|
// Only drag if not clicking on a window or taskbar
|
||||||
|
if (e.target.closest('.window-wrapper') || e.target.closest('.taskbar')) {
|
||||||
|
console.log('Clicked on a window or taskbar, not dragging desktop icons container.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isIconsContainerDragging.value = true;
|
||||||
|
iconsContainerStartX.value = e.clientX;
|
||||||
|
iconsContainerStartY.value = e.clientY;
|
||||||
|
initialIconsContainerX.value = iconsContainerX.value;
|
||||||
|
initialIconsContainerY.value = iconsContainerY.value;
|
||||||
|
console.log(`Icons container drag started. Initial pos: (${initialIconsContainerX.value}, ${initialIconsContainerY.value})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIconsContainerMouseMove = (e) => {
|
||||||
|
if (!isIconsContainerDragging.value) return;
|
||||||
|
const dx = e.clientX - iconsContainerStartX.value;
|
||||||
|
const dy = e.clientY - iconsContainerStartY.value;
|
||||||
|
|
||||||
|
let newX = initialIconsContainerX.value + dx;
|
||||||
|
let newY = initialIconsContainerY.value + dy;
|
||||||
|
|
||||||
|
// Get desktop content dimensions
|
||||||
|
const desktopContent = document.querySelector('.desktop-content');
|
||||||
|
const desktopWidth = desktopContent.offsetWidth;
|
||||||
|
const desktopHeight = desktopContent.offsetHeight;
|
||||||
|
|
||||||
|
// Get NuxtPage (icons container) dimensions
|
||||||
|
const nuxtPage = document.querySelector('.desktop-icons-container');
|
||||||
|
const nuxtPageWidth = nuxtPage.offsetWidth;
|
||||||
|
const nuxtPageHeight = nuxtPage.offsetHeight;
|
||||||
|
|
||||||
|
// Define boundaries
|
||||||
|
const minX = -(nuxtPageWidth - desktopWidth);
|
||||||
|
const minY = -(nuxtPageHeight - desktopHeight);
|
||||||
|
const maxX = 0;
|
||||||
|
const maxY = 0;
|
||||||
|
|
||||||
|
// Clamp newX and newY within boundaries
|
||||||
|
iconsContainerX.value = Math.max(minX, Math.min(maxX, newX));
|
||||||
|
iconsContainerY.value = Math.max(minY, Math.min(maxY, newY));
|
||||||
|
|
||||||
|
console.log(`Icons container dragging. Current pos: (${iconsContainerX.value}, ${iconsContainerY.value})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIconsContainerMouseUp = () => {
|
||||||
|
if (isIconsContainerDragging.value) {
|
||||||
|
isIconsContainerDragging.value = false;
|
||||||
|
console.log(`Icons container drag ended. Final pos: (${iconsContainerX.value}, ${iconsContainerY.value})`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Lifecycle Hooks ---
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const savedWindows = localStorage.getItem('windows');
|
||||||
|
if (savedWindows) {
|
||||||
|
const parsedWindows = JSON.parse(savedWindows);
|
||||||
|
parsedWindows.forEach(win => {
|
||||||
|
if (win.zIndex > currentZIndex.value) currentZIndex.value = win.zIndex;
|
||||||
|
windows.push(win);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.addEventListener('mousemove', handleIconsContainerMouseMove);
|
||||||
|
window.addEventListener('mouseup', handleIconsContainerMouseUp);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up localStorage on component unmount (optional, for development)
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// localStorage.removeItem('windows');
|
window.removeEventListener('mousemove', handleIconsContainerMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleIconsContainerMouseUp);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const minimizedWindows = computed(() => windows.filter(win => win.isMinimized));
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Taskbar
|
<Taskbar
|
||||||
@open-about-me="openAboutMeWindow"
|
:menu-items="startMenuItems"
|
||||||
@close-all-windows="closeAllWindows"
|
|
||||||
@open-terminal="openTerminalWindow"
|
|
||||||
:minimized-windows="minimizedWindows"
|
:minimized-windows="minimizedWindows"
|
||||||
|
@menu-action="handleMenuAction"
|
||||||
@restore-window="restoreWindow"
|
@restore-window="restoreWindow"
|
||||||
/>
|
/>
|
||||||
<div class="desktop-content">
|
<div class="desktop-content">
|
||||||
|
<div
|
||||||
|
class="desktop-icons-container"
|
||||||
|
:style="{ left: `${iconsContainerX}px`, top: `${iconsContainerY}px`, cursor: isIconsContainerDragging ? 'grabbing' : 'grab' }"
|
||||||
|
@mousedown="handleIconsContainerMouseDown"
|
||||||
|
>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Dynamically render windows -->
|
|
||||||
<template v-for="window in windows" :key="window.id">
|
<template v-for="window in windows" :key="window.id">
|
||||||
<AboutMeWindow
|
<AboutMeWindow
|
||||||
v-if="window.type === 'about-me' && window.isVisible && !window.isMinimized"
|
v-if="window.type === 'about-me' && window.isVisible && !window.isMinimized"
|
||||||
:window-data="window"
|
:window-data="window"
|
||||||
@close="closeWindow(window.id)"
|
@close="closeWindow(window.id)"
|
||||||
@minimize="minimizeWindow(window.id)"
|
@minimize="minimizeWindow(window.id)"
|
||||||
@restore="restoreWindow(window.id)"
|
|
||||||
@bring-to-front="bringWindowToFront(window.id)"
|
@bring-to-front="bringWindowToFront(window.id)"
|
||||||
@update-position="updateWindowPosition(window.id, $event.x, $event.y)"
|
@update-position="updateWindowPosition(window.id, $event)"
|
||||||
|
@update-dimensions="updateWindowDimensions(window.id, $event)"
|
||||||
/>
|
/>
|
||||||
<TerminalWindow
|
<TerminalWindow
|
||||||
v-if="window.type === 'terminal' && window.isVisible && !window.isMinimized"
|
v-if="window.type === 'terminal' && window.isVisible && !window.isMinimized"
|
||||||
:window-data="window"
|
:window-data="window"
|
||||||
@close="closeWindow(window.id)"
|
@close="closeWindow(window.id)"
|
||||||
@minimize="minimizeWindow(window.id)"
|
@minimize="minimizeWindow(window.id)"
|
||||||
@restore="restoreWindow(window.id)"
|
|
||||||
@bring-to-front="bringWindowToFront(window.id)"
|
@bring-to-front="bringWindowToFront(window.id)"
|
||||||
@update-position="updateWindowPosition(window.id, $event.x, $event.y)"
|
@update-position="updateWindowPosition(window.id, $event)"
|
||||||
|
@update-dimensions="updateWindowDimensions(window.id, $event)"
|
||||||
|
/>
|
||||||
|
<WinampWindow
|
||||||
|
v-if="window.type === 'winamp' && window.isVisible && !window.isMinimized"
|
||||||
|
:window-data="window"
|
||||||
|
@close="closeWindow(window.id)"
|
||||||
|
@minimize="minimizeWindow(window.id)"
|
||||||
|
@bring-to-front="bringWindowToFront(window.id)"
|
||||||
|
@update-position="updateWindowPosition(window.id, $event)"
|
||||||
|
@update-dimensions="updateWindowDimensions(window.id, $event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
html {
|
||||||
background-color: #008080; /* Windows 95 teal */
|
background-color: #008080;
|
||||||
background-image: url('https://picsum.photos/1920/1080'); /* Placeholder wallpaper */
|
background-image: url('https://picsum.photos/1920/1080');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
font-family: 'Courier New', Courier, monospace;
|
height: 100vh; /* Use vh for full viewport height */
|
||||||
margin: 0;
|
}
|
||||||
overflow: hidden; /* Hide scrollbars from the body */
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100vh; /* Use vh for full viewport height */
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adjust Taskbar position */
|
|
||||||
.taskbar {
|
.taskbar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
z-index: 1000; /* Ensure it's on top of other content */
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop-content {
|
.desktop-content {
|
||||||
margin-top: 40px; /* Push content down by the height of the taskbar */
|
position: absolute;
|
||||||
height: calc(100vh - 40px); /* Fill remaining height */
|
top: 40px;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: calc(100vh - 40px); /* Changed to 100vh - 40px */
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-y: auto; /* Allow scrolling for desktop content if needed */
|
background-color: transparent;
|
||||||
background-color: transparent; /* Changed to transparent */
|
border: none;
|
||||||
border: none; /* Removed border */
|
box-shadow: none;
|
||||||
box-shadow: none; /* Removed shadow */
|
overflow: hidden; /* Prevent scrollbars on the desktop content itself */
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-icons-container {
|
||||||
|
position: relative; /* Allow positioning of children (NuxtPage) */
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: grab;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,180 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<BaseWindow
|
||||||
v-if="windowData.isVisible && !windowData.isMinimized"
|
:window-data="windowData"
|
||||||
class="window"
|
@close="$emit('close')"
|
||||||
:style="windowStyle"
|
@minimize="$emit('minimize')"
|
||||||
@mousedown="bringToFront"
|
@bring-to-front="$emit('bring-to-front')"
|
||||||
|
@update-position="$emit('update-position', $event)"
|
||||||
|
@update-dimensions="$emit('update-dimensions', $event)"
|
||||||
>
|
>
|
||||||
<div class="title-bar" @mousedown="startDrag">
|
<div class="about-content">
|
||||||
<div class="title-bar-text">{{ t(windowData.title) }}</div>
|
|
||||||
<div class="title-bar-controls">
|
|
||||||
<button aria-label="Minimize" @click.stop="minimizeWindow"></button>
|
|
||||||
<button aria-label="Close" @click.stop="closeWindow"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="window-body">
|
|
||||||
<p>{{ t('hello_developer') }}</p>
|
<p>{{ t('hello_developer') }}</p>
|
||||||
<p>{{ t('demo_ui') }}</p>
|
<p>{{ t('demo_ui') }}</p>
|
||||||
<p>{{ t('coming_soon') }}</p>
|
<p>{{ t('coming_soon') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, ref, computed, watch } from 'vue';
|
import BaseWindow from './BaseWindow.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = defineProps({
|
defineProps({ windowData: Object });
|
||||||
windowData: {
|
defineEmits(['close', 'minimize', 'bring-to-front', 'update-position', 'update-dimensions']);
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits([
|
|
||||||
'close',
|
|
||||||
'minimize',
|
|
||||||
'restore',
|
|
||||||
'bring-to-front',
|
|
||||||
'update-position',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Local refs for dragging, updated by watch for initial position from props
|
|
||||||
const x = ref(props.windowData.x);
|
|
||||||
const y = ref(props.windowData.y);
|
|
||||||
|
|
||||||
let isDragging = false;
|
|
||||||
let dragOffsetX = 0;
|
|
||||||
let dragOffsetY = 0;
|
|
||||||
let animationFrameId = null; // To store requestAnimationFrame ID
|
|
||||||
|
|
||||||
// Watch for changes in windowData.x and windowData.y (e.g., from localStorage load or app.vue updates)
|
|
||||||
watch(() => props.windowData.x, (newX) => { x.value = newX; });
|
|
||||||
watch(() => props.windowData.y, (newY) => { y.value = newY; });
|
|
||||||
|
|
||||||
const windowStyle = computed(() => ({
|
|
||||||
left: `${x.value}px`,
|
|
||||||
top: `${y.value}px`,
|
|
||||||
width: `${props.windowData.width}px`,
|
|
||||||
height: `${props.windowData.height}px`,
|
|
||||||
zIndex: props.windowData.zIndex,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const startDrag = (event) => {
|
|
||||||
isDragging = true;
|
|
||||||
dragOffsetX = event.clientX - x.value;
|
|
||||||
dragOffsetY = event.clientY - y.value;
|
|
||||||
window.addEventListener('mousemove', doDrag);
|
|
||||||
window.addEventListener('mouseup', stopDrag);
|
|
||||||
bringToFront(); // Bring to front when dragging starts
|
|
||||||
};
|
|
||||||
|
|
||||||
const doDrag = (event) => {
|
|
||||||
if (isDragging) {
|
|
||||||
const newX = event.clientX - dragOffsetX;
|
|
||||||
const newY = event.clientY - dragOffsetY;
|
|
||||||
|
|
||||||
if (animationFrameId) {
|
|
||||||
cancelAnimationFrame(animationFrameId);
|
|
||||||
}
|
|
||||||
animationFrameId = requestAnimationFrame(() => {
|
|
||||||
x.value = newX;
|
|
||||||
y.value = newY;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopDrag = () => {
|
|
||||||
isDragging = false;
|
|
||||||
window.removeEventListener('mousemove', doDrag);
|
|
||||||
window.removeEventListener('mouseup', stopDrag);
|
|
||||||
if (animationFrameId) {
|
|
||||||
cancelAnimationFrame(animationFrameId);
|
|
||||||
animationFrameId = null;
|
|
||||||
}
|
|
||||||
// Emit the final updated position
|
|
||||||
emit('update-position', { x: x.value, y: y.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeWindow = () => {
|
|
||||||
emit('close');
|
|
||||||
};
|
|
||||||
|
|
||||||
const minimizeWindow = () => {
|
|
||||||
emit('minimize');
|
|
||||||
};
|
|
||||||
|
|
||||||
const bringToFront = () => {
|
|
||||||
emit('bring-to-front');
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.window {
|
.about-content {
|
||||||
position: absolute;
|
padding: 10px;
|
||||||
background-color: #c0c0c0; /* Classic Windows gray */
|
color: black;
|
||||||
border: 2px solid;
|
|
||||||
border-color: #ffffff #808080 #808080 #ffffff; /* 3D effect */
|
|
||||||
box-shadow: 4px 4px 0px #000; /* Simple black shadow */
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
/* z-index, top, left, width, height are set dynamically via style binding */
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar {
|
|
||||||
background: linear-gradient(to right, #0a246a, #a6caf0); /* Classic blue gradient */
|
|
||||||
padding: 3px 2px 3px 3px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-text {
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-controls button {
|
|
||||||
background-color: #c0c0c0;
|
|
||||||
border: 2px solid;
|
|
||||||
border-color: #ffffff #000 #000 #ffffff;
|
|
||||||
width: 16px;
|
|
||||||
height: 14px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1;
|
|
||||||
padding: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-controls button:active {
|
|
||||||
border-color: #000 #ffffff #ffffff #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-controls button[aria-label="Minimize"]::before {
|
|
||||||
content: '_';
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
top: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-controls button[aria-label="Close"]::before {
|
|
||||||
content: 'X';
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
top: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-body {
|
|
||||||
padding: 1rem;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
border-top: 2px solid #000;
|
|
||||||
background-color: #c0c0c0; /* Ensure classic gray background for body */
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,207 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="window-wrapper"
|
||||||
|
:style="{ top: windowData.y + 'px', left: windowData.x + 'px', width: windowData.width + 'px', height: windowData.height + 'px', zIndex: windowData.zIndex }"
|
||||||
|
@mousedown="bringToFront"
|
||||||
|
>
|
||||||
|
<div class="title-bar" @mousedown.prevent="startDrag">
|
||||||
|
<span class="title">{{ t(windowData.title) }}</span>
|
||||||
|
<div class="window-controls">
|
||||||
|
<button @click.stop="minimize">_</button>
|
||||||
|
<button @click.stop="close">X</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<slot></slot> <!-- Unique content will be injected here -->
|
||||||
|
</div>
|
||||||
|
<!-- Resize Handles -->
|
||||||
|
<div class="resize-handle top" @mousedown.prevent="startResize('top')"></div>
|
||||||
|
<div class="resize-handle bottom" @mousedown.prevent="startResize('bottom')"></div>
|
||||||
|
<div class="resize-handle left" @mousedown.prevent="startResize('left')"></div>
|
||||||
|
<div class="resize-handle right" @mousedown.prevent="startResize('right')"></div>
|
||||||
|
<div class="resize-handle top-left" @mousedown.prevent="startResize('top-left')"></div>
|
||||||
|
<div class="resize-handle top-right" @mousedown.prevent="startResize('top-right')"></div>
|
||||||
|
<div class="resize-handle bottom-left" @mousedown.prevent="startResize('bottom-left')"></div>
|
||||||
|
<div class="resize-handle bottom-right" @mousedown.prevent="startResize('bottom-right')"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const props = defineProps({ windowData: Object });
|
||||||
|
const emit = defineEmits(['close', 'minimize', 'bring-to-front', 'update-position', 'update-dimensions']);
|
||||||
|
|
||||||
|
const close = () => emit('close');
|
||||||
|
const minimize = () => emit('minimize');
|
||||||
|
const bringToFront = () => emit('bring-to-front');
|
||||||
|
|
||||||
|
const startDrag = (event) => {
|
||||||
|
const startX = event.clientX;
|
||||||
|
const startY = event.clientY;
|
||||||
|
const initialX = props.windowData.x;
|
||||||
|
const initialY = props.windowData.y;
|
||||||
|
|
||||||
|
const onMouseMove = (moveEvent) => {
|
||||||
|
let newX = initialX + (moveEvent.clientX - startX);
|
||||||
|
let newY = initialY + (moveEvent.clientY - startY);
|
||||||
|
|
||||||
|
// Get window dimensions
|
||||||
|
const windowWidth = props.windowData.width;
|
||||||
|
const windowHeight = props.windowData.height;
|
||||||
|
|
||||||
|
// Define boundaries
|
||||||
|
const minX = 0;
|
||||||
|
const minY = 40; // Taskbar height
|
||||||
|
const maxX = window.innerWidth - windowWidth;
|
||||||
|
const maxY = window.innerHeight - windowHeight;
|
||||||
|
|
||||||
|
// Clamp newX and newY within boundaries
|
||||||
|
newX = Math.max(minX, Math.min(maxX, newX));
|
||||||
|
newY = Math.max(minY, Math.min(maxY, newY));
|
||||||
|
|
||||||
|
emit('update-position', { x: newX, y: newY });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startResize = (handle) => {
|
||||||
|
const startX = event.clientX;
|
||||||
|
const startY = event.clientY;
|
||||||
|
const initialX = props.windowData.x;
|
||||||
|
const initialY = props.windowData.y;
|
||||||
|
const initialWidth = props.windowData.width;
|
||||||
|
const initialHeight = props.windowData.height;
|
||||||
|
const minWidth = 200;
|
||||||
|
const minHeight = 150;
|
||||||
|
|
||||||
|
const onMouseMove = (moveEvent) => {
|
||||||
|
const deltaX = moveEvent.clientX - startX;
|
||||||
|
const deltaY = moveEvent.clientY - startY;
|
||||||
|
|
||||||
|
let newX = initialX;
|
||||||
|
let newY = initialY;
|
||||||
|
let newWidth = initialWidth;
|
||||||
|
let newHeight = initialHeight;
|
||||||
|
|
||||||
|
if (handle.includes('right')) {
|
||||||
|
newWidth = Math.max(minWidth, initialWidth + deltaX);
|
||||||
|
}
|
||||||
|
if (handle.includes('left')) {
|
||||||
|
const calculatedWidth = initialWidth - deltaX;
|
||||||
|
if (calculatedWidth >= minWidth) {
|
||||||
|
newWidth = calculatedWidth;
|
||||||
|
newX = initialX + deltaX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (handle.includes('bottom')) {
|
||||||
|
newHeight = Math.max(minHeight, initialHeight + deltaY);
|
||||||
|
}
|
||||||
|
if (handle.includes('top')) {
|
||||||
|
const calculatedHeight = initialHeight - deltaY;
|
||||||
|
if (calculatedHeight >= minHeight) {
|
||||||
|
newHeight = calculatedHeight;
|
||||||
|
newY = initialY + deltaY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp position and dimensions during resize
|
||||||
|
const currentWindowRight = newX + newWidth;
|
||||||
|
const currentWindowBottom = newY + newHeight;
|
||||||
|
|
||||||
|
const maxAllowedX = window.innerWidth - newWidth;
|
||||||
|
const maxAllowedY = window.innerHeight - newHeight;
|
||||||
|
|
||||||
|
newX = Math.max(0, Math.min(maxAllowedX, newX));
|
||||||
|
newY = Math.max(40, Math.min(maxAllowedY, newY)); // 40px for taskbar
|
||||||
|
|
||||||
|
emit('update-position', { x: newX, y: newY });
|
||||||
|
emit('update-dimensions', { width: newWidth, height: newHeight });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.window-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
background-color: #c0c0c0;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #ffffff #808080 #808080 #ffffff;
|
||||||
|
box-shadow: 1px 1px 4px rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 200px;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
background-color: #000080;
|
||||||
|
color: white;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: move;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls button {
|
||||||
|
background-color: #c0c0c0;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #ffffff #808080 #808080 #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 2px;
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
/* padding: 10px; */ /* REMOVED PADDING */
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.resize-handle.top {
|
||||||
|
top: -4px; left: 4px; right: 4px; height: 8px; cursor: ns-resize;
|
||||||
|
}
|
||||||
|
.resize-handle.bottom {
|
||||||
|
bottom: -4px; left: 4px; right: 4px; height: 8px; cursor: ns-resize;
|
||||||
|
}
|
||||||
|
.resize-handle.left {
|
||||||
|
left: -4px; top: 4px; bottom: 4px; width: 8px; cursor: ew-resize;
|
||||||
|
}
|
||||||
|
.resize-handle.right {
|
||||||
|
right: -4px; top: 4px; bottom: 4px; width: 8px; cursor: ew-resize;
|
||||||
|
}
|
||||||
|
.resize-handle.top-left {
|
||||||
|
top: -4px; left: -4px; width: 16px; height: 16px; cursor: nwse-resize;
|
||||||
|
}
|
||||||
|
.resize-handle.top-right {
|
||||||
|
top: -4px; right: -4px; width: 16px; height: 16px; cursor: nesw-resize;
|
||||||
|
}
|
||||||
|
.resize-handle.bottom-left {
|
||||||
|
bottom: -4px; left: -4px; width: 16px; height: 16px; cursor: nesw-resize;
|
||||||
|
}
|
||||||
|
.resize-handle.bottom-right {
|
||||||
|
bottom: -4px; right: -4px; width: 16px; height: 16px; cursor: nwse-resize;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,19 +2,18 @@
|
||||||
<div class="taskbar">
|
<div class="taskbar">
|
||||||
<div class="start-section">
|
<div class="start-section">
|
||||||
<button class="start-button" @click="toggleStartMenu">
|
<button class="start-button" @click="toggleStartMenu">
|
||||||
<span class="start-icon">🪟</span>
|
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIGZpbGw9IiNjMGMwYzAiLz48cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iNyIgaGVpZ2h0PSI3IiBmaWxsPSIjMDAwMDgwIi8+PHJlY3QgeD0iMiIgeT0iMTEiIHdpZHRoPSI3IiBoZWlnaHQ9IjciIGZpbGw9IiMwMDAwODAiLz48cmVjdCB4PSIxMSIgeT0iMiIgd2lkdGg9IjciIGhlaWdodD0iNyIgZmlsbD0iIzAwMDA4MCIvPjxyZWN0IHg9IjExIiB5PSIxMSIgd2lkdGg9IjciIGhlaWdodD0iNyIgZmlsbD0iIzAwMDA4MCIvPjwvc3ZnPg==" alt="Start" class="start-icon" />
|
||||||
<span>{{ t('start') }}</span>
|
<span>{{ t('start') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="showStartMenu" class="start-menu">
|
<div v-if="showStartMenu" class="start-menu">
|
||||||
<div class="menu-item" @click="openTerminal">
|
<div
|
||||||
{{ t('terminal') }}
|
v-for="item in menuItems"
|
||||||
</div>
|
:key="item.labelKey"
|
||||||
<div class="menu-item" @click="openAboutMe">
|
class="menu-item"
|
||||||
{{ t('about_me') }}
|
@click="handleMenuClick(item.action)"
|
||||||
</div>
|
>
|
||||||
<!-- More menu items can go here -->
|
<img :src="item.icon" class="menu-icon" />
|
||||||
<div class="menu-item" @click="closeAllWindows">
|
<span>{{ t(item.labelKey) }}</span>
|
||||||
{{ t('close_all_windows') }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,36 +43,25 @@ import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t, locale, setLocale } = useI18n();
|
const { t, locale, setLocale } = useI18n();
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
minimizedWindows: {
|
minimizedWindows: { type: Array, default: () => [] },
|
||||||
type: Array,
|
menuItems: { type: Array, default: () => [] },
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['restore-window', 'menu-action']);
|
||||||
|
|
||||||
const currentTime = ref('');
|
const currentTime = ref('');
|
||||||
const isMounted = ref(false);
|
const isMounted = ref(false);
|
||||||
let timerId = null;
|
let timerId = null;
|
||||||
const showStartMenu = ref(false);
|
const showStartMenu = ref(false);
|
||||||
|
|
||||||
const emit = defineEmits(['open-about-me', 'restore-window', 'close-all-windows', 'open-terminal']);
|
|
||||||
|
|
||||||
const toggleStartMenu = () => {
|
const toggleStartMenu = () => {
|
||||||
showStartMenu.value = !showStartMenu.value;
|
showStartMenu.value = !showStartMenu.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openTerminal = () => {
|
const handleMenuClick = (action) => {
|
||||||
emit('open-terminal');
|
emit('menu-action', action);
|
||||||
showStartMenu.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const openAboutMe = () => {
|
|
||||||
emit('open-about-me');
|
|
||||||
showStartMenu.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeAllWindows = () => {
|
|
||||||
emit('close-all-windows');
|
|
||||||
showStartMenu.value = false;
|
showStartMenu.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,16 +76,15 @@ const restoreMinimizedWindow = (id) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCurrentTime = () => {
|
const updateCurrentTime = () => {
|
||||||
// Map i18n locale to a specific locale for time formatting
|
|
||||||
const timeLocale = locale.value === 'zh-tw' ? 'zh-TW' : 'en-US';
|
const timeLocale = locale.value === 'zh-tw' ? 'zh-TW' : 'en-US';
|
||||||
currentTime.value = new Date().toLocaleTimeString(timeLocale);
|
currentTime.value = new Date().toLocaleTimeString(timeLocale);
|
||||||
};
|
};
|
||||||
// Watch for locale changes to update the time format immediately
|
|
||||||
watch(locale, updateCurrentTime);
|
watch(locale, updateCurrentTime);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
isMounted.value = true;
|
isMounted.value = true;
|
||||||
updateCurrentTime(); // Set initial time based on current locale
|
updateCurrentTime();
|
||||||
timerId = setInterval(updateCurrentTime, 1000);
|
timerId = setInterval(updateCurrentTime, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -113,53 +100,20 @@ onUnmounted(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #c0c0c0; /* Classic Windows gray */
|
background-color: #c0c0c0;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
border-top: 2px solid #ffffff;
|
border-top: 2px solid #ffffff;
|
||||||
border-bottom: 2px solid #808080;
|
border-bottom: 2px solid #808080;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
position: relative; /* For positioning the start menu */
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.start-section {
|
.start-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-grow: 0; /* Don't let start section grow */
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1; /* Allow task buttons to take available space */
|
|
||||||
margin-left: 10px; /* Space between start button and task buttons */
|
|
||||||
gap: 6px;
|
|
||||||
overflow-x: auto; /* Allow horizontal scrolling if many windows */
|
|
||||||
padding-bottom: 2px; /* Prevent scrollbar from overlapping border */
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-button {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border: 2px solid;
|
|
||||||
border-color: #808080 #ffffff #ffffff #808080;
|
|
||||||
background-color: #c0c0c0;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
cursor: pointer;
|
|
||||||
color: black;
|
|
||||||
white-space: nowrap; /* Prevent text wrapping */
|
|
||||||
flex-shrink: 0; /* Prevent buttons from shrinking */
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-button:active {
|
|
||||||
border-color: #ffffff #808080 #808080 #ffffff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.start-button {
|
.start-button {
|
||||||
|
@ -181,26 +135,31 @@ onUnmounted(() => {
|
||||||
|
|
||||||
.start-icon {
|
.start-icon {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.start-menu {
|
.start-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%; /* Opens downwards from the taskbar */
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
background-color: #c0c0c0;
|
background-color: #c0c0c0;
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-color: #ffffff #808080 #808080 #ffffff;
|
border-color: #ffffff #808080 #808080 #ffffff;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
min-width: 160px;
|
min-width: 200px;
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
padding: 4px 8px;
|
padding: 8px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
color: black;
|
color: black;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item:hover {
|
.menu-item:hover {
|
||||||
|
@ -209,7 +168,43 @@ onUnmounted(() => {
|
||||||
border-color: #000080;
|
border-color: #000080;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-toggle {
|
.menu-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-left: 10px;
|
||||||
|
gap: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-button {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-color: #808080 #ffffff #ffffff #808080;
|
||||||
|
background-color: #c0c0c0;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: black;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-button:active {
|
||||||
|
border-color: #ffffff #808080 #808080 #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-toggle, .clock {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-color: #808080 #ffffff #ffffff #808080;
|
border-color: #808080 #ffffff #ffffff #808080;
|
||||||
|
@ -222,13 +217,4 @@ onUnmounted(() => {
|
||||||
.language-toggle:active {
|
.language-toggle:active {
|
||||||
border-color: #ffffff #808080 #808080 #ffffff;
|
border-color: #ffffff #808080 #808080 #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clock {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border: 2px solid;
|
|
||||||
border-color: #808080 #ffffff #ffffff #808080;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: black;
|
|
||||||
background-color: #c0c0c0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<BaseWindow
|
||||||
class="window-wrapper"
|
:window-data="windowData"
|
||||||
:style="{ top: windowData.y + 'px', left: windowData.x + 'px', width: windowData.width + 'px', height: windowData.height + 'px', zIndex: windowData.zIndex }"
|
@close="$emit('close')"
|
||||||
@mousedown="bringToFront"
|
@minimize="$emit('minimize')"
|
||||||
|
@bring-to-front="$emit('bring-to-front')"
|
||||||
|
@update-position="$emit('update-position', $event)"
|
||||||
|
@update-dimensions="$emit('update-dimensions', $event)"
|
||||||
>
|
>
|
||||||
<div class="title-bar" @mousedown.prevent="startDrag">
|
<div class="terminal-content">
|
||||||
<span class="title">{{ t(windowData.title) }}</span>
|
|
||||||
<div class="window-controls">
|
|
||||||
<button @click.stop="minimize">_</button>
|
|
||||||
<button @click.stop="close">X</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div class="terminal-section">
|
<div class="terminal-section">
|
||||||
<span class="prompt">guest@daniels-mac:~$</span>
|
<span class="prompt">guest@daniels-mac:~$</span>
|
||||||
<input
|
<input
|
||||||
|
@ -24,92 +20,38 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import BaseWindow from './BaseWindow.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
defineProps({ windowData: Object });
|
||||||
const props = defineProps({ windowData: Object });
|
defineEmits(['close', 'minimize', 'bring-to-front', 'update-position', 'update-dimensions']);
|
||||||
const emit = defineEmits(['close', 'minimize', 'bring-to-front', 'update-position']);
|
|
||||||
|
|
||||||
const command = ref('');
|
const command = ref('');
|
||||||
|
|
||||||
const runCommand = () => {
|
const runCommand = () => {
|
||||||
console.log(`Command executed: ${command.value}`);
|
|
||||||
command.value = '';
|
command.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const close = () => emit('close');
|
|
||||||
const minimize = () => emit('minimize');
|
|
||||||
const bringToFront = () => emit('bring-to-front');
|
|
||||||
|
|
||||||
const startDrag = (event) => {
|
|
||||||
const startX = event.clientX;
|
|
||||||
const startY = event.clientY;
|
|
||||||
const initialX = props.windowData.x;
|
|
||||||
const initialY = props.windowData.y;
|
|
||||||
|
|
||||||
const onMouseMove = (moveEvent) => {
|
|
||||||
const newX = initialX + (moveEvent.clientX - startX);
|
|
||||||
const newY = initialY + (moveEvent.clientY - startY);
|
|
||||||
emit('update-position', { x: newX, y: newY });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseUp = () => {
|
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
|
||||||
document.removeEventListener('mouseup', onMouseUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
|
||||||
document.addEventListener('mouseup', onMouseUp);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.window-wrapper {
|
.terminal-content {
|
||||||
position: absolute;
|
|
||||||
background-color: #c0c0c0; /* Windows 95 gray */
|
|
||||||
border: 2px solid;
|
|
||||||
border-color: #ffffff #808080 #808080 #ffffff;
|
|
||||||
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar {
|
|
||||||
background-color: #000080; /* Windows 95 blue */
|
|
||||||
color: white;
|
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
cursor: move;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-controls button {
|
|
||||||
background-color: #c0c0c0;
|
|
||||||
border: 1px solid;
|
|
||||||
border-color: #ffffff #808080 #808080 #ffffff;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: 2px;
|
|
||||||
min-width: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 10px;
|
|
||||||
flex-grow: 1;
|
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: #34d399; /* green-400 */
|
color: #34d399; /* green-400 */
|
||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 5px; /* Added small padding */
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-section {
|
.terminal-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt {
|
.prompt {
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script setup>
|
||||||
|
import BaseWindow from '~/components/BaseWindow.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'close',
|
||||||
|
'minimize',
|
||||||
|
'bring-to-front',
|
||||||
|
'update-position',
|
||||||
|
'update-dimensions',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleClose = () => emit('close');
|
||||||
|
const handleMinimize = () => emit('minimize');
|
||||||
|
const handleBringToFront = () => emit('bring-to-front');
|
||||||
|
const handleUpdatePosition = (newPosition) => emit('update-position', newPosition);
|
||||||
|
const handleUpdateDimensions = (newDimensions) => emit('update-dimensions', newDimensions);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseWindow
|
||||||
|
:window-data="windowData"
|
||||||
|
@close="handleClose"
|
||||||
|
@minimize="handleMinimize"
|
||||||
|
@bring-to-front="handleBringToFront"
|
||||||
|
@update-position="handleUpdatePosition"
|
||||||
|
@update-dimensions="handleUpdateDimensions"
|
||||||
|
>
|
||||||
|
<div class="winamp-content">
|
||||||
|
<!-- Winamp content goes here -->
|
||||||
|
<p>This is the Winamp player.</p>
|
||||||
|
<p>You can add your music player UI here.</p>
|
||||||
|
</div>
|
||||||
|
</BaseWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.winamp-content {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #C0C0C0;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: 'MS Sans Serif', 'Arial', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,149 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
windowData: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'close',
|
||||||
|
'minimize',
|
||||||
|
'bring-to-front',
|
||||||
|
'update-position',
|
||||||
|
'update-dimensions',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const startX = ref(0);
|
||||||
|
const startY = ref(0);
|
||||||
|
const initialX = ref(0); // Store initial x position when drag starts
|
||||||
|
const initialY = ref(0); // Store initial y position when drag starts
|
||||||
|
const currentX = ref(0); // New ref for current X during drag
|
||||||
|
const currentY = ref(0); // New ref for current Y during drag
|
||||||
|
|
||||||
|
const windowRef = ref(null);
|
||||||
|
|
||||||
|
const handleMouseDown = (e) => {
|
||||||
|
if (e.target.classList.contains('window-header') || e.target.classList.contains('window-title')) {
|
||||||
|
isDragging.value = true;
|
||||||
|
startX.value = e.clientX;
|
||||||
|
startY.value = e.clientY;
|
||||||
|
initialX.value = props.windowData.x; // Initialize with prop value at drag start
|
||||||
|
initialY.value = props.windowData.y; // Initialize with prop value at drag start
|
||||||
|
currentX.value = props.windowData.x; // Initialize current with prop value
|
||||||
|
currentY.value = props.windowData.y; // Initialize current with prop value
|
||||||
|
emit('bring-to-front');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e) => {
|
||||||
|
if (!isDragging.value) return;
|
||||||
|
const dx = e.clientX - startX.value;
|
||||||
|
const dy = e.clientY - startY.value;
|
||||||
|
// Update local position during drag based on initial position
|
||||||
|
currentX.value = initialX.value + dx; // Use initialX, not props.windowData.x
|
||||||
|
currentY.value = initialY.value + dy; // Use initialY, not props.windowData.y
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
if (isDragging.value) {
|
||||||
|
isDragging.value = false;
|
||||||
|
// Emit final position only once after drag ends
|
||||||
|
emit('update-position', { x: currentX.value, y: currentY.value });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
window.addEventListener('mouseup', handleMouseUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeWindow = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const minimizeWindow = () => {
|
||||||
|
emit('minimize');
|
||||||
|
};
|
||||||
|
|
||||||
|
const bringToFront = () => {
|
||||||
|
emit('bring-to-front');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="windowRef"
|
||||||
|
class="window"
|
||||||
|
:style="{
|
||||||
|
// Use currentX and currentY for positioning during drag
|
||||||
|
left: `${isDragging ? currentX : windowData.x}px`,
|
||||||
|
top: `${isDragging ? currentY : windowData.y}px`,
|
||||||
|
width: `${windowData.width}px`,
|
||||||
|
height: `${windowData.height}px`,
|
||||||
|
zIndex: windowData.zIndex,
|
||||||
|
}"
|
||||||
|
@mousedown="bringToFront"
|
||||||
|
>
|
||||||
|
<div class="window-header" @mousedown="handleMouseDown">
|
||||||
|
<span class="window-title">{{ windowData.title }}</span>
|
||||||
|
<div class="window-controls">
|
||||||
|
<button @click="minimizeWindow">_</button>
|
||||||
|
<button @click="closeWindow">X</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="window-content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.window {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid #000;
|
||||||
|
box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
background-color: #C0C0C0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
resize: both;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-header {
|
||||||
|
background-color: #000080;
|
||||||
|
color: white;
|
||||||
|
padding: 4px;
|
||||||
|
cursor: grab;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-family: 'MS Sans Serif', 'Arial', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls button {
|
||||||
|
background-color: #C0C0C0;
|
||||||
|
border: 1px solid #000;
|
||||||
|
padding: 1px 6px;
|
||||||
|
margin-left: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'MS Sans Serif', 'Arial', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
background-color: #C0C0C0;
|
||||||
|
border-top: 2px solid #000;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,16 +1,15 @@
|
||||||
|
<script setup>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
const openWinamp = inject('openWinamp');
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<div class="icon">
|
<div class="icon" @click="openWinamp">
|
||||||
<div class="icon-image">[PC]</div>
|
<div class="icon-image">[W]</div>
|
||||||
<div class="icon-label">{{ $t('my_computer') }}</div>
|
<div class="icon-label">Winamp</div>
|
||||||
</div>
|
|
||||||
<div class="icon">
|
|
||||||
<div class="icon-image">[BIN]</div>
|
|
||||||
<div class="icon-label">{{ $t('recycle_bin') }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="icon">
|
|
||||||
<div class="icon-image">[>]_</div>
|
|
||||||
<div class="icon-label">{{ $t('terminal') }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in New Issue