mirror of
https://github.com/hpware/news-analyze.git
synced 2025-06-23 13:04:23 +00:00
Updates UI components and gitignore rules for database backups, adds Facebook links to news org window, and improves desktop window title handling.
544 lines
14 KiB
Vue
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>
|