delete unused pages

This commit is contained in:
SrGooglo 2025-02-20 02:03:28 +00:00
parent b9ce363ef3
commit c4011f0674
6 changed files with 0 additions and 1821 deletions

View File

@ -1,35 +0,0 @@
import React from "react"
import * as antd from "antd"
import SyncModel from "@models/sync"
export default (props) => {
const [error, setError] = React.useState(null)
const makeSync = async () => {
const result = await SyncModel.spotifyCore.syncAuthCode(window.location.search.split("=")[1]).catch((err) => {
setError(err.message)
return false
})
if (result) {
window.close()
}
}
React.useEffect(() => {
makeSync()
}, [])
if (error) {
return <antd.Result
status="error"
title="Error while syncing your Spotify account"
subTitle={error}
/>
}
return <h3>Please wait meanwhile we are syncing your Spotify account</h3>
}

View File

@ -1,178 +0,0 @@
import React from "react"
import * as antd from "antd"
import { Icons } from "@components/Icons"
import UploadButton from "@components/UploadButton"
export default (props) => {
const [releaseName, setReleaseName] = React.useState(props.release.title)
const [releaseDescription, setReleaseDescription] = React.useState(props.release.description)
const [releaseThumbnail, setReleaseThumbnail] = React.useState(props.release.cover ?? props.release.thumbnail)
const [releaseVisibility, setReleaseVisibility] = React.useState(props.release.visibility)
const [releaseType, setReleaseType] = React.useState(props.release.type)
const handleReleaseTypeChange = (value) => {
setReleaseType(value)
props.onValueChange("type", value)
}
const handleTitleOnChange = (event) => {
setReleaseName(event.target.value)
props.onValueChange("title", event.target.value)
}
const handleDescriptionOnChange = (event) => {
setReleaseDescription(event.target.value)
props.onValueChange("description", event.target.value)
}
const handleCoverChange = (file) => {
setReleaseThumbnail(file.url)
props.onValueChange("cover", file.url)
}
const handleRemoveCover = () => {
setReleaseThumbnail(null)
props.onValueChange("cover", null)
}
const handleVisibilityChange = (value) => {
setReleaseVisibility(value)
props.onValueChange("public", value === "public")
}
return <div className="playlistCreator_layout_row">
<div
className="playlistCreator_layout_column"
>
<div className="field">
<div className="field_header">
<Icons.MdOutlineMusicNote />
<span>Title</span>
</div>
<antd.Input
className="inputText"
placeholder="Publish Title"
size="large"
bordered={false}
value={releaseName}
onChange={handleTitleOnChange}
maxLength={120}
/>
</div>
<div className="field">
<div className="field_header">
<Icons.MdOutlineDescription />
<span>Description</span>
</div>
<antd.Input.TextArea
className="inputText"
placeholder="Description (Support Markdown)"
bordered={false}
value={releaseDescription}
onChange={handleDescriptionOnChange}
maxLength={2500}
rows={4}
/>
</div>
<antd.Divider />
<div className="field">
<div className="field_header">
<Icons.IoMdRecording />
<span>Type</span>
</div>
<antd.Select
value={releaseType}
onChange={handleReleaseTypeChange}
defaultValue="album"
>
<antd.Select.Option value="album">Album</antd.Select.Option>
<antd.Select.Option value="ep">EP</antd.Select.Option>
<antd.Select.Option value="single">Single</antd.Select.Option>
</antd.Select>
</div>
<div className="field">
<div className="field_header">
<Icons.FiEye />
<span>Visibility</span>
</div>
<antd.Select
value={releaseVisibility}
onChange={handleVisibilityChange}
defaultValue={props.release.public ? "public" : "private"}
>
<antd.Select.Option value="public">Public</antd.Select.Option>
<antd.Select.Option value="private">Private</antd.Select.Option>
</antd.Select>
</div>
</div>
<div
className="playlistCreator_layout_column"
style={{
width: "50%",
maxWidth: "300px"
}}
>
<div className="field">
<div className="field_header">
<Icons.MdImage />
<span>Cover</span>
</div>
<div className="coverPreview">
<div className="coverPreview_preview">
<img src={releaseThumbnail ?? "/assets/no_song.png"} alt="Thumbnail" />
</div>
<div className="coverPreview_actions">
<UploadButton
onUploadDone={handleCoverChange}
multiple={false}
accept="image/*"
>
Upload cover
</UploadButton>
<antd.Button
onClick={handleRemoveCover}
disabled={!releaseThumbnail}
icon={<Icons.MdClose />}
type="text"
>
Remove Cover
</antd.Button>
</div>
</div>
</div>
<antd.Divider />
<div className="field">
{
props.release._id && <antd.Button
onClick={props.onDeletePlaylist}
icon={<Icons.MdDelete />}
danger
>
Delete Playlist
</antd.Button>
}
</div>
</div>
</div>
}

View File

@ -1,423 +0,0 @@
import React from "react"
import * as antd from "antd"
import classnames from "classnames"
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"
import UploadButton from "@components/UploadButton"
import { Icons } from "@components/Icons"
import MusicModel from "@models/music"
import "./index.less"
const UploadHint = (props) => {
return <div className="uploadHint">
<Icons.MdPlaylistAdd />
<p>Upload your tracks</p>
<p>Drag and drop your tracks here or click this box to start uploading files.</p>
</div>
}
const FileItemEditor = (props) => {
const [track, setTrack] = React.useState(props.track ?? {})
const handleChange = (key, value) => {
setTrack((oldData) => {
return {
...oldData,
[key]: value
}
})
}
const onRefreshCache = () => {
props.onRefreshCache(track)
}
const onClose = () => {
if (typeof props.close === "function") {
props.close()
}
}
const onSave = async () => {
await props.onSave(track)
if (typeof props.close === "function") {
props.close()
}
}
return <div className="fileItemEditor">
<div className="fileItemEditor_field">
<div className="fileItemEditor_field_header">
<Icons.MdImage />
<span>Thumbnail</span>
</div>
<div className="fileItemEditor_field_thumnail">
<img src={track.cover ?? track.thumbnail} />
</div>
<div className="fileItemEditor_actions">
<UploadButton
accept="image/*"
onUploadDone={(file) => handleChange("cover", file.url)}
/>
{
(track.cover ?? track.thumbnail) && <antd.Button
icon={<Icons.MdClose />}
type="text"
onClick={() => handleChange("cover", null)}
>
Remove
</antd.Button>
}
</div>
</div>
<div className="fileItemEditor_field">
<div className="fileItemEditor_field_header">
<Icons.MdOutlineMusicNote />
<span>Title</span>
</div>
<antd.Input
value={track.title}
placeholder="Track title"
onChange={(e) => handleChange("title", e.target.value)}
/>
</div>
<div className="fileItemEditor_field">
<div className="fileItemEditor_field_header">
<Icons.FiUser />
<span>Artist</span>
</div>
<antd.Input
value={track.artist}
placeholder="Artist"
onChange={(e) => handleChange("artist", e.target.value)}
/>
</div>
<div className="fileItemEditor_field">
<div className="fileItemEditor_field_header">
<Icons.MdAlbum />
<span>Album</span>
</div>
<antd.Input
value={track.album}
placeholder="Album"
onChange={(e) => handleChange("album", e.target.value)}
/>
</div>
<div className="fileItemEditor_field">
<div className="fileItemEditor_field_header">
<Icons.MdExplicit />
<span>Explicit</span>
</div>
<antd.Switch
checked={track.explicit}
onChange={(value) => handleChange("explicit", value)}
/>
</div>
<div className="fileItemEditor_field">
<div className="fileItemEditor_field_header">
<Icons.MdTimeline />
<span>Timestamps</span>
</div>
<antd.Button
disabled
>
Edit
</antd.Button>
</div>
<antd.Divider
style={{
margin: "5px 0",
}}
/>
<div className="fileItemEditor_field">
<div className="fileItemEditor_field_header">
<Icons.MdLyrics />
<span>Enable lyrics</span>
</div>
<antd.Switch
checked={track.lyricsEnabled}
onChange={(value) => handleChange("lyricsEnabled", value)}
/>
</div>
<div className="fileItemEditor_field">
<div className="fileItemEditor_field_header">
<Icons.MdTextFormat />
<span>Upload LRC</span>
</div>
<antd.Button
disabled
>
Upload
</antd.Button>
</div>
<div className="fileItemEditor_field">
<div className="fileItemEditor_field_header">
<Icons.FiTag />
<span>Spotify ID</span>
</div>
<antd.Input
value={track.spotifyId}
placeholder="ID"
onChange={(e) => handleChange("spotifyId", e.target.value)}
/>
</div>
<div className="fileItemEditor_actions">
{
track._id && <antd.Button
type="text"
icon={<Icons.MdRefresh />}
onClick={onRefreshCache}
>
Refresh Cache
</antd.Button>
}
<antd.Button
type="text"
icon={<Icons.MdClose />}
onClick={onClose}
>
Cancel
</antd.Button>
<antd.Button
type="primary"
icon={<Icons.MdCheck />}
onClick={onSave}
>
Save
</antd.Button>
</div>
</div>
}
const FileListItem = (props) => {
const isUploading = props.track.status === "uploading"
return <Draggable key={props.track.uid} draggableId={props.track.uid} index={props.index}>
{(provided, snapshot) => {
return <div
className={classnames(
"fileListItem",
{
["uploading"]: isUploading,
}
)}
ref={provided.innerRef}
{...provided.draggableProps}
>
{
isUploading &&
<Icons.LoadingOutlined
spin
className="fileListItem_loadingIcon"
/>
}
<div className={classnames(
"fileListItem_progress",
{
["hidden"]: !isUploading,
}
)}>
<antd.Progress
percent={props.track.progress ?? 0}
status={props.track.status === "error" ? "exception" : (props.track.progress === 100 ? "success" : "active")}
showInfo={false}
/>
</div>
<div className="fileListItem_cover">
<img
src={props.track.cover ?? props.track?.thumbnail}
alt="Track cover"
/>
</div>
<div className="fileListItem_details">
<div className="fileListItem_namings">
<h4>
<span
style={{
marginRight: "0.6rem",
fontSize: "0.7rem"
}}
>
{props.index + 1} -
</span>
{
props.track?.title ?? "Unknown title"
}
</h4>
<p
style={{
display: "flex",
alignItems: "center",
gap: "0.6rem",
}}
>
<span>
{
props.track?.artist ?? "Unknown artist"
}
</span>
<span>-</span>
<span>
{
props.track?.album ?? "Unknown album"
}
</span>
</p>
</div>
<div className="fileListItem_actions">
<antd.Button
type="primary"
icon={<Icons.MdEdit />}
onClick={props.onClickEdit}
disabled={isUploading}
/>
<antd.Popconfirm
title="Delete this track?"
description="Are you sure to delete this track?"
onConfirm={props.onClickRemove}
okText="Yes"
cancelText="No"
>
<antd.Button
icon={<Icons.MdDelete />}
/>
</antd.Popconfirm>
</div>
</div>
<div
{...provided.dragHandleProps}
className="fileListItem_dragHandle"
>
<Icons.MdDragIndicator />
</div>
</div>
}}
</Draggable>
}
export default (props) => {
const onClickEditTrack = (track) => {
app.layout.drawer.open("track_editor", FileItemEditor, {
type: "drawer",
props: {
width: "30vw",
minWidth: "600px",
},
componentProps: {
track,
onSave: (newTrackData) => {
console.log("Saving track", newTrackData)
props.handleTrackInfoChange(newTrackData.uid, newTrackData)
},
onRefreshCache: () => {
console.log("Refreshing cache for track", track.uid)
MusicModel.refreshTrackCache(track._id)
.catch(() => {
app.message.error("Failed to refresh cache for track")
})
.then(() => {
app.message.success("Successfully refreshed cache for track")
})
}
},
})
}
return <div className="tracksUploads">
<p>
Uploading files that are not permitted by our <a onClick={() => app.location.push("/terms")}>Terms of Service</a> may result in your account being suspended.
</p>
<div className="uploadBox">
<antd.Upload
className="uploader"
customRequest={props.handleUploadTrack}
onChange={props.onTrackUploaderChange}
accept="audio/*"
multiple
showUploadList={false}
>
{
props.fileList.length === 0 ?
<UploadHint /> : <antd.Button
className="uploadMoreButton"
icon={<Icons.FiPlus />}
/>
}
</antd.Upload>
<div className="fileList_wrapper">
<DragDropContext onDragEnd={props.handleTrackDragEnd}>
<Droppable droppableId="droppable">
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className="fileList"
>
{
props.trackList.map((track, index) => {
return <FileListItem
index={index}
track={track}
onClickChangeCover={() => {
return props.handleTrackCoverChange(track.uid)
}}
onTitleChange={(event) => {
return props.handleTrackInfoChange(track.uid, "title", event.target.value)
}}
onClickRemove={() => {
return props.handleTrackRemove(track.uid)
}}
onClickEdit={() => {
return onClickEditTrack(track)
}}
/>
})
}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
</div>
</div>
}

View File

@ -1,387 +0,0 @@
.tracksUploads {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
.uploadBox {
position: relative;
width: 100%;
//height: 100%;
padding: 10px;
overflow-y: hidden;
border: 1px solid var(--border-color);
border-radius: 8px;
.ant-upload {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
.uploadMoreButton {
width: 100%;
height: 30px;
margin-bottom: 10px;
background-color: transparent;
outline: 1px solid var(--border-color);
box-shadow: none;
&:hover {
height: 70px;
background-color: var(--background-color-accent);
}
}
}
.ant-upload-wrapper {
margin-bottom: 10px;
}
.fileList_wrapper {
position: relative;
border-radius: 8px;
overflow: hidden;
// add a blur effect on top and bottom to decorate overflown content
&::before,
&::after {
z-index: 50;
content: "";
position: absolute;
width: 100%;
height: 10px;
}
&::before {
top: 0;
left: 0;
background: linear-gradient(0deg,
rgba(255, 255, 255, 0) 0%,
var(--background-color-primary) 40%);
}
&::after {
bottom: 0;
left: 0;
background: linear-gradient(180deg,
rgba(255, 255, 255, 0) 0%,
var(--background-color-primary) 40%);
}
}
.fileList {
display: flex;
flex-direction: column;
max-height: 60vh;
overflow-y: scroll;
padding: 15px 0;
gap: 10px;
.fileListItem {
position: relative;
z-index: 49;
display: inline-flex;
flex-direction: row;
align-items: center;
padding: 10px;
border-radius: 8px;
background-color: var(--background-color-accent);
color: var(--text-color);
border: 1px solid var(--border-color);
gap: 10px;
&.uploading {
.fileListItem_cover {
img {
filter: blur(3px);
}
pointer-events: none;
}
}
.fileListItem_progress {
position: absolute;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
.ant-progress-line {
margin: 0 !important;
padding: 0 !important;
width: 40%;
}
&.hidden {
animation: hide 0.2s ease-in-out forwards;
animation-delay: 2s;
}
}
.fileListItem_loadingIcon {
position: absolute;
top: 0;
right: 0;
padding: 10px;
svg {
margin: 0 !important;
}
}
.ant-btn {
svg {
margin: 0 !important;
}
}
.fileListItem_cover {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.2s ease-in-out;
img {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 12px;
}
}
.fileListItem_details {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
margin-left: 20px;
width: 100%;
.fileListItem_namings {
width: 100%;
p {
font-size: 0.8rem;
}
}
.fileListItem_artist {
width: 100%;
span {
font-size: 0.8rem;
margin-right: 10px;
}
}
}
.fileListItem_actions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.ant-btn {
svg {
margin: 0 !important;
}
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
}
}
.fileListItem_dragHandle {
color: var(--text-color);
svg {
font-size: 1.5rem;
margin: 0 !important;
}
}
}
}
.uploadHint {
display: flex;
flex-direction: column;
align-self: center;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
svg {
font-size: 3rem;
}
h3 {
font-size: 1.5rem;
}
}
}
.fileItemEditor {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
.fileItemEditor_actions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
align-self: center;
gap: 10px;
}
.fileItemEditor_field {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
width: 100%;
.fileItemEditor_field_header {
display: inline-flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
width: 100%;
//margin-bottom: 10px;
h3 {
font-size: 1.2rem;
}
}
.fileItemEditor_field_actions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
}
.fileItemEditor_field_thumnail {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
img {
width: 15vw;
height: 15vw;
max-width: 330px;
max-height: 330px;
object-fit: cover;
border-radius: 12px;
background-color: black;
}
}
}
}
@keyframes hide {
0% {
opacity: 1;
}
99% {
opacity: 0;
width: fit-content;
}
100% {
width: 0px;
}
}

View File

@ -1,646 +0,0 @@
import React from "react"
import * as antd from "antd"
import jsmediatags from "jsmediatags/dist/jsmediatags.min.js"
import MusicModel from "@models/music"
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 PlaylistPublisherSteps extends React.Component {
state = {
releaseData: {
type: "album",
public: false,
},
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
})
})
},
orderTracksByFileName: () => {
let fileList = this.state.fileList
fileList = fileList.sort((a, b) => {
return a.name.localeCompare(b.name)
})
const trackList = fileList.map((file) => {
const track = this.state.trackList.find((track) => track.uid === file.uid)
return track
})
console.log(fileList, trackList)
this.setState({
fileList,
trackList
})
},
orderByArrayIndex: (order) => {
const trackList = this.state.trackList
let orderedTrackList = trackList.map((track, index) => {
// find in order
const orderIndex = order.findIndex((_track) => {
return _track.title === track.title && _track.artist === track.artist && _track.album === track.album
})
if (orderIndex === -1) {
track.order = index
} else {
track.order = orderIndex
}
return track
})
console.log(orderedTrackList)
orderedTrackList = orderedTrackList.sort((a, b) => {
return a.order - b.order
})
this.setState({
trackList: orderedTrackList
})
},
removeMetadataFromAllTracks: () => {
this.setState({
trackList: this.state.trackList.map((track) => {
delete track.metadata
return track
})
})
},
}
updateReleaseData = (key, value) => {
this.setState({
releaseData: {
...this.state.releaseData,
[key]: value
}
})
}
updateTrackList = (trackList) => {
this.setState({
trackList
})
}
canSubmit = () => {
const { releaseData, trackList, pendingTracksUpload } = this.state
const hasValidTitle = releaseData.title && releaseData.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 { releaseData: releaseData, trackList } = this.state
console.log(`Submitting playlist ${releaseData.title} with ${trackList.length} tracks`, releaseData, trackList)
const result = await MusicModel.putRelease({
...releaseData,
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)
}
})
})
}
handleFileProgress = (file, progress) => {
const trackList = this.state.trackList
const track = trackList.find((track) => track.uid === file.uid)
if (track) {
track.progress = progress
this.setState({
trackList
})
}
}
handleUploadTrack = async (req) => {
const response = await app.cores.remoteStorage.uploadFile(req.file, {
onProgress: this.handleFileProgress,
service: "premium-cdn"
}).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.release_id) {
console.error(`Cannot delete release without id`)
return
}
antd.Modal.confirm({
title: "Are you sure you want to delete this release?",
content: "This action cannot be undone",
okText: "Delete",
okType: "danger",
cancelText: "Cancel",
onOk: async () => {
const result = await MusicModel.deleteRelease(this.props.release_id, {
remove_with_tracks: true
})
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.releaseData.title === "string" && this.state.releaseData.title.length > 0
case 1:
return this.canSubmit()
default:
return true
}
}
componentDidMount() {
window._hacks = this._hacks
if (this.props.release_id) {
this.loadReleaseData(this.props.release_id)
} else {
this.setState({
loading: false
})
}
}
componentWillUnmount() {
delete window._hacks
}
loadReleaseData = async (id) => {
console.log(`Loading release data for ${id}...`)
const releaseData = await MusicModel.getReleaseData(id).catch((error) => {
console.error(error)
antd.message.error(error)
return false
})
console.log(releaseData)
if (releaseData) {
const trackList = releaseData.list.map((track) => {
return {
...track,
_id: track._id,
uid: track._id,
status: "done",
}
})
this.setState({
releaseData: releaseData,
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, {
release: this.state.releaseData,
trackList: this.state.trackList,
fileList: this.state.fileList,
onValueChange: (key, value) => {
this.updateReleaseData(key, value)
},
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>
}
}

View File

@ -1,152 +0,0 @@
.playlistCreator {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
//justify-content: center;
width: 50vw;
height: 100%;
min-width: 800px;
transition: all 0.3s ease-in-out;
.stepContent {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 20px 0;
}
.stepActions {
position: sticky;
bottom: 0;
right: 0;
display: flex;
flex-direction: row;
align-items: center;
//justify-content: flex-end;
width: 100%;
padding: 20px;
gap: 10px;
background-color: var(--background-color-accent);
border-radius: 12px;
}
.playlistCreator_layout_row {
display: flex;
flex-direction: row;
width: 100%;
gap: 20px;
.playlistCreator_layout_column {
display: flex;
flex-direction: column;
width: 100%;
gap: 20px;
}
}
.actions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 20px;
}
.field {
display: inline-flex;
flex-direction: column;
align-self: start;
width: 100%;
margin-bottom: 20px;
gap: 10px;
color: var(--text-color);
.field_header {
font-size: 1rem;
}
.inputText {
width: 100%;
background-color: transparent;
outline: 1px solid var(--border-color);
}
}
.coverPreview {
display: flex;
flex-direction: column;
gap: 10px;
.coverPreview_preview {
align-self: center;
width: 15vw;
height: 15vw;
max-width: 300px;
max-height: 300px;
img {
width: 15vw;
height: 15vw;
max-width: 300px;
max-height: 300px;
border-radius: 12px;
object-fit: cover;
}
}
.coverPreview_actions {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
width: 100%;
}
svg {
margin: 0 !important;
}
}
.ant-steps-icon {
svg {
margin: 0 !important;
}
}
}