use swappy

This commit is contained in:
SrGooglo 2025-02-11 16:15:16 +00:00
parent 7ae3c19e7d
commit bca40318bd
2 changed files with 354 additions and 338 deletions

View File

@ -12,103 +12,94 @@ import { ReleaseEditorStateContext } from "@contexts/MusicReleaseEditor"
import "./index.less" import "./index.less"
const TrackListItem = (props) => { const TrackListItem = (props) => {
const context = React.useContext(ReleaseEditorStateContext) const context = React.useContext(ReleaseEditorStateContext)
const [loading, setLoading] = React.useState(false) const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState(null) const [error, setError] = React.useState(null)
const { track } = props const { track } = props
async function onClickEditTrack() { async function onClickEditTrack() {
context.renderCustomPage({ context.renderCustomPage({
header: "Track Editor", header: "Track Editor",
content: <TrackEditor />, content: <TrackEditor />,
props: { props: {
track: track, track: track,
} },
}) })
} }
async function onClickRemoveTrack() { async function onClickRemoveTrack() {
props.onDelete(track.uid) props.onDelete(track.uid)
} }
return <Draggable console.log("render")
key={track._id ?? track.id}
draggableId={track.id ?? track._id}
index={props.index}
>
{
(provided, snapshot) => {
return <div
className={classnames(
"music-studio-release-editor-tracks-list-item",
{
["loading"]: loading,
["failed"]: !!error,
["disabled"]: props.disabled,
}
)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
className="music-studio-release-editor-tracks-list-item-progress"
style={{
"--upload-progress": `${props.uploading.progress}%`,
}}
/>
<div className="music-studio-release-editor-tracks-list-item-index"> return (
<span>{props.index + 1}</span> <div
</div> className={classnames(
"music-studio-release-editor-tracks-list-item",
{
["loading"]: loading,
["failed"]: !!error,
["disabled"]: props.disabled,
},
)}
data-swapy-item={track.id ?? track._id}
>
<div
className="music-studio-release-editor-tracks-list-item-progress"
style={{
"--upload-progress": `${props.uploading.progress}%`,
}}
/>
{ <div className="music-studio-release-editor-tracks-list-item-index">
props.uploading.working && <Icons.LoadingOutlined /> <span>{props.index + 1}</span>
} </div>
<Image {props.uploading.working && <Icons.LoadingOutlined />}
src={track.cover}
height={25}
width={25}
style={{
borderRadius: 8,
}}
/>
<span>{track.title}</span> <Image
src={track.cover}
height={25}
width={25}
style={{
borderRadius: 8,
}}
/>
<div className="music-studio-release-editor-tracks-list-item-actions"> <span>{track.title}</span>
<antd.Popconfirm
title="Are you sure you want to delete this track?"
onConfirm={onClickRemoveTrack}
okText="Yes"
disabled={props.disabled}
>
<antd.Button
type="ghost"
icon={<Icons.FiTrash2 />}
disabled={props.disabled}
/>
</antd.Popconfirm>
<antd.Button
type="ghost"
icon={<Icons.FiEdit2 />}
onClick={onClickEditTrack}
disabled={props.disabled}
/>
<div <div className="music-studio-release-editor-tracks-list-item-actions">
{...provided.dragHandleProps} <antd.Popconfirm
className="music-studio-release-editor-tracks-list-item-dragger" title="Are you sure you want to delete this track?"
> onConfirm={onClickRemoveTrack}
<Icons.MdDragIndicator /> okText="Yes"
</div> disabled={props.disabled}
</div> >
</div> <antd.Button
} type="ghost"
} icon={<Icons.FiTrash2 />}
</Draggable> disabled={props.disabled}
/>
</antd.Popconfirm>
<antd.Button
type="ghost"
icon={<Icons.FiEdit2 />}
onClick={onClickEditTrack}
disabled={props.disabled}
/>
<div
data-swapy-handle
className="music-studio-release-editor-tracks-list-item-dragger"
>
<Icons.MdDragIndicator />
</div>
</div>
</div>
)
} }
export default TrackListItem export default TrackListItem

View File

@ -2,6 +2,7 @@ import React from "react"
import * as antd from "antd" import * as antd from "antd"
import classnames from "classnames" import classnames from "classnames"
import { DragDropContext, Droppable } from "react-beautiful-dnd" import { DragDropContext, Droppable } from "react-beautiful-dnd"
import { createSwapy } from "swapy"
import TrackManifest from "@cores/player/classes/TrackManifest" import TrackManifest from "@cores/player/classes/TrackManifest"
@ -13,314 +14,338 @@ import UploadHint from "./components/UploadHint"
import "./index.less" import "./index.less"
class TracksManager extends React.Component { class TracksManager extends React.Component {
state = { swapyRef = React.createRef()
list: Array.isArray(this.props.list) ? this.props.list : [],
pendingUploads: [],
}
componentDidUpdate = (prevProps, prevState) => { state = {
if (prevState.list !== this.state.list) { list: Array.isArray(this.props.list) ? this.props.list : [],
if (typeof this.props.onChangeState === "function") { pendingUploads: [],
this.props.onChangeState(this.state) }
}
}
}
findTrackByUid = (uid) => { componentDidUpdate = (prevProps, prevState) => {
if (!uid) { if (prevState.list !== this.state.list) {
return false if (typeof this.props.onChangeState === "function") {
} this.props.onChangeState(this.state)
}
}
}
return this.state.list.find((item) => item.uid === uid) componentDidMount() {
} this.swapyRef.current = createSwapy(
document.getElementById("editor-tracks-list"),
{
animation: "dynamic",
dragAxis: "y",
},
)
addTrackToList = (track) => { this.swapyRef.current.onSwapEnd((event) => {
if (!track) { console.log("end", event)
return false this.orderTrackList(
} event.slotItemMap.asArray.map((item) => item.item),
)
})
}
this.setState({ componentWillUnmount() {
list: [...this.state.list, track], this.swapyRef.current.destroy()
}) }
}
removeTrackByUid = (uid) => { findTrackByUid = (uid) => {
if (!uid) { if (!uid) {
return false return false
} }
this.removeTrackUIDFromPendingUploads(uid) return this.state.list.find((item) => item.uid === uid)
}
this.setState({ addTrackToList = (track) => {
list: this.state.list.filter((item) => item.uid !== uid), if (!track) {
}) return false
}
} this.setState({
list: [...this.state.list, track],
})
}
modifyTrackByUid = (uid, track) => { removeTrackByUid = (uid) => {
console.log("modifyTrackByUid", uid, track) if (!uid) {
if (!uid || !track) { return false
return false }
}
this.setState({ this.removeTrackUIDFromPendingUploads(uid)
list: this.state.list.map((item) => {
if (item.uid === uid) {
return {
...item,
...track,
}
}
return item this.setState({
}), list: this.state.list.filter((item) => item.uid !== uid),
}) })
} }
addTrackUIDToPendingUploads = (uid) => { modifyTrackByUid = (uid, track) => {
if (!uid) { console.log("modifyTrackByUid", uid, track)
return false if (!uid || !track) {
} return false
}
const pendingUpload = this.state.pendingUploads.find((item) => item.uid === uid) this.setState({
list: this.state.list.map((item) => {
if (item.uid === uid) {
return {
...item,
...track,
}
}
if (!pendingUpload) { return item
this.setState({ }),
pendingUploads: [ })
...this.state.pendingUploads, }
{
uid: uid,
progress: 0
}
],
})
}
}
removeTrackUIDFromPendingUploads = (uid) => { addTrackUIDToPendingUploads = (uid) => {
if (!uid) { if (!uid) {
return false return false
} }
this.setState({ const pendingUpload = this.state.pendingUploads.find(
pendingUploads: this.state.pendingUploads.filter((item) => item.uid !== uid), (item) => item.uid === uid,
}) )
}
getUploadProgress = (uid) => { if (!pendingUpload) {
const uploadProgressIndex = this.state.pendingUploads.findIndex((item) => item.uid === uid) this.setState({
pendingUploads: [
...this.state.pendingUploads,
{
uid: uid,
progress: 0,
},
],
})
}
}
if (uploadProgressIndex === -1) { removeTrackUIDFromPendingUploads = (uid) => {
return 0 if (!uid) {
} return false
}
return this.state.pendingUploads[uploadProgressIndex].progress this.setState({
} pendingUploads: this.state.pendingUploads.filter(
(item) => item.uid !== uid,
),
})
}
updateUploadProgress = (uid, progress) => { getUploadProgress = (uid) => {
const uploadProgressIndex = this.state.pendingUploads.findIndex((item) => item.uid === uid) const uploadProgressIndex = this.state.pendingUploads.findIndex(
(item) => item.uid === uid,
)
if (uploadProgressIndex === -1) { if (uploadProgressIndex === -1) {
return false return 0
} }
const newData = [...this.state.pendingUploads] return this.state.pendingUploads[uploadProgressIndex].progress
}
newData[uploadProgressIndex].progress = progress updateUploadProgress = (uid, progress) => {
const uploadProgressIndex = this.state.pendingUploads.findIndex(
(item) => item.uid === uid,
)
console.log(`Updating progress for [${uid}] to [${progress}]`) if (uploadProgressIndex === -1) {
return false
}
this.setState({ const newData = [...this.state.pendingUploads]
pendingUploads: newData,
})
}
handleUploaderStateChange = async (change) => { newData[uploadProgressIndex].progress = progress
const uid = change.file.uid
console.log("handleUploaderStateChange", change) console.log(`Updating progress for [${uid}] to [${progress}]`)
switch (change.file.status) { this.setState({
case "uploading": { pendingUploads: newData,
this.addTrackUIDToPendingUploads(uid) })
}
const trackManifest = new TrackManifest({ handleUploaderStateChange = async (change) => {
uid: uid, const uid = change.file.uid
file: change.file,
onChange: this.modifyTrackByUid
})
this.addTrackToList(trackManifest) console.log("handleUploaderStateChange", change)
break switch (change.file.status) {
} case "uploading": {
case "done": { this.addTrackUIDToPendingUploads(uid)
// remove pending file
this.removeTrackUIDFromPendingUploads(uid)
let trackManifest = this.state.list.find((item) => item.uid === uid) const trackManifest = new TrackManifest({
uid: uid,
file: change.file,
onChange: this.modifyTrackByUid,
})
if (!trackManifest) { this.addTrackToList(trackManifest)
console.error(`Track with uid [${uid}] not found!`)
break
}
// // update track list break
// await this.modifyTrackByUid(uid, { }
// source: change.file.response.url case "done": {
// }) // remove pending file
this.removeTrackUIDFromPendingUploads(uid)
trackManifest.source = change.file.response.url let trackManifest = this.state.list.find(
trackManifest = await trackManifest.initialize() (item) => item.uid === uid,
)
await this.modifyTrackByUid(uid, trackManifest) if (!trackManifest) {
console.error(`Track with uid [${uid}] not found!`)
break
}
break // // update track list
} // await this.modifyTrackByUid(uid, {
case "error": { // source: change.file.response.url
// remove pending file // })
this.removeTrackUIDFromPendingUploads(uid)
// remove from tracklist trackManifest.source = change.file.response.url
await this.removeTrackByUid(uid) trackManifest = await trackManifest.initialize()
}
case "removed": {
// stop upload & delete from pending list and tracklist
await this.removeTrackByUid(uid)
}
default: {
break
}
}
}
uploadToStorage = async (req) => { await this.modifyTrackByUid(uid, trackManifest)
const response = await app.cores.remoteStorage.uploadFile(req.file, {
onProgress: this.handleTrackFileUploadProgress,
service: "b2",
headers: {
transmux: "a-dash"
}
}).catch((error) => {
console.error(error)
antd.message.error(error)
req.onError(error) break
}
case "error": {
// remove pending file
this.removeTrackUIDFromPendingUploads(uid)
return false // remove from tracklist
}) await this.removeTrackByUid(uid)
}
case "removed": {
// stop upload & delete from pending list and tracklist
await this.removeTrackByUid(uid)
}
default: {
break
}
}
}
if (response) { uploadToStorage = async (req) => {
req.onSuccess(response) const response = await app.cores.remoteStorage
} .uploadFile(req.file, {
} onProgress: this.handleTrackFileUploadProgress,
service: "b2",
headers: {
transmux: "a-dash",
},
})
.catch((error) => {
console.error(error)
antd.message.error(error)
handleTrackFileUploadProgress = async (file, progress) => { req.onError(error)
this.updateUploadProgress(file.uid, progress)
}
orderTrackList = (result) => { return false
if (!result.destination) { })
return
}
this.setState((prev) => { if (response) {
const trackList = [...prev.list] req.onSuccess(response)
}
}
const [removed] = trackList.splice(result.source.index, 1) handleTrackFileUploadProgress = async (file, progress) => {
this.updateUploadProgress(file.uid, progress)
}
trackList.splice(result.destination.index, 0, removed) orderTrackList = (orderedIdsArray) => {
this.setState((prev) => {
// move all list items by id
const orderedIds = orderedIdsArray.map((id) =>
this.state.list.find((item) => item._id === id),
)
console.log("orderedIds", orderedIds)
return {
list: orderedIds,
}
})
}
return { render() {
list: trackList console.log(`Tracks List >`, this.state.list)
}
})
}
render() { return (
console.log(`Tracks List >`, this.state.list) <div className="music-studio-release-editor-tracks">
<antd.Upload
className="music-studio-tracks-uploader"
onChange={this.handleUploaderStateChange}
customRequest={this.uploadToStorage}
showUploadList={false}
accept="audio/*"
multiple
>
{this.state.list.length === 0 ? (
<UploadHint />
) : (
<antd.Button
className="uploadMoreButton"
icon={<Icons.FiPlus />}
>
Add another
</antd.Button>
)}
</antd.Upload>
return <div className="music-studio-release-editor-tracks"> <div
<antd.Upload id="editor-tracks-list"
className="music-studio-tracks-uploader" className="music-studio-release-editor-tracks-list"
onChange={this.handleUploaderStateChange} >
customRequest={this.uploadToStorage} {this.state.list.length === 0 && (
showUploadList={false} <antd.Result status="info" title="No tracks" />
accept="audio/*" )}
multiple
>
{
this.state.list.length === 0 ?
<UploadHint /> : <antd.Button
className="uploadMoreButton"
icon={<Icons.FiPlus />}
>
Add another
</antd.Button>
}
</antd.Upload>
<DragDropContext {this.state.list.map((track, index) => {
onDragEnd={this.orderTrackList} const progress = this.getUploadProgress(track.uid)
>
<Droppable
droppableId="droppable"
>
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className="music-studio-release-editor-tracks-list"
>
{
this.state.list.length === 0 && <antd.Result
status="info"
title="No tracks"
/>
}
{
this.state.list.map((track, index) => {
const progress = this.getUploadProgress(track.uid)
return <TrackListItem return (
index={index} <div data-swapy-slot={track._id ?? track.uid}>
track={track} <TrackListItem
onEdit={this.modifyTrackByUid} index={index}
onDelete={this.removeTrackByUid} track={track}
uploading={{ onEdit={this.modifyTrackByUid}
progress: progress, onDelete={this.removeTrackByUid}
working: this.state.pendingUploads.find((item) => item.uid === track.uid) uploading={{
}} progress: progress,
disabled={progress > 0} working: this.state.pendingUploads.find(
/> (item) => item.uid === track.uid,
}) ),
} }}
{provided.placeholder} disabled={progress > 0}
</div> />
)} </div>
</Droppable> )
</DragDropContext> })}
</div> </div>
} </div>
)
}
} }
const ReleaseTracks = (props) => { const ReleaseTracks = (props) => {
const { state, setState } = props const { state, setState } = props
return <div className="music-studio-release-editor-tab"> return (
<h1>Tracks</h1> <div className="music-studio-release-editor-tab">
<h1>Tracks</h1>
<TracksManager <TracksManager
_id={state._id} _id={state._id}
list={state.list} list={state.list}
onChangeState={(managerState) => { onChangeState={(managerState) => {
setState({ setState({
...state, ...state,
...managerState ...managerState,
}) })
}} }}
/> />
</div> </div>
)
} }
export default ReleaseTracks export default ReleaseTracks