18 Commits

Author SHA1 Message Date
61391ff2a5 backend support record playback history 2023-01-26 21:21:15 +08:00
3b90ad56d6 Update github CI node version to 18 2022-12-04 20:21:03 +08:00
ec0dad00ef Replace react with preact
reduce js file bundle to 20%
2022-12-04 20:17:59 +08:00
c09e230972 Replace webpack with only esbuild
reduce node_modules size to only 18M
2022-12-04 20:17:56 +08:00
b808d4be99 wrap walk sql into transaction improve performance 2022-11-04 15:55:13 +08:00
5c3fb66db3 change /v1/get_file use URL params 2022-11-03 08:18:07 +08:00
df081d39ca Update npm dependencies 2022-11-03 01:33:54 +08:00
2f2254371b web show folderPath in folder page 2022-11-03 01:21:58 +08:00
857a5e9dd9 support /v1/get_files_in_folder return folder path 2022-11-03 01:21:25 +08:00
2b4bbdf25e web support api /v1/get_file_ffprobe_info 2022-11-03 00:58:40 +08:00
08a5650b30 add api /v1/get_file_ffprobe_info 2022-11-03 00:58:33 +08:00
8a9569ea61 fix: web mediaSession react hook dependencies 2022-09-29 10:41:10 +08:00
dc380590e7 Revert "Revert "add basic support for mediaSession""
This reverts commit e5fa4c2b65.
2022-09-29 10:40:56 +08:00
e5fa4c2b65 Revert "add basic support for mediaSession"
This reverts commit edd5eeb4c0.
It break prepare mode on Android devices.
2022-09-29 10:28:34 +08:00
97693d6bd0 Change getRandomFiles function to async 2022-09-22 15:27:41 +08:00
edd5eeb4c0 add basic support for mediaSession 2022-09-21 16:15:51 +08:00
adf0c24f91 always render AudioPlayer DOM 2022-09-21 15:12:12 +08:00
539fbb6501 add webm as default index file type 2022-09-21 15:08:58 +08:00
45 changed files with 922 additions and 37588 deletions

View File

@@ -57,7 +57,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Build web front end
run: |
make web

View File

@@ -57,6 +57,7 @@ func NewAPI(config commonconfig.Config) (*API, error) {
apiMux.HandleFunc("/get_file_stream", api.HandleGetFileStream)
apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs)
apiMux.HandleFunc("/get_file_info", api.HandleGetFileInfo)
apiMux.HandleFunc("/get_file_ffprobe_info", api.HandleGetFileFfprobeInfo)
apiMux.HandleFunc("/get_file_stream_direct", api.HandleGetFileStreamDirect)
apiMux.HandleFunc("/prepare_file_stream_direct", api.HandlePrepareFileStreamDirect)
apiMux.HandleFunc("/delete_file", api.HandleDeleteFile)
@@ -70,7 +71,7 @@ func NewAPI(config commonconfig.Config) (*API, error) {
// user
apiMux.HandleFunc("/login", api.HandleLogin)
apiMux.HandleFunc("/register", api.HandleRegister)
apiMux.HandleFunc("/logout", api.LoginAsAnonymous)
apiMux.HandleFunc("/logout", api.HandleLoginAsAnonymous)
apiMux.HandleFunc("/get_user_info", api.HandleGetUserInfo)
apiMux.HandleFunc("/get_users", api.HandleGetUsers)
apiMux.HandleFunc("/update_user_active", api.HandleUpdateUserActive)
@@ -94,6 +95,8 @@ func NewAPI(config commonconfig.Config) (*API, error) {
apiMux.HandleFunc("/update_review", api.HandleUpdateReview)
apiMux.HandleFunc("/delete_review", api.HandleDeleteReview)
apiMux.HandleFunc("/get_reviews_by_user", api.HandleGetReviewsByUser)
// statistic
apiMux.HandleFunc("/record_playback", api.HandleRecordPlayback)
// database
apiMux.HandleFunc("/walk", api.HandleWalk)
apiMux.HandleFunc("/reset", api.HandleReset)

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"strconv"
)
@@ -46,10 +47,8 @@ func (api *API) HandleGetFileInfo(w http.ResponseWriter, r *http.Request) {
}
}
// /get_file
// get raw file with io.Copy method
func (api *API) HandleGetFile(w http.ResponseWriter, r *http.Request) {
getFileRequest := &GetFileRequest{
func (api *API) HandleGetFileFfprobeInfo(w http.ResponseWriter, r *http.Request) {
getFileRequest := &GetFileRequest {
ID: -1,
}
@@ -65,12 +64,52 @@ func (api *API) HandleGetFile(w http.ResponseWriter, r *http.Request) {
return
}
log.Println("[api] Get file Ffprobe info", getFileRequest.ID)
file, err := api.Db.GetFile(getFileRequest.ID)
if err != nil {
api.HandleError(w, r, err)
return
}
path, err := file.Path()
if err != nil {
api.HandleError(w, r, err)
return
}
cmd := exec.Command("ffprobe", "-i", path, "-hide_banner")
cmd.Stderr = w
err = cmd.Run()
if err != nil {
api.HandleError(w, r, err)
return
}
}
// /api/v1/get_file?id=123
// get raw file with io.Copy method
func (api *API) HandleGetFile(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
ids := q["id"]
_id, err := strconv.Atoi(ids[0])
if err != nil {
api.HandleError(w, r, err)
return
}
id := int64(_id)
// check empty
if id < 0 {
api.HandleErrorString(w, r, `"id" can't be none or negative`)
return
}
file, err := api.Db.GetFile(id)
if err != nil {
api.HandleError(w, r, err)
return
}
path, err := file.Path()
if err != nil {
api.HandleError(w, r, err)

View File

@@ -14,7 +14,8 @@ type GetFilesInFolderRequest struct {
}
type GetFilesInFolderResponse struct {
Files *[]database.File `json:"files"`
Files *[]database.File `json:"files"`
Folder string `json:"folder"`
}
func (api *API) HandleGetFilesInFolder(w http.ResponseWriter, r *http.Request) {
@@ -34,14 +35,15 @@ func (api *API) HandleGetFilesInFolder(w http.ResponseWriter, r *http.Request) {
return
}
files, err := api.Db.GetFilesInFolder(getFilesInFolderRequest.Folder_id, getFilesInFolderRequest.Limit, getFilesInFolderRequest.Offset)
files, folder, err := api.Db.GetFilesInFolder(getFilesInFolderRequest.Folder_id, getFilesInFolderRequest.Limit, getFilesInFolderRequest.Offset)
if err != nil {
api.HandleError(w, r, err)
return
}
getFilesInFolderResponse := &GetFilesInFolderResponse{
Files: &files,
Files: &files,
Folder: folder,
}
log.Println("[api] Get files in folder", getFilesInFolderRequest.Folder_id)

44
pkg/api/handle_stat.go Normal file
View File

@@ -0,0 +1,44 @@
package api
import (
"encoding/json"
"msw-open-music/pkg/database"
"net/http"
"time"
)
type RecordPlaybackRequest struct {
Playback database.Playback `json:"playback"`
}
func (api *API) HandleRecordPlayback(w http.ResponseWriter, r *http.Request) {
recordPlaybackRequest := &RecordPlaybackRequest{}
err := json.NewDecoder(r.Body).Decode(recordPlaybackRequest)
if err != nil {
api.HandleError(w, r, err)
return
}
recordPlaybackRequest.Playback.Time = time.Now()
recordPlaybackRequest.Playback.UserID, err = api.GetUserID(w, r)
if err != nil {
if err == ErrNotLoggedIn {
user, err := api.Db.LoginAsAnonymous()
if err != nil {
api.HandleError(w, r, err)
return
}
recordPlaybackRequest.Playback.UserID = user.ID
} else {
api.HandleError(w, r, err)
return
}
}
err = api.Db.RecordPlayback(recordPlaybackRequest.Playback)
if err != nil {
api.HandleError(w, r, err)
return
}
api.HandleOK(w, r)
}

View File

@@ -17,23 +17,8 @@ type LoginResponse struct {
User *database.User `json:"user"`
}
func (api *API) LoginAsAnonymous(w http.ResponseWriter, r *http.Request) {
user, err := api.Db.LoginAsAnonymous()
if err != nil {
api.HandleError(w, r, err)
return
}
session, _ := api.store.Get(r, api.defaultSessionName)
// save session
session.Values["userId"] = user.ID
err = session.Save(r, w)
if err != nil {
api.HandleError(w, r, err)
return
}
func (api *API) HandleLoginAsAnonymous(w http.ResponseWriter, r *http.Request) {
user, err := api.LoginAsAnonymous(w, r)
resp := &LoginResponse{
User: user,
}
@@ -45,6 +30,25 @@ func (api *API) LoginAsAnonymous(w http.ResponseWriter, r *http.Request) {
}
}
func (api *API) LoginAsAnonymous(w http.ResponseWriter, r *http.Request) (*database.User, error) {
user, err := api.Db.LoginAsAnonymous()
if err != nil {
return nil, err
}
session, _ := api.store.Get(r, api.defaultSessionName)
// save session
session.Values["userId"] = user.ID
err = session.Save(r, w)
if err != nil {
return nil, err
}
// return user
return user, nil
}
func (api *API) HandleLogin(w http.ResponseWriter, r *http.Request) {
var user *database.User
var err error

View File

@@ -53,28 +53,29 @@ func (database *Database) GetRandomFilesWithTag(tagID, limit int64) ([]File, err
return files, nil
}
func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset int64) ([]File, error) {
func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset int64) ([]File, string, error) {
database.singleThreadLock.Lock()
defer database.singleThreadLock.Unlock()
rows, err := database.stmt.getFilesInFolder.Query(folder_id, limit, offset)
if err != nil {
return nil, err
return nil, "", err
}
defer rows.Close()
files := make([]File, 0)
folder := ""
for rows.Next() {
file := File{
Db: database,
Folder_id: folder_id,
}
err = rows.Scan(&file.ID, &file.Filename, &file.Filesize, &file.Foldername)
err = rows.Scan(&file.ID, &file.Filename, &file.Filesize, &file.Foldername, &folder)
if err != nil {
return nil, err
return nil, "", err
}
files = append(files, file)
}
return files, nil
return files, folder, nil
}
func (database *Database) SearchFolders(foldername string, limit int64, offset int64) ([]Folder, error) {
@@ -159,7 +160,16 @@ func (database *Database) Walk(root string, pattern []string, tagIDs []int64, us
tags = append(tags, tag)
}
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
tx, err := database.sqlConn.Begin()
if err != nil {
log.Fatal(err)
}
insertFolderStmt := tx.Stmt(database.stmt.insertFolder)
insertFileStmt := tx.Stmt(database.stmt.insertFile)
putTagOnFileStmt := tx.Stmt(database.stmt.putTagOnFile)
findFolderStmt := tx.Stmt(database.stmt.findFolder)
err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
@@ -174,20 +184,47 @@ func (database *Database) Walk(root string, pattern []string, tagIDs []int64, us
return nil
}
// insert file, folder will aut created
fileID, err := database.Insert(path, info.Size())
// insert file and folder
// fileID, err := database.Insert(path, info.Size())
// if err != nil {
// return err
// }
var folderID int64
folder, filename := filepath.Split(path)
err = findFolderStmt.QueryRow(folder).Scan(&folderID)
if err != nil {
result, err := insertFolderStmt.Exec(folder, filepath.Base(folder))
if err != nil {
return err
}
folderID, err = result.LastInsertId()
if err != nil {
return err
}
}
result, err := insertFileStmt.Exec(folderID, filename, filename, info.Size())
if err != nil {
return err
}
fileID, err := result.LastInsertId()
if err != nil {
return err
}
for _, tag := range tags {
err = database.PutTagOnFile(tag.ID, fileID, userID)
_, err := putTagOnFileStmt.Exec(tag.ID, fileID, userID)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
return tx.Commit()
}
func (database *Database) GetFolder(folderId int64) (*Folder, error) {

View File

@@ -0,0 +1,7 @@
package database
func (database *Database) RecordPlayback(playback Playback) error {
_, err := database.stmt.recordPlaybackStmt.Exec(
playback.UserID, playback.FileID, playback.Time, playback.Method, playback.Duration)
return err
}

View File

@@ -2,6 +2,7 @@ package database
import (
"database/sql"
"log"
)
var initFilesTableQuery = `CREATE TABLE IF NOT EXISTS files (
@@ -87,7 +88,8 @@ var initPlaybacksTableQuery = `CREATE TABLE IF NOT EXISTS playbacks (
user_id INTEGER NOT NULL,
file_id INTEGER NOT NULL,
time INTEGER NOT NULL,
mothod INTEGER NOT NULL,
method INTEGER NOT NULL,
duration INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (file_id) REFERENCES files(id)
);`
@@ -150,7 +152,7 @@ ORDER BY foldername
LIMIT ? OFFSET ?;`
var getFilesInFolderQuery = `SELECT
files.id, files.filename, files.filesize, folders.foldername
files.id, files.filename, files.filesize, folders.foldername, folders.folder
FROM files
JOIN folders ON files.folder_id = folders.id
WHERE folder_id = ?
@@ -285,6 +287,8 @@ var updateFilenameQuery = `UPDATE files SET filename = ? WHERE id = ?;`
var resetFilenameQuery = `UPDATE files SET filename = realname WHERE id = ?;`
var recordPlaybackQuery = `INSERT INTO playbacks (user_id, file_id, time, method, duration) VALUES ($1, $2, $3, $4, $5);`
type Stmt struct {
initFilesTable *sql.Stmt
initFoldersTable *sql.Stmt
@@ -345,6 +349,7 @@ type Stmt struct {
deleteFileReferenceInReviews *sql.Stmt
updateFilename *sql.Stmt
resetFilename *sql.Stmt
recordPlaybackStmt *sql.Stmt
}
func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
@@ -772,5 +777,12 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
return nil, err
}
stmt.recordPlaybackStmt, err = sqlConn.Prepare(recordPlaybackQuery)
if err != nil {
return nil, err
}
log.Println("Init statements finished")
return stmt, err
}

View File

@@ -2,6 +2,7 @@ package database
import (
"path/filepath"
"time"
)
type File struct {
@@ -17,7 +18,7 @@ type File struct {
type Folder struct {
Db *Database `json:"-"`
ID int64 `json:"id"`
Folder string `json:"-"`
Folder string `json:"folder"`
Foldername string `json:"foldername"`
}
@@ -58,6 +59,15 @@ type Feedback struct {
Time int64 `json:"time"`
}
type Playback struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
FileID int64 `json:"file_id"`
Time time.Time `json:"time"`
Method int64 `json:"method"`
Duration time.Duration `json:"Duration"`
}
var (
RoleAnonymous = int64(0)
RoleAdmin = int64(1)

View File

@@ -1,70 +1,9 @@
# Getting Started with Create React App
# MSW Open Music Web Frontend
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
This is a React single page application. And use Preact instead of React to achieve a smaller file size.
## Available Scripts
`node_modules` only has 19M. We uses esbuild and shell scripts and build only takes a milliseconds!
In the project directory, you can run:
## How to build
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
Simple run `./build.sh`, then all output files are under `./build/` directory.

6
web/build.sh Executable file
View File

@@ -0,0 +1,6 @@
rm -rf build
cp -raf public build
./node_modules/.bin/esbuild src/index.jsx --bundle --outfile=build/msw-open-music.js --alias:react=preact/compat --alias:react-dom=preact/compat --minify --analyze
cat public/index.html | sed "s/%PUBLIC_URL%/$PUBLIC_URL/" > build/index.html
echo "Build done, output files under ./build directory"

37937
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,42 +3,12 @@
"version": "1.2.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.15.0",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-scripts": "^4.0.3",
"water.css": "^2.1.1",
"web-vitals": "^1.1.2"
"@preact/compat": "^17.1.2",
"esbuild": "^0.15.17",
"react-router-dom": "^6.4.4",
"water.css": "^2.1.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react": "^17.0.34"
"build": "bash ./build.sh"
}
}

View File

@@ -6,36 +6,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Personal music streaming platform" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<!-- Add to homescreen for Chrome on Android -->
<link rel="stylesheet" href="%PUBLIC_URL%/msw-open-music.css" />
<meta name="mobile-web-app-capable" content="yes" />
<title>MSW Open Music</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script type="module" src="%PUBLIC_URL%/msw-open-music.js"></script>
</body>
</html>

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { HashRouter as Router, Routes, Route, NavLink } from "react-router-dom";
import "./App.css";

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router";
import { CalcReadableFilesizeDetail } from "./Common";
import * as React from 'react';
import {useEffect, useState} from "react";
import {useNavigate} from "react-router";
import {CalcReadableFilesizeDetail} from "./Common";
import FfmpegConfig from "./FfmpegConfig";
import FileDialog from "./FileDialog";
import { Tr } from "../translate";
@@ -22,7 +23,20 @@ function AudioPlayer(props) {
const [timerCount, setTimerCount] = useState(0);
const [timerID, setTimerID] = useState(null);
// init mediaSession API
useEffect(() => {
navigator.mediaSession.setActionHandler("stop", () => {
props.setPlayingFile({});
});
}, []);
useEffect(() => {
// media session related staff
navigator.mediaSession.metadata = new window.MediaMetadata({
title: props.playingFile.filename,
album: props.playingFile.foldername,
artwork: [{ src: "/favicon.png", type: "image/png" }],
});
// no playing file
if (props.playingFile.id === undefined) {
setPlayingURL("");
@@ -174,16 +188,14 @@ function AudioPlayer(props) {
)}
</span>
{playingURL !== "" && (
<audio
id="dom-player"
controls
autoPlay
loop={loop}
className="audio-player"
src={playingURL}
></audio>
)}
<audio
id="dom-player"
controls
autoPlay
loop={loop}
className="audio-player"
src={playingURL}
></audio>
<FfmpegConfig
selectedFfmpegConfig={selectedFfmpegConfig}

View File

@@ -1,10 +1,11 @@
import * as React from 'react';
import { useState, useEffect, useContext } from "react";
import { Tr, tr, langCodeContext } from "../translate";
function Database() {
const [walkPath, setWalkPath] = useState("");
const [patternString, setPatternString] = useState(
"wav flac mp3 ogg m4a mka"
"wav flac mp3 ogg m4a mka webm"
);
const [tags, setTags] = useState([]);
const [selectedTags, setSelectedTags] = useState([]);

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useContext, useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router";
import { tr, Tr, langCodeContext } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect, useContext } from "react";
import { useParams, useNavigate } from "react-router";
import { tr, Tr, langCodeContext } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { convertIntToDateTime } from "./Common";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useEffect, useState } from "react";
function FfmpegConfig(props) {

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useNavigate } from "react-router";
import { Tr } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState } from "react";
import { useNavigate } from "react-router";
import { CalcReadableFilesize } from "./Common";

View File

@@ -1,6 +1,7 @@
import { useNavigate, useParams } from "react-router";
import { useContext, useEffect, useState } from "react";
import { Tr, tr, langCodeContext } from "../translate";
import * as React from 'react';
import {useNavigate, useParams} from "react-router";
import {useContext, useEffect, useState} from "react";
import {Tr, tr, langCodeContext} from "../translate";
function FileInfo(props) {
let navigate = useNavigate();
@@ -15,7 +16,8 @@ function FileInfo(props) {
const [tags, setTags] = useState([]);
const [tagsOnFile, setTagsOnFile] = useState([]);
const [selectedTagID, setSelectedTagID] = useState("");
const { langCode } = useContext(langCodeContext);
const {langCode} = useContext(langCodeContext);
const [ffprobeInfo, setFfprobeInfo] = useState("");
function refresh() {
fetch(`/api/v1/get_file_info`, {
@@ -301,6 +303,27 @@ function FileInfo(props) {
</button>
</div>
</div>
<button onClick={async () => {
const resp = await fetch(`/api/v1/get_file_ffprobe_info`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: parseInt(params.id),
}),
});
const text = await resp.text();
setFfprobeInfo(text);
}}>FFprobe</button>
{ffprobeInfo && <textarea
style={{
height: "30em",
}}
>{ffprobeInfo}</textarea>}
</div>
);
}

View File

@@ -1,9 +1,10 @@
import { useParams } from "react-router";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery } from "./Common";
import * as React from 'react';
import {useParams} from "react-router";
import {useState, useEffect} from "react";
import {useNavigate} from "react-router-dom";
import {useQuery} from "./Common";
import FilesTable from "./FilesTable";
import { Tr } from "../translate";
import {Tr} from "../translate";
function FilesInFolder(props) {
let params = useParams();
@@ -13,13 +14,14 @@ function FilesInFolder(props) {
const [isLoading, setIsLoading] = useState(false);
const offset = parseInt(query.get("o")) || 0;
const [newFoldername, setNewFoldername] = useState("");
const [folderPath, setFolderPath] = useState("");
const limit = 10;
function refresh() {
setIsLoading(true);
fetch("/api/v1/get_files_in_folder", {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
folder_id: parseInt(params.id),
offset: offset,
@@ -32,6 +34,7 @@ function FilesInFolder(props) {
alert(data.error);
} else {
setFiles(data.files);
setFolderPath(data.folder);
if (data.files.length > 0) {
setNewFoldername(data.files[0].foldername);
}
@@ -63,7 +66,7 @@ function FilesInFolder(props) {
setIsLoading(true);
fetch("/api/v1/update_foldername", {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
id: parseInt(params.id),
foldername: newFoldername,
@@ -87,7 +90,7 @@ function FilesInFolder(props) {
setIsLoading(true);
fetch("/api/v1/reset_foldername", {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
id: parseInt(params.id),
}),
@@ -119,6 +122,7 @@ function FilesInFolder(props) {
<button onClick={nextPage}>{Tr("Next page")}</button>
</div>
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
<span>{folderPath}</span>
<div>
<input
type="text"

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import FileEntry from "./FileEntry";
import { Tr } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useNavigate } from "react-router";
import { Tr } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery } from "./Common";
@@ -13,12 +14,17 @@ function GetRandomFiles(props) {
const selectedTag = query.get("t") || "";
const { langCode } = useContext(langCodeContext);
function getRandomFiles() {
const fetchRandomFiles = async () => {
const resp = await fetch("/api/v1/get_random_files");
const json = await resp.json();
return json.files;
};
async function getRandomFiles() {
setIsLoading(true);
fetch("/api/v1/get_random_files")
.then((response) => response.json())
fetchRandomFiles()
.then((data) => {
setFiles(data.files);
setFiles(data);
})
.catch((error) => {
alert("get_random_files error: " + error);
@@ -28,9 +34,8 @@ function GetRandomFiles(props) {
});
}
function getRandomFilesWithTag() {
setIsLoading(true);
fetch("/api/v1/get_random_files_with_tag", {
const fetchRandomFilesWithTag = async (selectedTag) => {
const resp = await fetch("/api/v1/get_random_files_with_tag", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -38,14 +43,16 @@ function GetRandomFiles(props) {
body: JSON.stringify({
id: parseInt(selectedTag),
}),
})
.then((response) => response.json())
.then((data) => {
if (data.error) {
alert(data.error);
} else {
setFiles(data.files);
}
});
const json = await resp.json();
return json.files;
};
function getRandomFilesWithTag() {
setIsLoading(true);
fetchRandomFilesWithTag(selectedTag)
.then((files) => {
setFiles(files);
})
.catch((error) => {
alert("get_random_files_with_tag error: " + error);

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useNavigate } from "react-router-dom";
import { useContext, useState } from "react";
import { Tr, tr, langCodeContext } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useNavigate } from "react-router";
import Database from "./Database";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { Tr } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useNavigate } from "react-router-dom";
import { useContext, useState } from "react";
import { tr, Tr, langCodeContext } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { Link } from "react-router-dom";
import { convertIntToDateTime } from "./Common";
import { Tr, tr, langCodeContext } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect } from "react";
import { useParams } from "react-router";
import ReviewEntry from "./ReviewEntry";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect, useContext } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery } from "./Common";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery } from "./Common";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useContext, useEffect, useState } from "react";
import { useParams } from "react-router";
import FilesTable from "./FilesTable";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { Tr } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect, useContext } from "react";
import { useParams } from "react-router";
import ReviewEntry from "./ReviewEntry";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useEffect } from 'react';
function UserStatus(props) {

View File

@@ -1,18 +0,0 @@
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();

12
web/src/index.jsx Normal file
View File

@@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import 'water.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

View File

@@ -1,13 +0,0 @@
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;

View File

@@ -1,4 +1,5 @@
import { createContext, renderToString } from "react";
import * as React from 'react';
import { createContext } from "react";
import MAP_zh_CN from "./zh_CN";
const LANG_OPTIONS = {