Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions scripts/windows/Install-Moonsea-Windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 14 additions & 0 deletions scripts/windows/Install-Moonsea-WorkBuddy-Windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/manager-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
4 changes: 2 additions & 2 deletions src/manager.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
}));
Expand Down
28 changes: 28 additions & 0 deletions tests/manager.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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(
Expand Down
55 changes: 55 additions & 0 deletions web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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); }
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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; }
Expand Down
7 changes: 4 additions & 3 deletions web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -23,7 +23,7 @@ export default function Home() {
const featuredThemes = [...latestWallpapers, ...latestGradients].slice(0, 6);

return (
<>
<div className="landing-shell">
<SiteHeader />

<main className="landing-main">
Expand Down Expand Up @@ -51,6 +51,7 @@ export default function Home() {
</div>
</section>
</main>
</>
<SiteFooter />
</div>
);
}
2 changes: 1 addition & 1 deletion web/app/robots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: ["/", "/themes"],
allow: ["/", "/themes", "/updates"],
disallow: ["/admin", "/api", "/download"],
},
sitemap: new URL("/sitemap.xml", SITE_URL).toString(),
Expand Down
1 change: 1 addition & 0 deletions web/app/site-chrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function SiteFooter() {
</p>
<div className="footer-links">
<Link href="/themes">主题</Link>
<Link href="/updates">更新日志</Link>
<Link href="/privacy">隐私说明</Link>
<a href={WIKI_URL}>使用帮助</a>
</div>
Expand Down
5 changes: 5 additions & 0 deletions web/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: "weekly",
priority: 0.9,
},
{
url: new URL("/updates", SITE_URL).toString(),
changeFrequency: "weekly",
priority: 0.8,
},
...THEMES.map((theme) => ({
url: new URL(`/themes/${theme.id}`, SITE_URL).toString(),
changeFrequency: "weekly" as const,
Expand Down
98 changes: 98 additions & 0 deletions web/app/updates/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { Metadata } from "next";
import Image from "next/image";
import { SITE_UPDATES } from "../../lib/site-updates";
import { SiteFooter, SiteHeader } from "../site-chrome";

export const dynamic = "force-static";

export const metadata: Metadata = {
title: "更新日志",
description: "沿着月海的发布线,查看主题、壁纸、助手与网站体验的每一次更新。",
alternates: { canonical: "/updates" },
openGraph: {
title: "月海更新日志",
description: "主题、壁纸、助手与网站体验的持续更新。",
url: "/updates",
},
};

export default function UpdatesPage() {
return (
<div className="themes-shell updates-shell">
<SiteHeader />
<main className="updates-page">
<header className="updates-hero">
<p className="eyebrow">MOONSEA RELEASE CURRENT</p>
<h1>
月海,
<span>持续发生。</span>
</h1>
<p>
从一张新壁纸,到一次更自然的应用体验。
<br />
这里记录月海正在变好的每一步。
</p>
</header>

<ol className="updates-timeline" aria-label="月海更新记录">
{SITE_UPDATES.map((update, updateIndex) => (
<li className="update-entry" id={update.id} key={update.id}>
<div className="update-entry__meta">
<time dateTime={update.date}>{update.displayDate}</time>
<span>{update.kind}</span>
</div>

<div className="update-entry__rail" aria-hidden="true">
<span className="update-entry__node" />
</div>

<article className="update-entry__content">
<div className="update-entry__version">
<span>{update.version}</span>
{update.current ? <em>当前</em> : null}
</div>
<h2>{update.title}</h2>
<p className="update-entry__summary">{update.summary}</p>

{update.images?.length ? (
<div className="update-entry__media">
{update.images.map((image, imageIndex) => (
<Image
alt={image.alt}
height={image.height}
key={image.src}
priority={updateIndex === 0 && imageIndex === 0}
sizes="(max-width: 680px) calc(100vw - 70px), (max-width: 1040px) calc(100vw - 210px), 760px"
src={image.src}
unoptimized
width={image.width}
/>
))}
</div>
) : null}

<ul>
{update.details.map((detail) => (
<li key={detail}>{detail}</li>
))}
</ul>

{update.releaseUrl ? (
<a
className="update-entry__release"
href={update.releaseUrl}
rel="noreferrer"
target="_blank"
>
查看完整发布说明 <span aria-hidden="true">↗</span>
</a>
) : null}
</article>
</li>
))}
</ol>
</main>
<SiteFooter />
</div>
);
}
Loading