Rewrite web front-end using React

This is a big commit, some font-end function are still working,
including manage, error handle, as others.
This commit is contained in:
2021-11-21 17:30:43 +08:00
parent d556bbe0c8
commit e170c8b842
38 changed files with 38580 additions and 19880 deletions

View File

@@ -0,0 +1,154 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router";
import { CalcReadableFilesizeDetail } from "./Common";
import FfmpegConfig from "./FfmpegConfig";
import FileDialog from "./FileDialog";
function AudioPlayer(props) {
// props.playingFile
// props.setPlayingFile
const [fileDialogShowStatus, setFileDialogShowStatus] = useState(false);
const [loop, setLoop] = useState(true);
const [raw, setRaw] = useState(false);
const [prepare, setPrepare] = useState(false);
const [selectedFfmpegConfig, setSelectedFfmpegConfig] = useState({});
const [playingURL, setPlayingURL] = useState("");
const [isPreparing, setIsPreparing] = useState(false);
const [preparedFilesize, setPreparedFilesize] = useState(null);
useEffect(() => {
// no playing file
if (props.playingFile.id === undefined) {
setPlayingURL("");
return;
}
if (raw) {
console.log("Play raw file");
setPlayingURL("/api/v1/get_file_direct?id=" + props.playingFile.id);
} else {
if (prepare) {
// prepare file
setIsPreparing(true);
fetch("/api/v1/prepare_file_stream_direct", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: props.playingFile.id,
config_name: selectedFfmpegConfig.name,
}),
})
.then((response) => response.json())
.then((data) => {
setPreparedFilesize(data.filesize);
setIsPreparing(false);
setPlayingURL(
`/api/v1/get_file_stream_direct?id=${props.playingFile.id}&config=${selectedFfmpegConfig.name}`
);
});
} else {
setPlayingURL(
`/api/v1/get_file_stream?id=${props.playingFile.id}&config=${selectedFfmpegConfig.name}`
);
}
}
}, [props.playingFile.id, raw, prepare, selectedFfmpegConfig]);
let navigate = useNavigate();
return (
<div>
<h5>Player status</h5>
{props.playingFile.id && (
<span>
<FileDialog
showStatus={fileDialogShowStatus}
setShowStatus={setFileDialogShowStatus}
file={props.playingFile}
setPlayingFile={() => {
return;
}}
/>
<button
onClick={() => {
setFileDialogShowStatus(!fileDialogShowStatus);
}}
>
{props.playingFile.filename}
</button>
<button
onClick={() =>
navigate(`search-folders/${props.playingFile.folder_id}`)
}
>
{props.playingFile.foldername}
</button>
<button disabled>
{prepare
? CalcReadableFilesizeDetail(preparedFilesize)
: CalcReadableFilesizeDetail(props.playingFile.filesize)}
</button>
{isPreparing && <button disabled>Preparing...</button>}
{playingURL !== "" && (
<button
onClick={() => {
props.setPlayingFile({});
}}
>
Stop
</button>
)}
</span>
)}
<br />
<input
checked={loop}
onChange={(event) => setLoop(event.target.checked)}
type="checkbox"
/>
<label>Loop</label>
<input
checked={raw}
onChange={(event) => setRaw(event.target.checked)}
type="checkbox"
/>
<label>Raw</label>
{!raw && (
<span>
<input
checked={prepare}
onChange={(event) => setPrepare(event.target.checked)}
type="checkbox"
/>
<label>Prepare</label>
</span>
)}
{playingURL !== "" && (
<audio
controls
autoPlay
loop={loop}
className="audio-player"
src={playingURL}
></audio>
)}
<FfmpegConfig
selectedFfmpegConfig={selectedFfmpegConfig}
setSelectedFfmpegConfig={setSelectedFfmpegConfig}
/>
</div>
);
}
export default AudioPlayer;

View File

@@ -0,0 +1,40 @@
export function CalcReadableFilesize(filesize) {
if (filesize < 1024) {
return filesize;
}
if (filesize < 1024 * 1024) {
return Math.round(filesize / 1024) + "K";
}
if (filesize < 1024 * 1024 * 1024) {
return Math.round(filesize / 1024 / 1024) + "M";
}
if (filesize < 1024 * 1024 * 1024 * 1024) {
return Math.round(filesize / 1024 / 1024 / 1024) + "G";
}
}
export function CalcReadableFilesizeDetail(filesize) {
if (filesize < 1024 * 1024) {
return filesize;
}
if (filesize < 1024 * 1024 * 1024) {
return numberWithCommas(Math.round(filesize / 1024)) + "K";
}
if (filesize < 1024 * 1024 * 1024 * 1024) {
return numberWithCommas(Math.round(filesize / 1024 / 1024)) + "M";
}
if (filesize < 1024 * 1024 * 1024 * 1024 * 1024) {
return numberWithCommas(Math.round(filesize / 1024 / 1024 / 1024)) + "G";
}
}
function numberWithCommas(x) {
x = x.toString();
var pattern = /(-?\d+)(\d{3})/;
while (pattern.test(x)) x = x.replace(pattern, "$1,$2");
return x;
}
export function SayHello() {
return "Hello";
}

View File

@@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
function FfmpegConfig(props) {
// props.setSelectedFfmpegConfig
// props.selectedFfmpegConfig
const [ffmpegConfigList, setFfmpegConfigList] = useState([]);
useEffect(() => {
fetch("/api/v1/get_ffmpeg_config_list")
.then((response) => response.json())
.then((data) => {
setFfmpegConfigList(data.ffmpeg_config_list);
if (data.ffmpeg_config_list.length > 0) {
props.setSelectedFfmpegConfig(data.ffmpeg_config_list[0]);
}
});
}, []);
return (
<div className="ffmpeg-config">
<select
onChange={(event) => {
props.setSelectedFfmpegConfig(
ffmpegConfigList[event.target.selectedIndex]
);
}}
>
{ffmpegConfigList.map((ffmpegConfig) => (
<option key={ffmpegConfig.name}>{ffmpegConfig.name}</option>
))}
</select>
<span>{props.selectedFfmpegConfig.args}</span>
</div>
);
}
export default FfmpegConfig;

View File

@@ -0,0 +1,44 @@
import { useNavigate } from "react-router";
function FileDialog(props) {
// props.showStatus
// props.setShowStatus
// props.playingFile
// props.setPlayingFile
// props.file
let navigate = useNavigate();
if (!props.showStatus) {
return null;
}
return (
<dialog open>
<p>{props.file.filename}</p>
<FileDialog file={props.file} />
<p>
Download 使用 Axios 异步下载
<br />
Play 调用网页播放器播放
<br />
</p>
<button>Download</button>
<button
onClick={() => {
props.setPlayingFile(props.file);
props.setShowStatus(false);
}}
>
Play
</button>
<button onClick={() => {
navigate(`/share/${props.file.id}`)
props.setShowStatus(false);
}}>Share</button>
<button onClick={() => props.setShowStatus(false)}>Close</button>
</dialog>
);
}
export default FileDialog;

View File

@@ -0,0 +1,45 @@
import { useState } from "react";
import { useNavigate } from "react-router";
import { CalcReadableFilesize } from "./Common";
import FileDialog from "./FileDialog";
function FileEntry(props) {
const [showStatus, setShowStatus] = useState(false);
let navigate = useNavigate();
return (
<tr>
<td
className="clickable"
onClick={() => {
// double click to play file and close dialog
if (showStatus) {
props.setPlayingFile(props.file);
setShowStatus(false);
return;
}
setShowStatus(true);
}}
>
{props.file.filename}
</td>
<td
className="clickable"
onClick={() => navigate(`/search-folders/${props.file.folder_id}`)}
>
{props.file.foldername}
</td>
<td>
{CalcReadableFilesize(props.file.filesize)}
<FileDialog
setPlayingFile={props.setPlayingFile}
showStatus={showStatus}
setShowStatus={setShowStatus}
file={props.file}
/>
</td>
</tr>
);
}
export default FileEntry;

View File

@@ -0,0 +1,26 @@
import FileEntry from "./FileEntry";
function FilesTable(props) {
return (
<table>
<thead>
<tr>
<th>Filename</th>
<th>Folder Name</th>
<th>Size</th>
</tr>
</thead>
<tbody>
{props.files.map((file) => (
<FileEntry
setPlayingFile={props.setPlayingFile}
key={file.id}
file={file}
/>
))}
</tbody>
</table>
);
}
export default FilesTable;

View File

@@ -0,0 +1,32 @@
import { useNavigate } from "react-router";
function FoldersTable(props) {
let navigate = useNavigate();
return (
<table>
<thead>
<tr>
<th>Folder name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{props.folders.map((folder) => (
<tr key={folder.id}>
<td
onClick={() => navigate(`/search-folders/${folder.id}`)}
className="clickable"
>
{folder.foldername}
</td>
<td onClick={() => navigate(`/search-folders/${folder.id}`)}>
<button>View</button>
</td>
</tr>
))}
</tbody>
</table>
);
}
export default FoldersTable;

View File

@@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
import FilesTable from "./FilesTable";
function GetRandomFiles(props) {
const [files, setFiles] = useState([]);
const [isLoading, setIsLoading] = useState(false);
function refresh(setFiles) {
setIsLoading(true);
fetch("/api/v1/get_random_files")
.then((response) => response.json())
.then((data) => {
setIsLoading(false);
setFiles(data.files);
});
}
useEffect(() => {
refresh(setFiles);
}, []);
return (
<div className="page">
<div className="search_toolbar">
<button className="refresh" onClick={() => refresh(setFiles)}>
{isLoading ? "Loading..." : "Refresh"}
</button>
</div>
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
</div>
);
}
export default GetRandomFiles;

View File

@@ -0,0 +1,9 @@
function Manage() {
return (
<div>
<h2>Manage</h2>
</div>
)
}
export default Manage;

View File

@@ -0,0 +1,93 @@
import { useState, useEffect } from "react";
import FilesTable from "./FilesTable";
function SearchFiles(props) {
const [files, setFiles] = useState([]);
const [filename, setFilename] = useState("");
const [offset, setOffset] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const limit = 10;
function searchFiles() {
if (
filename === "" &&
(props.folder === undefined || props.folder.id === undefined)
) {
return;
}
const folder = props.folder ? props.folder : {};
const url = folder.id
? "/api/v1/get_files_in_folder"
: "/api/v1/search_files";
setIsLoading(true);
fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filename: filename,
limit: limit,
offset: offset,
folder_id: folder.id,
}),
})
.then((response) => response.json())
.then((data) => {
setIsLoading(false);
const files = data.files ? data.files : [];
setFiles(files);
});
}
function nextPage() {
setOffset(offset + limit);
}
function lastPage() {
const offsetValue = offset - limit;
if (offsetValue < 0) {
return;
}
setOffset(offsetValue);
}
useEffect(() => searchFiles(), [offset, props.folder]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div className="page">
<h3>Search Files</h3>
<div className="search_toolbar">
{!props.folder && (
<input
onChange={(event) => setFilename(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
searchFiles();
}
}}
type="text"
placeholder="Enter filename"
/>
)}
<button
disabled={!!props.folder}
onClick={() => {
searchFiles();
}}
>
{isLoading ? "Loading..." : "Search"}
</button>
{props.folder && props.folder.foldername && (
<button onClick={searchFiles}>{props.folder.foldername}</button>
)}
<button onClick={lastPage}>Last page</button>
<button disabled>
{offset} - {offset + files.length}
</button>
<button onClick={nextPage}>Next page</button>
</div>
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
</div>
);
}
export default SearchFiles;

View File

@@ -0,0 +1,94 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router";
import FoldersTable from "./FoldersTable";
import SearchFiles from "./SearchFiles";
function SearchFolders(props) {
const [foldername, setFoldername] = useState("");
const [folders, setFolders] = useState([]);
const [folder, setFolder] = useState({});
const [offset, setOffset] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const limit = 10;
function searchFolder() {
if (foldername === "") {
return;
}
setIsLoading(true);
fetch("/api/v1/search_folders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
foldername: foldername,
limit: limit,
offset: offset,
}),
})
.then((response) => response.json())
.then((data) => {
setIsLoading(false);
let folders;
if (data.folders) {
folders = data.folders;
} else {
folders = [];
}
setFolders(folders);
});
}
function nextPage() {
setOffset(offset + limit);
}
function lastPage() {
const offsetValue = offset - limit;
if (offsetValue < 0) {
return;
}
setOffset(offsetValue);
}
function viewFolder(folder) {
setFolder(folder);
}
let params = useParams();
useEffect(() => searchFolder(), [offset]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (params.id !== undefined) {
setFolder({ id: parseInt(params.id) });
}
}, [params.id]);
return (
<div className="page">
<h3>Search Folders</h3>
<div className="search_toolbar">
<input
onChange={(event) => setFoldername(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
searchFolder();
}
}}
type="text"
placeholder="Enter folder name"
/>
<button onClick={searchFolder}>
{isLoading ? "Loading..." : "Search"}
</button>
<button onClick={lastPage}>Last page</button>
<button disabled>
{offset} - {offset + limit}
</button>
<button onClick={nextPage}>Next page</button>
</div>
<FoldersTable viewFolder={viewFolder} folders={folders} />
<SearchFiles setPlayingFile={props.setPlayingFile} folder={folder} />
</div>
);
}
export default SearchFolders;

View File

@@ -0,0 +1,32 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router";
import FilesTable from "./FilesTable";
function Share(props) {
let params = useParams();
const [file, setFile] = useState([]);
useEffect(() => {
fetch("/api/v1/get_file_info", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: parseInt(params.id),
}),
})
.then((response) => response.json())
.then((data) => {
setFile([data]);
});
}, [params]);
return (
<div className="page">
<h3>Share with others! {params.id}</h3>
<p>
Share link: <a href={window.location.href}>{window.location.href}</a>
</p>
<FilesTable setPlayingFile={props.setPlayingFile} files={file} />
</div>
);
}
export default Share;