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:
94
web/src/App.css
Normal file
94
web/src/App.css
Normal file
@@ -0,0 +1,94 @@
|
||||
html {
|
||||
font-size: 1em;
|
||||
}
|
||||
body {
|
||||
margin: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.base {
|
||||
display: grid;
|
||||
grid-row-gap: 1em;
|
||||
}
|
||||
.header {
|
||||
color: white;
|
||||
background-color: rgb(63, 81, 181);
|
||||
box-shadow: 0 0 8px #393939;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
.title {
|
||||
margin-left: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.title-text {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
.logo {
|
||||
width: 39px;
|
||||
height: 39px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.nav-link {
|
||||
color: rgb(229, 232, 245);
|
||||
padding: 1em;
|
||||
}
|
||||
a.unset {
|
||||
color: unset;
|
||||
text-decoration: unset;
|
||||
}
|
||||
a.active {
|
||||
color: deeppink;
|
||||
background-color: lightgray;
|
||||
border-radius: 0.39em 0.39em 0 0;
|
||||
}
|
||||
.audio-player {
|
||||
height: 39px;
|
||||
width: 100%;
|
||||
}
|
||||
td.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
div.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
div.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
div.search_toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
div.feedback {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
button.refresh {
|
||||
width: 100%;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
padding-bottom: 0.5em;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
dialog {
|
||||
border: solid;
|
||||
}
|
||||
.player-options {
|
||||
display: flex;
|
||||
}
|
||||
.ffmpeg-config {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
79
web/src/App.js
Normal file
79
web/src/App.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
HashRouter as Router,
|
||||
Routes,
|
||||
Route,
|
||||
NavLink,
|
||||
} from "react-router-dom";
|
||||
import "./App.css";
|
||||
|
||||
import GetRandomFiles from "./component/GetRandomFiles";
|
||||
import SearchFiles from "./component/SearchFiles";
|
||||
import SearchFolders from "./component/SearchFolders";
|
||||
import Manage from "./component/Manage";
|
||||
import Share from "./component/Share";
|
||||
import AudioPlayer from "./component/AudioPlayer";
|
||||
import { useState } from "react";
|
||||
|
||||
function App() {
|
||||
const [playingFile, setPlayingFile] = useState({});
|
||||
return (
|
||||
<div className="base">
|
||||
<Router>
|
||||
<header className="header">
|
||||
<h3 className="title">
|
||||
<img src="favicon.png" alt="logo" className="logo" />
|
||||
<span className="title-text">MSW Open Music Project</span>
|
||||
</h3>
|
||||
<nav className="nav">
|
||||
<NavLink to="/" className="nav-link">
|
||||
Feeling luckly
|
||||
</NavLink>
|
||||
<NavLink to="/search-files" className="nav-link">
|
||||
Files
|
||||
</NavLink>
|
||||
<NavLink to="/search-folders" className="nav-link">
|
||||
Folders
|
||||
</NavLink>
|
||||
<NavLink to="/manage" className="nav-link">
|
||||
Manage
|
||||
</NavLink>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
index
|
||||
path="/"
|
||||
element={<GetRandomFiles setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/search-files"
|
||||
element={<SearchFiles setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/search-folders"
|
||||
element={<SearchFolders setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/search-folders/:id"
|
||||
element={<SearchFolders setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route path="/manage" element={<Manage />} />
|
||||
<Route
|
||||
path="/share/:id"
|
||||
element={<Share setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
<footer>
|
||||
<AudioPlayer
|
||||
playingFile={playingFile}
|
||||
setPlayingFile={setPlayingFile}
|
||||
/>
|
||||
</footer>
|
||||
</Router>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
8
web/src/App.test.js
Normal file
8
web/src/App.test.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
154
web/src/component/AudioPlayer.js
Normal file
154
web/src/component/AudioPlayer.js
Normal 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;
|
||||
40
web/src/component/Common.js
Normal file
40
web/src/component/Common.js
Normal 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";
|
||||
}
|
||||
38
web/src/component/FfmpegConfig.js
Normal file
38
web/src/component/FfmpegConfig.js
Normal 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;
|
||||
44
web/src/component/FileDialog.js
Normal file
44
web/src/component/FileDialog.js
Normal 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;
|
||||
45
web/src/component/FileEntry.js
Normal file
45
web/src/component/FileEntry.js
Normal 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;
|
||||
26
web/src/component/FilesTable.js
Normal file
26
web/src/component/FilesTable.js
Normal 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;
|
||||
32
web/src/component/FoldersTable.js
Normal file
32
web/src/component/FoldersTable.js
Normal 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;
|
||||
33
web/src/component/GetRandomFiles.js
Normal file
33
web/src/component/GetRandomFiles.js
Normal 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;
|
||||
9
web/src/component/Manage.js
Normal file
9
web/src/component/Manage.js
Normal file
@@ -0,0 +1,9 @@
|
||||
function Manage() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Manage</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Manage;
|
||||
93
web/src/component/SearchFiles.js
Normal file
93
web/src/component/SearchFiles.js
Normal 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;
|
||||
94
web/src/component/SearchFolders.js
Normal file
94
web/src/component/SearchFolders.js
Normal 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;
|
||||
32
web/src/component/Share.js
Normal file
32
web/src/component/Share.js
Normal 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;
|
||||
13
web/src/index.css
Normal file
13
web/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
18
web/src/index.js
Normal file
18
web/src/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import 'water.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
13
web/src/reportWebVitals.js
Normal file
13
web/src/reportWebVitals.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
5
web/src/setupTests.js
Normal file
5
web/src/setupTests.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
Reference in New Issue
Block a user