Compare commits
4 Commits
5ae3a6e584
...
3da4d8afc3
| Author | SHA1 | Date | |
|---|---|---|---|
| 3da4d8afc3 | |||
| 4fe530352d | |||
| 8cea8a40d0 | |||
| 33fe06ca9f |
|
|
@ -319,7 +319,7 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
||||||
className="flex-1 flex flex-col items-center gap-1 py-2"
|
className="flex-1 flex flex-col items-center gap-1 py-2"
|
||||||
>
|
>
|
||||||
<Wrench className="w-5 h-5 text-white" />
|
<Wrench className="w-5 h-5 text-white" />
|
||||||
<span className="text-xs text-white">Tools</span>
|
<span className="text-xs text-white">System</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,17 @@ export default function DeviceDetailOverlay({
|
||||||
updateDeviceSetting([...path, 'url'], file);
|
updateDeviceSetting([...path, 'url'], file);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isOutsideBase = (url: string, baseUrl: string): boolean => {
|
||||||
|
if (!baseUrl || !url.startsWith('/')) return false;
|
||||||
|
const normalizedBase = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/';
|
||||||
|
return url !== baseUrl && !url.startsWith(normalizedBase);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearBaseAndCache = (dev: any) => {
|
||||||
|
if ('base_url' in dev) dev.base_url = '';
|
||||||
|
if ('cache' in dev) dev.cache = '';
|
||||||
|
};
|
||||||
|
|
||||||
const handleFileSelect = async (selectedPath: string) => {
|
const handleFileSelect = async (selectedPath: string) => {
|
||||||
const devicePath = getDevicePath();
|
const devicePath = getDevicePath();
|
||||||
if (selectedPath.toLowerCase().endsWith('.lst')) {
|
if (selectedPath.toLowerCase().endsWith('.lst')) {
|
||||||
|
|
@ -168,6 +179,7 @@ export default function DeviceDetailOverlay({
|
||||||
const newConfig = JSON.parse(JSON.stringify(config));
|
const newConfig = JSON.parse(JSON.stringify(config));
|
||||||
let dev = newConfig;
|
let dev = newConfig;
|
||||||
for (const k of devicePath) dev = dev[k];
|
for (const k of devicePath) dev = dev[k];
|
||||||
|
if (isOutsideBase(files[0], dev.base_url || '')) clearBaseAndCache(dev);
|
||||||
dev.url = files[0];
|
dev.url = files[0];
|
||||||
dev.media_set = files;
|
dev.media_set = files;
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
|
|
@ -178,6 +190,7 @@ export default function DeviceDetailOverlay({
|
||||||
const newConfig = JSON.parse(JSON.stringify(config));
|
const newConfig = JSON.parse(JSON.stringify(config));
|
||||||
let dev = newConfig;
|
let dev = newConfig;
|
||||||
for (const k of devicePath) dev = dev[k];
|
for (const k of devicePath) dev = dev[k];
|
||||||
|
if (isOutsideBase(selectedPath, dev.base_url || '')) clearBaseAndCache(dev);
|
||||||
dev.url = selectedPath;
|
dev.url = selectedPath;
|
||||||
delete dev.media_set;
|
delete dev.media_set;
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
|
|
@ -342,8 +355,14 @@ export default function DeviceDetailOverlay({
|
||||||
type="text"
|
type="text"
|
||||||
value={deviceData.url}
|
value={deviceData.url}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const path = getDevicePath();
|
const newUrl = e.target.value;
|
||||||
updateDeviceSetting([...path, 'url'], e.target.value);
|
const devicePath = getDevicePath();
|
||||||
|
const newConfig = JSON.parse(JSON.stringify(config));
|
||||||
|
let dev = newConfig;
|
||||||
|
for (const k of devicePath) dev = dev[k];
|
||||||
|
if (isOutsideBase(newUrl, dev.base_url || '')) clearBaseAndCache(dev);
|
||||||
|
dev.url = newUrl;
|
||||||
|
setConfig(newConfig);
|
||||||
}}
|
}}
|
||||||
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
|
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Printer, HardDrive, Network, Box, ChevronRight, RefreshCw } from 'lucide-react';
|
import { Printer, HardDrive, Network, Box, ChevronRight, RefreshCw, FolderOpen } from 'lucide-react';
|
||||||
import DeviceDetailOverlay from './DeviceDetailOverlay';
|
import DeviceDetailOverlay from './DeviceDetailOverlay';
|
||||||
|
import MediaBrowser from './MediaBrowser';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useWs } from '../ws';
|
import { useWs } from '../ws';
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
|
||||||
};
|
};
|
||||||
const [selectedDeviceIndex, setSelectedDeviceIndex] = useState<number | null>(null);
|
const [selectedDeviceIndex, setSelectedDeviceIndex] = useState<number | null>(null);
|
||||||
const [isScanning, setIsScanning] = useState(false);
|
const [isScanning, setIsScanning] = useState(false);
|
||||||
|
const [showCassetteUrlBrowser, setShowCassetteUrlBrowser] = useState(false);
|
||||||
|
|
||||||
const hardware = config.hardware || {};
|
const hardware = config.hardware || {};
|
||||||
const modem = config.modem || {};
|
const modem = config.modem || {};
|
||||||
|
|
@ -255,7 +257,7 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h2 className="text-sm text-neutral-500">All Devices</h2>
|
<h2 className="text-sm text-neutral-500">IEC Devices</h2>
|
||||||
<button
|
<button
|
||||||
onClick={rescanBus}
|
onClick={rescanBus}
|
||||||
disabled={isScanning}
|
disabled={isScanning}
|
||||||
|
|
@ -325,63 +327,6 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Hardware ── */}
|
|
||||||
<h2 className="text-sm text-neutral-500 pt-4">Hardware</h2>
|
|
||||||
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
|
||||||
<div className="p-4 flex items-center justify-between">
|
|
||||||
<label className="text-sm text-neutral-500">PS/2</label>
|
|
||||||
<button
|
|
||||||
onClick={() => updateSetting(['hardware', 'ps2'], hardware.ps2 ? 0 : 1)}
|
|
||||||
className={`relative w-12 h-6 rounded-full transition-colors ${hardware.ps2 ? 'bg-blue-600' : 'bg-neutral-300'}`}
|
|
||||||
>
|
|
||||||
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${hardware.ps2 ? 'translate-x-6' : 'translate-x-0.5'}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 flex items-center justify-between">
|
|
||||||
<label className="text-sm text-neutral-500">User Port Enabled</label>
|
|
||||||
<button
|
|
||||||
onClick={() => updateSetting(['hardware', 'userport', 'enabled'], hardware.userport?.enabled ? 0 : 1)}
|
|
||||||
className={`relative w-12 h-6 rounded-full transition-colors ${hardware.userport?.enabled ? 'bg-blue-600' : 'bg-neutral-300'}`}
|
|
||||||
>
|
|
||||||
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${hardware.userport?.enabled ? 'translate-x-6' : 'translate-x-0.5'}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="p-4">
|
|
||||||
<label className="text-sm text-neutral-500 block mb-2">User Port Mode</label>
|
|
||||||
<select
|
|
||||||
value={hardware.userport?.mode?.split('|')[0] || 'serial'}
|
|
||||||
onChange={(e) => updateSetting(['hardware', 'userport', 'mode'], e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
|
|
||||||
>
|
|
||||||
<option value="serial">Serial</option>
|
|
||||||
<option value="parallel">Parallel</option>
|
|
||||||
<option value="IEEE-488">IEEE-488</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Modem ── */}
|
|
||||||
<h2 className="text-sm text-neutral-500 pt-4">Modem</h2>
|
|
||||||
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
|
||||||
<div className="p-4 flex items-center justify-between">
|
|
||||||
<label className="text-sm text-neutral-500">Modem Enabled</label>
|
|
||||||
<button
|
|
||||||
onClick={() => updateSetting(['modem', 'modem_enabled'], modem.modem_enabled ? 0 : 1)}
|
|
||||||
className={`relative w-12 h-6 rounded-full transition-colors ${modem.modem_enabled ? 'bg-blue-600' : 'bg-neutral-300'}`}
|
|
||||||
>
|
|
||||||
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${modem.modem_enabled ? 'translate-x-6' : 'translate-x-0.5'}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 flex items-center justify-between">
|
|
||||||
<label className="text-sm text-neutral-500">Sniffer Enabled</label>
|
|
||||||
<button
|
|
||||||
onClick={() => updateSetting(['modem', 'sniffer_enabled'], modem.sniffer_enabled ? 0 : 1)}
|
|
||||||
className={`relative w-12 h-6 rounded-full transition-colors ${modem.sniffer_enabled ? 'bg-blue-600' : 'bg-neutral-300'}`}
|
|
||||||
>
|
|
||||||
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${modem.sniffer_enabled ? 'translate-x-6' : 'translate-x-0.5'}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Cassette ── */}
|
{/* ── Cassette ── */}
|
||||||
<h2 className="text-sm text-neutral-500 pt-4">Cassette</h2>
|
<h2 className="text-sm text-neutral-500 pt-4">Cassette</h2>
|
||||||
|
|
@ -409,27 +354,46 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── BOIP ── */}
|
{/* ── Hardware ── */}
|
||||||
<h2 className="text-sm text-neutral-500 pt-4">BOIP</h2>
|
<h2 className="text-sm text-neutral-500 pt-4">User Port</h2>
|
||||||
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
||||||
<div className="p-4 flex items-center justify-between">
|
<div className="p-4 flex items-center justify-between">
|
||||||
<label className="text-sm text-neutral-500">Enabled</label>
|
<label className="text-sm text-neutral-500">User Port Enabled</label>
|
||||||
<button
|
<button
|
||||||
onClick={() => updateSetting(['boip', 'enabled'], boip.enabled ? 0 : 1)}
|
onClick={() => updateSetting(['hardware', 'userport', 'enabled'], hardware.userport?.enabled ? 0 : 1)}
|
||||||
className={`relative w-12 h-6 rounded-full transition-colors ${boip.enabled ? 'bg-blue-600' : 'bg-neutral-300'}`}
|
className={`relative w-12 h-6 rounded-full transition-colors ${hardware.userport?.enabled ? 'bg-blue-600' : 'bg-neutral-300'}`}
|
||||||
>
|
>
|
||||||
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${boip.enabled ? 'translate-x-6' : 'translate-x-0.5'}`} />
|
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${hardware.userport?.enabled ? 'translate-x-6' : 'translate-x-0.5'}`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<label className="text-sm text-neutral-500 block mb-2">Host</label>
|
<label className="text-sm text-neutral-500 block mb-2">User Port Mode</label>
|
||||||
<input type="text" value={boip.host || ''} onChange={(e) => updateSetting(['boip', 'host'], e.target.value)} className="w-full px-3 py-2 border border-neutral-300 rounded-lg" />
|
<select
|
||||||
</div>
|
value={hardware.userport?.mode?.split('|')[0] || 'serial'}
|
||||||
<div className="p-4">
|
onChange={(e) => updateSetting(['hardware', 'userport', 'mode'], e.target.value)}
|
||||||
<label className="text-sm text-neutral-500 block mb-2">Port</label>
|
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
|
||||||
<input type="text" value={boip.port || ''} onChange={(e) => updateSetting(['boip', 'port'], e.target.value)} className="w-full px-3 py-2 border border-neutral-300 rounded-lg" />
|
>
|
||||||
|
<option value="serial">Serial</option>
|
||||||
|
<option value="parallel">Parallel</option>
|
||||||
|
<option value="wic64">WiC64</option>
|
||||||
|
<option value="IEEE-488">IEEE-488</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Hardware ── */}
|
||||||
|
{/* <h2 className="text-sm text-neutral-500 pt-4">Other Hardware</h2>
|
||||||
|
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
||||||
|
<div className="p-4 flex items-center justify-between">
|
||||||
|
<label className="text-sm text-neutral-500">PS/2</label>
|
||||||
|
<button
|
||||||
|
onClick={() => updateSetting(['hardware', 'ps2'], hardware.ps2 ? 0 : 1)}
|
||||||
|
className={`relative w-12 h-6 rounded-full transition-colors ${hardware.ps2 ? 'bg-blue-600' : 'bg-neutral-300'}`}
|
||||||
|
>
|
||||||
|
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${hardware.ps2 ? 'translate-x-6' : 'translate-x-0.5'}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ interface IECPageProps {
|
||||||
|
|
||||||
export default function IECPage({ config, setConfig }: IECPageProps) {
|
export default function IECPage({ config, setConfig }: IECPageProps) {
|
||||||
const [showMediaBrowser, setShowMediaBrowser] = useState(false);
|
const [showMediaBrowser, setShowMediaBrowser] = useState(false);
|
||||||
|
const [driveRomBrowsingKey, setDriveRomBrowsingKey] = useState<string | null>(null);
|
||||||
const updateSetting = (path: string[], value: any) => {
|
const updateSetting = (path: string[], value: any) => {
|
||||||
const newConfig = JSON.parse(JSON.stringify(config));
|
const newConfig = JSON.parse(JSON.stringify(config));
|
||||||
let current = newConfig;
|
let current = newConfig;
|
||||||
|
|
@ -60,6 +61,22 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 flex items-center justify-between">
|
||||||
|
<label className="text-sm text-neutral-500">VDrive Mode</label>
|
||||||
|
<button
|
||||||
|
onClick={() => updateSetting(['iec', 'vdrive_mode'], iec.vdrive_mode ? 0 : 1)}
|
||||||
|
className={`relative w-12 h-6 rounded-full transition-colors ${
|
||||||
|
iec.vdrive_mode ? 'bg-blue-600' : 'bg-neutral-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
|
||||||
|
iec.vdrive_mode ? 'translate-x-6' : 'translate-x-0.5'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<label className="text-sm text-neutral-500 block mb-2">Boot Disk</label>
|
<label className="text-sm text-neutral-500 block mb-2">Boot Disk</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|
@ -91,6 +108,67 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-sm text-neutral-500 pt-4">Drive ROM</h2>
|
||||||
|
|
||||||
|
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
||||||
|
<div className="p-4 flex items-center justify-between">
|
||||||
|
<label className="text-sm text-neutral-500">Enabled</label>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const newConfig = JSON.parse(JSON.stringify(config));
|
||||||
|
const dr = newConfig.iec.drive_rom ?? {};
|
||||||
|
const current = dr.enabled ?? dr.auto ?? 0;
|
||||||
|
dr.enabled = current ? 0 : 1;
|
||||||
|
delete dr.auto;
|
||||||
|
newConfig.iec.drive_rom = dr;
|
||||||
|
setConfig(newConfig);
|
||||||
|
}}
|
||||||
|
className={`relative w-12 h-6 rounded-full transition-colors ${
|
||||||
|
(iec.drive_rom?.enabled ?? iec.drive_rom?.auto) ? 'bg-blue-600' : 'bg-neutral-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
|
||||||
|
(iec.drive_rom?.enabled ?? iec.drive_rom?.auto) ? 'translate-x-6' : 'translate-x-0.5'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{(['default', 'd64', 'd71', 'd81'] as const).map((key) => (
|
||||||
|
<div key={key} className="p-4">
|
||||||
|
<label className="text-sm text-neutral-500 block mb-2">
|
||||||
|
{key === 'default' ? 'Default' : key.toUpperCase()}
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={iec.drive_rom?.[key] || ''}
|
||||||
|
onChange={(e) => updateSetting(['iec', 'drive_rom', key], e.target.value)}
|
||||||
|
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setDriveRomBrowsingKey(key)}
|
||||||
|
className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-neutral-100"
|
||||||
|
title="Browse files"
|
||||||
|
>
|
||||||
|
<FolderOpen className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{driveRomBrowsingKey && (
|
||||||
|
<MediaBrowser
|
||||||
|
currentPath={iec.drive_rom?.[driveRomBrowsingKey] || '/'}
|
||||||
|
onSelect={(path) => {
|
||||||
|
updateSetting(['iec', 'drive_rom', driveRomBrowsingKey], path);
|
||||||
|
setDriveRomBrowsingKey(null);
|
||||||
|
}}
|
||||||
|
onClose={() => setDriveRomBrowsingKey(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<h2 className="text-sm text-neutral-500 pt-4">Directory Settings</h2>
|
<h2 className="text-sm text-neutral-500 pt-4">Directory Settings</h2>
|
||||||
|
|
||||||
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,15 @@
|
||||||
"iec": {
|
"iec": {
|
||||||
"enabled": 1,
|
"enabled": 1,
|
||||||
"vic20_mode": 0,
|
"vic20_mode": 0,
|
||||||
|
"vdrive": 0,
|
||||||
"boot_disk": "/autoboot[.PRG|.D64|.D81|etc]",
|
"boot_disk": "/autoboot[.PRG|.D64|.D81|etc]",
|
||||||
|
"rom": {
|
||||||
|
"enabled": 1,
|
||||||
|
"default": "dos1541|dos1541.jd|dos1541ii|dos1541ii.jd|dos1571|dos1571.jd|dos1581|dos1581.jd",
|
||||||
|
"d64": "dos1541|dos1541.jd|dos1541ii|dos1541ii.jd",
|
||||||
|
"d71": "dos1571|dos1571.jd",
|
||||||
|
"d81": "dos1581|dos1581.jd"
|
||||||
|
},
|
||||||
"directory": {
|
"directory": {
|
||||||
"force_0801": 1,
|
"force_0801": 1,
|
||||||
"nfo_header": 1,
|
"nfo_header": 1,
|
||||||
|
|
@ -137,14 +145,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"drive": {
|
"drive": {
|
||||||
"vdrive": 0,
|
|
||||||
"rom": {
|
|
||||||
"auto": 1,
|
|
||||||
"default": "dos1541|dos1541.jd|dos1541ii|dos1541ii.jd|dos1571|dos1571.jd|dos1581|dos1581.jd",
|
|
||||||
"d64": "dos1541|dos1541.jd|dos1541ii|dos1541ii.jd",
|
|
||||||
"d71": "dos1571|dos1571.jd",
|
|
||||||
"d81": "dos1581|dos1581.jd"
|
|
||||||
},
|
|
||||||
"8": {
|
"8": {
|
||||||
"enabled": 1,
|
"enabled": 1,
|
||||||
"url": "/sd",
|
"url": "/sd",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user