feat: initial commit

This commit is contained in:
Khairul Hidayat 2024-11-03 11:08:53 +00:00
commit 44decd4e7b
26 changed files with 4819 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
._.DS_Store

42
index.html Normal file
View File

@ -0,0 +1,42 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>✦ Eclair ✦</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
</head>
<body>
<div id="app">
<canvas class="background" id="bg-hash"></canvas>
<div class="background bg-no-repeat bg-cover bg-center z-[1]" id="bg-main" style="opacity: 0;"></div>
<form action="https://www.google.com/search" method="get" class="search-bar" style="opacity: 0;">
<img src="/images/google_logo.svg" class="w-8 h-8 ml-1.5 md:ml-2" alt="google" />
<input name="q" placeholder="Search" autofocus />
</form>
<div class="quick-launch" style="opacity: 0;">
<button type="button" title="Open Launcher" class="open-launcher"><img src="/images/chevron-up.svg"
class="transition-transform" alt="open launcher" /></button>
<div class="items"></div>
</div>
<div class="launcher" style="opacity: 0;">
<div class="sheet">
<div class="title flex items-center gap-4 ml-4 md:ml-0" role="button">
<img src="/images/layout-panel-left.svg" class="w-6 h-6" alt="launcher" />
<p class="text-2xl font-medium">Apps</p>
</div>
<div class="items mt-4"></div>
</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "launcher",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"typescript": "~5.6.2",
"vite": "^5.4.10",
"vite-plugin-pwa": "^0.20.5"
},
"dependencies": {
"blurhash": "^2.0.5"
}
}

4239
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/android-chrome-192x192.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
public/android-chrome-512x512.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

BIN
public/apple-touch-icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
public/favicon-16x16.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

BIN
public/favicon-32x32.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
public/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

BIN
public/images/1226538.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

1
public/images/chevron-up.svg Executable file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up"><path d="m18 15-6-6-6 6"/></svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/><path d="M1 1h22v22H1z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 742 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-layout-panel-left"><rect width="7" height="18" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/></svg>

After

Width:  |  Height:  |  Size: 372 B

19
public/site.webmanifest Executable file
View File

@ -0,0 +1,19 @@
{
"name": "Eclair Launcher",
"short_name": "Launcher",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

56
src/background.ts Normal file
View File

@ -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
src/launcher.ts Normal file
View File

@ -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
src/main.ts Normal file
View File

@ -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);

21
src/quick-launch.ts Normal file
View File

@ -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
src/style.css Normal file
View File

@ -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;
}

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

8
tailwind.config.js Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};

24
tsconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

30
vite.config.js Normal file
View File

@ -0,0 +1,30 @@
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
export default defineConfig({
plugins: [
VitePWA({
registerType: "autoUpdate",
manifest: {
name: "Eclair Launcher",
short_name: "Launcher",
start_url: "https://home.rul.sh",
icons: [
{
src: "/android-chrome-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/android-chrome-512x512.png",
sizes: "512x512",
type: "image/png",
},
],
theme_color: "#ffffff",
background_color: "#ffffff",
display: "standalone",
},
}),
],
});