Skip to content

Commit ede917b

Browse files
committed
feat: implement Microsoft download API integration in worker script and update configuration for asset handling
1 parent 01a9e01 commit ede917b

3 files changed

Lines changed: 205 additions & 43 deletions

File tree

src/components/docs/install/WindowsIsoDownload.astro

Lines changed: 79 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ const tabItems = [
183183

184184
<TabPanel value="microsoft-site">
185185
<ol>
186-
<li>Visit <a href="https://www.microsoft.com/en-us/software-download/windows11">Microsoft's Windows 11 download page</a></li>
186+
<li>Visit <a href="https://www.microsoft.com/software-download/windows11">Microsoft's Windows 11 download page</a></li>
187187
<li>Choose <strong>Download Windows 11 Disk Image (ISO) for x64 devices</strong> (or ARM64 for ARM systems)</li>
188188
<li>Select <strong>Windows 11 (multi-edition ISO)</strong><IconArrow /><strong>Confirm</strong></li>
189189
<li>Choose your language<IconArrow /><strong>Confirm</strong></li>
@@ -217,7 +217,6 @@ const tabItems = [
217217
</Tabs>
218218

219219
<style>
220-
/* Step transitions — Jakub recipe: opacity + translateY + blur */
221220
.msdl-widget [data-msdl-step] {
222221
animation: msdl-in 220ms cubic-bezier(0.33, 1, 0.68, 1) both;
223222
}
@@ -239,7 +238,6 @@ const tabItems = [
239238
}
240239
}
241240

242-
/* Custom dropdown panel — origin-aware enter from top */
243241
.msdl-select-panel {
244242
transform-origin: top center;
245243
animation: msdl-dropdown-in 180ms cubic-bezier(0.33, 1, 0.68, 1) both;
@@ -259,7 +257,6 @@ const tabItems = [
259257
}
260258
}
261259

262-
/* Thin scrollbar for option list */
263260
.msdl-options-scroll {
264261
scrollbar-width: thin;
265262
scrollbar-color: rgba(255, 255, 255, 0.08) transparent;
@@ -271,7 +268,6 @@ const tabItems = [
271268
border-radius: 2px;
272269
}
273270

274-
/* Respect reduced motion */
275271
@media (prefers-reduced-motion: reduce) {
276272
.msdl-widget [data-msdl-step],
277273
.msdl-select-panel {
@@ -281,28 +277,24 @@ const tabItems = [
281277
</style>
282278

283279
<script>
284-
// AGPL-3.0-or-later https://spdx.org/licenses/AGPL-3.0-or-later.html
285-
// Integrates with Gravesoft MSDL https://github.com/gravesoft/msdl
286-
287-
const API_URL = 'https://api.gravesoft.dev/msdl/';
288-
289-
// Product IDs sourced from https://github.com/gravesoft/msdl/blob/main/data/products.json
290-
const PRODUCT_IDS: Record<string, number> = {
291-
x64: 3113, // Windows 11 24H2
292-
arm64: 3131, // Windows 11 24H2 ARM64
293-
};
294-
295280
const ARCH_LABELS: Record<string, string> = {
296-
x64: 'Windows 11 24H2 · x64',
297-
arm64: 'Windows 11 24H2 · ARM64',
281+
x64: 'Windows 11 25H2 · x64',
282+
arm64: 'Windows 11 25H2 · ARM64',
298283
};
299284

285+
interface SkuInfo {
286+
Id: string | number;
287+
Language: string;
288+
LocalizedLanguage: string;
289+
FriendlyFileNames?: string[];
290+
}
291+
300292
let panelIdCounter = 0;
301293

302294
function buildSelectPanel(
303295
wrapper: HTMLElement,
304-
skus: Array<{ Id: number; LocalizedLanguage: string }>,
305-
onSelect: (id: number, label: string) => void,
296+
skus: SkuInfo[],
297+
onSelect: (id: string | number, label: string, language: string, friendlyFileNames?: string[]) => void,
306298
) {
307299
const trigger = wrapper.querySelector<HTMLButtonElement>('[data-msdl-select-trigger]')!;
308300
const chevron = wrapper.querySelector<SVGElement>('[data-msdl-select-chevron]')!;
@@ -341,7 +333,7 @@ const tabItems = [
341333
panel = document.createElement('div');
342334
panel.id = panelId;
343335
panel.className = 'msdl-select-panel overflow-hidden rounded-md border border-white/[0.08]';
344-
panel.style.cssText = 'position: fixed; z-index: 9999; background: oklch(0.14 0.038 264);';
336+
panel.style.cssText = 'position: fixed; z-index: 9999; background-color: var(--bg-primary);';
345337
panel.setAttribute('role', 'listbox');
346338
panel.setAttribute('aria-label', 'Select language');
347339

@@ -358,7 +350,7 @@ const tabItems = [
358350
opt.className =
359351
'w-full cursor-pointer px-3 py-2.5 text-left text-sm text-white/65 transition-colors duration-100 hover:bg-white/[0.06] hover:text-white/90 focus:outline-none focus:bg-white/[0.06] focus:text-white/90';
360352

361-
opt.addEventListener('click', () => selectOption(sku.Id, sku.LocalizedLanguage));
353+
opt.addEventListener('click', () => selectOption(sku.Id, sku.LocalizedLanguage, sku.Language, sku.FriendlyFileNames));
362354

363355
opt.addEventListener('keydown', (e) => {
364356
const opts = getOptions();
@@ -387,11 +379,11 @@ const tabItems = [
387379
}, 0);
388380
}
389381

390-
function selectOption(id: number, label: string) {
382+
function selectOption(id: string | number, label: string, language: string, friendlyFileNames?: string[]) {
391383
valueEl.textContent = label;
392384
valueEl.classList.remove('text-white/35');
393385
valueEl.classList.add('text-white/85');
394-
onSelect(id, label);
386+
onSelect(id, label, language, friendlyFileNames);
395387
closePanel();
396388
trigger.focus();
397389
}
@@ -441,7 +433,9 @@ const tabItems = [
441433

442434
function initMsdlWidget(root: HTMLElement) {
443435
let currentArch: string | null = null;
444-
let currentSkuId: number | null = null;
436+
let currentSkuId: string | number | null = null;
437+
let currentLanguage: string | null = null;
438+
let currentFriendlyFileNames: string[] | null = null;
445439
let retryAction: (() => void) | null = null;
446440
let selectControl: ReturnType<typeof buildSelectPanel> | null = null;
447441

@@ -461,25 +455,29 @@ const tabItems = [
461455
async function fetchLanguages(arch: string) {
462456
currentArch = arch;
463457
currentSkuId = null;
458+
currentLanguage = null;
464459
showStep('loading-langs');
465460

466-
const productId = PRODUCT_IDS[arch];
467461
try {
468-
const res = await fetch(`${API_URL}skuinfo?product_id=${productId}`);
462+
const res = await fetch(`/api/ms-iso/skus?arch=${arch}`);
469463
if (!res.ok) throw new Error(`HTTP ${res.status}`);
470464
const data = await res.json();
471-
if (data.Errors?.length) throw new Error(data.Errors[0].Value ?? 'API error');
465+
const skuError =
466+
data.Errors?.[0]?.Value ??
467+
data.ValidationContainer?.Errors?.[0]?.Value ??
468+
data.ValidationContainer?.ErrorList?.[0]?.Value;
469+
if (skuError) throw new Error(skuError);
472470
renderLanguageSelect(data, arch);
473471
} catch {
474472
showError(
475-
'Could not fetch languages. The MSDL service may be temporarily unavailable — try another method below.',
473+
'Could not fetch languages. The download service may be temporarily unavailable — try another method below.',
476474
() => fetchLanguages(arch),
477475
);
478476
}
479477
}
480478

481479
function renderLanguageSelect(
482-
data: { Skus?: Array<{ Id: number; LocalizedLanguage: string }> },
480+
data: { Skus?: SkuInfo[] },
483481
arch: string,
484482
) {
485483
const archLabel = root.querySelector<HTMLElement>('[data-msdl-arch-label]');
@@ -494,23 +492,31 @@ const tabItems = [
494492

495493
selectControl?.reset();
496494

497-
selectControl = buildSelectPanel(wrapper, skus, (id) => {
495+
selectControl = buildSelectPanel(wrapper, skus, (id, _label, language, friendlyFileNames) => {
498496
currentSkuId = id;
497+
currentLanguage = language;
498+
currentFriendlyFileNames = friendlyFileNames ?? null;
499499
getDownloadBtn.disabled = false;
500500
});
501501

502502
currentSkuId = null;
503+
currentLanguage = null;
504+
currentFriendlyFileNames = null;
503505
getDownloadBtn.disabled = true;
504506
showStep('select-lang');
505507
}
506508

507509
async function fetchDownload() {
508-
if (!currentArch || currentSkuId === null) return;
510+
if (!currentArch || currentSkuId === null || !currentLanguage) return;
509511
showStep('loading-download');
510512

511-
const productId = PRODUCT_IDS[currentArch];
513+
const params = new URLSearchParams({
514+
arch: currentArch,
515+
skuId: String(currentSkuId),
516+
language: currentLanguage,
517+
});
512518
try {
513-
const res = await fetch(`${API_URL}proxy?product_id=${productId}&sku_id=${currentSkuId}`);
519+
const res = await fetch(`/api/ms-iso/links?${params}`);
514520
if (!res.ok) throw new Error(`HTTP ${res.status}`);
515521
const data = await res.json();
516522
renderDownloads(data);
@@ -528,21 +534,46 @@ const tabItems = [
528534
LocalizedLanguage: string;
529535
}
530536

531-
function renderDownloads(data: { ProductDownloadOptions?: DownloadOption[] }) {
537+
interface ProxyError {
538+
Key?: string;
539+
Value?: string;
540+
Type?: number;
541+
}
542+
543+
function getProxyErrorMessage(errors: ProxyError[]): string {
544+
const first = errors[0];
545+
if (!first?.Value) return 'The download service is temporarily unavailable. Please try again in a moment or use the Microsoft website tab.';
546+
if (first.Key === 'ErrorSettings.SentinelReject') {
547+
return 'The download service is temporarily unavailable. Please try again in a moment or use the Microsoft website tab.';
548+
}
549+
if (first.Key === 'ErrorSettings.ProductKeyValidationError') {
550+
return 'This language could not be retrieved from the download service. Try a different language or use the Microsoft website tab.';
551+
}
552+
return first.Value;
553+
}
554+
555+
function renderDownloads(data: { ProductDownloadOptions?: DownloadOption[]; Errors?: ProxyError[] }) {
532556
const linksContainer = root.querySelector<HTMLElement>('[data-msdl-download-links]');
533557
if (!linksContainer) return;
534558
linksContainer.innerHTML = '';
535559

560+
if (data.Errors?.length) {
561+
showError(getProxyErrorMessage(data.Errors), fetchDownload);
562+
return;
563+
}
564+
536565
const options = data.ProductDownloadOptions;
537566
if (!options || options.length === 0) {
538567
showError('No download options were returned. Try a different language or use another method.');
539568
return;
540569
}
541570

542-
options.forEach((option) => {
571+
const preferredFilename = currentFriendlyFileNames?.[0];
572+
let didAutoDownload = false;
573+
options.forEach((option, index) => {
543574
const uriBase = option.Uri.split('?')[0] ?? option.Uri;
544575
const parts = uriBase.split('/');
545-
const filename = parts[parts.length - 1] ?? 'Windows.iso';
576+
const filename = preferredFilename ?? (parts[parts.length - 1] ?? 'Windows.iso');
546577

547578
const link = document.createElement('a');
548579
link.href = option.Uri;
@@ -553,12 +584,15 @@ const tabItems = [
553584
link.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/><path d="M7 11l5 5 5-5"/><path d="M12 4v12"/></svg><span class="truncate font-medium">${filename}</span>`;
554585
linksContainer.appendChild(link);
555586

556-
const autoLink = document.createElement('a');
557-
autoLink.href = option.Uri;
558-
autoLink.download = filename;
559-
document.body.appendChild(autoLink);
560-
autoLink.click();
561-
document.body.removeChild(autoLink);
587+
if (!didAutoDownload && index === 0) {
588+
const autoLink = document.createElement('a');
589+
autoLink.href = option.Uri;
590+
autoLink.download = filename;
591+
document.body.appendChild(autoLink);
592+
autoLink.click();
593+
document.body.removeChild(autoLink);
594+
didAutoDownload = true;
595+
}
562596
});
563597

564598
showStep('done');
@@ -569,6 +603,8 @@ const tabItems = [
569603
selectControl = null;
570604
currentArch = null;
571605
currentSkuId = null;
606+
currentLanguage = null;
607+
currentFriendlyFileNames = null;
572608
retryAction = null;
573609
showStep('idle');
574610
}

0 commit comments

Comments
 (0)