Compare commits

...

2 commits

Author SHA1 Message Date
flash a08793d992 Don't wait for EEPROM and sounds to finish loading during join. 2024-04-17 19:16:27 +00:00
flash 297f5caf82 Some cleanup. 2024-04-17 18:08:12 +00:00
5 changed files with 216 additions and 194 deletions

View file

@ -50,10 +50,10 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
try { try {
window.futami = await FutamiCommon.load(); window.futami = await FutamiCommon.load();
} catch(ex) { } catch(ex) {
console.error(ex); console.error('Failed to load common settings.', ex);
loadingOverlay.setIcon('cross'); loadingOverlay.setIcon('cross');
loadingOverlay.setHeader('Failed!'); loadingOverlay.setHeader('Failed!');
loadingOverlay.setMessage('Failed to load common settings!'); loadingOverlay.setMessage('Failed to load common settings.');
return; return;
} }
@ -97,7 +97,7 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
settings.define('eepromAutoInsert').default(true).create(); settings.define('eepromAutoInsert').default(true).create();
settings.define('autoEmbedV1').default(false).create(); settings.define('autoEmbedV1').default(false).create();
settings.define('soundEnable').default(true).critical().create(); settings.define('soundEnable').default(true).critical().create();
settings.define('soundPack').default('ajax-chat').create(); settings.define('soundPack').default('').create();
settings.define('soundVolume').default(80).create(); settings.define('soundVolume').default(80).create();
settings.define('soundEnableJoin').default(true).create(); settings.define('soundEnableJoin').default(true).create();
settings.define('soundEnableLeave').default(true).create(); settings.define('soundEnableLeave').default(true).create();
@ -136,20 +136,25 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
const soundCtx = new MamiSoundContext; const soundCtx = new MamiSoundContext;
ctx.sound = soundCtx; ctx.sound = soundCtx;
try { futami.getJson('sounds2')
const sounds = await futami.getJson('sounds2'); .catch(ex => { console.error('Failed to load sound library and packs.', ex); })
if(Array.isArray(sounds.library)) .then(sounds => {
soundCtx.library.register(sounds.library, true); if(Array.isArray(sounds.library))
if(Array.isArray(sounds.packs)) soundCtx.library.register(sounds.library, true);
soundCtx.packs.register(sounds.packs, true);
} catch(ex) {
console.error(ex);
}
if(!await MamiDetectAutoPlay()) { if(Array.isArray(sounds.packs)) {
settings.set('soundEnable', false); soundCtx.packs.register(sounds.packs, true);
settings.virtualise('soundEnable'); settings.touch('soundPack', true);
} }
});
MamiDetectAutoPlay()
.then(canAutoPlay => {
if(canAutoPlay) return;
settings.set('soundEnable', false);
settings.virtualise('soundEnable');
});
settings.watch('soundEnable', ev => { settings.watch('soundEnable', ev => {
if(ev.detail.value) { if(ev.detail.value) {
@ -157,9 +162,11 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
soundCtx.reset(); soundCtx.reset();
settings.touch('soundVolume'); settings.touch('soundVolume');
settings.touch('soundPack'); settings.touch('soundPack', true);
soundCtx.library.play(soundCtx.pack.getEventSound('server')); // do we need to do this?
if(!ev.detail.initial && !ev.detail.silent && ev.detail.local)
soundCtx.library.play(soundCtx.pack.getEventSound('server'));
} }
soundCtx.muted = !ev.detail.value; soundCtx.muted = !ev.detail.value;
@ -167,13 +174,21 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
settings.watch('soundPack', ev => { settings.watch('soundPack', ev => {
const packs = soundCtx.packs; const packs = soundCtx.packs;
if(!packs.has(ev.detail.value)) { let packName = ev.detail.value;
settings.delete(ev.detail.name);
return;
}
soundCtx.pack = packs.get(ev.detail.value); if(packName === '') {
if(!ev.detail.initial) soundCtx.library.play(soundCtx.pack.getEventSound('server')); const names = packs.names();
if(names.length < 1)
return;
packName = names[0];
} else if(!packs.has(packName))
return;
soundCtx.pack = packs.get(packName);
if(!ev.detail.initial && !ev.detail.silent && ev.detail.local)
soundCtx.library.play(soundCtx.pack.getEventSound('server'));
}); });
settings.watch('soundVolume', ev => { settings.watch('soundVolume', ev => {
@ -181,12 +196,18 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
}) })
// loading these asynchronously makes them not show up in the backlog
// revisit when emote reparsing is implemented
loadingOverlay.setMessage('Loading emoticons...'); loadingOverlay.setMessage('Loading emoticons...');
try { try {
const emotes = await futami.getJson('emotes'); const emotes = await futami.getJson('emotes');
MamiEmotes.loadLegacy(emotes); MamiEmotes.loadLegacy(emotes);
} catch(ex) { } catch(ex) {
console.error(ex); console.error('Failed to load emoticons.', ex);
} finally {
// this is currently called in the sock chat handlers
// does a permissions check which it can't do at this point
//Umi.UI.Emoticons.Init();
} }
@ -206,16 +227,6 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
}); });
loadingOverlay.setMessage('Loading EEPROM...');
try {
await MamiEEPROM.init();
ctx.eeprom = new EEPROM('1', futami.get('eeprom2'), MamiMisuzuAuth.getLine);
} catch(ex) {
console.error(ex);
ctx.eeprom = undefined;
}
loadingOverlay.setMessage('Preparing UI...'); loadingOverlay.setMessage('Preparing UI...');
ctx.textTriggers = new MamiTextTriggers; ctx.textTriggers = new MamiTextTriggers;
@ -303,22 +314,15 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
'click': function() { 'click': function() {
const sidebar = $c('sidebar')[0]; const sidebar = $c('sidebar')[0];
const toggle = Umi.UI.Toggles.Get('menu-toggle'); const toggle = Umi.UI.Toggles.Get('menu-toggle');
const toggleOpened = 'sidebar__selector-mode--menu-toggle-opened'; const isClosed = toggle.classList.contains('sidebar__selector-mode--menu-toggle-closed');
const toggleClosed = 'sidebar__selector-mode--menu-toggle-closed';
const isClosed = toggle.classList.contains(toggleClosed);
if(sidebarAnimation !== null) { if(sidebarAnimation !== null) {
sidebarAnimation.cancel(); sidebarAnimation.cancel();
sidebarAnimation = null; sidebarAnimation = null;
} }
if(isClosed) { toggle.classList.toggle('sidebar__selector-mode--menu-toggle-opened', isClosed);
toggle.classList.add(toggleOpened); toggle.classList.toggle('sidebar__selector-mode--menu-toggle-closed', !isClosed);
toggle.classList.remove(toggleClosed);
} else {
toggle.classList.add(toggleClosed);
toggle.classList.remove(toggleOpened);
}
let update; let update;
if(isClosed) if(isClosed)
@ -414,115 +418,116 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
}, 'Ready~'); }, 'Ready~');
pingToggle.appendChild(pingIndicator.getElement()); pingToggle.appendChild(pingIndicator.getElement());
if(ctx.eeprom !== undefined) {
Umi.UI.Menus.Add('uploads', 'Upload History', !FUTAMI_DEBUG);
const doUpload = async file => {
const uploadEntry = Umi.UI.Uploads.create(file.name);
const uploadTask = ctx.eeprom.create(file);
uploadTask.onProgress(prog => uploadEntry.setProgress(prog.progress));
uploadEntry.addOption('Cancel', () => uploadTask.abort());
try {
const fileInfo = await uploadTask.start();
uploadEntry.hideOptions();
uploadEntry.clearOptions();
uploadEntry.removeProgress();
uploadEntry.addOption('Open', fileInfo.url);
uploadEntry.addOption('Insert', () => Umi.UI.Markup.InsertRaw(insertText, ''));
uploadEntry.addOption('Delete', () => {
ctx.eeprom.delete(fileInfo)
.then(() => uploadEntry.remove())
.catch(ex => {
console.error(ex);
alert(ex);
});
});
let insertText;
if(fileInfo.isImage()) {
insertText = `[img]${fileInfo.url}[/img]`;
uploadEntry.setThumbnail(fileInfo.thumb);
} else if(fileInfo.isAudio()) {
insertText = `[audio]${fileInfo.url}[/audio]`;
uploadEntry.setThumbnail(fileInfo.thumb);
} else if(fileInfo.isVideo()) {
insertText = `[video]${fileInfo.url}[/video]`;
uploadEntry.setThumbnail(fileInfo.thumb);
} else
insertText = location.protocol + fileInfo.url;
if(settings.get('eepromAutoInsert'))
Umi.UI.Markup.InsertRaw(insertText, '');
} catch(ex) {
if(!ex.aborted) {
console.error(ex);
alert(ex);
}
uploadEntry.remove();
}
};
const uploadForm = $e({
tag: 'input',
attrs: {
type: 'file',
multiple: true,
style: { display: 'none' },
onchange: ev => {
for(const file of ev.target.files)
doUpload(file);
},
},
});
document.body.appendChild(uploadForm);
Umi.UI.InputMenus.AddButton('upload', 'Upload', () => uploadForm.click());
$i('umi-msg-text').onpaste = ev => {
if(ev.clipboardData && ev.clipboardData.files.length > 0)
for(const file of ev.clipboardData.files)
doUpload(file);
};
document.body.ondragenter = ev => {
ev.preventDefault();
ev.stopPropagation();
};
document.body.ondragover = ev => {
ev.preventDefault();
ev.stopPropagation();
};
document.body.ondragleave = ev => {
ev.preventDefault();
ev.stopPropagation();
};
document.body.ondrop = ev => {
ev.preventDefault();
ev.stopPropagation();
if(ev.dataTransfer && ev.dataTransfer.files.length > 0)
for(const file of ev.dataTransfer.files) {
if(file.name.slice(-5) === '.mami'
&& confirm('This file appears to be a settings export. Do you want to import it? This will overwrite your existing settings!')) {
(new MamiSettingsBackup(settings)).importFile(file);
return;
}
doUpload(file);
}
};
}
Umi.UI.InputMenus.Add('markup', 'BB Code'); Umi.UI.InputMenus.Add('markup', 'BB Code');
Umi.UI.InputMenus.Add('emotes', 'Emoticons'); Umi.UI.InputMenus.Add('emotes', 'Emoticons');
let doUpload;
MamiEEPROM.init()
.catch(ex => {
console.log('Failed to initialise EEPROM.', ex);
ctx.eeprom = undefined;
})
.then(() => {
ctx.eeprom = new EEPROM('1', futami.get('eeprom2'), MamiMisuzuAuth.getLine);
Umi.UI.Menus.Add('uploads', 'Upload History', !FUTAMI_DEBUG);
doUpload = async file => {
const uploadEntry = Umi.UI.Uploads.create(file.name);
const uploadTask = ctx.eeprom.create(file);
uploadTask.onProgress(prog => uploadEntry.setProgress(prog.progress));
uploadEntry.addOption('Cancel', () => uploadTask.abort());
try {
const fileInfo = await uploadTask.start();
uploadEntry.hideOptions();
uploadEntry.clearOptions();
uploadEntry.removeProgress();
uploadEntry.addOption('Open', fileInfo.url);
uploadEntry.addOption('Insert', () => Umi.UI.Markup.InsertRaw(insertText, ''));
uploadEntry.addOption('Delete', () => {
ctx.eeprom.delete(fileInfo)
.then(() => uploadEntry.remove())
.catch(ex => {
console.error(ex);
alert(ex);
});
});
let insertText;
if(fileInfo.isImage()) {
insertText = `[img]${fileInfo.url}[/img]`;
uploadEntry.setThumbnail(fileInfo.thumb);
} else if(fileInfo.isAudio()) {
insertText = `[audio]${fileInfo.url}[/audio]`;
uploadEntry.setThumbnail(fileInfo.thumb);
} else if(fileInfo.isVideo()) {
insertText = `[video]${fileInfo.url}[/video]`;
uploadEntry.setThumbnail(fileInfo.thumb);
} else
insertText = location.protocol + fileInfo.url;
if(settings.get('eepromAutoInsert'))
Umi.UI.Markup.InsertRaw(insertText, '');
} catch(ex) {
if(!ex.aborted) {
console.error(ex);
alert(ex);
}
uploadEntry.remove();
}
};
const uploadForm = $e({
tag: 'input',
attrs: {
type: 'file',
multiple: true,
style: { display: 'none' },
onchange: ev => {
for(const file of ev.target.files)
doUpload(file);
},
},
});
document.body.appendChild(uploadForm);
Umi.UI.InputMenus.AddButton('upload', 'Upload', () => uploadForm.click(), 'markup');
$i('umi-msg-text').onpaste = ev => {
if(ev.clipboardData && ev.clipboardData.files.length > 0)
for(const file of ev.clipboardData.files)
doUpload(file);
};
});
// figure out how to display a UI for this someday
//document.body.addEventListener('dragenter', ev => { console.info('dragenter', ev); });
//document.body.addEventListener('dragleave', ev => { console.info('dragleave', ev); });
document.body.addEventListener('dragover', ev => { ev.preventDefault(); });
document.body.addEventListener('drop', ev => {
if(ev.dataTransfer === undefined || ev.dataTransfer === null || ev.dataTransfer.files.length < 1)
return;
ev.preventDefault();
for(const file of ev.dataTransfer.files) {
if(file.name.slice(-5) === '.mami'
&& confirm('This file appears to be a settings export. Do you want to import it? This will overwrite your existing settings!')) {
(new MamiSettingsBackup(settings)).importFile(file);
return;
}
if(doUpload !== undefined)
doUpload(file);
}
});
window.addEventListener('beforeunload', function(ev) { window.addEventListener('beforeunload', function(ev) {
if(settings.get('closeTabConfirm')) { if(settings.get('closeTabConfirm')) {
ev.preventDefault(); ev.preventDefault();
@ -533,7 +538,6 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
}); });
// really not sure about all the watchers for the protocol just kinda being Listed here but we'll see i guess
loadingOverlay.setMessage('Connecting...'); loadingOverlay.setMessage('Connecting...');
const setLoadingOverlay = async (icon, header, message, optional) => { const setLoadingOverlay = async (icon, header, message, optional) => {

View file

@ -24,12 +24,12 @@ const MamiSettingsScoped = function(settings, prefix) {
}, },
has: name => settings.has(prefix + name), has: name => settings.has(prefix + name),
get: name => settings.get(prefix + name), get: name => settings.get(prefix + name),
set: (name, value) => settings.set(prefix + name, value), set: (name, value, silent) => settings.set(prefix + name, value, silent),
delete: name => settings.delete(prefix + name), delete: (name, silent) => settings.delete(prefix + name, silent),
toggle: name => settings.toggle(prefix + name), toggle: (name, silent) => settings.toggle(prefix + name, silent),
touch: name => settings.touch(prefix + name), touch: (name, silent) => settings.touch(prefix + name, silent),
clear: (criticalOnly, pfx) => settings.clear(criticalOnly, prefix + pfx), clear: (criticalOnly, pfx) => settings.clear(criticalOnly, prefix + pfx),
watch: (name, handler) => settings.watch(prefix + name, handler), watch: (name, handler, silent) => settings.watch(prefix + name, handler, silent),
unwatch: (name, handler) => settings.unwatch(prefix + name, handler), unwatch: (name, handler) => settings.unwatch(prefix + name, handler),
virtualise: name => settings.virtualise(prefix + name), virtualise: name => settings.virtualise(prefix + name),
scope: name => settings.scope(prefix + name), scope: name => settings.scope(prefix + name),

View file

@ -16,16 +16,18 @@ const MamiSettings = function(storageOrPrefix, eventTarget) {
const storage = new MamiSettingsVirtualStorage(storageOrPrefix); const storage = new MamiSettingsVirtualStorage(storageOrPrefix);
const settings = new Map; const settings = new Map;
const createUpdateEvent = (name, value, initial) => eventTarget.create(name, { const createUpdateEvent = (name, value, initial, silent, local) => eventTarget.create(name, {
name: name, name: name,
value: value, value: value,
initial: !!initial, initial: !!initial,
silent: !!silent,
local: !!local,
}); });
const dispatchUpdate = (name, value) => eventTarget.dispatch(createUpdateEvent(name, value)); const dispatchUpdate = (name, value, silent, local) => eventTarget.dispatch(createUpdateEvent(name, value, false, silent, local));
const broadcast = new BroadcastChannel(`${MAMI_MAIN_JS}:settings:${storage.name()}`); const broadcast = new BroadcastChannel(`${MAMI_MAIN_JS}:settings:${storage.name()}`);
const broadcastUpdate = (name, value) => { const broadcastUpdate = (name, value, silent) => {
setTimeout(() => broadcast.postMessage({ act: 'update', name: name, value: value }), 0); setTimeout(() => broadcast.postMessage({ act: 'update', name: name, value: value, silent: !!silent }), 0);
}; };
const getSetting = name => { const getSetting = name => {
@ -43,16 +45,16 @@ const MamiSettings = function(storageOrPrefix, eventTarget) {
return value === null ? setting.fallback : value; return value === null ? setting.fallback : value;
}; };
const deleteValue = setting => { const deleteValue = (setting, silent) => {
if(setting.immutable) if(setting.immutable)
return; return;
storage.delete(setting.name); storage.delete(setting.name);
dispatchUpdate(setting.name, setting.fallback); dispatchUpdate(setting.name, setting.fallback, silent, true);
broadcastUpdate(setting.name, setting.fallback); broadcastUpdate(setting.name, setting.fallback, silent);
}; };
const setValue = (setting, value) => { const setValue = (setting, value, silent) => {
if(value !== null) { if(value !== null) {
if(value === undefined) if(value === undefined)
value = null; value = null;
@ -94,8 +96,8 @@ const MamiSettings = function(storageOrPrefix, eventTarget) {
} else } else
storage.set(setting.name, value); storage.set(setting.name, value);
dispatchUpdate(setting.name, value); dispatchUpdate(setting.name, value, silent, true);
broadcastUpdate(setting.name, value); broadcastUpdate(setting.name, value, silent);
}; };
broadcast.onmessage = ev => { broadcast.onmessage = ev => {
@ -103,7 +105,7 @@ const MamiSettings = function(storageOrPrefix, eventTarget) {
return; return;
if(ev.data.act === 'update' && typeof ev.data.name === 'string') { if(ev.data.act === 'update' && typeof ev.data.name === 'string') {
dispatchUpdate(ev.data.name, ev.data.value); dispatchUpdate(ev.data.name, ev.data.value, ev.data.silent, false);
return; return;
} }
}; };
@ -196,26 +198,26 @@ const MamiSettings = function(storageOrPrefix, eventTarget) {
&& storage.get(setting.name) !== null; && storage.get(setting.name) !== null;
}, },
get: name => getValue(getSetting(name)), get: name => getValue(getSetting(name)),
set: (name, value) => setValue(getSetting(name), value), set: (name, value, silent) => setValue(getSetting(name), value, silent),
delete: name => deleteValue(getSetting(name)), delete: (name, silent) => deleteValue(getSetting(name), silent),
toggle: name => { toggle: (name, silent) => {
const setting = getSetting(name); const setting = getSetting(name);
if(!setting.immutable) if(!setting.immutable)
setValue(setting, !getValue(setting)); setValue(setting, !getValue(setting), silent);
}, },
touch: name => { touch: (name, silent) => {
const setting = getSetting(name); const setting = getSetting(name);
dispatchUpdate(setting.name, getValue(setting)); dispatchUpdate(setting.name, getValue(setting), silent, true);
}, },
clear: (criticalOnly, prefix) => { clear: (criticalOnly, prefix) => {
for(const setting of settings.values()) for(const setting of settings.values())
if((prefix === undefined || setting.name.startsWith(prefix)) && (!criticalOnly || setting.critical)) if((prefix === undefined || setting.name.startsWith(prefix)) && (!criticalOnly || setting.critical))
deleteValue(setting); deleteValue(setting);
}, },
watch: (name, handler) => { watch: (name, handler, silent) => {
const setting = getSetting(name); const setting = getSetting(name);
eventTarget.watch(setting.name, handler); eventTarget.watch(setting.name, handler);
handler(createUpdateEvent(setting.name, getValue(setting), true)); handler(createUpdateEvent(setting.name, getValue(setting), true, silent, true));
}, },
unwatch: (name, handler) => { unwatch: (name, handler) => {
eventTarget.unwatch(name, handler); eventTarget.unwatch(name, handler);

View file

@ -8,9 +8,11 @@ Umi.UI.InputMenus = (function() {
const inputMenuActive = 'input__menu--active'; const inputMenuActive = 'input__menu--active';
const inputButtonActive = 'input__button--active'; const inputButtonActive = 'input__button--active';
const createButtonId = id => `umi-msg-menu-btn-${id}`;
const toggle = function(baseId) { const toggle = function(baseId) {
const button = 'umi-msg-menu-btn-' + baseId, const button = createButtonId(baseId);
menu = 'umi-msg-menu-sub-' + baseId; const menu = 'umi-msg-menu-sub-' + baseId;
if($c(inputMenuActive).length) if($c(inputMenuActive).length)
$c(inputMenuActive)[0].classList.remove(inputMenuActive); $c(inputMenuActive)[0].classList.remove(inputMenuActive);
@ -30,7 +32,7 @@ Umi.UI.InputMenus = (function() {
tag: 'button', tag: 'button',
attrs: { attrs: {
type: 'button', type: 'button',
id: 'umi-msg-menu-btn-' + id, id: createButtonId(id),
classList: ['input__button', 'input__button--' + id], classList: ['input__button', 'input__button--' + id],
title: title, title: title,
onclick: onClick || (function() { onclick: onClick || (function() {
@ -41,20 +43,34 @@ Umi.UI.InputMenus = (function() {
}; };
return { return {
Add: function(baseId, title) { Add: function(baseId, title, beforeButtonId) {
if(ids.indexOf(baseId) < 0) { if(ids.includes(baseId))
ids.push(baseId); return;
$i('umi-msg-container').insertBefore(createButton(baseId, title), $i('umi-msg-send')); ids.push(baseId);
$i('umi-msg-menu').appendChild(
$e({ attrs: { 'class': ['input__menu', 'input__menu--' + baseId], id: 'umi-msg-menu-sub-' + baseId } }) let beforeButton;
); if(typeof beforeButtonId === 'string')
} beforeButton = $i(createButtonId(beforeButtonId))
if(!(beforeButton instanceof Element))
beforeButton = $i('umi-msg-send');
$i('umi-msg-container').insertBefore(createButton(baseId, title), beforeButton);
$i('umi-msg-menu').appendChild(
$e({ attrs: { 'class': ['input__menu', 'input__menu--' + baseId], id: 'umi-msg-menu-sub-' + baseId } })
);
}, },
AddButton: function(baseId, title, onClick) { AddButton: function(baseId, title, onClick, beforeButtonId) {
if(ids.indexOf(baseId) < 0) { if(ids.includes(baseId))
ids.push(baseId); return;
$i('umi-msg-container').insertBefore(createButton(baseId, title, onClick), $i('umi-msg-send')); ids.push(baseId);
}
let beforeButton;
if(typeof beforeButtonId === 'string')
beforeButton = $i(createButtonId(beforeButtonId))
if(!(beforeButton instanceof Element))
beforeButton = $i('umi-msg-send');
$i('umi-msg-container').insertBefore(createButton(baseId, title, onClick), beforeButton);
}, },
Get: function(baseId, button) { Get: function(baseId, button) {
const id = 'umi-msg-menu-' + (button ? 'btn' : 'sub') + '-' + baseId; const id = 'umi-msg-menu-' + (button ? 'btn' : 'sub') + '-' + baseId;
@ -63,7 +79,7 @@ Umi.UI.InputMenus = (function() {
return null; return null;
}, },
Remove: function(baseId) { Remove: function(baseId) {
$ri('umi-msg-menu-btn-' + baseId); $ri(createButtonId(baseId));
$ri('umi-msg-menu-sub-' + baseId); $ri('umi-msg-menu-sub-' + baseId);
}, },
Toggle: toggle, Toggle: toggle,

View file

@ -141,7 +141,7 @@ Umi.UI.Settings = (function() {
type: 'select', type: 'select',
options: () => { options: () => {
const packs = mami.sound.packs; const packs = mami.sound.packs;
const options = {}; const options = { '': 'Default' };
for(const name of packs.names()) for(const name of packs.names())
options[name] = packs.info(name).getTitle(); options[name] = packs.info(name).getTitle();