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
|
||||
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
|
||||
- name: Set up Cloud SDK
|
||||
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
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# 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.
|
||||
templates_path = ['_templates']
|
||||
|
@ -13,6 +13,7 @@ Music Tools
|
||||
src/music.db
|
||||
src/music.model
|
||||
src/music.tasks
|
||||
src/MusicTools
|
||||
|
||||
`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
|
||||
|
||||
music
|
||||
MusicTools
|
||||
|
8242
package-lock.json
generated
8242
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/icons": "^4.11.2",
|
||||
"axios": "^0.21.1",
|
||||
"chart.js": "^2.9.4",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"chart.js": "^3.3.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -35,9 +35,10 @@
|
||||
"babel-loader": "^8.2.2",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"css-loader": "^5.2.4",
|
||||
"jsdoc": "^3.6.7",
|
||||
"style-loader": "^0.23.1",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack": "^5.38.1",
|
||||
"webpack-cli": "^4.7.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]
|
||||
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]]
|
||||
name = "protobuf"
|
||||
version = "3.15.7"
|
||||
@ -542,6 +553,19 @@ docs = ["sphinxcontrib-websupport"]
|
||||
lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"]
|
||||
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]]
|
||||
name = "sphinxcontrib-applehelp"
|
||||
version = "1.0.2"
|
||||
@ -707,7 +731,7 @@ python-versions = "*"
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "8331f6a2897615556a573dc92c00b403a8be64b3a45e4e1a6d1bfacb006c013e"
|
||||
content-hash = "51d1e9070adab3c839b4b1c50ebd1dc0366354d56f6c1a9485af6d4bb362959f"
|
||||
|
||||
[metadata.files]
|
||||
alabaster = [
|
||||
@ -951,6 +975,9 @@ packaging = [
|
||||
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
|
||||
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
|
||||
]
|
||||
parsimonious = [
|
||||
{file = "parsimonious-0.7.0.tar.gz", hash = "sha256:396d424f64f834f9463e81ba79a331661507a21f1ed7b644f7f6a744006fd938"},
|
||||
]
|
||||
protobuf = [
|
||||
{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"},
|
||||
@ -1043,6 +1070,10 @@ sphinx = [
|
||||
{file = "Sphinx-3.5.3-py3-none-any.whl", hash = "sha256:3f01732296465648da43dec8fb40dc451ba79eb3e2cc5c6d79005fd98197107d"},
|
||||
{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 = [
|
||||
{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"},
|
||||
|
@ -28,6 +28,7 @@ spotfm = { git = "https://github.com/Sarsoo/spotfm.git" }
|
||||
[tool.poetry.dev-dependencies]
|
||||
pylint = "^2.5.3"
|
||||
Sphinx = "^3.5.3"
|
||||
sphinx-js = "^3.1.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
@ -7,6 +7,9 @@ import Lock from "./Lock.js";
|
||||
import Functions from "./Functions.js";
|
||||
import Tasks from "./Tasks.js";
|
||||
|
||||
/**
|
||||
* Admin router component for hosting cards
|
||||
*/
|
||||
class Admin extends Component {
|
||||
|
||||
constructor(props){
|
||||
@ -17,6 +20,11 @@ class Admin extends Component {
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tab change event
|
||||
* @param {*} e Event data
|
||||
* @param {*} newValue New tab data
|
||||
*/
|
||||
handleChange(e, newValue){
|
||||
this.setState({
|
||||
tab: newValue
|
||||
@ -34,8 +42,13 @@ class Admin extends Component {
|
||||
centered
|
||||
width="50%"
|
||||
>
|
||||
{/* LOCK CARD */}
|
||||
<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`} />
|
||||
|
||||
{/* RUNNING TASKS CARD */}
|
||||
<Tab label="Tasks" component={Link} to={`${this.props.match.url}/tasks`} />
|
||||
</Tabs>
|
||||
</Paper>
|
||||
|
@ -4,6 +4,9 @@ const axios = require('axios');
|
||||
import showMessage from "../Toast.js"
|
||||
import { Card, Button, ButtonGroup, CardContent, CardActions, Typography } from "@material-ui/core";
|
||||
|
||||
/**
|
||||
* Admin functions card component
|
||||
*/
|
||||
class Functions extends Component {
|
||||
|
||||
constructor(props){
|
||||
@ -13,6 +16,10 @@ class Functions extends Component {
|
||||
this.runStats = this.runStats.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make run all playlists request of API
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
runAllUsers(event){
|
||||
axios.get('/api/playlist/run/users')
|
||||
.then((response) => {
|
||||
@ -23,6 +30,10 @@ class Functions extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make run stats request of API
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
runStats(event){
|
||||
axios.get('/api/spotfm/playlist/refresh/users')
|
||||
.then((response) => {
|
||||
@ -38,11 +49,17 @@ class Functions extends Component {
|
||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
|
||||
{/* TITLE */}
|
||||
<Typography variant="h4" color="textPrimary">Admin Functions</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<ButtonGroup variant="contained" color="primary" className="full-width">
|
||||
|
||||
{/* RUN ALL PLAYLISTS 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>
|
||||
</ButtonGroup>
|
||||
</CardActions>
|
||||
|
@ -13,6 +13,9 @@ const useStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Account lock card component
|
||||
*/
|
||||
class Lock extends Component {
|
||||
|
||||
constructor(props){
|
||||
@ -27,6 +30,9 @@ class Lock extends Component {
|
||||
this.handleLock = this.handleLock.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make user infor request of API
|
||||
*/
|
||||
getUserInfo(){
|
||||
axios.get('/api/users')
|
||||
.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){
|
||||
axios.post('/api/user', {
|
||||
username: username,
|
||||
@ -60,8 +72,12 @@ class Lock extends Component {
|
||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
|
||||
{/* TITLE */}
|
||||
<Typography variant="h4" color="textPrimary">Account Locks</Typography>
|
||||
<Grid container spacing={3}>
|
||||
|
||||
{/* ACCOUNT CARDS */}
|
||||
{ this.state.accounts.map((account) => <Row account={account} handler={this.handleLock}
|
||||
key= {account.username}/>) }
|
||||
</Grid>
|
||||
@ -72,16 +88,27 @@ class Lock extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid of account cards with lock buttons
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
function Row(props){
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Grid item xs={12} sm={3} md={2}>
|
||||
<Card variant="outlined" className={classes.root}>
|
||||
<CardContent>
|
||||
|
||||
{/* USERNAME TITLE */}
|
||||
<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>
|
||||
</CardContent>
|
||||
<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)}>
|
||||
{props.account.locked ? "Unlock" : "Lock"}
|
||||
</Button>
|
||||
|
@ -5,6 +5,9 @@ import { Card, CardContent, Typography, Grid } from '@material-ui/core';
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
/**
|
||||
* Running tasks card component
|
||||
*/
|
||||
class Tasks extends Component {
|
||||
|
||||
constructor(props){
|
||||
@ -17,6 +20,9 @@ class Tasks extends Component {
|
||||
this.getTasks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks from API
|
||||
*/
|
||||
getTasks(){
|
||||
var self = this;
|
||||
axios.get('/api/admin/tasks')
|
||||
@ -34,6 +40,8 @@ class Tasks extends Component {
|
||||
render () {
|
||||
return (
|
||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||
|
||||
{/* GRID OF TASK CARDS */}
|
||||
<Grid container spacing={4}>
|
||||
{ this.state.tasks.map((entry) => <TaskType url={entry.url} count={entry.count} times={entry.scheduled_times} key={entry.url}/>)}
|
||||
</Grid>
|
||||
@ -42,6 +50,11 @@ class Tasks extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid of task cards
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
function TaskType(props) {
|
||||
return (
|
||||
<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';
|
||||
|
||||
/**
|
||||
* Into card for the home page
|
||||
*/
|
||||
class Index extends Component{
|
||||
|
||||
constructor(props){
|
||||
|
@ -1,5 +1,7 @@
|
||||
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 {
|
||||
|
||||
@ -20,30 +22,39 @@ class BarChart extends Component {
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: this.props.indexAxis, // vertical or horizontal bars
|
||||
plugins: {
|
||||
legend : {
|
||||
display : false
|
||||
display : true,
|
||||
labels: {
|
||||
color: 'white'
|
||||
}
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
rectangle : {
|
||||
backgroundColor: 'rgb(255, 255, 255)'
|
||||
bar : {
|
||||
backgroundColor: 'rgb(255, 255, 255)',
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgb(0, 0, 0)'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
y: {
|
||||
ticks: {
|
||||
fontColor: "#d8d8d8",
|
||||
fontSize: 16,
|
||||
stepSize: 1,
|
||||
beginAtZero: true
|
||||
color: "#d8d8d8",
|
||||
font: {
|
||||
size: 20
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
}
|
||||
},
|
||||
x: {
|
||||
ticks: {
|
||||
fontColor: "#d8d8d8",
|
||||
fontSize: 16,
|
||||
stepSize: 1
|
||||
color: "#d8d8d8",
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,5 +1,15 @@
|
||||
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 {
|
||||
|
||||
@ -20,36 +30,20 @@ class PieChart extends Component {
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend : {
|
||||
display : true,
|
||||
labels: {
|
||||
fontColor: 'white'
|
||||
color: 'white'
|
||||
}
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: this.props.padding
|
||||
},
|
||||
elements: {
|
||||
arc : {
|
||||
backgroundColor: ['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
|
||||
'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
|
||||
],
|
||||
backgroundColor: [...pieColours, ...pieColours, ...pieColours],
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgb(0, 0, 0)'
|
||||
}
|
||||
|
@ -34,6 +34,9 @@ const LazyAdmin = React.lazy(() => import("./Admin/AdminRouter"))
|
||||
const LazyTags = React.lazy(() => import("./Tag/TagRouter"))
|
||||
const LazyTag = React.lazy(() => import("./Tag/View"))
|
||||
|
||||
/**
|
||||
* Root component for app
|
||||
*/
|
||||
class MusicTools extends Component {
|
||||
|
||||
constructor(props){
|
||||
@ -46,14 +49,23 @@ class MusicTools extends Component {
|
||||
this.setOpen = this.setOpen.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user info from API on load
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.getUserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel get user info request
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.userInfoCancelToken.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user info from API
|
||||
*/
|
||||
getUserInfo(){
|
||||
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){
|
||||
this.setState({
|
||||
drawerOpen: bool
|
||||
@ -82,6 +98,9 @@ class MusicTools extends Component {
|
||||
return (
|
||||
<Router>
|
||||
<ThemeProvider theme={GlobalTheme}>
|
||||
|
||||
{/* TOP APP BAR */}
|
||||
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<IconButton edge="start" color="inherit" aria-label="menu" onClick={(e) => this.setOpen(true)}>
|
||||
@ -92,6 +111,9 @@ class MusicTools extends Component {
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{/* MENU DRAWER */}
|
||||
|
||||
<Drawer
|
||||
variant="persistent"
|
||||
anchor="left"
|
||||
@ -110,32 +132,45 @@ class MusicTools extends Component {
|
||||
onKeyDown={(e) => this.setOpen(false)}
|
||||
>
|
||||
<List>
|
||||
{/* HOME */}
|
||||
<ListItem button key="home" component={Link} to='/app'>
|
||||
<ListItemIcon><HomeIcon /></ListItemIcon>
|
||||
<ListItemText primary="Home" />
|
||||
</ListItem>
|
||||
|
||||
{/* PLAYLISTS */}
|
||||
<ListItem button key="playlists" component={Link} to='/app/playlists'>
|
||||
<ListItemIcon><QueueMusic /></ListItemIcon>
|
||||
<ListItemText primary="Playlists" />
|
||||
</ListItem>
|
||||
|
||||
{/* TAGS */}
|
||||
<ListItem button key="tags" component={Link} to='/app/tags'>
|
||||
<ListItemIcon><GroupWork /></ListItemIcon>
|
||||
<ListItemText primary="Tags" />
|
||||
</ListItem>
|
||||
|
||||
{/* SETTINGS */}
|
||||
<ListItem button key="settings" component={Link} to='/app/settings/password'>
|
||||
<ListItemIcon><Build /></ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItem>
|
||||
|
||||
{/* ADMIN */}
|
||||
{ this.state.type == 'admin' &&
|
||||
<ListItem button key="admin" component={Link} to='/app/admin/lock'>
|
||||
<ListItemIcon><AccountCircle /></ListItemIcon>
|
||||
<ListItemText primary="Admin" />
|
||||
</ListItem>
|
||||
}
|
||||
|
||||
{/* LOGOUT */}
|
||||
<ListItem button key="logout" onClick={(e) => { window.location.href = '/auth/logout' }}>
|
||||
<ListItemIcon><KeyboardBackspace /></ListItemIcon>
|
||||
<ListItemText primary="Logout" />
|
||||
</ListItem>
|
||||
|
||||
{/* SARSOO.XYZ */}
|
||||
<ListItem button key="sarsoo.xyz" onClick={(e) => { window.location.href = 'https://sarsoo.xyz' }}>
|
||||
<ListItemIcon><ExitToApp /></ListItemIcon>
|
||||
<ListItemText primary="sarsoo.xyz" />
|
||||
@ -143,6 +178,9 @@ class MusicTools extends Component {
|
||||
</List>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
{/* ROUTER SWITCH */}
|
||||
|
||||
<div className="full-width">
|
||||
<Switch>
|
||||
<React.Suspense fallback={<LoadingMessage/>}>
|
||||
|
@ -4,12 +4,19 @@ import { Route, Switch } from "react-router-dom";
|
||||
import PlaylistsView from "./PlaylistsList.js"
|
||||
import NewPlaylist from "./New.js";
|
||||
|
||||
/**
|
||||
* Router for playlist lists page, includes new playlist page
|
||||
*/
|
||||
class Playlists extends Component {
|
||||
render(){
|
||||
return (
|
||||
<div>
|
||||
<Switch>
|
||||
|
||||
{/* PLAYLIST LIST */}
|
||||
<Route exact path={`${this.props.match.url}/`} component={PlaylistsView} />
|
||||
|
||||
{/* NEW PLAYLIST */}
|
||||
<Route path={`${this.props.match.url}/new`} component={NewPlaylist} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
@ -5,6 +5,9 @@ import { Card, Button, FormControl, TextField, InputLabel, Select, CardActions,
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
/**
|
||||
* New playlist card
|
||||
*/
|
||||
class NewPlaylist extends Component {
|
||||
|
||||
constructor(props) {
|
||||
@ -18,10 +21,15 @@ class NewPlaylist extends Component {
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
/** Set initial state of playlist type description */
|
||||
componentDidMount(){
|
||||
this.setDescription('default');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set playlist type description
|
||||
* @param {*} value Playlist type string to match
|
||||
*/
|
||||
setDescription(value){
|
||||
switch(value){
|
||||
case 'default':
|
||||
@ -42,6 +50,10 @@ class NewPlaylist extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle input changes by setting state
|
||||
* @param {*} event
|
||||
*/
|
||||
handleInputChange(event){
|
||||
this.setState({
|
||||
[event.target.name]: event.target.value
|
||||
@ -49,6 +61,10 @@ class NewPlaylist extends Component {
|
||||
this.setDescription(event.target.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input and make new playlist API request
|
||||
* @param {*} event
|
||||
*/
|
||||
handleSubmit(event){
|
||||
var name = this.state.name;
|
||||
this.setState({
|
||||
@ -92,9 +108,13 @@ class NewPlaylist extends Component {
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
<Grid container spacing={5}>
|
||||
|
||||
{/* TITLE */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h3">New</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* PLAYLIST TYPE DROPDOWN */}
|
||||
<Grid item xs={12} sm={4}>
|
||||
<FormControl variant="filled">
|
||||
<InputLabel htmlFor="type-select">Type</InputLabel>
|
||||
@ -114,6 +134,8 @@ class NewPlaylist extends Component {
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
{/* PLAYLIST NAME TEXTBOX */}
|
||||
<Grid item xs={12} sm={8}>
|
||||
<TextField
|
||||
label="Name"
|
||||
@ -123,11 +145,15 @@ class NewPlaylist extends Component {
|
||||
value={this.state.name}
|
||||
className="full-width" />
|
||||
</Grid>
|
||||
|
||||
{/* PLAYLIST DESCRIPTION TEXT */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body2" color="textSecondary">{ this.state.description }</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
|
||||
{/* SUBMIT BUTTON */}
|
||||
<CardActions>
|
||||
<Button variant="contained" color="primary" className="full-width" onClick={this.handleSubmit}>Create</Button>
|
||||
</CardActions>
|
||||
|
@ -7,8 +7,15 @@ const axios = require('axios');
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
/**
|
||||
* Top-level object for hosting playlist card grid with new/run all buttons
|
||||
*/
|
||||
class PlaylistsView extends Component {
|
||||
|
||||
/**
|
||||
* Trigger loading playlist data during init
|
||||
* @param {*} props Component properties
|
||||
*/
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
@ -20,6 +27,9 @@ class PlaylistsView extends Component {
|
||||
this.handleRunAll = this.handleRunAll.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playlist data from API and set state with results
|
||||
*/
|
||||
getPlaylists(){
|
||||
var self = this;
|
||||
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){
|
||||
axios.get('/api/user')
|
||||
.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){
|
||||
axios.delete('/api/playlist', { params: { name: name } })
|
||||
.then((response) => {
|
||||
@ -72,6 +92,10 @@ class PlaylistsView extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Post run all playlists action to API
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
handleRunAll(event){
|
||||
axios.get('/api/user')
|
||||
.then((response) => {
|
||||
@ -93,6 +117,8 @@ class PlaylistsView extends Component {
|
||||
|
||||
render() {
|
||||
|
||||
// Show spinning loading circle until loaded playlist data
|
||||
|
||||
const grid = <PlaylistGrid playlists={this.state.playlists}
|
||||
handleRunPlaylist={this.handleRunPlaylist}
|
||||
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){
|
||||
return (
|
||||
<Grid container
|
||||
@ -110,6 +141,8 @@ function PlaylistGrid(props){
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
style={{padding: '24px'}}>
|
||||
|
||||
{/* BUTTON BLOCK (NEW/RUN ALL) */}
|
||||
<Grid item xs={12} sm={6} md={2}>
|
||||
<ButtonGroup
|
||||
color="primary"
|
||||
@ -119,6 +152,8 @@ function PlaylistGrid(props){
|
||||
<Button onClick={props.handleRunAll}>Run All</Button>
|
||||
</ButtonGroup>
|
||||
</Grid>
|
||||
|
||||
{/* PLAYLIST CARDS */}
|
||||
{ props.playlists.length == 0 ? (
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<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){
|
||||
return (
|
||||
<Grid item xs>
|
||||
<Card>
|
||||
|
||||
{/* NAME TITLE */}
|
||||
<CardContent>
|
||||
<Typography variant="h4" component="h2">
|
||||
{ props.playlist.name }
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
{/* BUTTONS */}
|
||||
<CardActions>
|
||||
<ButtonGroup
|
||||
color="primary"
|
||||
variant="contained">
|
||||
|
||||
{/* VIEW */}
|
||||
<Button component={Link} to={getPlaylistLink(props.playlist.name)}>View</Button>
|
||||
|
||||
{/* RUN */}
|
||||
<Button onClick={(e) => props.handleRunPlaylist(props.playlist.name, e)}>Run</Button>
|
||||
|
||||
{/* DELETE */}
|
||||
<Button onClick={(e) => props.handleDeletePlaylist(props.playlist.name, e)}>Delete</Button>
|
||||
</ButtonGroup>
|
||||
</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){
|
||||
return `/app/playlist/${playlistName}/edit`;
|
||||
}
|
||||
|
@ -7,6 +7,9 @@ import showMessage from "../../Toast.js"
|
||||
|
||||
const LazyPieChart = React.lazy(() => import("../../Maths/PieChart"))
|
||||
|
||||
/**
|
||||
* Playlist count tab for presenting listening stats
|
||||
*/
|
||||
export class Count extends Component {
|
||||
|
||||
constructor(props){
|
||||
@ -34,6 +37,9 @@ export class Count extends Component {
|
||||
this.updateStats = this.updateStats.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playlist info with stats from API and set state if user has Last.fm username
|
||||
*/
|
||||
getUserInfo(){
|
||||
axios.get(`/api/playlist?name=${ this.state.name }`)
|
||||
.then((response) => {
|
||||
@ -51,6 +57,9 @@ export class Count extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make stats refresh request of API
|
||||
*/
|
||||
updateStats(){
|
||||
axios.get(`/api/spotfm/playlist/refresh?name=${ this.state.name }`)
|
||||
.then((response) => {
|
||||
@ -71,19 +80,29 @@ export class Count extends Component {
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
<Grid container>
|
||||
|
||||
{/* SCROBBLE COUNT */}
|
||||
<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>
|
||||
</Grid>
|
||||
|
||||
{/* ALBUM COUNT */}
|
||||
<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>
|
||||
</Grid>
|
||||
|
||||
{/* ARTIST COUNT */}
|
||||
<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>
|
||||
</Grid>
|
||||
|
||||
{/* LAST UPDATED */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body2">Last Updated <b>{this.state.playlist.lastfm_stat_last_refresh.toLocaleString()}</b></Typography>
|
||||
</Grid>
|
||||
<React.Suspense fallback={<LoadingMessage/>}>
|
||||
|
||||
{/* TRACK PIE */}
|
||||
<Grid item xs={12} sm={12} md={4}>
|
||||
<LazyPieChart data={[{
|
||||
"label": `${this.state.playlist.name} Tracks`,
|
||||
@ -92,8 +111,11 @@ export class Count extends Component {
|
||||
"label": 'Other',
|
||||
"value": 100 - this.state.playlist.lastfm_stat_percent
|
||||
}]}
|
||||
title={this.state.playlist.name}/>
|
||||
title={this.state.playlist.name}
|
||||
padding={50}/>
|
||||
</Grid>
|
||||
|
||||
{/* ALBUM PIE */}
|
||||
<Grid item xs={12} sm={12} md={4}>
|
||||
<LazyPieChart data={[{
|
||||
"label": `${this.state.playlist.name} Albums`,
|
||||
@ -102,8 +124,11 @@ export class Count extends Component {
|
||||
"label": 'Other',
|
||||
"value": 100 - this.state.playlist.lastfm_stat_album_percent
|
||||
}]}
|
||||
title={this.state.playlist.name}/>
|
||||
title={this.state.playlist.name}
|
||||
padding={50}/>
|
||||
</Grid>
|
||||
|
||||
{/* ARTIST PIE */}
|
||||
<Grid item xs={12} sm={12} md={4}>
|
||||
<LazyPieChart data={[{
|
||||
"label": `${this.state.playlist.name} Artists`,
|
||||
@ -112,11 +137,14 @@ export class Count extends Component {
|
||||
"label": 'Other',
|
||||
"value": 100 - this.state.playlist.lastfm_stat_artist_percent
|
||||
}]}
|
||||
title={this.state.playlist.name}/>
|
||||
title={this.state.playlist.name}
|
||||
padding={50}/>
|
||||
</Grid>
|
||||
</React.Suspense>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
|
||||
{/* UPDATE BUTTON */}
|
||||
<CardActions>
|
||||
<Button variant="contained" color="primary" className="full-width" onClick={this.updateStats}>Update</Button>
|
||||
</CardActions>
|
||||
|
@ -46,6 +46,9 @@ const useStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Main view/edit card for playlists
|
||||
*/
|
||||
export class Edit extends Component{
|
||||
|
||||
constructor(props){
|
||||
@ -84,6 +87,9 @@ export class Edit extends Component{
|
||||
this.makeNetworkUpdate = this.makeNetworkUpdate.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playlist info and all playlists from API, sort and set state
|
||||
*/
|
||||
componentDidMount(){
|
||||
axios.all([this.getPlaylistInfo(), this.getPlaylists()])
|
||||
.then(axios.spread((info, playlists) => {
|
||||
@ -120,14 +126,26 @@ export class Edit extends Component{
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API playlist info request
|
||||
* @returns Playlist info request
|
||||
*/
|
||||
getPlaylistInfo(){
|
||||
return axios.get(`/api/playlist?name=${ this.state.name }`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API list of playlist infos request
|
||||
* @returns Playlists info request
|
||||
*/
|
||||
getPlaylists(){
|
||||
return axios.get(`/api/playlists`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle input box state changes, make API requests
|
||||
* @param {*} event
|
||||
*/
|
||||
handleInputChange(event){
|
||||
|
||||
this.setState({
|
||||
@ -164,6 +182,10 @@ export class Edit extends Component{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle checkbox state changes, make API requests
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
handleCheckChange(event){
|
||||
|
||||
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){
|
||||
let payload = {
|
||||
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){
|
||||
|
||||
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){
|
||||
|
||||
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){
|
||||
var parts = this.state.parts;
|
||||
parts = parts.filter(e => e !== id);
|
||||
@ -269,6 +308,11 @@ export class Edit extends Component{
|
||||
this.makeNetworkUpdate({parts: parts});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle removing watched music tools playlist reference
|
||||
* @param {*} id Subject playlist name
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
handleRemoveReference(id, event){
|
||||
var playlist_references = this.state.playlist_references;
|
||||
playlist_references = playlist_references.filter(e => e !== id);
|
||||
@ -283,6 +327,10 @@ export class Edit extends Component{
|
||||
this.makeNetworkUpdate({playlist_references: playlist_references});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle refreshing playlist action, checks for spotify link
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
handleRun(event){
|
||||
axios.get('/api/user')
|
||||
.then((response) => {
|
||||
@ -305,21 +353,29 @@ export class Edit extends Component{
|
||||
render(){
|
||||
|
||||
var date = new Date();
|
||||
console.log("hello from edit");
|
||||
|
||||
const table = (
|
||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
|
||||
{/* PLAYLIST NAME TITLE */}
|
||||
<Typography variant="h2" color="textPrimary">{this.state.name}</Typography>
|
||||
<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> }
|
||||
{/* SMART 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> }
|
||||
{/* SPOTIFY PLAYLIST REFERENCES */}
|
||||
{ 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>
|
||||
|
||||
{/* SPOTIFY PLAYLIST TEXTBOX */}
|
||||
<Grid item xs={8} sm={8} md={3}>
|
||||
<TextField
|
||||
name="newPlaylistName"
|
||||
@ -330,9 +386,12 @@ export class Edit extends Component{
|
||||
|
||||
/>
|
||||
</Grid>
|
||||
{/* SPOTIFY ADD BUTTON */}
|
||||
<Grid item xs={4} sm={4} md={3}>
|
||||
<Button variant="contained" className="full-width" onClick={this.handleAddPart} style={{verticalAlign: 'middle'}}>Add</Button>
|
||||
</Grid>
|
||||
|
||||
{/* SMART PLAYLIST DROPDOWN */}
|
||||
<Grid item xs={8} sm={8} md={3}>
|
||||
<FormControl variant="filled" style={{verticalAlign: 'middle'}}>
|
||||
<InputLabel htmlFor="chart_range">Managed Playlist</InputLabel>
|
||||
@ -351,22 +410,31 @@ export class Edit extends Component{
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
{/* SMART ADD BUTTON */}
|
||||
<Grid item xs={4} sm={4} md={3}>
|
||||
<Button variant="contained" className="full-width" onClick={this.handleAddReference} style={{verticalAlign: 'middle'}}>Add</Button>
|
||||
</Grid>
|
||||
|
||||
{/* CHECKBOXES */}
|
||||
<Grid item xs={12}>
|
||||
|
||||
{/* SHUFFLE */}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch color="primary" name="shuffle" checked={this.state.shuffle} onChange={this.handleCheckChange} />
|
||||
}
|
||||
labelPlacement="bottom"
|
||||
label="Shuffle"/>
|
||||
|
||||
{/* RECOMMENDATIONS */}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch color="primary" checked={this.state.include_recommendations} name="include_recommendations" onChange={this.handleCheckChange} />
|
||||
}
|
||||
labelPlacement="bottom"
|
||||
label="Recommendations"/>
|
||||
|
||||
{/* LIBRARY TRACKS */}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<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"
|
||||
label="Library Tracks"/>
|
||||
</Grid>
|
||||
|
||||
{/* NUMBER OF RECOMMENDATIONS */}
|
||||
{ this.state.include_recommendations == true &&
|
||||
<Grid item xs={12}>
|
||||
<TextField type="number"
|
||||
@ -384,6 +454,8 @@ export class Edit extends Component{
|
||||
onChange={this.handleInputChange}></TextField>
|
||||
</Grid>
|
||||
}
|
||||
|
||||
{/* LAST.FM CHART LENGTH */}
|
||||
{ this.state.type == 'fmchart' &&
|
||||
<Grid item xs={12}>
|
||||
<TextField type="number"
|
||||
@ -394,6 +466,8 @@ export class Edit extends Component{
|
||||
onChange={this.handleInputChange}></TextField>
|
||||
</Grid>
|
||||
}
|
||||
|
||||
{/* LAST.FM CHART TIME RANGE */}
|
||||
{ this.state.type == 'fmchart' &&
|
||||
<Grid item xs={12}>
|
||||
<FormControl variant="filled">
|
||||
@ -416,6 +490,8 @@ export class Edit extends Component{
|
||||
</FormControl>
|
||||
</Grid>
|
||||
}
|
||||
|
||||
{/* RECENTS DAYS SINCE */}
|
||||
{ this.state.type == 'recents' &&
|
||||
<Grid item xs={12}>
|
||||
<TextField type="number"
|
||||
@ -426,7 +502,11 @@ export class Edit extends Component{
|
||||
onChange={this.handleInputChange} />
|
||||
</Grid>
|
||||
}
|
||||
|
||||
{/* THIS/LAST MONTH */}
|
||||
<Grid item xs={12}>
|
||||
|
||||
{/* THIS MONTH */}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<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"
|
||||
labelPlacement="bottom"
|
||||
/>
|
||||
|
||||
{/* LAST MONTH */}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<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"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* PLAYLIST TYPE */}
|
||||
<Grid item xs={12}>
|
||||
<FormControl variant="filled">
|
||||
<InputLabel htmlFor="type-select">Type</InputLabel>
|
||||
@ -461,6 +545,8 @@ export class Edit extends Component{
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
|
||||
{/* RUN PLAYLIST */}
|
||||
<CardActions>
|
||||
<Button onClick={this.handleRun} variant="contained" color="primary" className="full-width" >Run</Button>
|
||||
</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) {
|
||||
return <option value={props.name}>{props.name}</option>;
|
||||
// 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) {
|
||||
return <Grid container
|
||||
spacing={3}
|
||||
@ -489,6 +585,11 @@ function ListBlock(props) {
|
||||
</Grid>
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart/Spotify playlist card including name and delete button
|
||||
* @param {*} props Properties
|
||||
* @returns Card component wrapped in grid cell
|
||||
*/
|
||||
function BlockGridItem (props) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
|
@ -6,6 +6,9 @@ import { Paper, Tabs, Tab} from '@material-ui/core';
|
||||
import {Edit} from "./Edit.js";
|
||||
import {Count} from "./Count.js";
|
||||
|
||||
/**
|
||||
* Playlist view structure with tabs for view/editing and statistics
|
||||
*/
|
||||
class View extends Component{
|
||||
|
||||
constructor(props){
|
||||
@ -16,6 +19,11 @@ class View extends Component{
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tab change event
|
||||
* @param {*} e Event args
|
||||
* @param {*} newValue New tab object
|
||||
*/
|
||||
handleChange(e, newValue){
|
||||
this.setState({
|
||||
tab: newValue
|
||||
@ -33,12 +41,20 @@ class View extends Component{
|
||||
centered
|
||||
width="50%"
|
||||
>
|
||||
|
||||
{/* VIEW/EDIT */}
|
||||
<Tab label="Edit" component={Link} to={`${this.props.match.url}/edit`} />
|
||||
|
||||
{/* STATS */}
|
||||
<Tab label="Count" component={Link} to={`${this.props.match.url}/count`} />
|
||||
</Tabs>
|
||||
</Paper>
|
||||
<Switch>
|
||||
|
||||
{/* VIEW/EDIT */}
|
||||
<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}/>} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
@ -5,6 +5,9 @@ import { Card, Grid, Button, TextField, CardContent, CardActions, Typography } f
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
/**
|
||||
* Change password card
|
||||
*/
|
||||
class ChangePassword extends Component {
|
||||
|
||||
constructor(props){
|
||||
@ -20,22 +23,40 @@ class ChangePassword extends Component {
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle current pw state change
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
handleCurrentChange(event){
|
||||
this.setState({
|
||||
'current': event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle new pw state change
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
handleNewChange(event){
|
||||
this.setState({
|
||||
'new1': event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle new again pw state change
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
handleNew2Change(event){
|
||||
this.setState({
|
||||
'new2': event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle submit button click, validate input, make network request
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
handleSubmit(event){
|
||||
|
||||
if(this.state.current.length == 0){
|
||||
@ -70,9 +91,13 @@ class ChangePassword extends Component {
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<CardContent>
|
||||
<Grid container spacing={2}>
|
||||
|
||||
{/* TITLE */}
|
||||
<Grid item className="full-width">
|
||||
<Typography variant="h4" color="textPrimary">Change Password</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* CURRENT PASSWORD */}
|
||||
<Grid item className="full-width">
|
||||
<TextField
|
||||
label="Current Password"
|
||||
@ -83,6 +108,8 @@ class ChangePassword extends Component {
|
||||
value={this.state.current}
|
||||
className="full-width" />
|
||||
</Grid>
|
||||
|
||||
{/* NEW PASSWORD */}
|
||||
<Grid item className="full-width">
|
||||
<TextField
|
||||
label="New Password"
|
||||
@ -93,6 +120,8 @@ class ChangePassword extends Component {
|
||||
value={this.state.new1}
|
||||
className="full-width" />
|
||||
</Grid>
|
||||
|
||||
{/* NEW PASSWORD 2 */}
|
||||
<Grid item className="full-width">
|
||||
<TextField
|
||||
label="New Password Again"
|
||||
@ -103,9 +132,13 @@ class ChangePassword extends Component {
|
||||
value={this.state.new2}
|
||||
className="full-width" />
|
||||
</Grid>
|
||||
|
||||
{/* ERROR MESSAGE */}
|
||||
{ this.state.error && <Grid item><Typography variant="textSeondary">{this.state.errorValue}</Typography></Grid>}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
|
||||
{/* SUBMIT BUTTON */}
|
||||
<CardActions>
|
||||
<Button type="submit" variant="contained" className="full-width" onClick={this.runStats}>Change</Button>
|
||||
</CardActions>
|
||||
|
@ -5,6 +5,9 @@ import { Card, Button, CardContent, CardActions, Typography, TextField, Grid } f
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
/**
|
||||
* Last.fm username setting card
|
||||
*/
|
||||
class LastFM extends Component {
|
||||
|
||||
constructor(props){
|
||||
@ -19,6 +22,9 @@ class LastFM extends Component {
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user info from API, set current username to state
|
||||
*/
|
||||
getUserInfo(){
|
||||
axios.get('/api/user')
|
||||
.then((response) => {
|
||||
@ -39,12 +45,20 @@ class LastFM extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle input box state change
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
handleChange(event){
|
||||
this.setState({
|
||||
'lastfm_username': event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle submit button, post API change request
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
handleSubmit(event){
|
||||
|
||||
var username = this.state.lastfm_username;
|
||||
@ -72,9 +86,13 @@ class LastFM extends Component {
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<CardContent>
|
||||
<Grid container spacing={3}>
|
||||
|
||||
{/* TITLE */}
|
||||
<Grid item className="full-width">
|
||||
<Typography variant="h4" color="textPrimary">Last.fm Username</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* USERNAME TEXTBOX */}
|
||||
<Grid item className="full-width">
|
||||
<TextField
|
||||
label="last.fm Username"
|
||||
@ -86,6 +104,8 @@ class LastFM extends Component {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
|
||||
{/* SUBMIT BUTTON */}
|
||||
<CardActions>
|
||||
<Button type="submit" variant="contained" className="full-width">Save</Button>
|
||||
</CardActions>
|
||||
|
@ -6,6 +6,9 @@ import ChangePassword from "./ChangePassword.js";
|
||||
import SpotifyLink from "./SpotifyLink.js";
|
||||
import LastFM from "./LastFM.js";
|
||||
|
||||
/**
|
||||
* Settings card tabs structure for hosting password/spotify linked/last.fm username tabs
|
||||
*/
|
||||
class Settings extends Component {
|
||||
|
||||
constructor(props){
|
||||
@ -16,6 +19,11 @@ class Settings extends Component {
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tab change event
|
||||
* @param {*} e Event args
|
||||
* @param {*} newValue New tab object
|
||||
*/
|
||||
handleChange(e, newValue){
|
||||
this.setState({
|
||||
tab: newValue
|
||||
@ -33,14 +41,25 @@ class Settings extends Component {
|
||||
centered
|
||||
width="50%"
|
||||
>
|
||||
{/* PASSWORD */}
|
||||
<Tab label="Password" component={Link} to={`${this.props.match.url}/password`} />
|
||||
|
||||
{/* 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`} />
|
||||
</Tabs>
|
||||
</Paper>
|
||||
<Switch>
|
||||
|
||||
{/* PASSWORD */}
|
||||
<Route path={`${this.props.match.url}/password`} component={ChangePassword} />
|
||||
|
||||
{/* SPOTIFY */}
|
||||
<Route path={`${this.props.match.url}/spotify`} component={SpotifyLink} />
|
||||
|
||||
{/* LAST.FM */}
|
||||
<Route path={`${this.props.match.url}/lastfm`} component={LastFM} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
@ -5,6 +5,9 @@ import { Card, Button, CardContent, CardActions, Typography } from "@material-ui
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
/**
|
||||
* Spotify account link settings card
|
||||
*/
|
||||
class SpotifyLink extends Component {
|
||||
|
||||
constructor(props){
|
||||
@ -16,6 +19,9 @@ class SpotifyLink extends Component {
|
||||
this.getUserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user info from API and set spotify link status to state
|
||||
*/
|
||||
getUserInfo(){
|
||||
axios.get('/api/user')
|
||||
.then((response) => {
|
||||
@ -33,10 +39,14 @@ class SpotifyLink extends Component {
|
||||
const table =
|
||||
<div style={{maxWidth: '400px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
|
||||
{/* STATUS */}
|
||||
<CardContent>
|
||||
<Typography variant="h4" color="textPrimary">Admin Functions</Typography>
|
||||
<Typography variant="body2" color="textSecondary">Status: { this.state.spotify_linked ? "Linked" : "Unlinked" }</Typography>
|
||||
</CardContent>
|
||||
|
||||
{/* STATE CHANGE BUTTON */}
|
||||
<CardActions>
|
||||
{ this.state.spotify_linked ? <DeAuthButton /> : <AuthButton /> }
|
||||
</CardActions>
|
||||
@ -49,10 +59,20 @@ class SpotifyLink extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate Spotify account button component
|
||||
* @param {*} props Properties
|
||||
* @returns Button component
|
||||
*/
|
||||
function AuthButton(props) {
|
||||
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) {
|
||||
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"
|
||||
|
||||
/**
|
||||
* New tag card component
|
||||
*/
|
||||
class NewTag extends Component {
|
||||
|
||||
constructor(props) {
|
||||
@ -16,12 +19,20 @@ class NewTag extends Component {
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tag id input box state changes
|
||||
* @param {*} event
|
||||
*/
|
||||
handleInputChange(event){
|
||||
this.setState({
|
||||
tag_id: event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input, make new tag API request
|
||||
* @param {*} event
|
||||
*/
|
||||
handleSubmit(event){
|
||||
var tag_id = this.state.tag_id;
|
||||
this.setState({
|
||||
@ -58,9 +69,13 @@ class NewTag extends Component {
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
<Grid container spacing={5}>
|
||||
|
||||
{/* TITLE */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h3">New Tag</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* NAME TEXTBOX */}
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
label="Name"
|
||||
@ -72,6 +87,8 @@ class NewTag extends Component {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
|
||||
{/* SUBMIT BUTTON */}
|
||||
<CardActions>
|
||||
<Button variant="contained" color="primary" className="full-width" onClick={this.handleSubmit}>Create</Button>
|
||||
</CardActions>
|
||||
|
@ -7,6 +7,9 @@ const axios = require('axios');
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
/**
|
||||
* Tag card list component
|
||||
*/
|
||||
class TagList extends Component {
|
||||
|
||||
constructor(props){
|
||||
@ -18,6 +21,9 @@ class TagList extends Component {
|
||||
this.handleDeleteTag = this.handleDeleteTag.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags info from API
|
||||
*/
|
||||
getTags(){
|
||||
var self = this;
|
||||
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){
|
||||
axios.delete(`/api/tag/${tag_id}`)
|
||||
.then((response) => {
|
||||
@ -60,6 +71,11 @@ class TagList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag card grid component
|
||||
* @param {*} props Properties
|
||||
* @returns Grid component
|
||||
*/
|
||||
function TagGrid(props){
|
||||
return (
|
||||
<Grid container
|
||||
@ -69,6 +85,8 @@ function TagGrid(props){
|
||||
alignItems="flex-start"
|
||||
style={{padding: '24px'}}>
|
||||
<Grid item xs={12} sm={6} md={2}>
|
||||
|
||||
{/* NEW BUTTON */}
|
||||
<ButtonGroup
|
||||
color="primary"
|
||||
orientation="vertical"
|
||||
@ -76,6 +94,8 @@ function TagGrid(props){
|
||||
<Button component={Link} to='tags/new' >New</Button>
|
||||
</ButtonGroup>
|
||||
</Grid>
|
||||
|
||||
{/* TAG CARDS */}
|
||||
{ props.tags.length == 0 ? (
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<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){
|
||||
return (
|
||||
<Grid item xs>
|
||||
<Card>
|
||||
<CardContent>
|
||||
|
||||
{/* TAG NAME */}
|
||||
<Typography variant="h4" component="h2">
|
||||
{ props.tag.name }
|
||||
</Typography>
|
||||
|
||||
{/* COUNT */}
|
||||
{'count' in props.tag &&
|
||||
<Typography variant="h6" style={{color: "#b3b3b3"}}>
|
||||
{ props.tag.count }
|
||||
@ -108,7 +137,11 @@ function TagCard(props){
|
||||
<ButtonGroup
|
||||
color="primary"
|
||||
variant="contained">
|
||||
|
||||
{/* 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>
|
||||
</ButtonGroup>
|
||||
</CardActions>
|
||||
@ -117,6 +150,11 @@ function TagCard(props){
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map tag name to URL
|
||||
* @param {*} tagName Subject tag name
|
||||
* @returns Tag URL
|
||||
*/
|
||||
function getTagLink(tagName){
|
||||
return `/app/tag/${tagName}`;
|
||||
}
|
||||
|
@ -4,12 +4,18 @@ import { Route, Switch } from "react-router-dom";
|
||||
import TagList from "./TagList.js"
|
||||
import New from "./New.js"
|
||||
|
||||
/**
|
||||
* Tag router for directing between tag list and new
|
||||
*/
|
||||
class TagRouter extends Component {
|
||||
render(){
|
||||
return (
|
||||
<div>
|
||||
<Switch>
|
||||
{/* TAG LIST */}
|
||||
<Route exact path={`${this.props.match.url}/`} component={TagList} />
|
||||
|
||||
{/* NEW */}
|
||||
<Route path={`${this.props.match.url}/new`} component={New} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
@ -17,6 +17,9 @@ const useStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Tag View card
|
||||
*/
|
||||
class View extends Component{
|
||||
|
||||
constructor(props){
|
||||
@ -48,6 +51,9 @@ class View extends Component{
|
||||
this.handleChangeAddType = this.handleChangeAddType.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag info from API on load
|
||||
*/
|
||||
componentDidMount(){
|
||||
this.getTag();
|
||||
// var intervalId = setInterval(() => {this.getTag(false)}, 5000);
|
||||
@ -64,6 +70,10 @@ class View extends Component{
|
||||
// clearTimeout(this.state.timeoutId);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Get tag info from API
|
||||
* @param {*} error_toast Whether to show toast on network error
|
||||
*/
|
||||
getTag(error_toast = true){
|
||||
axios.get(`/api/tag/${ this.state.tag_id }`)
|
||||
.then( (response) => {
|
||||
@ -100,6 +110,10 @@ class View extends Component{
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle input box state changes
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
handleInputChange(event){
|
||||
|
||||
this.setState({
|
||||
@ -108,6 +122,10 @@ class View extends Component{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle checkbox state changes, make network updates
|
||||
* @param {*} event Event data
|
||||
*/
|
||||
handleCheckChange(event){
|
||||
let payload = {...this.state.tag};
|
||||
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){
|
||||
axios.put(`/api/tag/${this.state.tag_id}`, changes).catch((error) => {
|
||||
showMessage(`Error updating ${Object.keys(changes).join(", ")} (${error.response.status})`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input and make tag refresh update of API
|
||||
* @param {*} event
|
||||
*/
|
||||
handleRun(event){
|
||||
axios.get('/api/user')
|
||||
.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){
|
||||
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){
|
||||
this.setState({
|
||||
addType: type
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input, make tag part add request of API
|
||||
* @returns
|
||||
*/
|
||||
handleAdd(){
|
||||
|
||||
var addType = this.state.addType;
|
||||
@ -273,17 +313,27 @@ class View extends Component{
|
||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
|
||||
{/* TAG NAME TITLE */}
|
||||
<Typography variant="h2" color="textPrimary">{this.state.tag.name}</Typography>
|
||||
<Grid container spacing={5}>
|
||||
|
||||
{/* ARTISTS TITLE */}
|
||||
{ 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}/> }
|
||||
|
||||
{/* ALBUMS TITLE */}
|
||||
{ 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}/> }
|
||||
|
||||
{/* TRACKS TITLE */}
|
||||
{ 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}/> }
|
||||
|
||||
{/* NAME TEXTBOX */}
|
||||
<Grid item xs={12} sm={this.state.addType != 'artists' ? 3 : 4} md={this.state.addType != 'artists' ? 3 : 4}>
|
||||
<TextField
|
||||
name="name"
|
||||
@ -292,6 +342,7 @@ class View extends Component{
|
||||
value={this.state.name}
|
||||
onChange={this.handleInputChange}></TextField>
|
||||
</Grid>
|
||||
{/* ARTIST NAME TEXTBOX */}
|
||||
{ this.state.addType != 'artists' &&
|
||||
<Grid item xs={12} sm={3} md={4}>
|
||||
<TextField
|
||||
@ -302,6 +353,8 @@ class View extends Component{
|
||||
onChange={this.handleInputChange}></TextField>
|
||||
</Grid>
|
||||
}
|
||||
|
||||
{/* ADD TYPE DROPDOWN */}
|
||||
<Grid item xs={12} sm={this.state.addType != 'artists' ? 2 : 4} md={this.state.addType != 'artists' ? 2 : 4}>
|
||||
<FormControl variant="filled">
|
||||
<InputLabel htmlFor="addType">Type</InputLabel>
|
||||
@ -319,9 +372,13 @@ class View extends Component{
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
{/* ADD BUTTON */}
|
||||
<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>
|
||||
</Grid>
|
||||
|
||||
{/* TIME CHECKBOX */}
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@ -331,15 +388,22 @@ class View extends Component{
|
||||
labelPlacement="bottom"
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* PIE CHART */}
|
||||
<Grid item xs={12}>
|
||||
<PieChart data={data}/>
|
||||
<PieChart data={data} padding={100}/>
|
||||
</Grid>
|
||||
|
||||
{/* BAR CHART */}
|
||||
<Grid item xs={12}>
|
||||
<BarChart data={data} title='scrobbles'/>
|
||||
<BarChart data={data} title='scrobbles' indexAxis='y'/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
{/* UPDATE BUTTON */}
|
||||
<CardActions>
|
||||
<Button onClick={this.handleRun} variant="contained" color="primary" className="full-width" >Update</Button>
|
||||
</CardActions>
|
||||
@ -353,6 +417,11 @@ class View extends Component{
|
||||
|
||||
export default View;
|
||||
|
||||
/**
|
||||
* Grid component for holding artist/album/track cards
|
||||
* @param {*} props Properties
|
||||
* @returns Grid component
|
||||
*/
|
||||
function ListBlock(props) {
|
||||
return <Grid container
|
||||
spacing={3}
|
||||
@ -365,6 +434,11 @@ function ListBlock(props) {
|
||||
</Grid>
|
||||
}
|
||||
|
||||
/**
|
||||
* Track/album/artist card with time info and delete button
|
||||
* @param {*} props Properties
|
||||
* @returns Card component wrapped in grid cell
|
||||
*/
|
||||
function BlockGridItem (props) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
@ -372,19 +446,26 @@ function BlockGridItem (props) {
|
||||
<Card variant="outlined" className={classes.root}>
|
||||
<CardContent>
|
||||
<Grid>
|
||||
|
||||
{/* NAME TITLE */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h4" color="textSecondary" className={classes.root}>{ props.music_obj.name }</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* ARTIST NAME */}
|
||||
{ 'artist' in props.music_obj &&
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1" color="textSecondary" className={classes.root}>{ props.music_obj.artist }</Typography>
|
||||
</Grid>
|
||||
}
|
||||
|
||||
{/* SCROBBLE COUNT */}
|
||||
{ 'count' in props.music_obj &&
|
||||
<Grid item xs={8}>
|
||||
<Typography variant="h4" color="textPrimary" className={classes.root}>📈 { props.music_obj.count }</Typography>
|
||||
</Grid>
|
||||
}
|
||||
{/* TIME */}
|
||||
{ 'time' in props.music_obj && props.showTime &&
|
||||
<Grid item xs={8}>
|
||||
<Typography variant="body1" color="textSecondary" className={classes.root}>🕒 { props.music_obj.time }</Typography>
|
||||
@ -392,6 +473,8 @@ function BlockGridItem (props) {
|
||||
}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
|
||||
{/* DELETE BUTTON */}
|
||||
<CardActions>
|
||||
<Button className="full-width" color="secondary" variant="contained" aria-label="delete" onClick={(e) => props.handler(props.music_obj, props.addType, e)} startIcon={<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) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
@ -409,12 +497,18 @@ function StatsCard (props) {
|
||||
<Card variant="outlined" className={classes.root}>
|
||||
<CardContent>
|
||||
<Grid container spacing={10}>
|
||||
|
||||
{/* SCROBBLE COUNT */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h1" color="textPrimary" className={classes.root}>📈 { props.count }</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* PERCENT */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h4" color="textSecondary" className={classes.root}>{ props.proportion.toFixed(2) }%</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* TOTAL TIME */}
|
||||
{props.showTime &&
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h4" color="textSecondary" className={classes.root}>🕒 { props.time }</Typography>
|
||||
|
@ -3,4 +3,6 @@ import ReactDOM from "react-dom";
|
||||
|
||||
import MusicTools from "./MusicTools";
|
||||
|
||||
// ROOT file for bootstrapping the Music Tools component and app
|
||||
|
||||
ReactDOM.render(<MusicTools />, document.getElementById('react'));
|
@ -1,4 +1,6 @@
|
||||
|
||||
// login function for validating login data
|
||||
|
||||
function handleLogin(){
|
||||
var username = document.forms['login']['username'].value;
|
||||
var password = document.forms['login']['password'].value;
|
||||
|
@ -1,4 +1,6 @@
|
||||
|
||||
// login function for validating register data
|
||||
|
||||
function handleRegister(){
|
||||
var username = document.forms['register']['username'].value;
|
||||
var password = document.forms['register']['password'].value;
|
||||
|
Loading…
Reference in New Issue
Block a user