More pages converted to use localization

This commit is contained in:
Thegan Govender
2024-02-08 16:22:05 +02:00
parent b4243e694c
commit f2d0b66bf8
25 changed files with 263 additions and 152 deletions

View File

@@ -41,32 +41,111 @@
"TOTAL_TIME": "Total Time",
"TOTAL_FILES": "Total Files",
"LIBRARY_SIZE": "Library Size",
"TOTAL_PLAYS": "Total Plays",
"TOTAL_PLAYBACK": "Total Playback",
"LAST_PLAYED": "Last Played",
"LAST_ACTIVITY": "Last Activity"
},
"GLOBAL_STATS":{
"LAST_24_HRS": "Last 24 Hours",
"LAST_7_DAYS": "Last 7 Days",
"LAST_30_DAYS": "Last 30 Days",
"ALL_TIME": "All Time",
"ITEM_STATS": "Item Stats"
},
"ITEM_INFO": {
"FILE_NAME": "File Name",
"FILE_PATH": "File Path",
"FILE_SIZE": "File Size",
"RUNTIME": "Runtime",
"AVERAGE_RUNTIME": "Average Runtime",
"OPEN_IN_JELLYFIN": "Open in Jellyfin",
"ARCHIVED_DATA_OPTIONS":"Archived Data Options",
"PURGE":"Purge",
"CONFIRM_ACTION":"Confirm Action",
"CONFIRM_ACTION_MESSAGE":"Are you sure you want to Purge this item",
"CONFIRM_ACTION_MESSAGE_2":"and Associated Playback Activity"
},
"LIBRARY_INFO":{
"LIBRARY_STATS": "Library Stats",
"LIBRARY_ACTIVITY": "Library Activity"
},
"TAB_CONTROLS":{
"OVERVIEW": "Overview",
"ACTIVITY": "Activity",
"OPTIONS": "Options"
},
"ITEM_ACTIVITY":"Item Activity",
"ACTIVITY_TABLE":{
"MODAL":{
"HEADER":"Stream Info"
},
"IP_ADDRESS":"IP Address",
"CLIENT":"Client",
"DATE":"Date",
"PLAYBACK_DURATION":"Playback Duration",
"TOTAL_PLAYBACK":"Total Playback"
},
"TABLE_NAV_BUTTONS":{
"FIRST":"First",
"LAST":"Last",
"NEXT":"Next",
"PREVIOUS":"Previous"
},
"PURGE_OPTIONS":{
"PURGE_CACHE":"Purge Cached Item",
"PURGE_CACHE_WITH_ACTIVITY":"Purge Cached Item and Playback Activity"
},
"ERROR_MESSAGES":{
"FETCH_THIS_ITEM":"Fetch this item from Jellyfin",
"NO_ACTIVITY":"No Activity Found",
"NEVER":"Never",
"N/A":"N/A"
},
"SHOW_ARCHIVED_LIBRARIES":"Show Archived Libraries",
"HIDE_ARCHIVED_LIBRARIES":"Hide Archived Libraries",
"UNITS":{
"DAY": "Day",
"DAYS": "Days",
"HOUR": "Hour",
"HOURS": "Hours",
"MINUTE": "Minute",
"MINUTES": "Minutes",
"SECOND": "Second",
"SECONDS": "Seconds",
"PLAYS": "Plays",
"ITEMS": "Items"
},
"USERS_PAGE":{
"ALL_USERS": "All Users",
"LAST_CLIENT": "Last Client",
"LAST_SEEN": "Last Seen",
"AGO": "Ago",
"USER_STATS": "User Stats",
"USER_ACTIVITY": "User Activity"
},
"TOTAL": "Total",
"LAST": "Last",
"SERIES": "Series",
"SEASON": "Season",
"SEASONS": "Seasons",
"EPISODE": "Episode",
"EPISODES": "Episodes",
"MOVIES": "Movies",
"SONGS": "Songs",
"FILES": "Files",
"LIBRARIES": "Libraries",
"USER":"User",
"USERS": "Users",
"TYPE": "Type",
"NEW_VERSION_AVAILABLE":"New version available"
"NEW_VERSION_AVAILABLE":"New version available",
"ARCHIVED":"Archived",
"CLOSE":"Close",
"TOTAL_PLAYS": "Total Plays",
"TITLE":"Title",
"VIEWS": "Views",
"WATCH_TIME": "Watch Time",
"LAST_WATCHED": "Last Watched",
"MEDIA": "Media"
}

View File

@@ -7,6 +7,7 @@ import Config from "../lib/config";
import ActivityTable from "./components/activity/activity-table";
import Loading from "./components/general/loading";
import { Trans } from "react-i18next";
function Activity() {
const [data, setData] = useState();
@@ -71,10 +72,10 @@ function Activity() {
if (data.length === 0) {
return (<div>
<div className="Heading">
<h1>Activity</h1>
<h1><Trans i18nKey="MENU_TABS.ACTIVITY"/></h1>
</div>
<div className="Activity">
<h1>No Activity to display</h1>
<h1><Trans i18nKey="ERROR_MESSAGES.NO_ACTIVITY"/></h1>
</div>
</div>
);
@@ -83,9 +84,9 @@ function Activity() {
return (
<div className="Activity">
<div className="Heading">
<h1>Activity</h1>
<h1><Trans i18nKey="MENU_TABS.ACTIVITY"/></h1>
<div className="pagination-range">
<div className="header">Items</div>
<div className="header"><Trans i18nKey="UNITS.ITEMS"/></div>
<select value={itemCount} onChange={(event) => {setItemCount(event.target.value);}}>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -22,6 +22,8 @@ import IndeterminateCircleFillIcon from 'remixicon-react/IndeterminateCircleFill
import StreamInfo from './stream_info';
import '../../css/activity/activity-table.css';
import { Trans } from 'react-i18next';
import i18next from 'i18next';
// localStorage.setItem('hour12',true);
@@ -42,7 +44,7 @@ function formatTotalWatchTime(seconds) {
}
if (remainingSeconds > 0) {
timeString += `${remainingSeconds} ${remainingSeconds === 1 ? 'second' : 'seconds'}`;
timeString += `${remainingSeconds} ${remainingSeconds === 1 ? i18next.t("UNITS.SECOND").toLowerCase() : i18next.t("UNITS.SECONDS").toLowerCase()}`;
}
return timeString.trim();
@@ -84,12 +86,12 @@ function Row(data) {
<Modal show={modalState} onHide={()=>setModalState(false)} >
<Modal.Header>
<Modal.Title>Stream Info: {!row.SeriesName ? row.NowPlayingItemName : row.SeriesName+' - '+ row.NowPlayingItemName} ({row.UserName})</Modal.Title>
<Modal.Title><Trans i18nKey="ACTIVITY_TABLE.MODAL.HEADER"/>: {!row.SeriesName ? row.NowPlayingItemName : row.SeriesName+' - '+ row.NowPlayingItemName} ({row.UserName})</Modal.Title>
</Modal.Header>
<StreamInfo data={modalData}/>
<Modal.Footer>
<Button variant="outline-primary" onClick={()=>setModalState(false)}>
Close
<Trans i18nKey="CLOSE"/>
</Button>
</Modal.Footer>
</Modal>
@@ -120,13 +122,13 @@ function Row(data) {
<Table aria-label="sub-activity" className='rounded-2'>
<TableHead>
<TableRow>
<TableCell>User</TableCell>
<TableCell>IP Address</TableCell>
<TableCell>Title</TableCell>
<TableCell>Client</TableCell>
<TableCell>Date</TableCell>
<TableCell>Playback Duration</TableCell>
<TableCell>Plays</TableCell>
<TableCell><Trans i18nKey="USER"/></TableCell>
<TableCell><Trans i18nKey="ACTIVITY_TABLE.IP_ADDRESS"/></TableCell>
<TableCell><Trans i18nKey="TITLE"/></TableCell>
<TableCell><Trans i18nKey="ACTIVITY_TABLE.CLIENT"/></TableCell>
<TableCell><Trans i18nKey="ACTIVITY_TABLE.DATE"/></TableCell>
<TableCell><Trans i18nKey="ACTIVITY_TABLE.PLAYBACK_DURATION"/></TableCell>
<TableCell><Trans i18nKey="UNITS.PLAYS"/></TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -163,43 +165,43 @@ function EnhancedTableHead(props) {
id: 'UserName',
numeric: false,
disablePadding: false,
label: 'User',
label: i18next.t("USER"),
},
{
id: 'RemoteEndPoint',
numeric: false,
disablePadding: false,
label: 'IP Address',
label: i18next.t("ACTIVITY_TABLE.IP_ADDRESS"),
},
{
id: 'NowPlayingItemName',
numeric: false,
disablePadding: false,
label: 'Title',
label: i18next.t("TITLE"),
},
{
id: 'Client',
numeric: false,
disablePadding: false,
label: 'Client',
label: i18next.t("ACTIVITY_TABLE.CLIENT"),
},
{
id: 'ActivityDateInserted',
numeric: false,
disablePadding: false,
label: 'Date',
label: i18next.t("ACTIVITY_TABLE.DATE"),
},
{
id: 'PlaybackDuration',
numeric: false,
disablePadding: false,
label: 'Total Playback',
label: i18next.t("ACTIVITY_TABLE.TOTAL_PLAYBACK"),
},
{
id: 'TotalPlays',
numeric: false,
disablePadding: false,
label: 'Total Plays',
label: i18next.t("TOTAL_PLAYS"),
},
];
@@ -326,7 +328,7 @@ export default function ActivityTable(props) {
{visibleRows.map((row) => (
<Row key={row.Id+row.NowPlayingItemId+row.EpisodeId} row={row} />
))}
{props.data.length===0 ? <tr><td colSpan="8" style={{ textAlign: "center", fontStyle: "italic" ,color:"grey"}} className='py-2'>No Activity Found</td></tr> :''}
{props.data.length===0 ? <tr><td colSpan="8" style={{ textAlign: "center", fontStyle: "italic" ,color:"grey"}} className='py-2'><Trans i18nKey="ERROR_MESSAGES.NO_ACTIVITY"/></td></tr> :''}
</TableBody>
</Table>
@@ -335,21 +337,21 @@ export default function ActivityTable(props) {
<div className='d-flex justify-content-end my-2'>
<ButtonGroup className="pagination-buttons">
<Button className="page-btn" onClick={()=>setPage(0)} disabled={page === 0}>
First
<Trans i18nKey="TABLE_NAV_BUTTONS.FIRST"/>
</Button>
<Button className="page-btn" onClick={handlePreviousPageClick} disabled={page === 0}>
Previous
<Trans i18nKey="TABLE_NAV_BUTTONS.PREVIOUS"/>
</Button>
<div className="page-number d-flex align-items-center justify-content-center">{`${(page *rowsPerPage) + 1}-${Math.min(((page * rowsPerPage)+ 1 ) + (rowsPerPage - 1),props.data.length)} of ${props.data.length}`}</div>
<Button className="page-btn" onClick={handleNextPageClick} disabled={page >= Math.ceil(props.data.length / rowsPerPage) - 1}>
Next
<Trans i18nKey="TABLE_NAV_BUTTONS.NEXT"/>
</Button>
<Button className="page-btn" onClick={()=>setPage(Math.ceil(props.data.length / rowsPerPage) - 1)} disabled={page >= Math.ceil(props.data.length / rowsPerPage) - 1}>
Last
<Trans i18nKey="TABLE_NAV_BUTTONS.LAST"/>
</Button>
</ButtonGroup>
</div>

View File

@@ -1,17 +1,19 @@
import React, {useState} from "react";
import {useState} from "react";
import { Link } from "react-router-dom";
import { Blurhash } from 'react-blurhash';
import ArchiveDrawerFillIcon from 'remixicon-react/ArchiveDrawerFillIcon';
import "../../css/lastplayed.css";
import { Trans } from "react-i18next";
import i18next from "i18next";
function formatTime(time) {
const units = {
days: ['Day', 'Days'],
hours: ['Hour', 'Hours'],
minutes: ['Minute', 'Minutes'],
seconds: ['Second', 'Seconds']
days: [i18next.t("UNITS.DAY"), i18next.t("UNITS.DAYS")],
hours: [i18next.t("UNITS.HOUR"), i18next.t("UNITS.HOUR")],
minutes: [i18next.t("UNITS.MINUTE"), i18next.t("UNITS.MINUTES")],
seconds: [i18next.t("UNITS.SECOND"), i18next.t("UNITS.SECONDS")]
};
let formattedTime = '';
@@ -26,7 +28,7 @@ function formatTime(time) {
formattedTime = `${time.seconds} ${units.seconds[time.seconds > 1 ? 1 : 0]}`;
}
return `${formattedTime} ago`;
return `${formattedTime+' '+i18next.t("AGO").toLowerCase()}`;
}
@@ -59,7 +61,7 @@ function LastWatchedCard(props) {
}
<div className="d-flex flex-column justify-content-center align-items-center position-absolute">
<ArchiveDrawerFillIcon className="w-100 h-100 mb-2"/>
<span>Archived</span>
<span><Trans i18nKey="ARCHIVED"/></span>
</div>
</div>
}

View File

@@ -21,6 +21,8 @@ import ItemNotFound from "./item-info/item-not-found";
import Config from "../../lib/config";
import Loading from "./general/loading";
import ItemOptions from "./item-info/item-options";
import { Trans } from "react-i18next";
import i18next from "i18next";
@@ -165,7 +167,7 @@ const cardBgStyle = {
}
<div className="d-flex flex-column justify-content-center align-items-center position-absolute">
<ArchiveDrawerFillIcon className="w-100 h-100 mb-2"/>
<span>Archived</span>
<span><Trans i18nKey="ARCHIVED"/></span>
</div>
</div>
}
@@ -182,23 +184,23 @@ const cardBgStyle = {
}
</h1>
<Link className="px-2" to={ config.hostUrl+"/web/index.html#!/details?id="+ (data.EpisodeId ||data.Id)} title="Open in Jellyfin" target="_blank"><ExternalLinkFillIcon/></Link>
<Link className="px-2" to={ config.hostUrl+"/web/index.html#!/details?id="+ (data.EpisodeId ||data.Id)} title={i18next.t("ITEM_INFO.OPEN_IN_JELLYFIN")} target="_blank"><ExternalLinkFillIcon/></Link>
</div>
<div className="my-3">
{data.Type==="Episode"? <p><Link to={`/libraries/item/${data.SeasonId}`} className="fw-bold">{data.SeasonName}</Link> Episode {data.IndexNumber} - {data.Name}</p> : <></> }
{data.Type==="Episode"? <p><Link to={`/libraries/item/${data.SeasonId}`} className="fw-bold">{data.SeasonName}</Link> <Trans i18nKey="EPISODE"/> {data.IndexNumber} - {data.Name}</p> : <></> }
{data.Type==="Season"? <p>{data.Name}</p> : <></> }
{data.FileName ? <p style={{color:"lightgrey"}} className="fst-italic fs-6">File Name: {data.FileName}</p> :<></>}
{data.Path ? <p style={{color:"lightgrey"}} className="fst-italic fs-6">File Path: {data.Path}</p> :<></>}
{data.RunTimeTicks ? <p style={{color:"lightgrey"}} className="fst-italic fs-6">{data.Type==="Series"?"Average Runtime" : "Runtime"}: {ticksToTimeString(data.RunTimeTicks)}</p> :<></>}
{data.Size ? <p style={{color:"lightgrey"}} className="fst-italic fs-6">File Size: {formatFileSize(data.Size)}</p> :<></>}
{data.FileName ? <p style={{color:"lightgrey"}} className="fst-italic fs-6"><Trans i18nKey="ITEM_INFO.FILE_NAME"/>: {data.FileName}</p> :<></>}
{data.Path ? <p style={{color:"lightgrey"}} className="fst-italic fs-6"><Trans i18nKey="ITEM_INFO.FILE_PATH"/>: {data.Path}</p> :<></>}
{data.RunTimeTicks ? <p style={{color:"lightgrey"}} className="fst-italic fs-6">{data.Type==="Series"? i18next.t("ITEM_INFO.AVERAGE_RUNTIME") : i18next.t("ITEM_INFO.RUNTIME")}: {ticksToTimeString(data.RunTimeTicks)}</p> :<></>}
{data.Size ? <p style={{color:"lightgrey"}} className="fst-italic fs-6"><Trans i18nKey="ITEM_INFO.FILE_SIZE"/>: {formatFileSize(data.Size)}</p> :<></>}
</div>
<ButtonGroup>
<Button onClick={() => setActiveTab('tabOverview')} active={activeTab==='tabOverview'} variant='outline-primary' type='button'>Overview</Button>
<Button onClick={() => setActiveTab('tabActivity')} active={activeTab==='tabActivity'} variant='outline-primary' type='button'>Activity</Button>
<Button onClick={() => setActiveTab('tabOverview')} active={activeTab==='tabOverview'} variant='outline-primary' type='button'><Trans i18nKey="ITEM_INFO.OVERVIEW"/></Button>
<Button onClick={() => setActiveTab('tabActivity')} active={activeTab==='tabActivity'} variant='outline-primary' type='button'><Trans i18nKey="ITEM_INFO.ACTIVITY"/></Button>
{data.archived && (<Button onClick={() => setActiveTab('tabOptions')} active={activeTab==='tabOptions'} variant='outline-primary' type='button'>Options</Button>)}
{data.archived && (<Button onClick={() => setActiveTab('tabOptions')} active={activeTab==='tabOptions'} variant='outline-primary' type='button'><Trans i18nKey="ITEM_INFO.OPTIONS"/></Button>)}
</ButtonGroup>

View File

@@ -1,8 +1,9 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import axios from "axios";
import "../../css/globalstats.css";
import WatchTimeStats from "./globalstats/watchtimestats";
import { Trans } from "react-i18next";
function GlobalStats(props) {
const [dayStats, setDayStats] = useState({});
@@ -67,16 +68,15 @@ function GlobalStats(props) {
return () => clearInterval(intervalId);
}, [props.ItemId,token]);
// console.log(dayStats);
return (
<div>
<h1 className="py-3">Item Stats</h1>
<h1 className="py-3"><Trans i18nKey="GLOBAL_STATS.ITEM_STATS"/></h1>
<div className="global-stats-container">
<WatchTimeStats data={dayStats} heading={"Last 24 Hours"} />
<WatchTimeStats data={weekStats} heading={"Last 7 Days"} />
<WatchTimeStats data={monthStats} heading={"Last 30 Days"} />
<WatchTimeStats data={allStats} heading={"All Time"} />
<WatchTimeStats data={dayStats} heading={<Trans i18nKey="GLOBAL_STATS.LAST_24_HRS"/>} />
<WatchTimeStats data={weekStats} heading={<Trans i18nKey="GLOBAL_STATS.LAST_7_DAYS"/>} />
<WatchTimeStats data={monthStats} heading={<Trans i18nKey="GLOBAL_STATS.LAST_30_DAYS"/>} />
<WatchTimeStats data={allStats} heading={<Trans i18nKey="GLOBAL_STATS.ALL_TIME"/>} />
</div>
</div>
);

View File

@@ -1,14 +1,14 @@
import React from "react";
import "../../../css/globalstats.css";
import i18next from "i18next";
import { Trans } from "react-i18next";
function WatchTimeStats(props) {
function formatTime(totalSeconds, numberClassName, labelClassName) {
const units = [
{ label: 'Day', seconds: 86400 },
{ label: 'Hour', seconds: 3600 },
{ label: 'Minute', seconds: 60 },
{ label: i18next.t("UNITS.DAY"), seconds: 86400 },
{ label: i18next.t("UNITS.HOUR"), seconds: 3600 },
{ label: i18next.t("UNITS.MINUTE"), seconds: 60 },
];
const parts = units.reduce((result, { label, seconds }) => {
@@ -17,8 +17,7 @@ function WatchTimeStats(props) {
const formattedValue = <p className={numberClassName}>{value}</p>;
const formattedLabel = (
<span className={labelClassName}>
{label}
{value === 1 ? '' : 's'}
{value === 1 ? label : i18next.t(`UNITS.${label.toUpperCase()}S`) }
</span>
);
result.push(
@@ -35,7 +34,7 @@ function WatchTimeStats(props) {
return (
<>
<p className={numberClassName}>0</p>{' '}
<p className={labelClassName}>Minutes</p>
<p className={labelClassName}><Trans i18nKey="UNITS.MINUTES"/></p>
</>
);
}
@@ -53,7 +52,7 @@ function WatchTimeStats(props) {
<div className="play-duration-stats" key={props.data.ItemId}>
<p className="stat-value"> {props.data.Plays || 0}</p>
<p className="stat-unit" >Plays /</p>
<p className="stat-unit" ><Trans i18nKey="UNITS.PLAYS"/> /</p>
<>{formatTime(props.data.total_playback_duration || 0,'stat-value','stat-unit')}</>

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import axios from "axios";
import ActivityTable from "../activity/activity-table";
import { Trans } from "react-i18next";
function ItemActivity(props) {
const [data, setData] = useState();
@@ -41,9 +42,9 @@ function ItemActivity(props) {
return (
<div className="Activity">
<div className="Heading">
<h1>Item Activity</h1>
<h1><Trans i18nKey="ITEM_ACTIVITY"/></h1>
<div className="pagination-range">
<div className="header">Items</div>
<div className="header"><Trans i18nKey="UNITS.ITEMS"/></div>
<select value={itemCount} onChange={(event) => {setItemCount(event.target.value);}}>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -1,8 +1,9 @@
import React, {useState} from "react";
import {useState} from "react";
import axios from "axios";
import "../../css/error.css";
import { Button } from "react-bootstrap";
import Loading from "../general/loading";
import { Trans } from "react-i18next";
function ItemNotFound(props) {
const [itemId] = useState(props.itemId);
@@ -47,7 +48,7 @@ function ItemNotFound(props) {
<div className="error">
<h1 className="error-title">{props.message}</h1>
<Button variant="primary" className="mt-3" onClick={()=> fetchItem()}>Fetch this item from Jellyfin</Button>
<Button variant="primary" className="mt-3" onClick={()=> fetchItem()}><Trans i18nKey="ERROR_MESSAGES.FETCH_THIS_ITEM"/></Button>
</div>
);
}

View File

@@ -1,7 +1,9 @@
import axios from "axios";
import i18next from "i18next";
import { useState } from "react";
import { Container, Row,Col, Modal } from "react-bootstrap";
import { Trans } from "react-i18next";
import { useNavigate } from "react-router-dom";
@@ -9,7 +11,16 @@ function ItemOptions(props) {
const token = localStorage.getItem('token');
const [show, setShow] = useState(false);
const options=[{description:"Purge Cached Item",withActivity:false},{description:"Purge Cached Item and Playback Activity",withActivity:true}];
const options=[
{
description:i18next.t("PURGE_OPTIONS.PURGE_CACHE"),
withActivity:false
},
{
description: i18next.t("PURGE_OPTIONS.PURGE_CACHE_WITH_ACTIVITY"),
withActivity:true
}
];
const [selectedOption, setSelectedOption] = useState(options[0]);
const navigate = useNavigate();
@@ -43,7 +54,7 @@ function ItemOptions(props) {
return (
<div className="Activity">
<div className="Heading mb-3">
<h1>Archived Data Options</h1>
<h1><Trans i18nKey="ITEM_INFO.ARCHIVED_DATA_OPTIONS"/></h1>
</div>
<Container className="p-0 m-0">
{options.map((option, index) => (
@@ -53,7 +64,7 @@ function ItemOptions(props) {
</Col>
<Col>
<button className="btn btn-danger w-25" onClick={()=>{setSelectedOption(option);setShow(true);}}>Purge</button>
<button className="btn btn-danger w-25" onClick={()=>{setSelectedOption(option);setShow(true);}}><Trans i18nKey="ITEM_INFO.PURGE"/></button>
</Col>
</Row>
@@ -62,17 +73,17 @@ function ItemOptions(props) {
<Modal show={show} onHide={() =>{setShow(false);}}>
<Modal.Header closeButton>
<Modal.Title>Confirm Action</Modal.Title>
<Modal.Title><Trans i18nKey="ITEM_INFO.CONFIRM_ACTION"/></Modal.Title>
</Modal.Header>
<Modal.Body>
<p>{"Are you sure you want to Purge this item"+(selectedOption.withActivity ? " and Associated Playback Activity?" : "?")}</p>
<p>{i18next.t("ITEM_INFO.CONFIRM_ACTION_MESSAGE")+(selectedOption.withActivity ? ` ${i18next.t("ITEM_INFO.CONFIRM_ACTION_MESSAGE_2")}?` : "?")}</p>
</Modal.Body>
<Modal.Footer>
<button className="btn btn-danger" onClick={() => {execPurge(selectedOption.withActivity);}}>
Purge
<Trans i18nKey="ITEM_INFO.PURGE"/>
</button>
<button className="btn btn-primary" onClick={()=>{setShow(false);}}>
Close
<Trans i18nKey="CLOSE"/>
</button>
</Modal.Footer>
</Modal>

View File

@@ -1,10 +1,11 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import axios from "axios";
import MoreItemCards from "./more-items/more-items-card";
import Config from "../../../lib/config";
import "../../css/users/user-details.css";
import i18next from "i18next";
function MoreItems(props) {
const [data, setData] = useState();
@@ -66,7 +67,7 @@ function MoreItems(props) {
return (
<div className="last-played">
<h1 className="my-3">{props.data.Type==="Season" ? "Episodes" : "Seasons"}</h1>
<h1 className="my-3">{props.data.Type==="Season" ? i18next.t("EPISODES") : i18next.t("SEASONS")}</h1>
<div className="last-played-container">
{data.sort((a,b) => a.IndexNumber-b.IndexNumber).map((item) => (

View File

@@ -4,6 +4,7 @@ import { Link } from "react-router-dom";
import { useParams } from 'react-router-dom';
import ArchiveDrawerFillIcon from 'remixicon-react/ArchiveDrawerFillIcon';
import "../../../css/lastplayed.css";
import { Trans } from "react-i18next";
@@ -55,7 +56,7 @@ function MoreItemCards(props) {
}
<div className="d-flex flex-column justify-content-center align-items-center position-absolute">
<ArchiveDrawerFillIcon className="w-100 h-100 mb-2"/>
<span>Archived</span>
<span><Trans i18nKey="ARCHIVED"/></span>
</div>
</div>
}

View File

@@ -15,6 +15,7 @@ import LibraryItems from './library/library-items';
import ErrorBoundary from './general/ErrorBoundary';
import { Tabs, Tab, Button, ButtonGroup } from 'react-bootstrap';
import { Trans } from 'react-i18next';
@@ -73,9 +74,9 @@ function LibraryInfo() {
<div>
<p className="user-name">{data.Name}</p>
<ButtonGroup>
<Button onClick={() => setActiveTab('tabOverview')} active={activeTab==='tabOverview'} variant='outline-primary' type='button'>Overview</Button>
<Button onClick={() => setActiveTab('tabItems')} active={activeTab==='tabItems'} variant='outline-primary' type='button'>Media</Button>
<Button onClick={() => setActiveTab('tabActivity')} active={activeTab==='tabActivity'} variant='outline-primary' type='button'>Activity</Button>
<Button onClick={() => setActiveTab('tabOverview')} active={activeTab==='tabOverview'} variant='outline-primary' type='button'><Trans i18nKey="TAB_CONTROLS.OVERVIEW"/></Button>
<Button onClick={() => setActiveTab('tabItems')} active={activeTab==='tabItems'} variant='outline-primary' type='button'><Trans i18nKey="MEDIA"/></Button>
<Button onClick={() => setActiveTab('tabActivity')} active={activeTab==='tabActivity'} variant='outline-primary' type='button'><Trans i18nKey="TAB_CONTROLS.ACTIVITY"/></Button>
</ButtonGroup>
</div>
@@ -91,12 +92,12 @@ function LibraryInfo() {
<LibraryLastWatched LibraryId={LibraryId}/>
</Tab>
<Tab eventKey="tabActivity" className='bg-transparent'>
<LibraryActivity LibraryId={LibraryId}/>
</Tab>
<Tab eventKey="tabItems" className='bg-transparent'>
<LibraryItems LibraryId={LibraryId}/>
</Tab>
<Tab eventKey="tabActivity" className='bg-transparent'>
<LibraryActivity LibraryId={LibraryId}/>
</Tab>
</Tabs>
</div>
);

View File

@@ -1,14 +1,14 @@
import React from "react";
import "../../../css/globalstats.css";
import i18next from "i18next";
import { Trans } from "react-i18next";
function WatchTimeStats(props) {
function formatTime(totalSeconds, numberClassName, labelClassName) {
const units = [
{ label: 'Day', seconds: 86400 },
{ label: 'Hour', seconds: 3600 },
{ label: 'Minute', seconds: 60 },
{ label: i18next.t("UNITS.DAY"), seconds: 86400 },
{ label: i18next.t("UNITS.HOUR"), seconds: 3600 },
{ label: i18next.t("UNITS.MINUTE"), seconds: 60 },
];
const parts = units.reduce((result, { label, seconds }) => {
@@ -17,8 +17,7 @@ function WatchTimeStats(props) {
const formattedValue = <p className={numberClassName}>{value}</p>;
const formattedLabel = (
<span className={labelClassName}>
{label}
{value === 1 ? '' : 's'}
{value === 1 ? label : i18next.t(`UNITS.${label.toUpperCase()}S`) }
</span>
);
result.push(
@@ -35,7 +34,7 @@ function WatchTimeStats(props) {
return (
<>
<p className={numberClassName}>0</p>{' '}
<p className={labelClassName}>Minutes</p>
<p className={labelClassName}><Trans i18nKey="UNITS.MINUTES"/></p>
</>
);
}
@@ -53,7 +52,7 @@ function WatchTimeStats(props) {
<div className="play-duration-stats" key={props.data.UserId}>
<p className="stat-value"> {props.data.Plays || 0}</p>
<p className="stat-unit" >Plays /</p>
<p className="stat-unit" ><Trans i18nKey="UNITS.PLAYS"/> /</p>
<>{formatTime(props.data.total_playback_duration || 0,'stat-value','stat-unit')}</>

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import axios from "axios";
// import ItemCardInfo from "./LastWatched/last-watched-card";
@@ -8,6 +8,7 @@ import LastWatchedCard from "../general/last-watched-card";
import Config from "../../../lib/config";
import "../../css/users/user-details.css";
import { Trans } from "react-i18next";
function LibraryLastWatched(props) {
const [data, setData] = useState();
@@ -61,7 +62,7 @@ function LibraryLastWatched(props) {
return (
<div className="last-played">
<h1 className="my-3">Last Watched</h1>
<h1 className="my-3"><Trans i18nKey="LAST_WATCHED"/></h1>
<div className="last-played-container">
{data.map((item) => (
<LastWatchedCard data={item} base_url={config.hostUrl} key={item.Id+item.SeasonNumber+item.EpisodeNumber}/>

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import axios from "axios";
import ActivityTable from "../activity/activity-table";
import { Trans } from "react-i18next";
function LibraryActivity(props) {
const [data, setData] = useState();
@@ -42,9 +43,9 @@ function LibraryActivity(props) {
return (
<div className="Activity">
<div className="Heading">
<h1>Library Activity</h1>
<h1><Trans i18nKey={"LIBRARY_INFO.LIBRARY_ACTIVITY"}/></h1>
<div className="pagination-range">
<div className="header">Items</div>
<div className="header"><Trans i18nKey={"UNITS.ITEMS"}/></div>
<select value={itemCount} onChange={(event) => {setItemCount(event.target.value);}}>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -106,9 +106,9 @@ function LibraryCard(props) {
function formatLastActivityTime(time) {
const units = {
days: ['Day', 'Days'],
hours: ['Hour', 'Hours'],
minutes: ['Minute', 'Minutes']
days: [i18next.t("UNITS.DAY"), i18next.t("UNITS.DAYS")],
hours: [i18next.t("UNITS.HOUR"), i18next.t("UNITS.HOUR")],
minutes: [i18next.t("UNITS.MINUTE"), i18next.t("UNITS.MINUTES")]
};
let formattedTime = '';
@@ -120,7 +120,7 @@ function LibraryCard(props) {
}
}
return `${formattedTime}ago`;
return `${formattedTime+i18next.t("AGO").toLowerCase()}`;
}
return (
@@ -170,7 +170,7 @@ function LibraryCard(props) {
</Row>
<Row className="space-between-end card-row">
<Col className="card-label"><Trans i18nKey="LIBRARY_CARD.TOTAL_PLAYS" /></Col>
<Col className="card-label"><Trans i18nKey="TOTAL_PLAYS" /></Col>
<Col className="text-end">{props.data.Plays}</Col>
</Row>

View File

@@ -12,6 +12,7 @@ import Config from "../../../lib/config";
import "../../css/library/media-items.css";
import "../../css/width_breakpoint_css.css";
import "../../css/radius_breakpoint_css.css";
import { Trans } from "react-i18next";
function LibraryItems(props) {
const [data, setData] = useState();
@@ -90,15 +91,15 @@ function LibraryItems(props) {
return (
<div className="library-items">
<div className="d-md-flex justify-content-between">
<h1 className="my-3">Media</h1>
<h1 className="my-3"><Trans i18nKey="LIBRARY_INFO.MEDIA"/></h1>
<div className="d-flex flex-column flex-md-row">
<div className="d-flex flex-row w-100">
<FormSelect onChange={(e) => sortOrderLogic(e.target.value) } className="my-md-3 w-100 rounded-0 rounded-start">
<option value="Title">Title</option>
<option value="Views">Views</option>
<option value="WatchTime">Watch Time</option>
<option value="Title"><Trans i18nKey="TITLE"/></option>
<option value="Views"><Trans i18nKey="VIEWS"/></option>
<option value="WatchTime"><Trans i18nKey="WATCH_TIME"/></option>
</FormSelect>
<Button className="my-md-3 rounded-0 rounded-end" onClick={()=>setSortAsc(!sortAsc)}>

View File

@@ -1,8 +1,9 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import axios from "axios";
import "../../css/globalstats.css";
import WatchTimeStats from "./globalstats/watchtimestats";
import { Trans } from "react-i18next";
function LibraryGlobalStats(props) {
const [dayStats, setDayStats] = useState({});
@@ -69,12 +70,12 @@ function LibraryGlobalStats(props) {
return (
<div>
<h1 className="my-3">Library Stats</h1>
<h1 className="my-3"><Trans i18nKey="LIBRARY_INFO.LIBRARY_STATS"/></h1>
<div className="global-stats-container">
<WatchTimeStats data={dayStats} heading={"Last 24 Hours"} />
<WatchTimeStats data={weekStats} heading={"Last 7 Days"} />
<WatchTimeStats data={monthStats} heading={"Last 30 Days"} />
<WatchTimeStats data={allStats} heading={"All Time"} />
<WatchTimeStats data={dayStats} heading={<Trans i18nKey="GLOBAL_STATS.LAST_24_HRS"/>} />
<WatchTimeStats data={weekStats} heading={<Trans i18nKey="GLOBAL_STATS.LAST_7_DAYS"/>} />
<WatchTimeStats data={monthStats} heading={<Trans i18nKey="GLOBAL_STATS.LAST_30_DAYS"/>} />
<WatchTimeStats data={allStats} heading={<Trans i18nKey="GLOBAL_STATS.ALL_TIME"/>} />
</div>
</div>
);

View File

@@ -5,6 +5,7 @@ import RecentlyAddedCard from "./RecentlyAdded/recently-added-card";
import "../../css/users/user-details.css";
import ErrorBoundary from "../general/ErrorBoundary";
import { Trans } from "react-i18next";
function RecentlyAdded(props) {
const [data, setData] = useState();
@@ -57,7 +58,7 @@ function RecentlyAdded(props) {
return (
<div className="last-played">
<h1 className="my-3">Recently Added</h1>
<h1 className="my-3"><Trans i18nKey="HOME_PAGE.RECENTLY_ADDED"/></h1>
<div className="last-played-container">
{data && data.map((item) => (
<ErrorBoundary key={item.Id}>

View File

@@ -1,5 +1,5 @@
import { useParams } from 'react-router-dom';
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import axios from "axios";
import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon";
import Config from "../../lib/config";
@@ -9,6 +9,7 @@ import GlobalStats from './user-info/globalStats';
import LastPlayed from './user-info/lastplayed';
import UserActivity from './user-info/user-activity';
import "../css/users/user-details.css";
import { Trans } from 'react-i18next';
@@ -93,8 +94,8 @@ function UserInfo() {
<div>
<p className="user-name">{data.Name}</p>
<ButtonGroup>
<Button onClick={() => setActiveTab('tabOverview')} active={activeTab==='tabOverview'} variant='outline-primary' type='button'>Overview</Button>
<Button onClick={() => setActiveTab('tabActivity')} active={activeTab==='tabActivity'} variant='outline-primary' type='button'>Activity</Button>
<Button onClick={() => setActiveTab('tabOverview')} active={activeTab==='tabOverview'} variant='outline-primary' type='button'><Trans i18nKey="TAB_CONTROLS.OVERVIEW"/></Button>
<Button onClick={() => setActiveTab('tabActivity')} active={activeTab==='tabActivity'} variant='outline-primary' type='button'><Trans i18nKey="TAB_CONTROLS.ACTIVITY"/></Button>
</ButtonGroup>
</div>

View File

@@ -3,6 +3,7 @@ import axios from "axios";
import "../../css/globalstats.css";
import WatchTimeStats from "./globalstats/watchtimestats";
import { Trans } from "react-i18next";
function GlobalStats(props) {
const [dayStats, setDayStats] = useState({});
@@ -69,12 +70,12 @@ function GlobalStats(props) {
return (
<div>
<h1 className="py-3">User Stats</h1>
<h1 className="py-3"><Trans i18nKey="USERS_PAGE.USER_STATS"/></h1>
<div className="global-stats-container">
<WatchTimeStats data={dayStats} heading={"Last 24 Hours"} />
<WatchTimeStats data={weekStats} heading={"Last 7 Days"} />
<WatchTimeStats data={monthStats} heading={"Last 30 Days"} />
<WatchTimeStats data={allStats} heading={"All Time"} />
<WatchTimeStats data={dayStats} heading={<Trans i18nKey="GLOBAL_STATS.LAST_24_HRS"/>} />
<WatchTimeStats data={weekStats} heading={<Trans i18nKey="GLOBAL_STATS.LAST_7_DAYS"/>} />
<WatchTimeStats data={monthStats} heading={<Trans i18nKey="GLOBAL_STATS.LAST_30_DAYS"/>} />
<WatchTimeStats data={allStats} heading={<Trans i18nKey="GLOBAL_STATS.ALL_TIME"/>} />
</div>
</div>
);

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import axios from "axios";
import LastWatchedCard from "../general/last-watched-card";
@@ -6,6 +6,7 @@ import ErrorBoundary from "../general/ErrorBoundary";
import Config from "../../../lib/config";
import "../../css/users/user-details.css";
import { Trans } from "react-i18next";
function LastPlayed(props) {
const [data, setData] = useState();
@@ -61,7 +62,7 @@ function LastPlayed(props) {
return (
<div className="last-played">
<h1 className="my-3">Last Watched</h1>
<h1 className="my-3"><Trans i18nKey="LAST_WATCHED"/></h1>
<div className="last-played-container">
{data.map((item) => (

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import axios from "axios";
import ActivityTable from "../activity/activity-table";
import { Trans } from "react-i18next";
function UserActivity(props) {
const [data, setData] = useState();
@@ -39,9 +40,9 @@ function UserActivity(props) {
return (
<div className="Activity">
<div className="Heading">
<h1>User Activity</h1>
<h1><Trans i18nKey="USERS_PAGE.USER_ACTIVITY"/></h1>
<div className="pagination-range">
<div className="header">Items</div>
<div className="header"><Trans i18nKey="UNITS.ITEMS"/></div>
<select value={itemCount} onChange={(event) => {setItemCount(event.target.value);}}>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -19,6 +19,8 @@ import { visuallyHidden } from '@mui/utils';
import "./css/users/users.css";
import Loading from "./components/general/loading";
import i18next from "i18next";
import { Trans } from "react-i18next";
const token = localStorage.getItem('token');
@@ -35,37 +37,37 @@ function EnhancedTableHead(props) {
id: 'UserName',
numeric: false,
disablePadding: true,
label: 'User',
label: i18next.t("USER"),
},
{
id: 'LastWatched',
numeric: false,
disablePadding: false,
label: 'Last Watched',
label: i18next.t("LAST_WATCHED"),
},
{
id: 'LastClient',
numeric: false,
disablePadding: false,
label: 'Last Client',
label: i18next.t("USERS_PAGE.LAST_CLIENT"),
},
{
id: 'TotalPlays',
numeric: false,
disablePadding: false,
label: 'Plays',
label: i18next.t("UNITS.PLAYS"),
},
{
id: 'TotalWatchTime',
numeric: false,
disablePadding: false,
label: 'Watch Time',
label: i18next.t("WATCH_TIME"),
},
{
id: 'LastSeen',
numeric: false,
disablePadding: false,
label: 'Last Seen',
label: i18next.t("USERS.LAST_SEEN"),
},
];
@@ -110,11 +112,11 @@ function Row(row) {
let formattedTime='';
if(hours)
{
formattedTime+=`${hours} hours`;
formattedTime+=`${hours} ${i18next.t("UNITS.HOURS")}`;
}
if(minutes)
{
formattedTime+=` ${minutes} minutes`;
formattedTime+=` ${minutes} ${i18next.t("UNITS.MINUTES")}`;
}
return formattedTime ;
@@ -122,10 +124,10 @@ function Row(row) {
function formatLastSeenTime(time) {
const units = {
days: ['Day', 'Days'],
hours: ['Hour', 'Hours'],
minutes: ['Minute', 'Minutes'],
seconds: ['Second', 'Seconds']
days: [i18next.t("UNITS.DAY"), i18next.t("UNITS.DAYS")],
hours: [i18next.t("UNITS.HOUR"), i18next.t("UNITS.HOUR")],
minutes: [i18next.t("UNITS.MINUTE"), i18next.t("UNITS.MINUTES")],
seconds: [i18next.t("UNITS.SECOND"), i18next.t("UNITS.SECONDS")]
};
let formattedTime = '';
@@ -137,7 +139,7 @@ function Row(row) {
}
}
return `${formattedTime}ago`;
return `${formattedTime+i18next.t("AGO").toLowerCase()}`;
}
@@ -160,11 +162,11 @@ function Row(row) {
)}
</TableCell>
<TableCell><Link to={`/users/${data.UserId}`} className="text-decoration-none">{data.UserName}</Link></TableCell>
<TableCell><Link to={`/libraries/item/${data.NowPlayingItemId}`} className="text-decoration-none">{data.LastWatched || 'never'}</Link></TableCell>
<TableCell>{data.LastClient || 'n/a'}</TableCell>
<TableCell style={{textTransform:data.LastWatched ? 'none':'lowercase'}}><Link to={`/libraries/item/${data.NowPlayingItemId}`} className="text-decoration-none">{data.LastWatched || i18next.t("ERROR_MESSAGES.NEVER")}</Link></TableCell>
<TableCell style={{textTransform:data.LastClient ? 'none':'lowercase'}}>{data.LastClient || i18next.t("ERROR_MESSAGES.N/A")}</TableCell>
<TableCell>{data.TotalPlays}</TableCell>
<TableCell>{formatTotalWatchTime(data.TotalWatchTime) || '0 minutes'}</TableCell>
<TableCell>{data.LastSeen ? formatLastSeenTime(data.LastSeen) : 'never'}</TableCell>
<TableCell>{formatTotalWatchTime(data.TotalWatchTime) || `0 ${i18next.t("UNITS.MINUTES")}`}</TableCell>
<TableCell style={{textTransform: data.LastSeen ? 'none' :'lowercase'}}>{data.LastSeen ? formatLastSeenTime(data.LastSeen) : i18next.t("ERROR_MESSAGES.NEVER")}</TableCell>
</TableRow>
</React.Fragment>
@@ -248,10 +250,10 @@ function Users() {
return ' never';
}
const units = {
days: ['Day', 'Days'],
hours: ['Hour', 'Hours'],
minutes: ['Minute', 'Minutes'],
seconds: ['Second', 'Seconds']
days: [i18next.t("UNITS.DAY"), i18next.t("UNITS.DAYS")],
hours: [i18next.t("UNITS.HOUR"), i18next.t("UNITS.HOUR")],
minutes: [i18next.t("UNITS.MINUTE"), i18next.t("UNITS.MINUTES")],
seconds: [i18next.t("UNITS.SECOND"), i18next.t("UNITS.SECONDS")]
};
let formattedTime = '';
@@ -263,7 +265,7 @@ function Users() {
}
}
return `${formattedTime}ago`;
return `${formattedTime+i18next.t("AGO").toLowerCase()}`;
}
@@ -343,9 +345,9 @@ function Users() {
return (
<div className="Users">
<div className="Heading py-2">
<h1 >All Users</h1>
<h1><Trans i18nKey="USERS_PAGE.ALL_USERS"/></h1>
<div className="pagination-range">
<div className="header">Items</div>
<div className="header"><Trans i18nKey="UNITS.ITEMS"/></div>
<select value={itemCount} onChange={(event) => {setRowsPerPage(event.target.value); setPage(0); setItemCount(event.target.value);}}>
<option value="10">10</option>
<option value="25">25</option>
@@ -376,21 +378,21 @@ function Users() {
<div className='d-flex justify-content-end my-2'>
<ButtonGroup className="pagination-buttons">
<Button className="page-btn" onClick={()=>setPage(0)} disabled={page === 0}>
First
<Trans i18nKey="TABLE_NAV_BUTTONS.FIRST"/>
</Button>
<Button className="page-btn" onClick={handlePreviousPageClick} disabled={page === 0}>
Previous
<Trans i18nKey="TABLE_NAV_BUTTONS.PREVIOUS"/>
</Button>
<div className="page-number d-flex align-items-center justify-content-center">{`${page *rowsPerPage + 1}-${Math.min((page * rowsPerPage+ 1 ) + (rowsPerPage - 1),data.length)} of ${data.length}`}</div>
<Button className="page-btn" onClick={handleNextPageClick} disabled={page >= Math.ceil(data.length / rowsPerPage) - 1}>
Next
<Trans i18nKey="TABLE_NAV_BUTTONS.NEXT"/>
</Button>
<Button className="page-btn" onClick={()=>setPage(Math.ceil(data.length / rowsPerPage) - 1)} disabled={page >= Math.ceil(data.length / rowsPerPage) - 1}>
Last
<Trans i18nKey="TABLE_NAV_BUTTONS.LAST"/>
</Button>
</ButtonGroup>
</div>