Compare commits

..

7 Commits

Author SHA1 Message Date
M1ngdaXie
c8a6fc2871 feat: add 'go-local-rag-email' project to the Projects list and update terminal commands
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-05-08 09:12:09 +08:00
M1ngdaXie
01a429f8e1 feat: add versioning support for assets and update version to 1.0.0
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-05-07 21:46:53 +08:00
M1ngdaXie
74554210dd feat: enhance user interface with CRT effects, update AboutMe and Projects sections, and add new Collab app
All checks were successful
Deploy / deploy (push) Successful in 14s
2026-05-07 21:42:06 +08:00
M1ngdaXie
38bc80c566 feat(wallpaper): add pixel city wallpaper to the wallpaper collection
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-03-29 17:03:50 -07:00
M1ngdaXie
c0dfdb1e54 fix(deploy): ensure target directory is created before deployment
All checks were successful
Deploy / deploy (push) Successful in 15s
2026-03-26 23:11:15 -07:00
M1ngdaXie
326c4b7a67 refactor(deploy): streamline deployment workflow by consolidating steps
Some checks failed
Deploy / deploy (push) Failing after 19s
2026-03-26 23:00:24 -07:00
M1ngdaXie
dddbc26725 fix(deploy): update branch name from 'main' to 'master' in deployment workflow
Some checks failed
Deploy / deploy (push) Has been cancelled
2026-03-26 22:46:17 -07:00
17 changed files with 354 additions and 71 deletions

View File

@@ -3,7 +3,7 @@ name: Deploy
on: on:
push: push:
branches: branches:
- main - master
jobs: jobs:
deploy: deploy:
@@ -12,19 +12,11 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Node - name: Install & Build
uses: actions/setup-node@v4 run: npm ci && npm run build
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies - name: Deploy
run: npm ci
- name: Build
run: npm run build
- name: Deploy to /var/www
run: | run: |
mkdir -p /var/www/os.m1ngdaxie.com
rm -rf /var/www/os.m1ngdaxie.com/* rm -rf /var/www/os.m1ngdaxie.com/*
cp -r dist/* /var/www/os.m1ngdaxie.com/ cp -r dist/* /var/www/os.m1ngdaxie.com/

View File

@@ -2,8 +2,10 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg?v=1.0.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="version" content="1.0.0" />
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
<title>MingdaOS</title> <title>MingdaOS</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

View File

@@ -1,7 +1,7 @@
{ {
"name": "mingda-os", "name": "mingda-os",
"private": true, "private": true,
"version": "0.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

BIN
public/docnest-icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

26
public/pgp.asc Normal file
View File

@@ -0,0 +1,26 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEafxj9hYJKwYBBAHaRw8BAQdAuem8vfNFdu8MdEgkufBSWXIyYZofTxx/hNqM
WKAoi3O0JE1pbmdkYSBYaWUgPHhpZW1pbmdkYTIwMjBAZ21haWwuY29tPoi1BBMW
CgBdFiEEjFCtXc4OlJ45+vGdYJxwTlRKC/0FAmn8Y/YbFIAAAAAABAAObWFudTIs
Mi41KzEuMTIsMCwzAhsBBQkJZgGABQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheA
AAoJEGCccE5USgv9iugA/2BRU4ooaWF89QigzIyCK0g//qzw//0ZqT0dsKsYhYwX
AQCv+9lJnAJSpV+CsypiKtAWp7Rf3ryim2ZnSE0Jxiy8C7gzBGn8aooWCSsGAQQB
2kcPAQEHQJej8vLd6Zp/iC6j9rd/2jVPqDvS3HjH8eqNHYUpOVruiQERBBgWCgBC
FiEEjFCtXc4OlJ45+vGdYJxwTlRKC/0FAmn8aoobFIAAAAAABAAObWFudTIsMi41
KzEuMTIsMCwzAhsCBQkDwmcAAIEJEGCccE5USgv9diAEGRYKAB0WIQSNntaQmSEM
uam1aJhRySHSOW66vwUCafxqigAKCRBRySHSOW66v/LdAP9co9+y961vHGuVuif4
V5/Ngj9f3zGPJCS2726Dt7HH3gEAhUMV6kIUW0Ii9UXcTi54m+ldHEHcXVyOAlA4
GYtlLwkKDAD7B3FSt615NnFWE522VrmcA/UyrcvvyGMBJTIISuWz5X4A/3msKBYh
V3Ov5ir30JsHdq/34yKCKFDTKdqt3lXjf34MuDgEafxq7xIKKwYBBAGXVQEFAQEH
QHYOZ8u8g9QGrTkj9Hd4/A5JKHCLQb/72PiKrnaTmRB0AwEIB4iaBBgWCgBCFiEE
jFCtXc4OlJ45+vGdYJxwTlRKC/0FAmn8au8bFIAAAAAABAAObWFudTIsMi41KzEu
MTIsMCwzAhsMBQkDwmcAAAoJEGCccE5USgv9gbAA/AvmH45NgIyD8T1qQ+kEtRSv
O+CQLHbOt44R5wOuGMdmAQD/Elb2BuIHeGnFL/Q5x8+irKX59cJhxVWgRxPL0DW3
CLgzBGn8aygWCSsGAQQB2kcPAQEHQH3HCFluWlBCg+wE2z7IlCueCMOLK+ghOtIY
sBzwdVxIiJoEGBYKAEIWIQSMUK1dzg6Unjn68Z1gnHBOVEoL/QUCafxrKBsUgAAA
AAAEAA5tYW51MiwyLjUrMS4xMiwwLDMCGyAFCQPCZwAACgkQYJxwTlRKC/2PwAD+
KVRABLUlz27lVoY27Y54wyDIssIXYvw//2LU92a0SzsBAJLa2F1uihpBO6C5PgoL
F7lvu2C0si9LdM0JclC2UvUD
=opD7
-----END PGP PUBLIC KEY BLOCK-----

BIN
public/wallpaper-city.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 KiB

View File

@@ -37,3 +37,43 @@ html, body, #root {
font-smooth: never; font-smooth: never;
image-rendering: pixelated; image-rendering: pixelated;
} }
/* CRT scanlines + faint phosphor glow over the entire desktop */
#root::after {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9998;
background:
repeating-linear-gradient(
0deg,
rgba(0, 255, 255, 0.025) 0px,
rgba(0, 255, 255, 0.025) 1px,
transparent 1px,
transparent 3px
);
mix-blend-mode: screen;
animation: crt-flicker 4.2s steps(60) infinite;
}
@keyframes crt-flicker {
0%, 100% { opacity: 0.85; }
47% { opacity: 0.92; }
50% { opacity: 0.6; }
53% { opacity: 0.9; }
}
@keyframes blink-caret {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
.cypher-caret {
display: inline-block;
width: 0.55em;
margin-left: 2px;
background: var(--pixel-cyan);
color: transparent;
animation: blink-caret 1s steps(1) infinite;
}

View File

@@ -1,36 +1,179 @@
import { APP_VERSION } from '../config/version';
export default function AboutMe() { export default function AboutMe() {
return ( return (
<div style={{ padding: '24px', color: '#fff', fontFamily: 'var(--font-ui)' }}> <div
<div style={{ display: 'flex', gap: 20, alignItems: 'flex-start', marginBottom: 20 }}> style={{ padding: "24px", color: "#fff", fontFamily: "var(--font-ui)" }}
<div style={{ >
width: 72, height: 72, borderRadius: '50%', <div
background: 'linear-gradient(135deg, #0a84ff, #bf5af2)', style={{
display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: "var(--font-mono)",
fontSize: 36, flexShrink: 0 fontSize: 12,
}}>👤</div> color: "var(--pixel-cyan)",
marginBottom: 16,
letterSpacing: 0.5,
}}
>
<span style={{ opacity: 0.5 }}>mingda@doitou-sv:~$ </span>
whoami<span className="cypher-caret"></span>
</div>
<div
style={{
display: "flex",
gap: 20,
alignItems: "flex-start",
marginBottom: 20,
}}
>
<div
style={{
width: 72,
height: 72,
borderRadius: "50%",
background: "linear-gradient(135deg, #0a84ff, #bf5af2)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 36,
flexShrink: 0,
}}
>
👤
</div>
<div> <div>
<h2 style={{ fontSize: 20, fontWeight: 600, marginBottom: 4 }}>Mingda Xie <span style={{ color: 'rgba(255,255,255,0.4)', fontWeight: 400, fontSize: 14 }}></span></h2> <h2 style={{ fontSize: 20, fontWeight: 600, marginBottom: 4 }}>
<p style={{ fontSize: 13, color: 'rgba(255,255,255,0.6)', lineHeight: 1.5 }}>Backend Developer · CS Master's @ Northeastern University</p> Mingda Xie{" "}
<p style={{ fontSize: 13, color: 'rgba(255,255,255,0.5)' }}>Shenzhen-bound · Class of 2026</p> <span
style={{
color: "rgba(255,255,255,0.4)",
fontWeight: 400,
fontSize: 14,
}}
>
</span>
</h2>
<p
style={{
fontSize: 13,
color: "rgba(255,255,255,0.6)",
lineHeight: 1.5,
}}
>
Backend Developer · CS Master's @ Northeastern University
</p>
</div> </div>
</div> </div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 20 }}> <div
{['Java/Spring Cloud', 'Go', 'TypeScript/Bun', 'Redis', 'k3s', 'Microservices'].map(tag => ( style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 20 }}
<span key={tag} style={{ >
padding: '3px 10px', borderRadius: 20, {[
background: 'rgba(10, 132, 255, 0.15)', "Java/Spring Cloud",
border: '1px solid rgba(10,132,255,0.3)', "Go",
fontSize: 12, color: '#64d2ff' "TypeScript/Bun",
}}>{tag}</span> "Redis",
"k3s",
"Microservices",
].map((tag) => (
<span
key={tag}
style={{
padding: "2px 8px",
borderRadius: 0,
background: "rgba(0,255,255,0.06)",
border: "1px solid rgba(0,255,255,0.25)",
fontSize: 11,
fontFamily: "var(--font-mono)",
color: "var(--pixel-cyan)",
letterSpacing: 0.3,
}}
>
[{tag}]
</span>
))} ))}
</div> </div>
<p style={{ fontSize: 13, color: 'rgba(255,255,255,0.7)', lineHeight: 1.7, marginBottom: 16 }}> <p
Backend engineer who runs a personal k3s cluster on Vultr, ships microservices, and builds things that occasionally work in production. style={{
LeetCode 400+. Pixel art enjoyer. Stardew Valley veteran. Huge fan of 土屋アンナ. fontSize: 13,
fontFamily: "var(--font-mono)",
color: "rgba(255,255,255,0.7)",
lineHeight: 1.7,
marginBottom: 16,
}}
>
<span style={{ color: "rgba(0,255,255,0.45)" }}>// </span>
Backend engineer. Distributed systems, infra, and the long tail of
things that go wrong at 3am. Trust the math, not the institution.
</p> </p>
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}> {/* <p style={{ fontSize: 12, color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}>
"I am the dragon scroll, bitch." Genuine mastery over shortcuts. "I am the dragon scroll, bitch." Genuine mastery over shortcuts.
</p> </p> */}
<div
style={{
marginTop: 8,
padding: "12px 14px",
background: "rgba(0,0,0,0.25)",
border: "1px solid rgba(255,255,255,0.08)",
borderRadius: 8,
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 8,
}}
>
<span
style={{
fontSize: 11,
color: "rgba(255,255,255,0.4)",
letterSpacing: 0.5,
textTransform: "uppercase",
}}
>
Signed Identity
</span>
<a
href={`/pgp.asc?v=${APP_VERSION}`}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: 11,
color: "#64d2ff",
textDecoration: "none",
}}
>
pgp.asc
</a>
</div>
<pre
style={{
margin: 0,
fontFamily: "var(--font-mono)",
fontSize: 11,
lineHeight: 1.5,
color: "rgba(255,255,255,0.65)",
whiteSpace: "pre-wrap",
wordBreak: "break-all",
}}
>{`-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
identity: Mingda Xie
email: xiemingda2020@gmail.com
pgp: 8C50 AD5D CE0E 949E 39FA F19D 609C 704E 544A 0BFD
-----BEGIN PGP SIGNATURE-----
iJEEARYKADkWIQSNntaQmSEMuam1aJhRySHSOW66vwUCafyKpRsUgAAAAAAEAA5t
YW51MiwyLjUrMS4xMiwwLDMACgkQUckh0jluur8brQD/RzlXBuVuQnGioNsibNDm
Rm85/5ed2BROS5gaH4FCBrQBALjMapIlyCYBNvIZNQX1eFCY/WNfZQXe3woeZYlP
E/IF
=k1lO
-----END PGP SIGNATURE-----`}</pre>
</div>
</div> </div>
); );
} }

View File

@@ -1,7 +1,7 @@
const PROJECTS = [ const PROJECTS = [
{ name: 'CampusNest', desc: 'Campus housing platform with Spring Cloud microservices, Redis caching, and k3s deployment.', tags: ['Java', 'Spring Cloud', 'Redis', 'k3s'], color: '#0a84ff' }, { name: 'CampusNest', desc: 'Campus housing platform with Spring Cloud microservices, Redis caching, and k3s deployment.', tags: ['Java', 'Spring Cloud', 'Redis', 'k3s'], color: '#0a84ff' },
{ name: 'Alfred Bot', desc: 'Personal AI agent — task management, reminders, home automation bridge.', tags: ['Go', 'TypeScript', 'Bun'], color: '#bf5af2' }, { name: 'Alfred Bot', desc: 'Personal AI agent — task management, reminders, home automation bridge.', tags: ['Go', 'TypeScript', 'Bun'], color: '#bf5af2' },
{ name: 'Warden / Bastion', desc: 'Self-hosted auth and proxy layer running on k3s with Let\'s Encrypt.', tags: ['Nginx', 'k3s', 'VLESS'], color: '#30d158' }, { name: 'go-local-rag-email', desc: 'Local-first Gmail assistant: semantic search and AI summaries via RAG, runs offline.', tags: ['Go', 'Qdrant', 'SQLite', 'RAG'], color: '#00ffff' },
{ name: 'Collab Platform', desc: 'Real-time collaborative workspace with WebSocket rooms and conflict resolution.', tags: ['TypeScript', 'Bun', 'WebSocket'], color: '#ffd60a' }, { name: 'Collab Platform', desc: 'Real-time collaborative workspace with WebSocket rooms and conflict resolution.', tags: ['TypeScript', 'Bun', 'WebSocket'], color: '#ffd60a' },
{ name: '成语填空', desc: 'Daily Chinese idiom guessing game — Wordle-style, runs at chengyu.m1ngdaxie.com.', tags: ['React', 'TypeScript'], color: '#ff453a' }, { name: '成语填空', desc: 'Daily Chinese idiom guessing game — Wordle-style, runs at chengyu.m1ngdaxie.com.', tags: ['React', 'TypeScript'], color: '#ff453a' },
]; ];

View File

@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from 'react';
import type { KeyboardEvent } from 'react'; import type { KeyboardEvent } from 'react';
import { useEffect, useRef, useState } from 'react';
interface Line { type: 'input' | 'output'; text: string; key: number } interface Line { type: 'input' | 'output'; text: string; key: number }
@@ -8,12 +8,34 @@ const COMMANDS: Record<string, string[]> = {
'Available commands:', 'Available commands:',
' help — show this list', ' help — show this list',
' about — who is mingda', ' about — who is mingda',
' whoami — current identity',
' projects — list projects', ' projects — list projects',
' skills — tech stack', ' skills — tech stack',
' contact — contact info', ' contact — contact info',
' pgp — public key fingerprint',
' motd — message of the day',
' neofetch — system info', ' neofetch — system info',
' clear — clear terminal', ' clear — clear terminal',
], ],
whoami: [
'mingda',
'uid=1000 groups=(wheel,crypto,sudoers)',
'shell=/bin/zsh tty=/dev/pts/0',
],
pgp: [
'PGP fingerprint:',
' 8C50 AD5D CE0E 949E 39FA F19D 609C 704E 544A 0BFD',
'',
'Public key: /pgp.asc',
'Verify before you trust.',
],
motd: [
'cypherpunks write code.',
'privacy is necessary for an open society in the electronic age.',
'we cannot expect governments, corporations, or other large,',
'faceless organizations to grant us privacy out of their beneficence.',
' — eric hughes, 1993',
],
about: [ about: [
'Mingda Xie (解明达)', 'Mingda Xie (解明达)',
'Backend Developer · CS Master\'s @ Northeastern University', 'Backend Developer · CS Master\'s @ Northeastern University',
@@ -28,7 +50,7 @@ const COMMANDS: Record<string, string[]> = {
'Projects:', 'Projects:',
' CampusNest — Spring Cloud housing platform', ' CampusNest — Spring Cloud housing platform',
' Alfred Bot — Self-hosted AI agent', ' Alfred Bot — Self-hosted AI agent',
' Warden/Bastion — k3s auth + proxy layer', ' rag-email — Local-first Gmail RAG assistant (Go + Qdrant)',
' Collab Platform— Real-time collaborative workspace', ' Collab Platform— Real-time collaborative workspace',
' 成语填空 — Daily Chinese idiom game', ' 成语填空 — Daily Chinese idiom game',
], ],
@@ -55,7 +77,7 @@ const COMMANDS: Record<string, string[]> = {
' .::!!!::. Kernel: Ubuntu 24.04', ' .::!!!::. Kernel: Ubuntu 24.04',
' .:::!!!:::. RAM: 4GB', ' .:::!!!:::. RAM: 4GB',
' .::::!!!::::. Shell: zsh', ' .::::!!!::::. Shell: zsh',
' .:::::!!!:::::. Services: Alfred, Chengyu, k3s, VLESS', ' .:::::!!!:::::. Services: Alfred, Chengyu, k3s',
'', '',
' nginx + k3s + let\'s encrypt', ' nginx + k3s + let\'s encrypt',
], ],
@@ -68,8 +90,9 @@ function nextKey() { return ++lineCounter; }
export default function Terminal() { export default function Terminal() {
const [lines, setLines] = useState<Line[]>([ const [lines, setLines] = useState<Line[]>([
{ type: 'output', text: 'MingdaOS Terminal v1.0', key: nextKey() }, { type: 'output', text: 'MingdaOS Terminal v1.0 [secure tty]', key: nextKey() },
{ type: 'output', text: 'Type "help" to see available commands.', key: nextKey() }, { type: 'output', text: 'kernel: linux 6.x | tls: 1.3 | pgp: 8C50AD5D...0BFD', key: nextKey() },
{ type: 'output', text: 'cypherpunks write code. type "help" for commands.', key: nextKey() },
{ type: 'output', text: '', key: nextKey() }, { type: 'output', text: '', key: nextKey() },
]); ]);
const [input, setInput] = useState(''); const [input, setInput] = useState('');

View File

@@ -2,6 +2,7 @@
position: fixed; position: fixed;
inset: 0; inset: 0;
background-size: 24px 24px, 12px 12px, cover; background-size: 24px 24px, 12px 12px, cover;
background-position: center;
overflow: hidden; overflow: hidden;
} }

View File

@@ -31,7 +31,10 @@ export default function Desktop() {
return ( return (
<div <div
className="desktop" className="desktop"
style={{ background: WALLPAPERS[state.wallpaper] }} style={{
background: WALLPAPERS[state.wallpaper],
backgroundSize: WALLPAPERS[state.wallpaper].startsWith('url(') ? 'cover' : undefined,
}}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
onClick={() => setCtxMenu(null)} onClick={() => setCtxMenu(null)}
> >
@@ -47,12 +50,19 @@ export default function Desktop() {
gridRow: app.desktopPosition.row + 1, gridRow: app.desktopPosition.row + 1,
}} }}
> >
<DesktopIcon app={app} onOpen={() => openWindow(app.id)} /> <DesktopIcon
app={app}
onOpen={() =>
app.externalUrl
? window.open(app.externalUrl, '_blank', 'noopener,noreferrer')
: openWindow(app.id)
}
/>
</div> </div>
))} ))}
</div> </div>
{APPS.map(app => ( {APPS.filter(app => !app.externalUrl).map(app => (
<Window key={app.id} app={app} /> <Window key={app.id} app={app} />
))} ))}

View File

@@ -10,7 +10,9 @@ export default function DesktopIcon({ app, onOpen }: Props) {
return ( return (
<div className="desktop-icon" onDoubleClick={onOpen}> <div className="desktop-icon" onDoubleClick={onOpen}>
<div className="desktop-icon-img" style={{ background: app.iconGradient }}> <div className="desktop-icon-img" style={{ background: app.iconGradient }}>
<span>{app.emoji}</span> {app.iconImage
? <img src={app.iconImage} alt={app.title} style={{ width: '70%', height: '70%', objectFit: 'contain' }} />
: <span>{app.emoji}</span>}
</div> </div>
<span className="desktop-icon-label">{app.title}</span> <span className="desktop-icon-label">{app.title}</span>
</div> </div>

View File

@@ -1,31 +1,53 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import './MenuBar.css'; import './MenuBar.css';
import { APP_VERSION } from '../../config/version';
const MENU_ITEMS = ['File', 'Edit', 'View', 'Go', 'Help']; const MENU_ITEMS = ['encrypt', 'sign', 'verify', 'wipe', 'about'];
const BOOT_AT = Date.now();
export default function MenuBar() { export default function MenuBar() {
const [time, setTime] = useState(() => formatTime(new Date())); const [time, setTime] = useState(() => formatTime(new Date()));
const [uptime, setUptime] = useState(() => formatUptime(0));
useEffect(() => { useEffect(() => {
const id = setInterval(() => setTime(formatTime(new Date())), 1000); const id = setInterval(() => {
setTime(formatTime(new Date()));
setUptime(formatUptime(Date.now() - BOOT_AT));
}, 1000);
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
return ( return (
<div className="menubar"> <div className="menubar">
<div className="menubar-left"> <div className="menubar-left">
<span className="menubar-logo">&#63743;</span> <span className="menubar-logo">[ MingdaOS v{APP_VERSION} ]</span>
{MENU_ITEMS.map(item => ( {MENU_ITEMS.map(item => (
<span key={item} className="menubar-item">{item}</span> <span key={item} className="menubar-item">{item}</span>
))} ))}
</div> </div>
<div className="menubar-right"> <div className="menubar-right">
<span className="menubar-clock">{time}</span> <span className="menubar-clock" style={{ opacity: 0.55, marginRight: 12 }}>
up {uptime}
</span>
<span className="menubar-clock">
{time}<span className="cypher-caret"></span>
</span>
</div> </div>
</div> </div>
); );
} }
function formatTime(d: Date) { function formatTime(d: Date) {
return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); const hh = String(d.getUTCHours()).padStart(2, '0');
const mm = String(d.getUTCMinutes()).padStart(2, '0');
const ss = String(d.getUTCSeconds()).padStart(2, '0');
return `${hh}:${mm}:${ss} UTC`;
}
function formatUptime(ms: number) {
const s = Math.floor(ms / 1000);
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
return `${String(h).padStart(2, '0')}h${String(m).padStart(2, '0')}m${String(sec).padStart(2, '0')}s`;
} }

View File

@@ -7,6 +7,7 @@ import Alfred from '../apps/Alfred';
import Chengyu from '../apps/Chengyu'; import Chengyu from '../apps/Chengyu';
import Poker from '../apps/Poker'; import Poker from '../apps/Poker';
import Trash from '../apps/Trash'; import Trash from '../apps/Trash';
import { withVersion } from './version';
export const APPS: AppConfig[] = [ export const APPS: AppConfig[] = [
{ {
@@ -89,6 +90,18 @@ export const APPS: AppConfig[] = [
component: Trash, component: Trash,
desktopPosition: { col: 1, row: 3 }, desktopPosition: { col: 1, row: 3 },
}, },
{
id: 'collab',
title: 'Collab',
emoji: '',
iconGradient: 'linear-gradient(135deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02))',
iconImage: withVersion('/docnest-icon-32.png'),
defaultSize: { width: 400, height: 360 },
defaultPosition: { x: 0, y: 0 },
component: Links,
desktopPosition: { col: 0, row: 4 },
externalUrl: 'https://collab.m1ngdaxie.com/',
},
]; ];
export const WALLPAPERS = [ export const WALLPAPERS = [
@@ -107,4 +120,6 @@ export const WALLPAPERS = [
linear-gradient(135deg, #050510 0%, #06060f 100%)`, linear-gradient(135deg, #050510 0%, #06060f 100%)`,
// Solid dark — flat and minimal // Solid dark — flat and minimal
`linear-gradient(135deg, #050510 0%, #080818 100%)`, `linear-gradient(135deg, #050510 0%, #080818 100%)`,
// Pixel city — neon cityscape
`url(${withVersion('/wallpaper-city.png')})`,
]; ];

5
src/config/version.ts Normal file
View File

@@ -0,0 +1,5 @@
export const APP_VERSION = '1.0.0';
export function withVersion(url: string): string {
return url.includes('?') ? `${url}&v=${APP_VERSION}` : `${url}?v=${APP_VERSION}`;
}

View File

@@ -21,6 +21,8 @@ export interface AppConfig {
defaultPosition: { x: number; y: number }; defaultPosition: { x: number; y: number };
component: ComponentType; component: ComponentType;
desktopPosition: { col: number; row: number }; desktopPosition: { col: number; row: number };
externalUrl?: string;
iconImage?: string;
} }
export interface OSState { export interface OSState {