mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-11 11:34:15 +00:00
575 lines
16 KiB
JavaScript
Executable File
575 lines
16 KiB
JavaScript
Executable File
import React from "react"
|
|
import * as antd from "antd"
|
|
import jsmediatags from "jsmediatags/dist/jsmediatags.min.js"
|
|
|
|
import PlaylistModel from "models/playlists"
|
|
|
|
import BasicInformation from "./components/BasicInformation"
|
|
import TracksUploads from "./components/TracksUploads"
|
|
|
|
import "./index.less"
|
|
|
|
const allowedTrackFieldChanges = [
|
|
"title",
|
|
"artist",
|
|
"cover",
|
|
"thumbnail",
|
|
"album",
|
|
"year",
|
|
"genre",
|
|
"comment",
|
|
"explicit",
|
|
"lyricsEnabled",
|
|
"spotifyId",
|
|
"public",
|
|
]
|
|
|
|
function createDefaultTrackData({
|
|
uid,
|
|
status = "uploading",
|
|
title,
|
|
artist,
|
|
album,
|
|
source,
|
|
cover = "https://storage.ragestudio.net/comty-static-assets/default_song.png",
|
|
lyricsEnabled = false,
|
|
explicit = false,
|
|
spotifyId = null,
|
|
}) {
|
|
return {
|
|
uid: uid,
|
|
title: title,
|
|
artist: artist,
|
|
album: album,
|
|
source: source,
|
|
status: status,
|
|
cover: cover,
|
|
lyricsEnabled: lyricsEnabled,
|
|
explicit: explicit,
|
|
spotifyId: spotifyId,
|
|
}
|
|
}
|
|
|
|
export default class PlaylistCreatorSteps extends React.Component {
|
|
state = {
|
|
playlistData: {},
|
|
|
|
fileList: [],
|
|
trackList: [],
|
|
pendingTracksUpload: [],
|
|
|
|
loading: true,
|
|
submitting: false,
|
|
|
|
currentStep: 0,
|
|
}
|
|
|
|
_hacks = {
|
|
revertTrackOrders: () => {
|
|
this.setState({
|
|
trackList: this.state.trackList.reverse()
|
|
})
|
|
},
|
|
enableLyricsForAllTracks: () => {
|
|
this.setState({
|
|
trackList: this.state.trackList.map((track) => {
|
|
track.lyricsEnabled = true
|
|
return track
|
|
})
|
|
})
|
|
},
|
|
disableLyricsForAllTracks: () => {
|
|
this.setState({
|
|
trackList: this.state.trackList.map((track) => {
|
|
track.lyricsEnabled = false
|
|
return track
|
|
})
|
|
})
|
|
},
|
|
}
|
|
|
|
updatePlaylistData = (key, value) => {
|
|
this.setState({
|
|
playlistData: {
|
|
...this.state.playlistData,
|
|
[key]: value
|
|
}
|
|
})
|
|
}
|
|
|
|
updateTrackList = (trackList) => {
|
|
this.setState({
|
|
trackList
|
|
})
|
|
}
|
|
|
|
canSubmit = () => {
|
|
const { playlistData, trackList, pendingTracksUpload } = this.state
|
|
|
|
const hasValidTitle = playlistData.title && playlistData.title.length > 0
|
|
const hasTracks = trackList.length > 0
|
|
const hasPendingUploads = pendingTracksUpload.length > 0
|
|
const tracksHasValidData = trackList.every((track) => {
|
|
return track.title !== null && track.title?.length !== 0 && track.source !== null && track.source?.length !== 0
|
|
})
|
|
|
|
return hasValidTitle && hasTracks && !hasPendingUploads && tracksHasValidData
|
|
}
|
|
|
|
submit = async () => {
|
|
this.setState({
|
|
submitting: true
|
|
})
|
|
|
|
const { playlistData, trackList } = this.state
|
|
|
|
console.log(`Submitting playlist ${playlistData.title} with ${trackList.length} tracks`, playlistData, trackList)
|
|
|
|
const result = await PlaylistModel.putPlaylist({
|
|
...playlistData,
|
|
list: trackList,
|
|
})
|
|
|
|
this.setState({
|
|
submitting: false
|
|
})
|
|
|
|
if (result) {
|
|
app.message.success("Playlist published")
|
|
|
|
if (typeof this.props.onModification === "function") {
|
|
this.props.onModification()
|
|
}
|
|
|
|
if (typeof this.props.close === "function") {
|
|
this.props.close()
|
|
}
|
|
}
|
|
}
|
|
|
|
// TRACK UPLOADS METHODS
|
|
analyzeTrackMetadata = async (file) => {
|
|
return new Promise((resolve, reject) => {
|
|
jsmediatags.read(file, {
|
|
onSuccess: (data) => {
|
|
return resolve(data)
|
|
},
|
|
onError: (error) => {
|
|
return reject(error)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
handleUploadTrack = async (req) => {
|
|
const response = await app.cores.remoteStorage.uploadFile(req.file, {
|
|
timeout: 2000
|
|
}).catch((error) => {
|
|
console.error(error)
|
|
antd.message.error(error)
|
|
|
|
req.onError(error)
|
|
|
|
return false
|
|
})
|
|
|
|
if (response) {
|
|
req.onSuccess(response)
|
|
}
|
|
}
|
|
|
|
handleTrackDragEnd = (result) => {
|
|
if (!result.destination) {
|
|
return
|
|
}
|
|
|
|
const trackList = this.state.trackList
|
|
|
|
const [removed] = trackList.splice(result.source.index, 1)
|
|
|
|
trackList.splice(result.destination.index, 0, removed)
|
|
|
|
this.setState({
|
|
trackList,
|
|
})
|
|
}
|
|
|
|
handleTrackRemove = (uid) => {
|
|
this.setState({
|
|
fileList: this.state.fileList.filter((file) => file.uid !== uid),
|
|
trackList: this.state.trackList.filter((file) => file.uid !== uid),
|
|
pendingTracksUpload: this.state.pendingTracksUpload.filter((file_uid) => file_uid !== uid)
|
|
})
|
|
}
|
|
|
|
handleTrackInfoChange = (uid, key, value) => {
|
|
if (!uid) {
|
|
console.error(`Cannot update track withouth uid`)
|
|
return
|
|
}
|
|
|
|
let trackList = this.state.trackList
|
|
|
|
const track = trackList.find((track) => track.uid === uid)
|
|
|
|
if (typeof key === "object") {
|
|
allowedTrackFieldChanges.forEach((_key) => {
|
|
if (typeof key[_key] !== "undefined") {
|
|
track[_key] = key[_key]
|
|
}
|
|
})
|
|
} else {
|
|
if (!allowedTrackFieldChanges.includes(key)) {
|
|
console.error(`Cannot update track with key ${key}`)
|
|
return
|
|
}
|
|
|
|
track[key] = value
|
|
}
|
|
|
|
this.setState({
|
|
trackList: trackList
|
|
})
|
|
|
|
console.log(`New data for track ${uid}: `, this.state.trackList.find((track) => track.uid === uid))
|
|
}
|
|
|
|
handleTrackCoverChange = async (uid, file) => {
|
|
if (!uid) {
|
|
console.error(`Cannot update track withouth uid`)
|
|
return
|
|
}
|
|
|
|
// upload cover file
|
|
const result = await app.cores.remoteStorage.uploadFile(file, {
|
|
timeout: 2000
|
|
})
|
|
|
|
console.log(`Uploaded cover for track ${uid}: `, result)
|
|
|
|
if (result) {
|
|
this.handleTrackInfoChange(uid, "cover", result.url)
|
|
}
|
|
}
|
|
|
|
handleDeletePlaylist = async () => {
|
|
if (!this.props.playlist_id) {
|
|
console.error(`Cannot delete playlist without id`)
|
|
return
|
|
}
|
|
|
|
antd.Modal.confirm({
|
|
title: "Are you sure you want to delete this playlist?",
|
|
content: "This action cannot be undone",
|
|
okText: "Delete",
|
|
okType: "danger",
|
|
cancelText: "Cancel",
|
|
onOk: async () => {
|
|
const result = await PlaylistModel.deletePlaylist(this.props.playlist_id)
|
|
|
|
if (result) {
|
|
app.message.success("Playlist deleted")
|
|
|
|
if (typeof this.props.onModification === "function") {
|
|
this.props.onModification()
|
|
}
|
|
|
|
if (typeof this.props.close === "function") {
|
|
this.props.close()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
onTrackUploaderChange = async (change) => {
|
|
switch (change.file.status) {
|
|
case "uploading": {
|
|
const { pendingTracksUpload } = this.state
|
|
|
|
if (!pendingTracksUpload.includes(change.file.uid)) {
|
|
pendingTracksUpload.push(change.file.uid)
|
|
}
|
|
|
|
const trackMetadata = await this.analyzeTrackMetadata(change.file.originFileObj)
|
|
.catch((error) => {
|
|
console.error(`Failed to analyze track metadata: `, error)
|
|
|
|
// return empty metadata
|
|
return {
|
|
tags: {}
|
|
}
|
|
})
|
|
|
|
console.log(trackMetadata)
|
|
|
|
if (trackMetadata.tags.picture) {
|
|
const data = trackMetadata.tags.picture.data
|
|
const format = trackMetadata.tags.picture.format
|
|
|
|
if (data && format) {
|
|
console.log(data, format)
|
|
|
|
const filenameExt = format.split("/")[1]
|
|
const filename = `cover.${filenameExt}`
|
|
|
|
const byteArray = new Uint8Array(data)
|
|
const blob = new Blob([byteArray], { type: data.type })
|
|
|
|
// create a file object
|
|
const file = new File([blob], filename, {
|
|
type: format,
|
|
})
|
|
|
|
console.log(file)
|
|
|
|
this.handleTrackCoverChange(change.file.uid, file)
|
|
}
|
|
}
|
|
|
|
this.setState({
|
|
pendingTracksUpload: pendingTracksUpload,
|
|
fileList: [...this.state.fileList, change.file],
|
|
trackList: [...this.state.trackList, createDefaultTrackData({
|
|
uid: change.file.uid,
|
|
title: trackMetadata.tags.title ?? change.file.name,
|
|
artist: trackMetadata.tags.artist ?? null,
|
|
album: trackMetadata.tags.album ?? null,
|
|
tags: trackMetadata.tags,
|
|
})]
|
|
})
|
|
|
|
break
|
|
}
|
|
case "done": {
|
|
// remove pending file
|
|
this.setState({
|
|
pendingTracksUpload: this.state.pendingTracksUpload.filter((uid) => uid !== change.file.uid)
|
|
})
|
|
|
|
// update file url in the track info
|
|
const track = this.state.trackList.find((file) => file.uid === change.file.uid)
|
|
|
|
if (track) {
|
|
track.source = change.file.response.url
|
|
track.status = "done"
|
|
}
|
|
|
|
this.setState({
|
|
trackList: this.state.trackList
|
|
})
|
|
|
|
break
|
|
}
|
|
case "error": {
|
|
// remove pending file
|
|
this.handleTrackRemove(change.file.uid)
|
|
|
|
// open a dialog to show the error and ask user to retry
|
|
antd.Modal.error({
|
|
title: "Upload failed",
|
|
content: "An error occurred while uploading the file. You want to retry?",
|
|
cancelText: "No",
|
|
okText: "Retry",
|
|
onOk: () => {
|
|
this.handleUploadTrack(change)
|
|
},
|
|
onCancel: () => {
|
|
this.handleTrackRemove(change.file.uid)
|
|
}
|
|
})
|
|
}
|
|
case "removed": {
|
|
this.handleTrackRemove(change.file.uid)
|
|
}
|
|
|
|
default: {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
steps = [
|
|
{
|
|
title: "Information",
|
|
crender: BasicInformation,
|
|
},
|
|
{
|
|
title: "Tracks",
|
|
crender: TracksUploads,
|
|
}
|
|
]
|
|
|
|
onChangeStep = (toStep) => {
|
|
// check if can change step
|
|
if (toStep > this.state.currentStep) {
|
|
if (!this.canNextStep()) {
|
|
return
|
|
}
|
|
}
|
|
|
|
this.setState({
|
|
currentStep: toStep
|
|
})
|
|
}
|
|
|
|
nextStep = () => {
|
|
if (!this.canNextStep()) {
|
|
return
|
|
}
|
|
|
|
const nextStep = this.state.currentStep + 1
|
|
|
|
if (nextStep >= this.steps.length) {
|
|
return this.submit()
|
|
}
|
|
|
|
this.setState({
|
|
currentStep: nextStep
|
|
})
|
|
}
|
|
|
|
previousStep = () => {
|
|
const previusStep = this.state.currentStep - 1
|
|
|
|
if (previusStep < 0) {
|
|
return
|
|
}
|
|
|
|
this.setState({
|
|
currentStep: previusStep
|
|
})
|
|
}
|
|
|
|
canNextStep = () => {
|
|
// check current step
|
|
switch (this.state.currentStep) {
|
|
case 0:
|
|
return typeof this.state.playlistData.title === "string" && this.state.playlistData.title.length > 0
|
|
case 1:
|
|
return this.canSubmit()
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
window._hacks = this._hacks
|
|
|
|
if (this.props.playlist_id) {
|
|
this.loadPlaylistData(this.props.playlist_id)
|
|
} else {
|
|
this.setState({
|
|
loading: false
|
|
})
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
delete window._hacks
|
|
}
|
|
|
|
loadPlaylistData = async (playlist_id) => {
|
|
console.log(`Loading playlist data for playlist ${playlist_id}...`)
|
|
|
|
const playlistData = await PlaylistModel.getPlaylist(playlist_id).catch((error) => {
|
|
console.error(error)
|
|
antd.message.error(error)
|
|
|
|
return false
|
|
})
|
|
|
|
if (playlistData) {
|
|
const trackList = playlistData.list.map((track) => {
|
|
return {
|
|
...track,
|
|
_id: track._id,
|
|
uid: track._id,
|
|
status: "done",
|
|
}
|
|
})
|
|
|
|
this.setState({
|
|
playlistData: playlistData,
|
|
trackList: trackList,
|
|
fileList: trackList.map((track) => {
|
|
return {
|
|
uid: track.uid,
|
|
name: track.title,
|
|
status: "done",
|
|
url: track.source
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
this.setState({
|
|
loading: false
|
|
})
|
|
}
|
|
|
|
render() {
|
|
if (this.state.loading) {
|
|
return <antd.Skeleton active />
|
|
}
|
|
|
|
return <div className="playlistCreator">
|
|
<antd.Steps
|
|
direction="horizontal"
|
|
current={this.state.currentStep}
|
|
onChange={this.onChangeStep}
|
|
items={this.steps}
|
|
/>
|
|
|
|
<div className="stepContent">
|
|
{
|
|
React.createElement(this.steps[this.state.currentStep].crender, {
|
|
playlist: this.state.playlistData,
|
|
|
|
trackList: this.state.trackList,
|
|
fileList: this.state.fileList,
|
|
|
|
onTitleChange: (title) => {
|
|
this.updatePlaylistData("title", title)
|
|
},
|
|
onDescriptionChange: (description) => {
|
|
this.updatePlaylistData("description", description)
|
|
},
|
|
onPlaylistCoverChange: (url) => {
|
|
this.updatePlaylistData("cover", url)
|
|
},
|
|
onVisibilityChange: (visibility) => {
|
|
this.updatePlaylistData("public", visibility)
|
|
},
|
|
onDeletePlaylist: this.handleDeletePlaylist,
|
|
|
|
handleUploadTrack: this.handleUploadTrack,
|
|
handleTrackDragEnd: this.handleTrackDragEnd,
|
|
handleTrackRemove: this.handleTrackRemove,
|
|
handleTrackInfoChange: this.handleTrackInfoChange,
|
|
onTrackUploaderChange: this.onTrackUploaderChange,
|
|
})
|
|
}
|
|
</div>
|
|
|
|
<div className="stepActions">
|
|
<antd.Button
|
|
onClick={this.previousStep}
|
|
disabled={this.state.currentStep === 0}
|
|
>
|
|
Previous
|
|
</antd.Button>
|
|
|
|
<antd.Button
|
|
type="primary"
|
|
onClick={this.nextStep}
|
|
disabled={!this.canNextStep()}
|
|
>
|
|
{
|
|
this.state.currentStep === this.steps.length - 1 ? "Finish" : "Next"
|
|
}
|
|
</antd.Button>
|
|
</div>
|
|
</div>
|
|
}
|
|
} |