{update.title}
+{update.summary}
+ + {update.images?.length ? ( +-
+ {update.details.map((detail) => (
+
- {detail} + ))} +
diff --git a/scripts/windows/Install-Moonsea-Windows.ps1 b/scripts/windows/Install-Moonsea-Windows.ps1 index 3e5fae4..5b367c4 100644 --- a/scripts/windows/Install-Moonsea-Windows.ps1 +++ b/scripts/windows/Install-Moonsea-Windows.ps1 @@ -62,6 +62,8 @@ $draftSourcePath = Join-Path $projectRoot "assets\admin-drafts" $managerExtension = [System.IO.Path]::GetExtension($ManagerPath) $managerFileName = if ($managerExtension -eq ".mjs") { "MoonseaManager.mjs" } else { "MoonseaManager.exe" } $managerPidPath = Join-Path $InstallRoot "manager.pid" +$adminMarkerPath = Join-Path $InstallRoot "admin-access.enabled" +$legacyAdminMarkerPath = Join-Path (Join-Path $env:LOCALAPPDATA "MoonseaCodex") "admin-access.enabled" $packageMetadataPath = Join-Path $projectRoot "package.json" $updaterSourcePath = Join-Path $scriptRoot "Update-Moonsea-Windows.ps1" @@ -192,6 +194,11 @@ New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null New-Item -ItemType Directory -Path $buildsRoot -Force | Out-Null New-Item -ItemType Directory -Path $profilePath -Force | Out-Null New-Item -ItemType Directory -Path $releasesRoot -Force | Out-Null +if (-not (Test-Path -LiteralPath $adminMarkerPath -PathType Leaf) -and + -not $adminMarkerPath.Equals($legacyAdminMarkerPath, [System.StringComparison]::OrdinalIgnoreCase) -and + (Test-Path -LiteralPath $legacyAdminMarkerPath -PathType Leaf)) { + Copy-Item -LiteralPath $legacyAdminMarkerPath -Destination $adminMarkerPath +} $needsBuild = $true if (Test-Path -LiteralPath $activeBuild -PathType Container) { diff --git a/scripts/windows/Install-Moonsea-WorkBuddy-Windows.ps1 b/scripts/windows/Install-Moonsea-WorkBuddy-Windows.ps1 index 4a02fe9..054c3eb 100644 --- a/scripts/windows/Install-Moonsea-WorkBuddy-Windows.ps1 +++ b/scripts/windows/Install-Moonsea-WorkBuddy-Windows.ps1 @@ -62,6 +62,11 @@ $draftSourcePath = Join-Path $projectRoot "assets\admin-drafts" $managerExtension = [System.IO.Path]::GetExtension($ManagerPath) $managerFileName = if ($managerExtension -eq ".mjs") { "MoonseaWorkBuddyManager.mjs" } else { "MoonseaWorkBuddyManager.exe" } $managerPidPath = Join-Path $InstallRoot "manager.pid" +$adminMarkerPath = Join-Path $InstallRoot "admin-access.enabled" +$legacyAdminMarkerPaths = @( + (Join-Path (Join-Path $env:LOCALAPPDATA "MoonseaWorkBuddy") "admin-access.enabled"), + (Join-Path (Join-Path $env:LOCALAPPDATA "MoonseaCodex") "admin-access.enabled") +) $packageMetadataPath = Join-Path $projectRoot "package.json" $updaterSourcePath = Join-Path $scriptRoot "Update-Moonsea-WorkBuddy-Windows.ps1" @@ -227,6 +232,15 @@ New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null New-Item -ItemType Directory -Path $buildsRoot -Force | Out-Null New-Item -ItemType Directory -Path $profilePath -Force | Out-Null New-Item -ItemType Directory -Path $releasesRoot -Force | Out-Null +if (-not (Test-Path -LiteralPath $adminMarkerPath -PathType Leaf)) { + $legacyAdminMarkerPath = $legacyAdminMarkerPaths | Where-Object { + -not $adminMarkerPath.Equals($_, [System.StringComparison]::OrdinalIgnoreCase) -and + (Test-Path -LiteralPath $_ -PathType Leaf) + } | Select-Object -First 1 + if ($legacyAdminMarkerPath) { + Copy-Item -LiteralPath $legacyAdminMarkerPath -Destination $adminMarkerPath + } +} $needsBuild = $true if (Test-Path -LiteralPath $activeBuild -PathType Container) { diff --git a/src/manager-core.mjs b/src/manager-core.mjs index 4559858..14df76b 100644 --- a/src/manager-core.mjs +++ b/src/manager-core.mjs @@ -375,7 +375,7 @@ export function createRequestHandler({ sendJson(response, 200, { ok: true, appVersion, - adminAccess, + adminAccess: typeof adminAccess === "function" ? adminAccess() : adminAccess, catalogVersion: 3, themeDeliveryVersion, ...(await status(profilePath)), diff --git a/src/manager.mjs b/src/manager.mjs index 6a9a293..41fe973 100644 --- a/src/manager.mjs +++ b/src/manager.mjs @@ -57,7 +57,7 @@ if (appPidArgument !== null && (!Number.isInteger(appPid) || appPid < 1)) { } const projectRoot = findProjectRoot(); const pidPath = path.join(installRoot, "manager.pid"); -const adminAccess = fs.existsSync(path.join(installRoot, "admin-access.enabled")); +const hasAdminAccess = () => fs.existsSync(path.join(installRoot, "admin-access.enabled")); const updaterPath = process.platform === "win32" ? path.join( projectRoot, @@ -215,7 +215,7 @@ const server = http.createServer(createRequestHandler({ adminRoot: path.join(projectRoot, "admin"), draftRoot: path.join(projectRoot, "assets", "admin-drafts"), appVersion: APP_VERSION, - adminAccess, + adminAccess: hasAdminAccess, updateService, resolveTheme: (themeId) => themeDeliveryService.resolve(themeId), })); diff --git a/tests/manager.test.mjs b/tests/manager.test.mjs index 42f893d..386bd0b 100644 --- a/tests/manager.test.mjs +++ b/tests/manager.test.mjs @@ -142,6 +142,19 @@ test("管理员入口仅在本机授权标记存在时向官网公开", async () const owner = await requestLocalPage(ownerHandler, "/api/status", PUBLIC_SITE_ORIGIN); assert.equal(owner.statusCode, 200); assert.equal(JSON.parse(owner.body).adminAccess, true); + + let dynamicAccess = false; + const dynamicHandler = createRequestHandler({ + profilePath: "fixture", + siteRoot: path.join(projectRoot, "site"), + adminAccess: () => dynamicAccess, + status: async () => ({ connected: false, message: "fixture" }), + }); + const beforeMarker = await requestLocalPage(dynamicHandler, "/api/status", PUBLIC_SITE_ORIGIN); + assert.equal(JSON.parse(beforeMarker.body).adminAccess, false); + dynamicAccess = true; + const afterMarker = await requestLocalPage(dynamicHandler, "/api/status", PUBLIC_SITE_ORIGIN); + assert.equal(JSON.parse(afterMarker.body).adminAccess, true); }); test("主题创作台只由本机助手提供且实验壁纸不进入公开目录", async () => { @@ -625,6 +638,21 @@ test("Windows 发布脚本兼容非 UTF-8 系统区域的 PowerShell 5.1", () => assert.match(installer, /\$output \| ForEach-Object \{ Write-Host \$_ \}/); }); +test("Windows 自定义安装目录会迁移已有的管理员入口标记", () => { + const scriptsRoot = path.join(projectRoot, "scripts", "windows"); + const codexInstaller = fs.readFileSync( + path.join(scriptsRoot, "Install-Moonsea-Windows.ps1"), + "ascii", + ); + const workBuddyInstaller = fs.readFileSync( + path.join(scriptsRoot, "Install-Moonsea-WorkBuddy-Windows.ps1"), + "ascii", + ); + + assert.match(codexInstaller, /\$legacyAdminMarkerPath[\s\S]*Copy-Item -LiteralPath \$legacyAdminMarkerPath -Destination \$adminMarkerPath/); + assert.match(workBuddyInstaller, /\$legacyAdminMarkerPaths[\s\S]*"MoonseaCodex"[\s\S]*Copy-Item -LiteralPath \$legacyAdminMarkerPath -Destination \$adminMarkerPath/); +}); + test("WorkBuddy Windows 链路使用独立客户端协议与安装器", () => { const scriptsRoot = path.join(projectRoot, "scripts", "windows"); const installer = fs.readFileSync( diff --git a/web/app/globals.css b/web/app/globals.css index 64c8b24..56dd885 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -53,6 +53,8 @@ main, footer { width: min(var(--site-width), calc(100% - 40px)); margin-inline: .eyebrow { margin: 0 0 20px; color: #8fc8cf; font-size: 11px; font-weight: 780; letter-spacing: .16em; } .section-kicker { margin: 0 0 14px; color: #8fc8cf; font-size: 14px; font-weight: 720; letter-spacing: .02em; } +.landing-shell { --line: rgba(167, 204, 202, .18); --muted: #8ea7a9; min-height: 100vh; color: #edf5f0; background: #061823; } +.landing-shell footer { color: #8ea7a9; } .landing-main { position: relative; width: 100%; min-height: calc(100dvh - 72px); overflow: clip; isolation: isolate; background: #061823; } .landing-main > section { position: relative; z-index: 1; } .moonsea-backdrop { position: fixed; z-index: 0; inset: 72px 0 0; overflow: hidden; pointer-events: none; background: @@ -310,6 +312,38 @@ a.footer-owner-entry:hover, a.footer-owner-entry:focus-visible { color: #dce9e5; .themes-shell .theme-card__footer button:disabled { color: #71898b; background: rgba(5, 27, 37, .64); } .themes-shell footer { color: #8ea7a9; } +.updates-page { width: min(1050px, calc(100% - 40px)); padding: clamp(86px, 9vw, 132px) 0 132px; } +.updates-hero { display: grid; max-width: 780px; margin: 0 0 clamp(92px, 11vw, 148px); } +.updates-hero .eyebrow { margin-bottom: 25px; color: #82bfc2; } +.updates-hero h1 { margin: 0; color: #edf5f0; font-size: clamp(62px, 8vw, 108px); font-weight: 710; line-height: .88; letter-spacing: -.075em; text-wrap: balance; } +.updates-hero h1 span { display: block; color: #b9cfcb; } +.updates-hero > p:last-child { max-width: 580px; margin: 34px 0 0; color: #9eb6b6; font-size: clamp(16px, 1.6vw, 19px); line-height: 1.85; } +.updates-timeline { position: relative; display: grid; gap: 0; padding: 0; margin: 0; list-style: none; } +.updates-timeline::before { position: absolute; z-index: 0; top: 11px; bottom: 0; left: 171px; width: 1px; content: ""; background: linear-gradient(180deg, rgba(119, 199, 198, .72), rgba(117, 176, 178, .22) 78%, transparent); } +.update-entry { position: relative; display: grid; grid-template-columns: 140px 62px minmax(0, 1fr); align-items: start; } +.update-entry + .update-entry { padding-top: clamp(86px, 10vw, 132px); } +.update-entry__meta { display: grid; justify-items: end; gap: 8px; padding-top: 5px; color: #86a4a5; text-align: right; } +.update-entry__meta time { color: #c6d7d2; font-size: 13px; font-weight: 710; letter-spacing: .02em; } +.update-entry__meta span { font-size: 10px; font-weight: 740; letter-spacing: .12em; } +.update-entry__rail { position: relative; z-index: 1; display: grid; min-height: 28px; place-items: start center; padding-top: 1px; } +.update-entry__node { position: relative; display: block; width: 22px; height: 22px; border: 1px solid rgba(140, 207, 203, .60); border-radius: 50%; background: #082833; box-shadow: 0 0 0 6px rgba(6, 31, 42, .92), 0 0 26px rgba(90, 187, 187, .18); } +.update-entry__node::before { position: absolute; inset: 6px; border-radius: 50%; content: ""; background: #9fd3cc; box-shadow: 0 0 11px rgba(146, 217, 209, .68); } +.update-entry__node::after { position: absolute; top: 10px; left: 20px; width: 29px; height: 1px; content: ""; background: linear-gradient(90deg, rgba(144, 207, 203, .66), transparent); } +.update-entry__content { min-width: 0; padding-top: 1px; } +.update-entry__version { display: flex; min-height: 24px; align-items: center; gap: 9px; color: #8fc2c1; font-size: 12px; font-weight: 760; letter-spacing: .07em; } +.update-entry__version em { padding: 4px 7px; border: 1px solid rgba(245, 223, 157, .38); border-radius: 999px; color: #f0dda4; background: rgba(180, 145, 59, .10); font-size: 9px; font-style: normal; letter-spacing: .1em; } +.update-entry__content h2 { max-width: 760px; margin: 18px 0 0; color: #edf5f0; font-size: clamp(34px, 4.3vw, 58px); font-weight: 690; line-height: 1.04; letter-spacing: -.052em; text-wrap: balance; } +.update-entry__summary { max-width: 700px; margin: 23px 0 0; color: #aec3c1; font-size: clamp(16px, 1.5vw, 18px); line-height: 1.8; } +.update-entry__media { display: grid; gap: 16px; margin-top: 35px; } +.update-entry__media img { display: block; width: 100%; height: auto; border: 1px solid rgba(174, 214, 210, .22); border-radius: 15px; background: #03151f; box-shadow: 0 30px 78px rgba(0, 8, 14, .42); } +.update-entry__content ul { display: grid; gap: 12px; max-width: 690px; padding: 0; margin: 29px 0 0; color: #98b0b0; list-style: none; } +.update-entry__content li { position: relative; padding-left: 18px; font-size: 14px; line-height: 1.72; } +.update-entry__content li::before { position: absolute; top: .7em; left: 0; width: 7px; height: 1px; content: ""; background: #70aaa9; } +.update-entry__release { display: inline-flex; margin-top: 30px; align-items: center; gap: 7px; color: #c9dbd6; font-size: 13px; font-weight: 700; text-decoration-color: rgba(164, 207, 204, .38); text-underline-offset: 6px; } +.update-entry__release:hover { color: #f0f5ef; text-decoration-color: currentColor; } +.update-entry__release span { font-size: 15px; transition: transform .18s ease; } +.update-entry__release:hover span { transform: translate(2px, -2px); } + .theme-detail { display: grid; min-height: calc(100vh - 178px); align-items: center; grid-template-columns: minmax(0, 1.18fr) minmax(340px, .82fr); gap: clamp(44px, 7vw, 104px); padding-block: clamp(72px, 9vw, 128px); } .theme-detail__preview { aspect-ratio: 16 / 11; padding: clamp(24px, 4vw, 52px); overflow: hidden; border: 1px solid rgba(163, 204, 203, .22); border-radius: 16px; box-shadow: 0 34px 90px rgba(0, 8, 15, .44); } .theme-detail__preview.is-pro { padding: clamp(18px, 2.5vw, 34px); } @@ -394,6 +428,9 @@ a.footer-owner-entry:hover, a.footer-owner-entry:focus-visible { color: #dce9e5; .theme-gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); } .metric-grid--usage { grid-template-columns: repeat(3, 1fr); } .data-grid--three { grid-template-columns: 1fr 1fr; } + .updates-page { width: min(920px, calc(100% - 40px)); } + .updates-timeline::before { left: 151px; } + .update-entry { grid-template-columns: 120px 62px minmax(0, 1fr); } } @media (max-width: 680px) { @@ -446,6 +483,24 @@ a.footer-owner-entry:hover, a.footer-owner-entry:focus-visible { color: #dce9e5; .theme-preview-dialog__window.pro-codex-window .pro-codex-composer { min-height: 40px; } .theme-preview-dialog__footer { align-items: stretch; flex-direction: column; gap: 12px; padding: 14px 16px 16px; } .theme-preview-dialog__footer > div { display: grid; grid-template-columns: 1fr 1fr; } + .updates-page { width: min(100% - 28px, var(--site-width)); padding: 64px 0 92px; } + .updates-hero { margin-bottom: 82px; } + .updates-hero h1 { font-size: clamp(54px, 17vw, 78px); } + .updates-hero > p:last-child { margin-top: 26px; font-size: 15px; } + .updates-timeline::before { left: 11px; } + .update-entry { grid-template-columns: 34px minmax(0, 1fr); } + .update-entry + .update-entry { padding-top: 84px; } + .update-entry__meta { grid-column: 2; grid-row: 1; display: flex; justify-items: start; gap: 10px; padding: 0 0 12px; text-align: left; } + .update-entry__meta time, .update-entry__meta span { font-size: 10px; } + .update-entry__meta span::before { margin-right: 10px; content: "·"; } + .update-entry__rail { grid-column: 1; grid-row: 1 / span 2; justify-items: start; padding-top: 0; } + .update-entry__node { width: 22px; height: 22px; } + .update-entry__node::after { width: 17px; } + .update-entry__content { grid-column: 2; grid-row: 2; } + .update-entry__content h2 { margin-top: 14px; font-size: clamp(32px, 10vw, 46px); } + .update-entry__summary { margin-top: 19px; font-size: 15px; } + .update-entry__media { margin-top: 28px; } + .update-entry__media img { border-radius: 11px; } footer { align-items: flex-start; flex-direction: column; justify-content: center; gap: 12px; padding-block: 26px; } .footer-links { flex-wrap: wrap; gap: 12px 18px; } .activity-chart { gap: 4px; } diff --git a/web/app/page.tsx b/web/app/page.tsx index e95da6d..07cc3c1 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -2,7 +2,7 @@ import type { Metadata } from "next"; import { THEMES } from "../lib/theme-catalog"; import { FeaturedThemeSwitcher } from "./featured-theme-switcher"; import { MoonseaRipple } from "./moonsea-ripple"; -import { SiteHeader } from "./site-chrome"; +import { SiteFooter, SiteHeader } from "./site-chrome"; import { ThemeBrowserLink } from "./theme-browser-link"; export const dynamic = "force-static"; @@ -23,7 +23,7 @@ export default function Home() { const featuredThemes = [...latestWallpapers, ...latestGradients].slice(0, 6); return ( - <> +
MOONSEA RELEASE CURRENT
+
+ 从一张新壁纸,到一次更自然的应用体验。
+
+ 这里记录月海正在变好的每一步。
+
{update.summary}
+ + {update.images?.length ? ( +