Skip to content

Commit 53a2fd2

Browse files
authored
Merge pull request #2833 from esokullu/main
fix apocalypse downloads and readiness showcase
2 parents 167d7cf + eb8b996 commit 53a2fd2

55 files changed

Lines changed: 575 additions & 45 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/chrome/src/agent/emergency-box.js

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -621,15 +621,23 @@ export async function resolveEmergencyResource(resource, fetchImpl = globalThis.
621621
return normalizedRecord(resource, { url });
622622
}
623623

624-
export async function downloadEmergencyResource(resource, options = {}) {
625-
const store = options.store || createEmergencyBoxStore();
626-
const storage = options.storage || createEmergencyBoxStorage();
627-
const fetchImpl = options.fetchImpl || globalThis.fetch;
628-
const signal = options.signal;
629-
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
630-
const resolved = await resolveEmergencyResource(resource, fetchImpl);
624+
export async function withEmergencyResourceLock(resourceId, task, options = {}) {
625+
const lockManager = options.lockManager ?? globalThis.navigator?.locks;
626+
if (typeof lockManager?.request !== 'function') return await task();
627+
const lockName = `webbrain-emergency-pdf:${safeResourceKey(resourceId)}`;
628+
const lockOptions = { mode: 'exclusive' };
629+
if (options.signal) lockOptions.signal = options.signal;
630+
return await lockManager.request(lockName, lockOptions, task);
631+
}
632+
633+
async function downloadResolvedEmergencyResource(resolved, options) {
634+
const { store, storage, fetchImpl, signal, onProgress } = options;
631635
const storageKey = resolved.storageKey || resolved.id;
632636
const existing = await store.get(resolved.id);
637+
if (existing?.status === 'ready' && (existing.storageKey || existing.id) === storageKey) {
638+
onProgress(existing);
639+
return existing;
640+
}
633641
const replacedStorageKey = existing?.storageKey && existing.storageKey !== storageKey
634642
? existing.storageKey
635643
: '';
@@ -720,6 +728,33 @@ export async function downloadEmergencyResource(resource, options = {}) {
720728
}
721729
}
722730

731+
export async function downloadEmergencyResource(resource, options = {}) {
732+
const store = options.store || createEmergencyBoxStore();
733+
const storage = options.storage || createEmergencyBoxStorage();
734+
const fetchImpl = options.fetchImpl || globalThis.fetch;
735+
const signal = options.signal;
736+
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
737+
const resolved = await resolveEmergencyResource(resource, fetchImpl);
738+
try {
739+
return await withEmergencyResourceLock(resolved.id, () => downloadResolvedEmergencyResource(resolved, {
740+
store,
741+
storage,
742+
fetchImpl,
743+
signal,
744+
onProgress,
745+
}), { lockManager: options.lockManager, signal });
746+
} catch (error) {
747+
if (error?.name !== 'AbortError' && !signal?.aborted) throw error;
748+
return await store.get(resolved.id) || normalizedRecord(resolved, {
749+
storageKey: resolved.storageKey || resolved.id,
750+
status: 'paused',
751+
bytesReceived: 0,
752+
error: '',
753+
updatedAt: Date.now(),
754+
});
755+
}
756+
}
757+
723758
export async function deleteEmergencyResource(id, options = {}) {
724759
const store = options.store || createEmergencyBoxStore();
725760
const storage = options.storage || createEmergencyBoxStorage();

src/chrome/src/ui/download-tracker.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ async function runItemAction(item, action) {
384384
if (item.modelKind === 'text' && state) textModelState = state;
385385
} else if (item.kind === 'pdf') {
386386
if (action === 'resume' && !/\/emergency-box\.html$/.test(globalThis.location?.pathname || '')) {
387+
signalEmergencyBox('pause', item);
387388
globalThis.location.href = `${item.href}?resume=${encodeURIComponent(item.sourceId)}`;
388389
return;
389390
}

src/chrome/src/ui/emergency-box.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,15 @@ function resourceById(id) {
300300
}
301301

302302
async function startDownload(resource, options = {}) {
303-
if (!resource || downloads.has(resource.id) || !apocalypseEnabled) return;
303+
if (!resource || !apocalypseEnabled) return;
304+
const activeEntry = downloads.get(resource.id);
305+
if (activeEntry) {
306+
if (options.resume !== true) return;
307+
activeEntry.controller.abort();
308+
if (activeEntry.promise) await activeEntry.promise.catch(() => {});
309+
resource = resourceById(resource.id) || resource;
310+
if (resource.status === 'ready') return resource;
311+
}
304312
if (options.confirm !== false) {
305313
const confirmed = globalThis.confirm(t('eb.confirm_download', {
306314
title: resource.title,
@@ -355,7 +363,7 @@ async function handleDownloadControl(detail = {}) {
355363
const resource = resourceById(id);
356364
if (!id || !resource) return;
357365
if (action === 'pause') downloads.get(id)?.controller.abort();
358-
if (action === 'resume') await startDownload(resource, { confirm: false });
366+
if (action === 'resume') await startDownload(resource, { confirm: false, resume: true });
359367
if (action === 'stop') await stopAndDeleteDownload(id);
360368
}
361369

@@ -442,7 +450,9 @@ elements['resource-list'].addEventListener('click', async event => {
442450
if (!button) return;
443451
const { action, id } = button.dataset;
444452
const resource = resourceById(id);
445-
if (action === 'download') await startDownload(resource);
453+
if (action === 'download') await startDownload(resource, {
454+
resume: ['paused', 'error'].includes(resource?.status),
455+
});
446456
if (action === 'pause') downloads.get(id)?.controller.abort();
447457
if (action === 'read') openReader(resource);
448458
if (action === 'delete' && globalThis.confirm(t('eb.confirm_delete', { title: resource?.title || id }))) {
@@ -481,6 +491,6 @@ refreshState({ recoverInterrupted: true }).then(async () => {
481491
globalThis.history.replaceState({}, '', globalThis.location.pathname);
482492
const resource = resourceById(resumeId);
483493
if (resource && ['paused', 'error'].includes(resource.status)) {
484-
await startDownload(resource, { confirm: false });
494+
await startDownload(resource, { confirm: false, resume: true });
485495
}
486496
}).catch(error => setNotice(error.message, 'error'));

src/chrome/src/ui/emergency-communication.js

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,18 @@ const ESSENTIALS = new Set(['cold', 'come', 'die', 'drink', 'eat', 'fire', 'good
5858
const BODY = new Set(['belly', 'blood', 'bone', 'breast', 'ear', 'eye', 'fat', 'foot', 'hair', 'hand', 'head', 'hear', 'heart', 'knee', 'liver', 'mouth', 'neck', 'nose', 'skin', 'tongue', 'tooth']);
5959
const PEOPLE = new Set(['I', 'man', 'many', 'one', 'person', 'that', 'this', 'two', 'we', 'what', 'who', 'woman', 'you']);
6060

61-
const currentCopy = COPY[getLocale()] || COPY.en;
62-
const languageDisplayNameFormatters = [...new Set(['en', getLocale()])].flatMap(locale => {
63-
try { return [new Intl.DisplayNames([locale], { type: 'language' })]; } catch { return []; }
64-
});
61+
function copyForLocale() {
62+
return COPY[getLocale()] || COPY.en;
63+
}
64+
65+
function createLanguageDisplayNameFormatters() {
66+
return [...new Set(['en', getLocale()])].flatMap(locale => {
67+
try { return [new Intl.DisplayNames([locale], { type: 'language' })]; } catch { return []; }
68+
});
69+
}
70+
71+
let currentCopy = copyForLocale();
72+
let languageDisplayNameFormatters = createLanguageDisplayNameFormatters();
6573
let currentThemeMode = 'system';
6674
loadMode().then(mode => { currentThemeMode = mode; applyMode(mode, { syncStorage: false }); });
6775
watch(() => currentThemeMode);
@@ -128,6 +136,15 @@ function languageAliases(language) {
128136
return [...new Set(aliases.filter(Boolean).map(normalizeSearch))];
129137
}
130138

139+
function rebuildLanguageIndex() {
140+
if (!lexicon) return;
141+
languageLabels = new Map(lexicon.languages.map(language => [languageLabel(language), language]));
142+
languageRows = lexicon.languages.map((language, index) => {
143+
const aliases = languageAliases(language);
144+
return { language, index, aliases, searchText: aliases.join(' ') };
145+
});
146+
}
147+
131148
function languageMatchScore(row, query) {
132149
if (row.aliases.includes(query)) return 0;
133150
if (row.aliases.some(alias => alias.startsWith(query))) return 1;
@@ -341,6 +358,24 @@ elements['word-dialog'].addEventListener('click', event => {
341358
if (event.target === elements['word-dialog']) elements['word-dialog'].close();
342359
});
343360

361+
document.addEventListener('wb-locale-changed', () => {
362+
const selectedLabel = selectedLanguage ? languageLabel(selectedLanguage) : '';
363+
const pickerQuery = languagePickerOpen && elements['language-input'].value !== selectedLabel
364+
? elements['language-input'].value
365+
: '';
366+
currentCopy = copyForLocale();
367+
languageDisplayNameFormatters = createLanguageDisplayNameFormatters();
368+
applyCopy();
369+
if (!lexicon) return;
370+
rebuildLanguageIndex();
371+
elements['concept-count'].textContent = lexicon.conceptCount.toLocaleString(getLocale());
372+
elements['language-count'].textContent = lexicon.languageCount.toLocaleString(getLocale());
373+
elements['variety-count'].textContent = lexicon.varietyCount.toLocaleString(getLocale());
374+
if (languagePickerOpen) renderLanguageOptions(pickerQuery);
375+
else if (selectedLanguage) elements['language-input'].value = languageLabel(selectedLanguage);
376+
render();
377+
});
378+
344379
applyCopy();
345380
try {
346381
const response = await fetch('./data/panlex-swadesh-110.json');
@@ -349,11 +384,7 @@ try {
349384
elements['concept-count'].textContent = lexicon.conceptCount.toLocaleString();
350385
elements['language-count'].textContent = lexicon.languageCount.toLocaleString();
351386
elements['variety-count'].textContent = lexicon.varietyCount.toLocaleString();
352-
languageLabels = new Map(lexicon.languages.map(language => [languageLabel(language), language]));
353-
languageRows = lexicon.languages.map((language, index) => {
354-
const aliases = languageAliases(language);
355-
return { language, index, aliases, searchText: aliases.join(' ') };
356-
});
387+
rebuildLanguageIndex();
357388
const preferredUid = DEFAULT_UID[getLocale()] || 'eng-000';
358389
selectLanguage(lexicon.languages.find(language => language.uid === preferredUid)
359390
|| lexicon.languages.find(language => language.uid === 'eng-000') || lexicon.languages[0], { closePicker: false });

src/firefox/src/agent/emergency-box.js

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -621,15 +621,23 @@ export async function resolveEmergencyResource(resource, fetchImpl = globalThis.
621621
return normalizedRecord(resource, { url });
622622
}
623623

624-
export async function downloadEmergencyResource(resource, options = {}) {
625-
const store = options.store || createEmergencyBoxStore();
626-
const storage = options.storage || createEmergencyBoxStorage();
627-
const fetchImpl = options.fetchImpl || globalThis.fetch;
628-
const signal = options.signal;
629-
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
630-
const resolved = await resolveEmergencyResource(resource, fetchImpl);
624+
export async function withEmergencyResourceLock(resourceId, task, options = {}) {
625+
const lockManager = options.lockManager ?? globalThis.navigator?.locks;
626+
if (typeof lockManager?.request !== 'function') return await task();
627+
const lockName = `webbrain-emergency-pdf:${safeResourceKey(resourceId)}`;
628+
const lockOptions = { mode: 'exclusive' };
629+
if (options.signal) lockOptions.signal = options.signal;
630+
return await lockManager.request(lockName, lockOptions, task);
631+
}
632+
633+
async function downloadResolvedEmergencyResource(resolved, options) {
634+
const { store, storage, fetchImpl, signal, onProgress } = options;
631635
const storageKey = resolved.storageKey || resolved.id;
632636
const existing = await store.get(resolved.id);
637+
if (existing?.status === 'ready' && (existing.storageKey || existing.id) === storageKey) {
638+
onProgress(existing);
639+
return existing;
640+
}
633641
const replacedStorageKey = existing?.storageKey && existing.storageKey !== storageKey
634642
? existing.storageKey
635643
: '';
@@ -720,6 +728,33 @@ export async function downloadEmergencyResource(resource, options = {}) {
720728
}
721729
}
722730

731+
export async function downloadEmergencyResource(resource, options = {}) {
732+
const store = options.store || createEmergencyBoxStore();
733+
const storage = options.storage || createEmergencyBoxStorage();
734+
const fetchImpl = options.fetchImpl || globalThis.fetch;
735+
const signal = options.signal;
736+
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
737+
const resolved = await resolveEmergencyResource(resource, fetchImpl);
738+
try {
739+
return await withEmergencyResourceLock(resolved.id, () => downloadResolvedEmergencyResource(resolved, {
740+
store,
741+
storage,
742+
fetchImpl,
743+
signal,
744+
onProgress,
745+
}), { lockManager: options.lockManager, signal });
746+
} catch (error) {
747+
if (error?.name !== 'AbortError' && !signal?.aborted) throw error;
748+
return await store.get(resolved.id) || normalizedRecord(resolved, {
749+
storageKey: resolved.storageKey || resolved.id,
750+
status: 'paused',
751+
bytesReceived: 0,
752+
error: '',
753+
updatedAt: Date.now(),
754+
});
755+
}
756+
}
757+
723758
export async function deleteEmergencyResource(id, options = {}) {
724759
const store = options.store || createEmergencyBoxStore();
725760
const storage = options.storage || createEmergencyBoxStorage();

src/firefox/src/ui/download-tracker.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ async function runItemAction(item, action) {
384384
if (item.modelKind === 'text' && state) textModelState = state;
385385
} else if (item.kind === 'pdf') {
386386
if (action === 'resume' && !/\/emergency-box\.html$/.test(globalThis.location?.pathname || '')) {
387+
signalEmergencyBox('pause', item);
387388
globalThis.location.href = `${item.href}?resume=${encodeURIComponent(item.sourceId)}`;
388389
return;
389390
}

src/firefox/src/ui/emergency-box.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,15 @@ function resourceById(id) {
300300
}
301301

302302
async function startDownload(resource, options = {}) {
303-
if (!resource || downloads.has(resource.id) || !apocalypseEnabled) return;
303+
if (!resource || !apocalypseEnabled) return;
304+
const activeEntry = downloads.get(resource.id);
305+
if (activeEntry) {
306+
if (options.resume !== true) return;
307+
activeEntry.controller.abort();
308+
if (activeEntry.promise) await activeEntry.promise.catch(() => {});
309+
resource = resourceById(resource.id) || resource;
310+
if (resource.status === 'ready') return resource;
311+
}
304312
if (options.confirm !== false) {
305313
const confirmed = globalThis.confirm(t('eb.confirm_download', {
306314
title: resource.title,
@@ -355,7 +363,7 @@ async function handleDownloadControl(detail = {}) {
355363
const resource = resourceById(id);
356364
if (!id || !resource) return;
357365
if (action === 'pause') downloads.get(id)?.controller.abort();
358-
if (action === 'resume') await startDownload(resource, { confirm: false });
366+
if (action === 'resume') await startDownload(resource, { confirm: false, resume: true });
359367
if (action === 'stop') await stopAndDeleteDownload(id);
360368
}
361369

@@ -442,7 +450,9 @@ elements['resource-list'].addEventListener('click', async event => {
442450
if (!button) return;
443451
const { action, id } = button.dataset;
444452
const resource = resourceById(id);
445-
if (action === 'download') await startDownload(resource);
453+
if (action === 'download') await startDownload(resource, {
454+
resume: ['paused', 'error'].includes(resource?.status),
455+
});
446456
if (action === 'pause') downloads.get(id)?.controller.abort();
447457
if (action === 'read') openReader(resource);
448458
if (action === 'delete' && globalThis.confirm(t('eb.confirm_delete', { title: resource?.title || id }))) {
@@ -481,6 +491,6 @@ refreshState({ recoverInterrupted: true }).then(async () => {
481491
globalThis.history.replaceState({}, '', globalThis.location.pathname);
482492
const resource = resourceById(resumeId);
483493
if (resource && ['paused', 'error'].includes(resource.status)) {
484-
await startDownload(resource, { confirm: false });
494+
await startDownload(resource, { confirm: false, resume: true });
485495
}
486496
}).catch(error => setNotice(error.message, 'error'));

src/firefox/src/ui/emergency-communication.js

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,18 @@ const ESSENTIALS = new Set(['cold', 'come', 'die', 'drink', 'eat', 'fire', 'good
5858
const BODY = new Set(['belly', 'blood', 'bone', 'breast', 'ear', 'eye', 'fat', 'foot', 'hair', 'hand', 'head', 'hear', 'heart', 'knee', 'liver', 'mouth', 'neck', 'nose', 'skin', 'tongue', 'tooth']);
5959
const PEOPLE = new Set(['I', 'man', 'many', 'one', 'person', 'that', 'this', 'two', 'we', 'what', 'who', 'woman', 'you']);
6060

61-
const currentCopy = COPY[getLocale()] || COPY.en;
62-
const languageDisplayNameFormatters = [...new Set(['en', getLocale()])].flatMap(locale => {
63-
try { return [new Intl.DisplayNames([locale], { type: 'language' })]; } catch { return []; }
64-
});
61+
function copyForLocale() {
62+
return COPY[getLocale()] || COPY.en;
63+
}
64+
65+
function createLanguageDisplayNameFormatters() {
66+
return [...new Set(['en', getLocale()])].flatMap(locale => {
67+
try { return [new Intl.DisplayNames([locale], { type: 'language' })]; } catch { return []; }
68+
});
69+
}
70+
71+
let currentCopy = copyForLocale();
72+
let languageDisplayNameFormatters = createLanguageDisplayNameFormatters();
6573
let currentThemeMode = 'system';
6674
loadMode().then(mode => { currentThemeMode = mode; applyMode(mode, { syncStorage: false }); });
6775
watch(() => currentThemeMode);
@@ -128,6 +136,15 @@ function languageAliases(language) {
128136
return [...new Set(aliases.filter(Boolean).map(normalizeSearch))];
129137
}
130138

139+
function rebuildLanguageIndex() {
140+
if (!lexicon) return;
141+
languageLabels = new Map(lexicon.languages.map(language => [languageLabel(language), language]));
142+
languageRows = lexicon.languages.map((language, index) => {
143+
const aliases = languageAliases(language);
144+
return { language, index, aliases, searchText: aliases.join(' ') };
145+
});
146+
}
147+
131148
function languageMatchScore(row, query) {
132149
if (row.aliases.includes(query)) return 0;
133150
if (row.aliases.some(alias => alias.startsWith(query))) return 1;
@@ -341,6 +358,24 @@ elements['word-dialog'].addEventListener('click', event => {
341358
if (event.target === elements['word-dialog']) elements['word-dialog'].close();
342359
});
343360

361+
document.addEventListener('wb-locale-changed', () => {
362+
const selectedLabel = selectedLanguage ? languageLabel(selectedLanguage) : '';
363+
const pickerQuery = languagePickerOpen && elements['language-input'].value !== selectedLabel
364+
? elements['language-input'].value
365+
: '';
366+
currentCopy = copyForLocale();
367+
languageDisplayNameFormatters = createLanguageDisplayNameFormatters();
368+
applyCopy();
369+
if (!lexicon) return;
370+
rebuildLanguageIndex();
371+
elements['concept-count'].textContent = lexicon.conceptCount.toLocaleString(getLocale());
372+
elements['language-count'].textContent = lexicon.languageCount.toLocaleString(getLocale());
373+
elements['variety-count'].textContent = lexicon.varietyCount.toLocaleString(getLocale());
374+
if (languagePickerOpen) renderLanguageOptions(pickerQuery);
375+
else if (selectedLanguage) elements['language-input'].value = languageLabel(selectedLanguage);
376+
render();
377+
});
378+
344379
applyCopy();
345380
try {
346381
const response = await fetch('./data/panlex-swadesh-110.json');
@@ -349,11 +384,7 @@ try {
349384
elements['concept-count'].textContent = lexicon.conceptCount.toLocaleString();
350385
elements['language-count'].textContent = lexicon.languageCount.toLocaleString();
351386
elements['variety-count'].textContent = lexicon.varietyCount.toLocaleString();
352-
languageLabels = new Map(lexicon.languages.map(language => [languageLabel(language), language]));
353-
languageRows = lexicon.languages.map((language, index) => {
354-
const aliases = languageAliases(language);
355-
return { language, index, aliases, searchText: aliases.join(' ') };
356-
});
387+
rebuildLanguageIndex();
357388
const preferredUid = DEFAULT_UID[getLocale()] || 'eng-000';
358389
selectLanguage(lexicon.languages.find(language => language.uid === preferredUid)
359390
|| lexicon.languages.find(language => language.uid === 'eng-000') || lexicon.languages[0], { closePicker: false });

src/firefox/src/ui/locales/ar.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,8 @@ export default {
683683
'st.display.advanced': 'متقدم',
684684
...getApocalypseModeCopy('ar'),
685685
...getEmergencyBoxCopy('ar'),
686+
'st.providers.webgpu_download.stopping': 'جارٍ الإيقاف وحذف الملفات…',
687+
'st.providers.webgpu_download.stop': 'إيقاف وحذف',
686688
'st.display.clarify_timeout.label': 'مهلة التوضيح',
687689
'st.display.clarify_timeout.desc': 'مدة انتظار الرد على سؤال التوضيح قبل اختيار الخيار الأول تلقائيًا (أو انتهاء المهلة إن لم توجد خيارات). 0 = فوري (اختيار تلقائي دائمًا). أعلى من 1200 ثانية = انتظار بلا حدود (إيقاف). الافتراضي 60 ثانية. لا ينطبق على أذونات أو تأكيدات إرسال النماذج.',
688690
'st.display.clarify_timeout.off': 'إيقاف',

0 commit comments

Comments
 (0)