refactor: split components, extract lib logic, and add mock/theming support
This commit is contained in:
parent
a5e002879c
commit
749e7b3389
11 changed files with 587 additions and 683 deletions
|
|
@ -1,706 +1,55 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { exec } from 'kernelsu';
|
||||
import locate from './locate.json';
|
||||
import './app.css';
|
||||
import '@/app.css';
|
||||
import { L } from '@lib/store.js';
|
||||
|
||||
// Components
|
||||
import TopBar from '@comp/TopBar.svelte';
|
||||
import ConfigView from '@comp/ConfigView.svelte';
|
||||
import ModulesView from '@comp/ModulesView.svelte';
|
||||
import LogsView from '@comp/LogsView.svelte';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
moduledir: '/data/adb/modules',
|
||||
tempdir: '',
|
||||
mountsource: 'KSU',
|
||||
logfile: '/data/adb/magic_mount/mm.log',
|
||||
verbose: false,
|
||||
umount: false,
|
||||
partitions: []
|
||||
// Tabs
|
||||
const tabs = {
|
||||
config: { component: ConfigView, label: 'tabs.config' }, // label 为 locate key
|
||||
module: { component: ModulesView, label: 'tabs.modules' },
|
||||
log: { component: LogsView, label: 'tabs.logs' }
|
||||
};
|
||||
|
||||
const CONFIG_PATH = '/data/adb/magic_mount/mm.conf';
|
||||
|
||||
// i18n
|
||||
let lang = 'en';
|
||||
$: L = locate[lang];
|
||||
|
||||
// Autoload lang
|
||||
const availableLanguages = Object.keys(locate).map(code => ({
|
||||
code,
|
||||
name: locate[code]?.lang?.display || code.toUpperCase()
|
||||
}));
|
||||
|
||||
let langDropdownOpen = false;
|
||||
|
||||
function toggleLangDropdown() {
|
||||
langDropdownOpen = !langDropdownOpen;
|
||||
}
|
||||
|
||||
function selectLanguage(newLang) {
|
||||
lang = newLang;
|
||||
langDropdownOpen = false;
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem('mm-lang', newLang);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickOutside(event) {
|
||||
const dropdown = event.target.closest('.lang-dropdown');
|
||||
if (!dropdown && langDropdownOpen) {
|
||||
langDropdownOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
// tabs
|
||||
let activeTab = 'config';
|
||||
let isSwitching = false;
|
||||
|
||||
function switchTab(tab) {
|
||||
if (tab === activeTab || isSwitching) return;
|
||||
|
||||
function switchTab(key) {
|
||||
if (activeTab === key || isSwitching) return;
|
||||
isSwitching = true;
|
||||
|
||||
setTimeout(() => {
|
||||
activeTab = tab;
|
||||
activeTab = key;
|
||||
isSwitching = false;
|
||||
}, 300);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
|
||||
// config state
|
||||
let config = { ...DEFAULT_CONFIG };
|
||||
let configLoading = false;
|
||||
let configSaving = false;
|
||||
let configMessage = null;
|
||||
|
||||
// log state
|
||||
let logSelection = 'current';
|
||||
let logContent = '';
|
||||
let logLoading = false;
|
||||
let logError = null;
|
||||
|
||||
// partitions input
|
||||
let partitionInput = '';
|
||||
|
||||
// modules state
|
||||
let modules = [];
|
||||
let modulesLoading = false;
|
||||
let modulesError = null;
|
||||
|
||||
// helpers
|
||||
function isTrueValue(v) {
|
||||
const s = v.trim().toLowerCase();
|
||||
return s === '1' || s === 'true' || s === 'yes' || s === 'on';
|
||||
}
|
||||
|
||||
function parseKvConfig(text) {
|
||||
try {
|
||||
const result = { ...DEFAULT_CONFIG };
|
||||
const lines = text.split('\n');
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
|
||||
const eqIndex = line.indexOf('=');
|
||||
if (eqIndex < 0) continue;
|
||||
|
||||
let key = line.slice(0, eqIndex).trim();
|
||||
let value = line.slice(eqIndex + 1).trim();
|
||||
|
||||
if (!key || !value) continue;
|
||||
|
||||
switch (key) {
|
||||
case 'module_dir':
|
||||
result.moduledir = value;
|
||||
break;
|
||||
case 'temp_dir':
|
||||
result.tempdir = value;
|
||||
break;
|
||||
case 'mount_source':
|
||||
result.mountsource = value;
|
||||
break;
|
||||
case 'log_file':
|
||||
result.logfile = value;
|
||||
break;
|
||||
case 'debug':
|
||||
result.verbose = isTrueValue(value);
|
||||
break;
|
||||
case 'umount':
|
||||
result.umount = isTrueValue(value);
|
||||
break;
|
||||
case 'partitions':
|
||||
result.partitions = value
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse config:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function serializeKvConfig(cfg) {
|
||||
const lines = ['# Magic Mount Configuration File', '# Generated by Web UI', ''];
|
||||
|
||||
const moduleDir = cfg.moduledir || DEFAULT_CONFIG.moduledir;
|
||||
const mountSource = cfg.mountsource || DEFAULT_CONFIG.mountsource;
|
||||
const logFile = cfg.logfile || DEFAULT_CONFIG.logfile;
|
||||
|
||||
lines.push(`module_dir=${moduleDir}`);
|
||||
|
||||
if (cfg.tempdir) {
|
||||
lines.push(`temp_dir=${cfg.tempdir}`);
|
||||
}
|
||||
|
||||
lines.push(`mount_source=${mountSource}`);
|
||||
|
||||
if (logFile) {
|
||||
lines.push(`log_file=${logFile}`);
|
||||
}
|
||||
|
||||
lines.push(`debug=${cfg.verbose ? 'true' : 'false'}`);
|
||||
|
||||
lines.push(`umount=${cfg.umount ? 'true' : 'false'}`);
|
||||
|
||||
if (cfg.partitions.length > 0) {
|
||||
lines.push(`partitions=${cfg.partitions.join(',')}`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// load config
|
||||
async function loadConfig() {
|
||||
configLoading = true;
|
||||
configMessage = null;
|
||||
|
||||
try {
|
||||
const { errno, stdout, stderr } = await exec(
|
||||
`[ -f "${CONFIG_PATH}" ] && cat "${CONFIG_PATH}" || echo ""`
|
||||
);
|
||||
|
||||
if (errno !== 0) {
|
||||
console.error(stderr);
|
||||
config = { ...DEFAULT_CONFIG };
|
||||
partitionInput = '';
|
||||
configMessage = L.config.loadError;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stdout.trim()) {
|
||||
config = { ...DEFAULT_CONFIG };
|
||||
partitionInput = '';
|
||||
configMessage = L.config.loadDefault;
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseKvConfig(stdout);
|
||||
if (parsed) {
|
||||
config = parsed;
|
||||
partitionInput = parsed.partitions.join(', ');
|
||||
configMessage = L.config.loadSuccess;
|
||||
} else {
|
||||
config = { ...DEFAULT_CONFIG };
|
||||
partitionInput = '';
|
||||
configMessage = L.config.loadError;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
config = { ...DEFAULT_CONFIG };
|
||||
partitionInput = '';
|
||||
configMessage = L.config.loadError;
|
||||
} finally {
|
||||
configLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// save config
|
||||
async function saveConfig() {
|
||||
configSaving = true;
|
||||
configMessage = null;
|
||||
|
||||
try {
|
||||
const partitions = partitionInput
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s);
|
||||
|
||||
const configToSave = { ...config, partitions };
|
||||
|
||||
const content = serializeKvConfig(configToSave);
|
||||
const escapedContent = content.replace(/'/g, "'\\''");
|
||||
|
||||
const shell = `
|
||||
mkdir -p "$(dirname "${CONFIG_PATH}")" && \
|
||||
printf '%s\n' '${escapedContent}' > "${CONFIG_PATH}"
|
||||
`;
|
||||
|
||||
const { errno, stderr } = await exec(shell);
|
||||
|
||||
if (errno !== 0) {
|
||||
console.error(stderr);
|
||||
configMessage = L.config.saveFailed;
|
||||
} else {
|
||||
config = configToSave;
|
||||
configMessage = L.config.saveSuccess;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
configMessage = L.config.saveError;
|
||||
} finally {
|
||||
configSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
// load log
|
||||
async function loadLog() {
|
||||
logLoading = true;
|
||||
logError = null;
|
||||
logContent = '';
|
||||
try {
|
||||
const basePath = config.logfile || DEFAULT_CONFIG.logfile;
|
||||
const fullPath = logSelection === 'current' ? basePath : `${basePath}.old`;
|
||||
|
||||
const { errno, stdout, stderr } = await exec(
|
||||
`[ -f "${fullPath}" ] && cat "${fullPath}" || echo "Log file not found"`
|
||||
);
|
||||
|
||||
if (errno !== 0) {
|
||||
console.error(stderr);
|
||||
logError = L.logs.readFailed + ` (errno=${errno})`;
|
||||
} else {
|
||||
logContent = stdout || '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
logError = L.logs.readException;
|
||||
} finally {
|
||||
logLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// load modules
|
||||
async function loadModules() {
|
||||
modulesLoading = true;
|
||||
modulesError = null;
|
||||
modules = [];
|
||||
try {
|
||||
const moduleDir = config.moduledir || DEFAULT_CONFIG.moduledir;
|
||||
|
||||
const shell = `
|
||||
MOD_DIR="${moduleDir}"
|
||||
[ -d "$MOD_DIR" ] || exit 0
|
||||
for m in "$MOD_DIR"/*; do
|
||||
[ -d "$m" ] || continue
|
||||
[ -d "$m/system" ] || continue
|
||||
name="$(basename "$m")"
|
||||
disabled=0
|
||||
skip=0
|
||||
if [ -e "$m/disable" ] || [ -e "$m/remove" ]; then
|
||||
disabled=1
|
||||
fi
|
||||
if [ -e "$m/skip_mount" ]; then
|
||||
skip=1
|
||||
fi
|
||||
printf '%s|%s|%s\n' "$name" "$disabled" "$skip"
|
||||
done
|
||||
`;
|
||||
|
||||
const { errno, stdout, stderr } = await exec(shell);
|
||||
|
||||
if (errno !== 0) {
|
||||
console.error(stderr);
|
||||
modulesError = L.modules.loadError;
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = stdout
|
||||
.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.length > 0);
|
||||
|
||||
modules = lines.map(line => {
|
||||
const [name, disabledStr, skipStr] = line.split('|');
|
||||
return {
|
||||
name: name || '(unknown)',
|
||||
disabledByFlag: disabledStr === '1',
|
||||
skipMount: skipStr === '1',
|
||||
toggling: false
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
modulesError = L.modules.loadError;
|
||||
} finally {
|
||||
modulesLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleSkipMount(event, mod) {
|
||||
if (mod.disabledByFlag) {
|
||||
event.target.checked = !event.target.checked;
|
||||
return
|
||||
};
|
||||
|
||||
const moduleDir = config.moduledir || DEFAULT_CONFIG.moduledir;
|
||||
const modulePath = `${moduleDir}/${mod.name}`;
|
||||
|
||||
modules = modules.map(m =>
|
||||
m.name === mod.name ? { ...m, toggling: true, error: undefined } : m
|
||||
);
|
||||
|
||||
try {
|
||||
const shell = mod.skipMount
|
||||
? `rm -f "${modulePath}/skip_mount"`
|
||||
: `touch "${modulePath}/skip_mount"`;
|
||||
|
||||
const { errno, stderr } = await exec(shell);
|
||||
|
||||
if (errno !== 0) {
|
||||
console.error(stderr);
|
||||
event.target.checked = !event.target.checked;
|
||||
modules = modules.map(m =>
|
||||
m.name === mod.name ? { ...m, toggling: false, error: L.modules.toggleError } : m
|
||||
);
|
||||
} else {
|
||||
modules = modules.map(m =>
|
||||
m.name === mod.name
|
||||
? { ...m, skipMount: !m.skipMount, toggling: false, error: undefined }
|
||||
: m
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
event.target.checked = !event.target.checked;
|
||||
modules = modules.map(m =>
|
||||
m.name === mod.name ? { ...m, toggling: false, error: L.modules.toggleError } : m
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = window.localStorage.getItem('mm-lang');
|
||||
if (saved && locate[saved]) {
|
||||
lang = saved;
|
||||
}
|
||||
|
||||
// 添加全局点击监听
|
||||
window.addEventListener('click', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
}
|
||||
loadConfig();
|
||||
});
|
||||
// reactive statements for tab changes
|
||||
let configLoaded = false;
|
||||
$: if (activeTab === 'config' && !configLoaded ) {
|
||||
loadConfig().then(() => {
|
||||
configLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
$: if (activeTab === 'log') {
|
||||
loadLog();
|
||||
}
|
||||
|
||||
$: if (activeTab === 'module') {
|
||||
loadModules();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = window.localStorage.getItem('mm-lang');
|
||||
if (saved && locate[saved]) {
|
||||
lang = saved;
|
||||
}
|
||||
|
||||
window.addEventListener('click', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
$: effectiveModuleDir = config.moduledir || DEFAULT_CONFIG.moduledir;
|
||||
</script>
|
||||
|
||||
<div class="app-root">
|
||||
<div class="top-bar">
|
||||
<div class="title">Magic Mount</div>
|
||||
|
||||
<div class="lang-dropdown">
|
||||
<button
|
||||
type="button"
|
||||
class="lang-select"
|
||||
on:click|stopPropagation={toggleLangDropdown}
|
||||
>
|
||||
{availableLanguages.find(l => l.code === lang)?.name || lang}
|
||||
<span class="arrow {langDropdownOpen ? 'open' : ''}">▼</span>
|
||||
</button>
|
||||
|
||||
{#if langDropdownOpen}
|
||||
<div class="lang-menu">
|
||||
{#each availableLanguages as { code, name }}
|
||||
<button
|
||||
type="button"
|
||||
class="lang-option {code === lang ? 'active' : ''}"
|
||||
on:click={() => selectLanguage(code)}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<TopBar />
|
||||
|
||||
<div class="app-main">
|
||||
{#if isSwitching}
|
||||
<div class="tab-overlay">
|
||||
<div class="tab-spinner"></div>
|
||||
</div>
|
||||
<div class="tab-overlay"><div class="tab-spinner"></div></div>
|
||||
{/if}
|
||||
|
||||
{#key activeTab}
|
||||
{#if activeTab === 'config'}
|
||||
<div
|
||||
class="card"
|
||||
in:fade={{ duration: 180 }}
|
||||
out:fade={{ duration: 180 }}
|
||||
>
|
||||
<h2>{L.config.title}</h2>
|
||||
|
||||
{#if configLoading}
|
||||
<p class="hint">Loading...</p>
|
||||
{/if}
|
||||
{#if configMessage}
|
||||
<p class="hint">{configMessage}</p>
|
||||
{/if}
|
||||
|
||||
<div class="field">
|
||||
<label for="verbose-setting">{L.config.verboseLabel}</label>
|
||||
<div class="loglevel-switch" id="verbose-setting" role="group">
|
||||
<button
|
||||
type="button"
|
||||
class="lv-btn {!config.verbose ? 'active' : ''}"
|
||||
on:click|preventDefault={() => (config.verbose = false)}
|
||||
>
|
||||
{L.config.verboseOff}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="lv-btn {config.verbose ? 'active' : ''}"
|
||||
on:click|preventDefault={() => (config.verbose = true)}
|
||||
>
|
||||
{L.config.verboseOn}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="umount-setting">{L.config.umountLabel}</label>
|
||||
<div class="loglevel-switch" id="umount-setting" role="group">
|
||||
<button
|
||||
type="button"
|
||||
class="lv-btn {!config.umount ? 'active' : ''}"
|
||||
on:click|preventDefault={() => (config.umount = false)}
|
||||
>
|
||||
{L.config.umountOff}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="lv-btn {config.umount ? 'active' : ''}"
|
||||
on:click|preventDefault={() => (config.umount = true)}
|
||||
>
|
||||
{L.config.umountOn}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="moduledir-input">{L.config.moduleDir}</label>
|
||||
<input
|
||||
id="moduledir-input"
|
||||
type="text"
|
||||
bind:value={config.moduledir}
|
||||
placeholder={DEFAULT_CONFIG.moduledir}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="tempdir-input">{L.config.tempDir}</label>
|
||||
<input
|
||||
id="tempdir-input"
|
||||
type="text"
|
||||
bind:value={config.tempdir}
|
||||
placeholder="(auto-select if empty)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="mountsource-input">{L.config.mountSource}</label>
|
||||
<input
|
||||
id="mountsource-input"
|
||||
type="text"
|
||||
bind:value={config.mountsource}
|
||||
placeholder={DEFAULT_CONFIG.mountsource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="logfile-input">{L.config.logFile}</label>
|
||||
<input
|
||||
id="logfile-input"
|
||||
type="text"
|
||||
bind:value={config.logfile}
|
||||
placeholder={DEFAULT_CONFIG.logfile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="partitions-input">{L.config.extPartitions}</label>
|
||||
<input
|
||||
id="partitions-input"
|
||||
type="text"
|
||||
bind:value={partitionInput}
|
||||
placeholder="eg. mi_ext,my_stock"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" on:click|preventDefault={loadConfig} disabled={configLoading || configSaving}>
|
||||
{L.config.reload}
|
||||
</button>
|
||||
<button type="button" class="primary" on:click|preventDefault={saveConfig} disabled={configSaving || configLoading}>
|
||||
{configSaving ? 'Saving...' : L.config.save}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="path">{L.config.pathLabel}: {CONFIG_PATH}</p>
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'module'}
|
||||
<div
|
||||
class="card"
|
||||
in:fade={{ duration: 180 }}
|
||||
out:fade={{ duration: 180 }}
|
||||
>
|
||||
<h2>{L.modules.title}</h2>
|
||||
|
||||
<p class="path">{L.modules.basePath}: {effectiveModuleDir}</p>
|
||||
|
||||
{#if modulesError}
|
||||
<p class="error">{modulesError}</p>
|
||||
{/if}
|
||||
{#if modulesLoading && modules.length === 0}
|
||||
<p class="hint">Loading...</p>
|
||||
{/if}
|
||||
|
||||
{#if !modulesLoading && modules.length === 0 && !modulesError}
|
||||
<p class="hint">{L.modules.empty}</p>
|
||||
{/if}
|
||||
|
||||
{#if !modulesLoading && modules.length > 0}
|
||||
<div class="module-list">
|
||||
{#each modules as m (m.name)}
|
||||
<div class="module-row">
|
||||
<div class="module-info">
|
||||
<span class="module-name">{m.name}</span>
|
||||
<label class="switch {m.disabledByFlag ? 'disabled' : ''}">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={m.disabledByFlag ? false : !m.skipMount}
|
||||
disabled={m.disabledByFlag || m.toggling}
|
||||
on:change={(e) => {
|
||||
if (!m.disabledByFlag) toggleSkipMount(e,m);
|
||||
}}
|
||||
/>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
{#if m.error}
|
||||
<div class="error small">{m.error}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="actions" style="margin-top: 20px">
|
||||
<button type="button" on:click|preventDefault={loadModules} disabled={modulesLoading}>
|
||||
{modulesLoading ? 'Loading...' : L.modules.reload}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div
|
||||
class="card"
|
||||
in:fade={{ duration: 180 }}
|
||||
out:fade={{ duration: 180 }}
|
||||
>
|
||||
<h2>{L.logs.title}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label for="log-select">{L.logs.select}</label>
|
||||
<div class="log-select-row">
|
||||
<select id="log-select" bind:value={logSelection}>
|
||||
<option value="current">{L.logs.current}</option>
|
||||
<option value="old">{L.logs.old}</option>
|
||||
</select>
|
||||
|
||||
<button type="button" class="refresh-btn" on:click|preventDefault={loadLog} disabled={logLoading}>
|
||||
{logLoading ? 'Loading...' : L.logs.refresh}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if logError}
|
||||
<p class="error">{logError}</p>
|
||||
{/if}
|
||||
|
||||
<pre class="log-view">{logLoading && !logContent ? 'Loading...' : logContent || L.logs.empty}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
<svelte:component this={tabs[activeTab].component} />
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<div class="bottom-bar">
|
||||
<button
|
||||
type="button"
|
||||
class="tab-btn {activeTab === 'config' ? 'active' : ''}"
|
||||
on:click|preventDefault={() => switchTab('config')}
|
||||
>
|
||||
{L.tabs.config}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="tab-btn {activeTab === 'module' ? 'active' : ''}"
|
||||
on:click|preventDefault={() => switchTab('module')}
|
||||
>
|
||||
{L.tabs.modules}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="tab-btn {activeTab === 'log' ? 'active' : ''}"
|
||||
on:click|preventDefault={() => switchTab('log')}
|
||||
>
|
||||
{L.tabs.logs}
|
||||
</button>
|
||||
{#each Object.entries(tabs) as [key, item]}
|
||||
<button
|
||||
type="button"
|
||||
class="tab-btn {activeTab === key ? 'active' : ''}"
|
||||
on:click={() => switchTab(key)}
|
||||
>
|
||||
{$L[item.label.split('.')[0]][item.label.split('.')[1]]}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Component-specific styles only if needed */
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/* ---------- Base Layout ---------- */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
|
|
@ -186,7 +185,8 @@ body {
|
|||
gap: 4px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
.field label,
|
||||
.field .field-label {
|
||||
font-size: 13px;
|
||||
color: var(--nord4);
|
||||
}
|
||||
|
|
|
|||
126
webui/src/components/ConfigView.svelte
Normal file
126
webui/src/components/ConfigView.svelte
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { L } from '@lib/store.js';
|
||||
import * as utils from '@lib/utils.js';
|
||||
|
||||
let config = { ...utils.DEFAULT_CONFIG };
|
||||
let loading = false;
|
||||
let saving = false;
|
||||
let message = null;
|
||||
let partitionsInput = '';
|
||||
|
||||
const CONFIG_FILE_PATH = '/data/adb/magic_mount/mm.conf';
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
message = null;
|
||||
try {
|
||||
config = await utils.loadConfig();
|
||||
partitionsInput = config.partitions.join(', ');
|
||||
message = $L.config.loadSuccess;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message = $L.config.loadError;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving = true;
|
||||
message = null;
|
||||
try {
|
||||
config.partitions = partitionsInput.split(',').map(s => s.trim()).filter(Boolean);
|
||||
await utils.saveConfig(config);
|
||||
message = $L.config.saveSuccess;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message = $L.config.saveFailed;
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(load);
|
||||
</script>
|
||||
|
||||
<div class="card" in:fade={{ duration: 180 }}>
|
||||
<h2>{$L.config.title}</h2>
|
||||
|
||||
{#if loading}<p class="hint">Loading...</p>{/if}
|
||||
{#if message}<p class="hint">{message}</p>{/if}
|
||||
|
||||
<div class="field">
|
||||
<span class="field-label">{$L.config.verboseLabel}</span>
|
||||
<div class="loglevel-switch">
|
||||
<button type="button" class="lv-btn {!config.verbose ? 'active' : ''}" on:click={() => config.verbose = false}>{$L.config.verboseOff}</button>
|
||||
<button type="button" class="lv-btn {config.verbose ? 'active' : ''}" on:click={() => config.verbose = true}>{$L.config.verboseOn}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="field-label">{$L.config.umountLabel}</span>
|
||||
<div class="loglevel-switch">
|
||||
<button type="button" class="lv-btn {!config.umount ? 'active' : ''}" on:click={() => config.umount = false}>{$L.config.umountOff}</button>
|
||||
<button type="button" class="lv-btn {config.umount ? 'active' : ''}" on:click={() => config.umount = true}>{$L.config.umountOn}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label" for="inp-moduledir">{$L.config.moduleDir}</label>
|
||||
<input
|
||||
id="inp-moduledir"
|
||||
type="text"
|
||||
bind:value={config.moduledir}
|
||||
placeholder={utils.DEFAULT_CONFIG.moduledir}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label" for="inp-tempdir">{$L.config.tempDir}</label>
|
||||
<input
|
||||
id="inp-tempdir"
|
||||
type="text"
|
||||
bind:value={config.tempdir}
|
||||
placeholder="(auto)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label" for="inp-mountsource">{$L.config.mountSource}</label>
|
||||
<input
|
||||
id="inp-mountsource"
|
||||
type="text"
|
||||
bind:value={config.mountsource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label" for="inp-logfile">{$L.config.logFile}</label>
|
||||
<input
|
||||
id="inp-logfile"
|
||||
type="text"
|
||||
bind:value={config.logfile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label" for="inp-partitions">{$L.config.extPartitions}</label>
|
||||
<input
|
||||
id="inp-partitions"
|
||||
type="text"
|
||||
bind:value={partitionsInput}
|
||||
placeholder="eg. mi_ext,my_stock"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" on:click={load} disabled={loading || saving}>{$L.config.reload}</button>
|
||||
<button type="button" class="primary" on:click={save} disabled={loading || saving}>
|
||||
{saving ? 'Saving...' : $L.config.save}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="path">{$L.config.pathLabel}: {CONFIG_FILE_PATH}</p>
|
||||
</div>
|
||||
50
webui/src/components/LogsView.svelte
Normal file
50
webui/src/components/LogsView.svelte
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { L } from '@lib/store.js';
|
||||
import * as utils from '@lib/utils.js';
|
||||
|
||||
let selection = 'current';
|
||||
let content = '';
|
||||
let loading = false;
|
||||
let error = null;
|
||||
let logFile = utils.DEFAULT_CONFIG.logfile;
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
error = null;
|
||||
content = '';
|
||||
try {
|
||||
const cfg = await utils.loadConfig();
|
||||
logFile = cfg.logfile || utils.DEFAULT_CONFIG.logfile;
|
||||
|
||||
content = await utils.fetchLog(logFile, selection === 'old');
|
||||
} catch (e) {
|
||||
error = $L.logs.readFailed;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: selection, load();
|
||||
</script>
|
||||
|
||||
<div class="card" in:fade={{ duration: 180 }}>
|
||||
<h2>{$L.logs.title}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label for="log-select">{$L.logs.select}</label>
|
||||
<div class="log-select-row">
|
||||
<select id="log-select" bind:value={selection}>
|
||||
<option value="current">{$L.logs.current}</option>
|
||||
<option value="old">{$L.logs.old}</option>
|
||||
</select>
|
||||
<button class="refresh-btn" on:click={load} disabled={loading}>
|
||||
{loading ? '...' : $L.logs.refresh}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}<p class="error">{error}</p>{/if}
|
||||
<pre class="log-view">{loading && !content ? 'Loading...' : (content || $L.logs.empty)}</pre>
|
||||
</div>
|
||||
91
webui/src/components/ModulesView.svelte
Normal file
91
webui/src/components/ModulesView.svelte
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { L } from '@lib/store.js';
|
||||
import * as utils from '@lib/utils.js';
|
||||
|
||||
let modules = [];
|
||||
let loading = false;
|
||||
let error = null;
|
||||
// 简单的缓存 moduleDir,实际可以从 Config 获取或者再次读取 Config
|
||||
// 这里为了简单,我们假设使用默认或重新读取一次 Config 来获取路径,或者从 props 传入
|
||||
// 更好的方式是 Store 中存储 config,这里简化为重新获取
|
||||
let moduleDir = utils.DEFAULT_CONFIG.moduledir;
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
// 确保获取最新的 moduleDir
|
||||
const cfg = await utils.loadConfig();
|
||||
moduleDir = cfg.moduledir || utils.DEFAULT_CONFIG.moduledir;
|
||||
modules = await utils.fetchModules(moduleDir);
|
||||
} catch (e) {
|
||||
error = $L.modules.loadError;
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggle(mod) {
|
||||
if (mod.disabledByFlag || mod.toggling) return;
|
||||
|
||||
// Optimistic UI update
|
||||
const oldState = mod.skipMount;
|
||||
mod.toggling = true;
|
||||
modules = modules; // trigger update
|
||||
|
||||
try {
|
||||
await utils.toggleModuleSkip(moduleDir, mod.name, !oldState);
|
||||
mod.skipMount = !oldState;
|
||||
mod.error = undefined;
|
||||
} catch (e) {
|
||||
mod.error = $L.modules.toggleError;
|
||||
mod.skipMount = oldState; // revert
|
||||
} finally {
|
||||
mod.toggling = false;
|
||||
modules = modules; // trigger update
|
||||
}
|
||||
}
|
||||
|
||||
onMount(load);
|
||||
</script>
|
||||
|
||||
<div class="card" in:fade={{ duration: 180 }}>
|
||||
<h2>{$L.modules.title}</h2>
|
||||
<p class="path">{$L.modules.basePath}: {moduleDir}</p>
|
||||
|
||||
{#if error}<p class="error">{error}</p>{/if}
|
||||
{#if loading}<p class="hint">Loading...</p>{/if}
|
||||
|
||||
{#if !loading && modules.length === 0 && !error}
|
||||
<p class="hint">{$L.modules.empty}</p>
|
||||
{/if}
|
||||
|
||||
<div class="module-list">
|
||||
{#each modules as m (m.name)}
|
||||
<div class="module-row">
|
||||
<div class="module-info">
|
||||
<span class="module-name">{m.name}</span>
|
||||
<label class="switch" class:disabled={m.disabledByFlag}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!m.skipMount}
|
||||
disabled={m.disabledByFlag || m.toggling}
|
||||
on:change={() => toggle(m)}
|
||||
/>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
{#if m.error}<div class="error small">{m.error}</div>{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="actions" style="margin-top: 20px">
|
||||
<button on:click={load} disabled={loading}>
|
||||
{loading ? 'Loading...' : $L.modules.reload}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
40
webui/src/components/TopBar.svelte
Normal file
40
webui/src/components/TopBar.svelte
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<script>
|
||||
import { lang, availableLanguages } from '@lib/store.js';
|
||||
|
||||
let dropdownOpen = false;
|
||||
|
||||
function selectLanguage(code) {
|
||||
$lang = code;
|
||||
dropdownOpen = false;
|
||||
}
|
||||
|
||||
function handleClickOutside(event) {
|
||||
if (dropdownOpen && !event.target.closest('.lang-dropdown')) {
|
||||
dropdownOpen = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:click={handleClickOutside} />
|
||||
|
||||
<div class="top-bar">
|
||||
<div class="title">Magic Mount</div>
|
||||
<div class="lang-dropdown">
|
||||
<button type="button" class="lang-select" on:click|stopPropagation={() => dropdownOpen = !dropdownOpen}>
|
||||
{availableLanguages.find(l => l.code === $lang)?.name || $lang}
|
||||
<span class="arrow {dropdownOpen ? 'open' : ''}">▼</span>
|
||||
</button>
|
||||
|
||||
{#if dropdownOpen}
|
||||
<div class="lang-menu">
|
||||
{#each availableLanguages as l}
|
||||
<button
|
||||
class="lang-option {l.code === $lang ? 'active' : ''}"
|
||||
on:click={() => selectLanguage(l.code)}>
|
||||
{l.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
104
webui/src/lib/locate.json
Normal file
104
webui/src/lib/locate.json
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"en": {
|
||||
"lang": {
|
||||
"display": "English"
|
||||
},
|
||||
"tabs": {
|
||||
"config": "Config",
|
||||
"modules": "Modules",
|
||||
"logs": "Logs"
|
||||
},
|
||||
"config": {
|
||||
"title": "Configuration",
|
||||
"verboseLabel": "Log Level",
|
||||
"verboseOff": "Normal",
|
||||
"verboseOn": "Verbose",
|
||||
"umountLabel": "Try Unmount",
|
||||
"umountOff": "Disabled",
|
||||
"umountOn": "Enabled",
|
||||
"moduleDir": "Module Directory",
|
||||
"tempDir": "Temp Directory",
|
||||
"mountSource": "Mount Source",
|
||||
"logFile": "Log File",
|
||||
"extPartitions": "Extended Partitions",
|
||||
"reload": "Reload",
|
||||
"save": "Save",
|
||||
"pathLabel": "Config Path",
|
||||
"loadError": "Failed to load config",
|
||||
"loadDefault": "Using default config",
|
||||
"loadSuccess": "Config loaded",
|
||||
"saveFailed": "Failed to save config",
|
||||
"saveSuccess": "Config saved successfully",
|
||||
"saveError": "Error saving config"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Logs",
|
||||
"select": "Select Log File",
|
||||
"current": "Current Log",
|
||||
"old": "Old Log",
|
||||
"refresh": "Refresh",
|
||||
"empty": "No logs available",
|
||||
"readFailed": "Failed to read log",
|
||||
"readException": "Error reading log"
|
||||
},
|
||||
"modules": {
|
||||
"title": "Modules",
|
||||
"basePath": "Module Directory",
|
||||
"empty": "No modules found",
|
||||
"reload": "Reload",
|
||||
"loadError": "Failed to load modules",
|
||||
"toggleError": "Failed to toggle module"
|
||||
}
|
||||
},
|
||||
"zh": {
|
||||
"lang": {
|
||||
"display": "中文"
|
||||
},
|
||||
"tabs": {
|
||||
"config": "配置",
|
||||
"modules": "模块",
|
||||
"logs": "日志"
|
||||
},
|
||||
"config": {
|
||||
"title": "配置",
|
||||
"verboseLabel": "日志级别",
|
||||
"verboseOff": "普通",
|
||||
"verboseOn": "详细",
|
||||
"umountLabel": "尝试卸载",
|
||||
"umountOff": "禁用",
|
||||
"umountOn": "启用",
|
||||
"moduleDir": "模块目录",
|
||||
"tempDir": "临时目录",
|
||||
"mountSource": "挂载源",
|
||||
"logFile": "日志文件",
|
||||
"extPartitions": "扩展分区",
|
||||
"reload": "重新加载",
|
||||
"save": "保存",
|
||||
"pathLabel": "配置路径",
|
||||
"loadError": "加载配置失败",
|
||||
"loadDefault": "使用默认配置",
|
||||
"loadSuccess": "配置已加载",
|
||||
"saveFailed": "保存配置失败",
|
||||
"saveSuccess": "配置保存成功",
|
||||
"saveError": "保存配置出错"
|
||||
},
|
||||
"logs": {
|
||||
"title": "日志",
|
||||
"select": "选择日志文件",
|
||||
"current": "当前日志",
|
||||
"old": "旧日志",
|
||||
"refresh": "刷新",
|
||||
"empty": "暂无日志",
|
||||
"readFailed": "读取日志失败",
|
||||
"readException": "读取日志出错"
|
||||
},
|
||||
"modules": {
|
||||
"title": "模块",
|
||||
"basePath": "模块目录",
|
||||
"empty": "未找到模块",
|
||||
"reload": "重新加载",
|
||||
"loadError": "加载模块失败",
|
||||
"toggleError": "切换模块失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
74
webui/src/lib/mock.js
Normal file
74
webui/src/lib/mock.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
const delay = (ms = 500) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
export const MockAPI = {
|
||||
async loadConfig(defaultConfig) {
|
||||
console.log('[Dev] Load Mock config...');
|
||||
await delay();
|
||||
|
||||
return {
|
||||
...defaultConfig,
|
||||
moduledir: '/data/adb/modules',
|
||||
partitions: ['mi_ext', 'my_stock'],
|
||||
verbose: true,
|
||||
umount: false
|
||||
};
|
||||
},
|
||||
|
||||
async saveConfig(config) {
|
||||
console.log('[Dev] Save Mock config:', config);
|
||||
await delay(800);
|
||||
|
||||
if (Math.random() > 0.9) {
|
||||
throw new Error("Save error:IO error");
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
async fetchLog(path, isOld) {
|
||||
console.log(`[Dev] Fetch log: ${path} (old=${isOld})`);
|
||||
await delay();
|
||||
|
||||
if (isOld) {
|
||||
return `[Old Log]
|
||||
Nothing interesting here.
|
||||
Just some history data.`;
|
||||
}
|
||||
|
||||
return `[INFO] main.c:72: Loading config file: /data/adb/magic_mount/mm.conf
|
||||
[DEBUG] module_tree.c:226: extra_partition_register: processing 'my_stock' (len=8)
|
||||
[INFO] module_tree.c:259: extra_partition_register: success added 'my_stock' (total: 1 partitions)
|
||||
[DEBUG] main.c:152: Added extra partition: my_stock
|
||||
[DEBUG] module_tree.c:226: extra_partition_register: processing 'sys' (len=3)
|
||||
[WARN] module_tree.c:244: extra_partition_register: rejected 'sys' (blacklisted)
|
||||
[DEBUG] main.c:152: Added extra partition: test
|
||||
[INFO] utils.c:283: auto tempdir selected: /mnt/vendor/.magic_mount (from /mnt/vendor)
|
||||
[INFO] main.c:336: Magic Mount mock Starting
|
||||
[INFO] main.c:337: Configuration:
|
||||
[INFO] main.c:338: Module directory: /data/adb/modules
|
||||
[INFO] main.c:339: Temp directory: /mnt/vendor/.magic_mount
|
||||
[INFO] main.c:340: Mount source: KSU
|
||||
[INFO] main.c:341: Log level: DEBUG
|
||||
[INFO] main.c:343: Extra partitions: 1
|
||||
[INFO] main.c:345: - custom_part
|
||||
[INFO] module_tree.c:663: build_mount_tree: module_dir=/data/adb/modules`;
|
||||
},
|
||||
|
||||
async fetchModules(moduleDir) {
|
||||
console.log(`[Dev] Scan modules: ${moduleDir}`);
|
||||
await delay();
|
||||
|
||||
return [
|
||||
{ name: 'mod1', disabledByFlag: false, skipMount: false },
|
||||
{ name: 'mod2', disabledByFlag: false, skipMount: true },
|
||||
{ name: 'mod3', disabledByFlag: true, skipMount: false },
|
||||
{ name: 'mod4', disabledByFlag: false, skipMount: false },
|
||||
{ name: 'mod5', disabledByFlag: false, skipMount: false }
|
||||
];
|
||||
},
|
||||
|
||||
async toggleModuleSkip(moduleDir, modName, shouldSkip) {
|
||||
console.log(`[Dev] Switch module ${modName} skip_mount=${shouldSkip}`);
|
||||
await delay(300);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
24
webui/src/lib/store.js
Normal file
24
webui/src/lib/store.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { writable, derived } from 'svelte/store';
|
||||
import locate from './locate.json';
|
||||
|
||||
const getInitialLang = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.localStorage.getItem('mm-lang') || 'en';
|
||||
}
|
||||
return 'en';
|
||||
};
|
||||
|
||||
export const lang = writable(getInitialLang());
|
||||
|
||||
lang.subscribe(val => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem('mm-lang', val);
|
||||
}
|
||||
});
|
||||
|
||||
export const L = derived(lang, ($lang) => locate[$lang] || locate['en']);
|
||||
|
||||
export const availableLanguages = Object.keys(locate).map(code => ({
|
||||
code,
|
||||
name: locate[code]?.lang?.display || code.toUpperCase()
|
||||
}));
|
||||
138
webui/src/lib/utils.js
Normal file
138
webui/src/lib/utils.js
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { exec } from 'kernelsu';
|
||||
import { MockAPI } from './mock.js';
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
moduledir: '/data/adb/modules',
|
||||
tempdir: '',
|
||||
mountsource: 'KSU',
|
||||
logfile: '/data/adb/magic_mount/mm.log',
|
||||
verbose: false,
|
||||
umount: true,
|
||||
partitions: []
|
||||
};
|
||||
|
||||
const CONFIG_PATH = '/data/adb/magic_mount/mm.conf';
|
||||
|
||||
function isTrueValue(v) {
|
||||
const s = String(v).trim().toLowerCase();
|
||||
return ['1', 'true', 'yes', 'on'].includes(s);
|
||||
}
|
||||
|
||||
export async function loadConfig() {
|
||||
if (import.meta.env.DEV) {
|
||||
return MockAPI.loadConfig(DEFAULT_CONFIG);
|
||||
}
|
||||
|
||||
const { errno, stdout } = await exec(
|
||||
`[ -f "${CONFIG_PATH}" ] && cat "${CONFIG_PATH}" || echo ""`
|
||||
);
|
||||
|
||||
if (errno !== 0) throw new Error("Read config failed");
|
||||
if (!stdout.trim()) return { ...DEFAULT_CONFIG };
|
||||
|
||||
const result = { ...DEFAULT_CONFIG };
|
||||
const lines = stdout.split('\n');
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
const [key, ...vals] = line.split('=');
|
||||
if (!key || !vals.length) continue;
|
||||
const value = vals.join('=').trim();
|
||||
|
||||
switch (key.trim()) {
|
||||
case 'module_dir': result.moduledir = value; break;
|
||||
case 'temp_dir': result.tempdir = value; break;
|
||||
case 'mount_source': result.mountsource = value; break;
|
||||
case 'log_file': result.logfile = value; break;
|
||||
case 'debug': result.verbose = isTrueValue(value); break;
|
||||
case 'umount': result.umount = isTrueValue(value); break;
|
||||
case 'partitions':
|
||||
result.partitions = value.split(',').map(s => s.trim()).filter(Boolean);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function saveConfig(cfg) {
|
||||
if (import.meta.env.DEV) {
|
||||
return MockAPI.saveConfig(cfg);
|
||||
}
|
||||
|
||||
const lines = ['# Magic Mount Configuration File', '# Generated by Web UI', ''];
|
||||
lines.push(`module_dir=${cfg.moduledir || DEFAULT_CONFIG.moduledir}`);
|
||||
if (cfg.tempdir) lines.push(`temp_dir=${cfg.tempdir}`);
|
||||
lines.push(`mount_source=${cfg.mountsource || DEFAULT_CONFIG.mountsource}`);
|
||||
if (cfg.logfile) lines.push(`log_file=${cfg.logfile}`);
|
||||
lines.push(`debug=${cfg.verbose ? 'true' : 'false'}`);
|
||||
lines.push(`umount=${cfg.umount ? 'true' : 'false'}`);
|
||||
if (cfg.partitions.length > 0) lines.push(`partitions=${cfg.partitions.join(',')}`);
|
||||
|
||||
const content = lines.join('\n').replace(/'/g, "'\\''");
|
||||
const shell = `mkdir -p "$(dirname "${CONFIG_PATH}")" && printf '%s\n' '${content}' > "${CONFIG_PATH}"`;
|
||||
|
||||
const { errno, stderr } = await exec(shell);
|
||||
if (errno !== 0) throw new Error(stderr || "Save failed");
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function fetchLog(path, isOld = false) {
|
||||
if (import.meta.env.DEV) {
|
||||
return MockAPI.fetchLog(path, isOld);
|
||||
}
|
||||
|
||||
const fullPath = isOld ? `${path}.old` : path;
|
||||
const { errno, stdout, stderr } = await exec(
|
||||
`[ -f "${fullPath}" ] && cat "${fullPath}" || echo ""`
|
||||
);
|
||||
if (errno !== 0) throw new Error(stderr);
|
||||
return stdout || '';
|
||||
}
|
||||
|
||||
export async function fetchModules(moduleDir) {
|
||||
if (import.meta.env.DEV) {
|
||||
const mocks = await MockAPI.fetchModules(moduleDir);
|
||||
return mocks.map(m => ({ ...m, toggling: false }));
|
||||
}
|
||||
|
||||
const shell = `
|
||||
MOD_DIR="${moduleDir}"
|
||||
[ -d "$MOD_DIR" ] || exit 0
|
||||
for m in "$MOD_DIR"/*; do
|
||||
[ -d "$m" ] || continue
|
||||
[ -d "$m/system" ] || continue
|
||||
name="$(basename "$m")"
|
||||
disabled=0; skip=0
|
||||
[ -e "$m/disable" ] || [ -e "$m/remove" ] && disabled=1
|
||||
[ -e "$m/skip_mount" ] && skip=1
|
||||
printf '%s|%s|%s\n' "$name" "$disabled" "$skip"
|
||||
done
|
||||
`;
|
||||
|
||||
const { errno, stdout } = await exec(shell);
|
||||
if (errno !== 0) throw new Error("List modules failed");
|
||||
|
||||
return stdout.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(Boolean)
|
||||
.map(line => {
|
||||
const [name, dis, skip] = line.split('|');
|
||||
return {
|
||||
name,
|
||||
disabledByFlag: dis === '1',
|
||||
skipMount: skip === '1',
|
||||
toggling: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function toggleModuleSkip(moduleDir, modName, shouldSkip) {
|
||||
if (import.meta.env.DEV) {
|
||||
return MockAPI.toggleModuleSkip(moduleDir, modName, shouldSkip);
|
||||
}
|
||||
|
||||
const path = `${moduleDir}/${modName}/skip_mount`;
|
||||
const cmd = shouldSkip ? `touch "${path}"` : `rm -f "${path}"`;
|
||||
const { errno, stderr } = await exec(cmd);
|
||||
if (errno !== 0) throw new Error(stderr);
|
||||
}
|
||||
|
|
@ -1,5 +1,10 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
|
|
@ -7,5 +12,12 @@ export default defineConfig({
|
|||
build: {
|
||||
outDir: '../template/webroot',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@lib': path.resolve(__dirname, './src/lib'),
|
||||
'@comp': path.resolve(__dirname, './src/components')
|
||||
}
|
||||
},
|
||||
plugins: [svelte()],
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue