Compare commits
No commits in common. "290cdb8ae94366e4011fec8d32b85e4bfc8fdc7a" and "4b2456859f692756f9549fdb3773880af4a7a79a" have entirely different histories.
290cdb8ae9
...
4b2456859f
|
|
@ -57,8 +57,6 @@ export default function App() {
|
||||||
const [showSearch, setShowSearch] = useState(false);
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
const [devicesOpenId, setDevicesOpenId] = useState<string | null>(null);
|
const [devicesOpenId, setDevicesOpenId] = useState<string | null>(null);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [fileManagerInitialPath, setFileManagerInitialPath] = useState<string | undefined>(undefined);
|
|
||||||
const [fileManagerReturnPage, setFileManagerReturnPage] = useState<Page>('apps');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = 'save-status';
|
const id = 'save-status';
|
||||||
|
|
@ -101,7 +99,7 @@ export default function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const pages = {
|
const pages = {
|
||||||
status: <StatusPage config={config} setConfig={setConfig} onOpenFileManager={(path) => { setFileManagerInitialPath(path); setFileManagerReturnPage('status'); setCurrentPage('file-manager'); }} />,
|
status: <StatusPage config={config} setConfig={setConfig} />,
|
||||||
devices: <DevicesPage config={config} setConfig={setConfig} openDeviceId={devicesOpenId} onClearOpenDevice={() => setDevicesOpenId(null)} />,
|
devices: <DevicesPage config={config} setConfig={setConfig} openDeviceId={devicesOpenId} onClearOpenDevice={() => setDevicesOpenId(null)} />,
|
||||||
iec: <IECPage config={config} setConfig={setConfig} />,
|
iec: <IECPage config={config} setConfig={setConfig} />,
|
||||||
network: <NetworkPage config={config} setConfig={setConfig} />,
|
network: <NetworkPage config={config} setConfig={setConfig} />,
|
||||||
|
|
@ -116,7 +114,7 @@ export default function App() {
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold mb-4 text-blue-700">Management</h2>
|
<h2 className="text-lg font-semibold mb-4 text-blue-700">Management</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
<AppCard icon={<Folder className="w-7 h-7" />} label="Media Manager" onClick={() => { setFileManagerInitialPath(undefined); setCurrentPage('file-manager'); }} />
|
<AppCard icon={<Folder className="w-7 h-7" />} label="Media Manager" onClick={() => setCurrentPage('file-manager')} />
|
||||||
<AppCard icon={<Printer className="w-7 h-7" />} label="Print Manager" onClick={() => setCurrentPage('print-manager')} />
|
<AppCard icon={<Printer className="w-7 h-7" />} label="Print Manager" onClick={() => setCurrentPage('print-manager')} />
|
||||||
<AppCard icon={<Terminal className="w-7 h-7" />} label="Serial Console" onClick={() => setCurrentPage('serial-console')} />
|
<AppCard icon={<Terminal className="w-7 h-7" />} label="Serial Console" onClick={() => setCurrentPage('serial-console')} />
|
||||||
<AppCard icon={<Link className="w-7 h-7" />} label="Short Codes" onClick={() => setCurrentPage('serial-console')} />
|
<AppCard icon={<Link className="w-7 h-7" />} label="Short Codes" onClick={() => setCurrentPage('serial-console')} />
|
||||||
|
|
@ -165,8 +163,7 @@ export default function App() {
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
'file-manager': <MediaManager
|
'file-manager': <MediaManager
|
||||||
initialPath={fileManagerInitialPath}
|
onBack={() => setCurrentPage('apps')}
|
||||||
onBack={() => setCurrentPage(fileManagerReturnPage)}
|
|
||||||
config={config}
|
config={config}
|
||||||
setConfig={setConfig}
|
setConfig={setConfig}
|
||||||
onNavigateToDevice={(id) => { setCurrentPage('devices'); setDevicesOpenId(id); }}
|
onNavigateToDevice={(id) => { setCurrentPage('devices'); setDevicesOpenId(id); }}
|
||||||
|
|
@ -234,7 +231,7 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
||||||
return (
|
return (
|
||||||
<WsProvider>
|
<WsProvider>
|
||||||
<div className="size-full flex flex-col bg-neutral-50">
|
<div className="size-full flex flex-col bg-neutral-50">
|
||||||
<Toaster position="bottom-center" offset="calc(4rem + env(safe-area-inset-bottom))" />
|
<Toaster position="top-center" />
|
||||||
<header className="bg-[#4d4d4d] px-0 py-0 flex-shrink-0" style={{ paddingTop: 'env(safe-area-inset-top)' }}>
|
<header className="bg-[#4d4d4d] px-0 py-0 flex-shrink-0" style={{ paddingTop: 'env(safe-area-inset-top)' }}>
|
||||||
<div className="flex items-stretch justify-between min-h-[56px]">
|
<div className="flex items-stretch justify-between min-h-[56px]">
|
||||||
<button onClick={() => setCurrentPage('status')} className="flex items-center h-full">
|
<button onClick={() => setCurrentPage('status')} className="flex items-center h-full">
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { listDirectory, getWebDAVBaseUrl, type EntryInfo } from '../webdav';
|
|
||||||
import { IMAGE_EXTS } from './MediaEntry';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DirectorySlideshow({ path }: Props) {
|
|
||||||
const [images, setImages] = useState<EntryInfo[]>([]);
|
|
||||||
const [idx, setIdx] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!path) { setImages([]); return; }
|
|
||||||
listDirectory(path)
|
|
||||||
.then(entries => {
|
|
||||||
const imgs = entries.filter(e => {
|
|
||||||
if (e.type !== 'file') return false;
|
|
||||||
const ext = e.name.split('.').pop()?.toLowerCase() ?? '';
|
|
||||||
return IMAGE_EXTS.has(ext);
|
|
||||||
});
|
|
||||||
setImages(imgs);
|
|
||||||
setIdx(0);
|
|
||||||
})
|
|
||||||
.catch(() => setImages([]));
|
|
||||||
}, [path]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (images.length <= 1) return;
|
|
||||||
const t = setInterval(() => setIdx(i => (i + 1) % images.length), 4000);
|
|
||||||
return () => clearInterval(t);
|
|
||||||
}, [images.length]);
|
|
||||||
|
|
||||||
if (images.length === 0) return null;
|
|
||||||
|
|
||||||
const current = images[idx];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-3 rounded-lg overflow-hidden">
|
|
||||||
<img
|
|
||||||
key={current.path}
|
|
||||||
src={getWebDAVBaseUrl() + current.path}
|
|
||||||
alt={current.name}
|
|
||||||
className="w-full h-48 object-contain"
|
|
||||||
/>
|
|
||||||
{images.length > 1 && (
|
|
||||||
<div className="flex justify-center gap-1.5 py-1.5">
|
|
||||||
{images.map((_, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
onClick={() => setIdx(i)}
|
|
||||||
className={`w-1.5 h-1.5 rounded-full transition-colors ${i === idx ? 'bg-white' : 'bg-white/40'}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -193,7 +193,7 @@ export default function MediaBrowser({ currentPath, onSelect, onClose }: MediaBr
|
||||||
{!loading && !error && path !== null && (
|
{!loading && !error && path !== null && (
|
||||||
<>
|
<>
|
||||||
{path !== '/' && (
|
{path !== '/' && (
|
||||||
<button onClick={navigateUp} className="w-full pl-[14px] pr-4 py-3 flex items-center gap-3 border-b border-neutral-100 border-l-2 border-l-transparent transition-colors hover:bg-blue-50 hover:border-l-blue-400 text-left">
|
<button onClick={navigateUp} className="w-full px-4 py-3 flex items-center gap-3 hover:bg-neutral-50 border-b border-neutral-100 text-left">
|
||||||
<ArrowLeft className="w-5 h-5 text-neutral-400" />
|
<ArrowLeft className="w-5 h-5 text-neutral-400" />
|
||||||
<span className="text-neutral-600">..</span>
|
<span className="text-neutral-600">..</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ export function MediaEntry({
|
||||||
entry, onPrimaryClick, onActionsClick, leftSlot, nameSlot, selected, className,
|
entry, onPrimaryClick, onActionsClick, leftSlot, nameSlot, selected, className,
|
||||||
}: MediaEntryProps) {
|
}: MediaEntryProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`pl-[14px] pr-4 py-3 flex items-center gap-3 border-b border-neutral-100 border-l-2 border-l-transparent transition-colors hover:bg-blue-50 hover:border-l-blue-400 ${selected ? 'bg-blue-50 border-l-blue-400' : ''} ${className ?? ''}`}>
|
<div className={`px-4 py-3 flex items-center gap-3 border-b border-neutral-100 hover:bg-neutral-50 ${selected ? 'bg-blue-50' : ''} ${className ?? ''}`}>
|
||||||
{leftSlot}
|
{leftSlot}
|
||||||
<button className="flex-1 flex items-center gap-3 text-left min-w-0" onClick={onPrimaryClick}>
|
<button className="flex-1 flex items-center gap-3 text-left min-w-0" onClick={onPrimaryClick}>
|
||||||
<EntryIcon entry={entry} />
|
<EntryIcon entry={entry} />
|
||||||
|
|
|
||||||
|
|
@ -338,9 +338,9 @@ async function _getEntryBytes(entry: EntryInfo): Promise<Uint8Array> {
|
||||||
|
|
||||||
const FM_PATH_KEY = 'fileManager.path';
|
const FM_PATH_KEY = 'fileManager.path';
|
||||||
|
|
||||||
export default function MediaManager({ initialPath, rootPath, title, config, setConfig, onBack, onNavigateToDevice }: MediaManagerProps) {
|
export default function MediaManager({ initialPath = '/', rootPath, title, config, setConfig, onBack, onNavigateToDevice }: MediaManagerProps) {
|
||||||
const pathKey = rootPath ? `fileManager.path:${rootPath}` : FM_PATH_KEY;
|
const pathKey = rootPath ? `fileManager.path:${rootPath}` : FM_PATH_KEY;
|
||||||
const [path, setPath] = useState(() => normalizePath(initialPath ?? localStorage.getItem(pathKey) ?? rootPath ?? '/'));
|
const [path, setPath] = useState(() => normalizePath(localStorage.getItem(pathKey) || rootPath || initialPath));
|
||||||
const [entries, setEntries] = useState<EntryInfo[]>([]);
|
const [entries, setEntries] = useState<EntryInfo[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -1018,7 +1018,7 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
|
||||||
{path !== '/' && (
|
{path !== '/' && (
|
||||||
<button
|
<button
|
||||||
onClick={navigateUp}
|
onClick={navigateUp}
|
||||||
className="w-full pl-[14px] pr-4 py-3 flex items-center gap-3 border-b border-neutral-100 border-l-2 border-l-transparent transition-colors hover:bg-blue-50 hover:border-l-blue-400 text-left"
|
className="w-full px-4 py-3 flex items-center gap-3 hover:bg-neutral-50 border-b border-neutral-100 text-left"
|
||||||
>
|
>
|
||||||
<div className="w-4 flex-shrink-0" />
|
<div className="w-4 flex-shrink-0" />
|
||||||
<ArrowLeft className="w-5 h-5 text-neutral-400 flex-shrink-0" />
|
<ArrowLeft className="w-5 h-5 text-neutral-400 flex-shrink-0" />
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { HardDrive, Activity, Wifi, Radio, Clock, RefreshCw, Loader2, Printer, Power, Computer, Download, Trash2, Eye, FolderOpen } from 'lucide-react';
|
import { HardDrive, Activity, Wifi, Radio, Clock, RefreshCw, Loader2, Printer, Power, Computer, Download, Trash2, Eye } from 'lucide-react';
|
||||||
import { listDirectory, deletePath, getFileContents, getWebDAVBaseUrl, humanFileSize, splitPath, type EntryInfo } from '../webdav';
|
import { listDirectory, deletePath, getFileContents, getWebDAVBaseUrl, humanFileSize, type EntryInfo } from '../webdav';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { MediaEntry } from './MediaEntry';
|
import { MediaEntry } from './MediaEntry';
|
||||||
import DirectorySlideshow from './DirectorySlideshow';
|
|
||||||
import { useWs } from '../ws';
|
import { useWs } from '../ws';
|
||||||
import DeviceDetailOverlay from './DeviceDetailOverlay';
|
import DeviceDetailOverlay from './DeviceDetailOverlay';
|
||||||
import MediaSet from './MediaSet';
|
import MediaSet from './MediaSet';
|
||||||
|
|
@ -14,10 +13,9 @@ import { ConfirmDialog, type ConfirmOptions } from './ui/confirm-dialog';
|
||||||
interface StatusPageProps {
|
interface StatusPageProps {
|
||||||
config: any;
|
config: any;
|
||||||
setConfig: (config: any) => void;
|
setConfig: (config: any) => void;
|
||||||
onOpenFileManager?: (path: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StatusPage({ config, setConfig, onOpenFileManager }: StatusPageProps) {
|
export default function StatusPage({ config, setConfig }: StatusPageProps) {
|
||||||
const { status: wsStatus, send: wsSend } = useWs();
|
const { status: wsStatus, send: wsSend } = useWs();
|
||||||
|
|
||||||
// Mock memory stats
|
// Mock memory stats
|
||||||
|
|
@ -26,7 +24,8 @@ export default function StatusPage({ config, setConfig, onOpenFileManager }: Sta
|
||||||
psram: { total: 8192, free: 4096 },
|
psram: { total: 8192, free: 4096 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const [showDeviceOverlay, setShowDeviceOverlay] = useState(false);
|
// Overlay state for active device
|
||||||
|
const [showDeviceOverlay, setShowDeviceOverlay] = useState(false);
|
||||||
// Find the first enabled device as the active device
|
// Find the first enabled device as the active device
|
||||||
const findActiveDevice = () => {
|
const findActiveDevice = () => {
|
||||||
if (config.iec?.devices) {
|
if (config.iec?.devices) {
|
||||||
|
|
@ -41,9 +40,6 @@ export default function StatusPage({ config, setConfig, onOpenFileManager }: Sta
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeDevice = findActiveDevice();
|
const activeDevice = findActiveDevice();
|
||||||
const activeDir = activeDevice
|
|
||||||
? (activeDevice.base_url || splitPath(activeDevice.url || '/').parent)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const mediaSetFiles: string[] | null = (() => {
|
const mediaSetFiles: string[] | null = (() => {
|
||||||
if (!activeDevice?.url) return null;
|
if (!activeDevice?.url) return null;
|
||||||
|
|
@ -163,20 +159,9 @@ export default function StatusPage({ config, setConfig, onOpenFileManager }: Sta
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-2">
|
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||||
<button
|
|
||||||
onClick={() => onOpenFileManager?.(activeDevice.base_url || (activeDevice.url ? splitPath(activeDevice.url).parent : '/'))}
|
|
||||||
className="p-2 rounded hover:bg-neutral-100"
|
|
||||||
title="Browse files"
|
|
||||||
>
|
|
||||||
<FolderOpen className="w-5 h-5 text-neutral-500" />
|
|
||||||
</button>
|
|
||||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeDir && <DirectorySlideshow path={activeDir} />}
|
|
||||||
|
|
||||||
{mediaSetFiles && (
|
{mediaSetFiles && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<MediaSet files={mediaSetFiles} activeUrl={activeDevice.url ?? ''} onSwitch={switchActiveMedia} />
|
<MediaSet files={mediaSetFiles} activeUrl={activeDevice.url ?? ''} onSwitch={switchActiveMedia} />
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user