mirror of
https://github.com/Screenly/Anthias.git
synced 2025-12-23 22:38:05 -05:00
chore: remove semicolons from TS/TSX code (#2403)
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
"options": {
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": false,
|
||||
"semi": true,
|
||||
"semi": false,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -45,13 +45,13 @@ export default [
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'semi': ['error', 'always'],
|
||||
'quotes': ['error', 'single'],
|
||||
'indent': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'no-console': 'error',
|
||||
'no-debugger': 'warn',
|
||||
'no-unexpected-multiline': 'error',
|
||||
'react/jsx-uses-react': 'error',
|
||||
'react/jsx-uses-vars': 'error',
|
||||
'react/jsx-no-duplicate-props': 'error',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import {
|
||||
selectActiveAssets,
|
||||
updateAssetOrder,
|
||||
fetchAssets,
|
||||
} from '@/store/assets';
|
||||
} from '@/store/assets'
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
@@ -12,59 +12,59 @@ import {
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from '@dnd-kit/core';
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { SortableAssetRow } from '@/components/sortable-asset-row';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Asset, ActiveAssetsTableProps, AppDispatch } from '@/types';
|
||||
} from '@dnd-kit/sortable'
|
||||
import { SortableAssetRow } from '@/components/sortable-asset-row'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Asset, ActiveAssetsTableProps, AppDispatch } from '@/types'
|
||||
|
||||
export const ActiveAssetsTable = ({ onEditAsset }: ActiveAssetsTableProps) => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const activeAssets = useSelector(selectActiveAssets) as Asset[];
|
||||
const [items, setItems] = useState<Asset[]>(activeAssets);
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const activeAssets = useSelector(selectActiveAssets) as Asset[]
|
||||
const [items, setItems] = useState<Asset[]>(activeAssets)
|
||||
|
||||
useEffect(() => {
|
||||
setItems(activeAssets);
|
||||
}, [activeAssets]);
|
||||
setItems(activeAssets)
|
||||
}, [activeAssets])
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
)
|
||||
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
const { active, over } = event
|
||||
|
||||
if (!over || active.id === over.id) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const oldIndex = items.findIndex(
|
||||
(asset) => asset.asset_id.toString() === active.id,
|
||||
);
|
||||
)
|
||||
const newIndex = items.findIndex(
|
||||
(asset) => asset.asset_id.toString() === over.id,
|
||||
);
|
||||
)
|
||||
|
||||
const newItems = arrayMove(items, oldIndex, newIndex);
|
||||
setItems(newItems);
|
||||
const newItems = arrayMove(items, oldIndex, newIndex)
|
||||
setItems(newItems)
|
||||
|
||||
const activeIds = newItems.map((asset: Asset) => asset.asset_id);
|
||||
const activeIds = newItems.map((asset: Asset) => asset.asset_id)
|
||||
|
||||
try {
|
||||
await dispatch(updateAssetOrder(activeIds.join(','))).unwrap();
|
||||
dispatch(fetchAssets());
|
||||
await dispatch(updateAssetOrder(activeIds.join(','))).unwrap()
|
||||
dispatch(fetchAssets())
|
||||
} catch {
|
||||
setItems(activeAssets);
|
||||
setItems(activeAssets)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
@@ -121,5 +121,5 @@ export const ActiveAssetsTable = ({ onEditAsset }: ActiveAssetsTableProps) => {
|
||||
</tbody>
|
||||
</table>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
interface FileUploadTabProps {
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
dropZoneRef: React.RefObject<HTMLDivElement | null>;
|
||||
handleFileSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleFileDrop: (e: React.DragEvent<HTMLDivElement>) => void;
|
||||
handleDragOver: (e: React.DragEvent<HTMLDivElement>) => void;
|
||||
handleDragEnter: (e: React.DragEvent<HTMLDivElement>) => void;
|
||||
handleDragLeave: (e: React.DragEvent<HTMLDivElement>) => void;
|
||||
isSubmitting: boolean;
|
||||
uploadProgress: number;
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>
|
||||
dropZoneRef: React.RefObject<HTMLDivElement | null>
|
||||
handleFileSelect: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
handleFileDrop: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
handleDragOver: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
handleDragEnter: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
handleDragLeave: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
isSubmitting: boolean
|
||||
uploadProgress: number
|
||||
}
|
||||
|
||||
export const FileUploadTab = ({
|
||||
@@ -81,5 +81,5 @@ export const FileUploadTab = ({
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,26 +8,26 @@
|
||||
* @returns {string} - The mimetype of the file
|
||||
*/
|
||||
export const getMimetype = (filename: string): string => {
|
||||
const viduris = ['rtsp', 'rtmp'];
|
||||
const viduris = ['rtsp', 'rtmp']
|
||||
const mimetypes = [
|
||||
[['jpe', 'jpg', 'jpeg', 'png', 'pnm', 'gif', 'bmp'], 'image'],
|
||||
[['avi', 'mkv', 'mov', 'mpg', 'mpeg', 'mp4', 'ts', 'flv'], 'video'],
|
||||
];
|
||||
const domains = [[['www.youtube.com', 'youtu.be'], 'youtube_asset']];
|
||||
]
|
||||
const domains = [[['www.youtube.com', 'youtu.be'], 'youtube_asset']]
|
||||
|
||||
// Check if it's a streaming URL
|
||||
const scheme = filename.split(':')[0].toLowerCase();
|
||||
const scheme = filename.split(':')[0].toLowerCase()
|
||||
if (viduris.includes(scheme)) {
|
||||
return 'streaming';
|
||||
return 'streaming'
|
||||
}
|
||||
|
||||
// Check if it's a domain-specific asset
|
||||
try {
|
||||
const domain = filename.split('//')[1]?.toLowerCase().split('/')[0];
|
||||
const domain = filename.split('//')[1]?.toLowerCase().split('/')[0]
|
||||
if (domain) {
|
||||
for (const [domainList, type] of domains) {
|
||||
if (domainList.includes(domain)) {
|
||||
return type as string;
|
||||
return type as string
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,19 +35,19 @@ export const getMimetype = (filename: string): string => {
|
||||
|
||||
// Check file extension
|
||||
try {
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
const ext = filename.split('.').pop()?.toLowerCase()
|
||||
if (ext) {
|
||||
for (const [extList, type] of mimetypes) {
|
||||
if (extList.includes(ext)) {
|
||||
return type as string;
|
||||
return type as string
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Default to webpage
|
||||
return 'webpage';
|
||||
};
|
||||
return 'webpage'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duration for a mimetype
|
||||
@@ -62,25 +62,25 @@ export const getDurationForMimetype = (
|
||||
defaultStreamingDuration: number,
|
||||
): number => {
|
||||
if (mimetype === 'video') {
|
||||
return 0;
|
||||
return 0
|
||||
} else if (mimetype === 'streaming') {
|
||||
return defaultStreamingDuration;
|
||||
return defaultStreamingDuration
|
||||
} else {
|
||||
return defaultDuration;
|
||||
return defaultDuration
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default dates for an asset
|
||||
* @returns {Object} - Object containing start_date and end_date
|
||||
*/
|
||||
export const getDefaultDates = (): { start_date: string; end_date: string } => {
|
||||
const now = new Date();
|
||||
const endDate = new Date();
|
||||
endDate.setDate(endDate.getDate() + 30); // 30 days from now
|
||||
const now = new Date()
|
||||
const endDate = new Date()
|
||||
endDate.setDate(endDate.getDate() + 30) // 30 days from now
|
||||
|
||||
return {
|
||||
start_date: now.toISOString(),
|
||||
end_date: endDate.toISOString(),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { setActiveTab, selectAssetModalState } from '@/store/assets';
|
||||
import { Asset, AppDispatch } from '@/types';
|
||||
import { useFileUpload } from './use-file-upload';
|
||||
import { useAssetForm } from './use-asset-form';
|
||||
import { useModalAnimation } from './use-modal-animation';
|
||||
import { UriTab } from './uri-tab';
|
||||
import { FileUploadTab } from './file-upload-tab';
|
||||
import React, { useEffect } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { setActiveTab, selectAssetModalState } from '@/store/assets'
|
||||
import { Asset, AppDispatch } from '@/types'
|
||||
import { useFileUpload } from './use-file-upload'
|
||||
import { useAssetForm } from './use-asset-form'
|
||||
import { useModalAnimation } from './use-modal-animation'
|
||||
import { UriTab } from './uri-tab'
|
||||
import { FileUploadTab } from './file-upload-tab'
|
||||
|
||||
interface AddAssetModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (asset: Asset) => void;
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (asset: Asset) => void
|
||||
initialData?: {
|
||||
uri?: string;
|
||||
name?: string;
|
||||
mimetype?: string;
|
||||
duration?: number;
|
||||
skipAssetCheck?: boolean;
|
||||
};
|
||||
uri?: string
|
||||
name?: string
|
||||
mimetype?: string
|
||||
duration?: number
|
||||
skipAssetCheck?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const AddAssetModal = ({
|
||||
@@ -28,10 +28,10 @@ export const AddAssetModal = ({
|
||||
onSave,
|
||||
initialData = {},
|
||||
}: AddAssetModalProps) => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { activeTab, statusMessage, uploadProgress } = useSelector(
|
||||
selectAssetModalState,
|
||||
);
|
||||
)
|
||||
|
||||
// Use custom hooks
|
||||
const {
|
||||
@@ -42,7 +42,7 @@ export const AddAssetModal = ({
|
||||
handleDragOver,
|
||||
handleDragEnter,
|
||||
handleDragLeave,
|
||||
} = useFileUpload();
|
||||
} = useFileUpload()
|
||||
|
||||
const {
|
||||
formData,
|
||||
@@ -51,21 +51,21 @@ export const AddAssetModal = ({
|
||||
isSubmitting,
|
||||
handleInputChange,
|
||||
handleSubmit,
|
||||
} = useAssetForm(onSave, onClose);
|
||||
} = useAssetForm(onSave, onClose)
|
||||
|
||||
const { isVisible, modalRef, handleClose } = useModalAnimation(
|
||||
isOpen,
|
||||
onClose,
|
||||
);
|
||||
)
|
||||
|
||||
// Reset form data when modal is opened
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// Form reset is handled by the useAssetForm hook
|
||||
}
|
||||
}, [isOpen, initialData]);
|
||||
}, [isOpen, initialData])
|
||||
|
||||
if (!isOpen && !isVisible) return null;
|
||||
if (!isOpen && !isVisible) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -199,5 +199,5 @@ export const AddAssetModal = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FormData } from '@/types';
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { FormData } from '@/types'
|
||||
|
||||
interface UriTabProps {
|
||||
formData: FormData;
|
||||
isValid: boolean;
|
||||
errorMessage: string;
|
||||
isSubmitting: boolean;
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
formData: FormData
|
||||
isValid: boolean
|
||||
errorMessage: string
|
||||
isSubmitting: boolean
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export const UriTab = ({
|
||||
@@ -56,5 +56,5 @@ export const UriTab = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import {
|
||||
updateFormData,
|
||||
validateUrl,
|
||||
@@ -7,20 +7,20 @@ import {
|
||||
resetForm,
|
||||
saveAsset,
|
||||
selectAssetModalState,
|
||||
} from '@/store/assets';
|
||||
import { selectSettings } from '@/store/settings/index';
|
||||
} from '@/store/assets'
|
||||
import { selectSettings } from '@/store/settings/index'
|
||||
import {
|
||||
getMimetype,
|
||||
getDurationForMimetype,
|
||||
getDefaultDates,
|
||||
} from '@/components/add-asset-modal/file-upload-utils';
|
||||
import { Asset, AppDispatch } from '@/types';
|
||||
} from '@/components/add-asset-modal/file-upload-utils'
|
||||
import { Asset, AppDispatch } from '@/types'
|
||||
|
||||
export const useAssetForm = (
|
||||
onSave: (asset: Asset) => void,
|
||||
onClose: () => void,
|
||||
) => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const {
|
||||
activeTab,
|
||||
formData,
|
||||
@@ -28,49 +28,49 @@ export const useAssetForm = (
|
||||
errorMessage,
|
||||
statusMessage,
|
||||
isSubmitting,
|
||||
} = useSelector(selectAssetModalState);
|
||||
const settings = useSelector(selectSettings);
|
||||
} = useSelector(selectAssetModalState)
|
||||
const settings = useSelector(selectSettings)
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
const { name, value, type, checked } = e.target
|
||||
dispatch(
|
||||
updateFormData({
|
||||
[name]: type === 'checkbox' ? checked : value,
|
||||
}),
|
||||
);
|
||||
)
|
||||
|
||||
// Validate URL when it changes
|
||||
if (name === 'uri' && activeTab === 'uri') {
|
||||
dispatch(validateUrl(value));
|
||||
dispatch(validateUrl(value))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
if (activeTab === 'uri') {
|
||||
if (!formData.uri) {
|
||||
dispatch(setErrorMessage('Please enter a URL'));
|
||||
dispatch(setValid(false));
|
||||
return;
|
||||
dispatch(setErrorMessage('Please enter a URL'))
|
||||
dispatch(setValid(false))
|
||||
return
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// Determine mimetype based on URL
|
||||
const mimetype = getMimetype(formData.uri);
|
||||
const mimetype = getMimetype(formData.uri)
|
||||
|
||||
// Get duration based on mimetype
|
||||
const duration = getDurationForMimetype(
|
||||
mimetype,
|
||||
settings.defaultDuration,
|
||||
settings.defaultStreamingDuration,
|
||||
);
|
||||
)
|
||||
|
||||
// Get default dates
|
||||
const dates = getDefaultDates();
|
||||
const dates = getDefaultDates()
|
||||
|
||||
// Create asset data
|
||||
const assetData = {
|
||||
@@ -85,25 +85,25 @@ export const useAssetForm = (
|
||||
skip_asset_check: formData.skipAssetCheck ? 1 : 0,
|
||||
duration,
|
||||
...dates,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Save the asset
|
||||
const savedAsset = await dispatch(saveAsset({ assetData })).unwrap();
|
||||
const savedAsset = await dispatch(saveAsset({ assetData })).unwrap()
|
||||
|
||||
// Call the onSave callback with the asset data
|
||||
onSave(savedAsset);
|
||||
onSave(savedAsset)
|
||||
|
||||
// Reset form
|
||||
dispatch(resetForm());
|
||||
dispatch(resetForm())
|
||||
|
||||
// Close the modal
|
||||
onClose();
|
||||
onClose()
|
||||
} catch {
|
||||
dispatch(setErrorMessage('Failed to save asset. Please try again.'));
|
||||
dispatch(setErrorMessage('Failed to save asset. Please try again.'))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
@@ -114,5 +114,5 @@ export const useAssetForm = (
|
||||
isSubmitting,
|
||||
handleInputChange,
|
||||
handleSubmit,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useRef } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import {
|
||||
uploadFile,
|
||||
saveAsset,
|
||||
@@ -8,53 +8,53 @@ import {
|
||||
setUploadProgress,
|
||||
resetForm,
|
||||
selectAssetModalState,
|
||||
} from '@/store/assets';
|
||||
import { AppDispatch } from '@/types';
|
||||
} from '@/store/assets'
|
||||
import { AppDispatch } from '@/types'
|
||||
|
||||
export const useFileUpload = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const { formData } = useSelector(selectAssetModalState);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const dropZoneRef = useRef<HTMLDivElement>(null);
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { formData } = useSelector(selectAssetModalState)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const dropZoneRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
const file = e.target.files?.[0]
|
||||
if (file) {
|
||||
handleFileUpload(file);
|
||||
handleFileUpload(file)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleFileDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
const file = e.dataTransfer.files[0]
|
||||
if (file) {
|
||||
handleFileUpload(file);
|
||||
handleFileUpload(file)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const handleDragEnter = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const handleFileUpload = async (file: File) => {
|
||||
try {
|
||||
// Upload the file
|
||||
const result = await dispatch(
|
||||
uploadFile({ file, skipAssetCheck: formData.skipAssetCheck }),
|
||||
).unwrap();
|
||||
).unwrap()
|
||||
|
||||
// Create asset data
|
||||
const assetData = {
|
||||
@@ -70,37 +70,37 @@ export const useFileUpload = () => {
|
||||
duration: result.duration,
|
||||
skip_asset_check: formData.skipAssetCheck ? 1 : 0,
|
||||
...result.dates,
|
||||
};
|
||||
}
|
||||
|
||||
// Save the asset
|
||||
await dispatch(saveAsset({ assetData })).unwrap();
|
||||
await dispatch(saveAsset({ assetData })).unwrap()
|
||||
|
||||
// Reset form and show success message
|
||||
dispatch(resetForm());
|
||||
dispatch(setStatusMessage('Upload completed.'));
|
||||
dispatch(resetForm())
|
||||
dispatch(setStatusMessage('Upload completed.'))
|
||||
|
||||
// Reset file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
|
||||
// Hide status message after 5 seconds
|
||||
setTimeout(() => {
|
||||
dispatch(setStatusMessage(''));
|
||||
}, 5000);
|
||||
dispatch(setStatusMessage(''))
|
||||
}, 5000)
|
||||
} catch (error) {
|
||||
dispatch(setErrorMessage(`Upload failed: ${(error as Error).message}`));
|
||||
dispatch(setUploadProgress(0));
|
||||
dispatch(setErrorMessage(`Upload failed: ${(error as Error).message}`))
|
||||
dispatch(setUploadProgress(0))
|
||||
|
||||
// Reset the progress bar width directly
|
||||
const progressBar = document.querySelector(
|
||||
'.progress .bar',
|
||||
) as HTMLElement | null;
|
||||
) as HTMLElement | null
|
||||
if (progressBar) {
|
||||
progressBar.style.width = '0%';
|
||||
progressBar.style.width = '0%'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
fileInputRef,
|
||||
@@ -111,5 +111,5 @@ export const useFileUpload = () => {
|
||||
handleDragEnter,
|
||||
handleDragLeave,
|
||||
handleFileUpload,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
export const useModalAnimation = (isOpen: boolean, onClose: () => void) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isClosing] = useState(false);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [isClosing] = useState(false)
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Handle animation when modal opens/closes
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// Small delay to ensure the DOM is updated before adding the visible class
|
||||
setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, 10);
|
||||
setIsVisible(true)
|
||||
}, 10)
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
setIsVisible(false)
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, [isOpen])
|
||||
|
||||
// Handle clicks outside the modal
|
||||
useEffect(() => {
|
||||
@@ -24,35 +24,35 @@ export const useModalAnimation = (isOpen: boolean, onClose: () => void) => {
|
||||
modalRef.current &&
|
||||
!modalRef.current.contains(event.target as Node)
|
||||
) {
|
||||
handleClose();
|
||||
handleClose()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
/**
|
||||
* Handle modal close
|
||||
*/
|
||||
const handleClose = () => {
|
||||
// Start the closing animation
|
||||
setIsVisible(false);
|
||||
setIsVisible(false)
|
||||
// Wait for animation to complete before calling onClose
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 300);
|
||||
};
|
||||
onClose()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
return {
|
||||
isVisible,
|
||||
isClosing,
|
||||
modalRef,
|
||||
handleClose,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
interface AlertProps {
|
||||
message: string;
|
||||
message: string
|
||||
}
|
||||
|
||||
export const Alert = ({ message }: AlertProps) => {
|
||||
@@ -14,5 +14,5 @@ export const Alert = ({ message }: AlertProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { Routes, Route } from 'react-router';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { fetchAssets } from '@/store/assets';
|
||||
import { fetchSettings } from '@/store/settings';
|
||||
import { connectWebSocket, disconnectWebSocket } from '@/store/websocket';
|
||||
import { store } from '@/store/index';
|
||||
import { Routes, Route } from 'react-router'
|
||||
import { useEffect } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { fetchAssets } from '@/store/assets'
|
||||
import { fetchSettings } from '@/store/settings'
|
||||
import { connectWebSocket, disconnectWebSocket } from '@/store/websocket'
|
||||
import { store } from '@/store/index'
|
||||
|
||||
import { Integrations } from '@/components/integrations';
|
||||
import { Navbar } from '@/components/navbar';
|
||||
import { ScheduleOverview } from '@/components/home';
|
||||
import { Settings } from '@/components/settings';
|
||||
import { SystemInfo } from '@/components/system-info';
|
||||
import { Footer } from '@/components/footer';
|
||||
import Http404 from '@/components/http-404';
|
||||
import { Integrations } from '@/components/integrations'
|
||||
import { Navbar } from '@/components/navbar'
|
||||
import { ScheduleOverview } from '@/components/home'
|
||||
import { Settings } from '@/components/settings'
|
||||
import { SystemInfo } from '@/components/system-info'
|
||||
import { Footer } from '@/components/footer'
|
||||
import Http404 from '@/components/http-404'
|
||||
|
||||
export const App = () => {
|
||||
const dispatch = useDispatch<typeof store.dispatch>();
|
||||
const dispatch = useDispatch<typeof store.dispatch>()
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAssets());
|
||||
dispatch(fetchSettings());
|
||||
dispatch(connectWebSocket());
|
||||
dispatch(fetchAssets())
|
||||
dispatch(fetchSettings())
|
||||
dispatch(connectWebSocket())
|
||||
|
||||
// Cleanup function to disconnect WebSocket when component unmounts
|
||||
return () => {
|
||||
dispatch(disconnectWebSocket());
|
||||
};
|
||||
}, [dispatch]);
|
||||
dispatch(disconnectWebSocket())
|
||||
}
|
||||
}, [dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -42,5 +42,5 @@ export const App = () => {
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { FaDownload, FaPencilAlt, FaTrashAlt } from 'react-icons/fa';
|
||||
import classNames from 'classnames';
|
||||
import { FaDownload, FaPencilAlt, FaTrashAlt } from 'react-icons/fa'
|
||||
import classNames from 'classnames'
|
||||
|
||||
interface ActionButtonsProps {
|
||||
isDisabled: boolean;
|
||||
handleDownload: (event: React.MouseEvent) => void;
|
||||
handleEdit: () => void;
|
||||
handleDelete: () => void;
|
||||
isDisabled: boolean
|
||||
handleDownload: (event: React.MouseEvent) => void
|
||||
handleEdit: () => void
|
||||
handleDelete: () => void
|
||||
}
|
||||
|
||||
export const ActionButtons = ({
|
||||
@@ -23,9 +23,9 @@ export const ActionButtons = ({
|
||||
{
|
||||
disabled: isDisabled,
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const tooltipText = isDisabled ? 'Asset is currently being processed' : '';
|
||||
const tooltipText = isDisabled ? 'Asset is currently being processed' : ''
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -63,5 +63,5 @@ export const ActionButtons = ({
|
||||
<FaTrashAlt />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
import { GiHamburgerMenu } from 'react-icons/gi';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, forwardRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { GiHamburgerMenu } from 'react-icons/gi'
|
||||
import classNames from 'classnames'
|
||||
import { useEffect, forwardRef, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
import { toggleAssetEnabled, fetchAssets } from '@/store/assets';
|
||||
import { AssetRowProps, AppDispatch } from '@/types';
|
||||
import { toggleAssetEnabled, fetchAssets } from '@/store/assets'
|
||||
import { AssetRowProps, AppDispatch } from '@/types'
|
||||
|
||||
import {
|
||||
formatDate,
|
||||
formatDuration,
|
||||
handleDelete,
|
||||
handleDownload,
|
||||
} from '@/components/asset-row/utils';
|
||||
import { MimetypeIcon } from '@/components/asset-row/mimetype-icon';
|
||||
import { ActionButtons } from '@/components/asset-row/action-buttons';
|
||||
} from '@/components/asset-row/utils'
|
||||
import { MimetypeIcon } from '@/components/asset-row/mimetype-icon'
|
||||
import { ActionButtons } from '@/components/asset-row/action-buttons'
|
||||
|
||||
export const AssetRow = forwardRef<HTMLTableRowElement, AssetRowProps>(
|
||||
(props, ref) => {
|
||||
const defaultDateFormat = 'mm/dd/yyyy';
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
const [dateFormat, setDateFormat] = useState(defaultDateFormat);
|
||||
const [use24HourClock, setUse24HourClock] = useState(false);
|
||||
const defaultDateFormat = 'mm/dd/yyyy'
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const [isDisabled, setIsDisabled] = useState(false)
|
||||
const [dateFormat, setDateFormat] = useState(defaultDateFormat)
|
||||
const [use24HourClock, setUse24HourClock] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDateFormat = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v2/device_settings');
|
||||
const data = await response.json();
|
||||
setDateFormat(data.date_format);
|
||||
setUse24HourClock(data.use_24_hour_clock);
|
||||
const response = await fetch('/api/v2/device_settings')
|
||||
const data = await response.json()
|
||||
setDateFormat(data.date_format)
|
||||
setUse24HourClock(data.use_24_hour_clock)
|
||||
} catch {
|
||||
setDateFormat(defaultDateFormat);
|
||||
setUse24HourClock(false);
|
||||
setDateFormat(defaultDateFormat)
|
||||
setUse24HourClock(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fetchDateFormat();
|
||||
}, []);
|
||||
fetchDateFormat()
|
||||
}, [])
|
||||
|
||||
const handleToggle = async () => {
|
||||
const newValue = !props.isEnabled ? 1 : 0;
|
||||
setIsDisabled(true);
|
||||
const newValue = !props.isEnabled ? 1 : 0
|
||||
setIsDisabled(true)
|
||||
try {
|
||||
await dispatch(
|
||||
toggleAssetEnabled({ assetId: props.assetId, newValue }),
|
||||
).unwrap();
|
||||
dispatch(fetchAssets());
|
||||
).unwrap()
|
||||
dispatch(fetchAssets())
|
||||
} catch {
|
||||
} finally {
|
||||
setIsDisabled(false);
|
||||
setIsDisabled(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDownloadWrapper = (event: React.MouseEvent) => {
|
||||
handleDownload(event, props.assetId);
|
||||
};
|
||||
handleDownload(event, props.assetId)
|
||||
}
|
||||
|
||||
const handleDeleteWrapper = () => {
|
||||
handleDelete(props.assetId, setIsDisabled, dispatch, fetchAssets);
|
||||
};
|
||||
handleDelete(props.assetId, setIsDisabled, dispatch, fetchAssets)
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
if (props.onEditAsset) {
|
||||
@@ -74,9 +74,9 @@ export const AssetRow = forwardRef<HTMLTableRowElement, AssetRowProps>(
|
||||
is_enabled: props.isEnabled,
|
||||
nocache: props.nocache,
|
||||
skip_asset_check: props.skipAssetCheck,
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -184,6 +184,6 @@ export const AssetRow = forwardRef<HTMLTableRowElement, AssetRowProps>(
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
)
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FaImage, FaVideo, FaGlobe } from 'react-icons/fa';
|
||||
import { FaImage, FaVideo, FaGlobe } from 'react-icons/fa'
|
||||
|
||||
interface MimetypeIconProps {
|
||||
mimetype: string;
|
||||
className: string;
|
||||
style: React.CSSProperties;
|
||||
mimetype: string
|
||||
className: string
|
||||
style: React.CSSProperties
|
||||
}
|
||||
|
||||
export const MimetypeIcon = ({
|
||||
@@ -16,5 +16,5 @@ export const MimetypeIcon = ({
|
||||
video: <FaVideo className={className} style={style} />,
|
||||
webpage: <FaGlobe className={className} style={style} />,
|
||||
default: <FaGlobe className={className} style={style} />,
|
||||
}[mimetype];
|
||||
};
|
||||
}[mimetype]
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import Swal from 'sweetalert2';
|
||||
import { SWEETALERT_TIMER } from '@/constants';
|
||||
import { AppDispatch } from '@/types';
|
||||
import { fetchAssets } from '@/store/assets';
|
||||
import Swal from 'sweetalert2'
|
||||
import { SWEETALERT_TIMER } from '@/constants'
|
||||
import { AppDispatch } from '@/types'
|
||||
import { fetchAssets } from '@/store/assets'
|
||||
|
||||
export const formatDate = (
|
||||
date: string,
|
||||
dateFormat: string,
|
||||
use24HourClock = false,
|
||||
): string => {
|
||||
if (!date) return '';
|
||||
if (!date) return ''
|
||||
|
||||
// Create a Date object from the input date string
|
||||
const dateObj = new Date(date);
|
||||
const dateObj = new Date(date)
|
||||
|
||||
// Check if the date is valid
|
||||
if (isNaN(dateObj.getTime())) return date;
|
||||
if (isNaN(dateObj.getTime())) return date
|
||||
|
||||
// Extract the separator from the format
|
||||
const separator = dateFormat.includes('/')
|
||||
@@ -23,10 +23,10 @@ export const formatDate = (
|
||||
? '-'
|
||||
: dateFormat.includes('.')
|
||||
? '.'
|
||||
: '/';
|
||||
: '/'
|
||||
|
||||
// Extract the format parts from the dateFormat string
|
||||
const formatParts = dateFormat.split(/[\/\-\.]/);
|
||||
const formatParts = dateFormat.split(/[\/\-\.]/)
|
||||
|
||||
// Set up the date formatting options
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
@@ -37,73 +37,73 @@ export const formatDate = (
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: !use24HourClock, // Use 12-hour format if use24HourClock is false
|
||||
};
|
||||
}
|
||||
|
||||
// Create a formatter with the specified options
|
||||
const formatter = new Intl.DateTimeFormat('en-US', options);
|
||||
const formatter = new Intl.DateTimeFormat('en-US', options)
|
||||
|
||||
// Format the date and get the parts
|
||||
const formattedParts = formatter.formatToParts(dateObj);
|
||||
const formattedParts = formatter.formatToParts(dateObj)
|
||||
|
||||
// Extract the formatted values with null checks
|
||||
const month = formattedParts.find((p) => p.type === 'month')?.value || '';
|
||||
const day = formattedParts.find((p) => p.type === 'day')?.value || '';
|
||||
const year = formattedParts.find((p) => p.type === 'year')?.value || '';
|
||||
const hour = formattedParts.find((p) => p.type === 'hour')?.value || '';
|
||||
const minute = formattedParts.find((p) => p.type === 'minute')?.value || '';
|
||||
const second = formattedParts.find((p) => p.type === 'second')?.value || '';
|
||||
const month = formattedParts.find((p) => p.type === 'month')?.value || ''
|
||||
const day = formattedParts.find((p) => p.type === 'day')?.value || ''
|
||||
const year = formattedParts.find((p) => p.type === 'year')?.value || ''
|
||||
const hour = formattedParts.find((p) => p.type === 'hour')?.value || ''
|
||||
const minute = formattedParts.find((p) => p.type === 'minute')?.value || ''
|
||||
const second = formattedParts.find((p) => p.type === 'second')?.value || ''
|
||||
|
||||
// Get the period (AM/PM) if using 12-hour format
|
||||
let period = '';
|
||||
let period = ''
|
||||
if (!use24HourClock) {
|
||||
const periodPart = formattedParts.find((p) => p.type === 'dayPeriod');
|
||||
const periodPart = formattedParts.find((p) => p.type === 'dayPeriod')
|
||||
if (periodPart) {
|
||||
period = ` ${periodPart.value}`;
|
||||
period = ` ${periodPart.value}`
|
||||
}
|
||||
}
|
||||
|
||||
// Build the date part according to the format
|
||||
let datePart = '';
|
||||
let datePart = ''
|
||||
|
||||
// Determine the order based on the format
|
||||
if (formatParts[0].includes('mm')) {
|
||||
datePart = `${month}${separator}${day}${separator}${year}`;
|
||||
datePart = `${month}${separator}${day}${separator}${year}`
|
||||
} else if (formatParts[0].includes('dd')) {
|
||||
datePart = `${day}${separator}${month}${separator}${year}`;
|
||||
datePart = `${day}${separator}${month}${separator}${year}`
|
||||
} else if (formatParts[0].includes('yyyy')) {
|
||||
datePart = `${year}${separator}${month}${separator}${day}`;
|
||||
datePart = `${year}${separator}${month}${separator}${day}`
|
||||
} else {
|
||||
// Default to mm/dd/yyyy if format is not recognized
|
||||
datePart = `${month}${separator}${day}${separator}${year}`;
|
||||
datePart = `${month}${separator}${day}${separator}${year}`
|
||||
}
|
||||
|
||||
// Add the time part with AM/PM if using 12-hour format
|
||||
const timePart = `${hour}:${minute}:${second}${period}`;
|
||||
const timePart = `${hour}:${minute}:${second}${period}`
|
||||
|
||||
return `${datePart} ${timePart}`;
|
||||
};
|
||||
return `${datePart} ${timePart}`
|
||||
}
|
||||
|
||||
export const formatDuration = (seconds: number | string): string => {
|
||||
let durationString = '';
|
||||
const secInt = parseInt(seconds.toString());
|
||||
let durationString = ''
|
||||
const secInt = parseInt(seconds.toString())
|
||||
|
||||
const hours = Math.floor(secInt / 3600);
|
||||
const hours = Math.floor(secInt / 3600)
|
||||
if (hours > 0) {
|
||||
durationString += `${hours} hours `;
|
||||
durationString += `${hours} hours `
|
||||
}
|
||||
|
||||
const minutes = Math.floor(secInt / 60) % 60;
|
||||
const minutes = Math.floor(secInt / 60) % 60
|
||||
if (minutes > 0) {
|
||||
durationString += `${minutes} min `;
|
||||
durationString += `${minutes} min `
|
||||
}
|
||||
|
||||
const secs = secInt % 60;
|
||||
const secs = secInt % 60
|
||||
if (secs > 0) {
|
||||
durationString += `${secs} sec`;
|
||||
durationString += `${secs} sec`
|
||||
}
|
||||
|
||||
return durationString;
|
||||
};
|
||||
return durationString
|
||||
}
|
||||
|
||||
export const handleDelete = async (
|
||||
assetId: string,
|
||||
@@ -128,21 +128,21 @@ export const handleDelete = async (
|
||||
cancelButton: 'swal2-cancel',
|
||||
actions: 'swal2-actions',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
// Disable the row while deleting
|
||||
setIsDisabled(true);
|
||||
setIsDisabled(true)
|
||||
|
||||
// Make API call to delete the asset
|
||||
const response = await fetch(`/api/v2/assets/${assetId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// Refresh the assets list after successful deletion
|
||||
dispatch(fetchAssetsAction());
|
||||
dispatch(fetchAssetsAction())
|
||||
|
||||
// Show success message
|
||||
Swal.fire({
|
||||
@@ -156,7 +156,7 @@ export const handleDelete = async (
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
},
|
||||
});
|
||||
})
|
||||
} else {
|
||||
// Show error message
|
||||
Swal.fire({
|
||||
@@ -169,7 +169,7 @@ export const handleDelete = async (
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
Swal.fire({
|
||||
@@ -182,49 +182,49 @@ export const handleDelete = async (
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
});
|
||||
})
|
||||
} finally {
|
||||
setIsDisabled(false);
|
||||
setIsDisabled(false)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const handleDownload = async (
|
||||
event: React.MouseEvent,
|
||||
assetId: string,
|
||||
): Promise<void> => {
|
||||
event.preventDefault();
|
||||
event.preventDefault()
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v2/assets/${assetId}/content`);
|
||||
const result = await response.json();
|
||||
const response = await fetch(`/api/v2/assets/${assetId}/content`)
|
||||
const result = await response.json()
|
||||
|
||||
if (result.type === 'url') {
|
||||
window.open(result.url);
|
||||
window.open(result.url)
|
||||
} else if (result.type === 'file') {
|
||||
// Convert base64 to byte array
|
||||
const content = atob(result.content);
|
||||
const bytes = new Uint8Array(content.length);
|
||||
const content = atob(result.content)
|
||||
const bytes = new Uint8Array(content.length)
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
bytes[i] = content.charCodeAt(i);
|
||||
bytes[i] = content.charCodeAt(i)
|
||||
}
|
||||
|
||||
const mimetype = result.mimetype;
|
||||
const filename = result.filename;
|
||||
const mimetype = result.mimetype
|
||||
const filename = result.filename
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([bytes], { type: mimetype });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const blob = new Blob([bytes], { type: mimetype })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
const a = document.createElement('a');
|
||||
document.body.appendChild(a);
|
||||
a.download = filename;
|
||||
a.href = url;
|
||||
a.click();
|
||||
const a = document.createElement('a')
|
||||
document.body.appendChild(a)
|
||||
a.download = filename
|
||||
a.href = url
|
||||
a.click()
|
||||
|
||||
// Clean up
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url)
|
||||
a.remove()
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { EditFormData } from '@/types';
|
||||
import { EditFormData } from '@/types'
|
||||
|
||||
interface AdvancedFieldsProps {
|
||||
formData: EditFormData;
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
formData: EditFormData
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export const AdvancedFields = ({
|
||||
@@ -42,5 +42,5 @@ export const AdvancedFields = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AssetEditData } from '@/types';
|
||||
import { AssetEditData } from '@/types'
|
||||
|
||||
interface AssetLocationFieldProps {
|
||||
asset: AssetEditData | null;
|
||||
asset: AssetEditData | null
|
||||
}
|
||||
|
||||
export const AssetLocationField = ({ asset }: AssetLocationFieldProps) => {
|
||||
@@ -20,5 +20,5 @@ export const AssetLocationField = ({ asset }: AssetLocationFieldProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { EditFormData } from '@/types';
|
||||
import { EditFormData } from '@/types'
|
||||
|
||||
interface AssetTypeFieldProps {
|
||||
formData: EditFormData;
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
formData: EditFormData
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
|
||||
}
|
||||
|
||||
export const AssetTypeField = ({
|
||||
@@ -28,5 +28,5 @@ export const AssetTypeField = ({
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
interface DateFieldsProps {
|
||||
startDateDate: string;
|
||||
startDateTime: string;
|
||||
endDateDate: string;
|
||||
endDateTime: string;
|
||||
startDateDate: string
|
||||
startDateTime: string
|
||||
endDateDate: string
|
||||
endDateTime: string
|
||||
handleDateChange: (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
type: string,
|
||||
) => void;
|
||||
) => void
|
||||
}
|
||||
|
||||
export const DateFields = ({
|
||||
@@ -59,5 +59,5 @@ export const DateFields = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { EditFormData } from '@/types';
|
||||
import { EditFormData } from '@/types'
|
||||
|
||||
interface DurationFieldProps {
|
||||
formData: EditFormData;
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
formData: EditFormData
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export const DurationField = ({
|
||||
@@ -24,5 +24,5 @@ export const DurationField = ({
|
||||
seconds
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { AssetEditData, EditFormData } from '@/types';
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { AssetEditData, EditFormData } from '@/types'
|
||||
|
||||
import { handleSubmit } from '@/components/edit-asset-modal/utils';
|
||||
import { NameField } from '@/components/edit-asset-modal/name-field';
|
||||
import { AssetLocationField } from '@/components/edit-asset-modal/asset-location-field';
|
||||
import { AssetTypeField } from '@/components/edit-asset-modal/asset-type-field';
|
||||
import { PlayForField } from '@/components/edit-asset-modal/play-for-field';
|
||||
import { DateFields } from '@/components/edit-asset-modal/date-fields';
|
||||
import { DurationField } from '@/components/edit-asset-modal/duration-field';
|
||||
import { ModalFooter } from '@/components/edit-asset-modal/modal-footer';
|
||||
import { AdvancedFields } from '@/components/edit-asset-modal/advanced';
|
||||
import { handleSubmit } from '@/components/edit-asset-modal/utils'
|
||||
import { NameField } from '@/components/edit-asset-modal/name-field'
|
||||
import { AssetLocationField } from '@/components/edit-asset-modal/asset-location-field'
|
||||
import { AssetTypeField } from '@/components/edit-asset-modal/asset-type-field'
|
||||
import { PlayForField } from '@/components/edit-asset-modal/play-for-field'
|
||||
import { DateFields } from '@/components/edit-asset-modal/date-fields'
|
||||
import { DurationField } from '@/components/edit-asset-modal/duration-field'
|
||||
import { ModalFooter } from '@/components/edit-asset-modal/modal-footer'
|
||||
import { AdvancedFields } from '@/components/edit-asset-modal/advanced'
|
||||
|
||||
interface EditAssetModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
asset: AssetEditData | null;
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
asset: AssetEditData | null
|
||||
}
|
||||
|
||||
export const EditAssetModal = ({
|
||||
@@ -24,9 +24,9 @@ export const EditAssetModal = ({
|
||||
onClose,
|
||||
asset,
|
||||
}: EditAssetModalProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const dispatch = useDispatch()
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [formData, setFormData] = useState<EditFormData>({
|
||||
name: '',
|
||||
start_date: '',
|
||||
@@ -35,33 +35,33 @@ export const EditAssetModal = ({
|
||||
mimetype: 'webpage',
|
||||
nocache: false,
|
||||
skip_asset_check: false,
|
||||
});
|
||||
const [loopTimes, setLoopTimes] = useState('manual');
|
||||
const [startDateDate, setStartDateDate] = useState('');
|
||||
const [startDateTime, setStartDateTime] = useState('');
|
||||
const [endDateDate, setEndDateDate] = useState('');
|
||||
const [endDateTime, setEndDateTime] = useState('');
|
||||
})
|
||||
const [loopTimes, setLoopTimes] = useState('manual')
|
||||
const [startDateDate, setStartDateDate] = useState('')
|
||||
const [startDateTime, setStartDateTime] = useState('')
|
||||
const [endDateDate, setEndDateDate] = useState('')
|
||||
const [endDateTime, setEndDateTime] = useState('')
|
||||
|
||||
// Initialize form data when asset changes
|
||||
useEffect(() => {
|
||||
if (asset) {
|
||||
// Parse dates from UTC
|
||||
const startDate = new Date(asset.start_date);
|
||||
const endDate = new Date(asset.end_date);
|
||||
const startDate = new Date(asset.start_date)
|
||||
const endDate = new Date(asset.end_date)
|
||||
|
||||
// Format date and time parts in local timezone
|
||||
const formatDatePart = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
const formatTimePart = (date: Date) => {
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
};
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${hours}:${minutes}`
|
||||
}
|
||||
|
||||
setFormData({
|
||||
name: asset.name || '',
|
||||
@@ -71,72 +71,72 @@ export const EditAssetModal = ({
|
||||
mimetype: asset.mimetype || 'webpage',
|
||||
nocache: asset.nocache || false,
|
||||
skip_asset_check: asset.skip_asset_check || false,
|
||||
});
|
||||
})
|
||||
|
||||
setStartDateDate(formatDatePart(startDate));
|
||||
setStartDateTime(formatTimePart(startDate));
|
||||
setEndDateDate(formatDatePart(endDate));
|
||||
setEndDateTime(formatTimePart(endDate));
|
||||
setStartDateDate(formatDatePart(startDate))
|
||||
setStartDateTime(formatTimePart(startDate))
|
||||
setEndDateDate(formatDatePart(endDate))
|
||||
setEndDateTime(formatTimePart(endDate))
|
||||
}
|
||||
}, [asset]);
|
||||
}, [asset])
|
||||
|
||||
// Handle modal visibility
|
||||
useEffect(() => {
|
||||
setLoopTimes('manual');
|
||||
setLoopTimes('manual')
|
||||
|
||||
if (isOpen) {
|
||||
setIsVisible(true);
|
||||
setIsVisible(true)
|
||||
} else {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
}, 300); // Match the transition duration
|
||||
return () => clearTimeout(timer);
|
||||
setIsVisible(false)
|
||||
}, 300) // Match the transition duration
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, [isOpen])
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
setIsVisible(false)
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 300); // Match the transition duration
|
||||
};
|
||||
onClose()
|
||||
}, 300) // Match the transition duration
|
||||
}
|
||||
|
||||
const handleModalClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
// Only close if clicking the modal backdrop (outside the modal content)
|
||||
if (e.target === e.currentTarget) {
|
||||
handleClose();
|
||||
handleClose()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
) => {
|
||||
const { name, value, type } = e.target;
|
||||
const { name, value, type } = e.target
|
||||
const checked =
|
||||
e.target instanceof HTMLInputElement ? e.target.checked : false;
|
||||
e.target instanceof HTMLInputElement ? e.target.checked : false
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: type === 'checkbox' ? checked : value,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const handleDateChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
type: string,
|
||||
) => {
|
||||
const { value } = e.target;
|
||||
const { value } = e.target
|
||||
if (type === 'startDate') {
|
||||
setStartDateDate(value);
|
||||
setStartDateDate(value)
|
||||
} else if (type === 'startTime') {
|
||||
setStartDateTime(value);
|
||||
setStartDateTime(value)
|
||||
} else if (type === 'endDate') {
|
||||
setEndDateDate(value);
|
||||
setEndDateDate(value)
|
||||
} else if (type === 'endTime') {
|
||||
setEndDateTime(value);
|
||||
setEndDateTime(value)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!isOpen && !isVisible) return null;
|
||||
if (!isOpen && !isVisible) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -248,5 +248,5 @@ export const EditAssetModal = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,21 +3,21 @@ import {
|
||||
AssetEditData,
|
||||
EditFormData,
|
||||
HandleSubmitParams,
|
||||
} from '@/types';
|
||||
} from '@/types'
|
||||
|
||||
interface ModalFooterProps {
|
||||
asset: AssetEditData | null;
|
||||
formData: EditFormData;
|
||||
startDateDate: string;
|
||||
startDateTime: string;
|
||||
endDateDate: string;
|
||||
endDateTime: string;
|
||||
dispatch: AppDispatch;
|
||||
onClose: () => void;
|
||||
handleClose: () => void;
|
||||
isSubmitting: boolean;
|
||||
handleSubmit: (params: HandleSubmitParams) => void;
|
||||
setIsSubmitting: (isSubmitting: boolean) => void;
|
||||
asset: AssetEditData | null
|
||||
formData: EditFormData
|
||||
startDateDate: string
|
||||
startDateTime: string
|
||||
endDateDate: string
|
||||
endDateTime: string
|
||||
dispatch: AppDispatch
|
||||
onClose: () => void
|
||||
handleClose: () => void
|
||||
isSubmitting: boolean
|
||||
handleSubmit: (params: HandleSubmitParams) => void
|
||||
setIsSubmitting: (isSubmitting: boolean) => void
|
||||
}
|
||||
|
||||
export const ModalFooter = ({
|
||||
@@ -71,5 +71,5 @@ export const ModalFooter = ({
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { EditFormData } from '@/types';
|
||||
import { EditFormData } from '@/types'
|
||||
|
||||
interface NameFieldProps {
|
||||
formData: EditFormData;
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
formData: EditFormData
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export const NameField = ({ formData, handleInputChange }: NameFieldProps) => {
|
||||
@@ -20,5 +20,5 @@ export const NameField = ({ formData, handleInputChange }: NameFieldProps) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { handleLoopTimesChange } from '@/components/edit-asset-modal/utils';
|
||||
import { EditFormData } from '@/types';
|
||||
import { handleLoopTimesChange } from '@/components/edit-asset-modal/utils'
|
||||
import { EditFormData } from '@/types'
|
||||
|
||||
interface PlayForFieldProps {
|
||||
loopTimes: string;
|
||||
startDateDate: string;
|
||||
startDateTime: string;
|
||||
setLoopTimes: (value: string) => void;
|
||||
setEndDateDate: (value: string) => void;
|
||||
setEndDateTime: (value: string) => void;
|
||||
setFormData: (updater: (prev: EditFormData) => EditFormData) => void;
|
||||
loopTimes: string
|
||||
startDateDate: string
|
||||
startDateTime: string
|
||||
setLoopTimes: (value: string) => void
|
||||
setEndDateDate: (value: string) => void
|
||||
setEndDateTime: (value: string) => void
|
||||
setFormData: (updater: (prev: EditFormData) => EditFormData) => void
|
||||
}
|
||||
|
||||
export const PlayForField = ({
|
||||
@@ -49,5 +49,5 @@ export const PlayForField = ({
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { fetchAssets, updateAssetOrder } from '@/store/assets';
|
||||
import { Asset, RootState, EditFormData, HandleSubmitParams } from '@/types';
|
||||
import { fetchAssets, updateAssetOrder } from '@/store/assets'
|
||||
import { Asset, RootState, EditFormData, HandleSubmitParams } from '@/types'
|
||||
|
||||
interface HandleLoopTimesChangeParams {
|
||||
e: React.ChangeEvent<HTMLSelectElement>;
|
||||
startDateDate: string;
|
||||
startDateTime: string;
|
||||
setLoopTimes: (value: string) => void;
|
||||
setEndDateDate: (value: string) => void;
|
||||
setEndDateTime: (value: string) => void;
|
||||
setFormData: (updater: (prev: EditFormData) => EditFormData) => void;
|
||||
e: React.ChangeEvent<HTMLSelectElement>
|
||||
startDateDate: string
|
||||
startDateTime: string
|
||||
setLoopTimes: (value: string) => void
|
||||
setEndDateDate: (value: string) => void
|
||||
setEndDateTime: (value: string) => void
|
||||
setFormData: (updater: (prev: EditFormData) => EditFormData) => void
|
||||
}
|
||||
|
||||
export const handleSubmit = async ({
|
||||
@@ -23,13 +23,13 @@ export const handleSubmit = async ({
|
||||
onClose,
|
||||
setIsSubmitting,
|
||||
}: HandleSubmitParams) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
// Combine date and time parts
|
||||
const startDate = new Date(`${startDateDate}T${startDateTime}`);
|
||||
const endDate = new Date(`${endDateDate}T${endDateTime}`);
|
||||
const startDate = new Date(`${startDateDate}T${startDateTime}`)
|
||||
const endDate = new Date(`${endDateDate}T${endDateTime}`)
|
||||
|
||||
// Prepare data for API
|
||||
const updatedAsset = {
|
||||
@@ -39,7 +39,7 @@ export const handleSubmit = async ({
|
||||
asset_id: asset.id,
|
||||
is_enabled: asset.is_enabled,
|
||||
play_order: asset.play_order,
|
||||
};
|
||||
}
|
||||
|
||||
// Make API call to update asset
|
||||
const response = await fetch(`/api/v2/assets/${asset.id}`, {
|
||||
@@ -48,36 +48,36 @@ export const handleSubmit = async ({
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updatedAsset),
|
||||
});
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update asset');
|
||||
throw new Error('Failed to update asset')
|
||||
}
|
||||
|
||||
// Get active assets from Redux store and update order
|
||||
const activeAssetIds = dispatch((_: unknown, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const state = getState()
|
||||
return state.assets.items
|
||||
.filter((asset: Asset) => asset.is_active)
|
||||
.sort((a: Asset, b: Asset) => a.play_order - b.play_order)
|
||||
.map((asset: Asset) => asset.asset_id)
|
||||
.join(',');
|
||||
});
|
||||
.join(',')
|
||||
})
|
||||
|
||||
// Update the asset order so that active assets won't be reordered
|
||||
// unexpectedly when the asset is updated and saved.
|
||||
await dispatch(updateAssetOrder(activeAssetIds));
|
||||
await dispatch(updateAssetOrder(activeAssetIds))
|
||||
|
||||
// Refresh assets list
|
||||
dispatch(fetchAssets());
|
||||
dispatch(fetchAssets())
|
||||
|
||||
// Close modal
|
||||
onClose();
|
||||
onClose()
|
||||
} catch {
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const handleLoopTimesChange = ({
|
||||
e,
|
||||
@@ -88,57 +88,57 @@ export const handleLoopTimesChange = ({
|
||||
setEndDateTime,
|
||||
setFormData,
|
||||
}: HandleLoopTimesChangeParams) => {
|
||||
const playFor = e.target.value;
|
||||
setLoopTimes(playFor);
|
||||
const playFor = e.target.value
|
||||
setLoopTimes(playFor)
|
||||
|
||||
if (playFor === 'manual') {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// Get current start date and time in UTC
|
||||
const startDate = new Date(`${startDateDate}T${startDateTime}Z`);
|
||||
let endDate = new Date(startDate);
|
||||
const startDate = new Date(`${startDateDate}T${startDateTime}Z`)
|
||||
let endDate = new Date(startDate)
|
||||
|
||||
// Add time based on selection
|
||||
switch (playFor) {
|
||||
case 'day':
|
||||
endDate.setUTCDate(endDate.getUTCDate() + 1);
|
||||
break;
|
||||
endDate.setUTCDate(endDate.getUTCDate() + 1)
|
||||
break
|
||||
case 'week':
|
||||
endDate.setUTCDate(endDate.getUTCDate() + 7);
|
||||
break;
|
||||
endDate.setUTCDate(endDate.getUTCDate() + 7)
|
||||
break
|
||||
case 'month':
|
||||
endDate.setUTCMonth(endDate.getUTCMonth() + 1);
|
||||
break;
|
||||
endDate.setUTCMonth(endDate.getUTCMonth() + 1)
|
||||
break
|
||||
case 'year':
|
||||
endDate.setUTCFullYear(endDate.getUTCFullYear() + 1);
|
||||
break;
|
||||
endDate.setUTCFullYear(endDate.getUTCFullYear() + 1)
|
||||
break
|
||||
case 'forever':
|
||||
endDate.setUTCFullYear(9999);
|
||||
break;
|
||||
endDate.setUTCFullYear(9999)
|
||||
break
|
||||
}
|
||||
|
||||
// Format the new end date in ISO format with timezone
|
||||
const formatDatePart = (date: Date) => {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
const year = date.getUTCFullYear()
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getUTCDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
const formatTimePart = (date: Date) => {
|
||||
const hours = String(date.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
};
|
||||
const hours = String(date.getUTCHours()).padStart(2, '0')
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, '0')
|
||||
return `${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// Update end date and time
|
||||
setEndDateDate(formatDatePart(endDate));
|
||||
setEndDateTime(formatTimePart(endDate));
|
||||
setEndDateDate(formatDatePart(endDate))
|
||||
setEndDateTime(formatTimePart(endDate))
|
||||
|
||||
// Update formData with the ISO string
|
||||
setFormData((prev: EditFormData) => ({
|
||||
...prev,
|
||||
end_date: endDate.toISOString(),
|
||||
}));
|
||||
};
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
interface EmptyAssetMessageProps {
|
||||
onAddAssetClick: (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
onAddAssetClick: (e: React.MouseEvent<HTMLAnchorElement>) => void
|
||||
}
|
||||
|
||||
export const EmptyAssetMessage = ({
|
||||
@@ -13,5 +13,5 @@ export const EmptyAssetMessage = ({
|
||||
</a>{' '}
|
||||
now.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -67,16 +67,16 @@ export const Footer = () => {
|
||||
src={(() => {
|
||||
const url = new URL(
|
||||
'https://img.shields.io/github/stars/Screenly/Anthias',
|
||||
);
|
||||
)
|
||||
const params = new URLSearchParams({
|
||||
style: 'for-the-badge',
|
||||
labelColor: '#EBF0F4',
|
||||
color: '#FFE11A',
|
||||
logo: 'github',
|
||||
logoColor: 'black',
|
||||
});
|
||||
url.search = params.toString();
|
||||
return url.toString();
|
||||
})
|
||||
url.search = params.toString()
|
||||
return url.toString()
|
||||
})()}
|
||||
/>
|
||||
</a>
|
||||
@@ -90,5 +90,5 @@ export const Footer = () => {
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,121 +1,121 @@
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import $ from 'jquery';
|
||||
import 'bootstrap/js/dist/tooltip';
|
||||
import classNames from 'classnames'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import $ from 'jquery'
|
||||
import 'bootstrap/js/dist/tooltip'
|
||||
import {
|
||||
fetchAssets,
|
||||
selectActiveAssets,
|
||||
selectInactiveAssets,
|
||||
} from '@/store/assets';
|
||||
import { AssetEditData, AppDispatch } from '@/types';
|
||||
} from '@/store/assets'
|
||||
import { AssetEditData, AppDispatch } from '@/types'
|
||||
|
||||
import { EmptyAssetMessage } from '@/components/empty-asset-message';
|
||||
import { InactiveAssetsTable } from '@/components/inactive-assets';
|
||||
import { ActiveAssetsTable } from '@/components/active-assets';
|
||||
import { AddAssetModal } from '@/components/add-asset-modal';
|
||||
import { EditAssetModal } from '@/components/edit-asset-modal';
|
||||
import { EmptyAssetMessage } from '@/components/empty-asset-message'
|
||||
import { InactiveAssetsTable } from '@/components/inactive-assets'
|
||||
import { ActiveAssetsTable } from '@/components/active-assets'
|
||||
import { AddAssetModal } from '@/components/add-asset-modal'
|
||||
import { EditAssetModal } from '@/components/edit-asset-modal'
|
||||
|
||||
interface JQueryWithTooltip extends JQuery<HTMLElement> {
|
||||
tooltip(options?: object | string): JQueryWithTooltip;
|
||||
tooltip(options?: object | string): JQueryWithTooltip
|
||||
}
|
||||
|
||||
export const ScheduleOverview = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const activeAssets = useSelector(selectActiveAssets);
|
||||
const inactiveAssets = useSelector(selectInactiveAssets);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [assetToEdit, setAssetToEdit] = useState<AssetEditData | null>(null);
|
||||
const [playerName, setPlayerName] = useState('');
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const activeAssets = useSelector(selectActiveAssets)
|
||||
const inactiveAssets = useSelector(selectInactiveAssets)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
||||
const [assetToEdit, setAssetToEdit] = useState<AssetEditData | null>(null)
|
||||
const [playerName, setPlayerName] = useState('')
|
||||
|
||||
const fetchPlayerName = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v2/device_settings');
|
||||
const data = await response.json();
|
||||
setPlayerName(data.player_name || '');
|
||||
const response = await fetch('/api/v2/device_settings')
|
||||
const data = await response.json()
|
||||
setPlayerName(data.player_name || '')
|
||||
} catch {}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const title = playerName
|
||||
? `${playerName} · Schedule Overview`
|
||||
: 'Schedule Overview';
|
||||
document.title = title;
|
||||
dispatch(fetchAssets());
|
||||
fetchPlayerName();
|
||||
}, [dispatch, playerName]);
|
||||
: 'Schedule Overview'
|
||||
document.title = title
|
||||
dispatch(fetchAssets())
|
||||
fetchPlayerName()
|
||||
}, [dispatch, playerName])
|
||||
|
||||
// Initialize tooltips
|
||||
useEffect(() => {
|
||||
const initializeTooltips = () => {
|
||||
($('[data-toggle="tooltip"]') as JQueryWithTooltip).tooltip({
|
||||
;($('[data-toggle="tooltip"]') as JQueryWithTooltip).tooltip({
|
||||
placement: 'top',
|
||||
trigger: 'hover',
|
||||
html: true,
|
||||
delay: { show: 0, hide: 0 },
|
||||
animation: true,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
// Initial tooltip initialization
|
||||
initializeTooltips();
|
||||
initializeTooltips()
|
||||
|
||||
// Reinitialize tooltips when assets change
|
||||
const observer = new MutationObserver(() => {
|
||||
initializeTooltips();
|
||||
});
|
||||
initializeTooltips()
|
||||
})
|
||||
|
||||
// Observe changes in both active and inactive sections
|
||||
const activeSection = document.getElementById('active-assets-section');
|
||||
const inactiveSection = document.getElementById('inactive-assets-section');
|
||||
const activeSection = document.getElementById('active-assets-section')
|
||||
const inactiveSection = document.getElementById('inactive-assets-section')
|
||||
|
||||
if (activeSection) {
|
||||
observer.observe(activeSection, { childList: true, subtree: true });
|
||||
observer.observe(activeSection, { childList: true, subtree: true })
|
||||
}
|
||||
if (inactiveSection) {
|
||||
observer.observe(inactiveSection, { childList: true, subtree: true });
|
||||
observer.observe(inactiveSection, { childList: true, subtree: true })
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
($('[data-toggle="tooltip"]') as JQueryWithTooltip).tooltip('dispose');
|
||||
};
|
||||
}, [activeAssets, inactiveAssets]);
|
||||
observer.disconnect()
|
||||
;($('[data-toggle="tooltip"]') as JQueryWithTooltip).tooltip('dispose')
|
||||
}
|
||||
}, [activeAssets, inactiveAssets])
|
||||
|
||||
const handleAddAsset = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
setIsModalOpen(true);
|
||||
setAssetToEdit(null);
|
||||
};
|
||||
event.preventDefault()
|
||||
setIsModalOpen(true)
|
||||
setAssetToEdit(null)
|
||||
}
|
||||
|
||||
const handlePreviousAsset = async (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
await fetch('/api/v2/assets/control/previous');
|
||||
};
|
||||
event.preventDefault()
|
||||
await fetch('/api/v2/assets/control/previous')
|
||||
}
|
||||
|
||||
const handleNextAsset = async (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
await fetch('/api/v2/assets/control/next');
|
||||
};
|
||||
event.preventDefault()
|
||||
await fetch('/api/v2/assets/control/next')
|
||||
}
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
setIsModalOpen(false)
|
||||
}
|
||||
|
||||
const handleSaveAsset = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
setIsModalOpen(false)
|
||||
}
|
||||
|
||||
const handleEditAsset = (asset: AssetEditData) => {
|
||||
setAssetToEdit(asset);
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
setAssetToEdit(asset)
|
||||
setIsEditModalOpen(true)
|
||||
}
|
||||
|
||||
const handleCloseEditModal = () => {
|
||||
setIsEditModalOpen(false);
|
||||
setAssetToEdit(null);
|
||||
};
|
||||
setIsEditModalOpen(false)
|
||||
setAssetToEdit(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -236,5 +236,5 @@ export const ScheduleOverview = () => {
|
||||
asset={assetToEdit}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
const Http404: React.FC = () => {
|
||||
return (
|
||||
@@ -35,7 +35,7 @@ const Http404: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Http404;
|
||||
export default Http404
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { selectInactiveAssets } from '@/store/assets';
|
||||
import { AssetRow } from '@/components/asset-row';
|
||||
import { Asset, InactiveAssetsTableProps } from '@/types';
|
||||
import { useSelector } from 'react-redux'
|
||||
import { selectInactiveAssets } from '@/store/assets'
|
||||
import { AssetRow } from '@/components/asset-row'
|
||||
import { Asset, InactiveAssetsTableProps } from '@/types'
|
||||
|
||||
export const InactiveAssetsTable = ({
|
||||
onEditAsset,
|
||||
}: InactiveAssetsTableProps) => {
|
||||
const inactiveAssets = useSelector(selectInactiveAssets) as Asset[];
|
||||
const inactiveAssets = useSelector(selectInactiveAssets) as Asset[]
|
||||
|
||||
return (
|
||||
<table className="InactiveAssets table">
|
||||
@@ -68,5 +68,5 @@ export const InactiveAssetsTable = ({
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export const Integrations = () => {
|
||||
const [data, setData] = useState({
|
||||
@@ -9,8 +9,8 @@ export const Integrations = () => {
|
||||
balena_supervisor_version: '',
|
||||
balena_host_os_version: '',
|
||||
balena_device_name_at_init: '',
|
||||
});
|
||||
const [playerName, setPlayerName] = useState('');
|
||||
})
|
||||
const [playerName, setPlayerName] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
@@ -18,25 +18,25 @@ export const Integrations = () => {
|
||||
const [integrationsResponse, settingsResponse] = await Promise.all([
|
||||
fetch('/api/v2/integrations'),
|
||||
fetch('/api/v2/device_settings'),
|
||||
]);
|
||||
])
|
||||
|
||||
const [integrationsData, settingsData] = await Promise.all([
|
||||
integrationsResponse.json(),
|
||||
settingsResponse.json(),
|
||||
]);
|
||||
])
|
||||
|
||||
setData(integrationsData);
|
||||
setPlayerName(settingsData.player_name ?? '');
|
||||
setData(integrationsData)
|
||||
setPlayerName(settingsData.player_name ?? '')
|
||||
} catch {}
|
||||
};
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const title = playerName ? `${playerName} · Integrations` : 'Integrations';
|
||||
document.title = title;
|
||||
}, [playerName]);
|
||||
const title = playerName ? `${playerName} · Integrations` : 'Integrations'
|
||||
document.title = title
|
||||
}, [playerName])
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
@@ -120,5 +120,5 @@ export const Integrations = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
FaArrowCircleDown,
|
||||
FaRegClock,
|
||||
FaCog,
|
||||
FaPlusSquare,
|
||||
FaTasks,
|
||||
} from 'react-icons/fa';
|
||||
import { Link, NavLink } from 'react-router';
|
||||
} from 'react-icons/fa'
|
||||
import { Link, NavLink } from 'react-router'
|
||||
|
||||
export const Navbar = () => {
|
||||
const [upToDate, setUpToDate] = useState<boolean | null>(null);
|
||||
const [isBalena, setIsBalena] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [upToDate, setUpToDate] = useState<boolean | null>(null)
|
||||
const [isBalena, setIsBalena] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
@@ -19,25 +19,25 @@ export const Navbar = () => {
|
||||
const [integrationsResponse, infoResponse] = await Promise.all([
|
||||
fetch('/api/v2/integrations'),
|
||||
fetch('/api/v2/info'),
|
||||
]);
|
||||
])
|
||||
|
||||
const [integrationsData, infoData] = await Promise.all([
|
||||
integrationsResponse.json(),
|
||||
infoResponse.json(),
|
||||
]);
|
||||
])
|
||||
|
||||
setIsBalena(integrationsData.is_balena);
|
||||
setUpToDate(infoData.up_to_date);
|
||||
setIsBalena(integrationsData.is_balena)
|
||||
setUpToDate(infoData.up_to_date)
|
||||
} catch {
|
||||
setIsBalena(false);
|
||||
setUpToDate(false);
|
||||
setIsBalena(false)
|
||||
setUpToDate(false)
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -99,5 +99,5 @@ export const Navbar = () => {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { RootState } from '@/types';
|
||||
import { RootState } from '@/types'
|
||||
|
||||
export const AudioOutput = ({
|
||||
settings,
|
||||
handleInputChange,
|
||||
deviceModel,
|
||||
}: {
|
||||
settings: RootState['settings']['settings'];
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
deviceModel: string;
|
||||
settings: RootState['settings']['settings']
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
|
||||
deviceModel: string
|
||||
}) => {
|
||||
return (
|
||||
<div className="form-group">
|
||||
@@ -26,5 +26,5 @@ export const AudioOutput = ({
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { RootState } from '@/types';
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from '@/types'
|
||||
|
||||
import { updateSetting } from '@/store/settings';
|
||||
import { updateSetting } from '@/store/settings'
|
||||
|
||||
export const Authentication = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useDispatch()
|
||||
const { settings, prevAuthBackend, hasSavedBasicAuth } = useSelector(
|
||||
(state: RootState) => state.settings,
|
||||
);
|
||||
)
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
) => {
|
||||
const { name, value, type } = e.target;
|
||||
const { name, value, type } = e.target
|
||||
const checked =
|
||||
e.target instanceof HTMLInputElement ? e.target.checked : false;
|
||||
e.target instanceof HTMLInputElement ? e.target.checked : false
|
||||
dispatch(
|
||||
updateSetting({
|
||||
name: name as keyof RootState['settings']['settings'],
|
||||
value: type === 'checkbox' ? checked : value,
|
||||
}),
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const showCurrentPassword = () => {
|
||||
// Show current password if:
|
||||
@@ -31,8 +31,8 @@ export const Authentication = () => {
|
||||
hasSavedBasicAuth &&
|
||||
(settings.authBackend === 'auth_basic' ||
|
||||
prevAuthBackend === 'auth_basic')
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -114,5 +114,5 @@ export const Authentication = () => {
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Swal from 'sweetalert2';
|
||||
import { RootState, AppDispatch } from '@/types';
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import Swal from 'sweetalert2'
|
||||
import { RootState, AppDispatch } from '@/types'
|
||||
|
||||
import { SWEETALERT_TIMER } from '@/constants';
|
||||
import { SWEETALERT_TIMER } from '@/constants'
|
||||
import {
|
||||
createBackup,
|
||||
uploadBackup,
|
||||
resetUploadState,
|
||||
fetchSettings,
|
||||
} from '@/store/settings';
|
||||
} from '@/store/settings'
|
||||
|
||||
export const Backup = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { isUploading, uploadProgress } = useSelector(
|
||||
(state: RootState) => state.settings,
|
||||
);
|
||||
)
|
||||
|
||||
const handleBackup = async () => {
|
||||
const backupButton = document.getElementById(
|
||||
'btn-backup',
|
||||
) as HTMLButtonElement | null;
|
||||
) as HTMLButtonElement | null
|
||||
const uploadButton = document.getElementById(
|
||||
'btn-upload',
|
||||
) as HTMLButtonElement | null;
|
||||
) as HTMLButtonElement | null
|
||||
|
||||
if (!backupButton || !uploadButton) return;
|
||||
if (!backupButton || !uploadButton) return
|
||||
|
||||
const originalText = backupButton.textContent;
|
||||
backupButton.textContent = 'Preparing archive...';
|
||||
backupButton.disabled = true;
|
||||
uploadButton.disabled = true;
|
||||
const originalText = backupButton.textContent
|
||||
backupButton.textContent = 'Preparing archive...'
|
||||
backupButton.disabled = true
|
||||
uploadButton.disabled = true
|
||||
|
||||
try {
|
||||
const result = await dispatch(createBackup()).unwrap();
|
||||
const result = await dispatch(createBackup()).unwrap()
|
||||
if (result) {
|
||||
window.location.href = `/static_with_mime/${result}?mime=application/x-tgz`;
|
||||
window.location.href = `/static_with_mime/${result}?mime=application/x-tgz`
|
||||
}
|
||||
} catch (err) {
|
||||
await Swal.fire({
|
||||
@@ -49,49 +49,49 @@ export const Backup = () => {
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
});
|
||||
})
|
||||
} finally {
|
||||
if (backupButton) {
|
||||
backupButton.textContent = originalText;
|
||||
backupButton.disabled = false;
|
||||
backupButton.textContent = originalText
|
||||
backupButton.disabled = false
|
||||
}
|
||||
if (uploadButton) {
|
||||
uploadButton.disabled = false;
|
||||
uploadButton.disabled = false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleUpload = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
const fileInput = document.querySelector(
|
||||
'[name="backup_upload"]',
|
||||
) as HTMLInputElement | null;
|
||||
) as HTMLInputElement | null
|
||||
if (fileInput) {
|
||||
fileInput.value = ''; // Reset the file input
|
||||
fileInput.click();
|
||||
fileInput.value = '' // Reset the file input
|
||||
fileInput.click()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const uploadButton = document.getElementById(
|
||||
'btn-upload',
|
||||
) as HTMLElement | null;
|
||||
) as HTMLElement | null
|
||||
const backupButton = document.getElementById(
|
||||
'btn-backup',
|
||||
) as HTMLElement | null;
|
||||
) as HTMLElement | null
|
||||
const progressElement = document.querySelector(
|
||||
'.progress',
|
||||
) as HTMLElement | null;
|
||||
) as HTMLElement | null
|
||||
|
||||
if (uploadButton) uploadButton.style.display = 'none';
|
||||
if (backupButton) backupButton.style.display = 'none';
|
||||
if (progressElement) progressElement.style.display = 'block';
|
||||
if (uploadButton) uploadButton.style.display = 'none'
|
||||
if (backupButton) backupButton.style.display = 'none'
|
||||
if (progressElement) progressElement.style.display = 'block'
|
||||
|
||||
try {
|
||||
const result = await dispatch(uploadBackup(file)).unwrap();
|
||||
const result = await dispatch(uploadBackup(file)).unwrap()
|
||||
|
||||
if (result) {
|
||||
await Swal.fire({
|
||||
@@ -108,10 +108,10 @@ export const Backup = () => {
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// Fetch updated settings after successful recovery
|
||||
dispatch(fetchSettings());
|
||||
dispatch(fetchSettings())
|
||||
}
|
||||
} catch (err) {
|
||||
await Swal.fire({
|
||||
@@ -126,16 +126,16 @@ export const Backup = () => {
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
});
|
||||
})
|
||||
} finally {
|
||||
dispatch(resetUploadState());
|
||||
if (progressElement) progressElement.style.display = 'none';
|
||||
if (uploadButton) uploadButton.style.display = 'inline-block';
|
||||
if (backupButton) backupButton.style.display = 'inline-block';
|
||||
dispatch(resetUploadState())
|
||||
if (progressElement) progressElement.style.display = 'none'
|
||||
if (uploadButton) uploadButton.style.display = 'inline-block'
|
||||
if (backupButton) backupButton.style.display = 'inline-block'
|
||||
// Reset the file input
|
||||
e.target.value = '';
|
||||
e.target.value = ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -182,5 +182,5 @@ export const Backup = () => {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { RootState } from '@/types';
|
||||
import { RootState } from '@/types'
|
||||
|
||||
export const DateFormat = ({
|
||||
settings,
|
||||
handleInputChange,
|
||||
}: {
|
||||
settings: RootState['settings']['settings'];
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
settings: RootState['settings']['settings']
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
|
||||
}) => {
|
||||
return (
|
||||
<div className="form-group">
|
||||
@@ -29,5 +29,5 @@ export const DateFormat = ({
|
||||
<option value="yyyy.mm.dd">year.month.day</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { RootState } from '@/types';
|
||||
import { RootState } from '@/types'
|
||||
|
||||
export const DefaultDurations = ({
|
||||
settings,
|
||||
handleInputChange,
|
||||
}: {
|
||||
settings: RootState['settings']['settings'];
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
settings: RootState['settings']['settings']
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}) => {
|
||||
return (
|
||||
<div className="row">
|
||||
@@ -34,5 +34,5 @@ export const DefaultDurations = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Swal from 'sweetalert2';
|
||||
import { RootState, AppDispatch } from '@/types';
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import Swal from 'sweetalert2'
|
||||
import { RootState, AppDispatch } from '@/types'
|
||||
|
||||
import { SWEETALERT_TIMER } from '@/constants';
|
||||
import { SWEETALERT_TIMER } from '@/constants'
|
||||
import {
|
||||
fetchSettings,
|
||||
fetchDeviceModel,
|
||||
updateSettings,
|
||||
updateSetting,
|
||||
} from '@/store/settings';
|
||||
import { SystemControls } from '@/components/settings/system-controls';
|
||||
import { Backup } from '@/components/settings/backup';
|
||||
import { Authentication } from '@/components/settings/authentication';
|
||||
import { PlayerName } from '@/components/settings/player-name';
|
||||
import { DefaultDurations } from '@/components/settings/default-durations';
|
||||
import { AudioOutput } from '@/components/settings/audio-output';
|
||||
import { DateFormat } from '@/components/settings/date-format';
|
||||
import { ToggleableSetting } from '@/components/settings/toggleable-setting';
|
||||
import { Update } from '@/components/settings/update';
|
||||
} from '@/store/settings'
|
||||
import { SystemControls } from '@/components/settings/system-controls'
|
||||
import { Backup } from '@/components/settings/backup'
|
||||
import { Authentication } from '@/components/settings/authentication'
|
||||
import { PlayerName } from '@/components/settings/player-name'
|
||||
import { DefaultDurations } from '@/components/settings/default-durations'
|
||||
import { AudioOutput } from '@/components/settings/audio-output'
|
||||
import { DateFormat } from '@/components/settings/date-format'
|
||||
import { ToggleableSetting } from '@/components/settings/toggleable-setting'
|
||||
import { Update } from '@/components/settings/update'
|
||||
|
||||
export const Settings = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { settings, deviceModel, isLoading } = useSelector(
|
||||
(state: RootState) => state.settings,
|
||||
);
|
||||
const [upToDate, setUpToDate] = useState<boolean>(true);
|
||||
const [isBalena, setIsBalena] = useState<boolean>(false);
|
||||
)
|
||||
const [upToDate, setUpToDate] = useState<boolean>(true)
|
||||
const [isBalena, setIsBalena] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchSettings());
|
||||
dispatch(fetchDeviceModel());
|
||||
}, [dispatch]);
|
||||
dispatch(fetchSettings())
|
||||
dispatch(fetchDeviceModel())
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/v2/info')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setUpToDate(data.up_to_date);
|
||||
});
|
||||
setUpToDate(data.up_to_date)
|
||||
})
|
||||
|
||||
fetch('/api/v2/integrations')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setIsBalena(data.is_balena);
|
||||
});
|
||||
}, []);
|
||||
setIsBalena(data.is_balena)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const title = settings.playerName
|
||||
? `${settings.playerName} · Settings`
|
||||
: 'Settings';
|
||||
document.title = title;
|
||||
}, [settings.playerName]);
|
||||
: 'Settings'
|
||||
document.title = title
|
||||
}, [settings.playerName])
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
) => {
|
||||
const { name, value, type } = e.target;
|
||||
const { name, value, type } = e.target
|
||||
const checked =
|
||||
e.target instanceof HTMLInputElement ? e.target.checked : false;
|
||||
e.target instanceof HTMLInputElement ? e.target.checked : false
|
||||
dispatch(
|
||||
updateSetting({
|
||||
name: name as keyof RootState['settings']['settings'],
|
||||
value: type === 'checkbox' ? checked : value,
|
||||
}),
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
await dispatch(updateSettings(settings)).unwrap();
|
||||
await dispatch(updateSettings(settings)).unwrap()
|
||||
|
||||
await Swal.fire({
|
||||
title: 'Success!',
|
||||
@@ -85,9 +85,9 @@ export const Settings = () => {
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
dispatch(fetchSettings());
|
||||
dispatch(fetchSettings())
|
||||
} catch (err) {
|
||||
await Swal.fire({
|
||||
title: 'Error!',
|
||||
@@ -99,9 +99,9 @@ export const Settings = () => {
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
@@ -206,5 +206,5 @@ export const Settings = () => {
|
||||
<Backup />
|
||||
<SystemControls />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { RootState } from '@/types';
|
||||
import { RootState } from '@/types'
|
||||
|
||||
export const PlayerName = ({
|
||||
settings,
|
||||
handleInputChange,
|
||||
}: {
|
||||
settings: RootState['settings']['settings'];
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
settings: RootState['settings']['settings']
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}) => {
|
||||
return (
|
||||
<div className="form-group">
|
||||
@@ -20,5 +20,5 @@ export const PlayerName = ({
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Swal from 'sweetalert2';
|
||||
import { OperationConfig, AppDispatch } from '@/types';
|
||||
import { useDispatch } from 'react-redux'
|
||||
import Swal from 'sweetalert2'
|
||||
import { OperationConfig, AppDispatch } from '@/types'
|
||||
|
||||
import { SWEETALERT_TIMER } from '@/constants';
|
||||
import { systemOperation } from '@/store/settings';
|
||||
import { SWEETALERT_TIMER } from '@/constants'
|
||||
import { systemOperation } from '@/store/settings'
|
||||
|
||||
type SystemOperationType = 'reboot' | 'shutdown';
|
||||
type SystemOperationType = 'reboot' | 'shutdown'
|
||||
|
||||
export const SystemControls = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const handleSystemOperation = async (operation: SystemOperationType) => {
|
||||
const config: Record<SystemOperationType, OperationConfig> = {
|
||||
@@ -29,10 +29,10 @@ export const SystemControls = () => {
|
||||
'Device shutdown has started successfully.\nSoon you will be able to unplug the power from your Raspberry Pi.',
|
||||
errorMessage: 'Failed to shutdown device',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { title, text, confirmButtonText, endpoint, successMessage } =
|
||||
config[operation];
|
||||
config[operation]
|
||||
|
||||
const result = await Swal.fire({
|
||||
title,
|
||||
@@ -51,13 +51,13 @@ export const SystemControls = () => {
|
||||
cancelButton: 'swal2-cancel',
|
||||
actions: 'swal2-actions',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
await dispatch(
|
||||
systemOperation({ operation, endpoint, successMessage }),
|
||||
).unwrap();
|
||||
).unwrap()
|
||||
|
||||
await Swal.fire({
|
||||
title: 'Success!',
|
||||
@@ -70,7 +70,7 @@ export const SystemControls = () => {
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
},
|
||||
});
|
||||
})
|
||||
} catch (err) {
|
||||
await Swal.fire({
|
||||
title: 'Error!',
|
||||
@@ -84,13 +84,13 @@ export const SystemControls = () => {
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleReboot = () => handleSystemOperation('reboot');
|
||||
const handleShutdown = () => handleSystemOperation('shutdown');
|
||||
const handleReboot = () => handleSystemOperation('reboot')
|
||||
const handleShutdown = () => handleSystemOperation('shutdown')
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -122,5 +122,5 @@ export const SystemControls = () => {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RootState } from '@/types';
|
||||
import { RootState } from '@/types'
|
||||
|
||||
export const ToggleableSetting = ({
|
||||
settings,
|
||||
@@ -6,10 +6,10 @@ export const ToggleableSetting = ({
|
||||
label,
|
||||
name,
|
||||
}: {
|
||||
settings: RootState['settings']['settings'];
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
label: string;
|
||||
name: string;
|
||||
settings: RootState['settings']['settings']
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
label: string
|
||||
name: string
|
||||
}) => {
|
||||
return (
|
||||
<div className="form-inline mt-4">
|
||||
@@ -34,5 +34,5 @@ export const ToggleableSetting = ({
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export const Update = () => {
|
||||
const [ipAddresses, setIpAddresses] = useState<string[]>([]);
|
||||
const [hostUser, setHostUser] = useState<string>('<USER>');
|
||||
const [ipAddresses, setIpAddresses] = useState<string[]>([])
|
||||
const [hostUser, setHostUser] = useState<string>('<USER>')
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/v2/info')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setIpAddresses(data.ip_addresses);
|
||||
setIpAddresses(data.ip_addresses)
|
||||
|
||||
if (data.host_user) {
|
||||
setHostUser(data.host_user);
|
||||
setHostUser(data.host_user)
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -70,5 +70,5 @@ export const Update = () => {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { AssetRow } from './asset-row';
|
||||
import { AssetRowProps } from '@/types';
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { AssetRow } from './asset-row'
|
||||
import { AssetRowProps } from '@/types'
|
||||
|
||||
interface SortableAssetRowProps extends AssetRowProps {
|
||||
id: string;
|
||||
id: string
|
||||
}
|
||||
|
||||
export const SortableAssetRow = (props: SortableAssetRowProps) => {
|
||||
@@ -15,7 +15,7 @@ export const SortableAssetRow = (props: SortableAssetRowProps) => {
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: props.id });
|
||||
} = useSortable({ id: props.id })
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
@@ -24,7 +24,7 @@ export const SortableAssetRow = (props: SortableAssetRowProps) => {
|
||||
zIndex: isDragging ? 99999 : 'auto',
|
||||
position: isDragging ? 'relative' : 'static',
|
||||
backgroundColor: isDragging ? 'rgba(255, 255, 255, 0.1)' : 'transparent',
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<AssetRow
|
||||
@@ -34,5 +34,5 @@ export const SortableAssetRow = (props: SortableAssetRowProps) => {
|
||||
dragHandleProps={{ ...attributes, ...listeners }}
|
||||
isDragging={isDragging}
|
||||
/>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
AnthiasVersionValueProps,
|
||||
SkeletonProps,
|
||||
MemoryInfo,
|
||||
UptimeInfo,
|
||||
} from '@/types';
|
||||
} from '@/types'
|
||||
|
||||
const ANTHIAS_REPO_URL = 'https://github.com/Screenly/Anthias';
|
||||
const ANTHIAS_REPO_URL = 'https://github.com/Screenly/Anthias'
|
||||
|
||||
const AnthiasVersionValue = ({ version }: AnthiasVersionValueProps) => {
|
||||
const [commitLink, setCommitLink] = useState('');
|
||||
const [commitLink, setCommitLink] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (!version) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const [gitBranch, gitCommit] = version ? version.split('@') : ['', ''];
|
||||
const [gitBranch, gitCommit] = version ? version.split('@') : ['', '']
|
||||
|
||||
if (gitBranch === 'master') {
|
||||
setCommitLink(`${ANTHIAS_REPO_URL}/commit/${gitCommit}`);
|
||||
setCommitLink(`${ANTHIAS_REPO_URL}/commit/${gitCommit}`)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (commitLink) {
|
||||
return (
|
||||
<a href={commitLink} rel="noopener" target="_blank" className="text-dark">
|
||||
{version}
|
||||
</a>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return <>{version}</>;
|
||||
};
|
||||
return <>{version}</>
|
||||
}
|
||||
|
||||
const Skeleton = ({ children, isLoading }: SkeletonProps) => {
|
||||
return isLoading ? (
|
||||
<span className="placeholder placeholder-wave"></span>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export const SystemInfo = () => {
|
||||
const [loadAverage, setLoadAverage] = useState('');
|
||||
const [freeSpace, setFreeSpace] = useState('');
|
||||
const [loadAverage, setLoadAverage] = useState('')
|
||||
const [freeSpace, setFreeSpace] = useState('')
|
||||
const [memory, setMemory] = useState<MemoryInfo>({
|
||||
total: 0,
|
||||
used: 0,
|
||||
@@ -52,20 +52,20 @@ export const SystemInfo = () => {
|
||||
shared: 0,
|
||||
buff: 0,
|
||||
available: 0,
|
||||
});
|
||||
})
|
||||
const [uptime, setUptime] = useState<UptimeInfo>({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
});
|
||||
const [displayPower, setDisplayPower] = useState<string | null>(null);
|
||||
const [deviceModel, setDeviceModel] = useState('');
|
||||
const [anthiasVersion, setAnthiasVersion] = useState('');
|
||||
const [macAddress, setMacAddress] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [playerName, setPlayerName] = useState('');
|
||||
})
|
||||
const [displayPower, setDisplayPower] = useState<string | null>(null)
|
||||
const [deviceModel, setDeviceModel] = useState('')
|
||||
const [anthiasVersion, setAnthiasVersion] = useState('')
|
||||
const [macAddress, setMacAddress] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [playerName, setPlayerName] = useState('')
|
||||
|
||||
const initializeSystemInfo = async () => {
|
||||
setIsLoading(true);
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const [infoResponse, settingsResponse] = await Promise.all([
|
||||
@@ -75,40 +75,40 @@ export const SystemInfo = () => {
|
||||
},
|
||||
}),
|
||||
fetch('/api/v2/device_settings'),
|
||||
]);
|
||||
])
|
||||
|
||||
if (!infoResponse.ok) {
|
||||
throw new Error('Failed to fetch system info');
|
||||
throw new Error('Failed to fetch system info')
|
||||
}
|
||||
|
||||
const [systemInfo, settingsData] = await Promise.all([
|
||||
infoResponse.json(),
|
||||
settingsResponse.json(),
|
||||
]);
|
||||
])
|
||||
|
||||
setLoadAverage(systemInfo.loadavg);
|
||||
setFreeSpace(systemInfo.free_space);
|
||||
setMemory(systemInfo.memory);
|
||||
setUptime(systemInfo.uptime);
|
||||
setDisplayPower(systemInfo.display_power);
|
||||
setDeviceModel(systemInfo.device_model);
|
||||
setAnthiasVersion(systemInfo.anthias_version);
|
||||
setMacAddress(systemInfo.mac_address);
|
||||
setPlayerName(settingsData.player_name ?? '');
|
||||
setLoadAverage(systemInfo.loadavg)
|
||||
setFreeSpace(systemInfo.free_space)
|
||||
setMemory(systemInfo.memory)
|
||||
setUptime(systemInfo.uptime)
|
||||
setDisplayPower(systemInfo.display_power)
|
||||
setDeviceModel(systemInfo.device_model)
|
||||
setAnthiasVersion(systemInfo.anthias_version)
|
||||
setMacAddress(systemInfo.mac_address)
|
||||
setPlayerName(settingsData.player_name ?? '')
|
||||
} catch {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
initializeSystemInfo();
|
||||
}, []);
|
||||
initializeSystemInfo()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const title = playerName ? `${playerName} · System Info` : 'System Info';
|
||||
document.title = title;
|
||||
}, [playerName]);
|
||||
const title = playerName ? `${playerName} · System Info` : 'System Info'
|
||||
document.title = title
|
||||
}, [playerName])
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
@@ -215,5 +215,5 @@ export const SystemInfo = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const SWEETALERT_TIMER = 3500;
|
||||
export const SWEETALERT_TIMER = 3500
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from './store';
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import { store } from './store'
|
||||
|
||||
import '@/sass/anthias.scss';
|
||||
import { App } from '@/components/app';
|
||||
import '@/sass/anthias.scss'
|
||||
import { App } from '@/components/app'
|
||||
|
||||
const appElement = document.getElementById('app');
|
||||
const appElement = document.getElementById('app')
|
||||
if (!appElement) {
|
||||
throw new Error('App element not found');
|
||||
throw new Error('App element not found')
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(appElement);
|
||||
const root = ReactDOM.createRoot(appElement)
|
||||
|
||||
root.render(
|
||||
<BrowserRouter basename="/">
|
||||
@@ -19,4 +19,4 @@ root.render(
|
||||
<App />
|
||||
</Provider>
|
||||
</BrowserRouter>,
|
||||
);
|
||||
)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { addAsset } from './assets-list-slice';
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import { addAsset } from './assets-list-slice'
|
||||
import {
|
||||
UploadFileParams,
|
||||
SaveAssetParams,
|
||||
RootState,
|
||||
FileData,
|
||||
FormData,
|
||||
} from '@/types';
|
||||
import { getMimetype } from '@/components/add-asset-modal/file-upload-utils';
|
||||
} from '@/types'
|
||||
import { getMimetype } from '@/components/add-asset-modal/file-upload-utils'
|
||||
|
||||
// Async thunks for API operations
|
||||
export const uploadFile = createAsyncThunk(
|
||||
@@ -17,59 +17,59 @@ export const uploadFile = createAsyncThunk(
|
||||
{ dispatch, getState, rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file_upload', file);
|
||||
const formData = new FormData()
|
||||
formData.append('file_upload', file)
|
||||
|
||||
// Create XMLHttpRequest for progress tracking
|
||||
const xhr = new XMLHttpRequest();
|
||||
const xhr = new XMLHttpRequest()
|
||||
|
||||
const uploadPromise = new Promise((resolve, reject) => {
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const progress = Math.round((e.loaded / e.total) * 100);
|
||||
dispatch(setUploadProgress(progress));
|
||||
const progress = Math.round((e.loaded / e.total) * 100)
|
||||
dispatch(setUploadProgress(progress))
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
resolve(response);
|
||||
const response = JSON.parse(xhr.responseText)
|
||||
resolve(response)
|
||||
} catch {
|
||||
reject(new Error('Invalid JSON response'));
|
||||
reject(new Error('Invalid JSON response'))
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`Upload failed with status ${xhr.status}`));
|
||||
reject(new Error(`Upload failed with status ${xhr.status}`))
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
reject(new Error('Network error during upload'));
|
||||
});
|
||||
reject(new Error('Network error during upload'))
|
||||
})
|
||||
|
||||
xhr.addEventListener('abort', () => {
|
||||
reject(new Error('Upload aborted'));
|
||||
});
|
||||
});
|
||||
reject(new Error('Upload aborted'))
|
||||
})
|
||||
})
|
||||
|
||||
// Start the upload
|
||||
xhr.open('POST', '/api/v2/file_asset');
|
||||
xhr.send(formData);
|
||||
xhr.open('POST', '/api/v2/file_asset')
|
||||
xhr.send(formData)
|
||||
|
||||
// Wait for upload to complete
|
||||
const response = await uploadPromise;
|
||||
const response = await uploadPromise
|
||||
|
||||
// Get mimetype and duration
|
||||
const mimetype = getMimetype(file.name);
|
||||
const mimetypeString = Array.isArray(mimetype) ? mimetype[0] : mimetype;
|
||||
const state = getState() as RootState;
|
||||
const mimetype = getMimetype(file.name)
|
||||
const mimetypeString = Array.isArray(mimetype) ? mimetype[0] : mimetype
|
||||
const state = getState() as RootState
|
||||
const duration = getDurationForMimetype(
|
||||
mimetypeString,
|
||||
state.settings.settings.defaultDuration,
|
||||
state.settings.settings.defaultStreamingDuration,
|
||||
);
|
||||
const dates = getDefaultDates();
|
||||
)
|
||||
const dates = getDefaultDates()
|
||||
|
||||
return {
|
||||
fileData: response as FileData,
|
||||
@@ -78,12 +78,12 @@ export const uploadFile = createAsyncThunk(
|
||||
mimetype: mimetypeString,
|
||||
duration,
|
||||
dates,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return rejectWithValue((error as Error).message);
|
||||
return rejectWithValue((error as Error).message)
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export const saveAsset = createAsyncThunk(
|
||||
'assetModal/saveAsset',
|
||||
@@ -95,30 +95,30 @@ export const saveAsset = createAsyncThunk(
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(assetData),
|
||||
});
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return rejectWithValue('Failed to save asset');
|
||||
return rejectWithValue('Failed to save asset')
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json()
|
||||
|
||||
// Create the complete asset object with the response data
|
||||
const completeAsset = {
|
||||
...assetData,
|
||||
asset_id: data.asset_id,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
// Dispatch the addAsset action to update the assets list
|
||||
dispatch(addAsset(completeAsset));
|
||||
dispatch(addAsset(completeAsset))
|
||||
|
||||
return completeAsset;
|
||||
return completeAsset
|
||||
} catch (error) {
|
||||
return rejectWithValue((error as Error).message);
|
||||
return rejectWithValue((error as Error).message)
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const getDurationForMimetype = (
|
||||
mimetype: string,
|
||||
@@ -126,24 +126,24 @@ const getDurationForMimetype = (
|
||||
defaultStreamingDuration: number,
|
||||
) => {
|
||||
if (mimetype === 'video') {
|
||||
return 0;
|
||||
return 0
|
||||
} else if (mimetype === 'streaming') {
|
||||
return defaultStreamingDuration;
|
||||
return defaultStreamingDuration
|
||||
} else {
|
||||
return defaultDuration;
|
||||
return defaultDuration
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getDefaultDates = () => {
|
||||
const now = new Date();
|
||||
const endDate = new Date();
|
||||
endDate.setDate(endDate.getDate() + 30); // 30 days from now
|
||||
const now = new Date()
|
||||
const endDate = new Date()
|
||||
endDate.setDate(endDate.getDate() + 30) // 30 days from now
|
||||
|
||||
return {
|
||||
start_date: now.toISOString(),
|
||||
end_date: endDate.toISOString(),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Slice definition
|
||||
const assetModalSlice = createSlice({
|
||||
@@ -162,57 +162,57 @@ const assetModalSlice = createSlice({
|
||||
},
|
||||
reducers: {
|
||||
setActiveTab: (state, action) => {
|
||||
state.activeTab = action.payload;
|
||||
state.activeTab = action.payload
|
||||
},
|
||||
updateFormData: (state, action) => {
|
||||
state.formData = { ...state.formData, ...action.payload };
|
||||
state.formData = { ...state.formData, ...action.payload }
|
||||
},
|
||||
setValid: (state, action) => {
|
||||
state.isValid = action.payload;
|
||||
state.isValid = action.payload
|
||||
},
|
||||
setErrorMessage: (state, action) => {
|
||||
state.errorMessage = action.payload;
|
||||
state.errorMessage = action.payload
|
||||
},
|
||||
setStatusMessage: (state, action) => {
|
||||
state.statusMessage = action.payload;
|
||||
state.statusMessage = action.payload
|
||||
},
|
||||
setUploadProgress: (state, action) => {
|
||||
state.uploadProgress = action.payload;
|
||||
state.uploadProgress = action.payload
|
||||
},
|
||||
resetForm: (state) => {
|
||||
state.formData = {
|
||||
uri: '',
|
||||
skipAssetCheck: false,
|
||||
};
|
||||
state.isValid = true;
|
||||
state.errorMessage = '';
|
||||
state.statusMessage = '';
|
||||
state.isSubmitting = false;
|
||||
state.uploadProgress = 0;
|
||||
}
|
||||
state.isValid = true
|
||||
state.errorMessage = ''
|
||||
state.statusMessage = ''
|
||||
state.isSubmitting = false
|
||||
state.uploadProgress = 0
|
||||
},
|
||||
validateUrl: (state, action) => {
|
||||
const url = action.payload;
|
||||
const url = action.payload
|
||||
if (!url) {
|
||||
state.isValid = true;
|
||||
state.errorMessage = '';
|
||||
return;
|
||||
state.isValid = true
|
||||
state.errorMessage = ''
|
||||
return
|
||||
}
|
||||
|
||||
const urlPattern =
|
||||
/(http|https|rtsp|rtmp):\/\/[\w-]+(\.?[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?/;
|
||||
const isValidUrl = urlPattern.test(url);
|
||||
/(http|https|rtsp|rtmp):\/\/[\w-]+(\.?[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?/
|
||||
const isValidUrl = urlPattern.test(url)
|
||||
|
||||
state.isValid = isValidUrl;
|
||||
state.errorMessage = isValidUrl ? '' : 'Please enter a valid URL';
|
||||
state.isValid = isValidUrl
|
||||
state.errorMessage = isValidUrl ? '' : 'Please enter a valid URL'
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// Upload file
|
||||
.addCase(uploadFile.pending, (state) => {
|
||||
state.isSubmitting = true;
|
||||
state.statusMessage = '';
|
||||
state.uploadProgress = 0;
|
||||
state.isSubmitting = true
|
||||
state.statusMessage = ''
|
||||
state.uploadProgress = 0
|
||||
})
|
||||
.addCase(uploadFile.fulfilled, (state, action) => {
|
||||
const {
|
||||
@@ -222,7 +222,7 @@ const assetModalSlice = createSlice({
|
||||
mimetype,
|
||||
duration,
|
||||
dates,
|
||||
} = action.payload;
|
||||
} = action.payload
|
||||
|
||||
// Update form data with file name and other details
|
||||
state.formData = {
|
||||
@@ -233,31 +233,31 @@ const assetModalSlice = createSlice({
|
||||
mimetype,
|
||||
duration,
|
||||
dates,
|
||||
};
|
||||
}
|
||||
|
||||
state.statusMessage = 'Upload completed.';
|
||||
state.isSubmitting = false;
|
||||
state.uploadProgress = 0;
|
||||
state.statusMessage = 'Upload completed.'
|
||||
state.isSubmitting = false
|
||||
state.uploadProgress = 0
|
||||
})
|
||||
.addCase(uploadFile.rejected, (state, action) => {
|
||||
state.errorMessage = `Upload failed: ${action.payload}`;
|
||||
state.isSubmitting = false;
|
||||
state.uploadProgress = 0;
|
||||
state.errorMessage = `Upload failed: ${action.payload}`
|
||||
state.isSubmitting = false
|
||||
state.uploadProgress = 0
|
||||
})
|
||||
// Save asset
|
||||
.addCase(saveAsset.pending, (state) => {
|
||||
state.isSubmitting = true;
|
||||
state.isSubmitting = true
|
||||
})
|
||||
.addCase(saveAsset.fulfilled, (state) => {
|
||||
state.isSubmitting = false;
|
||||
state.statusMessage = 'Asset saved successfully.';
|
||||
state.isSubmitting = false
|
||||
state.statusMessage = 'Asset saved successfully.'
|
||||
})
|
||||
.addCase(saveAsset.rejected, (state, action) => {
|
||||
state.errorMessage = `Failed to save asset: ${action.payload}`;
|
||||
state.isSubmitting = false;
|
||||
});
|
||||
state.errorMessage = `Failed to save asset: ${action.payload}`
|
||||
state.isSubmitting = false
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// Export actions
|
||||
export const {
|
||||
@@ -269,10 +269,10 @@ export const {
|
||||
setUploadProgress,
|
||||
resetForm,
|
||||
validateUrl,
|
||||
} = assetModalSlice.actions;
|
||||
} = assetModalSlice.actions
|
||||
|
||||
// Export selectors
|
||||
export const selectAssetModalState = (state: RootState) => state.assetModal;
|
||||
export const selectAssetModalState = (state: RootState) => state.assetModal
|
||||
|
||||
// Export reducer
|
||||
export default assetModalSlice.reducer;
|
||||
export default assetModalSlice.reducer
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import {
|
||||
fetchAssets,
|
||||
updateAssetOrder,
|
||||
toggleAssetEnabled,
|
||||
} from '@/store/assets/assets-thunks';
|
||||
import { Asset, RootState } from '@/types';
|
||||
} from '@/store/assets/assets-thunks'
|
||||
import { Asset, RootState } from '@/types'
|
||||
|
||||
const initialState: RootState['assets'] = {
|
||||
items: [] as Asset[],
|
||||
status: 'idle' as const,
|
||||
error: null as string | null,
|
||||
};
|
||||
}
|
||||
|
||||
const assetsSlice = createSlice({
|
||||
name: 'assets',
|
||||
initialState,
|
||||
reducers: {
|
||||
addAsset: (state, action) => {
|
||||
state.items.push(action.payload);
|
||||
state.items.push(action.payload)
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchAssets.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.status = 'loading'
|
||||
})
|
||||
.addCase(fetchAssets.fulfilled, (state, action) => {
|
||||
state.status = 'succeeded';
|
||||
state.items = action.payload;
|
||||
state.status = 'succeeded'
|
||||
state.items = action.payload
|
||||
})
|
||||
.addCase(fetchAssets.rejected, (state, action) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.error.message || 'Failed to fetch assets';
|
||||
state.status = 'failed'
|
||||
state.error = action.error.message || 'Failed to fetch assets'
|
||||
})
|
||||
.addCase(updateAssetOrder.fulfilled, (state) => {
|
||||
state.status = 'succeeded';
|
||||
state.status = 'succeeded'
|
||||
})
|
||||
.addCase(toggleAssetEnabled.fulfilled, (state, action) => {
|
||||
const { assetId, newValue, playOrder } = action.payload;
|
||||
const asset = state.items.find((item) => item.asset_id === assetId);
|
||||
const { assetId, newValue, playOrder } = action.payload
|
||||
const asset = state.items.find((item) => item.asset_id === assetId)
|
||||
if (asset) {
|
||||
asset.is_enabled = newValue;
|
||||
asset.play_order = playOrder;
|
||||
asset.is_enabled = newValue
|
||||
asset.play_order = playOrder
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
export const { addAsset } = assetsSlice.actions;
|
||||
export const { addAsset } = assetsSlice.actions
|
||||
|
||||
export default assetsSlice.reducer;
|
||||
export default assetsSlice.reducer
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { RootState } from '@/types';
|
||||
import { RootState } from '@/types'
|
||||
|
||||
export const selectActiveAssets = (state: RootState) =>
|
||||
state.assets.items
|
||||
.filter((asset) => asset.is_active)
|
||||
.sort((a, b) => a.play_order - b.play_order);
|
||||
.sort((a, b) => a.play_order - b.play_order)
|
||||
|
||||
export const selectInactiveAssets = (state: RootState) =>
|
||||
state.assets.items.filter((asset) => !asset.is_active);
|
||||
state.assets.items.filter((asset) => !asset.is_active)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { Asset, ToggleAssetParams, RootState } from '@/types';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import { Asset, ToggleAssetParams, RootState } from '@/types'
|
||||
|
||||
export const fetchAssets = createAsyncThunk('assets/fetchAssets', async () => {
|
||||
const response = await fetch('/api/v2/assets');
|
||||
const data = await response.json();
|
||||
return data;
|
||||
});
|
||||
const response = await fetch('/api/v2/assets')
|
||||
const data = await response.json()
|
||||
return data
|
||||
})
|
||||
|
||||
export const updateAssetOrder = createAsyncThunk(
|
||||
'assets/updateOrder',
|
||||
@@ -16,27 +16,27 @@ export const updateAssetOrder = createAsyncThunk(
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ ids: orderedIds }),
|
||||
});
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update order');
|
||||
throw new Error('Failed to update order')
|
||||
}
|
||||
return orderedIds;
|
||||
return orderedIds
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export const toggleAssetEnabled = createAsyncThunk(
|
||||
'assets/toggleEnabled',
|
||||
async ({ assetId, newValue }: ToggleAssetParams, { dispatch, getState }) => {
|
||||
// First, fetch the current assets to determine the next play_order
|
||||
const response = await fetch('/api/v2/assets');
|
||||
const assets = await response.json();
|
||||
const response = await fetch('/api/v2/assets')
|
||||
const assets = await response.json()
|
||||
|
||||
// Get the current active assets to determine the next play_order
|
||||
const activeAssets = assets.filter((asset: Asset) => asset.is_active);
|
||||
const activeAssets = assets.filter((asset: Asset) => asset.is_active)
|
||||
|
||||
// If enabling the asset, set play_order to the next available position
|
||||
// If disabling the asset, set play_order to 0
|
||||
const playOrder = newValue === 1 ? activeAssets.length : 0;
|
||||
const playOrder = newValue === 1 ? activeAssets.length : 0
|
||||
|
||||
const updateResponse = await fetch(`/api/v2/assets/${assetId}`, {
|
||||
method: 'PATCH',
|
||||
@@ -47,22 +47,22 @@ export const toggleAssetEnabled = createAsyncThunk(
|
||||
is_enabled: newValue,
|
||||
play_order: playOrder,
|
||||
}),
|
||||
});
|
||||
})
|
||||
|
||||
const state = getState() as RootState;
|
||||
const state = getState() as RootState
|
||||
const activeAssetIds = state.assets.items
|
||||
.filter((asset) => asset.is_active)
|
||||
.sort((a, b) => a.play_order - b.play_order)
|
||||
.map((asset) => asset.asset_id)
|
||||
.concat(assetId);
|
||||
.concat(assetId)
|
||||
|
||||
await dispatch(updateAssetOrder(activeAssetIds.join(',')));
|
||||
await dispatch(updateAssetOrder(activeAssetIds.join(',')))
|
||||
|
||||
if (!updateResponse.ok) {
|
||||
throw new Error('Failed to update asset');
|
||||
throw new Error('Failed to update asset')
|
||||
}
|
||||
|
||||
// Return both the assetId and newValue for the reducer
|
||||
return { assetId, newValue, playOrder };
|
||||
return { assetId, newValue, playOrder }
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import assetsReducer from '@/store/assets/assets-list-slice';
|
||||
import { addAsset } from '@/store/assets/assets-list-slice';
|
||||
import assetsReducer from '@/store/assets/assets-list-slice'
|
||||
import { addAsset } from '@/store/assets/assets-list-slice'
|
||||
import {
|
||||
fetchAssets,
|
||||
updateAssetOrder,
|
||||
toggleAssetEnabled,
|
||||
} from '@/store/assets/assets-thunks';
|
||||
} from '@/store/assets/assets-thunks'
|
||||
import {
|
||||
selectActiveAssets,
|
||||
selectInactiveAssets,
|
||||
} from '@/store/assets/assets-selectors';
|
||||
import assetModalReducer from './asset-modal-slice';
|
||||
} from '@/store/assets/assets-selectors'
|
||||
import assetModalReducer from './asset-modal-slice'
|
||||
import {
|
||||
uploadFile,
|
||||
saveAsset,
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
resetForm,
|
||||
validateUrl,
|
||||
selectAssetModalState,
|
||||
} from './asset-modal-slice';
|
||||
} from './asset-modal-slice'
|
||||
|
||||
export {
|
||||
assetsReducer,
|
||||
@@ -45,4 +45,4 @@ export {
|
||||
resetForm,
|
||||
validateUrl,
|
||||
selectAssetModalState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { assetsReducer, assetModalReducer } from '@/store/assets';
|
||||
import settingsReducer from '@/store/settings';
|
||||
import websocketReducer from '@/store/websocket';
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import { assetsReducer, assetModalReducer } from '@/store/assets'
|
||||
import settingsReducer from '@/store/settings'
|
||||
import websocketReducer from '@/store/websocket'
|
||||
|
||||
const environment = process.env.ENVIRONMENT || 'production';
|
||||
const environment = process.env.ENVIRONMENT || 'production'
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
@@ -13,7 +13,7 @@ export const store = configureStore({
|
||||
websocket: websocketReducer,
|
||||
},
|
||||
devTools: environment === 'development',
|
||||
});
|
||||
})
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { SettingsData, SystemOperationParams, RootState } from '@/types';
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import { SettingsData, SystemOperationParams, RootState } from '@/types'
|
||||
|
||||
type SettingsState = RootState['settings']['settings'];
|
||||
type SettingsState = RootState['settings']['settings']
|
||||
|
||||
type UpdateSettingPayload = {
|
||||
name: keyof SettingsState;
|
||||
value: SettingsState[keyof SettingsState];
|
||||
};
|
||||
name: keyof SettingsState
|
||||
value: SettingsState[keyof SettingsState]
|
||||
}
|
||||
|
||||
export const fetchSettings = createAsyncThunk(
|
||||
'settings/fetchSettings',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await fetch('/api/v2/device_settings');
|
||||
const response = await fetch('/api/v2/device_settings')
|
||||
if (response?.url?.endsWith('/login/')) {
|
||||
window.location.href = response.url;
|
||||
window.location.href = response.url
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return rejectWithValue('Failed to fetch device settings');
|
||||
return rejectWithValue('Failed to fetch device settings')
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json()
|
||||
return {
|
||||
playerName: data.player_name || '',
|
||||
defaultDuration: data.default_duration || 0,
|
||||
@@ -35,21 +35,21 @@ export const fetchSettings = createAsyncThunk(
|
||||
shufflePlaylist: data.shuffle_playlist || false,
|
||||
use24HourClock: data.use_24_hour_clock || false,
|
||||
debugLogging: data.debug_logging || false,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return rejectWithValue((error as Error).message);
|
||||
return rejectWithValue((error as Error).message)
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export const fetchDeviceModel = createAsyncThunk(
|
||||
'settings/fetchDeviceModel',
|
||||
async () => {
|
||||
const response = await fetch('/api/v2/info');
|
||||
const data = await response.json();
|
||||
return data.device_model || '';
|
||||
const response = await fetch('/api/v2/info')
|
||||
const data = await response.json()
|
||||
return data.device_model || ''
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export const updateSettings = createAsyncThunk(
|
||||
'settings/updateSettings',
|
||||
@@ -77,20 +77,20 @@ export const updateSettings = createAsyncThunk(
|
||||
use_24_hour_clock: settings.use24HourClock,
|
||||
debug_logging: settings.debugLogging,
|
||||
}),
|
||||
});
|
||||
})
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to save settings');
|
||||
throw new Error(data.error || 'Failed to save settings')
|
||||
}
|
||||
|
||||
return data;
|
||||
return data
|
||||
} catch (error) {
|
||||
return rejectWithValue((error as Error).message);
|
||||
return rejectWithValue((error as Error).message)
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export const createBackup = createAsyncThunk(
|
||||
'settings/createBackup',
|
||||
@@ -98,44 +98,44 @@ export const createBackup = createAsyncThunk(
|
||||
try {
|
||||
const response = await fetch('/api/v2/backup', {
|
||||
method: 'POST',
|
||||
});
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create backup');
|
||||
throw new Error('Failed to create backup')
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
return rejectWithValue((error as Error).message);
|
||||
return rejectWithValue((error as Error).message)
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export const uploadBackup = createAsyncThunk(
|
||||
'settings/uploadBackup',
|
||||
async (file: File, { rejectWithValue }) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('backup_upload', file);
|
||||
const formData = new FormData()
|
||||
formData.append('backup_upload', file)
|
||||
|
||||
const response = await fetch('/api/v2/recover', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
})
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to upload backup');
|
||||
throw new Error(data.error || 'Failed to upload backup')
|
||||
}
|
||||
|
||||
return data;
|
||||
return data
|
||||
} catch (error) {
|
||||
return rejectWithValue((error as Error).message);
|
||||
return rejectWithValue((error as Error).message)
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export const systemOperation = createAsyncThunk(
|
||||
'settings/systemOperation',
|
||||
@@ -146,18 +146,18 @@ export const systemOperation = createAsyncThunk(
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
});
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to ${operation} device`);
|
||||
throw new Error(`Failed to ${operation} device`)
|
||||
}
|
||||
|
||||
return { operation, successMessage };
|
||||
return { operation, successMessage }
|
||||
} catch (error) {
|
||||
return rejectWithValue((error as Error).message);
|
||||
return rejectWithValue((error as Error).message)
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const initialState = {
|
||||
settings: {
|
||||
@@ -184,102 +184,102 @@ const initialState = {
|
||||
isUploading: false,
|
||||
uploadProgress: 0,
|
||||
error: null as string | null,
|
||||
};
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
name: 'settings',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateSetting: (state, action: { payload: UpdateSettingPayload }) => {
|
||||
const { name, value } = action.payload;
|
||||
const { name, value } = action.payload
|
||||
if (name === 'authBackend') {
|
||||
state.prevAuthBackend = state.settings.authBackend;
|
||||
state.prevAuthBackend = state.settings.authBackend
|
||||
}
|
||||
(
|
||||
;(
|
||||
state.settings as Record<
|
||||
keyof SettingsState,
|
||||
SettingsState[keyof SettingsState]
|
||||
>
|
||||
)[name] = value;
|
||||
)[name] = value
|
||||
},
|
||||
setUploadProgress: (state, action) => {
|
||||
state.uploadProgress = action.payload;
|
||||
state.uploadProgress = action.payload
|
||||
},
|
||||
resetUploadState: (state) => {
|
||||
state.isUploading = false;
|
||||
state.uploadProgress = 0;
|
||||
state.error = null;
|
||||
state.isUploading = false
|
||||
state.uploadProgress = 0
|
||||
state.error = null
|
||||
},
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
state.error = null
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// Fetch Settings
|
||||
.addCase(fetchSettings.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
state.isLoading = true
|
||||
state.error = null
|
||||
})
|
||||
.addCase(fetchSettings.fulfilled, (state, action) => {
|
||||
state.settings = { ...state.settings, ...action.payload };
|
||||
state.prevAuthBackend = action.payload.authBackend;
|
||||
state.hasSavedBasicAuth = action.payload.authBackend === 'auth_basic';
|
||||
state.isLoading = false;
|
||||
state.settings = { ...state.settings, ...action.payload }
|
||||
state.prevAuthBackend = action.payload.authBackend
|
||||
state.hasSavedBasicAuth = action.payload.authBackend === 'auth_basic'
|
||||
state.isLoading = false
|
||||
})
|
||||
.addCase(fetchSettings.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string | null;
|
||||
state.isLoading = false
|
||||
state.error = action.payload as string | null
|
||||
})
|
||||
// Fetch Device Model
|
||||
.addCase(fetchDeviceModel.fulfilled, (state, action) => {
|
||||
state.deviceModel = action.payload;
|
||||
state.deviceModel = action.payload
|
||||
})
|
||||
// Update Settings
|
||||
.addCase(updateSettings.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
state.isLoading = true
|
||||
state.error = null
|
||||
})
|
||||
.addCase(updateSettings.fulfilled, (state) => {
|
||||
state.isLoading = false;
|
||||
state.settings.currentPassword = '';
|
||||
state.hasSavedBasicAuth = state.settings.authBackend === 'auth_basic';
|
||||
state.isLoading = false
|
||||
state.settings.currentPassword = ''
|
||||
state.hasSavedBasicAuth = state.settings.authBackend === 'auth_basic'
|
||||
})
|
||||
.addCase(updateSettings.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string | null;
|
||||
state.isLoading = false
|
||||
state.error = action.payload as string | null
|
||||
})
|
||||
// Create Backup
|
||||
.addCase(createBackup.rejected, (state, action) => {
|
||||
state.error = action.payload as string | null;
|
||||
state.error = action.payload as string | null
|
||||
})
|
||||
// Upload Backup
|
||||
.addCase(uploadBackup.pending, (state) => {
|
||||
state.isUploading = true;
|
||||
state.error = null;
|
||||
state.isUploading = true
|
||||
state.error = null
|
||||
})
|
||||
.addCase(uploadBackup.fulfilled, (state) => {
|
||||
state.isUploading = false;
|
||||
state.isUploading = false
|
||||
})
|
||||
.addCase(uploadBackup.rejected, (state, action) => {
|
||||
state.isUploading = false;
|
||||
state.error = action.payload as string | null;
|
||||
state.isUploading = false
|
||||
state.error = action.payload as string | null
|
||||
})
|
||||
// System Operation
|
||||
.addCase(systemOperation.rejected, (state, action) => {
|
||||
state.error = action.payload as string | null;
|
||||
});
|
||||
state.error = action.payload as string | null
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
export const {
|
||||
updateSetting,
|
||||
setUploadProgress,
|
||||
resetUploadState,
|
||||
clearError,
|
||||
} = settingsSlice.actions;
|
||||
} = settingsSlice.actions
|
||||
|
||||
// Selectors
|
||||
export const selectSettings = (state: RootState) => state.settings.settings;
|
||||
export const selectSettings = (state: RootState) => state.settings.settings
|
||||
|
||||
export default settingsSlice.reducer;
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@@ -3,14 +3,14 @@ import {
|
||||
createSlice,
|
||||
createAsyncThunk,
|
||||
ThunkDispatch,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { handleWebSocketMessage } from './message-handler';
|
||||
} from '@reduxjs/toolkit'
|
||||
import { handleWebSocketMessage } from './message-handler'
|
||||
import {
|
||||
RootState,
|
||||
WebSocketMessage,
|
||||
WebSocketState,
|
||||
ExtendedWindow,
|
||||
} from '@/types';
|
||||
} from '@/types'
|
||||
|
||||
const initialState: WebSocketState = {
|
||||
isConnected: false,
|
||||
@@ -18,143 +18,143 @@ const initialState: WebSocketState = {
|
||||
error: null,
|
||||
lastMessage: null,
|
||||
reconnectAttempts: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const MAX_RECONNECT_ATTEMPTS = 5;
|
||||
const RECONNECT_DELAY = 3000;
|
||||
const MAX_RECONNECT_ATTEMPTS = 5
|
||||
const RECONNECT_DELAY = 3000
|
||||
|
||||
export const connectWebSocket = createAsyncThunk(
|
||||
'websocket/connect',
|
||||
async (_, { dispatch, getState }) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
const ws = new WebSocket(wsUrl)
|
||||
|
||||
ws.onopen = () => {
|
||||
dispatch(setConnected(true));
|
||||
dispatch(setConnecting(false));
|
||||
dispatch(setReconnectAttempts(0));
|
||||
resolve();
|
||||
};
|
||||
dispatch(setConnected(true))
|
||||
dispatch(setConnecting(false))
|
||||
dispatch(setReconnectAttempts(0))
|
||||
resolve()
|
||||
}
|
||||
|
||||
ws.onmessage = async (event) => {
|
||||
let messageData: string;
|
||||
let messageData: string
|
||||
|
||||
// Handle different message types (Blob, ArrayBuffer, or string)
|
||||
if (event.data instanceof Blob) {
|
||||
messageData = await event.data.text();
|
||||
messageData = await event.data.text()
|
||||
} else if (event.data instanceof ArrayBuffer) {
|
||||
messageData = new TextDecoder().decode(event.data);
|
||||
messageData = new TextDecoder().decode(event.data)
|
||||
} else {
|
||||
messageData = event.data;
|
||||
messageData = event.data
|
||||
}
|
||||
|
||||
try {
|
||||
const message = JSON.parse(messageData) as WebSocketMessage;
|
||||
dispatch(setLastMessage(message));
|
||||
const message = JSON.parse(messageData) as WebSocketMessage
|
||||
dispatch(setLastMessage(message))
|
||||
handleWebSocketMessage(
|
||||
message,
|
||||
dispatch as ThunkDispatch<RootState, unknown, UnknownAction>,
|
||||
);
|
||||
)
|
||||
} catch {
|
||||
// If it's not JSON, treat it as a string message
|
||||
dispatch(setLastMessage(messageData));
|
||||
dispatch(setLastMessage(messageData))
|
||||
handleWebSocketMessage(
|
||||
messageData,
|
||||
dispatch as ThunkDispatch<RootState, unknown, UnknownAction>,
|
||||
);
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
dispatch(setError('WebSocket connection error'));
|
||||
dispatch(setConnecting(false));
|
||||
reject(new Error('WebSocket connection failed'));
|
||||
};
|
||||
dispatch(setError('WebSocket connection error'))
|
||||
dispatch(setConnecting(false))
|
||||
reject(new Error('WebSocket connection failed'))
|
||||
}
|
||||
|
||||
ws.onclose = (event) => {
|
||||
dispatch(setConnected(false));
|
||||
dispatch(setConnecting(false));
|
||||
dispatch(setConnected(false))
|
||||
dispatch(setConnecting(false))
|
||||
|
||||
// Attempt to reconnect if not a normal closure
|
||||
if (event.code !== 1000) {
|
||||
const state = getState() as RootState;
|
||||
const currentAttempts = state.websocket.reconnectAttempts;
|
||||
const state = getState() as RootState
|
||||
const currentAttempts = state.websocket.reconnectAttempts
|
||||
|
||||
if (currentAttempts < MAX_RECONNECT_ATTEMPTS) {
|
||||
setTimeout(() => {
|
||||
dispatch(setReconnectAttempts(currentAttempts + 1));
|
||||
dispatch(connectWebSocket());
|
||||
}, RECONNECT_DELAY);
|
||||
dispatch(setReconnectAttempts(currentAttempts + 1))
|
||||
dispatch(connectWebSocket())
|
||||
}, RECONNECT_DELAY)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Store the WebSocket instance for later use
|
||||
(window as ExtendedWindow).anthiasWebSocket = ws;
|
||||
});
|
||||
;(window as ExtendedWindow).anthiasWebSocket = ws
|
||||
})
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export const disconnectWebSocket = createAsyncThunk(
|
||||
'websocket/disconnect',
|
||||
async () => {
|
||||
const ws = (window as ExtendedWindow).anthiasWebSocket;
|
||||
const ws = (window as ExtendedWindow).anthiasWebSocket
|
||||
if (ws) {
|
||||
ws.close(1000); // Normal closure
|
||||
(window as ExtendedWindow).anthiasWebSocket = undefined;
|
||||
ws.close(1000) // Normal closure
|
||||
;(window as ExtendedWindow).anthiasWebSocket = undefined
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const websocketSlice = createSlice({
|
||||
name: 'websocket',
|
||||
initialState,
|
||||
reducers: {
|
||||
setConnected: (state, action: { payload: boolean }) => {
|
||||
state.isConnected = action.payload;
|
||||
state.isConnected = action.payload
|
||||
},
|
||||
setConnecting: (state, action: { payload: boolean }) => {
|
||||
state.isConnecting = action.payload;
|
||||
state.isConnecting = action.payload
|
||||
},
|
||||
setError: (state, action: { payload: string }) => {
|
||||
state.error = action.payload;
|
||||
state.error = action.payload
|
||||
},
|
||||
setLastMessage: (
|
||||
state,
|
||||
action: { payload: WebSocketMessage | string | null },
|
||||
) => {
|
||||
state.lastMessage = action.payload;
|
||||
state.lastMessage = action.payload
|
||||
},
|
||||
setReconnectAttempts: (state, action: { payload: number }) => {
|
||||
state.reconnectAttempts = action.payload;
|
||||
state.reconnectAttempts = action.payload
|
||||
},
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
state.error = null
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(connectWebSocket.pending, (state) => {
|
||||
state.isConnecting = true;
|
||||
state.error = null;
|
||||
state.isConnecting = true
|
||||
state.error = null
|
||||
})
|
||||
.addCase(connectWebSocket.fulfilled, (state) => {
|
||||
state.isConnecting = false;
|
||||
state.error = null;
|
||||
state.isConnecting = false
|
||||
state.error = null
|
||||
})
|
||||
.addCase(connectWebSocket.rejected, (state, action) => {
|
||||
state.isConnecting = false;
|
||||
state.error = action.error.message || 'Failed to connect to WebSocket';
|
||||
state.isConnecting = false
|
||||
state.error = action.error.message || 'Failed to connect to WebSocket'
|
||||
})
|
||||
.addCase(disconnectWebSocket.fulfilled, (state) => {
|
||||
state.isConnected = false;
|
||||
state.isConnecting = false;
|
||||
});
|
||||
state.isConnected = false
|
||||
state.isConnecting = false
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
export const {
|
||||
setConnected,
|
||||
@@ -163,6 +163,6 @@ export const {
|
||||
setLastMessage,
|
||||
setReconnectAttempts,
|
||||
clearError,
|
||||
} = websocketSlice.actions;
|
||||
} = websocketSlice.actions
|
||||
|
||||
export default websocketSlice.reducer;
|
||||
export default websocketSlice.reducer
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { UnknownAction, ThunkDispatch } from '@reduxjs/toolkit';
|
||||
import { fetchAssets } from '@/store/assets';
|
||||
import { RootState, WebSocketMessage } from '@/types';
|
||||
import { UnknownAction, ThunkDispatch } from '@reduxjs/toolkit'
|
||||
import { fetchAssets } from '@/store/assets'
|
||||
import { RootState, WebSocketMessage } from '@/types'
|
||||
|
||||
export const handleWebSocketMessage = (
|
||||
message: WebSocketMessage | string,
|
||||
@@ -8,6 +8,6 @@ export const handleWebSocketMessage = (
|
||||
) => {
|
||||
// Only refresh assets if the message is a string resembling an asset ID (32-char hex)
|
||||
if (typeof message === 'string' && /^[a-fA-F0-9]{32}$/.test(message)) {
|
||||
dispatch(fetchAssets());
|
||||
dispatch(fetchAssets())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Alert } from '@/components/alert';
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import { Alert } from '@/components/alert'
|
||||
|
||||
describe('Alert', () => {
|
||||
it('renders the alert message', () => {
|
||||
const testMessage = 'This is a test alert message';
|
||||
render(<Alert message={testMessage} />);
|
||||
const testMessage = 'This is a test alert message'
|
||||
render(<Alert message={testMessage} />)
|
||||
|
||||
expect(screen.getByText(testMessage)).toBeTruthy();
|
||||
});
|
||||
expect(screen.getByText(testMessage)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders the close button', () => {
|
||||
render(<Alert message="Test message" />);
|
||||
render(<Alert message="Test message" />)
|
||||
|
||||
const closeButton = screen.getByRole('button');
|
||||
expect(closeButton).toBeTruthy();
|
||||
expect(closeButton.textContent).toBe('×');
|
||||
});
|
||||
const closeButton = screen.getByRole('button')
|
||||
expect(closeButton).toBeTruthy()
|
||||
expect(closeButton.textContent).toBe('×')
|
||||
})
|
||||
|
||||
it('renders with correct structure', () => {
|
||||
render(<Alert message="Test message" />);
|
||||
render(<Alert message="Test message" />)
|
||||
|
||||
const messageElement = screen.getByText('Test message');
|
||||
expect(messageElement).toBeTruthy();
|
||||
expect(messageElement.tagName).toBe('SPAN');
|
||||
});
|
||||
});
|
||||
const messageElement = screen.getByText('Test message')
|
||||
expect(messageElement).toBeTruthy()
|
||||
expect(messageElement.tagName).toBe('SPAN')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { ScheduleOverview } from '@/components/home';
|
||||
import { RootState } from '@/types';
|
||||
import { assetsReducer, assetModalReducer } from '@/store/assets';
|
||||
import settingsReducer from '@/store/settings';
|
||||
import websocketReducer from '@/store/websocket';
|
||||
import { render, screen, act } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import { ScheduleOverview } from '@/components/home'
|
||||
import { RootState } from '@/types'
|
||||
import { assetsReducer, assetModalReducer } from '@/store/assets'
|
||||
import settingsReducer from '@/store/settings'
|
||||
import websocketReducer from '@/store/websocket'
|
||||
|
||||
const initialState: RootState = {
|
||||
assets: {
|
||||
@@ -105,7 +105,7 @@ const initialState: RootState = {
|
||||
lastMessage: null,
|
||||
reconnectAttempts: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const createTestStore = (preloadedState = {}) => {
|
||||
return configureStore({
|
||||
@@ -116,30 +116,30 @@ const createTestStore = (preloadedState = {}) => {
|
||||
websocket: websocketReducer,
|
||||
},
|
||||
preloadedState,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const renderWithRedux = (
|
||||
component: React.ReactElement,
|
||||
state: RootState = initialState,
|
||||
) => {
|
||||
const store = createTestStore(state);
|
||||
const store = createTestStore(state)
|
||||
return {
|
||||
...render(<Provider store={store}>{component}</Provider>),
|
||||
store,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('ScheduleOverview', () => {
|
||||
it('renders the home page', async () => {
|
||||
await act(async () => {
|
||||
renderWithRedux(<ScheduleOverview />);
|
||||
});
|
||||
renderWithRedux(<ScheduleOverview />)
|
||||
})
|
||||
|
||||
expect(screen.getByText('Schedule Overview')).toBeInTheDocument();
|
||||
expect(screen.getByText('Schedule Overview')).toBeInTheDocument()
|
||||
|
||||
expect(screen.getByText('https://react.dev/')).toBeInTheDocument();
|
||||
expect(screen.getByText('https://angular.dev/')).toBeInTheDocument();
|
||||
expect(screen.getByText('https://vuejs.org/')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByText('https://react.dev/')).toBeInTheDocument()
|
||||
expect(screen.getByText('https://angular.dev/')).toBeInTheDocument()
|
||||
expect(screen.getByText('https://vuejs.org/')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { SystemInfo } from '@/components/system-info';
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import { SystemInfo } from '@/components/system-info'
|
||||
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
|
||||
const server = setupServer(
|
||||
http.get('/api/v2/info', () => {
|
||||
@@ -26,33 +26,33 @@ const server = setupServer(
|
||||
device_model: 'Generic x86_64 Device',
|
||||
anthias_version: 'master@3a4747f',
|
||||
mac_address: 'Unable to retrieve MAC address.',
|
||||
});
|
||||
})
|
||||
}),
|
||||
http.get('/api/v2/device_settings', () => {
|
||||
return HttpResponse.json({
|
||||
player_name: 'Test Player',
|
||||
});
|
||||
})
|
||||
}),
|
||||
);
|
||||
)
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
beforeAll(() => server.listen())
|
||||
afterEach(() => server.resetHandlers())
|
||||
afterAll(() => server.close())
|
||||
|
||||
describe('SystemInfo', () => {
|
||||
it('renders the system info', async () => {
|
||||
render(<SystemInfo />);
|
||||
render(<SystemInfo />)
|
||||
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('System Info');
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('System Info')
|
||||
|
||||
expect(screen.getByText('Load Average')).toBeInTheDocument();
|
||||
expect(screen.getByText('Free Space')).toBeInTheDocument();
|
||||
expect(screen.getByText('Memory')).toBeInTheDocument();
|
||||
expect(screen.getByText('Uptime')).toBeInTheDocument();
|
||||
expect(screen.getByText('Display Power (CEC)')).toBeInTheDocument();
|
||||
expect(screen.getByText('Device Model')).toBeInTheDocument();
|
||||
expect(screen.getByText('Anthias Version')).toBeInTheDocument();
|
||||
expect(screen.getByText('MAC Address')).toBeInTheDocument();
|
||||
expect(screen.getByText('Load Average')).toBeInTheDocument()
|
||||
expect(screen.getByText('Free Space')).toBeInTheDocument()
|
||||
expect(screen.getByText('Memory')).toBeInTheDocument()
|
||||
expect(screen.getByText('Uptime')).toBeInTheDocument()
|
||||
expect(screen.getByText('Display Power (CEC)')).toBeInTheDocument()
|
||||
expect(screen.getByText('Device Model')).toBeInTheDocument()
|
||||
expect(screen.getByText('Anthias Version')).toBeInTheDocument()
|
||||
expect(screen.getByText('MAC Address')).toBeInTheDocument()
|
||||
|
||||
const expectedValues = [
|
||||
'1.58',
|
||||
@@ -62,11 +62,11 @@ describe('SystemInfo', () => {
|
||||
'Generic x86_64 Device',
|
||||
'master@3a4747f',
|
||||
'Unable to retrieve MAC address.',
|
||||
];
|
||||
]
|
||||
|
||||
for (const value of expectedValues) {
|
||||
const element = await screen.findByText(value);
|
||||
expect(element).toBeInTheDocument();
|
||||
const element = await screen.findByText(value)
|
||||
expect(element).toBeInTheDocument()
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,271 +1,271 @@
|
||||
// Centralized type definitions for the Anthias application
|
||||
import { store } from '@/store/index';
|
||||
import { store } from '@/store/index'
|
||||
|
||||
// Asset-related types
|
||||
export interface Asset {
|
||||
asset_id: string;
|
||||
name: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
duration: number;
|
||||
uri: string;
|
||||
mimetype: string;
|
||||
is_enabled: number;
|
||||
nocache: boolean;
|
||||
skip_asset_check: boolean;
|
||||
is_active: boolean;
|
||||
play_order: number;
|
||||
is_processing: boolean;
|
||||
asset_id: string
|
||||
name: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
duration: number
|
||||
uri: string
|
||||
mimetype: string
|
||||
is_enabled: number
|
||||
nocache: boolean
|
||||
skip_asset_check: boolean
|
||||
is_active: boolean
|
||||
play_order: number
|
||||
is_processing: boolean
|
||||
}
|
||||
|
||||
export interface AssetEditData {
|
||||
id: string;
|
||||
name: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
duration: number;
|
||||
uri: string;
|
||||
mimetype: string;
|
||||
is_enabled: boolean;
|
||||
nocache: boolean;
|
||||
skip_asset_check: boolean;
|
||||
play_order?: number;
|
||||
id: string
|
||||
name: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
duration: number
|
||||
uri: string
|
||||
mimetype: string
|
||||
is_enabled: boolean
|
||||
nocache: boolean
|
||||
skip_asset_check: boolean
|
||||
play_order?: number
|
||||
}
|
||||
|
||||
export interface EditFormData {
|
||||
name: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
duration: string;
|
||||
mimetype: string;
|
||||
nocache: boolean;
|
||||
skip_asset_check: boolean;
|
||||
name: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
duration: string
|
||||
mimetype: string
|
||||
nocache: boolean
|
||||
skip_asset_check: boolean
|
||||
}
|
||||
|
||||
export interface HandleSubmitParams {
|
||||
e: React.FormEvent;
|
||||
asset: AssetEditData;
|
||||
formData: EditFormData;
|
||||
startDateDate: string;
|
||||
startDateTime: string;
|
||||
endDateDate: string;
|
||||
endDateTime: string;
|
||||
dispatch: AppDispatch;
|
||||
onClose: () => void;
|
||||
setIsSubmitting: (isSubmitting: boolean) => void;
|
||||
e: React.FormEvent
|
||||
asset: AssetEditData
|
||||
formData: EditFormData
|
||||
startDateDate: string
|
||||
startDateTime: string
|
||||
endDateDate: string
|
||||
endDateTime: string
|
||||
dispatch: AppDispatch
|
||||
onClose: () => void
|
||||
setIsSubmitting: (isSubmitting: boolean) => void
|
||||
}
|
||||
|
||||
// WebSocket-related types
|
||||
export interface WebSocketMessage {
|
||||
type?: string;
|
||||
data?: unknown;
|
||||
asset_id?: string;
|
||||
type?: string
|
||||
data?: unknown
|
||||
asset_id?: string
|
||||
}
|
||||
|
||||
export interface WebSocketState {
|
||||
isConnected: boolean;
|
||||
isConnecting: boolean;
|
||||
error: string | null;
|
||||
lastMessage: WebSocketMessage | string | null;
|
||||
reconnectAttempts: number;
|
||||
isConnected: boolean
|
||||
isConnecting: boolean
|
||||
error: string | null
|
||||
lastMessage: WebSocketMessage | string | null
|
||||
reconnectAttempts: number
|
||||
}
|
||||
|
||||
export interface ExtendedWindow extends Window {
|
||||
anthiasWebSocket?: WebSocket;
|
||||
anthiasWebSocket?: WebSocket
|
||||
}
|
||||
|
||||
// Redux store types
|
||||
export interface RootState {
|
||||
assets: {
|
||||
items: Asset[];
|
||||
status: 'idle' | 'loading' | 'succeeded' | 'failed';
|
||||
error: string | null;
|
||||
};
|
||||
items: Asset[]
|
||||
status: 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: string | null
|
||||
}
|
||||
assetModal: {
|
||||
activeTab: string;
|
||||
activeTab: string
|
||||
formData: {
|
||||
uri: string;
|
||||
skipAssetCheck: boolean;
|
||||
name?: string;
|
||||
mimetype?: string;
|
||||
duration?: number;
|
||||
uri: string
|
||||
skipAssetCheck: boolean
|
||||
name?: string
|
||||
mimetype?: string
|
||||
duration?: number
|
||||
dates?: {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
};
|
||||
};
|
||||
isValid: boolean;
|
||||
errorMessage: string;
|
||||
statusMessage: string;
|
||||
uploadProgress: number;
|
||||
isSubmitting: boolean;
|
||||
};
|
||||
start_date: string
|
||||
end_date: string
|
||||
}
|
||||
}
|
||||
isValid: boolean
|
||||
errorMessage: string
|
||||
statusMessage: string
|
||||
uploadProgress: number
|
||||
isSubmitting: boolean
|
||||
}
|
||||
settings: {
|
||||
settings: {
|
||||
playerName: string;
|
||||
defaultDuration: number;
|
||||
defaultStreamingDuration: number;
|
||||
audioOutput: string;
|
||||
dateFormat: string;
|
||||
authBackend: string;
|
||||
currentPassword: string;
|
||||
user: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
showSplash: boolean;
|
||||
defaultAssets: boolean;
|
||||
shufflePlaylist: boolean;
|
||||
use24HourClock: boolean;
|
||||
debugLogging: boolean;
|
||||
};
|
||||
deviceModel: string;
|
||||
prevAuthBackend: string;
|
||||
hasSavedBasicAuth: boolean;
|
||||
isLoading: boolean;
|
||||
isUploading: boolean;
|
||||
uploadProgress: number;
|
||||
error: string | null;
|
||||
};
|
||||
websocket: WebSocketState;
|
||||
playerName: string
|
||||
defaultDuration: number
|
||||
defaultStreamingDuration: number
|
||||
audioOutput: string
|
||||
dateFormat: string
|
||||
authBackend: string
|
||||
currentPassword: string
|
||||
user: string
|
||||
password: string
|
||||
confirmPassword: string
|
||||
showSplash: boolean
|
||||
defaultAssets: boolean
|
||||
shufflePlaylist: boolean
|
||||
use24HourClock: boolean
|
||||
debugLogging: boolean
|
||||
}
|
||||
deviceModel: string
|
||||
prevAuthBackend: string
|
||||
hasSavedBasicAuth: boolean
|
||||
isLoading: boolean
|
||||
isUploading: boolean
|
||||
uploadProgress: number
|
||||
error: string | null
|
||||
}
|
||||
websocket: WebSocketState
|
||||
}
|
||||
|
||||
// Component prop types
|
||||
export interface ActiveAssetsTableProps {
|
||||
onEditAsset: (asset: AssetEditData) => void;
|
||||
onEditAsset: (asset: AssetEditData) => void
|
||||
}
|
||||
|
||||
export interface InactiveAssetsTableProps {
|
||||
onEditAsset: (asset: AssetEditData) => void;
|
||||
onEditAsset: (asset: AssetEditData) => void
|
||||
}
|
||||
|
||||
export interface AssetRowProps {
|
||||
assetId: string;
|
||||
name: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
duration: number;
|
||||
uri: string;
|
||||
mimetype: string;
|
||||
isEnabled: boolean;
|
||||
nocache: boolean;
|
||||
skipAssetCheck: boolean;
|
||||
isProcessing?: number;
|
||||
style?: React.CSSProperties;
|
||||
showDragHandle?: boolean;
|
||||
dragHandleProps?: React.HTMLAttributes<HTMLElement>;
|
||||
isDragging?: boolean;
|
||||
onEditAsset?: (asset: AssetEditData) => void;
|
||||
assetId: string
|
||||
name: string
|
||||
startDate: string
|
||||
endDate: string
|
||||
duration: number
|
||||
uri: string
|
||||
mimetype: string
|
||||
isEnabled: boolean
|
||||
nocache: boolean
|
||||
skipAssetCheck: boolean
|
||||
isProcessing?: number
|
||||
style?: React.CSSProperties
|
||||
showDragHandle?: boolean
|
||||
dragHandleProps?: React.HTMLAttributes<HTMLElement>
|
||||
isDragging?: boolean
|
||||
onEditAsset?: (asset: AssetEditData) => void
|
||||
}
|
||||
|
||||
// Settings-related types
|
||||
export interface SettingsData {
|
||||
playerName: string;
|
||||
defaultDuration: number;
|
||||
defaultStreamingDuration: number;
|
||||
audioOutput: string;
|
||||
dateFormat: string;
|
||||
authBackend: string;
|
||||
currentPassword: string;
|
||||
user: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
showSplash: boolean;
|
||||
defaultAssets: boolean;
|
||||
shufflePlaylist: boolean;
|
||||
use24HourClock: boolean;
|
||||
debugLogging: boolean;
|
||||
playerName: string
|
||||
defaultDuration: number
|
||||
defaultStreamingDuration: number
|
||||
audioOutput: string
|
||||
dateFormat: string
|
||||
authBackend: string
|
||||
currentPassword: string
|
||||
user: string
|
||||
password: string
|
||||
confirmPassword: string
|
||||
showSplash: boolean
|
||||
defaultAssets: boolean
|
||||
shufflePlaylist: boolean
|
||||
use24HourClock: boolean
|
||||
debugLogging: boolean
|
||||
}
|
||||
|
||||
export interface SystemOperationParams {
|
||||
operation: string;
|
||||
endpoint: string;
|
||||
successMessage: string;
|
||||
operation: string
|
||||
endpoint: string
|
||||
successMessage: string
|
||||
}
|
||||
|
||||
export interface OperationConfig {
|
||||
operation?: string;
|
||||
endpoint: string;
|
||||
successMessage: string;
|
||||
confirmMessage?: string;
|
||||
title?: string;
|
||||
text?: string;
|
||||
confirmButtonText?: string;
|
||||
errorMessage?: string;
|
||||
operation?: string
|
||||
endpoint: string
|
||||
successMessage: string
|
||||
confirmMessage?: string
|
||||
title?: string
|
||||
text?: string
|
||||
confirmButtonText?: string
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
// Asset modal types
|
||||
export interface UploadFileParams {
|
||||
file: File;
|
||||
skipAssetCheck: boolean;
|
||||
file: File
|
||||
skipAssetCheck: boolean
|
||||
}
|
||||
|
||||
export interface SaveAssetParams {
|
||||
assetData: {
|
||||
duration: number;
|
||||
end_date: string;
|
||||
is_active: number;
|
||||
is_enabled: number;
|
||||
is_processing: number;
|
||||
mimetype: string;
|
||||
name: string;
|
||||
nocache: number;
|
||||
play_order: number;
|
||||
skip_asset_check: number;
|
||||
start_date: string;
|
||||
uri: string;
|
||||
};
|
||||
duration: number
|
||||
end_date: string
|
||||
is_active: number
|
||||
is_enabled: number
|
||||
is_processing: number
|
||||
mimetype: string
|
||||
name: string
|
||||
nocache: number
|
||||
play_order: number
|
||||
skip_asset_check: number
|
||||
start_date: string
|
||||
uri: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface FileData {
|
||||
uri: string;
|
||||
ext: string;
|
||||
uri: string
|
||||
ext: string
|
||||
}
|
||||
|
||||
export interface FormData {
|
||||
uri: string;
|
||||
skipAssetCheck: boolean;
|
||||
name?: string;
|
||||
mimetype?: string;
|
||||
duration?: number;
|
||||
uri: string
|
||||
skipAssetCheck: boolean
|
||||
name?: string
|
||||
mimetype?: string
|
||||
duration?: number
|
||||
dates?: {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
};
|
||||
start_date: string
|
||||
end_date: string
|
||||
}
|
||||
}
|
||||
|
||||
// Redux Toolkit types
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type AsyncThunkAction = ReturnType<typeof store.dispatch>;
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
export type AsyncThunkAction = ReturnType<typeof store.dispatch>
|
||||
|
||||
// Asset thunk types
|
||||
export interface ToggleAssetParams {
|
||||
assetId: string;
|
||||
newValue: number;
|
||||
assetId: string
|
||||
newValue: number
|
||||
}
|
||||
|
||||
// System info types
|
||||
export interface AnthiasVersionValueProps {
|
||||
version: string;
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface SkeletonProps {
|
||||
children: React.ReactNode;
|
||||
isLoading: boolean;
|
||||
children: React.ReactNode
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export interface MemoryInfo {
|
||||
total: number;
|
||||
used: number;
|
||||
free: number;
|
||||
shared: number;
|
||||
buff: number;
|
||||
available: number;
|
||||
percentage?: number;
|
||||
total: number
|
||||
used: number
|
||||
free: number
|
||||
shared: number
|
||||
buff: number
|
||||
available: number
|
||||
percentage?: number
|
||||
}
|
||||
|
||||
export interface UptimeInfo {
|
||||
days: number;
|
||||
hours: number;
|
||||
minutes?: number;
|
||||
seconds?: number;
|
||||
days: number
|
||||
hours: number
|
||||
minutes?: number
|
||||
seconds?: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user