@@ -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