redesign
This commit is contained in:
+65
-27
@@ -49,20 +49,75 @@ function Shell({
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const current = location.pathname;
|
||||
const isMobile = useMediaQuery("(max-width: 900px)");
|
||||
|
||||
return (
|
||||
<>
|
||||
<CssBaseline />
|
||||
<AppBar position="sticky" color="inherit" elevation={0}>
|
||||
<Toolbar sx={{ display: "flex", gap: 2, minHeight: 68 }}>
|
||||
<Typography variant="h6" sx={{ mr: 2, fontWeight: 700 }}>
|
||||
Manage
|
||||
</Typography>
|
||||
<Toolbar
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", md: "row" },
|
||||
alignItems: { xs: "stretch", md: "center" },
|
||||
gap: 1,
|
||||
py: { xs: 1, md: 0 },
|
||||
minHeight: { xs: "auto", md: 68 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, lineHeight: 1 }}>
|
||||
Manage
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={0.75}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
{authLabel && (
|
||||
<Chip size="small" variant="outlined" label={authLabel} />
|
||||
)}
|
||||
<Chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
label={darkMode ? "Dark" : "Light"}
|
||||
/>
|
||||
{onSignOut && (
|
||||
<Button size="small" variant="text" onClick={onSignOut}>
|
||||
Sign out
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
<Tabs
|
||||
value={current}
|
||||
textColor="primary"
|
||||
indicatorColor="primary"
|
||||
sx={{ flex: 1 }}
|
||||
variant={isMobile ? "scrollable" : "standard"}
|
||||
scrollButtons="auto"
|
||||
allowScrollButtonsMobile
|
||||
sx={{
|
||||
width: "100%",
|
||||
minHeight: 40,
|
||||
"& .MuiTab-root": {
|
||||
minHeight: 40,
|
||||
py: 1,
|
||||
px: 1.25,
|
||||
minWidth: { xs: 96, md: 120 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab value="/" label="Dashboard" component={NavLink} to="/" />
|
||||
<Tab
|
||||
@@ -73,31 +128,14 @@ function Shell({
|
||||
/>
|
||||
<Tab value="/media" label="Media" component={NavLink} to="/media" />
|
||||
<Tab value="/users" label="Users" component={NavLink} to="/users" />
|
||||
<Tab
|
||||
value="/files"
|
||||
label="File Browser"
|
||||
component={NavLink}
|
||||
to="/files"
|
||||
/>
|
||||
<Tab value="/files" label="Files" component={NavLink} to="/files" />
|
||||
</Tabs>
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
|
||||
{authLabel && (
|
||||
<Chip size="small" variant="outlined" label={authLabel} />
|
||||
)}
|
||||
<Chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
label={darkMode ? "Dark" : "Light"}
|
||||
/>
|
||||
{onSignOut && (
|
||||
<Button size="small" variant="text" onClick={onSignOut}>
|
||||
Sign out
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Container maxWidth={false} sx={{ py: 3 }}>
|
||||
<Container
|
||||
maxWidth={false}
|
||||
sx={{ py: { xs: 2, md: 3 }, px: { xs: 1.5, sm: 2.5, md: 3 } }}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/monitoring" element={<Monitoring />} />
|
||||
|
||||
@@ -8,8 +8,8 @@ interface Props {
|
||||
|
||||
export function MetricCard({ label, value, subtext }: Props) {
|
||||
return (
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Card variant="outlined" sx={{ height: "100%" }}>
|
||||
<CardContent sx={{ p: { xs: 1.5, sm: 2 } }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
@@ -17,14 +17,22 @@ export function MetricCard({ label, value, subtext }: Props) {
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ mt: 0.5, fontWeight: 700 }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
mt: 0.5,
|
||||
fontWeight: 700,
|
||||
fontSize: { xs: "1.05rem", sm: "1.5rem" },
|
||||
lineHeight: 1.15,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
{subtext && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ whiteSpace: "pre-line" }}
|
||||
sx={{ whiteSpace: "pre-line", display: "block", mt: 0.25 }}
|
||||
>
|
||||
{subtext}
|
||||
</Typography>
|
||||
|
||||
@@ -79,7 +79,7 @@ export function SessionActivityPanel({
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
variant="outlined"
|
||||
sx={{ maxHeight: 280, borderColor: "divider", borderRadius: 1 }}
|
||||
sx={{ maxHeight: 280, borderColor: "divider", borderRadius: 1, overflowX: "auto" }}
|
||||
>
|
||||
<Table size="small" stickyHeader aria-label="Session activity details">
|
||||
<TableHead>
|
||||
@@ -198,7 +198,7 @@ export function SessionActivityPanel({
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.75, minWidth: 140 }}>
|
||||
<TableCell sx={{ py: 0.75, minWidth: 140, display: { xs: "none", md: "table-cell" } }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
noWrap
|
||||
@@ -210,12 +210,12 @@ export function SessionActivityPanel({
|
||||
{session.type || "—"}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.75, minWidth: 140 }}>
|
||||
<TableCell sx={{ py: 0.75, minWidth: 140, display: { xs: "none", md: "table-cell" } }}>
|
||||
<Typography variant="body2" noWrap>
|
||||
{session.device || "Unknown device"}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.75, whiteSpace: "nowrap" }}>
|
||||
<TableCell sx={{ py: 0.75, whiteSpace: "nowrap", display: { xs: "none", md: "table-cell" } }}>
|
||||
<Typography variant="body2" noWrap>
|
||||
{session.transcoding === "yes"
|
||||
? session.transcoding_type
|
||||
|
||||
@@ -4,4 +4,9 @@ body,
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
useDirectoryListing,
|
||||
@@ -538,6 +539,7 @@ function FfprobeDetails({ path, data }: { path: string; data: FfprobeData }) {
|
||||
|
||||
export function FileBrowser() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const isMobile = useMediaQuery("(max-width: 900px)");
|
||||
const initialRequestedPath = searchParams.get("path") ?? "/";
|
||||
const initialSelectedPath =
|
||||
initialRequestedPath !== "/" &&
|
||||
@@ -628,7 +630,7 @@ export function FileBrowser() {
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="h5">File Browser</Typography>
|
||||
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Stack direction={isMobile ? "column" : "row"} spacing={1}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
@@ -637,10 +639,10 @@ export function FileBrowser() {
|
||||
onChange={(e) => setPathInput(e.target.value)}
|
||||
onKeyDown={handlePathSubmit}
|
||||
/>
|
||||
<Button variant="outlined" onClick={() => navigate(pathInput || "/")}>
|
||||
<Button fullWidth={isMobile} variant="outlined" onClick={() => navigate(pathInput || "/")}>
|
||||
Open
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={() => refetch()}>
|
||||
<Button fullWidth={isMobile} variant="outlined" onClick={() => refetch()}>
|
||||
Refresh
|
||||
</Button>
|
||||
</Stack>
|
||||
@@ -667,6 +669,7 @@ export function FileBrowser() {
|
||||
columns={columns}
|
||||
loading={isLoading}
|
||||
rowSelectionModel={rowSelectionModel}
|
||||
columnVisibilityModel={isMobile ? { ext: false, modified: false } : undefined}
|
||||
hideFooter
|
||||
sx={{
|
||||
"& .MuiDataGrid-columnHeaders": {
|
||||
@@ -731,7 +734,7 @@ export function FileBrowser() {
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Stack direction={isMobile ? "column" : "row"} spacing={1}>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!selectedJob || runJob.isPending}
|
||||
|
||||
@@ -17,10 +17,11 @@ import {
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
useMediaStatus,
|
||||
useMediaQuery,
|
||||
useMediaQuery as useMediaDataQuery,
|
||||
useBuildIndex,
|
||||
useStopBuildIndex,
|
||||
useForceStopBuildIndex,
|
||||
@@ -40,6 +41,7 @@ function formatDuration(seconds: number | null | undefined): string {
|
||||
|
||||
export function Media() {
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useMediaQuery("(max-width: 900px)");
|
||||
const { data: status } = useMediaStatus();
|
||||
const buildIndex = useBuildIndex();
|
||||
const stopBuildIndex = useStopBuildIndex();
|
||||
@@ -53,7 +55,7 @@ export function Media() {
|
||||
const [limit] = useState(100);
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
const { data: queryResult, isLoading } = useMediaQuery({
|
||||
const { data: queryResult, isLoading } = useMediaDataQuery({
|
||||
types,
|
||||
search,
|
||||
hdr_filter: hdrFilter,
|
||||
@@ -365,6 +367,21 @@ export function Media() {
|
||||
navigate(`/files?path=${encodeURIComponent(row.path)}`);
|
||||
}}
|
||||
pageSizeOptions={[100]}
|
||||
columnVisibilityModel={
|
||||
isMobile
|
||||
? {
|
||||
series: false,
|
||||
season: false,
|
||||
episode: false,
|
||||
bitrate: false,
|
||||
video: false,
|
||||
resolution: false,
|
||||
date_added: false,
|
||||
library: false,
|
||||
path: false,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
hideFooter
|
||||
sx={{
|
||||
"& .MuiDataGrid-columnHeaders": {
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import { MetricCard } from "../components/MetricCard";
|
||||
import { SessionActivityPanel } from "../components/SessionActivityPanel";
|
||||
@@ -62,6 +63,7 @@ export function UsersPage() {
|
||||
const { data: activity } = useActivity();
|
||||
const queueStatusQuery = useUserMessageQueueStatus();
|
||||
const sendUserMessage = useSendUserMessage();
|
||||
const isMobile = useMediaQuery("(max-width: 900px)");
|
||||
const [search, setSearch] = useState("");
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
|
||||
@@ -684,6 +686,7 @@ export function UsersPage() {
|
||||
py: 1.25,
|
||||
verticalAlign: "middle",
|
||||
textAlign: "center",
|
||||
display: { xs: "none", md: "table-cell" },
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
@@ -715,6 +718,7 @@ export function UsersPage() {
|
||||
py: 1.25,
|
||||
verticalAlign: "middle",
|
||||
textAlign: "center",
|
||||
display: { xs: "none", md: "table-cell" },
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
@@ -736,6 +740,7 @@ export function UsersPage() {
|
||||
py: 1.25,
|
||||
verticalAlign: "middle",
|
||||
textAlign: "center",
|
||||
display: { xs: "none", md: "table-cell" },
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
@@ -747,6 +752,7 @@ export function UsersPage() {
|
||||
py: 1.25,
|
||||
verticalAlign: "middle",
|
||||
textAlign: "center",
|
||||
display: { xs: "none", md: "table-cell" },
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
@@ -911,7 +917,7 @@ export function UsersPage() {
|
||||
) : null}
|
||||
</Drawer>
|
||||
|
||||
<Dialog open={composeOpen} onClose={closeCompose} fullWidth maxWidth="md">
|
||||
<Dialog open={composeOpen} onClose={closeCompose} fullWidth maxWidth="md" fullScreen={isMobile}>
|
||||
<DialogTitle sx={{ pr: 6 }}>
|
||||
Message selected users
|
||||
<IconButton
|
||||
|
||||
Reference in New Issue
Block a user