mirror of
https://github.com/khairul169/launcher.git
synced 2026-09-24 13:17:47 +07:00
feat: initial commit
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import * as blurhash from "blurhash";
|
||||
|
||||
const bgHash =
|
||||
"|ZCshDM^M_axogogWCWAWA-@WAWBj]axaxj[j]ay9boGobWCR%ayt7ofj[4,xZoeWCaxj[ofj]j[V=ofaxa}ofj[WCWVfP$|WEj^ofofayWBaya{s+R*ogs;axWBayj[j[RiWUj]j[WVj[j[axayRioKflfSa{axaxfPa#";
|
||||
const bgUrl = "/images/1226538.webp";
|
||||
|
||||
const onLoaded = () => {
|
||||
const searchBar = document.querySelector(".search-bar") as HTMLDivElement;
|
||||
if (searchBar) {
|
||||
searchBar.classList.add("loaded");
|
||||
searchBar.style.opacity = "1";
|
||||
}
|
||||
|
||||
const quickLaunch = document.querySelector(".quick-launch") as HTMLDivElement;
|
||||
if (quickLaunch) {
|
||||
quickLaunch.classList.add("loaded");
|
||||
quickLaunch.style.opacity = "1";
|
||||
}
|
||||
};
|
||||
|
||||
export const initBackground = () => {
|
||||
const hashCanvas = document.getElementById("bg-hash") as HTMLCanvasElement;
|
||||
if (!hashCanvas) {
|
||||
throw new Error("Could not find canvas");
|
||||
}
|
||||
|
||||
hashCanvas.width = 32;
|
||||
hashCanvas.height = 18;
|
||||
|
||||
const pixels = blurhash.decode(bgHash, 32, 18);
|
||||
const ctx = hashCanvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
throw new Error("Could not get 2d context");
|
||||
}
|
||||
|
||||
const imageData = ctx.createImageData(hashCanvas.width, hashCanvas.height);
|
||||
imageData.data.set(pixels);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
// Load background image
|
||||
const bg = document.getElementById("bg-main") as HTMLDivElement;
|
||||
const img = new Image();
|
||||
img.src = bgUrl;
|
||||
img.onload = () => {
|
||||
bg.style.backgroundImage = `url(${bgUrl})`;
|
||||
bg.style.opacity = "1";
|
||||
bg.classList.add("loaded");
|
||||
|
||||
setTimeout(() => {
|
||||
// remove hashCanvas from dom
|
||||
hashCanvas.remove();
|
||||
}, 2000);
|
||||
|
||||
onLoaded();
|
||||
};
|
||||
};
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
export const launcherItems: LauncherItem[] = [
|
||||
{
|
||||
name: "Jellyfin",
|
||||
href: "http://10.0.0.101:8096/web",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/jellyfin.svg",
|
||||
pinned: true,
|
||||
},
|
||||
{
|
||||
name: "Musicfin",
|
||||
href: "http://armbian:8096/web",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/jellyfin.svg",
|
||||
},
|
||||
{
|
||||
name: "Youtube2MP3",
|
||||
href: "http://yt2mp3.home.ip",
|
||||
icon: "https://git.rul.sh/khairul169/Go-YT2MP3/raw/branch/main/ui/public/android-chrome-512x512.png",
|
||||
},
|
||||
{
|
||||
name: "Memos",
|
||||
href: "https://memos.rul.sh",
|
||||
icon: "https://memos.rul.sh/apple-touch-icon.png",
|
||||
pinned: true,
|
||||
},
|
||||
{
|
||||
name: "Flatnotes",
|
||||
href: "http://100.64.0.3:8124",
|
||||
icon: "https://github.com/dullage/flatnotes/blob/develop/client/public/android-chrome-192x192.png?raw=true",
|
||||
},
|
||||
{
|
||||
name: "Gitea",
|
||||
href: "https://git.rul.sh",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/gitea.svg",
|
||||
},
|
||||
{
|
||||
name: "Gotify",
|
||||
href: "https://gotify.rul.sh",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/gotify.svg",
|
||||
},
|
||||
{
|
||||
name: "AriaNg",
|
||||
href: "http://ariang.home.ip/",
|
||||
icon: "https://raw.githubusercontent.com/mayswind/AriaNg/master/src/tileicon.png",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "File Browser",
|
||||
href: "http://10.0.0.101:9005/files",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/filebrowser.svg",
|
||||
pinned: true,
|
||||
},
|
||||
{
|
||||
name: "File Browser (Armbian)",
|
||||
href: "https://fm.rul.sh",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/filebrowser.svg",
|
||||
},
|
||||
{
|
||||
name: "Change Detection",
|
||||
href: "https://chgdetect.rul.sh/",
|
||||
icon: "https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/changedetectionio/static/images/avatar-256x256.png",
|
||||
},
|
||||
{
|
||||
name: "Vaultwarden",
|
||||
href: "https://vw.rul.sh",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/vaultwarden.svg",
|
||||
pinned: true,
|
||||
},
|
||||
{
|
||||
name: "Garage WebUI",
|
||||
href: "http://10.0.0.101:3909/buckets",
|
||||
icon: "https://garagehq.deuxfleurs.fr/icons/apple-touch-icon.png",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: "Pi-hole",
|
||||
href: "http://pi.hole/admin",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/pi-hole.svg",
|
||||
},
|
||||
{
|
||||
name: "Headscale",
|
||||
href: "https://headscale.rul.sh/web/devices.html",
|
||||
icon: "https://raw.githubusercontent.com/juanfont/headscale/refs/heads/main/docs/logo/headscale3-dots.svg",
|
||||
},
|
||||
{
|
||||
name: "Nginx Proxy Manager",
|
||||
href: "http://nginx-pm.home.ip/",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/nginx-proxy-manager.svg",
|
||||
},
|
||||
{
|
||||
name: "Nginx UI",
|
||||
href: "https://nginx-ui.rul.sh/",
|
||||
icon: "https://nginxui.com/assets/icon.svg",
|
||||
},
|
||||
{
|
||||
name: "pgAdmin",
|
||||
href: "http://pgadmin.home.ip/",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/pgadmin.svg",
|
||||
},
|
||||
{
|
||||
name: "phpMyAdmin",
|
||||
href: "http://pma.home.ip/",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/phpmyadmin.svg",
|
||||
},
|
||||
{
|
||||
name: "Mongo Express",
|
||||
href: "http://home:27018/",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/mongodb.svg",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "Hoppscotch",
|
||||
href: "https://hopp.rul.sh/",
|
||||
icon: "https://github.com/walkxcode/dashboard-icons/raw/be82e22c418f5980ee2a13064d50f1483df39c8c/svg/hoppscotch.svg",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "aaPanel",
|
||||
href: "https://10.0.0.102:29760/590d1b7a",
|
||||
icon: "https://www.aapanel.com/static/images/bt_logo.png",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "ProxmoxVE",
|
||||
href: "https://pve.rul.sh/",
|
||||
icon: "https://www.proxmox.com/apple-touch-icon.png",
|
||||
},
|
||||
{
|
||||
name: "LXConsole",
|
||||
href: "https://lxc.rul.sh/",
|
||||
icon: "https://github.com/PenningLabs/lxconsole/raw/refs/heads/main/lxconsole/static/assets/img/logo-light.svg",
|
||||
},
|
||||
{
|
||||
name: "Incus UI",
|
||||
href: "https://164.152.166.61:8443/",
|
||||
icon: "https://linuxcontainers.org/static/img/containers.small.png",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "Incus UI (Armbian)",
|
||||
href: "https://armbian:8443",
|
||||
icon: "https://linuxcontainers.org/static/img/containers.small.png",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
type LauncherItem = {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: string;
|
||||
disabled?: boolean;
|
||||
pinned?: boolean;
|
||||
};
|
||||
|
||||
const openBtn = document.querySelector(".open-launcher") as HTMLButtonElement;
|
||||
const launcher = document.querySelector(".launcher") as HTMLDivElement;
|
||||
const title = launcher.querySelector(".title") as HTMLButtonElement;
|
||||
const itemContainer = launcher.querySelector(".items") as HTMLDivElement;
|
||||
|
||||
let isLauncherOpen = false;
|
||||
|
||||
export const openLauncher = () => {
|
||||
launcher.classList.add("open");
|
||||
launcher.style.opacity = "1";
|
||||
isLauncherOpen = true;
|
||||
};
|
||||
|
||||
export const closeLauncher = () => {
|
||||
launcher.classList.remove("open");
|
||||
launcher.style.opacity = "0";
|
||||
isLauncherOpen = false;
|
||||
};
|
||||
|
||||
const onScroll = (e: WheelEvent) => {
|
||||
if (e.deltaY > 10 && !isLauncherOpen) {
|
||||
openLauncher();
|
||||
return;
|
||||
}
|
||||
|
||||
const curScroll = launcher.scrollTop;
|
||||
if (e.deltaY < -50 && isLauncherOpen && curScroll <= 1) {
|
||||
closeLauncher();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const initLauncher = () => {
|
||||
itemContainer.innerHTML = launcherItems
|
||||
.filter((item) => item.disabled !== true)
|
||||
.map((item) => {
|
||||
return `
|
||||
<a href="${item.href}" class="item" rel="noopener noreferrer" title="${item.name}">
|
||||
<div class="image">
|
||||
<img src="${item.icon}" alt="${item.name}" />
|
||||
</div>
|
||||
<span class="title">${item.name}</span>
|
||||
</a>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
title.addEventListener("click", closeLauncher);
|
||||
openBtn.addEventListener("click", openLauncher);
|
||||
document.addEventListener("wheel", onScroll);
|
||||
|
||||
launcher.addEventListener("click", (e) => {
|
||||
if (e.target === launcher) {
|
||||
closeLauncher();
|
||||
}
|
||||
});
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { initBackground } from "./background";
|
||||
import { initLauncher } from "./launcher";
|
||||
import { initQuickLaunch } from "./quick-launch";
|
||||
import "./style.css";
|
||||
|
||||
const onReady = () => {
|
||||
initBackground();
|
||||
initQuickLaunch();
|
||||
initLauncher();
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", onReady);
|
||||
@@ -0,0 +1,21 @@
|
||||
import { launcherItems } from "./launcher";
|
||||
|
||||
export const initQuickLaunch = () => {
|
||||
const container = document.querySelector(".quick-launch .items");
|
||||
if (!container) {
|
||||
throw new Error("Could not find quick-launch container");
|
||||
}
|
||||
|
||||
const pinnedItems = launcherItems.filter((item) => item.pinned);
|
||||
|
||||
container.innerHTML = pinnedItems
|
||||
.filter((item) => item.disabled !== true)
|
||||
.map((item) => {
|
||||
return `
|
||||
<a href="${item.href}" class="item" rel="noopener noreferrer" title="${item.name}">
|
||||
<img src="${item.icon}" alt="${item.name}" />
|
||||
</a>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
};
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
@apply w-full min-h-screen max-h-dvh relative bg-gray-900 overflow-hidden p-0;
|
||||
}
|
||||
|
||||
.background {
|
||||
@apply absolute left-0 top-0 w-full h-full scale-100 transition-[opacity,_transform] duration-1000 ease-in-out;
|
||||
}
|
||||
|
||||
.background.loaded {
|
||||
@apply scale-105;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
@apply bg-white/80 backdrop-blur-md shadow-lg rounded-full absolute top-[5%] md:top-[10%] left-1/2 -translate-x-1/2 z-[2] flex items-center overflow-hidden w-[90%] max-w-[500px];
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
@apply bg-transparent outline-none p-2 md:p-3 flex-1 w-full min-w-0;
|
||||
}
|
||||
|
||||
.search-bar.loaded {
|
||||
@apply transition-opacity duration-500 ease-in-out delay-300;
|
||||
}
|
||||
|
||||
.quick-launch {
|
||||
@apply absolute bottom-[3%] md:bottom-[10%] left-1/2 -translate-x-1/2 z-[3] w-full flex flex-col items-center justify-center;
|
||||
}
|
||||
|
||||
.quick-launch .open-launcher {
|
||||
@apply rounded-full size-16 mb-2 transition-[background,_transform] ease-in-out duration-700 flex items-center justify-center;
|
||||
}
|
||||
|
||||
.quick-launch .open-launcher:hover {
|
||||
@apply bg-white/10 -translate-y-2;
|
||||
}
|
||||
|
||||
.quick-launch .open-launcher:hover img {
|
||||
@apply scale-y-110;
|
||||
}
|
||||
|
||||
.quick-launch .items {
|
||||
@apply bg-white/10 backdrop-blur-[3px] shadow-lg p-4 rounded-xl flex items-center gap-4;
|
||||
}
|
||||
|
||||
.quick-launch .item {
|
||||
@apply bg-white rounded-xl p-2 aspect-square overflow-hidden block transition-transform flex-shrink-0;
|
||||
}
|
||||
|
||||
.quick-launch .item:hover {
|
||||
@apply scale-110;
|
||||
}
|
||||
|
||||
.quick-launch .item img {
|
||||
@apply size-12 rounded-lg;
|
||||
}
|
||||
|
||||
.quick-launch.loaded {
|
||||
@apply transition-opacity duration-500 ease-in-out delay-200;
|
||||
}
|
||||
|
||||
.launcher {
|
||||
@apply w-full h-screen max-h-dvh overflow-y-auto absolute translate-y-[10%] transition-all duration-300 ease-in-out z-10 pointer-events-none flex flex-col p-4 md:p-8;
|
||||
}
|
||||
|
||||
.launcher.open {
|
||||
@apply translate-y-0 pointer-events-auto backdrop-blur-lg;
|
||||
}
|
||||
|
||||
.launcher .sheet {
|
||||
@apply bg-white/50 backdrop-blur-sm rounded-2xl p-4 md:p-8 w-full max-w-2xl m-auto;
|
||||
}
|
||||
|
||||
.launcher .items {
|
||||
@apply grid grid-cols-3 lg:grid-cols-4 gap-y-4 gap-2 md:gap-4 md:gap-y-6 lg:gap-6 lg:gap-y-10 p-2 md:p-4;
|
||||
}
|
||||
|
||||
.launcher .item {
|
||||
@apply flex flex-col items-center transition-transform;
|
||||
}
|
||||
|
||||
.launcher .item:hover {
|
||||
@apply scale-110;
|
||||
}
|
||||
|
||||
.launcher .item .image {
|
||||
@apply bg-white rounded-xl p-2 aspect-square overflow-hidden;
|
||||
}
|
||||
|
||||
.launcher .item .image img {
|
||||
@apply size-12 md:size-14 rounded-lg;
|
||||
}
|
||||
|
||||
.launcher .item span {
|
||||
@apply text-center text-sm md:text-base leading-4 mt-3 block;
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user