Compare commits
6 Commits
v1.2.0-rc1
...
dbms
| Author | SHA1 | Date | |
|---|---|---|---|
|
824866bdd8
|
|||
|
0b76ea08cd
|
|||
|
ba1e96db26
|
|||
|
ff85724982
|
|||
|
ad388cf83b
|
|||
|
881334cccb
|
176
README.md
176
README.md
@@ -1,119 +1,97 @@
|
||||
# MSW Open Music Project
|
||||
|
||||
A light weight personal music streaming platform.
|
||||
> The best way to search for a music is to load up a huge playlist and shuffle until you find it.
|
||||
|
||||
A 💪 light weight ⚡️ blazingly fast 🖥️ cross platform personal music streaming platform. Manage your existing music files and enjoy them on any devices.
|
||||
|
||||
Front-end web application build with `react.js` and `water.css`, back-end build with `golang` and `sqlite`.
|
||||
|
||||
## Introduction
|
||||
|
||||
Screenshot
|
||||
|
||||

|
||||
|
||||
[TOC]
|
||||
### Features
|
||||
|
||||
### What it can do
|
||||
- 🔎 Index your existing music files, and record file name and folder information.
|
||||
|
||||
- Traverse the specified folder and index files whose extensions meet the requirements.
|
||||
- 📕 Use folder 📁 tag 🏷️ review 💬 to manage your music.
|
||||
|
||||
- Listening network port. Return to the front-end web page and process API requests.
|
||||
- 🌐 Provide a light weight web application with multi-language support.
|
||||
|
||||
- Call ffmpeg to transcode music or video.
|
||||
- 👥 Multi-user support.
|
||||
|
||||
- Manage files' information in the database, including adding tags, comments, etc.
|
||||
- 🔥 Call `ffmpeg` with customizable preset to stream your music.
|
||||
|
||||
### What it won't do
|
||||
- 🔗 Share music with others!
|
||||
|
||||
- Modify your file.
|
||||
### Try it if you...
|
||||
|
||||
- Read music metadata (for example, composer, album, genre).
|
||||
- Already saved a lot of music files on disk. 🖴
|
||||
|
||||
- Detect file changes (Need to manually update the database).
|
||||
- Downloaded tons of huge lossless music. 🎵
|
||||
|
||||
## ER Diagram
|
||||
- Wants to stream your music files from PC/Server to PC/phone. 😋
|
||||
|
||||
Entities Relationship Diagram
|
||||
|
||||

|
||||
|
||||
- `avatar` entity may change in future rc version.
|
||||
|
||||
- The first time you run the program, the server will create an anonymous user with id 1. All users who are not logged in will be automatically logged in to this account.
|
||||
|
||||
- `tmpfs` is store in memory, which will be empty everytime server restart.
|
||||
|
||||
## How to build
|
||||
|
||||
Compile software from source code. If you use the pre-compiled version, you don’t need to do this.
|
||||
|
||||
### Build the back-end server
|
||||
|
||||
`make linux` or `make windows`
|
||||
|
||||
The executable file is named `msw-open-music` or `msw-open-music.exe`
|
||||
|
||||
### Build the font-end web pages
|
||||
|
||||
To build production web page `make web`
|
||||
|
||||
This command will go into `web` directory and install `node_modules`. Then execute `npm run build` command. The built web pages is under `web/build` directory.
|
||||
|
||||
To start the development, run `cd web` and `npm start`
|
||||
- Wants to share your stored music. 😘
|
||||
|
||||
## Usage
|
||||
|
||||
> Security Warning (v1.2.0-rc1):
|
||||
>
|
||||
> The cookie stored in the client browser is encrypted using the environment variable "SESSION_KEY". The server will trust the client's cookie. Leaking this environment variable may cause security problems. This problem will be fixed in a future rc version.
|
||||
>
|
||||
> Password is not hashed in database, which will be fixed in next rc version.
|
||||
1. Modify the `secret` in `config.json`
|
||||
|
||||
Start back-end server. Server will listen on 8080 port.
|
||||
2. Run back-end server `msw-open-music.exe` or `msw-open-music`. Server will listen on 8080 port by default. Then open <http://127.0.0.1:8080> to setup first admin account.
|
||||
|
||||
Build the font-end web page, then go to <http://127.0.0.1:8080>
|
||||
The front-end HTML files are under `web/build`
|
||||
|
||||
By default:
|
||||
|
||||
- URL matched `/api/*` will process by back-end server.
|
||||
- Others URL matched `/*` will be served files under `web/build/`
|
||||
|
||||
### Run back-end server
|
||||
|
||||
> Token authencation no longer support in v1.2.0 stable version.
|
||||
|
||||
Configuration file is `config.json`, **Please modify your `token`** 。
|
||||
|
||||
Default `ffmpeg_threads` is 1. Seems value larger than 1 will not increase the audio encode speed.
|
||||
|
||||
#### config.json description
|
||||
|
||||
- `database_name` string type. The filename of `sqlite3` database. Will create if that file doesn't exist.
|
||||
- `addr` string type. The listen address and port.
|
||||
- `token` string type. Password.
|
||||
- `ffmpeg_config_list` list type, include `ffmpegConfig` object.
|
||||
- `file_life_time` integer type (second). Life time for temporary file. If the temporary file is not accessed for more than this time, back-end server will delete this file.
|
||||
- `cleaner_internal` integer type (second). Interval for `tmpfs` checking temporary file.
|
||||
- `root` string type. Directory to store temporary files. Default is `/tmp`, **please modify this directory if you are using Windows.**
|
||||
|
||||
### Run font-end web page
|
||||
|
||||
Open your web browser to <http://127.0.0.1:8080> you will see the web pages.
|
||||
|
||||
### Setup the first admin account
|
||||
### Setup first admin account
|
||||
|
||||
The first administrator account will be active automatically, other administrator accounts need active manually.
|
||||
|
||||
Go to register page, select the role to admin, and register the first admin account.
|
||||
|
||||
## About tmpfs
|
||||
#### config.json
|
||||
|
||||
If the `Prepare` mode is enabled in the font-wed player, back-end server will convert the whole file into the temporary folder, then serve file using native method. This cqan avoid ffmpeg pipe break problem cause by unstable network connection while streaming audio.
|
||||
- `secret` string type. Secret to encrypt the session.
|
||||
|
||||
- `database_name` string type. The filename of `sqlite3` database. Will create if that file doesn't exist.
|
||||
- `addr` string type. The listen address and port.
|
||||
- `ffmpeg_config_list` list type, include `ffmpegConfig` object.
|
||||
- `file_life_time` integer type (second). Life time for temporary file. If the temporary file is not accessed for more than this time, back-end server will delete this file.
|
||||
- `cleaner_internal` integer type (second). Interval for `tmpfs` checking temporary file.
|
||||
- `root` string type. Directory to store temporary files. Default is `/tmp`, **please modify this directory if you are using Windows.** Directory will be created if not exists.
|
||||
|
||||
For windows user, make sure you have `ffmpeg` installed.
|
||||
|
||||
## Development
|
||||
|
||||
Any issues or pull requests are welcome.
|
||||
|
||||
### Major changes log
|
||||
|
||||
- `v1.0.0` First version. Implement the core streaming function.
|
||||
- `v1.1.0` Use `React` to rewrite the font-end web pages.
|
||||
- `v1.2.0` Add user, tag, review and other functions for DBMS course project.
|
||||
|
||||
### ER Diagram
|
||||
|
||||
Database Entities Relationship Diagram
|
||||
|
||||

|
||||
|
||||
- `avatar` is not using currently
|
||||
|
||||
- The first time you run the program, the server will create an anonymous user with id `1`. All users who are not logged in will be automatically logged in to this account.
|
||||
|
||||
- `tmpfs` is store in memory, which will be empty everytime server restart.
|
||||
|
||||
### About tmpfs
|
||||
|
||||
If the `Prepare` mode is enabled in the font-wed player, back-end server will convert the whole file into the temporary folder, then serve file. This can avoid `ffmpeg` pipe break problem cause by unstable network connection while streaming audio.
|
||||
|
||||
The default temporary folder is `/tmp`, which is a `tmpfs` file system in Linux operating system. Default life time for temporary files is 600 seconds (10 minutes). If the temporary file is not accessed for more than this time, back-end server will delete this file.
|
||||
|
||||
## Change log
|
||||
|
||||
- `v1.0.0` First version. Ready to use in production environment.
|
||||
- `v1.1.0` Use `React` to rewrite the font-end web pages (Previous using `Vue`).
|
||||
- `v1.2.0` Add user, tag, review, video functions for DBMS course work.
|
||||
|
||||
## Back-end API references
|
||||
### Back-end API design
|
||||
|
||||
API does not need to respond any data will return the following JSON object.
|
||||
|
||||
@@ -133,40 +111,12 @@ Sometime errors happen, server will return the following JSON object, which `err
|
||||
|
||||
API does not need to send any data should use `GET` method, otherwise use `POST` method.
|
||||
|
||||
Server use cookies to authencate a user. Any request without cookies will be consider from an anonymouse user.
|
||||
Server use cookies to authenticate a user. Any request without cookies will be consider from an anonymous user (aka. user with ID `1`).
|
||||
|
||||
Some import source code file:
|
||||
Some important source code files:
|
||||
|
||||
- `pkg/api/api.go` define URL
|
||||
|
||||
- `pkg/database/sql_stmt.go` define SQL queries and do the init job.
|
||||
|
||||
- `pkg/database/struct.go` define JSON structures for database entities.
|
||||
|
||||
## Font-end web pages
|
||||
|
||||
- `/#/` Get random files.
|
||||
|
||||
- `/#/files` Search files.
|
||||
|
||||
- `/#/files/39` Information of the which id is 39.
|
||||
|
||||
- `/#/files/39/review` Reviews of the file with file id 39.
|
||||
|
||||
- `/#/files/39/share` Share page with the file id 39.
|
||||
|
||||
- `/#/folders` Search folders.
|
||||
|
||||
- `/#/folders/2614` Files in folder which id is 2614.
|
||||
|
||||
- `/#/manage` Manage page.
|
||||
|
||||
- `/#/manage/users` List users.
|
||||
|
||||
- `/#/manage/users/1` Information of user whose id is 1.
|
||||
|
||||
- `/#/manage/tags` List tags.
|
||||
|
||||
- `/#/manage/tags/1` Information of tag which id is 1.
|
||||
|
||||
- `/#/manage/feedbacks` List feedbacks.
|
||||
|
||||
7
go.mod
7
go.mod
@@ -1,8 +1,11 @@
|
||||
module msw-open-music
|
||||
|
||||
go 1.16
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/mattn/go-sqlite3 v1.14.7
|
||||
github.com/mattn/go-sqlite3 v1.14.14
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
)
|
||||
|
||||
require github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@@ -2,5 +2,7 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
|
||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
|
||||
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
|
||||
@@ -2,22 +2,22 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotLoggedIn = errors.New("not logged in")
|
||||
ErrNotAdmin = errors.New("not admin")
|
||||
ErrEmpty = errors.New("Empty field detected, please fill in all fields")
|
||||
ErrAnonymous = errors.New("Anonymous user detected, please login")
|
||||
ErrNotActive = errors.New("User is not active")
|
||||
ErrNotLoggedIn = errors.New("not logged in")
|
||||
ErrNotAdmin = errors.New("not admin")
|
||||
ErrEmpty = errors.New("Empty field detected, please fill in all fields")
|
||||
ErrAnonymous = errors.New("Anonymous user detected, please login")
|
||||
ErrNotActive = errors.New("User is not active")
|
||||
ErrWrongPassword = errors.New("Wrong password")
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (api *API) HandleError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
|
||||
@@ -105,6 +105,5 @@ func (api *API) HandleDeleteFeedback(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type DeleteFileRequest struct {
|
||||
|
||||
@@ -159,4 +159,3 @@ func (api *API) HandleDeleteTag(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,20 +8,20 @@ import (
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
sqlConn *sql.DB
|
||||
stmt *Stmt
|
||||
sqlConn *sql.DB
|
||||
stmt *Stmt
|
||||
singleThreadLock SingleThreadLock
|
||||
}
|
||||
|
||||
func NewSingleThreadLock(enabled bool) SingleThreadLock {
|
||||
return SingleThreadLock{
|
||||
lock: sync.Mutex{},
|
||||
lock: sync.Mutex{},
|
||||
enabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
type SingleThreadLock struct {
|
||||
lock sync.Mutex
|
||||
lock sync.Mutex
|
||||
enabled bool
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ func NewDatabase(dbName string, singleThread bool) (*Database, error) {
|
||||
|
||||
// new database
|
||||
database := &Database{
|
||||
sqlConn: sqlConn,
|
||||
stmt: stmt,
|
||||
sqlConn: sqlConn,
|
||||
stmt: stmt,
|
||||
singleThreadLock: NewSingleThreadLock(singleThread),
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("object not found")
|
||||
ErrNotFound = errors.New("object not found")
|
||||
ErrTagNotFound = errors.New("tag not found")
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ func (database *Database) GetTags() ([]*Tag, error) {
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (database *Database) UpdateTag(tag *Tag) (error) {
|
||||
func (database *Database) UpdateTag(tag *Tag) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func (database *Database) Login(username string, password string) (*User, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
@@ -7,10 +12,17 @@ func (database *Database) Login(username string, password string) (*User, error)
|
||||
user := &User{}
|
||||
|
||||
// get user from database
|
||||
err := database.stmt.getUser.QueryRow(username, password).Scan(&user.ID, &user.Username, &user.Role, &user.Active, &user.AvatarId)
|
||||
err := database.stmt.getUser.QueryRow(username).Scan(&user.ID, &user.Username, &user.Password, &user.Role, &user.Active, &user.AvatarId)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
// validate password
|
||||
err = database.ComparePassword(user.Password, password)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -44,6 +56,9 @@ func (database *Database) Register(username string, password string, usertype in
|
||||
active = true
|
||||
}
|
||||
|
||||
// encrypt password
|
||||
password = database.EncryptPassword(password)
|
||||
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
@@ -130,9 +145,27 @@ func (database *Database) UpdateUserPassword(id int64, password string) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
// encrypt password
|
||||
password = database.EncryptPassword(password)
|
||||
|
||||
_, err := database.stmt.updateUserPassword.Exec(password, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (database *Database) EncryptPassword(password string) string {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
log.Println("[database] Failed to hash password, using plaintext password")
|
||||
return password
|
||||
}
|
||||
|
||||
return string(hash)
|
||||
}
|
||||
|
||||
func (database *Database) ComparePassword(hashedPassword string, plainTextPassword string) error {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainTextPassword))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ var countUserQuery = `SELECT count(*) FROM users;`
|
||||
|
||||
var countAdminQuery = `SELECT count(*) FROM users WHERE role= 1;`
|
||||
|
||||
var getUserQuery = `SELECT id, username, role, active, avatar_id FROM users WHERE username = ? AND password = ? LIMIT 1;`
|
||||
var getUserQuery = `SELECT id, username, password, role, active, avatar_id FROM users WHERE username = ? LIMIT 1;`
|
||||
|
||||
var getUsersQuery = `SELECT id, username, role, active, avatar_id FROM users;`
|
||||
|
||||
|
||||
216
web/src/App.js
216
web/src/App.js
@@ -19,105 +19,135 @@ import UserStatus from "./component/UserStatus";
|
||||
import ReviewPage from "./component/ReviewPage";
|
||||
import UserProfile from "./component/UserProfile";
|
||||
import FeedbackPage from "./component/FeedbackPage";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
||||
|
||||
function App() {
|
||||
const [playingFile, setPlayingFile] = useState({});
|
||||
const [user, setUser] = useState({});
|
||||
const [langCode, setLangCode] = useState("en_US");
|
||||
|
||||
// select language
|
||||
useEffect(() => {
|
||||
const browserCode = window.navigator.language;
|
||||
for (const key in LANG_OPTIONS) {
|
||||
for (const i in LANG_OPTIONS[key].matches) {
|
||||
const code = LANG_OPTIONS[key].matches[i];
|
||||
if (code === browserCode) {
|
||||
setLangCode(key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallback to english
|
||||
setLangCode('en-US');
|
||||
}, []);
|
||||
|
||||
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>
|
||||
<UserStatus user={user} setUser={setUser} />
|
||||
</h3>
|
||||
<nav className="nav">
|
||||
<NavLink to="/" className="nav-link">
|
||||
Feeling luckly
|
||||
</NavLink>
|
||||
<NavLink to="/files" className="nav-link">
|
||||
Files
|
||||
</NavLink>
|
||||
<NavLink to="/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="/files"
|
||||
element={<SearchFiles setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/folders"
|
||||
element={<SearchFolders setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/folders/:id"
|
||||
element={<FilesInFolder setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage"
|
||||
element={<Manage user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/feedbacks"
|
||||
element={<FeedbackPage user={user} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/login"
|
||||
element={<Login user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/register"
|
||||
element={<Register user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route path="/manage/tags" element={<Tags user={user} />} />
|
||||
<Route path="/manage/tags/:id" element={<EditTag user={user} />} />
|
||||
<Route
|
||||
path="/manage/reviews/:id"
|
||||
element={<EditReview user={user} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/users"
|
||||
element={<ManageUser user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/users/:id"
|
||||
element={<UserProfile user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files/:id"
|
||||
element={<FileInfo setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files/:id/share"
|
||||
element={<Share setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files/:id/review"
|
||||
element={
|
||||
<ReviewPage user={user} setPlayingFile={setPlayingFile} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
<AudioPlayer
|
||||
playingFile={playingFile}
|
||||
setPlayingFile={setPlayingFile}
|
||||
/>
|
||||
</Router>
|
||||
<langCodeContext.Provider value={{ langCode, setLangCode }}>
|
||||
<Router>
|
||||
<header className="header">
|
||||
<h3 className="title">
|
||||
<img src="favicon.png" alt="logo" className="logo" />
|
||||
<span className="title-text">MSW Open Music Project</span>
|
||||
<UserStatus user={user} setUser={setUser} />
|
||||
</h3>
|
||||
<nav className="nav">
|
||||
<NavLink to="/" className="nav-link">
|
||||
{Tr("Feeling luckly")}
|
||||
</NavLink>
|
||||
<NavLink to="/files" className="nav-link">
|
||||
{Tr("Files")}
|
||||
</NavLink>
|
||||
<NavLink to="/folders" className="nav-link">
|
||||
{Tr("Folders")}
|
||||
</NavLink>
|
||||
<NavLink to="/manage" className="nav-link">
|
||||
{Tr("Manage")}
|
||||
</NavLink>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
index
|
||||
path="/"
|
||||
element={<GetRandomFiles setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files"
|
||||
element={<SearchFiles setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/folders"
|
||||
element={<SearchFolders setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/folders/:id"
|
||||
element={<FilesInFolder setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage"
|
||||
element={
|
||||
<Manage
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
setLangCode={setLangCode}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/feedbacks"
|
||||
element={<FeedbackPage user={user} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/login"
|
||||
element={<Login user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/register"
|
||||
element={<Register user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route path="/manage/tags" element={<Tags user={user} />} />
|
||||
<Route
|
||||
path="/manage/tags/:id"
|
||||
element={<EditTag user={user} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/reviews/:id"
|
||||
element={<EditReview user={user} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/users"
|
||||
element={<ManageUser user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/users/:id"
|
||||
element={<UserProfile user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files/:id"
|
||||
element={<FileInfo setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files/:id/share"
|
||||
element={<Share setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files/:id/review"
|
||||
element={
|
||||
<ReviewPage user={user} setPlayingFile={setPlayingFile} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
<AudioPlayer
|
||||
playingFile={playingFile}
|
||||
setPlayingFile={setPlayingFile}
|
||||
/>
|
||||
</Router>
|
||||
</langCodeContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router";
|
||||
import { CalcReadableFilesizeDetail } from "./Common";
|
||||
import FfmpegConfig from "./FfmpegConfig";
|
||||
import FileDialog from "./FileDialog";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function AudioPlayer(props) {
|
||||
// props.playingFile
|
||||
@@ -67,7 +68,7 @@ function AudioPlayer(props) {
|
||||
|
||||
return (
|
||||
<footer className="vertical">
|
||||
<h5>Player status</h5>
|
||||
<h5>{Tr("Player status")}</h5>
|
||||
{props.playingFile.id && (
|
||||
<span>
|
||||
<FileDialog
|
||||
@@ -105,7 +106,7 @@ function AudioPlayer(props) {
|
||||
props.setPlayingFile({});
|
||||
}}
|
||||
>
|
||||
Stop
|
||||
{Tr("Stop")}
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
@@ -138,7 +139,7 @@ function AudioPlayer(props) {
|
||||
);
|
||||
}}
|
||||
>
|
||||
Stop Timer
|
||||
{Tr("Stop Timer")}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
@@ -149,7 +150,7 @@ function AudioPlayer(props) {
|
||||
onChange={(event) => setLoop(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label>Loop</label>
|
||||
<label>{Tr("Loop")}</label>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
@@ -158,7 +159,7 @@ function AudioPlayer(props) {
|
||||
onChange={(event) => setRaw(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label>Raw</label>
|
||||
<label>{Tr("Raw")}</label>
|
||||
</span>
|
||||
|
||||
{!raw && (
|
||||
@@ -168,7 +169,7 @@ function AudioPlayer(props) {
|
||||
onChange={(event) => setPrepare(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label>Prepare</label>
|
||||
<label>{Tr("Prepare")}</label>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useState, useEffect } 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");
|
||||
const [patternString, setPatternString] = useState(
|
||||
"wav flac mp3 ogg m4a mka"
|
||||
);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [selectedTags, setSelectedTags] = useState([]);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function getTags() {
|
||||
fetch("/api/v1/get_tags")
|
||||
@@ -60,21 +64,21 @@ function Database() {
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h3>Update Database</h3>
|
||||
<h3>{Tr("Update Database")}</h3>
|
||||
<input
|
||||
type="text"
|
||||
value={walkPath}
|
||||
placeholder="walk path"
|
||||
placeholder={tr("walk path", langCode)}
|
||||
onChange={(e) => setWalkPath(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={patternString}
|
||||
placeholder="pattern wav flac mp3"
|
||||
placeholder={tr("pattern wav flac mp3", langCode)}
|
||||
onChange={(e) => setPatternString(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<h4>Tags</h4>
|
||||
<h4>{Tr("Tags")}</h4>
|
||||
{tags.map((tag) => (
|
||||
<div key={tag.id}>
|
||||
<input
|
||||
@@ -101,7 +105,7 @@ function Database() {
|
||||
}}
|
||||
disabled={updating}
|
||||
>
|
||||
{updating ? "Updating..." : "Update Database"}
|
||||
{updating ? Tr("Updating...") : Tr("Update Database")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { tr, Tr, langCodeContext } from "../translate";
|
||||
|
||||
function SingleReview() {
|
||||
let params = useParams();
|
||||
let navigate = useNavigate();
|
||||
const { langCode } = useContext(langCodeContext)
|
||||
|
||||
const [review, setReview] = useState({
|
||||
id: "",
|
||||
@@ -50,7 +52,7 @@ function SingleReview() {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert("Review updated!");
|
||||
alert(tr("Review updated", langCode));
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
@@ -71,7 +73,7 @@ function SingleReview() {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert("Review deleted!");
|
||||
alert(tr("Review deleted", langCode));
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
@@ -83,14 +85,14 @@ function SingleReview() {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Edit Review</h3>
|
||||
<h3>{Tr("Edit Review")}</h3>
|
||||
<textarea
|
||||
value={review.content}
|
||||
onChange={(e) => setReview({ ...review, content: e.target.value })}
|
||||
></textarea>
|
||||
<div>
|
||||
<button onClick={() => deleteReview()}>Delete</button>
|
||||
<button onClick={() => save()}>Save</button>
|
||||
<button onClick={() => deleteReview()}>{Tr("Delete")}</button>
|
||||
<button onClick={() => save()}>{Tr("Save")}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { tr, Tr, langCodeContext } from "../translate";
|
||||
|
||||
function EditTag() {
|
||||
let params = useParams();
|
||||
let navigate = useNavigate();
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
const [tag, setTag] = useState({
|
||||
id: "",
|
||||
@@ -54,7 +56,7 @@ function EditTag() {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert("Tag updated successfully");
|
||||
alert(tr("Tag updated successfully", langCode));
|
||||
refreshTagInfo();
|
||||
}
|
||||
});
|
||||
@@ -79,7 +81,7 @@ function EditTag() {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert("Tag deleted successfully");
|
||||
alert(tr("Tag deleted successfully", langCode));
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
@@ -87,9 +89,9 @@ function EditTag() {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Edit Tag</h3>
|
||||
<h3>{Tr("Edit Tag")}</h3>
|
||||
<div>
|
||||
<label htmlFor="id">ID</label>
|
||||
<label htmlFor="id">{Tr("ID")}</label>
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
@@ -98,7 +100,7 @@ function EditTag() {
|
||||
value={tag.id}
|
||||
onChange={(e) => setTag({ ...tag, id: e.target.value })}
|
||||
/>
|
||||
<label htmlFor="name">Created By</label>
|
||||
<label htmlFor="name">{Tr("Created by")}</label>
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
@@ -115,7 +117,7 @@ function EditTag() {
|
||||
})
|
||||
}
|
||||
/>
|
||||
<label htmlFor="name">Name</label>
|
||||
<label htmlFor="name">{Tr("Name")}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
@@ -123,15 +125,15 @@ function EditTag() {
|
||||
value={tag.name}
|
||||
onChange={(e) => setTag({ ...tag, name: e.target.value })}
|
||||
/>
|
||||
<label htmlFor="description">Description</label>
|
||||
<label htmlFor="description">{Tr("Description")}</label>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
value={tag.description}
|
||||
onChange={(e) => setTag({ ...tag, description: e.target.value })}
|
||||
/>
|
||||
<button onClick={deleteTag}>Delete</button>
|
||||
<button onClick={() => updateTagInfo()}>Save</button>
|
||||
<button onClick={deleteTag}>{Tr("Delete")}</button>
|
||||
<button onClick={() => updateTagInfo()}>{Tr("Save")}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { convertIntToDateTime } from "./Common";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FeedbackPage() {
|
||||
const [content, setContext] = useState("");
|
||||
@@ -45,17 +46,17 @@ function FeedbackPage() {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Feedback</h3>
|
||||
<h3>{Tr("Feedbacks")}</h3>
|
||||
<textarea value={content} onChange={(e) => setContext(e.target.value)} />
|
||||
<button onClick={() => submitFeedback()}>Submit</button>
|
||||
<button onClick={() => submitFeedback()}>{Tr("Submit")}</button>
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Feedback</th>
|
||||
<th>Date</th>
|
||||
<th>Action</th>
|
||||
<th>{Tr("User")}</th>
|
||||
<th>{Tr("Feedback")}</th>
|
||||
<th>{Tr("Date")}</th>
|
||||
<th>{Tr("Action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -90,7 +91,7 @@ function FeedbackPage() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
{Tr("Delete")}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useNavigate } from "react-router";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FileDialog(props) {
|
||||
// props.showStatus
|
||||
@@ -23,9 +24,9 @@ function FileDialog(props) {
|
||||
{props.file.filename}
|
||||
</p>
|
||||
<p>
|
||||
Play: play using browser player.
|
||||
{Tr("Play: play using browser player.")}
|
||||
<br />
|
||||
Info for more actions.
|
||||
{Tr("Info for more actions.")}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -33,7 +34,7 @@ function FileDialog(props) {
|
||||
props.setShowStatus(false);
|
||||
}}
|
||||
>
|
||||
Info
|
||||
{Tr("Info")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -41,9 +42,9 @@ function FileDialog(props) {
|
||||
props.setShowStatus(false);
|
||||
}}
|
||||
>
|
||||
Play
|
||||
{Tr("Play")}
|
||||
</button>
|
||||
<button onClick={() => props.setShowStatus(false)}>Close</button>
|
||||
<button onClick={() => props.setShowStatus(false)}>{Tr("Close")}</button>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function FileInfo(props) {
|
||||
let navigate = useNavigate();
|
||||
@@ -14,6 +15,7 @@ function FileInfo(props) {
|
||||
const [tags, setTags] = useState([]);
|
||||
const [tagsOnFile, setTagsOnFile] = useState([]);
|
||||
const [selectedTagID, setSelectedTagID] = useState("");
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function refresh() {
|
||||
fetch(`/api/v1/get_file_info`, {
|
||||
@@ -90,7 +92,9 @@ function FileInfo(props) {
|
||||
|
||||
function deleteFile() {
|
||||
// show Warning
|
||||
if (window.confirm("Are you sure you want to delete this file?")) {
|
||||
if (
|
||||
window.confirm(tr("Are you sure you want to delete this file?", langCode))
|
||||
) {
|
||||
fetch(`/api/v1/delete_file`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -127,7 +131,7 @@ function FileInfo(props) {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert("Filename updated");
|
||||
alert(tr("Filename updated", langCode));
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
@@ -163,42 +167,42 @@ function FileInfo(props) {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>File Details</h3>
|
||||
<h3>{Tr("File Details")}</h3>
|
||||
<div>
|
||||
<a href={downloadURL} download>
|
||||
<button>Download</button>
|
||||
<button>{Tr("Download")}</button>
|
||||
</a>
|
||||
<button
|
||||
onClick={() => {
|
||||
props.setPlayingFile(file);
|
||||
}}
|
||||
>
|
||||
Play
|
||||
{Tr("Play")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/files/${params.id}/review`);
|
||||
}}
|
||||
>
|
||||
Review
|
||||
{Tr("Review")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/files/${params.id}/share`);
|
||||
}}
|
||||
>
|
||||
Share
|
||||
{Tr("Share")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
deleteFile();
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
{Tr("Delete")}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="foldername">Folder Name:</label>
|
||||
<label htmlFor="foldername">{Tr("Folder Name")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="foldername"
|
||||
@@ -208,7 +212,7 @@ function FileInfo(props) {
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
<label htmlFor="filename">File Name:</label>
|
||||
<label htmlFor="filename">{Tr("Filename")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="filename"
|
||||
@@ -220,15 +224,15 @@ function FileInfo(props) {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="filesize">File Size:</label>
|
||||
<label htmlFor="filesize">{Tr("File size")}</label>
|
||||
<input type="text" id="filesize" value={file.filesize} readOnly />
|
||||
</div>
|
||||
<div className="horizontal">
|
||||
<button onClick={updateFilename}>Save</button>
|
||||
<button onClick={resetFilename}>Reset</button>
|
||||
<button onClick={updateFilename}>{Tr("Save")}</button>
|
||||
<button onClick={resetFilename}>{Tr("Reset")}</button>
|
||||
</div>
|
||||
<div>
|
||||
<label>Tags:</label>
|
||||
<label>{Tr("Tags")}</label>
|
||||
<ul>
|
||||
{tagsOnFile.map((tag) => {
|
||||
return (
|
||||
@@ -245,7 +249,7 @@ function FileInfo(props) {
|
||||
removeTagOnFile(tag.id);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
{Tr("Remove")}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
@@ -257,7 +261,7 @@ function FileInfo(props) {
|
||||
setSelectedTagID(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="">Select a tag</option>
|
||||
<option value="">{tr("Select a tag", langCode)}</option>
|
||||
{tags.map((tag) => {
|
||||
return (
|
||||
<option key={tag.id} value={tag.id}>
|
||||
@@ -270,7 +274,7 @@ function FileInfo(props) {
|
||||
onClick={() => {
|
||||
// check empty
|
||||
if (selectedTagID === "") {
|
||||
alert("Please select a tag");
|
||||
alert(tr("Please select a tag", langCode));
|
||||
return;
|
||||
}
|
||||
fetch(`/api/v1/put_tag_on_file`, {
|
||||
@@ -293,7 +297,7 @@ function FileInfo(props) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add Tag
|
||||
{Tr("Add tag")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "./Common";
|
||||
import FilesTable from "./FilesTable";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FilesInFolder(props) {
|
||||
let params = useParams();
|
||||
@@ -107,13 +108,15 @@ function FilesInFolder(props) {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Files in Folder</h3>
|
||||
<h3>{Tr("Files in Folder")}</h3>
|
||||
<div className="search_toolbar">
|
||||
<button onClick={lastPage}>Last page</button>
|
||||
<button onClick={lastPage}>{Tr("Last page")}</button>
|
||||
<button disabled>
|
||||
{isLoading ? "Loading..." : `${offset} - ${offset + files.length}`}
|
||||
{isLoading
|
||||
? Tr("Loading...")
|
||||
: `${offset} - ${offset + files.length}`}
|
||||
</button>
|
||||
<button onClick={nextPage}>Next page</button>
|
||||
<button onClick={nextPage}>{Tr("Next page")}</button>
|
||||
</div>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||
<div>
|
||||
@@ -123,8 +126,8 @@ function FilesInFolder(props) {
|
||||
onChange={(e) => setNewFoldername(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<button onClick={() => updateFoldername()}>Save</button>
|
||||
<button onClick={() => resetFoldername()}>Reset</button>
|
||||
<button onClick={() => updateFoldername()}>{Tr("Save")}</button>
|
||||
<button onClick={() => resetFoldername()}>{Tr("Reset")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import FileEntry from "./FileEntry";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FilesTable(props) {
|
||||
if (props.files.length === 0) {
|
||||
@@ -8,9 +9,9 @@ function FilesTable(props) {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<th>Folder Name</th>
|
||||
<th>Size</th>
|
||||
<th>{Tr("Filename")}</th>
|
||||
<th>{Tr("Folder Name")}</th>
|
||||
<th>{Tr("Size")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useNavigate } from "react-router";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FoldersTable(props) {
|
||||
let navigate = useNavigate();
|
||||
@@ -9,8 +10,8 @@ function FoldersTable(props) {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Folder name</th>
|
||||
<th>Action</th>
|
||||
<th>{Tr("Folder name")}</th>
|
||||
<th>{Tr("Action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -23,7 +24,7 @@ function FoldersTable(props) {
|
||||
{folder.foldername}
|
||||
</td>
|
||||
<td onClick={() => navigate(`/folders/${folder.id}`)}>
|
||||
<button>View</button>
|
||||
<button>{Tr("View")}</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "./Common";
|
||||
import FilesTable from "./FilesTable";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function GetRandomFiles(props) {
|
||||
const [files, setFiles] = useState([]);
|
||||
@@ -10,6 +11,7 @@ function GetRandomFiles(props) {
|
||||
const navigator = useNavigate();
|
||||
const query = useQuery();
|
||||
const selectedTag = query.get("t") || "";
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function getRandomFiles() {
|
||||
setIsLoading(true);
|
||||
@@ -84,7 +86,7 @@ function GetRandomFiles(props) {
|
||||
<div className="page">
|
||||
<div className="search_toolbar">
|
||||
<button className="refresh" onClick={() => refresh(setFiles)}>
|
||||
{isLoading ? "Loading..." : "Refresh"}
|
||||
{isLoading ? Tr("Loading...") : Tr("Refresh")}
|
||||
</button>
|
||||
<select
|
||||
className="tag_select"
|
||||
@@ -93,7 +95,7 @@ function GetRandomFiles(props) {
|
||||
}}
|
||||
value={selectedTag}
|
||||
>
|
||||
<option value="">All</option>
|
||||
<option value="">{tr("All", langCode)}</option>
|
||||
{tags.map((tag) => (
|
||||
<option key={tag.id} value={tag.id}>
|
||||
{tag.name}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function Login(props) {
|
||||
let navigate = useNavigate();
|
||||
let [username, setUsername] = useState("");
|
||||
let [password, setPassword] = useState("");
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function login() {
|
||||
if (!username || !password) {
|
||||
alert("Please enter username and password");
|
||||
alert(tr("Please enter username and password", langCode));
|
||||
return;
|
||||
}
|
||||
fetch("/api/v1/login", {
|
||||
@@ -34,15 +36,15 @@ function Login(props) {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>Login</h2>
|
||||
<label htmlFor="username">Username</label>
|
||||
<h2>{Tr("Login")}</h2>
|
||||
<label htmlFor="username">{Tr("Username")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password">Password</label>
|
||||
<label htmlFor="password">{Tr("Password")}</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
@@ -56,13 +58,13 @@ function Login(props) {
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
<button onClick={login}>Login</button>
|
||||
<button onClick={login}>{Tr("Login")}</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/manage/register");
|
||||
}}
|
||||
>
|
||||
Register
|
||||
{Tr("Register")}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
import { useNavigate } from "react-router";
|
||||
import Database from "./Database";
|
||||
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "../translate";
|
||||
import { useContext } from "react";
|
||||
|
||||
function Manage(props) {
|
||||
let navigate = useNavigate();
|
||||
const { langCode, setLangCode } = useContext(langCodeContext);
|
||||
const codes = Object.keys(LANG_OPTIONS);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>Manage</h2>
|
||||
<p>Hi, {props.user.username}</p>
|
||||
<h2>{Tr("Manage")}</h2>
|
||||
<p>
|
||||
{Tr("Hi")}, {props.user.username}
|
||||
</p>
|
||||
|
||||
<select
|
||||
onChange={(event) => {
|
||||
setLangCode(codes[event.target.selectedIndex]);
|
||||
}}
|
||||
>
|
||||
{codes.map((code) => {
|
||||
const langOption = LANG_OPTIONS[code];
|
||||
return <option key={code}>{langOption.name}</option>;
|
||||
})}
|
||||
</select>
|
||||
|
||||
{props.user.role === 0 && (
|
||||
<div>
|
||||
<button
|
||||
@@ -15,14 +34,14 @@ function Manage(props) {
|
||||
navigate("/manage/login");
|
||||
}}
|
||||
>
|
||||
Login
|
||||
{Tr("Login")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/manage/register");
|
||||
}}
|
||||
>
|
||||
Register
|
||||
{Tr("Register")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -33,7 +52,7 @@ function Manage(props) {
|
||||
navigate(`/manage/users/${props.user.id}`);
|
||||
}}
|
||||
>
|
||||
Profile
|
||||
{Tr("Profile")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -48,15 +67,17 @@ function Manage(props) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
{Tr("Logout")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<hr />
|
||||
<div className="horizontal">
|
||||
<button onClick={() => navigate("/manage/tags")}>Tags</button>
|
||||
<button onClick={() => navigate("/manage/users")}>Users</button>
|
||||
<button onClick={() => navigate("/manage/feedbacks")}>Feedbacks</button>
|
||||
<button onClick={() => navigate("/manage/tags")}>{Tr("Tags")}</button>
|
||||
<button onClick={() => navigate("/manage/users")}>{Tr("Users")}</button>
|
||||
<button onClick={() => navigate("/manage/feedbacks")}>
|
||||
{Tr("Feedbacks")}
|
||||
</button>
|
||||
</div>
|
||||
<Database />
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function ManageUser() {
|
||||
const [users, setUsers] = useState([]);
|
||||
const roleDict = {
|
||||
0: "Anonymous",
|
||||
1: "Admin",
|
||||
2: "Normal User",
|
||||
2: "User",
|
||||
};
|
||||
|
||||
function getUsers() {
|
||||
@@ -27,13 +28,13 @@ function ManageUser() {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Manage User</h3>
|
||||
<h3>{Tr("Manage User")}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Active</th>
|
||||
<th>{Tr("Name")}</th>
|
||||
<th>{Tr("Role")}</th>
|
||||
<th>{Tr("Active")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -42,7 +43,7 @@ function ManageUser() {
|
||||
<td>
|
||||
<Link to={`/manage/users/${user.id}`}>@{user.username}</Link>
|
||||
</td>
|
||||
<td>{roleDict[user.role]}</td>
|
||||
<td>{Tr(roleDict[user.role])}</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -57,13 +58,15 @@ function ManageUser() {
|
||||
id: user.id,
|
||||
active: e.target.checked,
|
||||
}),
|
||||
}).then((res) => res.json()).then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
getUsers();
|
||||
}
|
||||
});
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
getUsers();
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { tr, Tr, langCodeContext } from "../translate";
|
||||
|
||||
function Register() {
|
||||
let navigate = useNavigate();
|
||||
@@ -7,12 +8,13 @@ function Register() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [password2, setPassword2] = useState("");
|
||||
const [role, setRole] = useState("");
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function register() {
|
||||
if (!username || !password || !password2 || !role) {
|
||||
alert("Please fill out all fields");
|
||||
alert(tr("Please fill out all fields", langCode));
|
||||
} else if (password !== password2) {
|
||||
alert("Passwords do not match");
|
||||
alert(tr("Password do not match", langCode));
|
||||
} else {
|
||||
fetch("/api/v1/register", {
|
||||
method: "POST",
|
||||
@@ -38,22 +40,22 @@ function Register() {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>Register</h2>
|
||||
<label htmlFor="username">Username</label>
|
||||
<h2>{Tr("Register")}</h2>
|
||||
<label htmlFor="username">{Tr("Username")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password">Password</label>
|
||||
<label htmlFor="password">{Tr("Password")}</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password2">Confirm Password</label>
|
||||
<label htmlFor="password2">{Tr("Confirm Password")}</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password2"
|
||||
@@ -66,13 +68,13 @@ function Register() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="role">Role</label>
|
||||
<label htmlFor="role">{Tr("Role")}</label>
|
||||
<select value={role} onChange={(e) => setRole(e.target.value)}>
|
||||
<option value="">Select a role</option>
|
||||
<option value="2">User</option>
|
||||
<option value="1">Admin</option>
|
||||
<option value="">{tr("Select a role", langCode)}</option>
|
||||
<option value="2">{tr("User", langCode)}</option>
|
||||
<option value="1">{tr("Admin", langCode)}</option>
|
||||
</select>
|
||||
<button onClick={register}>Register</button>
|
||||
<button onClick={register}>{Tr("Register")}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { convertIntToDateTime } from "./Common";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
import { useContext } from "react";
|
||||
|
||||
function ReviewEntry(props) {
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
return (
|
||||
<div>
|
||||
<h4>
|
||||
<Link to={`/manage/users/${props.review.user.id}`}>
|
||||
@{props.review.user.username}
|
||||
</Link>{" "}
|
||||
review{" "}
|
||||
{Tr("review")}{" "}
|
||||
<Link to={`/files/${props.review.file.id}`}>
|
||||
{props.review.file.filename}
|
||||
</Link>{" "}
|
||||
on {convertIntToDateTime(props.review.created_at)}{" "}
|
||||
{Tr("on")} {convertIntToDateTime(props.review.created_at)}{" "}
|
||||
{props.review.updated_at !== 0 &&
|
||||
"(modified on " +
|
||||
convertIntToDateTime(props.review.updated_at) +
|
||||
")"}{" "}
|
||||
`(${tr("modified on", langCode)} ${convertIntToDateTime(
|
||||
props.review.updated_at
|
||||
)} ) `}
|
||||
{(props.user.role === 1 || props.review.user.id === props.user.id) &&
|
||||
props.user.role !== 0 && (
|
||||
<Link to={`/manage/reviews/${props.review.id}`}>Edit</Link>
|
||||
<Link to={`/manage/reviews/${props.review.id}`}>{Tr("Edit")}</Link>
|
||||
)}
|
||||
</h4>
|
||||
<p>{props.review.content}</p>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import ReviewEntry from "./ReviewEntry";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function ReviewPage(props) {
|
||||
let params = useParams();
|
||||
@@ -55,7 +56,7 @@ function ReviewPage(props) {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Review Page</h3>
|
||||
<h3>{Tr("Review Page")}</h3>
|
||||
<div>
|
||||
{reviews.map((review) => (
|
||||
<ReviewEntry key={review.id} review={review} user={props.user} />
|
||||
@@ -66,7 +67,7 @@ function ReviewPage(props) {
|
||||
value={newReview}
|
||||
onChange={(e) => setNewReview(e.target.value)}
|
||||
/>
|
||||
<button onClick={() => submitReview()}>Submit</button>
|
||||
<button onClick={() => submitReview()}>{Tr("Submit")}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "./Common";
|
||||
import FilesTable from "./FilesTable";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function SearchFiles(props) {
|
||||
const navigator = useNavigate();
|
||||
@@ -12,6 +13,7 @@ function SearchFiles(props) {
|
||||
const offset = parseInt(query.get("o")) || 0;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const limit = 10;
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function searchFiles() {
|
||||
// check empty filename
|
||||
@@ -57,7 +59,7 @@ function SearchFiles(props) {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Search Files</h3>
|
||||
<h3>{Tr("Search Files")}</h3>
|
||||
<div className="search_toolbar">
|
||||
<input
|
||||
onChange={(event) => setFilenameInput(event.target.value)}
|
||||
@@ -67,7 +69,7 @@ function SearchFiles(props) {
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
placeholder="Enter filename"
|
||||
placeholder={tr("Enter filename", langCode)}
|
||||
value={filenameInput}
|
||||
/>
|
||||
<button
|
||||
@@ -75,13 +77,13 @@ function SearchFiles(props) {
|
||||
navigator(`/files?q=${filenameInput}&o=0`);
|
||||
}}
|
||||
>
|
||||
{isLoading ? "Loading..." : "Search"}
|
||||
{isLoading ? Tr("Loading...") : Tr("Search")}
|
||||
</button>
|
||||
<button onClick={lastPage}>Last page</button>
|
||||
<button onClick={lastPage}>{Tr("Last page")}</button>
|
||||
<button disabled>
|
||||
{offset} - {offset + files.length}
|
||||
</button>
|
||||
<button onClick={nextPage}>Next page</button>
|
||||
<button onClick={nextPage}>{Tr("Next page")}</button>
|
||||
</div>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "./Common";
|
||||
import FoldersTable from "./FoldersTable";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function SearchFolders() {
|
||||
const navigator = useNavigate();
|
||||
@@ -12,6 +13,7 @@ function SearchFolders() {
|
||||
const offset = parseInt(query.get("o")) || 0;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const limit = 10;
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function searchFolder() {
|
||||
if (foldername === "") {
|
||||
@@ -55,7 +57,7 @@ function SearchFolders() {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Search Folders</h3>
|
||||
<h3>{Tr("Search Folders")}</h3>
|
||||
<div className="search_toolbar">
|
||||
<input
|
||||
onChange={(event) => setFoldernameInput(event.target.value)}
|
||||
@@ -65,7 +67,7 @@ function SearchFolders() {
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
placeholder="Enter folder name"
|
||||
placeholder={tr("Enter folder name", langCode)}
|
||||
value={foldernameInput}
|
||||
/>
|
||||
<button
|
||||
@@ -73,13 +75,13 @@ function SearchFolders() {
|
||||
navigator(`/folders?q=${foldernameInput}&o=0`);
|
||||
}}
|
||||
>
|
||||
{isLoading ? "Loading..." : "Search"}
|
||||
{isLoading ? Tr("Loading...") : Tr("Search")}
|
||||
</button>
|
||||
<button onClick={lastPage}>Last page</button>
|
||||
<button onClick={lastPage}>{Tr("Last page")}</button>
|
||||
<button disabled>
|
||||
{offset} - {offset + limit}
|
||||
</button>
|
||||
<button onClick={nextPage}>Next page</button>
|
||||
<button onClick={nextPage}>{Tr("Next page")}</button>
|
||||
</div>
|
||||
<FoldersTable folders={folders} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import FilesTable from "./FilesTable";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function Share(props) {
|
||||
let params = useParams();
|
||||
@@ -23,13 +24,14 @@ function Share(props) {
|
||||
}, [params]);
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Share with others!</h3>
|
||||
<h3>{Tr("Share with others!")}</h3>
|
||||
<p>
|
||||
👇 Click the filename below to enjoy music!
|
||||
<br />
|
||||
{Tr("Share link")}:{" "}
|
||||
<a href={window.location.href}>{window.location.href}</a>
|
||||
</p>
|
||||
<p>
|
||||
Share link: <a href={window.location.href}>{window.location.href}</a>
|
||||
👇 {Tr("Click the filename below to enjoy music!")}
|
||||
<br />
|
||||
</p>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={file} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function Tags() {
|
||||
const [tags, setTags] = useState([]);
|
||||
@@ -25,14 +26,14 @@ function Tags() {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Tags</h3>
|
||||
<h3>{Tr("Tags")}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Created By</th>
|
||||
<th>Actions</th>
|
||||
<th>{Tr("Name")}</th>
|
||||
<th>{Tr("Description")}</th>
|
||||
<th>{Tr("Created by")}</th>
|
||||
<th>{Tr("Action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -46,25 +47,25 @@ function Tags() {
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/manage/tags/${tag.id}`}>Edit</Link>
|
||||
<Link to={`/manage/tags/${tag.id}`}>{Tr("Edit")}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{!showAddTag && (
|
||||
<button onClick={() => setShowAddTag(true)}>Add Tag</button>
|
||||
<button onClick={() => setShowAddTag(true)}>{Tr("Add tag")}</button>
|
||||
)}
|
||||
{showAddTag && (
|
||||
<div>
|
||||
<label htmlFor="newTagName">New Tag Name</label>
|
||||
<label htmlFor="newTagName">{Tr("New Tag Name")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="newTagName"
|
||||
value={newTagName}
|
||||
onChange={(e) => setNewTagName(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="newTagDescription">New Tag Description</label>
|
||||
<label htmlFor="newTagDescription">{Tr("New Tag Description")}</label>
|
||||
<textarea
|
||||
id="newTagDescription"
|
||||
value={newTagDescription}
|
||||
@@ -94,7 +95,7 @@ function Tags() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Create Tag
|
||||
{Tr("Create tag")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import ReviewEntry from "./ReviewEntry";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function UserProfile(props) {
|
||||
let params = useParams();
|
||||
@@ -15,6 +16,7 @@ function UserProfile(props) {
|
||||
active: false,
|
||||
avatar_id: 0,
|
||||
});
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function getReviews() {
|
||||
fetch("/api/v1/get_reviews_by_user", {
|
||||
@@ -63,7 +65,7 @@ function UserProfile(props) {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>User Profile</h3>
|
||||
<h3>{Tr("User Profile")}</h3>
|
||||
<div className="horizontal">
|
||||
<input
|
||||
type="text"
|
||||
@@ -103,26 +105,26 @@ function UserProfile(props) {
|
||||
}}
|
||||
disabled={props.user.id !== user.id && props.user.role !== 1}
|
||||
>
|
||||
Save Username
|
||||
{Tr("Save username")}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
value={oldPassword}
|
||||
placeholder="Old Password"
|
||||
placeholder={tr("Old password", langCode)}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
placeholder="New Password"
|
||||
placeholder={tr("New password", langCode)}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={newPasswordConfirm}
|
||||
placeholder="Confirm New Password"
|
||||
placeholder={tr("Confirm new password", langCode)}
|
||||
onChange={(e) => setNewPasswordConfirm(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
@@ -144,7 +146,7 @@ function UserProfile(props) {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert("Password updated successfully!");
|
||||
alert(tr("Password updated successfully!", langCode));
|
||||
}
|
||||
});
|
||||
}}
|
||||
@@ -154,10 +156,10 @@ function UserProfile(props) {
|
||||
newPassword.length === 0
|
||||
}
|
||||
>
|
||||
Change Password
|
||||
{Tr("Change password")}
|
||||
</button>
|
||||
</div>
|
||||
<h4>Reviews</h4>
|
||||
<h4>{Tr("Reviews")}</h4>
|
||||
{reviews.map((review) => (
|
||||
<ReviewEntry key={review.id} review={review} user={props.user} />
|
||||
))}
|
||||
|
||||
44
web/src/translate/index.js
Normal file
44
web/src/translate/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createContext, renderToString } from "react";
|
||||
import MAP_zh_CN from "./zh_CN";
|
||||
|
||||
const LANG_OPTIONS = {
|
||||
"en-US": {
|
||||
name: "English",
|
||||
langMap: {},
|
||||
matches: ["en-US", "en"],
|
||||
},
|
||||
"zh-CN": {
|
||||
name: "中文(简体)",
|
||||
langMap: MAP_zh_CN,
|
||||
matches: ["zh-CN", "zh"],
|
||||
},
|
||||
};
|
||||
|
||||
const langCodeContext = createContext("en-US");
|
||||
|
||||
function tr(text, langCode) {
|
||||
const option = LANG_OPTIONS[langCode];
|
||||
if (option === undefined) {
|
||||
return text;
|
||||
}
|
||||
const langMap = LANG_OPTIONS[langCode].langMap;
|
||||
|
||||
const translatedText = langMap[text.toLowerCase()];
|
||||
if (translatedText === undefined) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return translatedText;
|
||||
}
|
||||
|
||||
function Tr(text) {
|
||||
return (
|
||||
<langCodeContext.Consumer>
|
||||
{({ langCode }) => {
|
||||
return tr(text, langCode);
|
||||
}}
|
||||
</langCodeContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
export { tr, Tr, LANG_OPTIONS, langCodeContext };
|
||||
105
web/src/translate/zh_CN.js
Normal file
105
web/src/translate/zh_CN.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const LANG_zh_CN = {
|
||||
"feeling luckly": "随机",
|
||||
files: "文件",
|
||||
folders: "文件夹",
|
||||
manage: "管理",
|
||||
"manage user": "用户管理",
|
||||
active: "激活",
|
||||
"search files": "搜索文件",
|
||||
"search folders": "搜索文件夹",
|
||||
"enter filename": "输入文件名",
|
||||
"enter folder name": "输入文件名",
|
||||
search: "搜索",
|
||||
"last page": "上一页",
|
||||
all: "全部",
|
||||
"loading...": "加载中...",
|
||||
"next page": "下一页",
|
||||
"search polders": "搜索文件夹",
|
||||
"share with others!": "分享给好友!",
|
||||
"click the filename below to enjoy music!": "点击下面的文件名开始享受音乐!",
|
||||
"share link": "分享链接",
|
||||
hi: "您好",
|
||||
profile: "个人信息",
|
||||
"user profile": "用户信息",
|
||||
"save username": "更改用户名",
|
||||
save: "保存",
|
||||
reset: "重置",
|
||||
"old password": "旧密码",
|
||||
"new password": "新密码",
|
||||
"confirm new password": "确认新密码",
|
||||
"change password": "更改密码",
|
||||
reviews: "评论",
|
||||
review: "评论",
|
||||
on: "在",
|
||||
edit: "编辑",
|
||||
"modified on": "修改于",
|
||||
share: "分享",
|
||||
delete: "删除",
|
||||
remove: "移除",
|
||||
"file details": "文件详情",
|
||||
download: "下载",
|
||||
logout: "登出",
|
||||
tags: "标签",
|
||||
"add tag": "添加标签",
|
||||
"select a tag": "选择一个标签",
|
||||
"review page": "评论页面",
|
||||
submit: "提交",
|
||||
users: "用户",
|
||||
feedbacks: "反馈",
|
||||
feedback: "反馈",
|
||||
date: "时间",
|
||||
action: "操作",
|
||||
"new tag name": "新标签名",
|
||||
"new tag description": "新标签描述",
|
||||
"update database": "更新索引",
|
||||
"updating...": "更新中...",
|
||||
refresh: "刷新",
|
||||
filename: "文件名",
|
||||
"folder name": "文件夹名",
|
||||
size: "大小",
|
||||
"player status": "播放状态",
|
||||
play: "播放",
|
||||
stop: "停止",
|
||||
"stop timer": "定时停止",
|
||||
loop: "循环",
|
||||
raw: "无损",
|
||||
prepare: "预转码",
|
||||
"file size": "文件大小",
|
||||
login: "登陆",
|
||||
register: "注册",
|
||||
"play: play using browser player.": "播放: 使用浏览器播放",
|
||||
"info for more actions.": "详细: 查看更多相关信息",
|
||||
info: "详细",
|
||||
close: "关闭",
|
||||
"please enter username and password": "请输入用户名和密码",
|
||||
username: "用户名",
|
||||
password: "密码",
|
||||
"please fill out all fields": "请完整填写所有信息",
|
||||
"password do not match": "两次密码不一致",
|
||||
"password updated successfully!": "密码已成功更新!",
|
||||
role: "身份",
|
||||
user: "用户",
|
||||
admin: "管理员",
|
||||
anonymous: "匿名",
|
||||
"select a role": "选择身份",
|
||||
"walk path": "遍历目录",
|
||||
"pattern wav flac mp3": "拓展名 wav flac mp3",
|
||||
"review updated": "已修改评论",
|
||||
"review deleted": "已删除评论",
|
||||
"edit review": "编辑评论",
|
||||
view: "查看",
|
||||
"tag updated successfully": "标签修改成功",
|
||||
"tag deleted successfully": "标签删除成功",
|
||||
"edit tag": "编辑标签",
|
||||
id: "编号",
|
||||
"created by": "创建者",
|
||||
"create tag": "创建新标签",
|
||||
name: "名称",
|
||||
description: "描述",
|
||||
"are you sure you want to delete this file?": "你确定要删除这个文件吗?",
|
||||
"filename updated": "已修改文件名",
|
||||
"please select a tag": "请选择一个标签",
|
||||
"files in folder": "文件夹内",
|
||||
};
|
||||
|
||||
export default LANG_zh_CN;
|
||||
Reference in New Issue
Block a user