Skip to content

Commit 1366a17

Browse files
authored
feat: en translation, add history download page, add download button on navbar (#6)
* feat: add latest version download, add all version download modal * fix: retrieve download modal * fix: delete elements relevant to modal * feat,fix: /en translation, add history download page, add download button on navbar * fix: minor translation fixes * fix: minor translation fix
1 parent 2c8fb36 commit 1366a17

File tree

22 files changed

+1127
-53
lines changed

22 files changed

+1127
-53
lines changed

app/[lang]/layout.jsx

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,61 @@
1-
import { Footer, Layout, Navbar, LastUpdated } from 'nextra-theme-docs'
2-
import { TitleFullWithLogo } from '../components/logo-title'
3-
import { Head, Search } from 'nextra/components'
4-
import { getPageMap } from 'nextra/page-map'
5-
import { localeResources } from '../../locales'
6-
import 'nextra-theme-docs/style.css'
7-
1+
import { Footer, Layout, Navbar, LastUpdated } from "nextra-theme-docs";
2+
import { Button as ChakraButton } from "@chakra-ui/react";
3+
import { TitleFullWithLogo } from "../components/logo-title";
4+
import { Head, Search } from "nextra/components";
5+
import { getPageMap } from "nextra/page-map";
6+
import { localeResources } from "../../locales";
7+
import "nextra-theme-docs/style.css";
8+
89
export const metadata = {
9-
applicationName: 'SJMCL',
10+
applicationName: "SJMCL",
1011
appleWebApp: {
11-
title: 'SJMCL Docs'
12+
title: "SJMCL Docs",
1213
},
13-
metadataBase: new URL('https://mc.sjtu.cn/sjmcl'),
14+
metadataBase: new URL("https://mc.sjtu.cn/sjmcl"),
1415
other: {
15-
'msapplication-TileColor': '#fff'
16+
"msapplication-TileColor": "#fff",
1617
},
1718
title: {
18-
default: 'SJMC Launcher',
19-
template: '%s | SJMCL'
19+
default: "SJMC Launcher",
20+
template: "%s | SJMCL",
2021
},
21-
}
22-
22+
};
23+
2324
export default async function RootLayout({ children, params }) {
24-
const { lang } = await params
25-
let pageMap = await getPageMap(`/${lang}`)
26-
const t = localeResources[lang || 'en'].translation
25+
const { lang } = await params;
26+
let pageMap = await getPageMap(`/${lang}`);
27+
const t = localeResources[lang || "en"].translation;
2728

2829
const navbar = (
2930
<Navbar
3031
logo={<TitleFullWithLogo />}
3132
projectLink="https://github.com/UNIkeEN/SJMCL"
32-
>
33-
</Navbar>
34-
)
33+
/>
34+
);
3535

3636
const search = (
37-
<Search
38-
placeholder={t.search.placeholder}
39-
/>
40-
)
41-
37+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
38+
<ChakraButton
39+
as="a"
40+
href={`/sjmcl/${lang}/download/latest`}
41+
colorScheme="blue"
42+
size="sm"
43+
_hover={{ textDecoration: "none" }}
44+
variant="outline"
45+
>
46+
{lang === "zh" ? "下载" : "download"}
47+
</ChakraButton>
48+
<Search placeholder={t.search.placeholder} />
49+
</div>
50+
);
51+
4252
const footer = (
4353
<Footer>
4454
沪 ICP 备 05052060 号-7
45-
<br/>
55+
<br />
4656
{new Date().getFullYear()} © {t.footer.copyright}
4757
</Footer>
48-
)
58+
);
4959

5060
return (
5161
<html
@@ -70,15 +80,16 @@ export default async function RootLayout({ children, params }) {
7080
docsRepositoryBase="https://github.com/UNIkeEN/SJMCL/tree/main"
7181
footer={footer}
7282
editLink={t.editLink}
73-
feedback={{content: t.feedbackLink}}
83+
feedback={{ content: t.feedbackLink }}
7484
lastUpdated={<LastUpdated>{t.lastUpdated}</LastUpdated>}
7585
i18n={Object.entries(localeResources).map(([locale, value]) => ({
76-
locale, name: value.display_name,
86+
locale,
87+
name: value.display_name,
7788
}))}
7889
>
7990
{children}
8091
</Layout>
8192
</body>
8293
</html>
83-
)
84-
}
94+
);
95+
}

app/styles/global.css

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
.secondary-text {
2-
color: var(--chakra-colors-chakra-placeholder-color);
3-
}
2+
color: var(--chakra-colors-chakra-placeholder-color);
3+
}
4+
5+
/* Also target .nextra-scrollbar that is two levels under a <nav> element
6+
(nav > * > .nextra-scrollbar) and hide its last child to cover the
7+
specific DOM structure used by the theme. */
8+
nav > .nextra-scrollbar > :last-child {
9+
display: none !important;
10+
}

content/en/_meta.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { theme } from "@chakra-ui/react";
2+
import { Navbar } from "nextra-theme-docs";
3+
14
export default {
25
index: {
36
type: 'page',
@@ -39,5 +42,9 @@ export default {
3942
timestamp: false,
4043
toc: false
4144
}
45+
},
46+
download: {
47+
title: 'Download',
48+
type: 'page',
4249
}
4350
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import {
5+
Box,
6+
Heading,
7+
Text,
8+
Spinner,
9+
HStack,
10+
VStack,
11+
Table,
12+
Thead,
13+
Tbody,
14+
Tr,
15+
Th,
16+
Td,
17+
Badge,
18+
Button,
19+
Link as CLink,
20+
} from "@chakra-ui/react";
21+
import { LuArrowDownToLine } from "react-icons/lu";
22+
23+
const API = "https://api.github.com/repos/UnikeEN/SJMCL/releases";
24+
const MC_BASE = "https://mc.sjtu.cn/sjmcl/releases/";
25+
26+
function inferMeta(name) {
27+
const n = name.toLowerCase();
28+
let os = "other";
29+
if (/win|windows/.test(n)) os = "Windows";
30+
else if (/mac|macos|darwin/.test(n)) os = "macOS";
31+
else if (/linux/.test(n)) os = "Linux";
32+
33+
let arch = "-";
34+
if (/aarch64|arm64/.test(n)) arch = "aarch64";
35+
else if (/x86_64|x64/.test(n)) arch = "x86_64";
36+
else if (/(i386|i686|x86)(?!_64)/.test(n)) arch = "i686";
37+
38+
let type = "file";
39+
if (/portable/i.test(n)) type = "portable";
40+
else if (/\.dmg$/i.test(n)) type = "dmg";
41+
else if (/\.msi$/i.test(n)) type = "msi";
42+
else if (/\.exe$/i.test(n)) type = "exe";
43+
else if (/\.appimage$/i.test(n)) type = "AppImage";
44+
else if (/\.deb$/i.test(n)) type = "deb";
45+
else if (/\.rpm$/i.test(n)) type = "rpm";
46+
else if (/\.tar\.gz$/i.test(n)) type = "tar.gz";
47+
else if (/\.zip$/i.test(n)) type = "zip";
48+
else if (/\.app\.tar\.gz$/i.test(n)) type = "app.tar.gz";
49+
50+
return { os, arch, type };
51+
}
52+
53+
function humanSize(bytes) {
54+
if (typeof bytes !== "number") return "-";
55+
const units = ["B", "KB", "MB", "GB"];
56+
let i = 0,
57+
s = bytes;
58+
while (s >= 1024 && i < units.length - 1) {
59+
s /= 1024;
60+
i++;
61+
}
62+
const v =
63+
s < 10 && i > 0 ? s.toFixed(2) : s < 100 ? s.toFixed(1) : Math.round(s);
64+
return `${v} ${units[i]}`;
65+
}
66+
67+
function shouldHighlightRow({ os, arch, name }) {
68+
const n = (name || "").toLowerCase();
69+
const isWinPortableExe =
70+
os === "Windows" &&
71+
arch === "x86_64" &&
72+
/portable/i.test(n) &&
73+
/\.exe$/i.test(n);
74+
75+
const isMacArmDmg = os === "macOS" && arch === "aarch64" && /\.dmg$/i.test(n);
76+
77+
return isWinPortableExe || isMacArmDmg;
78+
}
79+
80+
export default function History() {
81+
const [state, setState] = useState({
82+
loading: true,
83+
error: "",
84+
releases: [],
85+
});
86+
87+
useEffect(() => {
88+
let alive = true;
89+
(async () => {
90+
try {
91+
const res = await fetch(API, { cache: "no-store" });
92+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
93+
const json = await res.json();
94+
if (alive) setState({ loading: false, error: "", releases: json });
95+
} catch (e) {
96+
if (alive)
97+
setState({
98+
loading: false,
99+
error: "Failed to fetch release history",
100+
releases: [],
101+
});
102+
}
103+
})();
104+
return () => {
105+
alive = false;
106+
};
107+
}, []);
108+
109+
if (state.loading)
110+
return (
111+
<HStack>
112+
<Spinner />
113+
<Text>Fetching release history…</Text>
114+
</HStack>
115+
);
116+
if (state.error) return <Text>{state.error}</Text>;
117+
118+
const rows = state.releases
119+
.flatMap((release) => {
120+
const tag = release.tag_name || "";
121+
const files = (release.assets || []).map((a) => ({
122+
name: a.name,
123+
size: a.size,
124+
url: a.browser_download_url,
125+
}));
126+
return files.map((f) => {
127+
const name = f.name || "";
128+
const size = f.size;
129+
const { os, arch, type } = inferMeta(name);
130+
const url = f.url || MC_BASE + encodeURIComponent(name);
131+
return {
132+
tag,
133+
name,
134+
size,
135+
os,
136+
arch,
137+
type,
138+
url,
139+
published_at: release.published_at,
140+
prerelease: release.prerelease,
141+
};
142+
});
143+
})
144+
.sort((a, b) => {
145+
if (a.tag !== b.tag)
146+
return a.tag.localeCompare(b.tag, undefined, {
147+
numeric: true,
148+
sensitivity: "base",
149+
});
150+
const osOrder = { Windows: 1, macOS: 2, Linux: 3, other: 99 };
151+
const archOrder = {
152+
portable: 1,
153+
x86_64: 2,
154+
aarch64: 3,
155+
i686: 4,
156+
"-": 99,
157+
};
158+
const osDiff = (osOrder[a.os] || 99) - (osOrder[b.os] || 99);
159+
if (osDiff !== 0) return osDiff;
160+
const archDiff = (archOrder[a.arch] || 99) - (archOrder[b.arch] || 99);
161+
if (archDiff !== 0) return archDiff;
162+
return a.name.localeCompare(b.name);
163+
});
164+
165+
const groups = [];
166+
// group by tag (version)
167+
const tagMap = state.releases.reduce((m, r) => {
168+
(m[r.tag_name] ||= []).push(r);
169+
return m;
170+
}, {});
171+
172+
const orderedTags = Object.keys(tagMap);
173+
174+
return (
175+
<VStack align="stretch" spacing={4}>
176+
{rows.length === 0 ? (
177+
<Box>
178+
<Text>No historical release files available.</Text>
179+
</Box>
180+
) : (
181+
<Table size="sm" variant="simple">
182+
<Thead>
183+
<Tr>
184+
<Th>版本</Th>
185+
<Th>平台</Th>
186+
<Th>架构</Th>
187+
<Th>文件</Th>
188+
<Th>大小</Th>
189+
<Th>Download</Th>
190+
</Tr>
191+
</Thead>
192+
<Tbody>
193+
{rows.map((r) => (
194+
<Tr key={`${r.tag}-${r.name}`}>
195+
<Td>{r.tag}</Td>
196+
<Td>{r.os}</Td>
197+
<Td>{r.arch}</Td>
198+
<Td>
199+
<CLink href={r.url} isExternal>
200+
{r.name}
201+
</CLink>
202+
</Td>
203+
<Td>{humanSize(r.size)}</Td>
204+
<Td>
205+
<Button
206+
as="a"
207+
href={r.url}
208+
size="sm"
209+
leftIcon={<LuArrowDownToLine />}
210+
>
211+
Download
212+
</Button>
213+
</Td>
214+
</Tr>
215+
))}
216+
</Tbody>
217+
</Table>
218+
)}
219+
</VStack>
220+
);
221+
}

0 commit comments

Comments
 (0)