Android: dynamically create setting (#352)

This commit is contained in:
NullVector 2026-10-05 15:25:04 +10:30 committed by GitHub
parent 1a2f243bd1
commit fe2aa29813
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 332 additions and 286 deletions

View File

@ -24,6 +24,12 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="org.nxemu.ui.main.SettingsActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:exported="false"
android:screenOrientation="behind"
android:theme="@style/Theme.NxEmu.Settings" />
<activity
android:name="org.nxemu.ui.emulation.EmulationActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|smallestScreenSize|density"

View File

@ -14,7 +14,7 @@
<button class="icon-btn" type="button" aria-label="Refresh library" onclick="refreshGameList()">
<svg viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-2.6-6.2"/><path d="M21 3v6h-6"/></svg>
</button>
<button class="settings-btn" type="button" aria-label="Settings" onclick="navigate('settingsPage')">
<button class="settings-btn" type="button" aria-label="Settings" onclick="NxEmu.openSettings()">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
</button>
</div>
@ -27,161 +27,9 @@
<span id="touchHint">Tap a game <span class="hint-action">to play</span></span>
</div>
</div>
<div id="settingsPage" class="page" style="display:none">
<div class="topbar-back">
<button class="back-btn" type="button" aria-label="Back" onclick="goBack()">
<svg viewBox="0 0 24 24"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
</button>
<div class="page-title">Settings</div>
<span class="back-hint" hidden><span class="hint-pair"><span class="hint-key">A</span> <span class="hint-action">Select</span></span><span class="hint-pair"><span class="hint-key">B</span> <span class="hint-action">Back</span></span></span>
</div>
<div class="settings-list">
<div class="settings-item" onclick="navigate('gameFolders')">
<div class="setting-info">
<div class="setting-label">Game folders</div>
<div id="gameFoldersMeta" class="setting-sub">None added</div>
</div>
<svg class="setting-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
</div>
<div class="settings-item" onclick="navigate('overlaySettings')">
<div class="setting-info">
<div class="setting-label">On screen controller</div>
<div id="overlayShowMeta" class="setting-sub"></div>
</div>
<svg class="setting-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
</div>
<div class="settings-item" onclick="navigate('controllerSettings')">
<div class="setting-info">
<div class="setting-label">Controllers</div>
<div id="controllersMeta" class="setting-sub" style="white-space:pre-line"></div>
</div>
<svg class="setting-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
</div>
<div class="settings-item" onclick="navigate('themeSettings')">
<div class="setting-info">
<div class="setting-label">Theme</div>
<div id="themeModeMeta" class="setting-sub">Follow system</div>
</div>
<svg class="setting-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
</div>
</div>
</div>
<div id="controllerSettings" class="page" style="display:none">
<div class="topbar-back">
<button class="back-btn" type="button" aria-label="Back" onclick="goBack()">
<svg viewBox="0 0 24 24"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
</button>
<div class="page-title">Controllers</div>
<span class="back-hint" hidden><span class="hint-pair"><span class="hint-key">A</span> <span class="hint-action">Select</span></span><span class="hint-pair"><span class="hint-key">B</span> <span class="hint-action">Back</span></span></span>
</div>
<div id="controllerList" class="settings-list"></div>
</div>
<div id="controllerPlayer" class="page" style="display:none">
<div class="topbar-back">
<button class="back-btn" type="button" aria-label="Back" onclick="goBack()">
<svg viewBox="0 0 24 24"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
</button>
<div id="controllerPlayerTitle" class="page-title">Controller</div>
<span class="back-hint" hidden><span class="hint-pair"><span class="hint-key">A</span> <span class="hint-action">Select</span></span><span class="hint-pair"><span class="hint-key">B</span> <span class="hint-action">Back</span></span></span>
</div>
<div id="controllerPlayerList" class="settings-list"></div>
</div>
<div id="controllerChoices" class="page" style="display:none">
<div class="topbar-back">
<button class="back-btn" type="button" aria-label="Back" onclick="goBack()">
<svg viewBox="0 0 24 24"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
</button>
<div id="controllerChoicesTitle" class="page-title">Controller</div>
<span class="back-hint" hidden><span class="hint-pair"><span class="hint-key">A</span> <span class="hint-action">Select</span></span><span class="hint-pair"><span class="hint-key">B</span> <span class="hint-action">Back</span></span></span>
</div>
<div id="controllerChoiceList" class="settings-list"></div>
</div>
<div id="themeSettings" class="page" style="display:none">
<div class="topbar-back">
<button class="back-btn" type="button" aria-label="Back" onclick="goBack()">
<svg viewBox="0 0 24 24"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
</button>
<div class="page-title">Theme</div>
<span class="back-hint" hidden><span class="hint-pair"><span class="hint-key">A</span> <span class="hint-action">Select</span></span><span class="hint-pair"><span class="hint-key">B</span> <span class="hint-action">Back</span></span></span>
</div>
<div class="settings-list">
<div class="settings-item" onclick="setThemeMode(0)">
<div class="setting-info">
<div class="setting-label">Follow system</div>
</div>
<svg id="themeFollow" class="setting-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
</div>
<div class="settings-item" onclick="setThemeMode(1)">
<div class="setting-info">
<div class="setting-label">Light</div>
</div>
<svg id="themeLight" class="setting-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
</div>
<div class="settings-item" onclick="setThemeMode(2)">
<div class="setting-info">
<div class="setting-label">Dark</div>
</div>
<svg id="themeDark" class="setting-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
</div>
</div>
</div>
<div id="overlaySettings" class="page" style="display:none">
<div class="topbar-back">
<button class="back-btn" type="button" aria-label="Back" onclick="goBack()">
<svg viewBox="0 0 24 24"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
</button>
<div class="page-title">On screen controller</div>
<span class="back-hint" hidden><span class="hint-pair"><span class="hint-key">A</span> <span class="hint-action">Select</span></span><span class="hint-pair"><span class="hint-key">B</span> <span class="hint-action">Back</span></span></span>
</div>
<div class="settings-list">
<div class="settings-item" onclick="toggleOverlayBool('nxui:ShowInputOverlay', 'overlayShow')">
<div class="setting-info">
<div class="setting-label">Show overlay</div>
</div>
<button id="overlayShow" class="settings-toggle" type="button" aria-label="Show overlay"></button>
</div>
<div class="settings-item setting-item-static">
<div class="setting-info" style="flex:1">
<div class="setting-label">Scale <span id="overlayScaleValue">50%</span></div>
<input id="overlayScale" class="settings-range" type="range" min="0" max="150" value="50" oninput="setOverlayInt('nxui:OverlayScale', this.value, 'overlayScaleValue')">
</div>
</div>
<div class="settings-item setting-item-static">
<div class="setting-info" style="flex:1">
<div class="setting-label">Opacity <span id="overlayOpacityValue">100%</span></div>
<input id="overlayOpacity" class="settings-range" type="range" min="0" max="100" value="100" oninput="setOverlayInt('nxui:OverlayOpacity', this.value, 'overlayOpacityValue')">
</div>
</div>
<div class="settings-item" onclick="NxEmu.editOverlayLayout()">
<div class="setting-info">
<div class="setting-label">Edit overlay layout</div>
<div class="setting-sub">Drag buttons to reposition</div>
</div>
<svg class="setting-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
</div>
</div>
</div>
<div id="gameFolders" class="page" style="display:none">
<div class="topbar-back">
<button class="back-btn" type="button" aria-label="Back" onclick="goBack()">
<svg viewBox="0 0 24 24"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
</button>
<div class="page-title">Game folders</div>
<span class="back-hint" hidden><span class="hint-pair"><span class="hint-key">A</span> <span class="hint-action">Select</span></span><span class="hint-pair"><span class="hint-key">B</span> <span class="hint-action">Back</span></span></span>
</div>
<div id="folderList" class="settings-list"></div>
<div id="noFolders" class="empty-state">
<p class="empty-title">No folders yet</p>
<p class="empty-copy">Choose a folder that contains .nro, .dxci, or .dnsp files.</p>
<button class="primary-btn" onclick="NxEmu.addGameDirectory()">Add game folder</button>
</div>
<button class="fab" type="button" aria-label="Add game folder" onclick="NxEmu.addGameDirectory()">
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
</div>
<script>
applyTheme();
renderSettingsSummary();
applyControllerBrowser();
refreshGameList();
</script>
</body>

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<link rel="stylesheet" href="ui.css">
<script src="ui.js"></script>
</head>
<body>
<div id="settingsShell" class="page" style="display:none">
<div class="topbar-back">
<button class="back-btn" type="button" aria-label="Back" onclick="goBack()">
<svg viewBox="0 0 24 24"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
</button>
<div id="settingsTitle" class="page-title">Settings</div>
<span id="backHint" class="back-hint" hidden><span class="hint-pair"><span class="hint-key">A</span> <span class="hint-action">Select</span></span><span class="hint-pair"><span class="hint-key">B</span> <span class="hint-action">Back</span></span></span>
</div>
<div id="settingsBody"></div>
</div>
<script>
applyTheme();
applyControllerBrowser();
navStack = ['settingsPage'];
showPage('settingsPage');
</script>
</body>
</html>

View File

@ -28,16 +28,14 @@ function applyTheme() {
function onSettingChanged(setting) {
if (setting === 'nxui:GameDirectories') {
libraryDirty = true;
renderGameFolders();
renderSettingsSummary();
if (navStack[navStack.length - 1] === 'gamesPage') {
refreshGameList();
}
const top = navStack[navStack.length - 1];
if (top === 'gameFolders' || top === 'settingsPage') renderCurrentSettings();
if (top === 'gamesPage') refreshGameList();
}
if (setting === 'nxui:ThemeMode') {
applyTheme();
renderThemeSummary();
renderThemeSettings();
const top = navStack[navStack.length - 1];
if (top === 'themeSettings' || top === 'settingsPage') renderCurrentSettings();
}
}
@ -266,17 +264,15 @@ function applyControllerBrowser() {
const touchHint = document.getElementById('touchHint');
if (settingsHint) settingsHint.hidden = !attached;
if (touchHint) touchHint.hidden = attached;
document.querySelectorAll('.back-hint').forEach(function (hint) {
hint.hidden = !attached;
});
const backHint = document.getElementById('backHint');
if (backHint) backHint.hidden = !attached;
if (!attached) {
syncGameSelection(-1);
syncSettingsSelection(-1);
return;
}
if (currentSettingsPage()) {
const page = currentSettingsPage();
const remembered = settingsSelectionByPage[page.id];
if (currentSettingsId()) {
const remembered = settingsSelectionByPage[currentSettingsId()];
const index = settingsSelection >= 0 ? settingsSelection : (remembered == null ? 0 : remembered);
syncSettingsSelection(index);
}
@ -356,7 +352,7 @@ function shortPath(uri) {
function openSettingsFromPad() {
if (navStack[navStack.length - 1] !== 'gamesPage') return;
navigate('settingsPage');
if (typeof NxEmu.openSettings === 'function') NxEmu.openSettings();
}
function goBackFromPad() {
@ -365,12 +361,17 @@ function goBackFromPad() {
goBack();
}
function currentSettingsPage() {
function currentSettingsId() {
const id = navStack[navStack.length - 1];
if (!id || id === 'gamesPage') return null;
const page = document.getElementById(id);
if (!page || page.style.display === 'none') return null;
return page;
if (!id || !settingPage(id)) return null;
return id;
}
function currentSettingsPage() {
if (!currentSettingsId()) return null;
const shell = document.getElementById('settingsShell');
if (!shell || shell.style.display === 'none') return null;
return shell;
}
function settingsRows(page) {
@ -396,7 +397,8 @@ function syncSettingsSelection(index) {
return;
}
settingsSelection = Math.min(index, rows.length - 1);
settingsSelectionByPage[page.id] = settingsSelection;
const settingsId = currentSettingsId();
if (settingsId) settingsSelectionByPage[settingsId] = settingsSelection;
rows[settingsSelection].classList.add('is-selected');
rows[settingsSelection].scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
@ -462,6 +464,10 @@ function goBack() {
if (navStack.length > 1) {
navStack.pop();
showPage(navStack[navStack.length - 1]);
return;
}
if (typeof NxEmu.closeSettings === 'function') {
NxEmu.closeSettings();
}
}
@ -482,29 +488,9 @@ function formatGameFolder(uri) {
return volume + ' / ' + folder;
}
function renderSettingsSummary() {
const meta = document.getElementById('gameFoldersMeta');
if (meta) {
const n = getConfiguredFolders().length;
if (n === 0) meta.textContent = 'None added';
else if (n === 1) meta.textContent = '1 folder';
else meta.textContent = n + ' folders';
}
renderThemeSummary();
renderOverlaySummary();
renderControllersSummary();
}
function renderControllersSummary() {
const meta = document.getElementById('controllersMeta');
if (meta) {
var count = 0;
for (var player = 0; player < 8; player++) {
if (NxEmu.isControllerConnected(player)) count++;
}
meta.textContent = count === 0 ? 'Connected: none' : 'Connected: ' + count;
}
applyControllerBrowser();
if (navStack[navStack.length - 1] === 'settingsPage') renderCurrentSettings();
}
function getThemeMode() {
@ -521,82 +507,217 @@ function themeModeLabel(mode) {
return 'Follow system';
}
function renderThemeSummary() {
const meta = document.getElementById('themeModeMeta');
if (meta) meta.textContent = themeModeLabel(getThemeMode());
function folderCountLabel() {
const n = getConfiguredFolders().length;
if (n === 0) return 'None added';
if (n === 1) return '1 folder';
return n + ' folders';
}
function renderOverlaySummary() {
const meta = document.getElementById('overlayShowMeta');
if (!meta) return;
function overlayShowLabel() {
let shown = true;
try {
shown = NxEmu.getSettingBool('nxui:ShowInputOverlay');
} catch (e) {}
meta.textContent = 'Show overlay: ' + (shown ? 'Yes' : 'No');
return 'Show overlay: ' + (shown ? 'Yes' : 'No');
}
function renderThemeSettings() {
const mode = getThemeMode();
setToggle('themeFollow', mode === 0);
setToggle('themeLight', mode === 1);
setToggle('themeDark', mode === 2);
function connectedCountLabel() {
var count = 0;
for (var player = 0; player < 8; player++) {
if (NxEmu.isControllerConnected(player)) count++;
}
return count === 0 ? 'Connected: none' : 'Connected: ' + count;
}
function setThemeMode(mode) {
NxEmu.setSettingInt('nxui:ThemeMode', mode);
NxEmu.saveSettings();
applyTheme();
renderThemeSummary();
renderThemeSettings();
function readSettingBool(key) {
try {
return !!NxEmu.getSettingBool(key);
} catch (e) {
return false;
}
}
function setToggle(id, on) {
const el = document.getElementById(id);
if (!el) return;
el.classList.toggle('on', !!on);
function readSettingInt(key) {
try {
return NxEmu.getSettingInt(key);
} catch (e) {
return 0;
}
}
function renderOverlaySettings() {
setToggle('overlayShow', NxEmu.getSettingBool('nxui:ShowInputOverlay'));
const scale = NxEmu.getSettingInt('nxui:OverlayScale');
const opacity = NxEmu.getSettingInt('nxui:OverlayOpacity');
const scaleEl = document.getElementById('overlayScale');
const opacityEl = document.getElementById('overlayOpacity');
if (scaleEl) scaleEl.value = scale;
if (opacityEl) opacityEl.value = opacity;
const scaleValue = document.getElementById('overlayScaleValue');
const opacityValue = document.getElementById('overlayOpacityValue');
if (scaleValue) scaleValue.textContent = scale + '%';
if (opacityValue) opacityValue.textContent = opacity + '%';
const settingPages = [
{
id: 'settingsPage',
title: 'Settings',
rows: [
{ label: 'Game folders', type: 'page', page: 'gameFolders', detail: folderCountLabel },
{ label: 'On screen controller', type: 'page', page: 'overlaySettings', detail: overlayShowLabel },
{ label: 'Controllers', type: 'page', page: 'controllerSettings', detail: connectedCountLabel },
{ label: 'Theme', type: 'page', page: 'themeSettings', detail: function () { return themeModeLabel(getThemeMode()); } },
],
},
{
id: 'overlaySettings',
title: 'On screen controller',
rows: [
{ label: 'Show overlay', type: 'bool', key: 'nxui:ShowInputOverlay' },
{ label: 'Scale', type: 'int', key: 'nxui:OverlayScale', min: 0, max: 150, suffix: '%' },
{ label: 'Opacity', type: 'int', key: 'nxui:OverlayOpacity', min: 0, max: 100, suffix: '%' },
{ label: 'Edit overlay layout', detail: 'Drag buttons to reposition', type: 'action', run: function () { NxEmu.editOverlayLayout(); } },
],
},
{
id: 'themeSettings',
title: 'Theme',
rows: [
{ label: 'Follow system', type: 'choice', key: 'nxui:ThemeMode', value: 0, picked: function () { return getThemeMode() === 0; }, run: applyTheme },
{ label: 'Light', type: 'choice', key: 'nxui:ThemeMode', value: 1, picked: function () { return getThemeMode() === 1; }, run: applyTheme },
{ label: 'Dark', type: 'choice', key: 'nxui:ThemeMode', value: 2, picked: function () { return getThemeMode() === 2; }, run: applyTheme },
],
},
{ id: 'gameFolders', title: 'Game folders', render: renderGameFolders },
{ id: 'controllerSettings', title: 'Controllers', render: renderControllerSettings },
{ id: 'controllerPlayer', title: function () { return 'Controller ' + (controllerPlayerIndex + 1); }, render: renderControllerPlayer },
{ id: 'controllerChoices', title: function () { return controllerChoiceState.title; }, render: renderControllerChoices },
];
function settingPage(id) {
for (var i = 0; i < settingPages.length; i++) {
if (settingPages[i].id === id) return settingPages[i];
}
return null;
}
function toggleOverlayBool(key, toggleId) {
const next = !NxEmu.getSettingBool(key);
NxEmu.setSettingBool(key, next);
NxEmu.saveSettings();
setToggle(toggleId, next);
if (key === 'nxui:ShowInputOverlay') renderOverlaySummary();
function rowDetail(row) {
if (typeof row.detail === 'function') return row.detail();
return row.detail || '';
}
function setOverlayInt(key, value, labelId) {
const n = parseInt(value, 10);
NxEmu.setSettingInt(key, n);
NxEmu.saveSettings();
const label = document.getElementById(labelId);
if (label) label.textContent = n + '%';
function ensureSettingsList() {
const host = document.getElementById('settingsBody');
if (!host) return null;
host.innerHTML = '';
const list = document.createElement('div');
list.className = 'settings-list';
host.appendChild(list);
return list;
}
function renderSettingRows(spec) {
const list = ensureSettingsList();
if (!list) return;
spec.rows.forEach(function (row) {
if (row.type === 'bool') appendBoolRow(list, row);
else if (row.type === 'int') appendIntRow(list, row);
else if (row.type === 'choice') appendChoiceRow(list, row);
else appendLinkRow(list, row);
});
}
function appendLinkRow(list, row) {
const item = document.createElement('div');
item.className = 'settings-item';
const detail = rowDetail(row);
item.innerHTML =
'<div class="setting-info">' +
'<div class="setting-label">' + escapeHtml(row.label) + '</div>' +
(detail ? '<div class="setting-sub">' + escapeHtml(detail) + '</div>' : '') +
'</div>' + controllerChevron();
item.addEventListener('click', function () {
if (row.page) navigate(row.page);
else if (row.run) row.run();
});
list.appendChild(item);
}
function appendBoolRow(list, row) {
const on = readSettingBool(row.key);
const item = document.createElement('div');
item.className = 'settings-item';
item.innerHTML =
'<div class="setting-info"><div class="setting-label">' + escapeHtml(row.label) + '</div></div>' +
'<button class="settings-toggle' + (on ? ' on' : '') + '" type="button" aria-label="' + escapeHtml(row.label) + '"></button>';
item.addEventListener('click', function () {
NxEmu.setSettingBool(row.key, !readSettingBool(row.key));
NxEmu.saveSettings();
if (row.run) row.run();
renderCurrentSettings();
});
list.appendChild(item);
}
function appendIntRow(list, row) {
const value = readSettingInt(row.key);
const suffix = row.suffix || '';
const step = row.step == null ? 1 : row.step;
const item = document.createElement('div');
item.className = 'settings-item setting-item-static';
item.innerHTML =
'<div class="setting-info" style="flex:1">' +
'<div class="setting-label">' + escapeHtml(row.label) + ' <span class="slider-value">' + value + suffix + '</span></div>' +
'<input class="settings-range" type="range" min="' + row.min + '" max="' + row.max + '" step="' + step + '" value="' + value + '">' +
'</div>';
const input = item.querySelector('input');
const label = item.querySelector('.slider-value');
input.addEventListener('input', function () {
const next = parseInt(input.value, 10);
if (label) label.textContent = next + suffix;
NxEmu.setSettingInt(row.key, next);
NxEmu.saveSettings();
if (row.run) row.run();
});
list.appendChild(item);
}
function appendChoiceRow(list, row) {
const on = row.picked ? !!row.picked() : readSettingInt(row.key) === row.value;
const item = document.createElement('div');
item.className = 'settings-item';
item.innerHTML =
'<div class="setting-info"><div class="setting-label">' + escapeHtml(row.label) + '</div></div>' +
'<svg class="setting-check' + (on ? ' on' : '') + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>';
item.addEventListener('click', function () {
if (row.key) {
NxEmu.setSettingInt(row.key, row.value);
NxEmu.saveSettings();
}
if (row.run) row.run();
renderCurrentSettings();
});
list.appendChild(item);
}
function renderCurrentSettings() {
const spec = settingPage(navStack[navStack.length - 1]);
if (!spec) return;
const title = document.getElementById('settingsTitle');
if (title) title.textContent = typeof spec.title === 'function' ? spec.title() : spec.title;
if (spec.rows) renderSettingRows(spec);
else if (spec.render) spec.render();
refreshSettingsSelection();
}
function renderGameFolders() {
const list = document.getElementById('folderList');
const empty = document.getElementById('noFolders');
if (!list || !empty) return;
const host = document.getElementById('settingsBody');
if (!host) return;
host.innerHTML = '';
const dirs = getConfiguredFolders();
empty.style.display = dirs.length === 0 ? '' : 'none';
list.innerHTML = '';
const fab = document.querySelector('#gameFolders .fab');
if (fab) fab.style.display = dirs.length === 0 ? 'none' : '';
const list = document.createElement('div');
list.className = 'settings-list';
host.appendChild(list);
if (!dirs.length) {
const empty = document.createElement('div');
empty.className = 'empty-state';
empty.innerHTML =
'<p class="empty-title">No folders yet</p>' +
'<p class="empty-copy">Choose a folder that contains .nro, .dxci, or .dnsp files.</p>' +
'<button class="primary-btn" type="button">Add game folder</button>';
empty.querySelector('button').addEventListener('click', function () {
NxEmu.addGameDirectory();
});
host.appendChild(empty);
}
dirs.forEach(function (uri) {
const name = formatGameFolder(uri);
const item = document.createElement('div');
@ -613,6 +734,15 @@ function renderGameFolders() {
});
list.appendChild(item);
});
if (dirs.length) {
const fab = document.createElement('button');
fab.className = 'fab';
fab.type = 'button';
fab.setAttribute('aria-label', 'Add game folder');
fab.innerHTML = '<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>';
fab.addEventListener('click', function () { NxEmu.addGameDirectory(); });
host.appendChild(fab);
}
refreshSettingsSelection();
}
@ -627,6 +757,7 @@ function removeGameFolder(uri) {
var controllerPlayerIndex = 0;
var controllerFilterIndex = 0;
var controllerChoiceState = { title: 'Controller', items: [], onPick: function () {}, emptyText: '' };
var BTN = { A: 0, B: 1, X: 2, Y: 3, LStick: 4, RStick: 5, L: 6, R: 7, ZL: 8, ZR: 9, Plus: 10, Minus: 11, DLeft: 12, DUp: 13, DRight: 14, DDown: 15, SLLeft: 16, SRLeft: 17, Home: 18, Capture: 19, SLRight: 20, SRRight: 21 };
var STICK = { L: 0, R: 1 };
@ -661,14 +792,12 @@ function openControllerPlayer(index) {
}
function renderControllerPages() {
const listPage = document.getElementById('controllerSettings');
const playerPage = document.getElementById('controllerPlayer');
if (listPage && listPage.style.display !== 'none') renderControllerSettings();
if (playerPage && playerPage.style.display !== 'none') renderControllerPlayer();
const id = navStack[navStack.length - 1];
if (id === 'controllerSettings' || id === 'controllerPlayer') renderCurrentSettings();
}
function renderControllerSettings() {
const list = document.getElementById('controllerList');
const list = ensureSettingsList();
if (!list) return;
list.innerHTML = '';
for (var player = 0; player < 8; player++) {
@ -777,19 +906,16 @@ function appendButtons(list, buttons) {
}
function renderControllerPlayer() {
const title = document.getElementById('controllerPlayerTitle');
const list = document.getElementById('controllerPlayerList');
const list = ensureSettingsList();
if (!list) return;
const player = controllerPlayerIndex;
const style = NxEmu.getControllerStyle(player);
const filters = parseJsonArray(NxEmu.getControllerFilterNames());
if (controllerFilterIndex >= filters.length) controllerFilterIndex = 0;
if (title) title.textContent = 'Controller ' + (player + 1);
list.innerHTML = '';
appendControllerSwitch(list, 'Connected', NxEmu.isControllerConnected(player), function () {
NxEmu.setControllerConnected(player, !NxEmu.isControllerConnected(player));
renderControllerPlayer();
renderCurrentSettings();
});
appendControllerRow(list, 'Controller type', controllerStyleName(style), openControllerStyles);
appendControllerRow(list, 'Auto-map a controller', 'Select a device to attempt auto-mapping', openAutoMap);
@ -860,27 +986,37 @@ function renderControllerPlayer() {
refreshSettingsSelection();
}
function openControllerChoices(title, items, onPick, emptyText) {
const choiceList = document.getElementById('controllerChoiceList');
document.getElementById('controllerChoicesTitle').textContent = title;
if (choiceList) choiceList.style.display = '';
choiceList.innerHTML = '';
if (!items.length) {
function renderControllerChoices() {
const list = ensureSettingsList();
if (!list) return;
const state = controllerChoiceState;
const title = document.getElementById('settingsTitle');
if (title) title.textContent = state.title;
if (!state.items.length) {
const empty = document.createElement('div');
empty.className = 'settings-header';
empty.textContent = emptyText || '';
choiceList.appendChild(empty);
empty.textContent = state.emptyText || '';
list.appendChild(empty);
}
items.forEach(function (item, index) {
state.items.forEach(function (item, index) {
const row = document.createElement('div');
row.className = 'settings-item';
row.innerHTML = '<div class="setting-info"><div class="setting-label">' + escapeHtml(item.label) + '</div></div>' + controllerChevron();
row.addEventListener('click', function () { onPick(item, index); });
choiceList.appendChild(row);
row.addEventListener('click', function () { state.onPick(item, index); });
list.appendChild(row);
});
const page = document.getElementById('controllerChoices');
if (page.style.display === 'none') navigate('controllerChoices');
else refreshSettingsSelection();
refreshSettingsSelection();
}
function openControllerChoices(title, items, onPick, emptyText) {
controllerChoiceState = {
title: title,
items: items,
onPick: onPick,
emptyText: emptyText || '',
};
if (navStack[navStack.length - 1] === 'controllerChoices') renderCurrentSettings();
else navigate('controllerChoices');
}
function openControllerStyles() {
@ -917,22 +1053,20 @@ function openControllerFilter() {
function confirmResetController() {
if (!confirm("Reset this player to defaults?")) return;
NxEmu.resetControllerMappings(controllerPlayerIndex);
renderControllerPlayer();
renderCurrentSettings();
}
function showPage(page) {
document.querySelectorAll('.page').forEach(function (p) { p.style.display = 'none'; });
document.getElementById(page).style.display = '';
if (page !== 'gamesPage') {
const spec = settingPage(page);
if (spec) {
document.getElementById('settingsShell').style.display = '';
const remembered = settingsSelectionByPage[page];
settingsSelection = remembered == null ? 0 : remembered;
renderCurrentSettings();
return;
}
if (page === 'gameFolders') renderGameFolders();
if (page === 'settingsPage') renderSettingsSummary();
if (page === 'overlaySettings') renderOverlaySettings();
if (page === 'themeSettings') renderThemeSettings();
if (page === 'controllerSettings') renderControllerSettings();
if (page === 'controllerPlayer') renderControllerPlayer();
const el = document.getElementById(page);
if (el) el.style.display = '';
if (page === 'gamesPage' && libraryDirty) refreshGameList();
if (page !== 'gamesPage') refreshSettingsSelection();
}

View File

@ -26,7 +26,7 @@ import org.nxemu.ui.settings.OverlayLayoutActivity
import org.nxemu.utils.InputHandler
import org.nxemu.utils.ThemeHelper
class MainActivity : ComponentActivity() {
open class MainActivity : ComponentActivity() {
private lateinit var webView: WebView
private val inputMapping = InputMappingSession(this) { refreshControllerPages() }
private var emulationLaunchPending = false
@ -82,7 +82,9 @@ class MainActivity : ComponentActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
if (this !is SettingsActivity) {
installSplashScreen()
}
super.onCreate(savedInstanceState)
webView = WebView(this).apply {
setBackgroundColor(ThemeHelper.backgroundColor(this@MainActivity))
@ -141,7 +143,8 @@ class MainActivity : ComponentActivity() {
},
)
NativeLibrary.addSettingChangedListener(settingChangedForwarder)
webView.loadUrl("file:///android_asset/index.html")
val page = if (this is SettingsActivity) "settings.html" else "index.html"
webView.loadUrl("file:///android_asset/$page")
setContentView(webView)
ThemeHelper.applySystemBars(this)
}
@ -328,3 +331,5 @@ class MainActivity : ComponentActivity() {
private const val STICK_DEADZONE = 0.5f
}
}
class SettingsActivity : MainActivity()

View File

@ -1,5 +1,6 @@
package org.nxemu.ui.main
import android.content.Intent
import android.webkit.JavascriptInterface
import org.json.JSONArray
import org.nxemu.GameLibraryScanner
@ -139,6 +140,25 @@ class NxEmuBridge(private val activity: MainActivity) {
return NativeLibrary.queryRomMetadata(path)
}
@JavascriptInterface
fun openSettings() {
activity.runOnUiThread {
if (activity is SettingsActivity) {
return@runOnUiThread
}
activity.startActivity(Intent(activity, SettingsActivity::class.java))
}
}
@JavascriptInterface
fun closeSettings() {
activity.runOnUiThread {
if (activity is SettingsActivity) {
activity.finish()
}
}
}
@JavascriptInterface
fun launchGame(path: String) {
activity.runOnUiThread { activity.launchGame(path) }

View File

@ -9,4 +9,10 @@
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_nxemu_splash</item>
<item name="postSplashScreenTheme">@style/Theme.NxEmu</item>
</style>
<style name="Theme.NxEmu.Settings" parent="Theme.NxEmu">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/black</item>
<item name="android:windowIsFloating">false</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>