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 path = require("path");
|
||||||
const utils = require("@electron-toolkit/utils");
|
const utils = require("@electron-toolkit/utils");
|
||||||
const elysia = require("elysia");
|
const elysia = require("elysia");
|
||||||
const fs = require("fs");
|
|
||||||
const child_process = require("child_process");
|
const child_process = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
const util = require("util");
|
const util = require("util");
|
||||||
const node = require("@elysiajs/node");
|
const node = require("@elysiajs/node");
|
||||||
const icon = path.join(__dirname, "../../resources/icon.png");
|
const icon = path.join(__dirname, "../../resources/icon.png");
|
||||||
@@ -20,7 +20,7 @@ function getStartMenuRoots() {
|
|||||||
const { userProgramsPath, commonProgramsPath } = getStartMenuPaths();
|
const { userProgramsPath, commonProgramsPath } = getStartMenuPaths();
|
||||||
return [userProgramsPath, commonProgramsPath].filter((p) => Boolean(p));
|
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() {
|
function getWindowsPowerShellExe() {
|
||||||
const systemRoot = process.env["SystemRoot"] ?? process.env["WINDIR"] ?? "C:\\Windows";
|
const systemRoot = process.env["SystemRoot"] ?? process.env["WINDIR"] ?? "C:\\Windows";
|
||||||
return path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
return path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
||||||
@@ -124,7 +124,7 @@ async function listWindowsStartMenuApps() {
|
|||||||
const roots = getStartMenuRoots();
|
const roots = getStartMenuRoots();
|
||||||
const script = buildPowerShellScript(roots);
|
const script = buildPowerShellScript(roots);
|
||||||
const powershellExe = getWindowsPowerShellExe();
|
const powershellExe = getWindowsPowerShellExe();
|
||||||
const { stdout } = await execFileAsync$1(
|
const { stdout } = await execFileAsync$2(
|
||||||
powershellExe,
|
powershellExe,
|
||||||
["-NoProfile", "-NonInteractive", "-Sta", "-ExecutionPolicy", "Bypass", "-Command", script],
|
["-NoProfile", "-NonInteractive", "-Sta", "-ExecutionPolicy", "Bypass", "-Command", script],
|
||||||
{ windowsHide: true, maxBuffer: 50 * 1024 * 1024, timeout: 6e4 }
|
{ 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"));
|
result.sort((a, b) => a.name.localeCompare(b.name, "zh-CN"));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const execFileAsync = util.promisify(child_process.execFile);
|
const execFileAsync$1 = util.promisify(child_process.execFile);
|
||||||
function isUnderRoot(filePath, root) {
|
function isUnderRoot(filePath, root) {
|
||||||
const normalizedFile = path.resolve(filePath).toLowerCase();
|
const normalizedFile = path.resolve(filePath).toLowerCase();
|
||||||
const normalizedRoot = path.resolve(root).toLowerCase();
|
const normalizedRoot = path.resolve(root).toLowerCase();
|
||||||
@@ -166,7 +166,7 @@ function isUnderRoot(filePath, root) {
|
|||||||
async function launchStartMenuEntry(filePath) {
|
async function launchStartMenuEntry(filePath) {
|
||||||
if (process.platform !== "win32") return;
|
if (process.platform !== "win32") return;
|
||||||
if (filePath.startsWith("shell:AppsFolder\\")) {
|
if (filePath.startsWith("shell:AppsFolder\\")) {
|
||||||
await execFileAsync("explorer.exe", [filePath], { windowsHide: true });
|
await execFileAsync$1("explorer.exe", [filePath], { windowsHide: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const roots = getStartMenuRoots();
|
const roots = getStartMenuRoots();
|
||||||
@@ -179,6 +179,7 @@ async function launchStartMenuEntry(filePath) {
|
|||||||
throw new Error(result);
|
throw new Error(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const execFileAsync = util.promisify(child_process.execFile);
|
||||||
function createEiysiaApp(deps) {
|
function createEiysiaApp(deps) {
|
||||||
const iconCache = /* @__PURE__ */ new Map();
|
const iconCache = /* @__PURE__ */ new Map();
|
||||||
const appsCacheFilePath = path.join(electron.app.getPath("userData"), "apps-cache.json");
|
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 });
|
if (message === "PathNotAllowed") return new Response("Forbidden", { status: 403 });
|
||||||
return new Response("LaunchFailed", { status: 500 });
|
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", () => ({
|
}).get("/backend/port", () => ({
|
||||||
port: deps.getHttpPort()
|
port: deps.getHttpPort()
|
||||||
})).post("/app/minimize", () => {
|
})).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"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
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>
|
<script type="module" crossorigin src="./assets/index-CkK6wThO.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-DAQiHmWm.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-BOqh2SqZ.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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 { Elysia } from 'elysia'
|
||||||
|
import { execFile } from 'child_process'
|
||||||
import { promises as fs } from 'fs'
|
import { promises as fs } from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
import { promisify } from 'util'
|
||||||
import { launchStartMenuEntry, listWindowsStartMenuApps } from '../app_list'
|
import { launchStartMenuEntry, listWindowsStartMenuApps } from '../app_list'
|
||||||
|
|
||||||
export interface EiysiaDependencies {
|
export interface EiysiaDependencies {
|
||||||
@@ -9,6 +11,8 @@ export interface EiysiaDependencies {
|
|||||||
getHttpPort: () => number | null
|
getHttpPort: () => number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile)
|
||||||
|
|
||||||
export function createEiysiaApp(deps: EiysiaDependencies): {
|
export function createEiysiaApp(deps: EiysiaDependencies): {
|
||||||
handle: (request: Request) => Response | Promise<Response>
|
handle: (request: Request) => Response | Promise<Response>
|
||||||
} {
|
} {
|
||||||
@@ -216,6 +220,36 @@ export function createEiysiaApp(deps: EiysiaDependencies): {
|
|||||||
return new Response('LaunchFailed', { status: 500 })
|
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', () => ({
|
.get('/backend/port', () => ({
|
||||||
port: deps.getHttpPort()
|
port: deps.getHttpPort()
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,54 +1,332 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="screen">
|
<div class="screen">
|
||||||
<div class="clock">{{ timeText }}</div>
|
<div class="clock">{{ timeText }}</div>
|
||||||
<div v-if="page === 'home'" class="home">
|
<div
|
||||||
<button
|
class="pagesViewport"
|
||||||
class="openAppsButton touchButton"
|
@pointerdown="handlePagePointerDown"
|
||||||
type="button"
|
@pointermove="handlePagePointerMove"
|
||||||
@pointerup="openApps"
|
@pointerup="handlePagePointerUp"
|
||||||
@keydown.enter.prevent="openApps"
|
@pointercancel="handlePagePointerCancel"
|
||||||
@keydown.space.prevent="openApps"
|
>
|
||||||
>
|
<div class="pagesTrack" :data-active="mainPage">
|
||||||
应用列表
|
<div class="page page--home">
|
||||||
</button>
|
<div class="home">
|
||||||
</div>
|
<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="notesPanel">
|
||||||
<div class="appsTilesArea">
|
<div class="notesHeader">
|
||||||
<div v-if="isLoadingApps" class="appsLoading">加载中...</div>
|
<div class="notesHeaderTitle">作业版</div>
|
||||||
<div v-else-if="loadError" class="error">
|
<button class="notesAddButton touchButton" type="button" @pointerup="addNote">新建</button>
|
||||||
<div class="errorHeader">
|
</div>
|
||||||
<div class="errorTitle">加载失败</div>
|
<div class="notesGrid">
|
||||||
<button class="copyButton touchButton" type="button" @pointerup="copyLoadError">复制错误</button>
|
<div v-for="note in notes" :key="note.id" class="noteCard">
|
||||||
|
<textarea v-model="note.text" class="noteTextarea" placeholder="写点什么..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="errorBody">{{ loadError }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="appsList" @scroll="handleAppsListScroll">
|
<div class="page page--apps">
|
||||||
<template v-for="group in groupedApps" :key="group.key">
|
<div class="appsPage">
|
||||||
<div class="groupHeader">{{ group.label }}</div>
|
<div class="appsTilesArea">
|
||||||
<button
|
<div v-if="isLoadingApps" class="appsLoading">加载中...</div>
|
||||||
v-for="app in group.items"
|
<div v-else-if="loadError" class="error">
|
||||||
:key="app.id"
|
<div class="errorHeader">
|
||||||
class="item touchButton"
|
<div class="errorTitle">加载失败</div>
|
||||||
type="button"
|
<button class="copyButton touchButton" type="button" @pointerup="copyLoadError">复制错误</button>
|
||||||
@pointerdown="handleAppPointerDown($event, app.filePath)"
|
</div>
|
||||||
@pointermove="handleAppPointerMove($event)"
|
<div class="errorBody">{{ loadError }}</div>
|
||||||
@pointerup="handleAppPointerUp($event)"
|
</div>
|
||||||
@pointercancel="handleAppPointerCancel($event)"
|
|
||||||
@keydown.enter.prevent="launch(app.filePath)"
|
<div v-else class="appsList" @scroll="handleAppsListScroll">
|
||||||
@keydown.space.prevent="launch(app.filePath)"
|
<template v-for="group in groupedApps" :key="group.key">
|
||||||
>
|
<div class="groupHeader">{{ group.label }}</div>
|
||||||
<img class="icon" :src="app.iconDataUrl" alt="" />
|
<button
|
||||||
<div class="name">{{ app.name }}</div>
|
v-for="app in group.items"
|
||||||
</button>
|
:key="app.id"
|
||||||
</template>
|
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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="appsBottomBar">
|
<div v-if="isSettingsOpen" class="settingsPage">
|
||||||
<button class="backButton touchButton" type="button" @pointerup="closeApps">返回</button>
|
<div class="settingsContentArea">
|
||||||
<input v-model="query" class="appsSearch" type="text" placeholder="搜索应用..." />
|
<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">
|
<button class="appsExitButton touchButton" type="button" @pointerup="handleExit">
|
||||||
<svg
|
<svg
|
||||||
class="buttonIcon"
|
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"
|
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>
|
</svg>
|
||||||
Windows
|
回到Window
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<svg
|
||||||
class="buttonIcon"
|
class="buttonIcon"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
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"
|
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>
|
</svg>
|
||||||
Windows
|
回到Window
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="mainPage === 'home' && !isSettingsOpen"
|
||||||
|
class="settingsButton touchButton"
|
||||||
|
type="button"
|
||||||
|
@pointerup="openSettings"
|
||||||
|
>
|
||||||
|
设置
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 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 apps = ref<Array<{ id: string; name: string; filePath: string; iconDataUrl: string }>>([])
|
||||||
const query = ref('')
|
const query = ref('')
|
||||||
const loadError = ref<string | null>(null)
|
const loadError = ref<string | null>(null)
|
||||||
const isLoadingApps = ref(false)
|
const isLoadingApps = ref(false)
|
||||||
const isAppsScrolling = 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 timerId: number | undefined
|
||||||
let appsScrollTimerId: number | undefined
|
let appsScrollTimerId: number | undefined
|
||||||
let launchTimerId: number | undefined
|
let launchTimerId: number | undefined
|
||||||
|
let notesSaveTimerId: number | undefined
|
||||||
|
|
||||||
const activeAppPointer = ref<{
|
const activeAppPointer = ref<{
|
||||||
pointerId: number
|
pointerId: number
|
||||||
@@ -119,8 +671,121 @@ onMounted(() => {
|
|||||||
timerId = window.setInterval(() => {
|
timerId = window.setInterval(() => {
|
||||||
now.value = new Date()
|
now.value = new Date()
|
||||||
}, 1000)
|
}, 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 => {
|
const handleAppsListScroll = (): void => {
|
||||||
isAppsScrolling.value = true
|
isAppsScrolling.value = true
|
||||||
if (appsScrollTimerId) window.clearTimeout(appsScrollTimerId)
|
if (appsScrollTimerId) window.clearTimeout(appsScrollTimerId)
|
||||||
@@ -153,21 +818,159 @@ const loadApps = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openApps = async (): Promise<void> => {
|
const openApps = async (): Promise<void> => {
|
||||||
page.value = 'apps'
|
mainPage.value = 'apps'
|
||||||
if (apps.value.length === 0 || loadError.value) {
|
if (apps.value.length === 0 || loadError.value) {
|
||||||
await loadApps()
|
await loadApps()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeApps = (): void => {
|
const closeApps = (): void => {
|
||||||
page.value = 'home'
|
mainPage.value = 'home'
|
||||||
query.value = ''
|
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(() => {
|
onBeforeUnmount(() => {
|
||||||
if (timerId) window.clearInterval(timerId)
|
if (timerId) window.clearInterval(timerId)
|
||||||
if (appsScrollTimerId) window.clearTimeout(appsScrollTimerId)
|
if (appsScrollTimerId) window.clearTimeout(appsScrollTimerId)
|
||||||
if (launchTimerId) window.clearTimeout(launchTimerId)
|
if (launchTimerId) window.clearTimeout(launchTimerId)
|
||||||
|
if (notesSaveTimerId) window.clearTimeout(notesSaveTimerId)
|
||||||
|
if (widgetsSaveTimerId) window.clearTimeout(widgetsSaveTimerId)
|
||||||
|
if (actionSaveTimerId) window.clearTimeout(actionSaveTimerId)
|
||||||
})
|
})
|
||||||
|
|
||||||
const timeText = computed(() => {
|
const timeText = computed(() => {
|
||||||
|
|||||||
@@ -20,6 +20,36 @@ body {
|
|||||||
height: 100%;
|
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 {
|
.touchButton {
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
@@ -46,8 +76,257 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
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;
|
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 {
|
.openAppsButton {
|
||||||
@@ -78,6 +357,468 @@ body {
|
|||||||
box-sizing: border-box;
|
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 {
|
.backButton {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@@ -372,6 +1113,31 @@ body {
|
|||||||
backdrop-filter: blur(12px);
|
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 {
|
.buttonIcon {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user