502 lines
13 KiB
Vue

<script setup lang="ts">
// No layout
definePageMeta({
layout: false,
});
// interfaces
interface currentNavBarInterface {
name: string;
icon: string;
action: any;
flash: boolean;
windowAssociated: string;
}
interface associAppWindowInterface {
name: string;
id: string;
title: string;
component: any;
}
// Import plugins
import { gsap } from "gsap";
import { TextPlugin } from "gsap/TextPlugin";
gsap.registerPlugin(TextPlugin);
// Import Windows
import LoginWindow from "~/components/app/windows/login.vue";
import HotNewsWindow from "~/components/app/windows/hotnews.vue";
import SourcesWindow from "~/components/app/windows/sources.vue";
import AboutWindow from "~/components/app/windows/about.vue";
import ChatbotWindow from "~/components/app/windows/chatbot.vue";
import AboutNewsOrgWindow from "~/components/app/windows/aboutNewsOrg.vue";
import Error404Window from "~/components/app/windows/error404.vue";
// Icons
import {
ComputerDesktopIcon,
UserIcon,
LanguageIcon,
ChevronRightIcon,
} from "@heroicons/vue/24/outline";
// i18n
const { t, locale, locales } = useI18n();
const switchLocalePath = useSwitchLocalePath();
const localePath = useLocalePath();
// Router
const router = useRouter();
const route = useRoute();
// values
const popMessage = ref(null);
const menuOpen = ref(false);
const langMenuOpen = ref(false);
const lang = ref(locale.value);
const alertOpen = ref(false);
const currentNavBar = ref<currentNavBarInterface[]>([]);
const bootingAnimation = ref(true);
const activeWindows = ref<associAppWindowInterface[]>([]);
const openApp = ref();
const openAppId = ref();
const openAppNameQuery = ref();
const currentOpenAppId = ref(0);
const progress = ref(0);
const titleAppName = ref("Desktop");
const openingAppViaAnApp = ref(false);
const passedValues = ref();
// Key Data
const menuItems = [
{ name: t("app.hotnews"), windowName: "hotnews" },
{ name: t("app.news"), windowName: "news" },
{ name: t("app.sources"), windowName: "sources" },
{ name: t("app.starred"), windowName: "starred" },
{ name: t("app.chatbot"), windowName: "chatbot" },
{ name: t("app.about"), windowName: "about" },
{ name: t("app.terminal"), windowName: "tty" },
{ name: t("app.settings"), windowName: "settings" },
{ name: t("app.login"), windowName: "login" },
{ name: t("app.leave"), windowName: "leave" },
];
const associAppWindow = [
{
name: "hotnews",
id: "1",
title: t("app.hotnews"),
component: HotNewsWindow,
width: "700px",
height: "500px",
},
{
name: "login",
id: "2",
title: t("app.login"),
component: LoginWindow,
},
{
name: "sources",
id: "3",
title: t("app.sources"),
component: SourcesWindow,
width: "700px",
height: "500px",
},
{
name: "about",
id: "4",
title: t("app.about"),
component: AboutWindow,
},
{
name: "settings",
id: "5",
title: t("app.settings"),
component: Error404Window,
},
{
name: "news",
id: "6",
title: t("app.news"),
component: Error404Window,
},
{
name: "starred",
id: "7",
title: t("app.starred"),
component: Error404Window,
},
{
name: "chatbot",
id: "8",
title: t("app.chatbot"),
component: ChatbotWindow,
width: "400px",
height: "600px",
},
{
name: "error404",
id: "9",
title: t("app.error404"),
component: Error404Window,
},
{
name: "aboutNewsOrg",
id: "10",
title: t("app.aboutNewsOrg"),
component: AboutNewsOrgWindow,
},
{
name: "tty",
id: "11",
title: t("app.terminal"),
component: Error404Window,
}
];
/*
const keyboardShortcuts = {
'Meta+k': {
action: () => toggleMenu(),
description: 'Toggle menu'
},
'Meta+q': {
action: () => router.push(localePath("/home")),
description: 'Quit to home'
},
'Meta+w': {
action: () => closeWindow(activeWindows.value[activeWindows.value.length - 1]?.id),
description: 'Close current window'
},
'Meta+t': {
action: () => findAndOpenWindow('tty'),
description: 'Open terminal'
},
}
// Keyboard shortcuts
const handleKeyboardActions = (e: KeyboardEvent) => {
const key = [
e.metaKey ? "Meta": "",
e.ctrlKey ? "Ctrl": "",
e.shiftKey ? "Shift": "",
e.altKey ? "Alt": "",
e.key.toLowerCase()
].filter(Boolean).join('+')
const shortcut = keyboardShortcuts[key];
if (shortcut) {
console.log(`Shortcut triggered: ${key}`);
e.preventDefault()
shortcut.action()
}
}
onMounted(() => {
window.addEventListener('keydown', handleKeyboardActions)
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyboardActions)
});
*/
// Date
const currentDate = ref(
new Date().toLocaleDateString("zh-TW", {
month: "2-digit",
day: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
}),
);
onMounted(() => {
setInterval(() => {
currentDate.value = new Date().toLocaleDateString("zh-TW", {
month: "2-digit",
day: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
}, 1000);
});
// functions
const openWindow = (windowName?: string) => {
if (windowName === "leave") {
router.push(localePath("/home"));
} else {
if (windowName) findAndOpenWindow(windowName);
}
menuOpen.value = false;
};
const unMinWindow = (windowName?: string) => {
console.log(windowName);
};
// menus
const toggleMenu = () => {
menuOpen.value = !menuOpen.value;
};
// Lang Menu
const toggleLangMenu = () => {
langMenuOpen.value = !langMenuOpen.value;
};
// ?openapp= component
onMounted(async () => {
openApp.value = route.query.openapp;
openAppId.value = route.query.id;
if (openApp.value) {
openWindow(openApp.value);
}
});
const findAndOpenWindow = (windowName: string) => {
const app = associAppWindow.find((app) => app.name === windowName);
// Prevent dual logins
if (
windowName === "login" &&
activeWindows.value.some((window) => window.name === "login")
) {
return;
}
// Prevent dual about
if (
windowName === "about" &&
activeWindows.value.some((window) => window.name === "about")
) {
return;
}
if (app) {
// Use shallowRef for better performance with components
const windowComponent = shallowRef(app.component);
titleAppName.value = app.title;
activeWindows.value.push({
id: currentOpenAppId.value,
component: windowComponent,
name: windowName,
title: app.title,
width: app.width || "400px",
height: app.height || "300px",
});
currentOpenAppId.value++;
}
};
const obtainTopWindowPosition = (windowId: string) => {
if (!openingAppViaAnApp.value) {
const windowIndex = activeWindows.value.findIndex(
(window) => window.id === windowId,
);
if (windowIndex !== -1) {
const [window] = activeWindows.value.splice(windowIndex, 1);
titleAppName.value = window.name;
activeWindows.value.push(window);
}
}
};
const closeWindow = (windowId: string) => {
activeWindows.value = activeWindows.value.filter(
(window) => window.id !== windowId,
);
console.log("activeWindows.value", activeWindows.value);
};
const openNewWindowViaApp = (windowId: string) => {
openingAppViaAnApp.value = true;
findAndOpenWindow(windowId);
setTimeout(() => {
openingAppViaAnApp.value = false;
},1000);
}
const maxWindow = (windowId: string) => {};
// Title
useSeoMeta({
title: titleAppName.value + " - Desktop",
});
// Booting animation
onMounted(() => {
// booting animation bypass
const bootingHeaderParams = route.query.bypass;
if (bootingHeaderParams) {
bootingAnimation.value = false;
return;
}
if (bootingAnimation.value) {
gsap.to(popMessage.value, {
duration: 0.5,
text: t("app.booting"),
ease: "none",
});
setTimeout(() => {
bootingAnimation.value = false;
}, 2000);
}
});
watchEffect((cleanupFn) => {
const tier = setTimeout(() => (progress.value = 10), Math.random() * 50);
const timer = setTimeout(() => (progress.value = 30), Math.random() * 100);
const timmer = setTimeout(() => (progress.value = 70), Math.random() * 150);
const timmmer = setTimeout(() => (progress.value = 100), 1800);
cleanupFn(() => clearTimeout(tier));
cleanupFn(() => clearTimeout(timer));
cleanupFn(() => clearTimeout(timmer));
cleanupFn(() => clearTimeout(timmmer));
});
</script>
<template>
<div v-if="bootingAnimation">
<div
class="flex flex-col justify-center align-center text-center absolute w-full h-screen inset-0 overscroll-none"
>
<Progress
v-model="progress"
class="w-3/5 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
/>
<br />
<span class="text-xl text-bold mt-3"
>Launching your fancy desktop...</span
>
</div>
</div>
<div
class="absolute inset-x-0 flex flex-row px-2 py-1 bg-[#7D7C7C]/70 text-white justify-between align-center text-center z-50 overscroll-none"
v-else
>
<!--Menu container-->
<div class="flex flex-row g-2 text-gray-400 text-white z-9999">
<button
@click="toggleMenu"
class="w-8 h-8 text-white hover:text-blue-500 transition-all duration-100 flex flex-row"
>
<ComputerDesktopIcon />
</button>
<span class="ml-1 mr-2 text-[20px]">|</span>
<!--navbar icons for min and max application window-->
<button
class="flex flex-row items-center gap-x-2 text-gray-400 hover:text-gray-600 transition-all duration-100"
></button>
<div
v-for="item in currentNavBar"
:key="item.name"
class="flex flex-row items-center gap-x-2 hover:bg-gray-100 transition-all duration-100 px-4 py-2 cursor-pointer"
>
<button
@click="unMinWindow(item.windowAssociated)"
class="flex flex-row items-center gap-x-2 text-gray-400 hover:text-gray-600 transition-all duration-100"
>
<span>{{ item.name }}</span>
<span
v-if="item.flash"
class="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75"
></span>
<span v-if="item.icon" :class="item.icon"> </span>
</button>
</div>
</div>
<div class="flex flex-row gap-5">
<button
class="p-1 hover:text-blue-200 transition-all duration-100 hover:bg-gray-500 rounded"
@click="toggleLangMenu"
>
{{ t("localeflag") }}
</button>
<div class="text-center align-middle justify-center text-white">
{{ currentDate }}
</div>
</div>
</div>
<div class="w-full h-[2.5em]"></div>
<!--Menu-->
<Transition
enter-active-class="animate__animated animate__fadeInDown animate_fast03"
leave-active-class="animate__animated animate__fadeOutUp animate_fast03"
>
<div
class="m-2 p-2 bg-gray-800 shadow-lg w-fit rounded-[10px] v-9998"
v-if="menuOpen"
>
<div v-for="item in menuItems" :key="item.name" class="">
<button
@click="openWindow(item.windowName)"
class="flex flex-row items-center gap-x-2 text-gray-400 hover:text-gray-600 transition-all duration-100"
>
<span>{{ item.name }}</span>
<ChevronRightIcon class="w-4 h-4 justify-center align-center" />
</button>
</div>
</div>
</Transition>
<Transition
enter-active-class="animate__animated animate__fadeInDown animate_fast03"
leave-active-class="animate__animated animate__fadeOutUp animate_fast03"
>
<div v-if="langMenuOpen">
<div
class="w-48 bg-white rounded-md shadow-lg py-1 flex flex-col gap-y-5"
>
<a
v-for="loc in availableLocales"
:key="loc.code"
:href="switchLocalePath(loc.code)"
v-on:click="langMenuOpen = false"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-all duration-100"
>
{{ loc.name || loc.code }}
</a>
</div>
</div>
</Transition>
<!--Main desktop contents-->
<div
class="flex flex-col justify-center align-center text-center absolute w-full h-screen inset-x-0 inset-y-0 z-[-10]"
id="desktop"
></div>
<Transition>
<div>
<DraggableWindow
v-for="window in activeWindows"
:key="window.id"
:title="window.title"
@close="closeWindow(window.id)"
@min="unMinWindow(window.id)"
:width="window.width"
:height="window.height"
@click="obtainTopWindowPosition(window.id)"
@maximize="maxWindow(window.id)"
>
<Suspense>
<Component
:is="window.component"
@error="console.error('Error:', $event)"
@windowopener="openNewWindowViaApp($event)"
@loadValue=""
:values="passedValues"
/>
</Suspense>
</DraggableWindow>
</div>
</Transition>
</template>