764 lines
29 KiB
HTML
764 lines
29 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=300, initial-scale=1" />
|
|
<title>Tray</title>
|
|
<style>
|
|
:root {
|
|
--bg: #141b26;
|
|
--panel: #19212b;
|
|
--text: #f4f5f8;
|
|
--muted: #929bac;
|
|
--line: rgba(255,255,255,.08);
|
|
--hover: rgba(255,255,255,.07);
|
|
--accent: #6ee7ff;
|
|
--violet: #7367f5;
|
|
--ok: #43c996;
|
|
--warn: #e7bd35;
|
|
--danger: #f05e68;
|
|
}
|
|
html, body {
|
|
margin: 0;
|
|
width: 300px;
|
|
height: 348px;
|
|
overflow: hidden;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font: 650 12px/1.35 "Segoe UI Variable", "Segoe UI", Nunito, sans-serif;
|
|
-webkit-font-smoothing: antialiased;
|
|
text-rendering: geometricPrecision;
|
|
user-select: none;
|
|
}
|
|
html[data-theme=light] {
|
|
--bg: #eef1f6;
|
|
--panel: #fff;
|
|
--text: #111827;
|
|
--muted: #526071;
|
|
--line: rgba(15,23,42,.08);
|
|
--hover: rgba(15,23,42,.05);
|
|
}
|
|
#app {
|
|
box-sizing: border-box;
|
|
width: 300px;
|
|
height: 348px;
|
|
padding: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
button {
|
|
border: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
font: inherit;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
}
|
|
.head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-height: 28px;
|
|
padding: 0 8px;
|
|
flex: none;
|
|
}
|
|
.head b {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-size: 12px;
|
|
}
|
|
.dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--muted);
|
|
box-shadow: 0 0 0 3px rgba(146,155,172,.14);
|
|
flex: none;
|
|
}
|
|
.st-online .dot { background: var(--ok); box-shadow: 0 0 0 3px rgba(67,201,150,.16); }
|
|
.st-offline .dot { background: var(--warn); box-shadow: 0 0 0 3px rgba(231,189,53,.14); }
|
|
.st-error .dot { background: var(--danger); box-shadow: 0 0 0 3px rgba(240,94,104,.14); }
|
|
.st-syncing .dot { background: #4f9df5; box-shadow: 0 0 0 3px rgba(79,157,245,.14); }
|
|
.badge {
|
|
min-width: 18px;
|
|
height: 18px;
|
|
padding: 0 6px;
|
|
border-radius: 999px;
|
|
background: rgba(115,103,245,.16);
|
|
color: #c4bfff;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
font-variant-numeric: tabular-nums;
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
.badge.due { background: rgba(231,189,53,.16); color: var(--warn); }
|
|
.item {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-width: 0;
|
|
color: inherit;
|
|
transition: background .16s ease, color .16s ease;
|
|
}
|
|
.item:hover { background: var(--hover); }
|
|
.item:focus-visible { outline: 2px solid var(--violet); outline-offset: -2px; }
|
|
.item i {
|
|
width: 16px;
|
|
height: 16px;
|
|
flex: none;
|
|
color: var(--muted);
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
.item i svg { width: 15px; height: 15px; display: block; }
|
|
.item span {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.item.on { background: rgba(115,103,245,.12); }
|
|
.item.on i { color: #c4bfff; }
|
|
.item.danger, .item.danger i { color: var(--danger); }
|
|
.sec {
|
|
margin: 2px 4px 0;
|
|
color: var(--muted);
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: .4px;
|
|
}
|
|
.projects { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
.chip {
|
|
max-width: 100%;
|
|
padding: 4px 8px;
|
|
border-radius: 999px;
|
|
background: var(--hover);
|
|
font-size: 11px;
|
|
}
|
|
|
|
html.skin-liquid, body.skin-liquid {
|
|
background:
|
|
radial-gradient(140px 90px at 8% -10%, rgba(255,255,255,.18), transparent 70%),
|
|
linear-gradient(180deg, #3a4252, #252b38);
|
|
}
|
|
.skin-liquid #app { gap: 8px; }
|
|
.skin-liquid .head {
|
|
height: 32px;
|
|
border-radius: 999px;
|
|
background: rgba(255,255,255,.1);
|
|
}
|
|
.skin-liquid .tiles {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
.skin-liquid .tile {
|
|
height: auto;
|
|
min-height: 64px;
|
|
padding: 10px 12px;
|
|
border-radius: 16px;
|
|
background: rgba(255,255,255,.08);
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
}
|
|
.skin-liquid .tile i {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 9px;
|
|
background: rgba(255,255,255,.1);
|
|
color: #e8eef8;
|
|
}
|
|
.skin-liquid .tile i svg { width: 16px; height: 16px; }
|
|
.skin-liquid .tile span { font-size: 12px; }
|
|
.skin-liquid .dock {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 6px;
|
|
}
|
|
.skin-liquid .dock .item {
|
|
flex: 1;
|
|
height: 36px;
|
|
justify-content: center;
|
|
border-radius: 12px;
|
|
background: rgba(255,255,255,.07);
|
|
}
|
|
.skin-liquid .dock .item span { display: none; }
|
|
.skin-liquid .foot {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr 1fr;
|
|
gap: 6px;
|
|
margin-top: auto;
|
|
}
|
|
.skin-liquid .foot .item {
|
|
height: 30px;
|
|
justify-content: center;
|
|
border-radius: 999px;
|
|
font-size: 11px;
|
|
}
|
|
|
|
html.skin-glass, body.skin-glass {
|
|
background:
|
|
radial-gradient(180px 120px at 100% 0%, rgba(115,103,245,.18), transparent 60%),
|
|
linear-gradient(180deg, #2a3342, #171d27);
|
|
}
|
|
.skin-glass #app { padding: 10px; gap: 8px; }
|
|
.skin-glass .head {
|
|
height: 36px;
|
|
padding: 0 12px;
|
|
border-radius: 10px;
|
|
background: rgba(255,255,255,.06);
|
|
}
|
|
.skin-glass .head b { font-size: 13px; font-weight: 700; }
|
|
.skin-glass .qs {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
}
|
|
.skin-glass .qs-tile {
|
|
height: 56px;
|
|
padding: 0 12px;
|
|
border-radius: 12px;
|
|
background: rgba(25,33,43,.82);
|
|
box-shadow: inset 0 1px 0 rgba(255,255,255,.06);
|
|
}
|
|
.skin-glass .qs-tile i {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 8px;
|
|
background: rgba(115,103,245,.2);
|
|
color: #c4bfff;
|
|
}
|
|
.skin-glass .qs-tile i svg { width: 16px; height: 16px; }
|
|
.skin-glass .qs-tile span { font-size: 13px; font-weight: 700; }
|
|
.skin-glass .qs-tile:hover { background: rgba(36,46,60,.95); }
|
|
.skin-glass .sys {
|
|
display: grid;
|
|
grid-template-columns: repeat(6, 1fr);
|
|
gap: 6px;
|
|
padding: 6px;
|
|
border-radius: 12px;
|
|
background: rgba(25,33,43,.7);
|
|
}
|
|
.skin-glass .sys .item {
|
|
height: 36px;
|
|
justify-content: center;
|
|
border-radius: 8px;
|
|
}
|
|
.skin-glass .sys .item span { display: none; }
|
|
.skin-glass .sys .item i { width: 18px; height: 18px; color: #c5cbe0; }
|
|
.skin-glass .sys .item:hover { background: rgba(255,255,255,.08); }
|
|
.skin-glass .quit {
|
|
height: 32px;
|
|
justify-content: center;
|
|
border-radius: 10px;
|
|
color: #f05e68;
|
|
background: rgba(240,94,104,.08);
|
|
}
|
|
|
|
html.skin-compact, body.skin-compact { background: #11151c; }
|
|
.skin-compact #app { padding: 10px 8px 8px; gap: 6px; }
|
|
.skin-compact .head { min-height: 20px; padding: 0 6px; }
|
|
.skin-compact .head b { font-size: 11px; color: var(--muted); font-weight: 600; }
|
|
.skin-compact .pad {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 4px 2px;
|
|
}
|
|
.skin-compact .item {
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
height: 76px;
|
|
border-radius: 10px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: .2px;
|
|
text-align: center;
|
|
}
|
|
.skin-compact .item i {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 12px;
|
|
background: #1b2230;
|
|
color: #d5dbeb;
|
|
}
|
|
.skin-compact .item i svg { width: 18px; height: 18px; }
|
|
.skin-compact .item:hover { background: transparent; }
|
|
.skin-compact .item:hover i { background: #252e40; }
|
|
.skin-compact .quit {
|
|
height: 24px;
|
|
justify-content: center;
|
|
color: var(--muted);
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
.skin-compact .quit i { display: none; }
|
|
|
|
html.skin-status, body.skin-status { background: #19212b; }
|
|
.skin-status #app { padding: 10px; gap: 8px; }
|
|
.skin-status .hero { display: flex; flex-direction: column; gap: 8px; flex: none; }
|
|
.skin-status .hero-user {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
height: 24px;
|
|
padding: 0 2px;
|
|
border-radius: 6px;
|
|
}
|
|
.skin-status .hero-user:hover { background: var(--hover); }
|
|
.skin-status .hero-user:focus-visible { outline: 2px solid var(--violet); outline-offset: 1px; }
|
|
.skin-status .hero-user b {
|
|
min-width: 0;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.skin-status .metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
|
.skin-status .metric {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
height: 40px;
|
|
padding: 0 12px;
|
|
border-radius: 10px;
|
|
background: #222b38;
|
|
transition: background .16s ease;
|
|
}
|
|
.skin-status .metric:hover { background: #2a3546; }
|
|
.skin-status .metric:focus-visible { outline: 2px solid var(--violet); outline-offset: 1px; }
|
|
.skin-status .metric strong {
|
|
font-size: 18px;
|
|
font-weight: 750;
|
|
font-variant-numeric: tabular-nums;
|
|
color: #c4bfff;
|
|
line-height: 1;
|
|
}
|
|
.skin-status .metric.due strong { color: #e7bd35; }
|
|
.skin-status .metric.empty strong { color: #6b7384; }
|
|
.skin-status .metric span { font-size: 11px; color: var(--muted); }
|
|
.skin-status .focus,
|
|
.skin-status .open { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; flex: none; }
|
|
.skin-status .focus .item,
|
|
.skin-status .work .item,
|
|
.skin-status .open .item {
|
|
height: 48px;
|
|
padding: 0 12px;
|
|
border-radius: 10px;
|
|
background: #222b38;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
}
|
|
.skin-status .focus .item i { width: 18px; height: 18px; color: #c4bfff; }
|
|
.skin-status .focus .item i svg { width: 17px; height: 17px; }
|
|
.skin-status .work {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
grid-template-rows: 1fr 1fr;
|
|
gap: 6px;
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
.skin-status .work .item { height: auto; min-height: 0; font-size: 12px; font-weight: 650; }
|
|
.skin-status .work .item i { color: #b8c0d4; }
|
|
.skin-status .open .item {
|
|
height: 34px;
|
|
background: rgba(255,255,255,.04);
|
|
font-size: 12px;
|
|
font-weight: 650;
|
|
}
|
|
.skin-status .item:hover { background: #2a3546; }
|
|
.skin-status .open .item:hover { background: rgba(255,255,255,.08); }
|
|
.skin-status .item.on { background: rgba(115,103,245,.14); }
|
|
.skin-status .item.on i { color: #c4bfff; }
|
|
.skin-status .projects { padding: 0 2px; }
|
|
.skin-status .foot { display: flex; flex-direction: column; gap: 4px; flex: none; }
|
|
.skin-status .sys { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
|
|
.skin-status .sys .item {
|
|
height: 32px;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
border-radius: 8px;
|
|
font-size: 10px;
|
|
font-weight: 650;
|
|
color: var(--muted);
|
|
}
|
|
.skin-status .sys .item i { color: #929bac; }
|
|
.skin-status .sys .item:hover { background: #222b38; color: var(--text); }
|
|
.skin-status .quit {
|
|
height: 26px;
|
|
justify-content: center;
|
|
border-radius: 8px;
|
|
color: #f05e68;
|
|
font-size: 12px;
|
|
}
|
|
.skin-status .quit:hover { background: rgba(240,94,104,.1); }
|
|
|
|
html.skin-neon, body.skin-neon { background: #151b24; }
|
|
.skin-neon #app { padding: 0; gap: 0; }
|
|
.skin-neon .head {
|
|
height: 40px;
|
|
padding: 0 12px;
|
|
border-bottom: 1px solid #303949;
|
|
background: #151b24;
|
|
}
|
|
.skin-neon .head b { font-size: 13px; font-weight: 700; color: #f4f5f8; }
|
|
.skin-neon .split {
|
|
display: grid;
|
|
grid-template-columns: 56px 1fr;
|
|
min-height: 0;
|
|
flex: 1;
|
|
}
|
|
.skin-neon .rail {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
padding: 8px;
|
|
border-right: 1px solid #303949;
|
|
background: #121820;
|
|
}
|
|
.skin-neon .rail .item {
|
|
flex: 1;
|
|
justify-content: center;
|
|
border-radius: 10px;
|
|
border: 0;
|
|
background: transparent;
|
|
}
|
|
.skin-neon .rail .item span {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
overflow: hidden;
|
|
clip: rect(0,0,0,0);
|
|
}
|
|
.skin-neon .rail .item i { width: 18px; height: 18px; color: #929bac; }
|
|
.skin-neon .rail .item i svg { width: 18px; height: 18px; }
|
|
.skin-neon .rail .item:hover,
|
|
.skin-neon .rail .item.on { background: #252e40; }
|
|
.skin-neon .rail .item:hover i,
|
|
.skin-neon .rail .item.on i { color: #c4bfff; }
|
|
.skin-neon .side {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
padding: 4px 8px 6px;
|
|
}
|
|
.skin-neon .sec {
|
|
margin: 4px 8px 0;
|
|
height: 14px;
|
|
line-height: 14px;
|
|
color: #929bac;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: .08em;
|
|
}
|
|
.skin-neon .list { display: flex; flex-direction: column; gap: 1px; }
|
|
.skin-neon .list .item {
|
|
height: 28px;
|
|
padding: 0 10px;
|
|
border-radius: 8px;
|
|
border-bottom: 0;
|
|
}
|
|
.skin-neon .list .item i { color: #929bac; }
|
|
.skin-neon .item:hover { background: rgba(255,255,255,.06); }
|
|
.skin-neon .item.on { background: rgba(115,103,245,.12); box-shadow: none; }
|
|
.skin-neon .item.on i { color: #c4bfff; }
|
|
.skin-neon .quit {
|
|
height: 28px;
|
|
margin-top: auto;
|
|
justify-content: flex-start;
|
|
padding: 0 10px;
|
|
border-top: 1px solid #303949;
|
|
border-radius: 0;
|
|
color: #f05e68;
|
|
}
|
|
.skin-neon .quit i { color: inherit; }
|
|
.skin-neon .quit:hover { background: rgba(240,94,104,.1); }
|
|
|
|
html[data-theme=light].skin-liquid, html[data-theme=light] body.skin-liquid {
|
|
background: linear-gradient(180deg, #f7f8fb, #e7ebf2);
|
|
color: #0f172a;
|
|
}
|
|
html[data-theme=light] .skin-liquid .tile,
|
|
html[data-theme=light] .skin-liquid .dock .item,
|
|
html[data-theme=light] .skin-liquid .head { background: rgba(255,255,255,.72); }
|
|
html[data-theme=light].skin-glass, html[data-theme=light] body.skin-glass {
|
|
background: linear-gradient(180deg, #f3f5f8, #e4e8ef);
|
|
color: #111827;
|
|
}
|
|
html[data-theme=light] .skin-glass .head,
|
|
html[data-theme=light] .skin-glass .qs-tile,
|
|
html[data-theme=light] .skin-glass .sys { background: rgba(255,255,255,.86); }
|
|
html[data-theme=light] .skin-glass .qs-tile i { background: rgba(115,103,245,.12); color: #7367f5; }
|
|
html[data-theme=light].skin-compact, html[data-theme=light] body.skin-compact { background: #f4f6fa; color: #111827; }
|
|
html[data-theme=light] .skin-compact .item i { background: #fff; color: #3b4556; }
|
|
html[data-theme=light].skin-status, html[data-theme=light] body.skin-status { background: #fff; color: #111827; }
|
|
html[data-theme=light] .skin-status .metric,
|
|
html[data-theme=light] .skin-status .focus .item,
|
|
html[data-theme=light] .skin-status .work .item { background: #f3f5f8; }
|
|
html[data-theme=light] .skin-status .metric:hover,
|
|
html[data-theme=light] .skin-status .item:hover { background: #e8edf4; }
|
|
html[data-theme=light] .skin-status .open .item { background: #f6f7fa; }
|
|
html[data-theme=light] .skin-status .open .item:hover { background: #e8edf4; }
|
|
html[data-theme=light] .skin-status .metric strong { color: #6557e8; }
|
|
html[data-theme=light] .skin-status .metric.due strong { color: #9a7b12; }
|
|
html[data-theme=light] .skin-status .metric.empty strong { color: #8a93a3; }
|
|
html[data-theme=light] .skin-status .focus .item i { color: #6557e8; }
|
|
html[data-theme=light] .skin-status .work .item i,
|
|
html[data-theme=light] .skin-status .sys .item i { color: #526071; }
|
|
html[data-theme=light] .skin-status .item.on { background: rgba(101,87,232,.1); }
|
|
html[data-theme=light] .skin-status .item.on i { color: #6557e8; }
|
|
html[data-theme=light] .skin-status .sys .item:hover { background: #f3f5f8; }
|
|
html[data-theme=light] .skin-status .quit { color: #dc3d4a; }
|
|
html[data-theme=light] .skin-status .quit:hover { background: rgba(220,61,74,.08); }
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.item { transition: none; }
|
|
}
|
|
html[data-theme=light] .badge { color: #4c3fd4; background: rgba(101,87,232,.12); }
|
|
html[data-theme=light].skin-neon, html[data-theme=light] body.skin-neon { background: #fff; color: #111827; }
|
|
html[data-theme=light] .skin-neon .head,
|
|
html[data-theme=light] .skin-neon .rail { background: #f6f7fa; border-color: #dce1ea; }
|
|
html[data-theme=light] .skin-neon .head b { color: #111827; }
|
|
html[data-theme=light] .skin-neon .rail .item i,
|
|
html[data-theme=light] .skin-neon .list .item i { color: #526071; }
|
|
html[data-theme=light] .skin-neon .rail .item:hover,
|
|
html[data-theme=light] .skin-neon .rail .item.on { background: #edf0f6; }
|
|
html[data-theme=light] .skin-neon .item.on { background: rgba(101,87,232,.1); }
|
|
html[data-theme=light] .skin-neon .quit { border-top-color: #dce1ea; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app"></div>
|
|
<script type="module">
|
|
const { Call, Events } = await import('/wails/runtime.js')
|
|
const call = (name, ...args) => Call.ByName('main.App.' + name, ...args)
|
|
const app = document.getElementById('app')
|
|
let state = { style: 'status', theme: 'dark', locale: 'zh-CN', status: 'signedOut', statusLabel: '未登录', unread: 0, todayDue: 0, autostart: false, projects: [], hasMoreProjects: false }
|
|
let projectsOpen = false
|
|
let lastSkin = ''
|
|
let lastLocale = ''
|
|
const zh = {
|
|
show: '主窗口', pad: '启动台', today: '今日', todos: '待办', tickets: '工单',
|
|
messages: '消息', ai: 'AI 分析', projects: '项目', sync: '同步', batch: '统计',
|
|
auto: '自启', restart: '重启', quit: '退出', all: '全部…', work: '事务', system: '系统',
|
|
signedOut: '未登录', unreadShort: '未读', dueShort: '到期'
|
|
}
|
|
const en = {
|
|
show: 'Window', pad: 'Launchpad', today: 'Today', todos: 'Todos', tickets: 'Tickets',
|
|
messages: 'Inbox', ai: 'AI', projects: 'Projects', sync: 'Sync', batch: 'Analyze',
|
|
auto: 'Startup', restart: 'Restart', quit: 'Quit', all: 'All…', work: 'Work', system: 'System',
|
|
signedOut: 'Signed out', unreadShort: 'Unread', dueShort: 'Due'
|
|
}
|
|
const labels = { ...zh }
|
|
const icons = {
|
|
show: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/></svg>',
|
|
pad: '<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="6" cy="6" r="1.7"/><circle cx="12" cy="6" r="1.7"/><circle cx="18" cy="6" r="1.7"/><circle cx="6" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="18" cy="12" r="1.7"/><circle cx="6" cy="18" r="1.7"/><circle cx="12" cy="18" r="1.7"/><circle cx="18" cy="18" r="1.7"/></svg>',
|
|
today: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/><path d="m9 16 2 2 4-4"/></svg>',
|
|
todos: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 6h13M8 12h13M8 18h13"/><path d="m3 6 1 1 2-2M3 12l1 1 2-2M3 18l1 1 2-2"/></svg>',
|
|
tickets: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M13 5v2M13 17v2M13 11v2"/></svg>',
|
|
messages: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10 19a2 2 0 0 0 4 0"/></svg>',
|
|
ai: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l1.6 4.8L18 9.4l-4.4 1.6L12 16l-1.6-4.8L6 9.4l4.4-1.6Z"/><path d="M19 15l.7 2 2 .7-2 .7-.7 2-.7-2-2-.7 2-.7Z"/></svg>',
|
|
projects: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg>',
|
|
sync: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-2.6-6.4"/><path d="M21 3v6h-6"/></svg>',
|
|
batch: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18"/><path d="M7 16v-5M12 16V8M17 16v-9"/></svg>',
|
|
auto: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6 9 17l-5-5"/></svg>',
|
|
restart: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="6" height="6" rx="1"/><path d="M4 12a8 8 0 1 0 2.2-5.5"/><path d="M4 4v5h5"/></svg>',
|
|
quit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v10"/><path d="M18.4 6.6a8 8 0 1 1-12.8 0"/></svg>'
|
|
}
|
|
|
|
function skinName() {
|
|
const s = state.style === 'minimal' ? 'compact' : (state.style || 'status')
|
|
return s === 'native' ? 'status' : s
|
|
}
|
|
|
|
function esc(v) {
|
|
return String(v ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
}
|
|
|
|
function cls(key) {
|
|
const on = (key === 'projects' && projectsOpen) || (key === 'auto' && state.autostart) ? ' on' : ''
|
|
const danger = key === 'quit' ? ' danger' : ''
|
|
return `item${on}${danger}`
|
|
}
|
|
|
|
function btn(key, extra) {
|
|
const extraAttr = extra ? ` data-extra="${extra}"` : ''
|
|
return `<button type="button" class="${cls(key)}${extra ? ' ' + extra : ''}" data-act="${key}" data-key="${key}" title="${esc(labels[key])}"${extraAttr}><i>${icons[key]}</i><span>${esc(labels[key])}</span></button>`
|
|
}
|
|
|
|
function head() {
|
|
const badges = (state.unread ? `<em class="badge" data-badge="unread">${esc(state.unread)}</em>` : '<em class="badge" data-badge="unread" hidden></em>') +
|
|
(state.todayDue ? `<em class="badge due" data-badge="due">${esc(state.todayDue)}</em>` : '<em class="badge due" data-badge="due" hidden></em>')
|
|
return `<header class="head" data-act="sync"><i class="dot"></i><b data-status>${esc(state.statusLabel || labels.signedOut)}</b>${badges}</header>`
|
|
}
|
|
|
|
function projectBlock() {
|
|
if (!projectsOpen || !state.projects?.length) return ''
|
|
return `<div class="projects" data-projects>${state.projects.map(p =>
|
|
`<button type="button" class="chip" data-act="project" data-arg="${esc(p.id)}">${esc(p.name)}</button>`
|
|
).join('')}${state.hasMoreProjects ? `<button type="button" class="chip" data-act="nav" data-arg="/projects">${esc(labels.all)}</button>` : ''}</div>`
|
|
}
|
|
|
|
function renderLiquid() {
|
|
return head() +
|
|
`<div class="tiles">${['show', 'pad', 'today', 'todos'].map(k => btn(k, 'tile')).join('')}</div>` +
|
|
`<div class="dock">${['tickets', 'messages', 'ai', 'projects', 'sync'].map(k => btn(k)).join('')}</div>` +
|
|
projectBlock() +
|
|
`<div class="foot">${['auto', 'restart', 'quit'].map(k => btn(k)).join('')}</div>`
|
|
}
|
|
|
|
function renderGlass() {
|
|
return head() +
|
|
`<div class="qs">${['today', 'todos', 'tickets', 'messages', 'ai', 'projects'].map(k => btn(k, 'qs-tile')).join('')}</div>` +
|
|
`<div class="sys">${['show', 'pad', 'sync', 'batch', 'auto', 'restart'].map(k => btn(k)).join('')}</div>` +
|
|
projectBlock() +
|
|
btn('quit', 'quit')
|
|
}
|
|
|
|
function renderCompact() {
|
|
return head() +
|
|
`<div class="pad">${['show', 'pad', 'today', 'todos', 'tickets', 'messages', 'ai', 'projects', 'sync', 'batch', 'auto', 'restart'].map(k => btn(k)).join('')}</div>` +
|
|
projectBlock() +
|
|
btn('quit', 'quit')
|
|
}
|
|
|
|
function renderStatus() {
|
|
const unread = Number(state.unread) || 0
|
|
const due = Number(state.todayDue) || 0
|
|
return `<section class="hero">` +
|
|
`<button type="button" class="hero-user" data-act="sync"><i class="dot" aria-hidden="true"></i><b data-status>${esc(state.statusLabel || labels.signedOut)}</b></button>` +
|
|
`<div class="metrics">` +
|
|
`<button type="button" class="metric${unread ? '' : ' empty'}" data-act="messages"><strong data-badge="unread">${unread}</strong><span>${esc(labels.unreadShort)}</span></button>` +
|
|
`<button type="button" class="metric due${due ? '' : ' empty'}" data-act="today"><strong data-badge="due">${due}</strong><span>${esc(labels.dueShort)}</span></button>` +
|
|
`</div>` +
|
|
`</section>` +
|
|
`<div class="focus">${btn('today', 'focus')}${btn('todos', 'focus')}</div>` +
|
|
`<div class="work">${['tickets', 'messages', 'ai', 'projects'].map(k => btn(k, 'work')).join('')}</div>` +
|
|
projectBlock() +
|
|
`<div class="open">${btn('show', 'open')}${btn('pad', 'open')}</div>` +
|
|
`<div class="foot">` +
|
|
`<div class="sys">${['sync', 'batch', 'auto', 'restart'].map(k => btn(k, 'sys')).join('')}</div>` +
|
|
btn('quit', 'quit') +
|
|
`</div>`
|
|
}
|
|
|
|
function renderNeon() {
|
|
return head() +
|
|
`<div class="split">` +
|
|
`<div class="rail">${['show', 'pad', 'today', 'todos'].map(k => btn(k)).join('')}</div>` +
|
|
`<div class="side">` +
|
|
`<div class="sec">${esc(labels.work)}</div>` +
|
|
`<div class="list">${['tickets', 'messages', 'ai', 'projects'].map(k => btn(k)).join('')}</div>` +
|
|
`<div class="sec">${esc(labels.system)}</div>` +
|
|
`<div class="list">${['sync', 'batch', 'auto', 'restart'].map(k => btn(k)).join('')}</div>` +
|
|
projectBlock() +
|
|
btn('quit', 'quit') +
|
|
`</div>` +
|
|
`</div>`
|
|
}
|
|
|
|
function applyChrome() {
|
|
const skin = skinName()
|
|
document.documentElement.dataset.theme = state.theme === 'light' ? 'light' : 'dark'
|
|
document.documentElement.lang = state.locale === 'en' ? 'en' : 'zh-CN'
|
|
const clsName = 'skin-' + skin + ' st-' + (state.status || 'signedOut')
|
|
document.documentElement.className = clsName
|
|
document.body.className = clsName
|
|
}
|
|
|
|
function render() {
|
|
const skin = skinName()
|
|
applyChrome()
|
|
lastSkin = skin
|
|
lastLocale = state.locale || ''
|
|
const html = {
|
|
liquid: renderLiquid,
|
|
glass: renderGlass,
|
|
compact: renderCompact,
|
|
neon: renderNeon
|
|
}[skin] || renderStatus
|
|
app.innerHTML = html()
|
|
}
|
|
|
|
function patch() {
|
|
applyChrome()
|
|
const label = app.querySelector('[data-status]')
|
|
if (label) label.textContent = state.statusLabel || labels.signedOut
|
|
const unread = app.querySelector('[data-badge=unread]')
|
|
if (unread) {
|
|
const metric = unread.closest('.metric')
|
|
unread.textContent = String(state.unread || 0)
|
|
if (metric) {
|
|
unread.hidden = false
|
|
metric.classList.toggle('empty', !state.unread)
|
|
} else {
|
|
unread.hidden = !state.unread
|
|
}
|
|
}
|
|
const due = app.querySelector('[data-badge=due]')
|
|
if (due) {
|
|
const metric = due.closest('.metric')
|
|
due.textContent = String(state.todayDue || 0)
|
|
if (metric) {
|
|
due.hidden = false
|
|
metric.classList.toggle('empty', !state.todayDue)
|
|
} else {
|
|
due.hidden = !state.todayDue
|
|
}
|
|
}
|
|
app.querySelectorAll('[data-key]').forEach(el => {
|
|
el.className = cls(el.dataset.key) + (el.dataset.extra ? ' ' + el.dataset.extra : '')
|
|
})
|
|
}
|
|
|
|
function applyLocale() {
|
|
Object.assign(labels, state.locale === 'en' ? en : zh)
|
|
}
|
|
|
|
async function sync(full) {
|
|
try {
|
|
const next = await call('GetTrayMenuState')
|
|
state = { ...state, ...next, projects: next.projects || [] }
|
|
applyLocale()
|
|
const skin = skinName()
|
|
if (full || skin !== lastSkin || (state.locale || '') !== lastLocale) render()
|
|
else patch()
|
|
} catch {}
|
|
}
|
|
|
|
async function act(action, arg) {
|
|
if (action === 'projects') { projectsOpen = !projectsOpen; render(); return }
|
|
const map = { show: ['show', ''], pad: ['nav', '/launchpad'], today: ['nav', '/today'], todos: ['nav', '/todos'], tickets: ['nav', '/tickets'], messages: ['nav', '/messages'], ai: ['nav', '/ai'], sync: ['sync', ''], batch: ['batch', ''], auto: ['autostart', ''], restart: ['restart', ''], quit: ['quit', ''], project: ['project', arg], nav: ['nav', arg] }
|
|
const pair = map[action]
|
|
if (!pair) return
|
|
try {
|
|
await call('TrayMenuAction', pair[0], String(pair[1] || ''))
|
|
if (pair[0] === 'autostart') await sync(false)
|
|
} catch {}
|
|
}
|
|
|
|
app.addEventListener('click', ev => {
|
|
const btn = ev.target.closest('[data-act]')
|
|
if (!btn) return
|
|
act(btn.dataset.act, btn.dataset.arg || '')
|
|
})
|
|
|
|
Events.On('tray:refresh', () => sync(false))
|
|
Events.On('tray:shown', () => { sync(false) })
|
|
await sync(true)
|
|
try { await call('TrayPopupReady') } catch {}
|
|
</script>
|
|
</body>
|
|
</html>
|