mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
1185 lines
35 KiB
JavaScript
1185 lines
35 KiB
JavaScript
import React, {useContext, useEffect, useRef, useState} from "react";
|
|
import {
|
|
fetchDecoded,
|
|
fetchDomains,
|
|
fetchFilesProgress,
|
|
filesSearch,
|
|
getArchive,
|
|
getChannel,
|
|
getChannels,
|
|
getDirectories,
|
|
getDownloads,
|
|
getFiles,
|
|
getInventory,
|
|
getOutdatedZims,
|
|
getSettings,
|
|
getStatistics,
|
|
getStatus,
|
|
getVideo,
|
|
getVideosStatistics,
|
|
searchArchives,
|
|
searchDirectories,
|
|
searchVideos,
|
|
searchZim,
|
|
setHotspot,
|
|
setThrottle,
|
|
} from "../api";
|
|
import {createSearchParams, useLocation, useSearchParams} from "react-router-dom";
|
|
import {enumerate, filterToMimetypes, humanFileSize, secondsToFullDuration} from "../components/Common";
|
|
import {QueryContext, SettingsContext, StatusContext} from "../contexts/contexts";
|
|
import {toast} from "react-semantic-toasts-2";
|
|
import {useSearch} from "../components/Search";
|
|
import _ from "lodash";
|
|
|
|
const calculatePage = (offset, limit) => {
|
|
return offset && limit ? Math.round((offset / limit) + 1) : 1;
|
|
}
|
|
|
|
const calculateTotalPages = (total, limit) => {
|
|
// Return the total divided by the limit, but always at least 1.
|
|
if (!total || !limit || total < limit) {
|
|
return 1;
|
|
}
|
|
return Math.round(total / limit) + 1;
|
|
}
|
|
|
|
export const useRecurringTimeout = (callback, delay) => {
|
|
const timer = useRef(null);
|
|
|
|
useEffect(() => {
|
|
const repeat = async () => {
|
|
// Recursively call the callback.
|
|
await callback();
|
|
timer.current = window.setTimeout(repeat, delay);
|
|
}
|
|
|
|
// Call the repeating function instantly to fetch data fast.
|
|
repeat();
|
|
|
|
// Clear the timeout on unload.
|
|
return () => clearTimeout(timer.current);
|
|
}, [delay])
|
|
}
|
|
|
|
export const useLatestRequest = (delay = 300, defaultLoading = false) => {
|
|
// A hook which ignores older requests and will only set `data` to the latest response's data.
|
|
// usage: sendRequest(async () => await yourAPICall(...args));
|
|
|
|
// The results from awaiting `fetchFunction`.
|
|
const [data, setData] = React.useState(null);
|
|
const latestRequestRef = React.useRef(0);
|
|
const debounceTimerRef = React.useRef(null);
|
|
// Loading while awaiting.
|
|
const [loading, setLoading] = React.useState(defaultLoading);
|
|
|
|
const sendRequest = React.useCallback((fetchFunction) => {
|
|
if (debounceTimerRef.current) {
|
|
clearTimeout(debounceTimerRef.current);
|
|
}
|
|
|
|
debounceTimerRef.current = setTimeout(async () => {
|
|
setLoading(true);
|
|
const requestId = ++latestRequestRef.current;
|
|
|
|
try {
|
|
const result = await fetchFunction();
|
|
|
|
if (requestId === latestRequestRef.current) {
|
|
setData(result);
|
|
}
|
|
} catch (error) {
|
|
console.error('Request failed:', error);
|
|
setData(null);
|
|
} finally {
|
|
if (requestId === latestRequestRef.current) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
}, delay);
|
|
}, [delay]);
|
|
|
|
return {data, sendRequest, loading};
|
|
};
|
|
|
|
const getSearchParamCopy = (searchParams) => {
|
|
let copy = {};
|
|
if (searchParams) {
|
|
Array.from(searchParams.keys()).forEach(key => {
|
|
const value = searchParams.getAll(key)
|
|
copy[key] = value.length === 1 ? value[0] : value;
|
|
})
|
|
}
|
|
copy = removeEmptyValues(copy);
|
|
return copy;
|
|
}
|
|
|
|
const removeEmptyValues = (obj) => {
|
|
Object.entries(obj).forEach(([k, v]) => {
|
|
// Clear any empty values.
|
|
if (v === null || v === undefined || (Array.isArray(v) && v.length === 0)) {
|
|
delete obj[k];
|
|
}
|
|
});
|
|
return obj
|
|
}
|
|
|
|
|
|
export const useQuery = () => {
|
|
const location = useLocation();
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
const updateQuery = (newParams, replace = false) => {
|
|
// Update the old state with the new values.
|
|
let newSearchParams = replace ?
|
|
newParams
|
|
: {...getSearchParamCopy(searchParams), ...newParams};
|
|
newSearchParams = removeEmptyValues(newSearchParams);
|
|
console.debug('useQuery.updateQuery', newSearchParams);
|
|
setSearchParams(newSearchParams);
|
|
};
|
|
|
|
const getLocationStr = (newSearchParams, pathname) => {
|
|
// Get the current location, but with new params appended.
|
|
newSearchParams = removeEmptyValues({...getSearchParamCopy(searchParams), ...newSearchParams});
|
|
const newQuery = createSearchParams(newSearchParams);
|
|
return `${pathname || location.pathname}?${newQuery.toString()}`
|
|
}
|
|
|
|
return {searchParams, updateQuery, getLocationStr}
|
|
}
|
|
|
|
|
|
export const QueryProvider = (props) => {
|
|
return <QueryContext.Provider value={useQuery()}>
|
|
{props.children}
|
|
</QueryContext.Provider>
|
|
}
|
|
|
|
export const useOneQuery = (name) => {
|
|
const {searchParams, updateQuery} = React.useContext(QueryContext);
|
|
const value = searchParams.get(name);
|
|
|
|
const setValue = (newValue, replace = false) => {
|
|
updateQuery({[name]: newValue}, replace);
|
|
}
|
|
|
|
return [value, setValue]
|
|
}
|
|
|
|
export const useAllQuery = (name) => {
|
|
const {searchParams, updateQuery} = React.useContext(QueryContext);
|
|
const value = searchParams.getAll(name);
|
|
|
|
const setValue = (newValue, replace = false) => {
|
|
updateQuery({[name]: newValue}, replace);
|
|
}
|
|
|
|
return [value, setValue]
|
|
}
|
|
|
|
export const useDomains = () => {
|
|
const [domains, setDomains] = useState(null);
|
|
const [total, setTotal] = useState(null);
|
|
|
|
const _fetchDomains = async () => {
|
|
setDomains(null);
|
|
setTotal(0);
|
|
try {
|
|
let [domains, total] = await fetchDomains();
|
|
setDomains(domains);
|
|
setTotal(total);
|
|
} catch (e) {
|
|
setDomains(undefined); // Display error.
|
|
setTotal(0);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
_fetchDomains();
|
|
}, []);
|
|
|
|
return [domains, total];
|
|
}
|
|
|
|
export const useArchive = (archiveId) => {
|
|
const [archiveFileGroup, setArchiveFileGroup] = useState(null);
|
|
const [history, setHistory] = useState(null);
|
|
|
|
const fetchArchive = async () => {
|
|
try {
|
|
const [file_group, history] = await getArchive(archiveId);
|
|
setArchiveFileGroup(file_group);
|
|
setHistory(history);
|
|
} catch (e) {
|
|
console.error(e);
|
|
setArchiveFileGroup(undefined);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchArchive();
|
|
}, [archiveId]);
|
|
|
|
return {archiveFile: archiveFileGroup, history, fetchArchive};
|
|
}
|
|
|
|
export const usePages = (defaultLimit = 24, totalPages = 0) => {
|
|
const {searchParams, updateQuery} = React.useContext(QueryContext);
|
|
const offset = searchParams.get('o') || 0;
|
|
const limit = parseInt(searchParams.get('l') || defaultLimit || 24);
|
|
const [activePage, setActivePage] = useState(calculatePage(offset, limit));
|
|
const [totalPages_, setTotalPages] = useState(totalPages);
|
|
|
|
const setLimit = (value) => {
|
|
setPage(1);
|
|
updateQuery({l: value, o: null});
|
|
}
|
|
|
|
const setPage = (value) => {
|
|
console.debug(`setPage ${value}`);
|
|
setActivePage(value);
|
|
value = value - 1; // Page really starts as 0.
|
|
updateQuery({o: value * limit});
|
|
}
|
|
|
|
const setTotal = (total) => {
|
|
const newTotalPages = calculateTotalPages(total, limit);
|
|
console.debug('newTotalPages', newTotalPages);
|
|
setTotalPages(newTotalPages);
|
|
}
|
|
|
|
// Used for useEffect.
|
|
const effect = JSON.stringify({offset, limit, activePage});
|
|
|
|
return {offset, limit, setLimit, activePage, setPage, totalPages: totalPages_, setTotal, effect};
|
|
}
|
|
|
|
export const useSearchArchives = (defaultLimit) => {
|
|
const {domain} = useSearchDomain();
|
|
const {offset, limit, setLimit, activePage, setPage} = usePages(defaultLimit);
|
|
const {searchParams, updateQuery} = React.useContext(QueryContext);
|
|
const searchStr = searchParams.get('q') || '';
|
|
const order = searchParams.get('order');
|
|
const activeTags = searchParams.getAll('tag');
|
|
const {view} = useSearchView();
|
|
const headline = view === 'headline';
|
|
|
|
const [archives, setArchives] = useState(null);
|
|
const [totalPages, setTotalPages] = useState(0);
|
|
|
|
const localSearchArchives = async () => {
|
|
setArchives(null);
|
|
setTotalPages(0);
|
|
try {
|
|
let [archives_, total] = await searchArchives(offset, limit, domain, searchStr, order, activeTags, headline);
|
|
setArchives(archives_);
|
|
setTotalPages(calculateTotalPages(total, limit));
|
|
} catch (e) {
|
|
console.error(e);
|
|
toast({
|
|
type: 'error',
|
|
title: 'Unexpected server response',
|
|
description: 'Could not get archives',
|
|
time: 5000,
|
|
});
|
|
setArchives(undefined); // Could not get Archives, display error.
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
localSearchArchives();
|
|
}, [searchStr, limit, domain, order, activePage, JSON.stringify(activeTags), headline]);
|
|
|
|
const setSearchStr = (value) => {
|
|
updateQuery({q: value, o: 0, order: undefined});
|
|
}
|
|
|
|
const setOrderBy = (value) => {
|
|
setPage(1);
|
|
updateQuery({order: value});
|
|
}
|
|
|
|
return {
|
|
archives,
|
|
limit,
|
|
setLimit,
|
|
offset,
|
|
order,
|
|
setOrderBy,
|
|
totalPages,
|
|
activePage,
|
|
setPage,
|
|
searchStr,
|
|
setSearchStr,
|
|
fetchArchives: localSearchArchives,
|
|
}
|
|
}
|
|
|
|
export const useSearchVideos = (defaultLimit, channelId, order_by) => {
|
|
const {searchParams, updateQuery} = React.useContext(QueryContext);
|
|
const {offset, limit, setLimit, activePage, setPage} = usePages(defaultLimit);
|
|
const searchStr = searchParams.get('q') || '';
|
|
const order = searchParams.get('order') || order_by;
|
|
const activeTags = searchParams.getAll('tag');
|
|
const {view} = useSearchView();
|
|
const headline = view === 'headline';
|
|
|
|
const [videos, setVideos] = useState(null);
|
|
const [totalPages, setTotalPages] = useState(0);
|
|
|
|
const localSearchVideos = async () => {
|
|
setVideos(null);
|
|
setTotalPages(0);
|
|
try {
|
|
let [videos_, total] = await searchVideos(offset, limit, channelId, searchStr, order, activeTags, headline);
|
|
setVideos(videos_);
|
|
setTotalPages(calculateTotalPages(total, limit));
|
|
} catch (e) {
|
|
console.error(e);
|
|
setVideos(undefined);// Could not get Videos, display error.
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
localSearchVideos();
|
|
}, [searchStr, limit, channelId, offset, order_by, JSON.stringify(activeTags), headline]);
|
|
|
|
const setSearchStr = (value) => {
|
|
updateQuery({q: value, o: 0, order: undefined});
|
|
}
|
|
|
|
const setOrderBy = (value) => {
|
|
setPage(1);
|
|
updateQuery({order: value});
|
|
}
|
|
|
|
return {
|
|
videos,
|
|
totalPages,
|
|
limit,
|
|
offset,
|
|
order,
|
|
searchStr,
|
|
activePage,
|
|
setPage,
|
|
setLimit,
|
|
setSearchStr,
|
|
setOrderBy,
|
|
fetchVideos: localSearchVideos,
|
|
activeTags,
|
|
}
|
|
}
|
|
|
|
export const useVideo = (videoId) => {
|
|
const [videoFile, setVideoFile] = useState(null);
|
|
const [prevFile, setPrevFile] = useState(null);
|
|
const [nextFile, setNextFile] = useState(null);
|
|
|
|
const fetchVideo = async () => {
|
|
if (videoId && videoFile && Number(videoFile.video.id) !== Number(videoId)) {
|
|
// Video is changing. Clear the old video before fetching.
|
|
setVideoFile(null);
|
|
setPrevFile(null);
|
|
setNextFile(null);
|
|
}
|
|
try {
|
|
const [v, p, n] = await getVideo(videoId);
|
|
setVideoFile(v);
|
|
setPrevFile(p);
|
|
setNextFile(n);
|
|
} catch (e) {
|
|
console.error(e);
|
|
setVideoFile(undefined);
|
|
setPrevFile(undefined);
|
|
setNextFile(undefined);
|
|
toast({
|
|
type: 'error',
|
|
title: 'Unexpected server response',
|
|
description: 'Could not get the video',
|
|
time: 5000,
|
|
})
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchVideo();
|
|
}, [videoId]);
|
|
|
|
return {videoFile, prevFile, nextFile, fetchVideo};
|
|
}
|
|
|
|
export const useChannel = (channel_id) => {
|
|
const emptyChannel = {
|
|
name: '',
|
|
directory: '',
|
|
mkdir: false,
|
|
url: '',
|
|
download_frequency: '',
|
|
match_regex: '',
|
|
};
|
|
const [fetched, setFetched] = useState(false);
|
|
const [channel, setChannel] = useState(emptyChannel);
|
|
const [original, setOriginal] = useState({});
|
|
|
|
const localGetChannel = async () => {
|
|
if (channel_id) {
|
|
try {
|
|
const c = await getChannel(channel_id);
|
|
// Prevent controlled to uncontrolled.
|
|
c['url'] = c['url'] || '';
|
|
c['download_frequency'] = c['download_frequency'] || '';
|
|
c['match_regex'] = c['match_regex'] || '';
|
|
setChannel(c);
|
|
setOriginal(c);
|
|
setFetched(true);
|
|
} catch (e) {
|
|
console.error(e);
|
|
toast({
|
|
type: 'error',
|
|
title: 'Unexpected server response',
|
|
description: 'Could not get Channel',
|
|
time: 5000,
|
|
});
|
|
}
|
|
} else {
|
|
setChannel(emptyChannel);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
localGetChannel();
|
|
}, [channel_id])
|
|
|
|
const changeValue = (name, value) => {
|
|
setChannel({...channel, [name]: value});
|
|
}
|
|
|
|
return {channel, changeValue, original, fetched, fetchChannel: localGetChannel};
|
|
}
|
|
|
|
export const useChannels = () => {
|
|
const [channels, setChannels] = useState(null);
|
|
|
|
const fetchChannels = async () => {
|
|
setChannels(null);
|
|
try {
|
|
const c = await getChannels();
|
|
setChannels(c);
|
|
} catch (e) {
|
|
console.error(e);
|
|
setChannels(undefined); // Could not get Channels, display error.
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchChannels();
|
|
}, []);
|
|
|
|
return {channels, fetchChannels}
|
|
}
|
|
|
|
export const useSearchFiles = (defaultLimit = 48, emptySearch = false, model) => {
|
|
const {
|
|
activeTags,
|
|
pages,
|
|
searchStr,
|
|
filter,
|
|
model: model_,
|
|
setSearchStr,
|
|
months,
|
|
dateRange,
|
|
} = useSearch(defaultLimit, emptySearch, model);
|
|
const {view} = useSearchView();
|
|
|
|
const [searchFiles, setSearchFiles] = useState(null);
|
|
const headline = view === 'headline';
|
|
|
|
const localSearchFiles = async () => {
|
|
if (!emptySearch && !searchStr && !activeTags) {
|
|
return;
|
|
}
|
|
const mimetypes = filterToMimetypes(filter);
|
|
setSearchFiles(null);
|
|
let fromDate;
|
|
let toDate;
|
|
if (dateRange) {
|
|
fromDate = dateRange[0];
|
|
toDate = dateRange[1];
|
|
}
|
|
console.log('localSearchFiles', fromDate, toDate, months);
|
|
try {
|
|
let [file_groups, total] = await filesSearch(
|
|
pages.offset, pages.limit, searchStr, mimetypes, model || model_, activeTags, headline,
|
|
months, fromDate, toDate);
|
|
setSearchFiles(file_groups);
|
|
pages.setTotal(total);
|
|
} catch (e) {
|
|
pages.setTotal(0);
|
|
console.error(e);
|
|
toast({
|
|
type: 'error',
|
|
title: 'Unexpected server response',
|
|
description: 'Could not get files',
|
|
time: 5000,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Only search after the user has stopped typing. Estimates will always happen.
|
|
const debouncedLocalSearchFiles = _.debounce(async () => {
|
|
await localSearchFiles();
|
|
}, 1000);
|
|
|
|
useEffect(() => {
|
|
if (searchStr || (activeTags && activeTags.length > 0)) {
|
|
debouncedLocalSearchFiles();
|
|
}
|
|
// Handle when this is unmounted.
|
|
return () => debouncedLocalSearchFiles.cancel();
|
|
}, [
|
|
searchStr,
|
|
pages.effect,
|
|
filter,
|
|
model,
|
|
model_,
|
|
JSON.stringify(activeTags),
|
|
headline,
|
|
JSON.stringify(months),
|
|
JSON.stringify(dateRange),
|
|
]);
|
|
|
|
return {
|
|
searchFiles,
|
|
searchStr,
|
|
filter,
|
|
setSearchStr,
|
|
pages,
|
|
activeTags
|
|
};
|
|
}
|
|
|
|
export const useBrowseFiles = () => {
|
|
const [browseFiles, setBrowseFiles] = useState(null);
|
|
const [openFolders, setOpenFolders] = useAllQuery('folders');
|
|
|
|
const fetchFiles = async () => {
|
|
try {
|
|
const files = await getFiles(openFolders);
|
|
setBrowseFiles(files);
|
|
} catch (e) {
|
|
console.error(e);
|
|
setBrowseFiles(undefined); // Display error.
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchFiles();
|
|
}, [JSON.stringify(openFolders)])
|
|
|
|
return {browseFiles, openFolders, setOpenFolders, fetchFiles};
|
|
}
|
|
|
|
export const useFilesProgress = () => {
|
|
const [progress, setProgress] = useState(null);
|
|
|
|
const localFetchFilesProgress = async () => {
|
|
try {
|
|
let p = await fetchFilesProgress();
|
|
setProgress(p);
|
|
} catch (e) {
|
|
setProgress(null);
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
localFetchFilesProgress();
|
|
}, []);
|
|
|
|
return {progress, fetchFilesProgress: localFetchFilesProgress}
|
|
}
|
|
|
|
export const useFilesProgressInterval = () => {
|
|
const {progress, fetchFilesProgress} = useFilesProgress();
|
|
|
|
useRecurringTimeout(fetchFilesProgress, 1000 * 3);
|
|
|
|
return {progress, fetchFilesProgress};
|
|
}
|
|
|
|
export const useHotspot = () => {
|
|
const [on, setOn] = useState(null);
|
|
const {status} = useContext(StatusContext);
|
|
// Hotspot is unsupported on Docker.
|
|
const {dockerized} = status;
|
|
|
|
useEffect(() => {
|
|
if (status && status['hotspot_status']) {
|
|
const {hotspot_status} = status;
|
|
if (hotspot_status === 'connected') {
|
|
setOn(true);
|
|
} else if (hotspot_status === 'disconnected' || hotspot_status === 'unavailable') {
|
|
setOn(false);
|
|
} else {
|
|
setOn(null);
|
|
}
|
|
}
|
|
}, [status]);
|
|
|
|
const localSetHotspot = async (on) => {
|
|
setOn(null);
|
|
await setHotspot(on);
|
|
}
|
|
|
|
return {on, setOn, setHotspot: localSetHotspot, dockerized};
|
|
}
|
|
|
|
export const useDownloads = () => {
|
|
const [onceDownloads, setOnceDownloads] = useState(null);
|
|
const [recurringDownloads, setRecurringDownloads] = useState(null);
|
|
const [pendingOnceDownloads, setPendingOnceDownloads] = useState(null);
|
|
|
|
const fetchDownloads = async () => {
|
|
try {
|
|
const data = await getDownloads();
|
|
setOnceDownloads(data['once_downloads']);
|
|
setRecurringDownloads(data['recurring_downloads']);
|
|
setPendingOnceDownloads(data['pending_once_downloads']);
|
|
} catch (e) {
|
|
console.error(e);
|
|
// Display errors.
|
|
setOnceDownloads(undefined);
|
|
setRecurringDownloads(undefined);
|
|
setPendingOnceDownloads(undefined);
|
|
}
|
|
}
|
|
|
|
useRecurringTimeout(fetchDownloads, 3 * 1000);
|
|
|
|
return {onceDownloads, recurringDownloads, pendingOnceDownloads, fetchDownloads}
|
|
}
|
|
|
|
export const useThrottle = () => {
|
|
const [on, setOn] = useState(null);
|
|
const {settings, fetchSettings} = React.useContext(SettingsContext);
|
|
|
|
useEffect(() => {
|
|
const status = settings['throttle_status'];
|
|
if (status === 'powersave') {
|
|
setOn(true);
|
|
} else if (status === 'ondemand') {
|
|
setOn(false);
|
|
} else {
|
|
setOn(null);
|
|
}
|
|
}, [JSON.stringify(settings)]);
|
|
|
|
const localSetThrottle = async (on) => {
|
|
setOn(null);
|
|
await setThrottle(on);
|
|
await fetchSettings();
|
|
}
|
|
|
|
return {on, setOn, setThrottle: localSetThrottle};
|
|
}
|
|
|
|
export const useDirectories = (defaultDirectory) => {
|
|
const [directories, setDirectories] = useState();
|
|
const [directory, setDirectory] = useState(defaultDirectory);
|
|
const [exists, setExists] = useState();
|
|
const [isDir, setIsDir] = useState();
|
|
const [isFile, setIsFile] = useState();
|
|
|
|
const fetchDirectories = async () => {
|
|
if (defaultDirectory && directory === '') {
|
|
setDirectory(defaultDirectory);
|
|
}
|
|
try {
|
|
const {directories, is_dir, exists_, is_file} = await getDirectories(directory);
|
|
setDirectories(directories);
|
|
setIsDir(is_dir);
|
|
setExists(exists_);
|
|
setIsFile(is_file);
|
|
} catch (e) {
|
|
toast({
|
|
type: 'error',
|
|
title: 'Unexpected server response',
|
|
description: 'Could not get directories',
|
|
time: 5000,
|
|
});
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchDirectories();
|
|
}, [directory, defaultDirectory]);
|
|
|
|
return {directory, directories, setDirectory, exists, isDir, isFile};
|
|
}
|
|
|
|
export const useSearchDirectories = (value) => {
|
|
const [directoryName, setDirectoryName] = useState(value ?? '');
|
|
const [directories, setDirectories] = useState(null);
|
|
const [channelDirectories, setChannelDirectories] = useState(null);
|
|
const [domainDirectories, setDomainDirectories] = useState(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const localSearchDirectories = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const {directories: dirs, channel_directories, domain_directories} = await searchDirectories(directoryName);
|
|
setDirectories(dirs);
|
|
setChannelDirectories(channel_directories);
|
|
setDomainDirectories(domain_directories);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
localSearchDirectories();
|
|
}, [directoryName]);
|
|
|
|
return {
|
|
directoryName,
|
|
setDirectoryName,
|
|
directories,
|
|
loading,
|
|
channelDirectories,
|
|
domainDirectories
|
|
}
|
|
}
|
|
|
|
export const useSettings = () => {
|
|
const [settings, setSettings] = useState({});
|
|
|
|
const fetchSettings = async () => {
|
|
try {
|
|
setSettings(await getSettings())
|
|
} catch (e) {
|
|
setSettings({});
|
|
toast({
|
|
type: 'error',
|
|
title: 'Unexpected server response',
|
|
description: 'Could not get directories',
|
|
time: 5000,
|
|
});
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchSettings();
|
|
}, []);
|
|
|
|
return {settings, fetchSettings};
|
|
}
|
|
|
|
export const useMediaDirectory = () => {
|
|
const {settings} = React.useContext(SettingsContext);
|
|
|
|
return settings['media_directory'];
|
|
}
|
|
|
|
export const useSettingsInterval = () => {
|
|
const {settings, fetchSettings} = useSettings();
|
|
|
|
useRecurringTimeout(fetchSettings, 1000 * 10);
|
|
|
|
return {settings, fetchSettings};
|
|
}
|
|
|
|
export const useStatus = () => {
|
|
const [status, setStatus] = useState({});
|
|
|
|
const fetchStatus = async () => {
|
|
try {
|
|
const s = await getStatus();
|
|
setStatus(s);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
return {status, fetchStatus}
|
|
}
|
|
|
|
export const useStatusInterval = () => {
|
|
const {status, fetchStatus} = useStatus();
|
|
|
|
useRecurringTimeout(fetchStatus, 1000 * 3);
|
|
|
|
return {status, fetchStatus};
|
|
}
|
|
|
|
export const StatusProvider = (props) => {
|
|
const statusValue = useStatusInterval();
|
|
const settingsValue = useSettingsInterval();
|
|
|
|
const statusMemoValue = React.useMemo(() => statusValue, [statusValue]);
|
|
const settingsMemoValue = React.useMemo(() => settingsValue, [settingsValue]);
|
|
|
|
return <StatusContext.Provider value={statusMemoValue}>
|
|
<SettingsContext.Provider value={settingsMemoValue}>
|
|
{props.children}
|
|
</SettingsContext.Provider>
|
|
</StatusContext.Provider>
|
|
}
|
|
|
|
export const useStatusFlag = (flag) => {
|
|
const {status} = useContext(StatusContext);
|
|
return status && status['flags'] && status['flags'][flag];
|
|
}
|
|
|
|
export const useCPUTemperature = () => {
|
|
const {status} = React.useContext(StatusContext);
|
|
let temperature = 0;
|
|
let highTemperature = 75;
|
|
let criticalTemperature = 85;
|
|
|
|
if (status && status['cpu_info']) {
|
|
temperature = status['cpu_info']['temperature'];
|
|
highTemperature = status['cpu_info']['high_temperature'];
|
|
criticalTemperature = status['cpu_info']['critical_temperature'];
|
|
}
|
|
|
|
return {temperature, highTemperature, criticalTemperature}
|
|
}
|
|
|
|
export const useLoad = () => {
|
|
const {status} = React.useContext(StatusContext);
|
|
let minute_1;
|
|
let minute_5;
|
|
let minute_15;
|
|
let mediumLoad = false;
|
|
let highLoad = false;
|
|
let cores;
|
|
|
|
if (status && status['load']) {
|
|
minute_1 = status['load']['minute_1'];
|
|
minute_5 = status['load']['minute_5'];
|
|
minute_15 = status['load']['minute_15'];
|
|
|
|
cores = status['cpu_info']['cores'];
|
|
const quarter = cores / 4;
|
|
if (cores && minute_1 >= (quarter * 3)) {
|
|
highLoad = true;
|
|
} else if (cores && minute_1 >= (quarter * 2)) {
|
|
mediumLoad = true;
|
|
}
|
|
}
|
|
|
|
return {minute_1, minute_5, minute_15, mediumLoad, highLoad, cores};
|
|
}
|
|
|
|
|
|
export const useVideoStatistics = () => {
|
|
const [statistics, setStatistics] = useState(null);
|
|
|
|
const fetchStatistics = async () => {
|
|
try {
|
|
let stats = await getVideosStatistics();
|
|
stats.videos.sum_duration = secondsToFullDuration(stats.videos.sum_duration);
|
|
stats.videos.sum_size = humanFileSize(stats.videos.sum_size);
|
|
stats.videos.max_size = humanFileSize(stats.videos.max_size);
|
|
stats.historical.average_size = humanFileSize(stats.historical.average_size);
|
|
setStatistics(stats);
|
|
} catch (e) {
|
|
console.error(e);
|
|
setStatistics(undefined); // Display error.
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchStatistics();
|
|
}, []);
|
|
|
|
return {statistics, fetchStatistics}
|
|
}
|
|
|
|
export const useInventory = (inventoryId) => {
|
|
const [byCategory, setByCategory] = useState();
|
|
const [bySubcategory, setBySubcategory] = useState();
|
|
const [byName, setByName] = useState();
|
|
|
|
const fetchInventory = async () => {
|
|
if (!inventoryId) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
const inventory = await getInventory(inventoryId);
|
|
setByCategory(enumerate(inventory['by_category']));
|
|
setBySubcategory(enumerate(inventory['by_subcategory']));
|
|
setByName(enumerate(inventory['by_name']));
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchInventory();
|
|
}, [inventoryId]);
|
|
|
|
return {byCategory, bySubcategory, byName, fetchInventory}
|
|
}
|
|
|
|
export const useStatistics = () => {
|
|
const [statistics, setStatistics] = useState();
|
|
|
|
const fetchFileStatistics = async () => {
|
|
try {
|
|
const s = await getStatistics();
|
|
setStatistics(s);
|
|
} catch (e) {
|
|
console.error(e);
|
|
setStatistics(undefined);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
setStatistics({});
|
|
fetchFileStatistics();
|
|
}, []);
|
|
|
|
return {statistics};
|
|
}
|
|
|
|
export const useSearchFilter = () => {
|
|
const [filter, setFilter] = useOneQuery('filter');
|
|
return {filter, setFilter}
|
|
}
|
|
|
|
export const useSearchDomain = () => {
|
|
const [domains] = useDomains();
|
|
const [domain, setDomain] = useOneQuery('domain');
|
|
return {domain, domains, setDomain}
|
|
}
|
|
|
|
export const useSearchView = () => {
|
|
const [view, setView] = useOneQuery('view');
|
|
return {view, setView}
|
|
}
|
|
|
|
export const useSearchOrder = () => {
|
|
const [sort, setSort] = useOneQuery('order');
|
|
return {sort, setSort}
|
|
}
|
|
|
|
export const useSearchDate = () => {
|
|
// Filters files using
|
|
// ?month=1&month=2&month=3
|
|
// and
|
|
// ?fromDate=2023&toDate=2024
|
|
const {searchParams, updateQuery} = React.useContext(QueryContext);
|
|
const months = searchParams.getAll('month');
|
|
let fromDate = searchParams.get('fromDate');
|
|
let toDate = searchParams.get('toDate');
|
|
fromDate = fromDate ? parseInt(fromDate) : null;
|
|
toDate = toDate ? parseInt(toDate) : null;
|
|
|
|
const clearDate = () => {
|
|
console.log('clearDate');
|
|
updateQuery({fromDate: null, toDate: null, month: null, 'o': 0});
|
|
}
|
|
|
|
const setDates = (newFromDate, newToDate, newMonths) => {
|
|
console.log('setDates', newFromDate, newToDate, newMonths);
|
|
updateQuery({fromDate: newFromDate, toDate: newToDate, month: newMonths, 'o': 0});
|
|
}
|
|
|
|
return {dateRange: [fromDate, toDate], setDates, months, clearDate}
|
|
}
|
|
|
|
export const useUploadFile = () => {
|
|
const [files, setFiles] = useState([]);
|
|
const [progresses, setProgresses] = useState({});
|
|
const [destination, setDestination] = useState('');
|
|
|
|
const handleProgress = (name, chunk, totalChunks, status, type) => {
|
|
const percent = Math.round((100 * chunk) / totalChunks);
|
|
const newProgress = {[name]: {percent, status, type}};
|
|
setProgresses(prevState => ({...prevState, ...newProgress}));
|
|
}
|
|
|
|
const handleFilesChange = (newFiles) => {
|
|
let newProgresses = {};
|
|
newFiles.map(i => {
|
|
newProgresses = {...newProgresses, [i['name']]: {percent: 0, status: 'pending'}};
|
|
});
|
|
setProgresses(newProgresses);
|
|
setFiles(newFiles);
|
|
}
|
|
|
|
const uploadChunk = async (file, chunkNum, chunkSize, totalChunks, tries, maxTries) => {
|
|
if (tries > maxTries) {
|
|
console.error(`Exceeded max tries ${maxTries}`);
|
|
}
|
|
|
|
const start = chunkNum * chunkSize;
|
|
const end = Math.min(start + chunkSize, file.size);
|
|
// The bytes that we will send.
|
|
const chunk = file.slice(start, end);
|
|
|
|
const formData = new FormData();
|
|
formData.append('chunkNumber', chunkNum.toString());
|
|
formData.append('filename', file.path);
|
|
formData.append('totalChunks', totalChunks.toString());
|
|
formData.append('destination', destination);
|
|
// Send the size that we're actually sending.
|
|
formData.append('chunkSize', chunk.size.toString());
|
|
formData.append('chunk', chunk);
|
|
|
|
console.debug(`file upload: tries=${tries} chunkNum=${chunkNum} totalChunks=${totalChunks} chunkSize=${chunk.size} destination=${destination}`);
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('POST', '/api/files/upload', true);
|
|
xhr.onreadystatechange = async () => {
|
|
if (xhr.readyState === 4) {
|
|
if (xhr.status === 200 || xhr.status === 416) {
|
|
const data = JSON.parse(xhr.responseText);
|
|
handleProgress(file.name, chunkNum, totalChunks, 'pending', file.type);
|
|
const expectedChunk = data['expected_chunk'];
|
|
if (xhr.status === 416) {
|
|
console.warn(`Server requested a different chunk ${chunkNum}`);
|
|
await uploadChunk(file, expectedChunk, chunkSize, totalChunks, tries + 1, maxTries);
|
|
} else {
|
|
console.debug(`Uploading of chunk ${chunkNum} succeeded, got request for chunk ${chunkNum}`);
|
|
// Success, reset tries.
|
|
await uploadChunk(file, expectedChunk, chunkSize, totalChunks, 0, maxTries);
|
|
}
|
|
} else if (xhr.status === 201) {
|
|
handleProgress(file.name, totalChunks, totalChunks, 'complete', file.type);
|
|
console.info(`Uploading of ${file.path} completed.`);
|
|
} else if (xhr.status === 400) {
|
|
handleProgress(file.name, totalChunks, totalChunks, 'conflicting', file.type);
|
|
const data = JSON.parse(xhr.responseText);
|
|
if (data['code'] === 'FILE_UPLOAD_FAILED') {
|
|
console.error('File already exists. Giving up.');
|
|
}
|
|
} else {
|
|
handleProgress(file.name, totalChunks, totalChunks, 'failed', file.type);
|
|
console.error(`Failed to upload chunk ${chunkNum}. Giving up.`);
|
|
}
|
|
}
|
|
}
|
|
await xhr.send(formData);
|
|
};
|
|
|
|
const doUpload = async () => {
|
|
if (!files || files.length === 0 || !destination) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
console.log(`Starting file upload`);
|
|
console.log(file);
|
|
|
|
const chunkNum = 0;
|
|
const chunkSize = 10 * 1024 * 1024; // 10MB
|
|
const totalChunks = Math.ceil(file.size / chunkSize);
|
|
const tries = 0;
|
|
const maxTries = 20;
|
|
|
|
// Start recursive function to upload the file.
|
|
await uploadChunk(file, chunkNum, chunkSize, totalChunks, tries, maxTries);
|
|
}
|
|
|
|
// Clear form after upload.
|
|
setFiles([]);
|
|
}
|
|
|
|
const doClear = () => {
|
|
setFiles([]);
|
|
setProgresses({});
|
|
}
|
|
|
|
useEffect(() => {
|
|
doUpload()
|
|
}, [JSON.stringify(files)]);
|
|
|
|
return {files, setFiles: handleFilesChange, progresses, destination, setDestination, doClear, doUpload}
|
|
}
|
|
|
|
export const useSearchZim = (searchStr, zimId, active, activeTags, defaultLimit = 10) => {
|
|
const [zim, setZim] = useState(null);
|
|
const pages = usePages(defaultLimit);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const localFetchSearch = async () => {
|
|
if (!active) {
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
const zim = await searchZim(pages.offset, pages.limit, searchStr, zimId, activeTags);
|
|
setZim(zim);
|
|
pages.setTotal(zim.estimate);
|
|
} catch (e) {
|
|
pages.setTotal(0);
|
|
console.error(`Failed to search Zim ${zimId}`);
|
|
console.error(e);
|
|
}
|
|
setLoading(false);
|
|
}
|
|
|
|
useEffect(() => {
|
|
localFetchSearch();
|
|
}, [active, searchStr, JSON.stringify(activeTags), pages.effect]);
|
|
|
|
return {zim, fetchSearch: localFetchSearch, pages, loading}
|
|
}
|
|
|
|
export const useOutdatedZims = () => {
|
|
const [outdated, setOutdated] = useState(null);
|
|
const [current, setCurrent] = useState(null);
|
|
|
|
const localFetchOutdatedZims = async () => {
|
|
const zims = await getOutdatedZims();
|
|
setOutdated(zims['outdated']);
|
|
setCurrent(zims['current']);
|
|
}
|
|
|
|
useEffect(() => {
|
|
localFetchOutdatedZims();
|
|
}, []);
|
|
|
|
return {outdated, current}
|
|
}
|
|
|
|
export const useVINDecoder = (defaultVINNumber = '') => {
|
|
const [value, setValue] = useState(defaultVINNumber);
|
|
// The response vin from the API.
|
|
const [vin, setVin] = useState(null);
|
|
|
|
const localFetchDecoded = async () => {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const vin = await fetchDecoded(value);
|
|
setVin(vin);
|
|
} catch (e) {
|
|
console.error('Failed to decode VIN number');
|
|
setVin(null);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
localFetchDecoded();
|
|
}, [value]);
|
|
|
|
return {value, setValue, vin}
|
|
}
|
|
|
|
export const useWROLMode = () => {
|
|
// Returns the current boolean WROL Mode, during fetch this returns null.
|
|
const {status} = useContext(StatusContext);
|
|
return status ? status.wrol_mode : null;
|
|
}
|
|
|
|
export const useDockerized = () => {
|
|
const {status} = React.useContext(StatusContext);
|
|
return status && status['dockerized'] === true;
|
|
}
|