news-analyze/pages/app/desktop/index.vue
吳元皓 94fbf1551d Add support for database backups and enhance UI elements
Updates UI components and gitignore rules for database backups, adds
Facebook links to news org window, and improves desktop window title
handling.
2025-05-15 10:54:39 +08:00

544 lines
14 KiB
Vue

<script setup lang="ts">
// No layout
definePageMeta({
layout: false,
});
// interfaces
interface currentNavBarInterface {
name: string;
icon: string;
action: any;
flash: boolean;
windowAssociated: string;
minimized: boolean;
}
interface associAppWindowInterface {
name: string;
id: string;
title: string;
component: any;
}
// Import plugins
import { v4 as uuidv4 } from "uuid";
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();
const globalWindowVal = ref(new Map());
// 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,
width: "600px",
height: "400px",
},
{
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
onMounted(() => {
associAppWindow.forEach((window) => {
globalWindowVal.value.set(window.name, {
id: window.id,
title: window.title,
windowCount: 1,
});
});
});
const openWindow = (windowName?: string) => {
if (windowName === "leave") {
if (confirm("Are you sure?")) {
router.push(localePath("/home"));
} else {
return;
}
} 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;
const abosluteId = uuidv4();
activeWindows.value.push({
id: currentOpenAppId.value,
absoluteId: abosluteId,
component: windowComponent,
name: windowName,
title: app.title,
width: app.width || "400px",
height: app.height || "300px",
});
currentOpenAppId.value++;
// Add to navbar
const windowNameVal2 =
globalWindowVal.value.get(windowName).windowCount === 1
? windowName
: windowName +
"(" +
globalWindowVal.value.get(windowName).windowCount +
")";
currentNavBar.value.push({
name: windowNameVal2,
icon: "anything",
action: "idk",
flash: true,
windowAssociated: abosluteId,
minimized: false,
});
globalWindowVal.value.get(windowName).windowCount++;
}
};
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.title;
activeWindows.value.push(window);
}
}
};
const closeWindow = (windowId: string, windowAID: string) => {
activeWindows.value = activeWindows.value.filter(
(window) => window.id !== windowId,
);
currentNavBar.value = currentNavBar.value.filter(
(window) => window.windowAssociated !== windowAID,
);
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: "Desktop",
});
watchEffect(() => {
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">{{ t("app.launchtext") }}</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
class="overflow-hidden overflow-x-auto overflow-y-hidden scrollbar-thin scrollbar-track-transparent scrollbar-thumb-white flex-nowrap whitespace-nowrap min-w-0"
>
<div class="flex flex-row flex-shrink-0 min-w-0">
<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-150 px-4 py-1 cursor-pointer group rounded-xl"
>
<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>
</button>
</div>
</div>
</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, window.absoluteId)"
@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>