mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
0.0.2
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -3,8 +3,8 @@ const electron = require("electron");
|
||||
const path = require("path");
|
||||
const utils = require("@electron-toolkit/utils");
|
||||
const elysia = require("elysia");
|
||||
const fs = require("fs");
|
||||
const child_process = require("child_process");
|
||||
const fs = require("fs");
|
||||
const util = require("util");
|
||||
const node = require("@elysiajs/node");
|
||||
const icon = path.join(__dirname, "../../resources/icon.png");
|
||||
@@ -20,7 +20,7 @@ function getStartMenuRoots() {
|
||||
const { userProgramsPath, commonProgramsPath } = getStartMenuPaths();
|
||||
return [userProgramsPath, commonProgramsPath].filter((p) => Boolean(p));
|
||||
}
|
||||
const execFileAsync$1 = util.promisify(child_process.execFile);
|
||||
const execFileAsync$2 = util.promisify(child_process.execFile);
|
||||
function getWindowsPowerShellExe() {
|
||||
const systemRoot = process.env["SystemRoot"] ?? process.env["WINDIR"] ?? "C:\\Windows";
|
||||
return path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
||||
@@ -124,7 +124,7 @@ async function listWindowsStartMenuApps() {
|
||||
const roots = getStartMenuRoots();
|
||||
const script = buildPowerShellScript(roots);
|
||||
const powershellExe = getWindowsPowerShellExe();
|
||||
const { stdout } = await execFileAsync$1(
|
||||
const { stdout } = await execFileAsync$2(
|
||||
powershellExe,
|
||||
["-NoProfile", "-NonInteractive", "-Sta", "-ExecutionPolicy", "Bypass", "-Command", script],
|
||||
{ windowsHide: true, maxBuffer: 50 * 1024 * 1024, timeout: 6e4 }
|
||||
@@ -157,7 +157,7 @@ async function listWindowsStartMenuApps() {
|
||||
result.sort((a, b) => a.name.localeCompare(b.name, "zh-CN"));
|
||||
return result;
|
||||
}
|
||||
const execFileAsync = util.promisify(child_process.execFile);
|
||||
const execFileAsync$1 = util.promisify(child_process.execFile);
|
||||
function isUnderRoot(filePath, root) {
|
||||
const normalizedFile = path.resolve(filePath).toLowerCase();
|
||||
const normalizedRoot = path.resolve(root).toLowerCase();
|
||||
@@ -166,7 +166,7 @@ function isUnderRoot(filePath, root) {
|
||||
async function launchStartMenuEntry(filePath) {
|
||||
if (process.platform !== "win32") return;
|
||||
if (filePath.startsWith("shell:AppsFolder\\")) {
|
||||
await execFileAsync("explorer.exe", [filePath], { windowsHide: true });
|
||||
await execFileAsync$1("explorer.exe", [filePath], { windowsHide: true });
|
||||
return;
|
||||
}
|
||||
const roots = getStartMenuRoots();
|
||||
@@ -179,6 +179,7 @@ async function launchStartMenuEntry(filePath) {
|
||||
throw new Error(result);
|
||||
}
|
||||
}
|
||||
const execFileAsync = util.promisify(child_process.execFile);
|
||||
function createEiysiaApp(deps) {
|
||||
const iconCache = /* @__PURE__ */ new Map();
|
||||
const appsCacheFilePath = path.join(electron.app.getPath("userData"), "apps-cache.json");
|
||||
@@ -328,6 +329,27 @@ function createEiysiaApp(deps) {
|
||||
if (message === "PathNotAllowed") return new Response("Forbidden", { status: 403 });
|
||||
return new Response("LaunchFailed", { status: 500 });
|
||||
}
|
||||
}).post("/open/external", async ({ body }) => {
|
||||
const payload = body;
|
||||
if (typeof payload.url !== "string") {
|
||||
return new Response("BadRequest", { status: 400 });
|
||||
}
|
||||
const url = payload.url.trim();
|
||||
if (!url) return new Response("BadRequest", { status: 400 });
|
||||
const lower = url.toLowerCase();
|
||||
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("file:")) {
|
||||
return new Response("Forbidden", { status: 403 });
|
||||
}
|
||||
try {
|
||||
if (process.platform === "win32" && lower.startsWith("shell:")) {
|
||||
await execFileAsync("explorer.exe", [url], { windowsHide: true });
|
||||
return { ok: true };
|
||||
}
|
||||
await electron.shell.openExternal(url);
|
||||
return { ok: true };
|
||||
} catch {
|
||||
return new Response("OpenFailed", { status: 500 });
|
||||
}
|
||||
}).get("/backend/port", () => ({
|
||||
port: deps.getHttpPort()
|
||||
})).post("/app/minimize", () => {
|
||||
|
||||
1216
out/renderer/assets/index-BOqh2SqZ.css
Normal file
1216
out/renderer/assets/index-BOqh2SqZ.css
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,450 +0,0 @@
|
||||
:root {
|
||||
--ev-c-white: #ffffff;
|
||||
--ev-c-white-soft: #f8f8f8;
|
||||
--ev-c-white-mute: #f2f2f2;
|
||||
|
||||
--ev-c-black: #1b1b1f;
|
||||
--ev-c-black-soft: #222222;
|
||||
--ev-c-black-mute: #282828;
|
||||
|
||||
--ev-c-gray-1: #515c67;
|
||||
--ev-c-gray-2: #414853;
|
||||
--ev-c-gray-3: #32363f;
|
||||
|
||||
--ev-c-text-1: rgba(255, 255, 245, 0.86);
|
||||
--ev-c-text-2: rgba(235, 235, 245, 0.6);
|
||||
--ev-c-text-3: rgba(235, 235, 245, 0.38);
|
||||
|
||||
--ev-button-alt-border: transparent;
|
||||
--ev-button-alt-text: var(--ev-c-text-1);
|
||||
--ev-button-alt-bg: var(--ev-c-gray-3);
|
||||
--ev-button-alt-hover-border: transparent;
|
||||
--ev-button-alt-hover-text: var(--ev-c-text-1);
|
||||
--ev-button-alt-hover-bg: var(--ev-c-gray-2);
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-background: var(--ev-c-black);
|
||||
--color-background-soft: var(--ev-c-black-soft);
|
||||
--color-background-mute: var(--ev-c-black-mute);
|
||||
|
||||
--color-text: var(--ev-c-text-1);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%201422%20800'%20opacity='0.3'%3e%3cdefs%3e%3clinearGradient%20x1='50%25'%20y1='0%25'%20x2='50%25'%20y2='100%25'%20id='oooscillate-grad'%3e%3cstop%20stop-color='hsl(206,%2075%25,%2049%25)'%20stop-opacity='1'%20offset='0%25'%3e%3c/stop%3e%3cstop%20stop-color='hsl(331,%2090%25,%2056%25)'%20stop-opacity='1'%20offset='100%25'%3e%3c/stop%3e%3c/linearGradient%3e%3c/defs%3e%3cg%20stroke-width='1'%20stroke='url(%23oooscillate-grad)'%20fill='none'%20stroke-linecap='round'%3e%3cpath%20d='M%200%20448%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20448'%20opacity='0.05'%3e%3c/path%3e%3cpath%20d='M%200%20420%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20420'%20opacity='0.11'%3e%3c/path%3e%3cpath%20d='M%200%20392%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20392'%20opacity='0.18'%3e%3c/path%3e%3cpath%20d='M%200%20364%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20364'%20opacity='0.24'%3e%3c/path%3e%3cpath%20d='M%200%20336%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20336'%20opacity='0.30'%3e%3c/path%3e%3cpath%20d='M%200%20308%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20308'%20opacity='0.37'%3e%3c/path%3e%3cpath%20d='M%200%20280%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20280'%20opacity='0.43'%3e%3c/path%3e%3cpath%20d='M%200%20252%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20252'%20opacity='0.49'%3e%3c/path%3e%3cpath%20d='M%200%20224%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20224'%20opacity='0.56'%3e%3c/path%3e%3cpath%20d='M%200%20196%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20196'%20opacity='0.62'%3e%3c/path%3e%3cpath%20d='M%200%20168%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20168'%20opacity='0.68'%3e%3c/path%3e%3cpath%20d='M%200%20140%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20140'%20opacity='0.75'%3e%3c/path%3e%3cpath%20d='M%200%20112%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20112'%20opacity='0.81'%3e%3c/path%3e%3cpath%20d='M%200%2084%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%2084'%20opacity='0.87'%3e%3c/path%3e%3cpath%20d='M%200%2056%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%2056'%20opacity='0.94'%3e%3c/path%3e%3c/g%3e%3c/svg%3e");
|
||||
background-size: cover;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.touchButton {
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.launcher {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
top: 84px;
|
||||
width: 520px;
|
||||
height: calc(100vh - 168px);
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 14px;
|
||||
backdrop-filter: blur(12px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.home {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.openAppsButton {
|
||||
cursor: pointer;
|
||||
border-radius: 18px;
|
||||
padding: 20px 28px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
color: var(--ev-c-text-1);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(12px);
|
||||
font-size: 22px;
|
||||
line-height: 26px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.openAppsButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.26);
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.appsPage {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
cursor: pointer;
|
||||
border-radius: 16px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: var(--ev-c-text-1);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
font-weight: 800;
|
||||
height: 72px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.backButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.appsTilesArea {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
top: 88px;
|
||||
bottom: 120px;
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 14px;
|
||||
backdrop-filter: blur(12px);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.appsList {
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 4px 6px;
|
||||
box-sizing: border-box;
|
||||
columns: 6 280px;
|
||||
column-gap: 14px;
|
||||
column-fill: auto;
|
||||
touch-action: pan-x;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-x: contain;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.appsList::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.appsLoading {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.appsBottomBar {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
height: 92px;
|
||||
padding: 10px 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr 140px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(12px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.appsSearch {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
color: var(--ev-c-text-1);
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.appsExitButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 16px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: var(--ev-c-text-1);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
font-weight: 800;
|
||||
height: 72px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.appsExitButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
color: var(--ev-c-text-1);
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 120, 120, 0.25);
|
||||
background: rgba(120, 0, 0, 0.18);
|
||||
color: var(--ev-c-text-1);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
user-select: text;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.errorHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.errorTitle {
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.errorBody {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: var(--ev-c-text-1);
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.copyButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.26);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 12px;
|
||||
height: calc(100% - 50px);
|
||||
overflow: auto;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.groupHeader {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 0 10px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4px;
|
||||
opacity: 0.9;
|
||||
break-inside: avoid;
|
||||
-webkit-column-break-inside: avoid;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 72px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: var(--ev-c-text-1);
|
||||
margin: 0 0 10px;
|
||||
break-inside: avoid;
|
||||
-webkit-column-break-inside: avoid;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.item:active {
|
||||
border-color: rgba(255, 255, 255, 0.26);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.path {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
opacity: 0.75;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.clock {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
top: 16px;
|
||||
padding: 10px 12px;
|
||||
font-size: 28px;
|
||||
line-height: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--ev-c-text-1);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.exitButton {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
border-radius: 14px;
|
||||
padding: 10px 16px;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: var(--ev-c-text-1);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.buttonIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.exitButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
@@ -8,8 +8,8 @@
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
||||
/>
|
||||
<script type="module" crossorigin src="./assets/index-C9aAt2YI.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DAQiHmWm.css">
|
||||
<script type="module" crossorigin src="./assets/index-CkK6wThO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BOqh2SqZ.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { app as electronApp, BrowserWindow } from 'electron'
|
||||
import { app as electronApp, BrowserWindow, shell } from 'electron'
|
||||
import { Elysia } from 'elysia'
|
||||
import { execFile } from 'child_process'
|
||||
import { promises as fs } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { promisify } from 'util'
|
||||
import { launchStartMenuEntry, listWindowsStartMenuApps } from '../app_list'
|
||||
|
||||
export interface EiysiaDependencies {
|
||||
@@ -9,6 +11,8 @@ export interface EiysiaDependencies {
|
||||
getHttpPort: () => number | null
|
||||
}
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
export function createEiysiaApp(deps: EiysiaDependencies): {
|
||||
handle: (request: Request) => Response | Promise<Response>
|
||||
} {
|
||||
@@ -216,6 +220,36 @@ export function createEiysiaApp(deps: EiysiaDependencies): {
|
||||
return new Response('LaunchFailed', { status: 500 })
|
||||
}
|
||||
})
|
||||
.post('/open/external', async ({ body }) => {
|
||||
const payload = body as { url?: unknown }
|
||||
if (typeof payload.url !== 'string') {
|
||||
return new Response('BadRequest', { status: 400 })
|
||||
}
|
||||
|
||||
const url = payload.url.trim()
|
||||
if (!url) return new Response('BadRequest', { status: 400 })
|
||||
|
||||
const lower = url.toLowerCase()
|
||||
if (
|
||||
lower.startsWith('javascript:') ||
|
||||
lower.startsWith('data:') ||
|
||||
lower.startsWith('file:')
|
||||
) {
|
||||
return new Response('Forbidden', { status: 403 })
|
||||
}
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32' && lower.startsWith('shell:')) {
|
||||
await execFileAsync('explorer.exe', [url], { windowsHide: true })
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
await shell.openExternal(url)
|
||||
return { ok: true }
|
||||
} catch {
|
||||
return new Response('OpenFailed', { status: 500 })
|
||||
}
|
||||
})
|
||||
.get('/backend/port', () => ({
|
||||
port: deps.getHttpPort()
|
||||
}))
|
||||
|
||||
@@ -1,54 +1,332 @@
|
||||
<template>
|
||||
<div class="screen">
|
||||
<div class="clock">{{ timeText }}</div>
|
||||
<div v-if="page === 'home'" class="home">
|
||||
<button
|
||||
class="openAppsButton touchButton"
|
||||
type="button"
|
||||
@pointerup="openApps"
|
||||
@keydown.enter.prevent="openApps"
|
||||
@keydown.space.prevent="openApps"
|
||||
>
|
||||
应用列表
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="pagesViewport"
|
||||
@pointerdown="handlePagePointerDown"
|
||||
@pointermove="handlePagePointerMove"
|
||||
@pointerup="handlePagePointerUp"
|
||||
@pointercancel="handlePagePointerCancel"
|
||||
>
|
||||
<div class="pagesTrack" :data-active="mainPage">
|
||||
<div class="page page--home">
|
||||
<div class="home">
|
||||
<div class="desktopGridShell">
|
||||
<div class="desktopGrid">
|
||||
<component
|
||||
:is="tile.clickable ? 'button' : 'div'"
|
||||
v-for="tile in placedDesktopTiles"
|
||||
:key="tile.id"
|
||||
class="deskTile touchButton"
|
||||
:class="[
|
||||
tile.variant ? `deskTile--${tile.variant}` : '',
|
||||
tile.id === 'writingPanel' ? 'deskTile--panel' : ''
|
||||
]"
|
||||
:style="{
|
||||
gridColumn: `${tile.col} / span ${tile.colSpan}`,
|
||||
gridRow: `${tile.row} / span ${tile.rowSpan}`
|
||||
}"
|
||||
:type="tile.clickable ? 'button' : undefined"
|
||||
@pointerup="tile.clickable ? handleTilePointerUp(tile) : undefined"
|
||||
>
|
||||
<template v-if="tile.id === 'writingPanel'">
|
||||
<div class="writingPanelTitle">书写</div>
|
||||
<div class="writingPanelButtons">
|
||||
<button
|
||||
v-for="a in writingActions"
|
||||
:key="a.id"
|
||||
class="writingActionButton touchButton"
|
||||
type="button"
|
||||
@pointerup="handleWritingActionPointerUp(a.id)"
|
||||
>
|
||||
<svg
|
||||
class="writingActionIcon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path fill="currentColor" :d="iconPath(a.icon)" />
|
||||
</svg>
|
||||
<div class="writingActionText">{{ a.label }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="deskTileRow">
|
||||
<svg
|
||||
class="deskTileIcon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path fill="currentColor" :d="iconPath(tile.icon)" />
|
||||
</svg>
|
||||
<div class="deskTileText">{{ tile.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</component>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="appsPage">
|
||||
<div class="appsTilesArea">
|
||||
<div v-if="isLoadingApps" class="appsLoading">加载中...</div>
|
||||
<div v-else-if="loadError" class="error">
|
||||
<div class="errorHeader">
|
||||
<div class="errorTitle">加载失败</div>
|
||||
<button class="copyButton touchButton" type="button" @pointerup="copyLoadError">复制错误</button>
|
||||
<div class="notesPanel">
|
||||
<div class="notesHeader">
|
||||
<div class="notesHeaderTitle">作业版</div>
|
||||
<button class="notesAddButton touchButton" type="button" @pointerup="addNote">新建</button>
|
||||
</div>
|
||||
<div class="notesGrid">
|
||||
<div v-for="note in notes" :key="note.id" class="noteCard">
|
||||
<textarea v-model="note.text" class="noteTextarea" placeholder="写点什么..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="errorBody">{{ loadError }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="appsList" @scroll="handleAppsListScroll">
|
||||
<template v-for="group in groupedApps" :key="group.key">
|
||||
<div class="groupHeader">{{ group.label }}</div>
|
||||
<button
|
||||
v-for="app in group.items"
|
||||
:key="app.id"
|
||||
class="item touchButton"
|
||||
type="button"
|
||||
@pointerdown="handleAppPointerDown($event, app.filePath)"
|
||||
@pointermove="handleAppPointerMove($event)"
|
||||
@pointerup="handleAppPointerUp($event)"
|
||||
@pointercancel="handleAppPointerCancel($event)"
|
||||
@keydown.enter.prevent="launch(app.filePath)"
|
||||
@keydown.space.prevent="launch(app.filePath)"
|
||||
>
|
||||
<img class="icon" :src="app.iconDataUrl" alt="" />
|
||||
<div class="name">{{ app.name }}</div>
|
||||
</button>
|
||||
</template>
|
||||
<div class="page page--apps">
|
||||
<div class="appsPage">
|
||||
<div class="appsTilesArea">
|
||||
<div v-if="isLoadingApps" class="appsLoading">加载中...</div>
|
||||
<div v-else-if="loadError" class="error">
|
||||
<div class="errorHeader">
|
||||
<div class="errorTitle">加载失败</div>
|
||||
<button class="copyButton touchButton" type="button" @pointerup="copyLoadError">复制错误</button>
|
||||
</div>
|
||||
<div class="errorBody">{{ loadError }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="appsList" @scroll="handleAppsListScroll">
|
||||
<template v-for="group in groupedApps" :key="group.key">
|
||||
<div class="groupHeader">{{ group.label }}</div>
|
||||
<button
|
||||
v-for="app in group.items"
|
||||
:key="app.id"
|
||||
class="item touchButton"
|
||||
type="button"
|
||||
@pointerdown="handleAppPointerDown($event, app.filePath)"
|
||||
@pointermove="handleAppPointerMove($event)"
|
||||
@pointerup="handleAppPointerUp($event)"
|
||||
@pointercancel="handleAppPointerCancel($event)"
|
||||
@keydown.enter.prevent="launch(app.filePath)"
|
||||
@keydown.space.prevent="launch(app.filePath)"
|
||||
>
|
||||
<img class="icon" :src="app.iconDataUrl" alt="" />
|
||||
<div class="name">{{ app.name }}</div>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="appsBottomBar">
|
||||
<button class="backButton touchButton" type="button" @pointerup="closeApps">返回</button>
|
||||
<input v-model="query" class="appsSearch" type="text" placeholder="搜索应用..." />
|
||||
<button class="appsExitButton touchButton" type="button" @pointerup="handleExit">
|
||||
<svg
|
||||
class="buttonIcon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8.5 9A1.5 1.5 0 0 0 10 7.5v-4A1.5 1.5 0 0 0 8.5 2h-6A1.5 1.5 0 0 0 1 3.5v4a1.5 1.5 0 0 0 1 1.415l.019.006c.15.051.313.079.481.079zm6.75-3H11V5h4.25A2.75 2.75 0 0 1 18 7.75v6.5A2.75 2.75 0 0 1 15.25 17H4.75A2.75 2.75 0 0 1 2 14.25v-4.3q.243.05.5.05H3v4.25c0 .966.784 1.75 1.75 1.75h10.5A1.75 1.75 0 0 0 17 14.25v-6.5A1.75 1.75 0 0 0 15.25 6M14 12.293l-2.646-2.647a.5.5 0 0 0-.708.708L13.293 13H11.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 .5-.497V10.5a.5.5 0 0 0-1 0z"
|
||||
/>
|
||||
</svg>
|
||||
回到Window
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="appsBottomBar">
|
||||
<button class="backButton touchButton" type="button" @pointerup="closeApps">返回</button>
|
||||
<input v-model="query" class="appsSearch" type="text" placeholder="搜索应用..." />
|
||||
<div v-if="isSettingsOpen" class="settingsPage">
|
||||
<div class="settingsContentArea">
|
||||
<div class="settingsSidebar">
|
||||
<div class="settingsSidebarTitle">设置</div>
|
||||
<button
|
||||
class="settingsTabButton touchButton"
|
||||
type="button"
|
||||
:data-active="settingsTab === 'homeEdit'"
|
||||
@pointerup="selectSettingsTab('homeEdit')"
|
||||
>
|
||||
主页编辑
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settingsDetail">
|
||||
<div class="settingsDetailHeader">
|
||||
<div class="settingsDetailTitle">主页编辑</div>
|
||||
<div class="settingsDetailHeaderActions">
|
||||
<button
|
||||
v-if="!isHomeEditMode"
|
||||
class="settingsPrimaryButton touchButton"
|
||||
type="button"
|
||||
@pointerup="enterHomeEdit"
|
||||
>
|
||||
进入主页编辑
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="settingsPrimaryButton touchButton"
|
||||
type="button"
|
||||
@pointerup="exitHomeEdit"
|
||||
>
|
||||
退出编辑
|
||||
</button>
|
||||
<button class="settingsResetButton touchButton" type="button" @pointerup="restoreDefaultWidgets">
|
||||
恢复默认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settingsDetailBody">
|
||||
<template v-if="!isHomeEditMode">
|
||||
<div class="settingsSection">
|
||||
<div class="settingsSectionTitle">组件与点击行为</div>
|
||||
<div class="settingsPreviewList">
|
||||
<div v-for="w in settingsWidgets" :key="w.id" class="settingsPreviewItem">
|
||||
<div class="settingsPreviewMain">
|
||||
<div class="settingsPreviewTitle">{{ w.title }}</div>
|
||||
<div class="settingsPreviewMeta">
|
||||
{{ w.sizeLabel }} · {{ w.enabled ? '已添加' : '未添加' }} · {{ w.visible ? '显示中' : '未显示' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsPreviewBehavior">
|
||||
<template v-if="w.id === 'writingPanel'">
|
||||
<div class="settingsPreviewBehaviorTitle">内置按钮</div>
|
||||
<div v-for="a in writingActions" :key="a.id" class="settingsPreviewBehaviorRow">
|
||||
<div class="settingsPreviewBehaviorLabel">{{ a.label }}</div>
|
||||
<div class="settingsPreviewBehaviorValue">{{ actionSummary(a.id) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="settingsPreviewBehaviorTitle">点击行为</div>
|
||||
<div class="settingsPreviewBehaviorValue">{{ actionSummary(w.id) }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="settingsSection">
|
||||
<div class="settingsSectionTitle">主页组件(点按添加/移除)</div>
|
||||
<div class="homeEditGrid">
|
||||
<button
|
||||
v-for="w in widgetsInOrder"
|
||||
:key="w.id"
|
||||
class="homeWidgetCard touchButton"
|
||||
type="button"
|
||||
:data-active="widgetsEnabled[w.id] !== false"
|
||||
:data-selected="homeEditSelectedId === w.id"
|
||||
@pointerup="toggleHomeWidget(w.id)"
|
||||
>
|
||||
<svg
|
||||
class="homeWidgetCardIcon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path fill="currentColor" :d="iconPath(w.icon)" />
|
||||
</svg>
|
||||
<div class="homeWidgetCardTitle">{{ w.title }}</div>
|
||||
<div class="homeWidgetCardMeta">{{ w.size === '2x4' ? '2×4' : w.size === '4x2' ? '4×2' : '2×2' }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settingsSection">
|
||||
<div class="settingsSectionTitle">排序(仅已添加)</div>
|
||||
<div class="homeEditOrderList">
|
||||
<div v-for="id in enabledWidgetOrder" :key="id" class="homeEditOrderRow">
|
||||
<div class="homeEditOrderTitle">{{ widgetTitle(id) }}</div>
|
||||
<div class="homeEditOrderActions">
|
||||
<button class="settingsActionButton touchButton" type="button" @pointerup="moveEnabledWidget(id, -1)">
|
||||
上移
|
||||
</button>
|
||||
<button class="settingsActionButton touchButton" type="button" @pointerup="moveEnabledWidget(id, 1)">
|
||||
下移
|
||||
</button>
|
||||
<button class="settingsActionButton touchButton" type="button" @pointerup="selectHomeWidget(id)">
|
||||
设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settingsSection">
|
||||
<div class="settingsSectionTitle">详细设置:{{ widgetTitle(homeEditSelectedId) }}</div>
|
||||
<div class="settingsConfigArea">
|
||||
<template v-if="homeEditSelectedId === 'writingPanel'">
|
||||
<div v-for="a in writingActions" :key="a.id" class="settingsSubAction">
|
||||
<div class="settingsSubActionTitle">{{ a.label }}</div>
|
||||
<select
|
||||
class="settingsSelect touchButton"
|
||||
:value="actionConfigs[a.id]?.kind ?? 'url'"
|
||||
@change="setActionKind(a.id, ($event.target as HTMLSelectElement).value as ActionKind)"
|
||||
>
|
||||
<option value="app">打开应用</option>
|
||||
<option value="url">打开URL</option>
|
||||
</select>
|
||||
<input
|
||||
class="settingsInput"
|
||||
type="text"
|
||||
:value="actionConfigs[a.id]?.target ?? ''"
|
||||
@input="setActionTarget(a.id, ($event.target as HTMLInputElement).value)"
|
||||
placeholder="输入开始菜单路径或URL"
|
||||
/>
|
||||
<button class="settingsMiniButton touchButton" type="button" @pointerup="clearAction(a.id)">
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="settingsSingleActionRow">
|
||||
<select
|
||||
class="settingsSelect touchButton"
|
||||
:value="actionConfigs[homeEditSelectedId]?.kind ?? 'url'"
|
||||
@change="
|
||||
setActionKind(homeEditSelectedId, ($event.target as HTMLSelectElement).value as ActionKind)
|
||||
"
|
||||
>
|
||||
<option value="app">打开应用</option>
|
||||
<option value="url">打开URL</option>
|
||||
</select>
|
||||
<input
|
||||
class="settingsInput"
|
||||
type="text"
|
||||
:value="actionConfigs[homeEditSelectedId]?.target ?? ''"
|
||||
@input="setActionTarget(homeEditSelectedId, ($event.target as HTMLInputElement).value)"
|
||||
placeholder="输入开始菜单路径或URL"
|
||||
/>
|
||||
<button class="settingsMiniButton touchButton" type="button" @pointerup="clearAction(homeEditSelectedId)">
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
<div class="settingsActionHint">当前:{{ actionSummary(homeEditSelectedId) }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsBottomBar">
|
||||
<button class="backButton touchButton" type="button" @pointerup="closeSettings">返回</button>
|
||||
<button class="appsExitButton touchButton" type="button" @pointerup="handleExit">
|
||||
<svg
|
||||
class="buttonIcon"
|
||||
@@ -64,11 +342,11 @@
|
||||
d="M8.5 9A1.5 1.5 0 0 0 10 7.5v-4A1.5 1.5 0 0 0 8.5 2h-6A1.5 1.5 0 0 0 1 3.5v4a1.5 1.5 0 0 0 1 1.415l.019.006c.15.051.313.079.481.079zm6.75-3H11V5h4.25A2.75 2.75 0 0 1 18 7.75v6.5A2.75 2.75 0 0 1 15.25 17H4.75A2.75 2.75 0 0 1 2 14.25v-4.3q.243.05.5.05H3v4.25c0 .966.784 1.75 1.75 1.75h10.5A1.75 1.75 0 0 0 17 14.25v-6.5A1.75 1.75 0 0 0 15.25 6M14 12.293l-2.646-2.647a.5.5 0 0 0-.708.708L13.293 13H11.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 .5-.497V10.5a.5.5 0 0 0-1 0z"
|
||||
/>
|
||||
</svg>
|
||||
Windows
|
||||
回到Window
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button v-if="page === 'home'" class="exitButton touchButton" type="button" @pointerup="handleExit">
|
||||
<button v-if="mainPage === 'home' && !isSettingsOpen" class="exitButton touchButton" type="button" @pointerup="handleExit">
|
||||
<svg
|
||||
class="buttonIcon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -83,27 +361,301 @@
|
||||
d="M8.5 9A1.5 1.5 0 0 0 10 7.5v-4A1.5 1.5 0 0 0 8.5 2h-6A1.5 1.5 0 0 0 1 3.5v4a1.5 1.5 0 0 0 1 1.415l.019.006c.15.051.313.079.481.079zm6.75-3H11V5h4.25A2.75 2.75 0 0 1 18 7.75v6.5A2.75 2.75 0 0 1 15.25 17H4.75A2.75 2.75 0 0 1 2 14.25v-4.3q.243.05.5.05H3v4.25c0 .966.784 1.75 1.75 1.75h10.5A1.75 1.75 0 0 0 17 14.25v-6.5A1.75 1.75 0 0 0 15.25 6M14 12.293l-2.646-2.647a.5.5 0 0 0-.708.708L13.293 13H11.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 .5-.497V10.5a.5.5 0 0 0-1 0z"
|
||||
/>
|
||||
</svg>
|
||||
Windows
|
||||
回到Window
|
||||
</button>
|
||||
<button
|
||||
v-if="mainPage === 'home' && !isSettingsOpen"
|
||||
class="settingsButton touchButton"
|
||||
type="button"
|
||||
@pointerup="openSettings"
|
||||
>
|
||||
设置
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
type Page = 'home' | 'apps'
|
||||
type MainPage = 'home' | 'apps'
|
||||
|
||||
const now = ref(new Date())
|
||||
const page = ref<Page>('home')
|
||||
const mainPage = ref<MainPage>('home')
|
||||
const isSettingsOpen = ref(false)
|
||||
const apps = ref<Array<{ id: string; name: string; filePath: string; iconDataUrl: string }>>([])
|
||||
const query = ref('')
|
||||
const loadError = ref<string | null>(null)
|
||||
const isLoadingApps = ref(false)
|
||||
const isAppsScrolling = ref(false)
|
||||
type Note = { id: string; text: string }
|
||||
const notes = ref<Note[]>([])
|
||||
|
||||
type ActionKind = 'app' | 'url'
|
||||
type ActionConfig = { kind: ActionKind; target: string }
|
||||
type WidgetSize = '2x2' | '4x2' | '2x4'
|
||||
type TileIcon = 'pen' | 'folder' | 'camera' | 'globe' | 'apps' | 'note' | 'doc' | 'comment'
|
||||
type DesktopTile = {
|
||||
id: string
|
||||
title: string
|
||||
size: WidgetSize
|
||||
icon: TileIcon
|
||||
clickable: boolean
|
||||
onActivate?: () => void
|
||||
variant?: 'accent' | 'soft'
|
||||
}
|
||||
|
||||
type PlacedDesktopTile = DesktopTile & {
|
||||
row: number
|
||||
col: number
|
||||
rowSpan: number
|
||||
colSpan: number
|
||||
}
|
||||
|
||||
const iconPath = (icon: TileIcon): string => {
|
||||
switch (icon) {
|
||||
case 'pen':
|
||||
return 'M14.1 2.65a2.25 2.25 0 0 1 3.182 3.182l-8.6 8.6a2.25 2.25 0 0 1-1.04.576l-3.1.775a.75.75 0 0 1-.91-.91l.775-3.1a2.25 2.25 0 0 1 .576-1.04zm2.121 1.06a.75.75 0 0 0-1.06 0L6.7 12.174a.75.75 0 0 0-.192.346l-.44 1.76 1.76-.44a.75.75 0 0 0 .346-.192l8.462-8.462a.75.75 0 0 0 0-1.06z'
|
||||
case 'folder':
|
||||
return 'M3.5 4.5A2.5 2.5 0 0 1 6 2h2.25c.43 0 .84.184 1.126.505L10.2 3.5H14A2.5 2.5 0 0 1 16.5 6v7.5A2.5 2.5 0 0 1 14 16H6A2.5 2.5 0 0 1 3.5 13.5zM6 3.5A1 1 0 0 0 5 4.5V5h10v-1a1 1 0 0 0-1-1h-4.1a.75.75 0 0 1-.562-.253L8.086 3.5zm9 3H5v7a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1z'
|
||||
case 'camera':
|
||||
return 'M7 4.5h6l.6 1.2c.127.254.387.414.67.414H15A2.5 2.5 0 0 1 17.5 8.6v6.9A2.5 2.5 0 0 1 15 18H5A2.5 2.5 0 0 1 2.5 15.5V8.6A2.5 2.5 0 0 1 5 6.114h.73c.283 0 .543-.16.67-.414zM10 15.5a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'
|
||||
case 'globe':
|
||||
return 'M10 2a8 8 0 1 1 0 16 8 8 0 0 1 0-16m5.93 7H13.6a12 12 0 0 0-1.03-4.09A6.5 6.5 0 0 1 15.93 9M10 3.5c-.9 0-1.98 1.6-2.45 5.5h4.9c-.47-3.9-1.55-5.5-2.45-5.5m-2.57 1.41A12 12 0 0 0 6.4 9H4.07a6.5 6.5 0 0 1 3.36-4.09M4.07 11H6.4c.17 1.5.56 2.92 1.03 4.09A6.5 6.5 0 0 1 4.07 11m3.48 0c.47 3.9 1.55 5.5 2.45 5.5s1.98-1.6 2.45-5.5zm5.02 4.09c.47-1.17.86-2.59 1.03-4.09h2.33a6.5 6.5 0 0 1-3.36 4.09'
|
||||
case 'apps':
|
||||
return 'M4.5 3A1.5 1.5 0 0 0 3 4.5v2A1.5 1.5 0 0 0 4.5 8h2A1.5 1.5 0 0 0 8 6.5v-2A1.5 1.5 0 0 0 6.5 3zm9 0A1.5 1.5 0 0 0 12 4.5v2A1.5 1.5 0 0 0 13.5 8h2A1.5 1.5 0 0 0 17 6.5v-2A1.5 1.5 0 0 0 15.5 3zM3 13.5A1.5 1.5 0 0 1 4.5 12h2A1.5 1.5 0 0 1 8 13.5v2A1.5 1.5 0 0 1 6.5 17h-2A1.5 1.5 0 0 1 3 15.5zm10.5-1.5A1.5 1.5 0 0 0 12 13.5v2a1.5 1.5 0 0 0 1.5 1.5h2a1.5 1.5 0 0 0 1.5-1.5v-2a1.5 1.5 0 0 0-1.5-1.5z'
|
||||
case 'note':
|
||||
return 'M6 2.5A2.5 2.5 0 0 0 3.5 5v10A2.5 2.5 0 0 0 6 17.5h5.5a.75.75 0 0 0 .53-.22l3.25-3.25a.75.75 0 0 0 .22-.53V5A2.5 2.5 0 0 0 13 2.5zm6.5 12.94V14a.5.5 0 0 1 .5-.5h1.44z'
|
||||
case 'doc':
|
||||
return 'M6 2.5A2.5 2.5 0 0 0 3.5 5v10A2.5 2.5 0 0 0 6 17.5h8A2.5 2.5 0 0 0 16.5 15V7.75a.75.75 0 0 0-.22-.53l-3.5-3.5a.75.75 0 0 0-.53-.22zM12.5 4.56 14.44 6.5H13a.5.5 0 0 1-.5-.5z'
|
||||
case 'comment':
|
||||
return 'M5.5 3.5A2.5 2.5 0 0 0 3 6v6a2.5 2.5 0 0 0 2.5 2.5H7.3l2.2 1.76a.75.75 0 0 0 .94 0l2.2-1.76H14.5A2.5 2.5 0 0 0 17 12V6a2.5 2.5 0 0 0-2.5-2.5z'
|
||||
}
|
||||
}
|
||||
|
||||
const writingActions: Array<{ id: string; label: string; icon: TileIcon }> = [
|
||||
{ id: 'writing.whiteboard', label: '白板书写', icon: 'pen' },
|
||||
{ id: 'writing.annotate', label: '智能批注', icon: 'comment' },
|
||||
{ id: 'writing.historyNotes', label: '历史笔记', icon: 'note' },
|
||||
{ id: 'writing.classNotes', label: '课堂笔记', icon: 'note' },
|
||||
{ id: 'writing.historyDocs', label: '历史文档', icon: 'doc' }
|
||||
]
|
||||
|
||||
const actionConfigs = ref<Record<string, ActionConfig>>({})
|
||||
let actionSaveTimerId: number | undefined
|
||||
|
||||
type SettingsTab = 'homeEdit'
|
||||
const settingsTab = ref<SettingsTab>('homeEdit')
|
||||
const isHomeEditMode = ref(false)
|
||||
const homeEditSelectedId = ref<string>('whiteboard')
|
||||
|
||||
let pageSwipePointerId: number | null = null
|
||||
let pageSwipeStartX = 0
|
||||
let pageSwipeStartY = 0
|
||||
let pageSwipeStartTime = 0
|
||||
let pageSwipeStartMainPage: MainPage = 'home'
|
||||
let suppressTapUntil = 0
|
||||
|
||||
const availableWidgets: DesktopTile[] = [
|
||||
{ id: 'writingPanel', title: '书写', size: '2x4', icon: 'pen', clickable: false, variant: 'soft' },
|
||||
{
|
||||
id: 'whiteboard',
|
||||
title: '白板书写',
|
||||
size: '2x2',
|
||||
icon: 'pen',
|
||||
clickable: true,
|
||||
onActivate: () => void activateAction('whiteboard'),
|
||||
variant: 'soft'
|
||||
},
|
||||
{
|
||||
id: 'fileManager',
|
||||
title: '文件管理',
|
||||
size: '2x2',
|
||||
icon: 'folder',
|
||||
clickable: true,
|
||||
onActivate: () => void activateAction('fileManager')
|
||||
},
|
||||
{
|
||||
id: 'visualPresenter',
|
||||
title: '视频展台',
|
||||
size: '2x2',
|
||||
icon: 'camera',
|
||||
clickable: true,
|
||||
onActivate: () => void activateAction('visualPresenter')
|
||||
},
|
||||
{
|
||||
id: 'browser',
|
||||
title: '浏览器',
|
||||
size: '2x2',
|
||||
icon: 'globe',
|
||||
clickable: true,
|
||||
onActivate: () => void activateAction('browser')
|
||||
},
|
||||
{
|
||||
id: 'moreApps',
|
||||
title: '更多应用',
|
||||
size: '2x2',
|
||||
icon: 'apps',
|
||||
clickable: true,
|
||||
onActivate: () => void activateAction('moreApps', () => void openApps()),
|
||||
variant: 'accent'
|
||||
}
|
||||
]
|
||||
|
||||
const widgetsOrder = ref<string[]>(['writingPanel', 'whiteboard', 'fileManager', 'visualPresenter', 'browser', 'moreApps'])
|
||||
const widgetsEnabled = ref<Record<string, boolean>>({
|
||||
writingPanel: true,
|
||||
whiteboard: true,
|
||||
fileManager: true,
|
||||
visualPresenter: true,
|
||||
browser: true,
|
||||
moreApps: true
|
||||
})
|
||||
|
||||
let widgetsSaveTimerId: number | undefined
|
||||
|
||||
const widgetsInOrder = computed<DesktopTile[]>(() => {
|
||||
const map = new Map(availableWidgets.map((w) => [w.id, w]))
|
||||
const seen = new Set<string>()
|
||||
const ordered: DesktopTile[] = []
|
||||
for (const id of widgetsOrder.value) {
|
||||
const w = map.get(id)
|
||||
if (!w) continue
|
||||
if (seen.has(id)) continue
|
||||
seen.add(id)
|
||||
ordered.push(w)
|
||||
}
|
||||
for (const w of availableWidgets) {
|
||||
if (seen.has(w.id)) continue
|
||||
ordered.push(w)
|
||||
}
|
||||
return ordered
|
||||
})
|
||||
|
||||
const enabledWidgets = computed<DesktopTile[]>(() => {
|
||||
return widgetsInOrder.value.filter((w) => widgetsEnabled.value[w.id] !== false)
|
||||
})
|
||||
|
||||
const placedDesktopTiles = computed<PlacedDesktopTile[]>(() => {
|
||||
const cols = 4
|
||||
const rows = 4
|
||||
const occupied = Array.from({ length: rows }, () => Array.from({ length: cols }, () => false))
|
||||
|
||||
const tryPlace = (tile: DesktopTile): PlacedDesktopTile | null => {
|
||||
const rowSpan = tile.size === '2x4' ? 4 : 2
|
||||
const colSpan = tile.size === '4x2' ? 4 : 2
|
||||
for (let r = 1; r <= rows - rowSpan + 1; r += 1) {
|
||||
for (let c = 1; c <= cols - colSpan + 1; c += 1) {
|
||||
let ok = true
|
||||
for (let rr = r; rr < r + rowSpan; rr += 1) {
|
||||
for (let cc = c; cc < c + colSpan; cc += 1) {
|
||||
if (occupied[rr - 1]?.[cc - 1]) {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!ok) break
|
||||
}
|
||||
if (!ok) continue
|
||||
for (let rr = r; rr < r + rowSpan; rr += 1) {
|
||||
for (let cc = c; cc < c + colSpan; cc += 1) {
|
||||
occupied[rr - 1][cc - 1] = true
|
||||
}
|
||||
}
|
||||
return { ...tile, row: r, col: c, rowSpan, colSpan }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const out: PlacedDesktopTile[] = []
|
||||
for (const tile of enabledWidgets.value) {
|
||||
const placed = tryPlace(tile)
|
||||
if (placed) out.push(placed)
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
const placedWidgetIds = computed(() => new Set(placedDesktopTiles.value.map((t) => t.id)))
|
||||
|
||||
const settingsWidgets = computed(() => {
|
||||
return widgetsInOrder.value.map((w) => {
|
||||
const sizeLabel = w.size === '2x4' ? '2×4' : w.size === '4x2' ? '4×2' : '2×2'
|
||||
const enabled = widgetsEnabled.value[w.id] !== false
|
||||
const visible = placedWidgetIds.value.has(w.id)
|
||||
return { id: w.id, title: w.title, sizeLabel, enabled, visible }
|
||||
})
|
||||
})
|
||||
|
||||
const widgetTitle = (id: string): string => {
|
||||
if (id === 'writingPanel') return '书写'
|
||||
const found = availableWidgets.find((w) => w.id === id)
|
||||
if (found) return found.title
|
||||
const foundAction = writingActions.find((a) => a.id === id)
|
||||
if (foundAction) return foundAction.label
|
||||
return id
|
||||
}
|
||||
|
||||
const actionSummary = (id: string): string => {
|
||||
const config = actionConfigs.value[id]
|
||||
const target = config?.target?.trim() ?? ''
|
||||
if (!config || !target) {
|
||||
if (id === 'moreApps') return '默认:打开应用列表'
|
||||
return '未配置'
|
||||
}
|
||||
return config.kind === 'app' ? `打开应用:${target}` : `打开URL:${target}`
|
||||
}
|
||||
|
||||
const enabledWidgetOrder = computed(() => {
|
||||
return widgetsOrder.value.filter((id) => widgetsEnabled.value[id] !== false)
|
||||
})
|
||||
|
||||
const moveEnabledWidget = (id: string, delta: -1 | 1): void => {
|
||||
const enabled = enabledWidgetOrder.value
|
||||
const index = enabled.indexOf(id)
|
||||
if (index < 0) return
|
||||
const nextIndex = index + delta
|
||||
if (nextIndex < 0 || nextIndex >= enabled.length) return
|
||||
const otherId = enabled[nextIndex]
|
||||
|
||||
const full = [...widgetsOrder.value]
|
||||
const a = full.indexOf(id)
|
||||
const b = full.indexOf(otherId)
|
||||
if (a < 0 || b < 0) return
|
||||
full[a] = otherId
|
||||
full[b] = id
|
||||
widgetsOrder.value = full
|
||||
}
|
||||
|
||||
const selectSettingsTab = (tab: SettingsTab): void => {
|
||||
settingsTab.value = tab
|
||||
isHomeEditMode.value = false
|
||||
}
|
||||
|
||||
const enterHomeEdit = (): void => {
|
||||
settingsTab.value = 'homeEdit'
|
||||
isHomeEditMode.value = true
|
||||
if (!homeEditSelectedId.value) {
|
||||
homeEditSelectedId.value = 'whiteboard'
|
||||
}
|
||||
}
|
||||
|
||||
const exitHomeEdit = (): void => {
|
||||
isHomeEditMode.value = false
|
||||
}
|
||||
|
||||
const selectHomeWidget = (id: string): void => {
|
||||
homeEditSelectedId.value = id
|
||||
}
|
||||
|
||||
const toggleHomeWidget = (id: string): void => {
|
||||
homeEditSelectedId.value = id
|
||||
toggleWidgetEnabled(id)
|
||||
}
|
||||
|
||||
let timerId: number | undefined
|
||||
let appsScrollTimerId: number | undefined
|
||||
let launchTimerId: number | undefined
|
||||
let notesSaveTimerId: number | undefined
|
||||
|
||||
const activeAppPointer = ref<{
|
||||
pointerId: number
|
||||
@@ -119,8 +671,121 @@ onMounted(() => {
|
||||
timerId = window.setInterval(() => {
|
||||
now.value = new Date()
|
||||
}, 1000)
|
||||
|
||||
try {
|
||||
const savedV2 = window.localStorage.getItem('lanmountain.notes.v2')
|
||||
if (typeof savedV2 === 'string' && savedV2) {
|
||||
const parsed = JSON.parse(savedV2) as { notes?: unknown }
|
||||
const rawNotes = Array.isArray(parsed.notes) ? parsed.notes : []
|
||||
const loaded: Note[] = rawNotes
|
||||
.map((n) => {
|
||||
const id = typeof (n as { id?: unknown }).id === 'string' ? (n as { id: string }).id : ''
|
||||
const text = typeof (n as { text?: unknown }).text === 'string' ? (n as { text: string }).text : ''
|
||||
if (!id) return null
|
||||
return { id, text }
|
||||
})
|
||||
.filter(Boolean) as Note[]
|
||||
notes.value = loaded
|
||||
} else {
|
||||
const savedV1 = window.localStorage.getItem('lanmountain.notes.v1')
|
||||
if (typeof savedV1 === 'string' && savedV1.trim()) {
|
||||
notes.value = [{ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, text: savedV1 }]
|
||||
window.localStorage.removeItem('lanmountain.notes.v1')
|
||||
} else {
|
||||
notes.value = []
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem('lanmountain.desktop.widgets.v1')
|
||||
if (typeof raw !== 'string' || !raw) return
|
||||
const parsed = JSON.parse(raw) as {
|
||||
order?: unknown
|
||||
enabled?: unknown
|
||||
}
|
||||
const order = Array.isArray(parsed.order) ? parsed.order.filter((x) => typeof x === 'string') : null
|
||||
const enabled =
|
||||
parsed.enabled && typeof parsed.enabled === 'object' && !Array.isArray(parsed.enabled) ? parsed.enabled : null
|
||||
if (order) widgetsOrder.value = order as string[]
|
||||
if (enabled) {
|
||||
const next: Record<string, boolean> = { ...widgetsEnabled.value }
|
||||
for (const [k, v] of Object.entries(enabled as Record<string, unknown>)) {
|
||||
if (typeof v === 'boolean') next[k] = v
|
||||
}
|
||||
widgetsEnabled.value = next
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem('lanmountain.desktop.actions.v1')
|
||||
if (typeof raw === 'string' && raw) {
|
||||
const parsed = JSON.parse(raw) as { actions?: unknown }
|
||||
const actions =
|
||||
parsed.actions && typeof parsed.actions === 'object' && !Array.isArray(parsed.actions) ? parsed.actions : null
|
||||
if (actions) {
|
||||
const next: Record<string, ActionConfig> = {}
|
||||
for (const [k, v] of Object.entries(actions as Record<string, unknown>)) {
|
||||
if (!v || typeof v !== 'object' || Array.isArray(v)) continue
|
||||
const kind = (v as { kind?: unknown }).kind
|
||||
const target = (v as { target?: unknown }).target
|
||||
if ((kind === 'app' || kind === 'url') && typeof target === 'string') {
|
||||
next[k] = { kind, target }
|
||||
}
|
||||
}
|
||||
actionConfigs.value = next
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
|
||||
watch(
|
||||
notes,
|
||||
(next) => {
|
||||
if (notesSaveTimerId) window.clearTimeout(notesSaveTimerId)
|
||||
notesSaveTimerId = window.setTimeout(() => {
|
||||
try {
|
||||
window.localStorage.setItem('lanmountain.notes.v2', JSON.stringify({ notes: next }))
|
||||
} catch {}
|
||||
}, 250)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const addNote = (): void => {
|
||||
const id = `${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
notes.value = [{ id, text: '' }, ...notes.value]
|
||||
}
|
||||
|
||||
watch(
|
||||
[widgetsOrder, widgetsEnabled],
|
||||
() => {
|
||||
if (widgetsSaveTimerId) window.clearTimeout(widgetsSaveTimerId)
|
||||
widgetsSaveTimerId = window.setTimeout(() => {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
'lanmountain.desktop.widgets.v1',
|
||||
JSON.stringify({ order: widgetsOrder.value, enabled: widgetsEnabled.value })
|
||||
)
|
||||
} catch {}
|
||||
}, 250)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
actionConfigs,
|
||||
(next) => {
|
||||
if (actionSaveTimerId) window.clearTimeout(actionSaveTimerId)
|
||||
actionSaveTimerId = window.setTimeout(() => {
|
||||
try {
|
||||
window.localStorage.setItem('lanmountain.desktop.actions.v1', JSON.stringify({ actions: next }))
|
||||
} catch {}
|
||||
}, 250)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const handleAppsListScroll = (): void => {
|
||||
isAppsScrolling.value = true
|
||||
if (appsScrollTimerId) window.clearTimeout(appsScrollTimerId)
|
||||
@@ -153,21 +818,159 @@ const loadApps = async (): Promise<void> => {
|
||||
}
|
||||
|
||||
const openApps = async (): Promise<void> => {
|
||||
page.value = 'apps'
|
||||
mainPage.value = 'apps'
|
||||
if (apps.value.length === 0 || loadError.value) {
|
||||
await loadApps()
|
||||
}
|
||||
}
|
||||
|
||||
const closeApps = (): void => {
|
||||
page.value = 'home'
|
||||
mainPage.value = 'home'
|
||||
query.value = ''
|
||||
}
|
||||
|
||||
const openExternal = async (url: string): Promise<void> => {
|
||||
await window.api.call({ method: 'POST', path: '/open/external', body: { url } })
|
||||
}
|
||||
|
||||
const activateAction = async (actionId: string, fallback?: () => void): Promise<void> => {
|
||||
const config = actionConfigs.value[actionId]
|
||||
const target = config?.target?.trim() ?? ''
|
||||
if (!config || !target) {
|
||||
fallback?.()
|
||||
return
|
||||
}
|
||||
if (config.kind === 'app') {
|
||||
await launch(target)
|
||||
return
|
||||
}
|
||||
await openExternal(target)
|
||||
}
|
||||
|
||||
const openSettings = (): void => {
|
||||
isSettingsOpen.value = true
|
||||
settingsTab.value = 'homeEdit'
|
||||
isHomeEditMode.value = false
|
||||
}
|
||||
|
||||
const closeSettings = (): void => {
|
||||
isSettingsOpen.value = false
|
||||
isHomeEditMode.value = false
|
||||
}
|
||||
|
||||
const handleTilePointerUp = async (tile: DesktopTile): Promise<void> => {
|
||||
if (Date.now() < suppressTapUntil) return
|
||||
await tile.onActivate?.()
|
||||
}
|
||||
|
||||
const handleWritingActionPointerUp = async (actionId: string): Promise<void> => {
|
||||
if (Date.now() < suppressTapUntil) return
|
||||
await activateAction(actionId)
|
||||
}
|
||||
|
||||
const handlePagePointerDown = (event: PointerEvent): void => {
|
||||
if (isSettingsOpen.value) return
|
||||
if (pageSwipePointerId !== null) return
|
||||
const tag = (event.target as HTMLElement | null)?.tagName?.toLowerCase() ?? ''
|
||||
if (tag === 'input' || tag === 'textarea' || tag === 'select') return
|
||||
|
||||
if (mainPage.value === 'apps' && event.clientX > 36) {
|
||||
return
|
||||
}
|
||||
|
||||
pageSwipePointerId = event.pointerId
|
||||
pageSwipeStartX = event.clientX
|
||||
pageSwipeStartY = event.clientY
|
||||
pageSwipeStartTime = Date.now()
|
||||
pageSwipeStartMainPage = mainPage.value
|
||||
}
|
||||
|
||||
const handlePagePointerMove = (event: PointerEvent): void => {
|
||||
if (pageSwipePointerId === null) return
|
||||
if (event.pointerId !== pageSwipePointerId) return
|
||||
|
||||
const dx = event.clientX - pageSwipeStartX
|
||||
const dy = event.clientY - pageSwipeStartY
|
||||
if (Math.abs(dx) < 12 && Math.abs(dy) < 12) return
|
||||
|
||||
if (Math.abs(dx) > Math.abs(dy) + 24 && Math.abs(dx) > 60) {
|
||||
suppressTapUntil = Date.now() + 350
|
||||
}
|
||||
}
|
||||
|
||||
const handlePagePointerUp = (event: PointerEvent): void => {
|
||||
if (pageSwipePointerId === null) return
|
||||
if (event.pointerId !== pageSwipePointerId) return
|
||||
|
||||
const dt = Date.now() - pageSwipeStartTime
|
||||
const dx = event.clientX - pageSwipeStartX
|
||||
const dy = event.clientY - pageSwipeStartY
|
||||
|
||||
pageSwipePointerId = null
|
||||
|
||||
if (dt > 900) return
|
||||
if (Math.abs(dx) < 90) return
|
||||
if (Math.abs(dx) <= Math.abs(dy) + 28) return
|
||||
|
||||
suppressTapUntil = Date.now() + 450
|
||||
|
||||
if (pageSwipeStartMainPage === 'home' && dx < -90) {
|
||||
void openApps()
|
||||
return
|
||||
}
|
||||
|
||||
if (pageSwipeStartMainPage === 'apps' && dx > 90 && pageSwipeStartX <= 36) {
|
||||
closeApps()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePagePointerCancel = (event: PointerEvent): void => {
|
||||
if (pageSwipePointerId === null) return
|
||||
if (event.pointerId !== pageSwipePointerId) return
|
||||
pageSwipePointerId = null
|
||||
}
|
||||
|
||||
const restoreDefaultWidgets = (): void => {
|
||||
widgetsOrder.value = ['writingPanel', 'whiteboard', 'fileManager', 'visualPresenter', 'browser', 'moreApps']
|
||||
widgetsEnabled.value = {
|
||||
writingPanel: true,
|
||||
whiteboard: true,
|
||||
fileManager: true,
|
||||
visualPresenter: true,
|
||||
browser: true,
|
||||
moreApps: true
|
||||
}
|
||||
}
|
||||
|
||||
const toggleWidgetEnabled = (id: string): void => {
|
||||
const next = { ...widgetsEnabled.value }
|
||||
next[id] = !(next[id] !== false)
|
||||
widgetsEnabled.value = next
|
||||
}
|
||||
|
||||
const setActionKind = (id: string, kind: ActionKind): void => {
|
||||
const current = actionConfigs.value[id] ?? { kind, target: '' }
|
||||
actionConfigs.value = { ...actionConfigs.value, [id]: { ...current, kind } }
|
||||
}
|
||||
|
||||
const setActionTarget = (id: string, target: string): void => {
|
||||
const current = actionConfigs.value[id] ?? { kind: 'url', target: '' }
|
||||
actionConfigs.value = { ...actionConfigs.value, [id]: { ...current, target } }
|
||||
}
|
||||
|
||||
const clearAction = (id: string): void => {
|
||||
const next = { ...actionConfigs.value }
|
||||
delete next[id]
|
||||
actionConfigs.value = next
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timerId) window.clearInterval(timerId)
|
||||
if (appsScrollTimerId) window.clearTimeout(appsScrollTimerId)
|
||||
if (launchTimerId) window.clearTimeout(launchTimerId)
|
||||
if (notesSaveTimerId) window.clearTimeout(notesSaveTimerId)
|
||||
if (widgetsSaveTimerId) window.clearTimeout(widgetsSaveTimerId)
|
||||
if (actionSaveTimerId) window.clearTimeout(actionSaveTimerId)
|
||||
})
|
||||
|
||||
const timeText = computed(() => {
|
||||
|
||||
@@ -20,6 +20,36 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pagesViewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pagesTrack {
|
||||
width: 200%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
transition: transform 220ms ease;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.pagesTrack[data-active='home'] {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.pagesTrack[data-active='apps'] {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
transform: translateZ(0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.touchButton {
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
@@ -46,8 +76,257 @@ body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.desktopGridShell {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
top: 88px;
|
||||
bottom: 96px;
|
||||
right: calc(50vw + 8px);
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.desktopGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr));
|
||||
grid-template-rows: repeat(4, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.deskTile {
|
||||
cursor: default;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: var(--ev-c-text-1);
|
||||
padding: 14px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button.deskTile {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.deskTile:hover {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.deskTile:active {
|
||||
border-color: rgba(255, 255, 255, 0.26);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.deskTileTitle {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.deskTileSubtitle {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
font-weight: 700;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.deskTileRow {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.deskTileIcon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex: 0 0 auto;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.deskTileText {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.deskTile--panel {
|
||||
justify-content: flex-start;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.writingPanelTitle {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-weight: 900;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.writingPanelButtons {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.writingActionButton {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: 14px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
color: var(--ev-c-text-1);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.writingActionButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.writingActionButton:active {
|
||||
border-color: rgba(255, 255, 255, 0.26);
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.writingActionIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex: 0 0 auto;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.writingActionText {
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.deskTile--accent {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.deskTile--soft {
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.notesPanel {
|
||||
position: fixed;
|
||||
top: 88px;
|
||||
right: 16px;
|
||||
bottom: 96px;
|
||||
left: calc(50vw + 8px);
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 14px;
|
||||
backdrop-filter: blur(12px);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-rows: 56px 1fr;
|
||||
}
|
||||
|
||||
.notesHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 14px;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
font-weight: 800;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.notesHeaderTitle {
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.notesAddButton {
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: var(--ev-c-text-1);
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 800;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.notesAddButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
.notesGrid {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
grid-auto-rows: 200px;
|
||||
gap: 12px;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.notesGrid::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.noteCard {
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.noteTextarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
resize: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: var(--ev-c-text-1);
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
font-weight: 700;
|
||||
user-select: text;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.noteTextarea::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.openAppsButton {
|
||||
@@ -78,6 +357,468 @@ body {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.settingsPage {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.settingsContentArea {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
top: 88px;
|
||||
bottom: 120px;
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 14px;
|
||||
backdrop-filter: blur(12px);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settingsSidebar {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.settingsSidebarTitle {
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
font-weight: 900;
|
||||
opacity: 0.95;
|
||||
padding: 4px 4px 8px;
|
||||
}
|
||||
|
||||
.settingsTabButton {
|
||||
cursor: pointer;
|
||||
height: 56px;
|
||||
border-radius: 14px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: var(--ev-c-text-1);
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 900;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.settingsTabButton[data-active='true'] {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
.settingsTabButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
.settingsDetail {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-rows: 64px 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.settingsDetailHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.settingsDetailTitle {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.settingsDetailHeaderActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.settingsDetailBody {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 0 4px 4px;
|
||||
box-sizing: border-box;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.settingsDetailBody::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settingsSection {
|
||||
padding: 8px 2px;
|
||||
}
|
||||
|
||||
.settingsSectionTitle {
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 900;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settingsPrimaryButton {
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: var(--ev-c-text-1);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 900;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.settingsPrimaryButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.48);
|
||||
}
|
||||
|
||||
.settingsResetButton {
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: var(--ev-c-text-1);
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 800;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.settingsResetButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.settingsPreviewList {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settingsPreviewItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.settingsPreviewMain {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.settingsPreviewTitle {
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.settingsPreviewMeta {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
font-weight: 700;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.settingsPreviewBehavior {
|
||||
width: 100%;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding-top: 10px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.settingsPreviewBehaviorTitle {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
font-weight: 900;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.settingsPreviewBehaviorRow {
|
||||
display: grid;
|
||||
grid-template-columns: 160px 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.settingsPreviewBehaviorLabel {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
font-weight: 800;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.settingsPreviewBehaviorValue {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
font-weight: 700;
|
||||
opacity: 0.9;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.homeEditGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.homeWidgetCard {
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: var(--ev-c-text-1);
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
display: grid;
|
||||
grid-template-rows: 24px 1fr 18px;
|
||||
gap: 8px;
|
||||
height: 112px;
|
||||
}
|
||||
|
||||
.homeWidgetCard[data-active='true'] {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.12),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.12) inset;
|
||||
}
|
||||
|
||||
.homeWidgetCard[data-selected='true'] {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.3);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.homeWidgetCardIcon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.homeWidgetCardTitle {
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 900;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.homeWidgetCardMeta {
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
font-weight: 700;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.homeEditOrderList {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.homeEditOrderRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.homeEditOrderTitle {
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.homeEditOrderActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.settingsConfigArea {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.settingsSingleActionRow {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr 92px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.settingsActionHint {
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
font-weight: 700;
|
||||
opacity: 0.8;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.settingsSelect {
|
||||
height: 46px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
color: var(--ev-c-text-1);
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.settingsInput {
|
||||
height: 46px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
color: var(--ev-c-text-1);
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 700;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.settingsMiniButton {
|
||||
cursor: pointer;
|
||||
height: 46px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
color: var(--ev-c-text-1);
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.settingsMiniButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
.settingsSubAction {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 180px 140px 1fr 92px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.settingsSubActionTitle {
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 900;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.settingsActionButton {
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: var(--ev-c-text-1);
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
font-weight: 800;
|
||||
height: 46px;
|
||||
min-width: 92px;
|
||||
}
|
||||
|
||||
.settingsActionButton[data-active='true'] {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
.settingsActionButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
.settingsBottomBar {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
height: 92px;
|
||||
padding: 10px 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr 180px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(12px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.settingsBottomBar .backButton {
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
.settingsBottomBar .appsExitButton {
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
cursor: pointer;
|
||||
border-radius: 16px;
|
||||
@@ -372,6 +1113,31 @@ body {
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.settingsButton {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
bottom: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
border-radius: 14px;
|
||||
padding: 10px 18px;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: var(--ev-c-text-1);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.settingsButton:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.buttonIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
Reference in New Issue
Block a user