updated chart.js, webpack. added js docs, added js to sphinx
This commit is contained in:
parent
a33d91dba4
commit
4fc4676041
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@ -83,6 +83,20 @@ jobs:
|
|||||||
- name: Install Python Dependencies
|
- name: Install Python Dependencies
|
||||||
run: poetry install
|
run: poetry install
|
||||||
|
|
||||||
|
# JS setup for jsdoc
|
||||||
|
- name: Install Node ${{ matrix.node }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
|
# JS setup for jsdoc
|
||||||
|
- name: Install jsdoc
|
||||||
|
run: npm install jsdoc
|
||||||
|
|
||||||
|
# JS setup for jsdoc
|
||||||
|
- name: Add node_modules/.bin to PATH
|
||||||
|
run: echo "${GITHUB_WORKSPACE}/node_modules/.bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
# DEPLOY for setting up cloud API
|
# DEPLOY for setting up cloud API
|
||||||
- name: Set up Cloud SDK
|
- name: Set up Cloud SDK
|
||||||
uses: google-github-actions/setup-gcloud@master
|
uses: google-github-actions/setup-gcloud@master
|
||||||
|
@ -28,7 +28,10 @@ author = 'Sarsoo'
|
|||||||
# Add any Sphinx extension module names here, as strings. They can be
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.napoleon']
|
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.napoleon', 'sphinx_js']
|
||||||
|
|
||||||
|
js_source_path = '../src/js'
|
||||||
|
jsdoc_config_path = 'jsdoc.json'
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
|
@ -13,6 +13,7 @@ Music Tools
|
|||||||
src/music.db
|
src/music.db
|
||||||
src/music.model
|
src/music.model
|
||||||
src/music.tasks
|
src/music.tasks
|
||||||
|
src/MusicTools
|
||||||
|
|
||||||
`Music Tools <https://music.sarsoo.xyz>`_
|
`Music Tools <https://music.sarsoo.xyz>`_
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
|
5
docs/jsdoc.json
Normal file
5
docs/jsdoc.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"opts": {
|
||||||
|
"recurse": true
|
||||||
|
}
|
||||||
|
}
|
6
docs/src/MusicTools.MusicTools.rst
Normal file
6
docs/src/MusicTools.MusicTools.rst
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
MusicTools
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. js:autoclass:: MusicTools
|
||||||
|
:members:
|
||||||
|
:private-members:
|
61
docs/src/MusicTools.Playlist.rst
Normal file
61
docs/src/MusicTools.Playlist.rst
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
Playlist
|
||||||
|
=================
|
||||||
|
|
||||||
|
Router
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. js:autoclass:: Playlists
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
Playlists List
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. js:autoclass:: PlaylistsView
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
.. js:autoclass:: PlaylistGrid
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
.. js:autoclass:: PlaylistCard
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
.. js:autofunction:: getPlaylistLink
|
||||||
|
|
||||||
|
New Playlist Card
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. js:autoclass:: NewPlaylist
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
Playlist Router
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. js:autoclass:: PlaylistRouter.View
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
Playlist View
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. js:autoclass:: Edit
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
.. js:autofunction:: ReferenceEntry
|
||||||
|
|
||||||
|
.. js:autofunction:: Edit.ListBlock
|
||||||
|
|
||||||
|
.. js:autofunction:: Edit.BlockGridItem
|
||||||
|
|
||||||
|
Playlist Stats View
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. js:autoclass:: Count
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
30
docs/src/MusicTools.Tag.rst
Normal file
30
docs/src/MusicTools.Tag.rst
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
Tag
|
||||||
|
=================
|
||||||
|
|
||||||
|
Router
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. js:autoclass:: TagRouter
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
Tags List
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. js:autoclass:: TagList
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
.. js:autofunction:: TagGrid
|
||||||
|
|
||||||
|
.. js:autofunction:: TagCard
|
||||||
|
|
||||||
|
.. js:autofunction:: getTagLink
|
||||||
|
|
||||||
|
New Tag Card
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. js:autoclass:: NewTag
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
13
docs/src/MusicTools.rst
Normal file
13
docs/src/MusicTools.rst
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Music Tools React
|
||||||
|
===================
|
||||||
|
|
||||||
|
Subpackages
|
||||||
|
-----------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
|
||||||
|
MusicTools.MusicTools
|
||||||
|
MusicTools.Playlist
|
||||||
|
MusicTools.Tag
|
||||||
|
|
@ -5,3 +5,4 @@ music
|
|||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
|
||||||
music
|
music
|
||||||
|
MusicTools
|
||||||
|
8248
package-lock.json
generated
8248
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -22,9 +22,9 @@
|
|||||||
"@material-ui/core": "^4.11.3",
|
"@material-ui/core": "^4.11.3",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"chart.js": "^2.9.4",
|
"chart.js": "^3.3.2",
|
||||||
"react": "^16.14.0",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^17.0.2",
|
||||||
"react-router-dom": "^5.2.0"
|
"react-router-dom": "^5.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -35,9 +35,10 @@
|
|||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"css-loader": "^5.2.4",
|
"css-loader": "^5.2.4",
|
||||||
|
"jsdoc": "^3.6.7",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^5.38.1",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^4.7.2",
|
||||||
"webpack-merge": "^4.2.2"
|
"webpack-merge": "^4.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
poetry.lock
generated
33
poetry.lock
generated
@ -386,6 +386,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pyparsing = ">=2.0.2"
|
pyparsing = ">=2.0.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parsimonious"
|
||||||
|
version = "0.7.0"
|
||||||
|
description = "(Soon to be) the fastest pure-Python PEG parser I could muster"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
six = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf"
|
name = "protobuf"
|
||||||
version = "3.15.7"
|
version = "3.15.7"
|
||||||
@ -542,6 +553,19 @@ docs = ["sphinxcontrib-websupport"]
|
|||||||
lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"]
|
lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"]
|
||||||
test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"]
|
test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sphinx-js"
|
||||||
|
version = "3.1.2"
|
||||||
|
description = "Support for using Sphinx on JSDoc-documented JS code"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Jinja2 = ">2.0,<3.0"
|
||||||
|
parsimonious = ">=0.7.0,<0.8.0"
|
||||||
|
Sphinx = ">=3.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sphinxcontrib-applehelp"
|
name = "sphinxcontrib-applehelp"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@ -707,7 +731,7 @@ python-versions = "*"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
content-hash = "8331f6a2897615556a573dc92c00b403a8be64b3a45e4e1a6d1bfacb006c013e"
|
content-hash = "51d1e9070adab3c839b4b1c50ebd1dc0366354d56f6c1a9485af6d4bb362959f"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
alabaster = [
|
alabaster = [
|
||||||
@ -951,6 +975,9 @@ packaging = [
|
|||||||
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
|
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
|
||||||
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
|
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
|
||||||
]
|
]
|
||||||
|
parsimonious = [
|
||||||
|
{file = "parsimonious-0.7.0.tar.gz", hash = "sha256:396d424f64f834f9463e81ba79a331661507a21f1ed7b644f7f6a744006fd938"},
|
||||||
|
]
|
||||||
protobuf = [
|
protobuf = [
|
||||||
{file = "protobuf-3.15.7-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a14141d5c967362d2eedff8825d2b69cc36a5b3ed6b1f618557a04e58a3cf787"},
|
{file = "protobuf-3.15.7-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a14141d5c967362d2eedff8825d2b69cc36a5b3ed6b1f618557a04e58a3cf787"},
|
||||||
{file = "protobuf-3.15.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d54d78f621852ec4fdd1484d1263ca04d4bf5ffdf7abffdbb939e444b6ff3385"},
|
{file = "protobuf-3.15.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d54d78f621852ec4fdd1484d1263ca04d4bf5ffdf7abffdbb939e444b6ff3385"},
|
||||||
@ -1043,6 +1070,10 @@ sphinx = [
|
|||||||
{file = "Sphinx-3.5.3-py3-none-any.whl", hash = "sha256:3f01732296465648da43dec8fb40dc451ba79eb3e2cc5c6d79005fd98197107d"},
|
{file = "Sphinx-3.5.3-py3-none-any.whl", hash = "sha256:3f01732296465648da43dec8fb40dc451ba79eb3e2cc5c6d79005fd98197107d"},
|
||||||
{file = "Sphinx-3.5.3.tar.gz", hash = "sha256:ce9c228456131bab09a3d7d10ae58474de562a6f79abb3dc811ae401cf8c1abc"},
|
{file = "Sphinx-3.5.3.tar.gz", hash = "sha256:ce9c228456131bab09a3d7d10ae58474de562a6f79abb3dc811ae401cf8c1abc"},
|
||||||
]
|
]
|
||||||
|
sphinx-js = [
|
||||||
|
{file = "sphinx-js-3.1.2.tar.gz", hash = "sha256:04fe0d2fec6d39b505d70500d0132cfa0efc834760c9598048c1a9dbbc175732"},
|
||||||
|
{file = "sphinx_js-3.1.2-py2.py3-none-any.whl", hash = "sha256:4503accb74ba3a15e0e59e20ec18b15be1932b2c8e8b82e03ace39a415899785"},
|
||||||
|
]
|
||||||
sphinxcontrib-applehelp = [
|
sphinxcontrib-applehelp = [
|
||||||
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
|
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
|
||||||
{file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
|
{file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
|
||||||
|
@ -28,6 +28,7 @@ spotfm = { git = "https://github.com/Sarsoo/spotfm.git" }
|
|||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pylint = "^2.5.3"
|
pylint = "^2.5.3"
|
||||||
Sphinx = "^3.5.3"
|
Sphinx = "^3.5.3"
|
||||||
|
sphinx-js = "^3.1.2"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
@ -7,6 +7,9 @@ import Lock from "./Lock.js";
|
|||||||
import Functions from "./Functions.js";
|
import Functions from "./Functions.js";
|
||||||
import Tasks from "./Tasks.js";
|
import Tasks from "./Tasks.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin router component for hosting cards
|
||||||
|
*/
|
||||||
class Admin extends Component {
|
class Admin extends Component {
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -17,6 +20,11 @@ class Admin extends Component {
|
|||||||
this.handleChange = this.handleChange.bind(this);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle tab change event
|
||||||
|
* @param {*} e Event data
|
||||||
|
* @param {*} newValue New tab data
|
||||||
|
*/
|
||||||
handleChange(e, newValue){
|
handleChange(e, newValue){
|
||||||
this.setState({
|
this.setState({
|
||||||
tab: newValue
|
tab: newValue
|
||||||
@ -34,8 +42,13 @@ class Admin extends Component {
|
|||||||
centered
|
centered
|
||||||
width="50%"
|
width="50%"
|
||||||
>
|
>
|
||||||
|
{/* LOCK CARD */}
|
||||||
<Tab label="Lock Accounts" component={Link} to={`${this.props.match.url}/lock`} />
|
<Tab label="Lock Accounts" component={Link} to={`${this.props.match.url}/lock`} />
|
||||||
|
|
||||||
|
{/* FUNCTIONS CARD */}
|
||||||
<Tab label="Functions" component={Link} to={`${this.props.match.url}/functions`} />
|
<Tab label="Functions" component={Link} to={`${this.props.match.url}/functions`} />
|
||||||
|
|
||||||
|
{/* RUNNING TASKS CARD */}
|
||||||
<Tab label="Tasks" component={Link} to={`${this.props.match.url}/tasks`} />
|
<Tab label="Tasks" component={Link} to={`${this.props.match.url}/tasks`} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
@ -4,6 +4,9 @@ const axios = require('axios');
|
|||||||
import showMessage from "../Toast.js"
|
import showMessage from "../Toast.js"
|
||||||
import { Card, Button, ButtonGroup, CardContent, CardActions, Typography } from "@material-ui/core";
|
import { Card, Button, ButtonGroup, CardContent, CardActions, Typography } from "@material-ui/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin functions card component
|
||||||
|
*/
|
||||||
class Functions extends Component {
|
class Functions extends Component {
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -13,6 +16,10 @@ class Functions extends Component {
|
|||||||
this.runStats = this.runStats.bind(this);
|
this.runStats = this.runStats.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make run all playlists request of API
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
runAllUsers(event){
|
runAllUsers(event){
|
||||||
axios.get('/api/playlist/run/users')
|
axios.get('/api/playlist/run/users')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -23,6 +30,10 @@ class Functions extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make run stats request of API
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
runStats(event){
|
runStats(event){
|
||||||
axios.get('/api/spotfm/playlist/refresh/users')
|
axios.get('/api/spotfm/playlist/refresh/users')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -38,11 +49,17 @@ class Functions extends Component {
|
|||||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||||
<Card align="center">
|
<Card align="center">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
||||||
|
{/* TITLE */}
|
||||||
<Typography variant="h4" color="textPrimary">Admin Functions</Typography>
|
<Typography variant="h4" color="textPrimary">Admin Functions</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<ButtonGroup variant="contained" color="primary" className="full-width">
|
<ButtonGroup variant="contained" color="primary" className="full-width">
|
||||||
|
|
||||||
|
{/* RUN ALL PLAYLISTS BUTTON */}
|
||||||
<Button className="full-width button" onClick={this.runAllUsers}>Run All Users</Button>
|
<Button className="full-width button" onClick={this.runAllUsers}>Run All Users</Button>
|
||||||
|
|
||||||
|
{/* RUN STATS BUTTON */}
|
||||||
<Button className="full-width button" onClick={this.runStats}>Run Stats</Button>
|
<Button className="full-width button" onClick={this.runStats}>Run Stats</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
|
@ -13,6 +13,9 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account lock card component
|
||||||
|
*/
|
||||||
class Lock extends Component {
|
class Lock extends Component {
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -27,6 +30,9 @@ class Lock extends Component {
|
|||||||
this.handleLock = this.handleLock.bind(this);
|
this.handleLock = this.handleLock.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make user infor request of API
|
||||||
|
*/
|
||||||
getUserInfo(){
|
getUserInfo(){
|
||||||
axios.get('/api/users')
|
axios.get('/api/users')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -40,6 +46,12 @@ class Lock extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make lock request of API
|
||||||
|
* @param {*} event Event data
|
||||||
|
* @param {*} username Subject username
|
||||||
|
* @param {*} to_state Target lock state
|
||||||
|
*/
|
||||||
handleLock(event, username, to_state){
|
handleLock(event, username, to_state){
|
||||||
axios.post('/api/user', {
|
axios.post('/api/user', {
|
||||||
username: username,
|
username: username,
|
||||||
@ -60,8 +72,12 @@ class Lock extends Component {
|
|||||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||||
<Card align="center">
|
<Card align="center">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
||||||
|
{/* TITLE */}
|
||||||
<Typography variant="h4" color="textPrimary">Account Locks</Typography>
|
<Typography variant="h4" color="textPrimary">Account Locks</Typography>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
|
|
||||||
|
{/* ACCOUNT CARDS */}
|
||||||
{ this.state.accounts.map((account) => <Row account={account} handler={this.handleLock}
|
{ this.state.accounts.map((account) => <Row account={account} handler={this.handleLock}
|
||||||
key= {account.username}/>) }
|
key= {account.username}/>) }
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -72,16 +88,27 @@ class Lock extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid of account cards with lock buttons
|
||||||
|
* @param {*} props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
function Row(props){
|
function Row(props){
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
return (
|
return (
|
||||||
<Grid item xs={12} sm={3} md={2}>
|
<Grid item xs={12} sm={3} md={2}>
|
||||||
<Card variant="outlined" className={classes.root}>
|
<Card variant="outlined" className={classes.root}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
||||||
|
{/* USERNAME TITLE */}
|
||||||
<Typography variant="h5" color="textSecondary" className={classes.root}>{ props.account.username }</Typography>
|
<Typography variant="h5" color="textSecondary" className={classes.root}>{ props.account.username }</Typography>
|
||||||
|
|
||||||
|
{/* LAST LOGIN */}
|
||||||
<Typography variant="body2" color="textSecondary" className={classes.root}>{ props.account.last_login }</Typography>
|
<Typography variant="body2" color="textSecondary" className={classes.root}>{ props.account.last_login }</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
|
|
||||||
|
{/* LOCK BUTTON */}
|
||||||
<Button className="full-width" color="secondary" variant="contained" aria-label="delete" onClick={(e) => props.handler(e, props.account.username, !props.account.locked)}>
|
<Button className="full-width" color="secondary" variant="contained" aria-label="delete" onClick={(e) => props.handler(e, props.account.username, !props.account.locked)}>
|
||||||
{props.account.locked ? "Unlock" : "Lock"}
|
{props.account.locked ? "Unlock" : "Lock"}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -5,6 +5,9 @@ import { Card, CardContent, Typography, Grid } from '@material-ui/core';
|
|||||||
|
|
||||||
import showMessage from "../Toast.js"
|
import showMessage from "../Toast.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Running tasks card component
|
||||||
|
*/
|
||||||
class Tasks extends Component {
|
class Tasks extends Component {
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -17,6 +20,9 @@ class Tasks extends Component {
|
|||||||
this.getTasks();
|
this.getTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tasks from API
|
||||||
|
*/
|
||||||
getTasks(){
|
getTasks(){
|
||||||
var self = this;
|
var self = this;
|
||||||
axios.get('/api/admin/tasks')
|
axios.get('/api/admin/tasks')
|
||||||
@ -34,6 +40,8 @@ class Tasks extends Component {
|
|||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||||
|
|
||||||
|
{/* GRID OF TASK CARDS */}
|
||||||
<Grid container spacing={4}>
|
<Grid container spacing={4}>
|
||||||
{ this.state.tasks.map((entry) => <TaskType url={entry.url} count={entry.count} times={entry.scheduled_times} key={entry.url}/>)}
|
{ this.state.tasks.map((entry) => <TaskType url={entry.url} count={entry.count} times={entry.scheduled_times} key={entry.url}/>)}
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -42,6 +50,11 @@ class Tasks extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid of task cards
|
||||||
|
* @param {*} props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
function TaskType(props) {
|
function TaskType(props) {
|
||||||
return (
|
return (
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={12} sm={6} md={4}>
|
||||||
|
@ -2,6 +2,9 @@ import React, { Component } from "react";
|
|||||||
|
|
||||||
import { Card, CardContent, Typography, Grid } from '@material-ui/core';
|
import { Card, CardContent, Typography, Grid } from '@material-ui/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Into card for the home page
|
||||||
|
*/
|
||||||
class Index extends Component{
|
class Index extends Component{
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
var Chart = require('chart.js');
|
import { Chart, BarElement, BarController, LinearScale, CategoryScale, Legend, Title, Tooltip } from 'chart.js';
|
||||||
|
|
||||||
|
Chart.register(BarElement, BarController, LinearScale, CategoryScale, Legend, Title, Tooltip);
|
||||||
|
|
||||||
class BarChart extends Component {
|
class BarChart extends Component {
|
||||||
|
|
||||||
@ -20,30 +22,39 @@ class BarChart extends Component {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
legend : {
|
indexAxis: this.props.indexAxis, // vertical or horizontal bars
|
||||||
display : false
|
plugins: {
|
||||||
|
legend : {
|
||||||
|
display : true,
|
||||||
|
labels: {
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
elements: {
|
elements: {
|
||||||
rectangle : {
|
bar : {
|
||||||
backgroundColor: 'rgb(255, 255, 255)'
|
backgroundColor: 'rgb(255, 255, 255)',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: 'rgb(0, 0, 0)'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
yAxes: [{
|
y: {
|
||||||
ticks: {
|
ticks: {
|
||||||
fontColor: "#d8d8d8",
|
color: "#d8d8d8",
|
||||||
fontSize: 16,
|
font: {
|
||||||
stepSize: 1,
|
size: 20
|
||||||
beginAtZero: true
|
}
|
||||||
}
|
}
|
||||||
}],
|
},
|
||||||
xAxes: [{
|
x: {
|
||||||
ticks: {
|
ticks: {
|
||||||
fontColor: "#d8d8d8",
|
color: "#d8d8d8",
|
||||||
fontSize: 16,
|
font: {
|
||||||
stepSize: 1
|
size: 16
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,15 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
var Chart = require('chart.js');
|
import { Chart, ArcElement, DoughnutController, Legend, Title, Tooltip } from 'chart.js';
|
||||||
|
|
||||||
|
Chart.register(ArcElement, DoughnutController, Legend, Title, Tooltip);
|
||||||
|
|
||||||
|
var pieColours = ['rgb(55, 61, 255)', //blue
|
||||||
|
'rgb(255, 55, 61)', //red
|
||||||
|
'rgb(7, 211, 4)', //green
|
||||||
|
'rgb(228, 242, 31)', //yellow
|
||||||
|
'rgb(31, 242, 221)', //light blue
|
||||||
|
'rgb(242, 31, 235)', //pink
|
||||||
|
'rgb(242, 164, 31)'];
|
||||||
|
|
||||||
class PieChart extends Component {
|
class PieChart extends Component {
|
||||||
|
|
||||||
@ -20,36 +30,20 @@ class PieChart extends Component {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
legend : {
|
plugins: {
|
||||||
display : true,
|
legend : {
|
||||||
labels: {
|
display : true,
|
||||||
fontColor: 'white'
|
labels: {
|
||||||
}
|
color: 'white'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
padding: this.props.padding
|
||||||
},
|
},
|
||||||
elements: {
|
elements: {
|
||||||
arc : {
|
arc : {
|
||||||
backgroundColor: ['rgb(55, 61, 255)', //blue
|
backgroundColor: [...pieColours, ...pieColours, ...pieColours],
|
||||||
'rgb(255, 55, 61)', //red
|
|
||||||
'rgb(7, 211, 4)', //green
|
|
||||||
'rgb(228, 242, 31)', //yellow
|
|
||||||
'rgb(31, 242, 221)', //light blue
|
|
||||||
'rgb(242, 31, 235)', //pink
|
|
||||||
'rgb(242, 164, 31)', //orange
|
|
||||||
'rgb(55, 61, 255)', //blue
|
|
||||||
'rgb(255, 55, 61)', //red
|
|
||||||
'rgb(7, 211, 4)', //green
|
|
||||||
'rgb(228, 242, 31)', //yellow
|
|
||||||
'rgb(31, 242, 221)', //light blue
|
|
||||||
'rgb(242, 31, 235)', //pink
|
|
||||||
'rgb(242, 164, 31)', //orange
|
|
||||||
'rgb(55, 61, 255)', //blue
|
|
||||||
'rgb(255, 55, 61)', //red
|
|
||||||
'rgb(7, 211, 4)', //green
|
|
||||||
'rgb(228, 242, 31)', //yellow
|
|
||||||
'rgb(31, 242, 221)', //light blue
|
|
||||||
'rgb(242, 31, 235)', //pink
|
|
||||||
'rgb(242, 164, 31)' //orange
|
|
||||||
],
|
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: 'rgb(0, 0, 0)'
|
borderColor: 'rgb(0, 0, 0)'
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,9 @@ const LazyAdmin = React.lazy(() => import("./Admin/AdminRouter"))
|
|||||||
const LazyTags = React.lazy(() => import("./Tag/TagRouter"))
|
const LazyTags = React.lazy(() => import("./Tag/TagRouter"))
|
||||||
const LazyTag = React.lazy(() => import("./Tag/View"))
|
const LazyTag = React.lazy(() => import("./Tag/View"))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root component for app
|
||||||
|
*/
|
||||||
class MusicTools extends Component {
|
class MusicTools extends Component {
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -46,14 +49,23 @@ class MusicTools extends Component {
|
|||||||
this.setOpen = this.setOpen.bind(this);
|
this.setOpen = this.setOpen.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user info from API on load
|
||||||
|
*/
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.getUserInfo();
|
this.getUserInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel get user info request
|
||||||
|
*/
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.userInfoCancelToken.cancel();
|
this.userInfoCancelToken.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user info from API
|
||||||
|
*/
|
||||||
getUserInfo(){
|
getUserInfo(){
|
||||||
this.userInfoCancelToken = axios.CancelToken.source();
|
this.userInfoCancelToken = axios.CancelToken.source();
|
||||||
|
|
||||||
@ -72,6 +84,10 @@ class MusicTools extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether side app drawer is open
|
||||||
|
* @param {*} bool Open state of side drawer
|
||||||
|
*/
|
||||||
setOpen(bool){
|
setOpen(bool){
|
||||||
this.setState({
|
this.setState({
|
||||||
drawerOpen: bool
|
drawerOpen: bool
|
||||||
@ -82,16 +98,22 @@ class MusicTools extends Component {
|
|||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<ThemeProvider theme={GlobalTheme}>
|
<ThemeProvider theme={GlobalTheme}>
|
||||||
|
|
||||||
|
{/* TOP APP BAR */}
|
||||||
|
|
||||||
<AppBar position="static">
|
<AppBar position="static">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton edge="start" color="inherit" aria-label="menu" onClick={(e) => this.setOpen(true)}>
|
<IconButton edge="start" color="inherit" aria-label="menu" onClick={(e) => this.setOpen(true)}>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">
|
||||||
<Link to='/app/playlists' style={{textDecoration: 'none'}}>Music Tools</Link>
|
<Link to='/app/playlists' style={{textDecoration: 'none'}}>Music Tools</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
|
||||||
|
{/* MENU DRAWER */}
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
variant="persistent"
|
variant="persistent"
|
||||||
anchor="left"
|
anchor="left"
|
||||||
@ -99,9 +121,9 @@ class MusicTools extends Component {
|
|||||||
onClose={(e) => this.setOpen(false)}
|
onClose={(e) => this.setOpen(false)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<IconButton onClick={(e) => this.setOpen(false)}>
|
<IconButton onClick={(e) => this.setOpen(false)}>
|
||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div
|
<div
|
||||||
@ -110,32 +132,45 @@ class MusicTools extends Component {
|
|||||||
onKeyDown={(e) => this.setOpen(false)}
|
onKeyDown={(e) => this.setOpen(false)}
|
||||||
>
|
>
|
||||||
<List>
|
<List>
|
||||||
|
{/* HOME */}
|
||||||
<ListItem button key="home" component={Link} to='/app'>
|
<ListItem button key="home" component={Link} to='/app'>
|
||||||
<ListItemIcon><HomeIcon /></ListItemIcon>
|
<ListItemIcon><HomeIcon /></ListItemIcon>
|
||||||
<ListItemText primary="Home" />
|
<ListItemText primary="Home" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
{/* PLAYLISTS */}
|
||||||
<ListItem button key="playlists" component={Link} to='/app/playlists'>
|
<ListItem button key="playlists" component={Link} to='/app/playlists'>
|
||||||
<ListItemIcon><QueueMusic /></ListItemIcon>
|
<ListItemIcon><QueueMusic /></ListItemIcon>
|
||||||
<ListItemText primary="Playlists" />
|
<ListItemText primary="Playlists" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
{/* TAGS */}
|
||||||
<ListItem button key="tags" component={Link} to='/app/tags'>
|
<ListItem button key="tags" component={Link} to='/app/tags'>
|
||||||
<ListItemIcon><GroupWork /></ListItemIcon>
|
<ListItemIcon><GroupWork /></ListItemIcon>
|
||||||
<ListItemText primary="Tags" />
|
<ListItemText primary="Tags" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
{/* SETTINGS */}
|
||||||
<ListItem button key="settings" component={Link} to='/app/settings/password'>
|
<ListItem button key="settings" component={Link} to='/app/settings/password'>
|
||||||
<ListItemIcon><Build /></ListItemIcon>
|
<ListItemIcon><Build /></ListItemIcon>
|
||||||
<ListItemText primary="Settings" />
|
<ListItemText primary="Settings" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
{/* ADMIN */}
|
||||||
{ this.state.type == 'admin' &&
|
{ this.state.type == 'admin' &&
|
||||||
<ListItem button key="admin" component={Link} to='/app/admin/lock'>
|
<ListItem button key="admin" component={Link} to='/app/admin/lock'>
|
||||||
<ListItemIcon><AccountCircle /></ListItemIcon>
|
<ListItemIcon><AccountCircle /></ListItemIcon>
|
||||||
<ListItemText primary="Admin" />
|
<ListItemText primary="Admin" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{/* LOGOUT */}
|
||||||
<ListItem button key="logout" onClick={(e) => { window.location.href = '/auth/logout' }}>
|
<ListItem button key="logout" onClick={(e) => { window.location.href = '/auth/logout' }}>
|
||||||
<ListItemIcon><KeyboardBackspace /></ListItemIcon>
|
<ListItemIcon><KeyboardBackspace /></ListItemIcon>
|
||||||
<ListItemText primary="Logout" />
|
<ListItemText primary="Logout" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
{/* SARSOO.XYZ */}
|
||||||
<ListItem button key="sarsoo.xyz" onClick={(e) => { window.location.href = 'https://sarsoo.xyz' }}>
|
<ListItem button key="sarsoo.xyz" onClick={(e) => { window.location.href = 'https://sarsoo.xyz' }}>
|
||||||
<ListItemIcon><ExitToApp /></ListItemIcon>
|
<ListItemIcon><ExitToApp /></ListItemIcon>
|
||||||
<ListItemText primary="sarsoo.xyz" />
|
<ListItemText primary="sarsoo.xyz" />
|
||||||
@ -143,6 +178,9 @@ class MusicTools extends Component {
|
|||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
{/* ROUTER SWITCH */}
|
||||||
|
|
||||||
<div className="full-width">
|
<div className="full-width">
|
||||||
<Switch>
|
<Switch>
|
||||||
<React.Suspense fallback={<LoadingMessage/>}>
|
<React.Suspense fallback={<LoadingMessage/>}>
|
||||||
|
@ -4,12 +4,19 @@ import { Route, Switch } from "react-router-dom";
|
|||||||
import PlaylistsView from "./PlaylistsList.js"
|
import PlaylistsView from "./PlaylistsList.js"
|
||||||
import NewPlaylist from "./New.js";
|
import NewPlaylist from "./New.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router for playlist lists page, includes new playlist page
|
||||||
|
*/
|
||||||
class Playlists extends Component {
|
class Playlists extends Component {
|
||||||
render(){
|
render(){
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|
||||||
|
{/* PLAYLIST LIST */}
|
||||||
<Route exact path={`${this.props.match.url}/`} component={PlaylistsView} />
|
<Route exact path={`${this.props.match.url}/`} component={PlaylistsView} />
|
||||||
|
|
||||||
|
{/* NEW PLAYLIST */}
|
||||||
<Route path={`${this.props.match.url}/new`} component={NewPlaylist} />
|
<Route path={`${this.props.match.url}/new`} component={NewPlaylist} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,9 @@ import { Card, Button, FormControl, TextField, InputLabel, Select, CardActions,
|
|||||||
|
|
||||||
import showMessage from "../Toast.js"
|
import showMessage from "../Toast.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New playlist card
|
||||||
|
*/
|
||||||
class NewPlaylist extends Component {
|
class NewPlaylist extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -18,10 +21,15 @@ class NewPlaylist extends Component {
|
|||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set initial state of playlist type description */
|
||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
this.setDescription('default');
|
this.setDescription('default');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set playlist type description
|
||||||
|
* @param {*} value Playlist type string to match
|
||||||
|
*/
|
||||||
setDescription(value){
|
setDescription(value){
|
||||||
switch(value){
|
switch(value){
|
||||||
case 'default':
|
case 'default':
|
||||||
@ -42,6 +50,10 @@ class NewPlaylist extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle input changes by setting state
|
||||||
|
* @param {*} event
|
||||||
|
*/
|
||||||
handleInputChange(event){
|
handleInputChange(event){
|
||||||
this.setState({
|
this.setState({
|
||||||
[event.target.name]: event.target.value
|
[event.target.name]: event.target.value
|
||||||
@ -49,6 +61,10 @@ class NewPlaylist extends Component {
|
|||||||
this.setDescription(event.target.value);
|
this.setDescription(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate input and make new playlist API request
|
||||||
|
* @param {*} event
|
||||||
|
*/
|
||||||
handleSubmit(event){
|
handleSubmit(event){
|
||||||
var name = this.state.name;
|
var name = this.state.name;
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -92,9 +108,13 @@ class NewPlaylist extends Component {
|
|||||||
<Card align="center">
|
<Card align="center">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid container spacing={5}>
|
<Grid container spacing={5}>
|
||||||
|
|
||||||
|
{/* TITLE */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="h3">New</Typography>
|
<Typography variant="h3">New</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* PLAYLIST TYPE DROPDOWN */}
|
||||||
<Grid item xs={12} sm={4}>
|
<Grid item xs={12} sm={4}>
|
||||||
<FormControl variant="filled">
|
<FormControl variant="filled">
|
||||||
<InputLabel htmlFor="type-select">Type</InputLabel>
|
<InputLabel htmlFor="type-select">Type</InputLabel>
|
||||||
@ -114,6 +134,8 @@ class NewPlaylist extends Component {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* PLAYLIST NAME TEXTBOX */}
|
||||||
<Grid item xs={12} sm={8}>
|
<Grid item xs={12} sm={8}>
|
||||||
<TextField
|
<TextField
|
||||||
label="Name"
|
label="Name"
|
||||||
@ -123,11 +145,15 @@ class NewPlaylist extends Component {
|
|||||||
value={this.state.name}
|
value={this.state.name}
|
||||||
className="full-width" />
|
className="full-width" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* PLAYLIST DESCRIPTION TEXT */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="body2" color="textSecondary">{ this.state.description }</Typography>
|
<Typography variant="body2" color="textSecondary">{ this.state.description }</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* SUBMIT BUTTON */}
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Button variant="contained" color="primary" className="full-width" onClick={this.handleSubmit}>Create</Button>
|
<Button variant="contained" color="primary" className="full-width" onClick={this.handleSubmit}>Create</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
|
@ -7,8 +7,15 @@ const axios = require('axios');
|
|||||||
|
|
||||||
import showMessage from "../Toast.js"
|
import showMessage from "../Toast.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top-level object for hosting playlist card grid with new/run all buttons
|
||||||
|
*/
|
||||||
class PlaylistsView extends Component {
|
class PlaylistsView extends Component {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger loading playlist data during init
|
||||||
|
* @param {*} props Component properties
|
||||||
|
*/
|
||||||
constructor(props){
|
constructor(props){
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -20,6 +27,9 @@ class PlaylistsView extends Component {
|
|||||||
this.handleRunAll = this.handleRunAll.bind(this);
|
this.handleRunAll = this.handleRunAll.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get playlist data from API and set state with results
|
||||||
|
*/
|
||||||
getPlaylists(){
|
getPlaylists(){
|
||||||
var self = this;
|
var self = this;
|
||||||
axios.get('/api/playlists')
|
axios.get('/api/playlists')
|
||||||
@ -43,6 +53,11 @@ class PlaylistsView extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post run playlist action to API
|
||||||
|
* @param {*} name Playlist name to run
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleRunPlaylist(name, event){
|
handleRunPlaylist(name, event){
|
||||||
axios.get('/api/user')
|
axios.get('/api/user')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -62,6 +77,11 @@ class PlaylistsView extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post delete playlist action to API
|
||||||
|
* @param {*} name Playlist name to delete
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleDeletePlaylist(name, event){
|
handleDeletePlaylist(name, event){
|
||||||
axios.delete('/api/playlist', { params: { name: name } })
|
axios.delete('/api/playlist', { params: { name: name } })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -72,6 +92,10 @@ class PlaylistsView extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post run all playlists action to API
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleRunAll(event){
|
handleRunAll(event){
|
||||||
axios.get('/api/user')
|
axios.get('/api/user')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -93,6 +117,8 @@ class PlaylistsView extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
|
// Show spinning loading circle until loaded playlist data
|
||||||
|
|
||||||
const grid = <PlaylistGrid playlists={this.state.playlists}
|
const grid = <PlaylistGrid playlists={this.state.playlists}
|
||||||
handleRunPlaylist={this.handleRunPlaylist}
|
handleRunPlaylist={this.handleRunPlaylist}
|
||||||
handleDeletePlaylist={this.handleDeletePlaylist}
|
handleDeletePlaylist={this.handleDeletePlaylist}
|
||||||
@ -102,6 +128,11 @@ class PlaylistsView extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playlist grid component for new/run all buttons and playlist cards
|
||||||
|
* @param {*} props Component properties
|
||||||
|
* @returns Grid component
|
||||||
|
*/
|
||||||
function PlaylistGrid(props){
|
function PlaylistGrid(props){
|
||||||
return (
|
return (
|
||||||
<Grid container
|
<Grid container
|
||||||
@ -110,6 +141,8 @@ function PlaylistGrid(props){
|
|||||||
justify="flex-start"
|
justify="flex-start"
|
||||||
alignItems="flex-start"
|
alignItems="flex-start"
|
||||||
style={{padding: '24px'}}>
|
style={{padding: '24px'}}>
|
||||||
|
|
||||||
|
{/* BUTTON BLOCK (NEW/RUN ALL) */}
|
||||||
<Grid item xs={12} sm={6} md={2}>
|
<Grid item xs={12} sm={6} md={2}>
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -119,6 +152,8 @@ function PlaylistGrid(props){
|
|||||||
<Button onClick={props.handleRunAll}>Run All</Button>
|
<Button onClick={props.handleRunAll}>Run All</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* PLAYLIST CARDS */}
|
||||||
{ props.playlists.length == 0 ? (
|
{ props.playlists.length == 0 ? (
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
<Grid item xs={12} sm={6} md={3}>
|
||||||
<Typography variant="h5" component="h2">No Playlists</Typography>
|
<Typography variant="h5" component="h2">No Playlists</Typography>
|
||||||
@ -133,21 +168,36 @@ function PlaylistGrid(props){
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playlist card component with view/run/delete buttons
|
||||||
|
* @param {*} props Component properties
|
||||||
|
* @returns Playlist card component
|
||||||
|
*/
|
||||||
function PlaylistCard(props){
|
function PlaylistCard(props){
|
||||||
return (
|
return (
|
||||||
<Grid item xs>
|
<Grid item xs>
|
||||||
<Card>
|
<Card>
|
||||||
|
|
||||||
|
{/* NAME TITLE */}
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h4" component="h2">
|
<Typography variant="h4" component="h2">
|
||||||
{ props.playlist.name }
|
{ props.playlist.name }
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* BUTTONS */}
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="contained">
|
variant="contained">
|
||||||
|
|
||||||
|
{/* VIEW */}
|
||||||
<Button component={Link} to={getPlaylistLink(props.playlist.name)}>View</Button>
|
<Button component={Link} to={getPlaylistLink(props.playlist.name)}>View</Button>
|
||||||
|
|
||||||
|
{/* RUN */}
|
||||||
<Button onClick={(e) => props.handleRunPlaylist(props.playlist.name, e)}>Run</Button>
|
<Button onClick={(e) => props.handleRunPlaylist(props.playlist.name, e)}>Run</Button>
|
||||||
|
|
||||||
|
{/* DELETE */}
|
||||||
<Button onClick={(e) => props.handleDeletePlaylist(props.playlist.name, e)}>Delete</Button>
|
<Button onClick={(e) => props.handleDeletePlaylist(props.playlist.name, e)}>Delete</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
@ -156,6 +206,11 @@ function PlaylistCard(props){
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get URL for playlist given name
|
||||||
|
* @param {*} playlistName Subject playlist name
|
||||||
|
* @returns URL string
|
||||||
|
*/
|
||||||
function getPlaylistLink(playlistName){
|
function getPlaylistLink(playlistName){
|
||||||
return `/app/playlist/${playlistName}/edit`;
|
return `/app/playlist/${playlistName}/edit`;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,9 @@ import showMessage from "../../Toast.js"
|
|||||||
|
|
||||||
const LazyPieChart = React.lazy(() => import("../../Maths/PieChart"))
|
const LazyPieChart = React.lazy(() => import("../../Maths/PieChart"))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playlist count tab for presenting listening stats
|
||||||
|
*/
|
||||||
export class Count extends Component {
|
export class Count extends Component {
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -34,6 +37,9 @@ export class Count extends Component {
|
|||||||
this.updateStats = this.updateStats.bind(this);
|
this.updateStats = this.updateStats.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get playlist info with stats from API and set state if user has Last.fm username
|
||||||
|
*/
|
||||||
getUserInfo(){
|
getUserInfo(){
|
||||||
axios.get(`/api/playlist?name=${ this.state.name }`)
|
axios.get(`/api/playlist?name=${ this.state.name }`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -51,6 +57,9 @@ export class Count extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make stats refresh request of API
|
||||||
|
*/
|
||||||
updateStats(){
|
updateStats(){
|
||||||
axios.get(`/api/spotfm/playlist/refresh?name=${ this.state.name }`)
|
axios.get(`/api/spotfm/playlist/refresh?name=${ this.state.name }`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -71,19 +80,29 @@ export class Count extends Component {
|
|||||||
<Card align="center">
|
<Card align="center">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
|
|
||||||
|
{/* SCROBBLE COUNT */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="body2">Scrobble Count: <b>{this.state.playlist.lastfm_stat_count.toLocaleString()} / {this.state.playlist.lastfm_stat_percent}%</b></Typography>
|
<Typography variant="body2">Scrobble Count: <b>{this.state.playlist.lastfm_stat_count.toLocaleString()} / {this.state.playlist.lastfm_stat_percent}%</b></Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* ALBUM COUNT */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="body2">Album Count: <b>{this.state.playlist.lastfm_stat_album_count.toLocaleString()} / {this.state.playlist.lastfm_stat_album_percent}%</b></Typography>
|
<Typography variant="body2">Album Count: <b>{this.state.playlist.lastfm_stat_album_count.toLocaleString()} / {this.state.playlist.lastfm_stat_album_percent}%</b></Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* ARTIST COUNT */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="body2">Artist Count: <b>{this.state.playlist.lastfm_stat_artist_count.toLocaleString()} / {this.state.playlist.lastfm_stat_artist_percent}%</b></Typography>
|
<Typography variant="body2">Artist Count: <b>{this.state.playlist.lastfm_stat_artist_count.toLocaleString()} / {this.state.playlist.lastfm_stat_artist_percent}%</b></Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* LAST UPDATED */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="body2">Last Updated <b>{this.state.playlist.lastfm_stat_last_refresh.toLocaleString()}</b></Typography>
|
<Typography variant="body2">Last Updated <b>{this.state.playlist.lastfm_stat_last_refresh.toLocaleString()}</b></Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<React.Suspense fallback={<LoadingMessage/>}>
|
<React.Suspense fallback={<LoadingMessage/>}>
|
||||||
|
|
||||||
|
{/* TRACK PIE */}
|
||||||
<Grid item xs={12} sm={12} md={4}>
|
<Grid item xs={12} sm={12} md={4}>
|
||||||
<LazyPieChart data={[{
|
<LazyPieChart data={[{
|
||||||
"label": `${this.state.playlist.name} Tracks`,
|
"label": `${this.state.playlist.name} Tracks`,
|
||||||
@ -92,8 +111,11 @@ export class Count extends Component {
|
|||||||
"label": 'Other',
|
"label": 'Other',
|
||||||
"value": 100 - this.state.playlist.lastfm_stat_percent
|
"value": 100 - this.state.playlist.lastfm_stat_percent
|
||||||
}]}
|
}]}
|
||||||
title={this.state.playlist.name}/>
|
title={this.state.playlist.name}
|
||||||
|
padding={50}/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* ALBUM PIE */}
|
||||||
<Grid item xs={12} sm={12} md={4}>
|
<Grid item xs={12} sm={12} md={4}>
|
||||||
<LazyPieChart data={[{
|
<LazyPieChart data={[{
|
||||||
"label": `${this.state.playlist.name} Albums`,
|
"label": `${this.state.playlist.name} Albums`,
|
||||||
@ -102,8 +124,11 @@ export class Count extends Component {
|
|||||||
"label": 'Other',
|
"label": 'Other',
|
||||||
"value": 100 - this.state.playlist.lastfm_stat_album_percent
|
"value": 100 - this.state.playlist.lastfm_stat_album_percent
|
||||||
}]}
|
}]}
|
||||||
title={this.state.playlist.name}/>
|
title={this.state.playlist.name}
|
||||||
|
padding={50}/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* ARTIST PIE */}
|
||||||
<Grid item xs={12} sm={12} md={4}>
|
<Grid item xs={12} sm={12} md={4}>
|
||||||
<LazyPieChart data={[{
|
<LazyPieChart data={[{
|
||||||
"label": `${this.state.playlist.name} Artists`,
|
"label": `${this.state.playlist.name} Artists`,
|
||||||
@ -112,11 +137,14 @@ export class Count extends Component {
|
|||||||
"label": 'Other',
|
"label": 'Other',
|
||||||
"value": 100 - this.state.playlist.lastfm_stat_artist_percent
|
"value": 100 - this.state.playlist.lastfm_stat_artist_percent
|
||||||
}]}
|
}]}
|
||||||
title={this.state.playlist.name}/>
|
title={this.state.playlist.name}
|
||||||
|
padding={50}/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</Grid>
|
</Grid>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* UPDATE BUTTON */}
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Button variant="contained" color="primary" className="full-width" onClick={this.updateStats}>Update</Button>
|
<Button variant="contained" color="primary" className="full-width" onClick={this.updateStats}>Update</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
|
@ -46,6 +46,9 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main view/edit card for playlists
|
||||||
|
*/
|
||||||
export class Edit extends Component{
|
export class Edit extends Component{
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -84,6 +87,9 @@ export class Edit extends Component{
|
|||||||
this.makeNetworkUpdate = this.makeNetworkUpdate.bind(this);
|
this.makeNetworkUpdate = this.makeNetworkUpdate.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get playlist info and all playlists from API, sort and set state
|
||||||
|
*/
|
||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
axios.all([this.getPlaylistInfo(), this.getPlaylists()])
|
axios.all([this.getPlaylistInfo(), this.getPlaylists()])
|
||||||
.then(axios.spread((info, playlists) => {
|
.then(axios.spread((info, playlists) => {
|
||||||
@ -120,14 +126,26 @@ export class Edit extends Component{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API playlist info request
|
||||||
|
* @returns Playlist info request
|
||||||
|
*/
|
||||||
getPlaylistInfo(){
|
getPlaylistInfo(){
|
||||||
return axios.get(`/api/playlist?name=${ this.state.name }`);
|
return axios.get(`/api/playlist?name=${ this.state.name }`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API list of playlist infos request
|
||||||
|
* @returns Playlists info request
|
||||||
|
*/
|
||||||
getPlaylists(){
|
getPlaylists(){
|
||||||
return axios.get(`/api/playlists`);
|
return axios.get(`/api/playlists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle input box state changes, make API requests
|
||||||
|
* @param {*} event
|
||||||
|
*/
|
||||||
handleInputChange(event){
|
handleInputChange(event){
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -164,6 +182,10 @@ export class Edit extends Component{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle checkbox state changes, make API requests
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleCheckChange(event){
|
handleCheckChange(event){
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -176,6 +198,10 @@ export class Edit extends Component{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send playlist info updates to API
|
||||||
|
* @param {*} changes Dictionary of changes to make
|
||||||
|
*/
|
||||||
makeNetworkUpdate(changes){
|
makeNetworkUpdate(changes){
|
||||||
let payload = {
|
let payload = {
|
||||||
name: this.state.name
|
name: this.state.name
|
||||||
@ -190,6 +216,10 @@ export class Edit extends Component{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle adding new watched Spotify playlist name string
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleAddPart(event){
|
handleAddPart(event){
|
||||||
|
|
||||||
if(this.state.newPlaylistName.length != 0){
|
if(this.state.newPlaylistName.length != 0){
|
||||||
@ -221,6 +251,10 @@ export class Edit extends Component{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle adding new watched music tools playlist reference
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleAddReference(event){
|
handleAddReference(event){
|
||||||
|
|
||||||
if(this.state.newReferenceName.length != 0){
|
if(this.state.newReferenceName.length != 0){
|
||||||
@ -255,6 +289,11 @@ export class Edit extends Component{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle removing watched Spotify playlist name string
|
||||||
|
* @param {*} id Subject playlist name
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleRemovePart(id, event){
|
handleRemovePart(id, event){
|
||||||
var parts = this.state.parts;
|
var parts = this.state.parts;
|
||||||
parts = parts.filter(e => e !== id);
|
parts = parts.filter(e => e !== id);
|
||||||
@ -269,6 +308,11 @@ export class Edit extends Component{
|
|||||||
this.makeNetworkUpdate({parts: parts});
|
this.makeNetworkUpdate({parts: parts});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle removing watched music tools playlist reference
|
||||||
|
* @param {*} id Subject playlist name
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleRemoveReference(id, event){
|
handleRemoveReference(id, event){
|
||||||
var playlist_references = this.state.playlist_references;
|
var playlist_references = this.state.playlist_references;
|
||||||
playlist_references = playlist_references.filter(e => e !== id);
|
playlist_references = playlist_references.filter(e => e !== id);
|
||||||
@ -283,6 +327,10 @@ export class Edit extends Component{
|
|||||||
this.makeNetworkUpdate({playlist_references: playlist_references});
|
this.makeNetworkUpdate({playlist_references: playlist_references});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle refreshing playlist action, checks for spotify link
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleRun(event){
|
handleRun(event){
|
||||||
axios.get('/api/user')
|
axios.get('/api/user')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -305,21 +353,29 @@ export class Edit extends Component{
|
|||||||
render(){
|
render(){
|
||||||
|
|
||||||
var date = new Date();
|
var date = new Date();
|
||||||
console.log("hello from edit");
|
|
||||||
|
|
||||||
const table = (
|
const table = (
|
||||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||||
<Card align="center">
|
<Card align="center">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
||||||
|
{/* PLAYLIST NAME TITLE */}
|
||||||
<Typography variant="h2" color="textPrimary">{this.state.name}</Typography>
|
<Typography variant="h2" color="textPrimary">{this.state.name}</Typography>
|
||||||
<Grid container spacing={5}>
|
<Grid container spacing={5}>
|
||||||
|
|
||||||
|
{/* MANAGED PLAYLISTS TITLE */}
|
||||||
{ this.state.playlist_references.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Managed</Typography></Grid> }
|
{ this.state.playlist_references.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Managed</Typography></Grid> }
|
||||||
|
{/* SMART PLAYLIST REFERENCES */}
|
||||||
{ this.state.playlist_references.length > 0 && <ListBlock handler={this.handleRemoveReference} list={this.state.playlist_references}/> }
|
{ this.state.playlist_references.length > 0 && <ListBlock handler={this.handleRemoveReference} list={this.state.playlist_references}/> }
|
||||||
|
|
||||||
|
{/* SPOTIFY PLALISTS TITLE */}
|
||||||
{ this.state.parts.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Spotify</Typography></Grid> }
|
{ this.state.parts.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Spotify</Typography></Grid> }
|
||||||
|
{/* SPOTIFY PLAYLIST REFERENCES */}
|
||||||
{ this.state.parts.length > 0 && <ListBlock handler={this.handleRemovePart} list={this.state.parts}/> }
|
{ this.state.parts.length > 0 && <ListBlock handler={this.handleRemovePart} list={this.state.parts}/> }
|
||||||
|
{/* SPOTIFY DESCRIPTION */}
|
||||||
<Grid item xs={12} ><Typography variant="body2" color="textSecondary">Spotify playlist can be the name of either your own created playlist or one you follow, names are case sensitive</Typography></Grid>
|
<Grid item xs={12} ><Typography variant="body2" color="textSecondary">Spotify playlist can be the name of either your own created playlist or one you follow, names are case sensitive</Typography></Grid>
|
||||||
|
|
||||||
|
{/* SPOTIFY PLAYLIST TEXTBOX */}
|
||||||
<Grid item xs={8} sm={8} md={3}>
|
<Grid item xs={8} sm={8} md={3}>
|
||||||
<TextField
|
<TextField
|
||||||
name="newPlaylistName"
|
name="newPlaylistName"
|
||||||
@ -330,9 +386,12 @@ export class Edit extends Component{
|
|||||||
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{/* SPOTIFY ADD BUTTON */}
|
||||||
<Grid item xs={4} sm={4} md={3}>
|
<Grid item xs={4} sm={4} md={3}>
|
||||||
<Button variant="contained" className="full-width" onClick={this.handleAddPart} style={{verticalAlign: 'middle'}}>Add</Button>
|
<Button variant="contained" className="full-width" onClick={this.handleAddPart} style={{verticalAlign: 'middle'}}>Add</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* SMART PLAYLIST DROPDOWN */}
|
||||||
<Grid item xs={8} sm={8} md={3}>
|
<Grid item xs={8} sm={8} md={3}>
|
||||||
<FormControl variant="filled" style={{verticalAlign: 'middle'}}>
|
<FormControl variant="filled" style={{verticalAlign: 'middle'}}>
|
||||||
<InputLabel htmlFor="chart_range">Managed Playlist</InputLabel>
|
<InputLabel htmlFor="chart_range">Managed Playlist</InputLabel>
|
||||||
@ -351,22 +410,31 @@ export class Edit extends Component{
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{/* SMART ADD BUTTON */}
|
||||||
<Grid item xs={4} sm={4} md={3}>
|
<Grid item xs={4} sm={4} md={3}>
|
||||||
<Button variant="contained" className="full-width" onClick={this.handleAddReference} style={{verticalAlign: 'middle'}}>Add</Button>
|
<Button variant="contained" className="full-width" onClick={this.handleAddReference} style={{verticalAlign: 'middle'}}>Add</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* CHECKBOXES */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
|
|
||||||
|
{/* SHUFFLE */}
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch color="primary" name="shuffle" checked={this.state.shuffle} onChange={this.handleCheckChange} />
|
<Switch color="primary" name="shuffle" checked={this.state.shuffle} onChange={this.handleCheckChange} />
|
||||||
}
|
}
|
||||||
labelPlacement="bottom"
|
labelPlacement="bottom"
|
||||||
label="Shuffle"/>
|
label="Shuffle"/>
|
||||||
|
|
||||||
|
{/* RECOMMENDATIONS */}
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch color="primary" checked={this.state.include_recommendations} name="include_recommendations" onChange={this.handleCheckChange} />
|
<Switch color="primary" checked={this.state.include_recommendations} name="include_recommendations" onChange={this.handleCheckChange} />
|
||||||
}
|
}
|
||||||
labelPlacement="bottom"
|
labelPlacement="bottom"
|
||||||
label="Recommendations"/>
|
label="Recommendations"/>
|
||||||
|
|
||||||
|
{/* LIBRARY TRACKS */}
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch color="primary" checked={this.state.include_library_tracks} name="include_library_tracks" onChange={this.handleCheckChange} />
|
<Switch color="primary" checked={this.state.include_library_tracks} name="include_library_tracks" onChange={this.handleCheckChange} />
|
||||||
@ -374,6 +442,8 @@ export class Edit extends Component{
|
|||||||
labelPlacement="bottom"
|
labelPlacement="bottom"
|
||||||
label="Library Tracks"/>
|
label="Library Tracks"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* NUMBER OF RECOMMENDATIONS */}
|
||||||
{ this.state.include_recommendations == true &&
|
{ this.state.include_recommendations == true &&
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<TextField type="number"
|
<TextField type="number"
|
||||||
@ -384,6 +454,8 @@ export class Edit extends Component{
|
|||||||
onChange={this.handleInputChange}></TextField>
|
onChange={this.handleInputChange}></TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{/* LAST.FM CHART LENGTH */}
|
||||||
{ this.state.type == 'fmchart' &&
|
{ this.state.type == 'fmchart' &&
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<TextField type="number"
|
<TextField type="number"
|
||||||
@ -394,6 +466,8 @@ export class Edit extends Component{
|
|||||||
onChange={this.handleInputChange}></TextField>
|
onChange={this.handleInputChange}></TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{/* LAST.FM CHART TIME RANGE */}
|
||||||
{ this.state.type == 'fmchart' &&
|
{ this.state.type == 'fmchart' &&
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FormControl variant="filled">
|
<FormControl variant="filled">
|
||||||
@ -416,6 +490,8 @@ export class Edit extends Component{
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{/* RECENTS DAYS SINCE */}
|
||||||
{ this.state.type == 'recents' &&
|
{ this.state.type == 'recents' &&
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<TextField type="number"
|
<TextField type="number"
|
||||||
@ -426,7 +502,11 @@ export class Edit extends Component{
|
|||||||
onChange={this.handleInputChange} />
|
onChange={this.handleInputChange} />
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{/* THIS/LAST MONTH */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
|
|
||||||
|
{/* THIS MONTH */}
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch color="primary" checked={this.state.add_this_month} name="add_this_month" onChange={this.handleCheckChange} />
|
<Switch color="primary" checked={this.state.add_this_month} name="add_this_month" onChange={this.handleCheckChange} />
|
||||||
@ -434,6 +514,8 @@ export class Edit extends Component{
|
|||||||
label="This Month"
|
label="This Month"
|
||||||
labelPlacement="bottom"
|
labelPlacement="bottom"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* LAST MONTH */}
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch color="primary" checked={this.state.add_last_month} name="add_last_month" onChange={this.handleCheckChange} />
|
<Switch color="primary" checked={this.state.add_last_month} name="add_last_month" onChange={this.handleCheckChange} />
|
||||||
@ -442,6 +524,8 @@ export class Edit extends Component{
|
|||||||
labelPlacement="bottom"
|
labelPlacement="bottom"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* PLAYLIST TYPE */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FormControl variant="filled">
|
<FormControl variant="filled">
|
||||||
<InputLabel htmlFor="type-select">Type</InputLabel>
|
<InputLabel htmlFor="type-select">Type</InputLabel>
|
||||||
@ -461,6 +545,8 @@ export class Edit extends Component{
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* RUN PLAYLIST */}
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Button onClick={this.handleRun} variant="contained" color="primary" className="full-width" >Run</Button>
|
<Button onClick={this.handleRun} variant="contained" color="primary" className="full-width" >Run</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
@ -473,11 +559,21 @@ export class Edit extends Component{
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smart playlist entry in dropbox
|
||||||
|
* @param {*} props Properties containing name
|
||||||
|
* @returns Dropbox option component
|
||||||
|
*/
|
||||||
function ReferenceEntry(props) {
|
function ReferenceEntry(props) {
|
||||||
return <option value={props.name}>{props.name}</option>;
|
return <option value={props.name}>{props.name}</option>;
|
||||||
// return <MenuItem value={props.name}>{props.name}</MenuItem>;
|
// return <MenuItem value={props.name}>{props.name}</MenuItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid of cards for smart/Spotify playlist names with delete button
|
||||||
|
* @param {*} props Properties
|
||||||
|
* @returns Grid component
|
||||||
|
*/
|
||||||
function ListBlock(props) {
|
function ListBlock(props) {
|
||||||
return <Grid container
|
return <Grid container
|
||||||
spacing={3}
|
spacing={3}
|
||||||
@ -489,6 +585,11 @@ function ListBlock(props) {
|
|||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smart/Spotify playlist card including name and delete button
|
||||||
|
* @param {*} props Properties
|
||||||
|
* @returns Card component wrapped in grid cell
|
||||||
|
*/
|
||||||
function BlockGridItem (props) {
|
function BlockGridItem (props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
return (
|
return (
|
||||||
|
@ -6,6 +6,9 @@ import { Paper, Tabs, Tab} from '@material-ui/core';
|
|||||||
import {Edit} from "./Edit.js";
|
import {Edit} from "./Edit.js";
|
||||||
import {Count} from "./Count.js";
|
import {Count} from "./Count.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playlist view structure with tabs for view/editing and statistics
|
||||||
|
*/
|
||||||
class View extends Component{
|
class View extends Component{
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -16,6 +19,11 @@ class View extends Component{
|
|||||||
this.handleChange = this.handleChange.bind(this);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle tab change event
|
||||||
|
* @param {*} e Event args
|
||||||
|
* @param {*} newValue New tab object
|
||||||
|
*/
|
||||||
handleChange(e, newValue){
|
handleChange(e, newValue){
|
||||||
this.setState({
|
this.setState({
|
||||||
tab: newValue
|
tab: newValue
|
||||||
@ -33,12 +41,20 @@ class View extends Component{
|
|||||||
centered
|
centered
|
||||||
width="50%"
|
width="50%"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
{/* VIEW/EDIT */}
|
||||||
<Tab label="Edit" component={Link} to={`${this.props.match.url}/edit`} />
|
<Tab label="Edit" component={Link} to={`${this.props.match.url}/edit`} />
|
||||||
|
|
||||||
|
{/* STATS */}
|
||||||
<Tab label="Count" component={Link} to={`${this.props.match.url}/count`} />
|
<Tab label="Count" component={Link} to={`${this.props.match.url}/count`} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|
||||||
|
{/* VIEW/EDIT */}
|
||||||
<Route path={`${this.props.match.url}/edit`} render={(props) => <Edit {...props} name={this.props.match.params.name}/>} />
|
<Route path={`${this.props.match.url}/edit`} render={(props) => <Edit {...props} name={this.props.match.params.name}/>} />
|
||||||
|
|
||||||
|
{/* STATS */}
|
||||||
<Route path={`${this.props.match.url}/count`} render={(props) => <Count {...props} name={this.props.match.params.name}/>} />
|
<Route path={`${this.props.match.url}/count`} render={(props) => <Count {...props} name={this.props.match.params.name}/>} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,9 @@ import { Card, Grid, Button, TextField, CardContent, CardActions, Typography } f
|
|||||||
|
|
||||||
import showMessage from "../Toast.js"
|
import showMessage from "../Toast.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change password card
|
||||||
|
*/
|
||||||
class ChangePassword extends Component {
|
class ChangePassword extends Component {
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -20,22 +23,40 @@ class ChangePassword extends Component {
|
|||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle current pw state change
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleCurrentChange(event){
|
handleCurrentChange(event){
|
||||||
this.setState({
|
this.setState({
|
||||||
'current': event.target.value
|
'current': event.target.value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle new pw state change
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleNewChange(event){
|
handleNewChange(event){
|
||||||
this.setState({
|
this.setState({
|
||||||
'new1': event.target.value
|
'new1': event.target.value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle new again pw state change
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleNew2Change(event){
|
handleNew2Change(event){
|
||||||
this.setState({
|
this.setState({
|
||||||
'new2': event.target.value
|
'new2': event.target.value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle submit button click, validate input, make network request
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleSubmit(event){
|
handleSubmit(event){
|
||||||
|
|
||||||
if(this.state.current.length == 0){
|
if(this.state.current.length == 0){
|
||||||
@ -70,9 +91,13 @@ class ChangePassword extends Component {
|
|||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={this.handleSubmit}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
|
|
||||||
|
{/* TITLE */}
|
||||||
<Grid item className="full-width">
|
<Grid item className="full-width">
|
||||||
<Typography variant="h4" color="textPrimary">Change Password</Typography>
|
<Typography variant="h4" color="textPrimary">Change Password</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* CURRENT PASSWORD */}
|
||||||
<Grid item className="full-width">
|
<Grid item className="full-width">
|
||||||
<TextField
|
<TextField
|
||||||
label="Current Password"
|
label="Current Password"
|
||||||
@ -83,6 +108,8 @@ class ChangePassword extends Component {
|
|||||||
value={this.state.current}
|
value={this.state.current}
|
||||||
className="full-width" />
|
className="full-width" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* NEW PASSWORD */}
|
||||||
<Grid item className="full-width">
|
<Grid item className="full-width">
|
||||||
<TextField
|
<TextField
|
||||||
label="New Password"
|
label="New Password"
|
||||||
@ -93,6 +120,8 @@ class ChangePassword extends Component {
|
|||||||
value={this.state.new1}
|
value={this.state.new1}
|
||||||
className="full-width" />
|
className="full-width" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* NEW PASSWORD 2 */}
|
||||||
<Grid item className="full-width">
|
<Grid item className="full-width">
|
||||||
<TextField
|
<TextField
|
||||||
label="New Password Again"
|
label="New Password Again"
|
||||||
@ -103,9 +132,13 @@ class ChangePassword extends Component {
|
|||||||
value={this.state.new2}
|
value={this.state.new2}
|
||||||
className="full-width" />
|
className="full-width" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* ERROR MESSAGE */}
|
||||||
{ this.state.error && <Grid item><Typography variant="textSeondary">{this.state.errorValue}</Typography></Grid>}
|
{ this.state.error && <Grid item><Typography variant="textSeondary">{this.state.errorValue}</Typography></Grid>}
|
||||||
</Grid>
|
</Grid>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* SUBMIT BUTTON */}
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Button type="submit" variant="contained" className="full-width" onClick={this.runStats}>Change</Button>
|
<Button type="submit" variant="contained" className="full-width" onClick={this.runStats}>Change</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
|
@ -5,6 +5,9 @@ import { Card, Button, CardContent, CardActions, Typography, TextField, Grid } f
|
|||||||
|
|
||||||
import showMessage from "../Toast.js"
|
import showMessage from "../Toast.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last.fm username setting card
|
||||||
|
*/
|
||||||
class LastFM extends Component {
|
class LastFM extends Component {
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -19,6 +22,9 @@ class LastFM extends Component {
|
|||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user info from API, set current username to state
|
||||||
|
*/
|
||||||
getUserInfo(){
|
getUserInfo(){
|
||||||
axios.get('/api/user')
|
axios.get('/api/user')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -39,12 +45,20 @@ class LastFM extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle input box state change
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleChange(event){
|
handleChange(event){
|
||||||
this.setState({
|
this.setState({
|
||||||
'lastfm_username': event.target.value
|
'lastfm_username': event.target.value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle submit button, post API change request
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleSubmit(event){
|
handleSubmit(event){
|
||||||
|
|
||||||
var username = this.state.lastfm_username;
|
var username = this.state.lastfm_username;
|
||||||
@ -72,9 +86,13 @@ class LastFM extends Component {
|
|||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={this.handleSubmit}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
|
|
||||||
|
{/* TITLE */}
|
||||||
<Grid item className="full-width">
|
<Grid item className="full-width">
|
||||||
<Typography variant="h4" color="textPrimary">Last.fm Username</Typography>
|
<Typography variant="h4" color="textPrimary">Last.fm Username</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* USERNAME TEXTBOX */}
|
||||||
<Grid item className="full-width">
|
<Grid item className="full-width">
|
||||||
<TextField
|
<TextField
|
||||||
label="last.fm Username"
|
label="last.fm Username"
|
||||||
@ -86,6 +104,8 @@ class LastFM extends Component {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* SUBMIT BUTTON */}
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Button type="submit" variant="contained" className="full-width">Save</Button>
|
<Button type="submit" variant="contained" className="full-width">Save</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
|
@ -6,6 +6,9 @@ import ChangePassword from "./ChangePassword.js";
|
|||||||
import SpotifyLink from "./SpotifyLink.js";
|
import SpotifyLink from "./SpotifyLink.js";
|
||||||
import LastFM from "./LastFM.js";
|
import LastFM from "./LastFM.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings card tabs structure for hosting password/spotify linked/last.fm username tabs
|
||||||
|
*/
|
||||||
class Settings extends Component {
|
class Settings extends Component {
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -16,6 +19,11 @@ class Settings extends Component {
|
|||||||
this.handleChange = this.handleChange.bind(this);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle tab change event
|
||||||
|
* @param {*} e Event args
|
||||||
|
* @param {*} newValue New tab object
|
||||||
|
*/
|
||||||
handleChange(e, newValue){
|
handleChange(e, newValue){
|
||||||
this.setState({
|
this.setState({
|
||||||
tab: newValue
|
tab: newValue
|
||||||
@ -33,14 +41,25 @@ class Settings extends Component {
|
|||||||
centered
|
centered
|
||||||
width="50%"
|
width="50%"
|
||||||
>
|
>
|
||||||
|
{/* PASSWORD */}
|
||||||
<Tab label="Password" component={Link} to={`${this.props.match.url}/password`} />
|
<Tab label="Password" component={Link} to={`${this.props.match.url}/password`} />
|
||||||
|
|
||||||
|
{/* SPOTIFY */}
|
||||||
<Tab label="Spotify" component={Link} to={`${this.props.match.url}/spotify`} />
|
<Tab label="Spotify" component={Link} to={`${this.props.match.url}/spotify`} />
|
||||||
|
|
||||||
|
{/* LAST.FM */}
|
||||||
<Tab label="Last.fm" component={Link} to={`${this.props.match.url}/lastfm`} />
|
<Tab label="Last.fm" component={Link} to={`${this.props.match.url}/lastfm`} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|
||||||
|
{/* PASSWORD */}
|
||||||
<Route path={`${this.props.match.url}/password`} component={ChangePassword} />
|
<Route path={`${this.props.match.url}/password`} component={ChangePassword} />
|
||||||
|
|
||||||
|
{/* SPOTIFY */}
|
||||||
<Route path={`${this.props.match.url}/spotify`} component={SpotifyLink} />
|
<Route path={`${this.props.match.url}/spotify`} component={SpotifyLink} />
|
||||||
|
|
||||||
|
{/* LAST.FM */}
|
||||||
<Route path={`${this.props.match.url}/lastfm`} component={LastFM} />
|
<Route path={`${this.props.match.url}/lastfm`} component={LastFM} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,9 @@ import { Card, Button, CardContent, CardActions, Typography } from "@material-ui
|
|||||||
|
|
||||||
import showMessage from "../Toast.js"
|
import showMessage from "../Toast.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spotify account link settings card
|
||||||
|
*/
|
||||||
class SpotifyLink extends Component {
|
class SpotifyLink extends Component {
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -16,6 +19,9 @@ class SpotifyLink extends Component {
|
|||||||
this.getUserInfo();
|
this.getUserInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user info from API and set spotify link status to state
|
||||||
|
*/
|
||||||
getUserInfo(){
|
getUserInfo(){
|
||||||
axios.get('/api/user')
|
axios.get('/api/user')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -33,10 +39,14 @@ class SpotifyLink extends Component {
|
|||||||
const table =
|
const table =
|
||||||
<div style={{maxWidth: '400px', margin: 'auto', marginTop: '20px'}}>
|
<div style={{maxWidth: '400px', margin: 'auto', marginTop: '20px'}}>
|
||||||
<Card align="center">
|
<Card align="center">
|
||||||
|
|
||||||
|
{/* STATUS */}
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h4" color="textPrimary">Admin Functions</Typography>
|
<Typography variant="h4" color="textPrimary">Admin Functions</Typography>
|
||||||
<Typography variant="body2" color="textSecondary">Status: { this.state.spotify_linked ? "Linked" : "Unlinked" }</Typography>
|
<Typography variant="body2" color="textSecondary">Status: { this.state.spotify_linked ? "Linked" : "Unlinked" }</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* STATE CHANGE BUTTON */}
|
||||||
<CardActions>
|
<CardActions>
|
||||||
{ this.state.spotify_linked ? <DeAuthButton /> : <AuthButton /> }
|
{ this.state.spotify_linked ? <DeAuthButton /> : <AuthButton /> }
|
||||||
</CardActions>
|
</CardActions>
|
||||||
@ -49,10 +59,20 @@ class SpotifyLink extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate Spotify account button component
|
||||||
|
* @param {*} props Properties
|
||||||
|
* @returns Button component
|
||||||
|
*/
|
||||||
function AuthButton(props) {
|
function AuthButton(props) {
|
||||||
return <Button component='a' variant="contained" className="full-width" href="/auth/spotify">Auth</Button>;
|
return <Button component='a' variant="contained" className="full-width" href="/auth/spotify">Auth</Button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deauthenticate Spotify account button component
|
||||||
|
* @param {*} props Properties
|
||||||
|
* @returns Button component
|
||||||
|
*/
|
||||||
function DeAuthButton(props) {
|
function DeAuthButton(props) {
|
||||||
return <Button component='a' variant="contained" className="full-width" href="/auth/spotify/deauth">De-Auth</Button>;
|
return <Button component='a' variant="contained" className="full-width" href="/auth/spotify/deauth">De-Auth</Button>;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,9 @@ import { Card, Button, TextField, CardActions, CardContent, Typography, Grid } f
|
|||||||
|
|
||||||
import showMessage from "../Toast.js"
|
import showMessage from "../Toast.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New tag card component
|
||||||
|
*/
|
||||||
class NewTag extends Component {
|
class NewTag extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -16,12 +19,20 @@ class NewTag extends Component {
|
|||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle tag id input box state changes
|
||||||
|
* @param {*} event
|
||||||
|
*/
|
||||||
handleInputChange(event){
|
handleInputChange(event){
|
||||||
this.setState({
|
this.setState({
|
||||||
tag_id: event.target.value
|
tag_id: event.target.value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate input, make new tag API request
|
||||||
|
* @param {*} event
|
||||||
|
*/
|
||||||
handleSubmit(event){
|
handleSubmit(event){
|
||||||
var tag_id = this.state.tag_id;
|
var tag_id = this.state.tag_id;
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -58,9 +69,13 @@ class NewTag extends Component {
|
|||||||
<Card align="center">
|
<Card align="center">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid container spacing={5}>
|
<Grid container spacing={5}>
|
||||||
|
|
||||||
|
{/* TITLE */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="h3">New Tag</Typography>
|
<Typography variant="h3">New Tag</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* NAME TEXTBOX */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<TextField
|
<TextField
|
||||||
label="Name"
|
label="Name"
|
||||||
@ -72,6 +87,8 @@ class NewTag extends Component {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* SUBMIT BUTTON */}
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Button variant="contained" color="primary" className="full-width" onClick={this.handleSubmit}>Create</Button>
|
<Button variant="contained" color="primary" className="full-width" onClick={this.handleSubmit}>Create</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
|
@ -7,6 +7,9 @@ const axios = require('axios');
|
|||||||
|
|
||||||
import showMessage from "../Toast.js"
|
import showMessage from "../Toast.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag card list component
|
||||||
|
*/
|
||||||
class TagList extends Component {
|
class TagList extends Component {
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -18,6 +21,9 @@ class TagList extends Component {
|
|||||||
this.handleDeleteTag = this.handleDeleteTag.bind(this);
|
this.handleDeleteTag = this.handleDeleteTag.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tags info from API
|
||||||
|
*/
|
||||||
getTags(){
|
getTags(){
|
||||||
var self = this;
|
var self = this;
|
||||||
axios.get('/api/tag')
|
axios.get('/api/tag')
|
||||||
@ -41,6 +47,11 @@ class TagList extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make delete tag request of API
|
||||||
|
* @param {*} tag_id Tag ID
|
||||||
|
* @param {*} event Event Data
|
||||||
|
*/
|
||||||
handleDeleteTag(tag_id, event){
|
handleDeleteTag(tag_id, event){
|
||||||
axios.delete(`/api/tag/${tag_id}`)
|
axios.delete(`/api/tag/${tag_id}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -60,6 +71,11 @@ class TagList extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag card grid component
|
||||||
|
* @param {*} props Properties
|
||||||
|
* @returns Grid component
|
||||||
|
*/
|
||||||
function TagGrid(props){
|
function TagGrid(props){
|
||||||
return (
|
return (
|
||||||
<Grid container
|
<Grid container
|
||||||
@ -69,6 +85,8 @@ function TagGrid(props){
|
|||||||
alignItems="flex-start"
|
alignItems="flex-start"
|
||||||
style={{padding: '24px'}}>
|
style={{padding: '24px'}}>
|
||||||
<Grid item xs={12} sm={6} md={2}>
|
<Grid item xs={12} sm={6} md={2}>
|
||||||
|
|
||||||
|
{/* NEW BUTTON */}
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
color="primary"
|
color="primary"
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
@ -76,6 +94,8 @@ function TagGrid(props){
|
|||||||
<Button component={Link} to='tags/new' >New</Button>
|
<Button component={Link} to='tags/new' >New</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* TAG CARDS */}
|
||||||
{ props.tags.length == 0 ? (
|
{ props.tags.length == 0 ? (
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
<Grid item xs={12} sm={6} md={3}>
|
||||||
<Typography variant="h5" component="h2">No Tags</Typography>
|
<Typography variant="h5" component="h2">No Tags</Typography>
|
||||||
@ -90,14 +110,23 @@ function TagGrid(props){
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag card component
|
||||||
|
* @param {*} props Properties
|
||||||
|
* @returns Card component
|
||||||
|
*/
|
||||||
function TagCard(props){
|
function TagCard(props){
|
||||||
return (
|
return (
|
||||||
<Grid item xs>
|
<Grid item xs>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
||||||
|
{/* TAG NAME */}
|
||||||
<Typography variant="h4" component="h2">
|
<Typography variant="h4" component="h2">
|
||||||
{ props.tag.name }
|
{ props.tag.name }
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
{/* COUNT */}
|
||||||
{'count' in props.tag &&
|
{'count' in props.tag &&
|
||||||
<Typography variant="h6" style={{color: "#b3b3b3"}}>
|
<Typography variant="h6" style={{color: "#b3b3b3"}}>
|
||||||
{ props.tag.count }
|
{ props.tag.count }
|
||||||
@ -108,7 +137,11 @@ function TagCard(props){
|
|||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="contained">
|
variant="contained">
|
||||||
|
|
||||||
|
{/* VIEW BUTTON */}
|
||||||
<Button component={Link} to={getTagLink(props.tag.tag_id)}>View</Button>
|
<Button component={Link} to={getTagLink(props.tag.tag_id)}>View</Button>
|
||||||
|
|
||||||
|
{/* DELETE BUTTON */}
|
||||||
<Button onClick={(e) => props.handleDeleteTag(props.tag.tag_id, e)}>Delete</Button>
|
<Button onClick={(e) => props.handleDeleteTag(props.tag.tag_id, e)}>Delete</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
@ -117,6 +150,11 @@ function TagCard(props){
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map tag name to URL
|
||||||
|
* @param {*} tagName Subject tag name
|
||||||
|
* @returns Tag URL
|
||||||
|
*/
|
||||||
function getTagLink(tagName){
|
function getTagLink(tagName){
|
||||||
return `/app/tag/${tagName}`;
|
return `/app/tag/${tagName}`;
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,18 @@ import { Route, Switch } from "react-router-dom";
|
|||||||
import TagList from "./TagList.js"
|
import TagList from "./TagList.js"
|
||||||
import New from "./New.js"
|
import New from "./New.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag router for directing between tag list and new
|
||||||
|
*/
|
||||||
class TagRouter extends Component {
|
class TagRouter extends Component {
|
||||||
render(){
|
render(){
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
{/* TAG LIST */}
|
||||||
<Route exact path={`${this.props.match.url}/`} component={TagList} />
|
<Route exact path={`${this.props.match.url}/`} component={TagList} />
|
||||||
|
|
||||||
|
{/* NEW */}
|
||||||
<Route path={`${this.props.match.url}/new`} component={New} />
|
<Route path={`${this.props.match.url}/new`} component={New} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,6 +17,9 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag View card
|
||||||
|
*/
|
||||||
class View extends Component{
|
class View extends Component{
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -48,6 +51,9 @@ class View extends Component{
|
|||||||
this.handleChangeAddType = this.handleChangeAddType.bind(this);
|
this.handleChangeAddType = this.handleChangeAddType.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tag info from API on load
|
||||||
|
*/
|
||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
this.getTag();
|
this.getTag();
|
||||||
// var intervalId = setInterval(() => {this.getTag(false)}, 5000);
|
// var intervalId = setInterval(() => {this.getTag(false)}, 5000);
|
||||||
@ -64,6 +70,10 @@ class View extends Component{
|
|||||||
// clearTimeout(this.state.timeoutId);
|
// clearTimeout(this.state.timeoutId);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tag info from API
|
||||||
|
* @param {*} error_toast Whether to show toast on network error
|
||||||
|
*/
|
||||||
getTag(error_toast = true){
|
getTag(error_toast = true){
|
||||||
axios.get(`/api/tag/${ this.state.tag_id }`)
|
axios.get(`/api/tag/${ this.state.tag_id }`)
|
||||||
.then( (response) => {
|
.then( (response) => {
|
||||||
@ -100,6 +110,10 @@ class View extends Component{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle input box state changes
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleInputChange(event){
|
handleInputChange(event){
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -108,6 +122,10 @@ class View extends Component{
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle checkbox state changes, make network updates
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleCheckChange(event){
|
handleCheckChange(event){
|
||||||
let payload = {...this.state.tag};
|
let payload = {...this.state.tag};
|
||||||
payload[event.target.name] = event.target.checked;
|
payload[event.target.name] = event.target.checked;
|
||||||
@ -120,12 +138,20 @@ class View extends Component{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Put tag info changes to API
|
||||||
|
* @param {*} changes Dictionary of changes to submit
|
||||||
|
*/
|
||||||
makeNetworkUpdate(changes){
|
makeNetworkUpdate(changes){
|
||||||
axios.put(`/api/tag/${this.state.tag_id}`, changes).catch((error) => {
|
axios.put(`/api/tag/${this.state.tag_id}`, changes).catch((error) => {
|
||||||
showMessage(`Error updating ${Object.keys(changes).join(", ")} (${error.response.status})`);
|
showMessage(`Error updating ${Object.keys(changes).join(", ")} (${error.response.status})`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate input and make tag refresh update of API
|
||||||
|
* @param {*} event
|
||||||
|
*/
|
||||||
handleRun(event){
|
handleRun(event){
|
||||||
axios.get('/api/user')
|
axios.get('/api/user')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -145,6 +171,12 @@ class View extends Component{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle remove watched part
|
||||||
|
* @param {*} music_obj Subject object to remove
|
||||||
|
* @param {*} addType Object type (tracks/albums/artists)
|
||||||
|
* @param {*} event Event data
|
||||||
|
*/
|
||||||
handleRemoveObj(music_obj, addType, event){
|
handleRemoveObj(music_obj, addType, event){
|
||||||
var startingItems = this.state.tag[addType].slice();
|
var startingItems = this.state.tag[addType].slice();
|
||||||
|
|
||||||
@ -177,12 +209,20 @@ class View extends Component{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle adding type drop down change
|
||||||
|
* @param {*} type
|
||||||
|
*/
|
||||||
handleChangeAddType(type){
|
handleChangeAddType(type){
|
||||||
this.setState({
|
this.setState({
|
||||||
addType: type
|
addType: type
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate input, make tag part add request of API
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
handleAdd(){
|
handleAdd(){
|
||||||
|
|
||||||
var addType = this.state.addType;
|
var addType = this.state.addType;
|
||||||
@ -273,17 +313,27 @@ class View extends Component{
|
|||||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||||
<Card align="center">
|
<Card align="center">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
||||||
|
{/* TAG NAME TITLE */}
|
||||||
<Typography variant="h2" color="textPrimary">{this.state.tag.name}</Typography>
|
<Typography variant="h2" color="textPrimary">{this.state.tag.name}</Typography>
|
||||||
<Grid container spacing={5}>
|
<Grid container spacing={5}>
|
||||||
|
|
||||||
|
{/* ARTISTS TITLE */}
|
||||||
{ this.state.tag.artists.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Artists</Typography></Grid> }
|
{ this.state.tag.artists.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Artists</Typography></Grid> }
|
||||||
|
{/* ARTIST CARDS */}
|
||||||
{ this.state.tag.artists.length > 0 && <ListBlock handler={this.handleRemoveObj} list={this.state.tag.artists} addType="artists" showTime={this.state.tag.time_objects}/> }
|
{ this.state.tag.artists.length > 0 && <ListBlock handler={this.handleRemoveObj} list={this.state.tag.artists} addType="artists" showTime={this.state.tag.time_objects}/> }
|
||||||
|
|
||||||
|
{/* ALBUMS TITLE */}
|
||||||
{ this.state.tag.albums.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Albums</Typography></Grid> }
|
{ this.state.tag.albums.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Albums</Typography></Grid> }
|
||||||
|
{/* ALBUM CARDS */}
|
||||||
{ this.state.tag.albums.length > 0 && <ListBlock handler={this.handleRemoveObj} list={this.state.tag.albums} addType="albums" showTime={this.state.tag.time_objects}/> }
|
{ this.state.tag.albums.length > 0 && <ListBlock handler={this.handleRemoveObj} list={this.state.tag.albums} addType="albums" showTime={this.state.tag.time_objects}/> }
|
||||||
|
|
||||||
|
{/* TRACKS TITLE */}
|
||||||
{ this.state.tag.tracks.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Tracks</Typography></Grid> }
|
{ this.state.tag.tracks.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Tracks</Typography></Grid> }
|
||||||
|
{/* TRACK CARDS */}
|
||||||
{ this.state.tag.tracks.length > 0 && <ListBlock handler={this.handleRemoveObj} list={this.state.tag.tracks} addType="tracks" showTime={this.state.tag.time_objects}/> }
|
{ this.state.tag.tracks.length > 0 && <ListBlock handler={this.handleRemoveObj} list={this.state.tag.tracks} addType="tracks" showTime={this.state.tag.time_objects}/> }
|
||||||
|
|
||||||
|
{/* NAME TEXTBOX */}
|
||||||
<Grid item xs={12} sm={this.state.addType != 'artists' ? 3 : 4} md={this.state.addType != 'artists' ? 3 : 4}>
|
<Grid item xs={12} sm={this.state.addType != 'artists' ? 3 : 4} md={this.state.addType != 'artists' ? 3 : 4}>
|
||||||
<TextField
|
<TextField
|
||||||
name="name"
|
name="name"
|
||||||
@ -292,16 +342,19 @@ class View extends Component{
|
|||||||
value={this.state.name}
|
value={this.state.name}
|
||||||
onChange={this.handleInputChange}></TextField>
|
onChange={this.handleInputChange}></TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{/* ARTIST NAME TEXTBOX */}
|
||||||
{ this.state.addType != 'artists' &&
|
{ this.state.addType != 'artists' &&
|
||||||
<Grid item xs={12} sm={3} md={4}>
|
<Grid item xs={12} sm={3} md={4}>
|
||||||
<TextField
|
<TextField
|
||||||
name="artist"
|
name="artist"
|
||||||
label="Artist"
|
label="Artist"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
value={this.state.artist}
|
value={this.state.artist}
|
||||||
onChange={this.handleInputChange}></TextField>
|
onChange={this.handleInputChange}></TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{/* ADD TYPE DROPDOWN */}
|
||||||
<Grid item xs={12} sm={this.state.addType != 'artists' ? 2 : 4} md={this.state.addType != 'artists' ? 2 : 4}>
|
<Grid item xs={12} sm={this.state.addType != 'artists' ? 2 : 4} md={this.state.addType != 'artists' ? 2 : 4}>
|
||||||
<FormControl variant="filled">
|
<FormControl variant="filled">
|
||||||
<InputLabel htmlFor="addType">Type</InputLabel>
|
<InputLabel htmlFor="addType">Type</InputLabel>
|
||||||
@ -319,9 +372,13 @@ class View extends Component{
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* ADD BUTTON */}
|
||||||
<Grid item xs={12} sm={this.state.addType != 'artists' ? 3 : 4} md={this.state.addType != 'artists' ? 3 : 4}>
|
<Grid item xs={12} sm={this.state.addType != 'artists' ? 3 : 4} md={this.state.addType != 'artists' ? 3 : 4}>
|
||||||
<Button variant="contained" onClick={this.handleAdd} className="full-width">Add</Button>
|
<Button variant="contained" onClick={this.handleAdd} className="full-width">Add</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* TIME CHECKBOX */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
@ -331,15 +388,22 @@ class View extends Component{
|
|||||||
labelPlacement="bottom"
|
labelPlacement="bottom"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* STATS CARD */}
|
||||||
<StatsCard count={this.state.tag.count} proportion={this.state.tag.proportion} showTime={this.state.tag.time_objects} time={this.state.tag.total_time}></StatsCard>
|
<StatsCard count={this.state.tag.count} proportion={this.state.tag.proportion} showTime={this.state.tag.time_objects} time={this.state.tag.total_time}></StatsCard>
|
||||||
|
|
||||||
|
{/* PIE CHART */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<PieChart data={data}/>
|
<PieChart data={data} padding={100}/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* BAR CHART */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<BarChart data={data} title='scrobbles'/>
|
<BarChart data={data} title='scrobbles' indexAxis='y'/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
{/* UPDATE BUTTON */}
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Button onClick={this.handleRun} variant="contained" color="primary" className="full-width" >Update</Button>
|
<Button onClick={this.handleRun} variant="contained" color="primary" className="full-width" >Update</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
@ -353,6 +417,11 @@ class View extends Component{
|
|||||||
|
|
||||||
export default View;
|
export default View;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid component for holding artist/album/track cards
|
||||||
|
* @param {*} props Properties
|
||||||
|
* @returns Grid component
|
||||||
|
*/
|
||||||
function ListBlock(props) {
|
function ListBlock(props) {
|
||||||
return <Grid container
|
return <Grid container
|
||||||
spacing={3}
|
spacing={3}
|
||||||
@ -365,6 +434,11 @@ function ListBlock(props) {
|
|||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track/album/artist card with time info and delete button
|
||||||
|
* @param {*} props Properties
|
||||||
|
* @returns Card component wrapped in grid cell
|
||||||
|
*/
|
||||||
function BlockGridItem (props) {
|
function BlockGridItem (props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
return (
|
return (
|
||||||
@ -372,19 +446,26 @@ function BlockGridItem (props) {
|
|||||||
<Card variant="outlined" className={classes.root}>
|
<Card variant="outlined" className={classes.root}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|
||||||
|
{/* NAME TITLE */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="h4" color="textSecondary" className={classes.root}>{ props.music_obj.name }</Typography>
|
<Typography variant="h4" color="textSecondary" className={classes.root}>{ props.music_obj.name }</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* ARTIST NAME */}
|
||||||
{ 'artist' in props.music_obj &&
|
{ 'artist' in props.music_obj &&
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="body1" color="textSecondary" className={classes.root}>{ props.music_obj.artist }</Typography>
|
<Typography variant="body1" color="textSecondary" className={classes.root}>{ props.music_obj.artist }</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{/* SCROBBLE COUNT */}
|
||||||
{ 'count' in props.music_obj &&
|
{ 'count' in props.music_obj &&
|
||||||
<Grid item xs={8}>
|
<Grid item xs={8}>
|
||||||
<Typography variant="h4" color="textPrimary" className={classes.root}>📈 { props.music_obj.count }</Typography>
|
<Typography variant="h4" color="textPrimary" className={classes.root}>📈 { props.music_obj.count }</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
{/* TIME */}
|
||||||
{ 'time' in props.music_obj && props.showTime &&
|
{ 'time' in props.music_obj && props.showTime &&
|
||||||
<Grid item xs={8}>
|
<Grid item xs={8}>
|
||||||
<Typography variant="body1" color="textSecondary" className={classes.root}>🕒 { props.music_obj.time }</Typography>
|
<Typography variant="body1" color="textSecondary" className={classes.root}>🕒 { props.music_obj.time }</Typography>
|
||||||
@ -392,6 +473,8 @@ function BlockGridItem (props) {
|
|||||||
}
|
}
|
||||||
</Grid>
|
</Grid>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* DELETE BUTTON */}
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Button className="full-width" color="secondary" variant="contained" aria-label="delete" onClick={(e) => props.handler(props.music_obj, props.addType, e)} startIcon={<Delete />}>
|
<Button className="full-width" color="secondary" variant="contained" aria-label="delete" onClick={(e) => props.handler(props.music_obj, props.addType, e)} startIcon={<Delete />}>
|
||||||
Delete
|
Delete
|
||||||
@ -402,6 +485,11 @@ function BlockGridItem (props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stats card component with total time and scrobbles
|
||||||
|
* @param {*} props Properties
|
||||||
|
* @returns Card component wrapped in grid cell
|
||||||
|
*/
|
||||||
function StatsCard (props) {
|
function StatsCard (props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
return (
|
return (
|
||||||
@ -409,12 +497,18 @@ function StatsCard (props) {
|
|||||||
<Card variant="outlined" className={classes.root}>
|
<Card variant="outlined" className={classes.root}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid container spacing={10}>
|
<Grid container spacing={10}>
|
||||||
|
|
||||||
|
{/* SCROBBLE COUNT */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="h1" color="textPrimary" className={classes.root}>📈 { props.count }</Typography>
|
<Typography variant="h1" color="textPrimary" className={classes.root}>📈 { props.count }</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* PERCENT */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="h4" color="textSecondary" className={classes.root}>{ props.proportion.toFixed(2) }%</Typography>
|
<Typography variant="h4" color="textSecondary" className={classes.root}>{ props.proportion.toFixed(2) }%</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* TOTAL TIME */}
|
||||||
{props.showTime &&
|
{props.showTime &&
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="h4" color="textSecondary" className={classes.root}>🕒 { props.time }</Typography>
|
<Typography variant="h4" color="textSecondary" className={classes.root}>🕒 { props.time }</Typography>
|
||||||
|
@ -3,4 +3,6 @@ import ReactDOM from "react-dom";
|
|||||||
|
|
||||||
import MusicTools from "./MusicTools";
|
import MusicTools from "./MusicTools";
|
||||||
|
|
||||||
|
// ROOT file for bootstrapping the Music Tools component and app
|
||||||
|
|
||||||
ReactDOM.render(<MusicTools />, document.getElementById('react'));
|
ReactDOM.render(<MusicTools />, document.getElementById('react'));
|
@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
// login function for validating login data
|
||||||
|
|
||||||
function handleLogin(){
|
function handleLogin(){
|
||||||
var username = document.forms['login']['username'].value;
|
var username = document.forms['login']['username'].value;
|
||||||
var password = document.forms['login']['password'].value;
|
var password = document.forms['login']['password'].value;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
// login function for validating register data
|
||||||
|
|
||||||
function handleRegister(){
|
function handleRegister(){
|
||||||
var username = document.forms['register']['username'].value;
|
var username = document.forms['register']['username'].value;
|
||||||
var password = document.forms['register']['password'].value;
|
var password = document.forms['register']['password'].value;
|
||||||
|
Loading…
Reference in New Issue
Block a user