Add change tracking and update to use "items" property

This commit is contained in:
SrGooglo 2025-04-24 06:13:37 +00:00
parent d738995054
commit 74021f38b6
4 changed files with 450 additions and 388 deletions

View File

@ -1,15 +1,17 @@
import React from "react"
import * as antd from "antd"
import { Icons, createIconRender } from "@components/Icons"
import MusicModel from "@models/music"
import compareObjectsByProperties from "@utils/compareObjectsByProperties"
import useUrlQueryActiveKey from "@hooks/useUrlQueryActiveKey"
import TrackManifest from "@cores/player/classes/TrackManifest"
import { DefaultReleaseEditorState, ReleaseEditorStateContext } from "@contexts/MusicReleaseEditor"
import {
DefaultReleaseEditorState,
ReleaseEditorStateContext,
} from "@contexts/MusicReleaseEditor"
import Tabs from "./tabs"
@ -25,14 +27,17 @@ const ReleaseEditor = (props) => {
const [submitError, setSubmitError] = React.useState(null)
const [loadError, setLoadError] = React.useState(null)
const [globalState, setGlobalState] = React.useState(DefaultReleaseEditorState)
const [globalState, setGlobalState] = React.useState(
DefaultReleaseEditorState,
)
const [initialValues, setInitialValues] = React.useState({})
const [customPage, setCustomPage] = React.useState(null)
const [customPageActions, setCustomPageActions] = React.useState([])
const [selectedTab, setSelectedTab] = useUrlQueryActiveKey({
defaultKey: "info",
queryKey: "tab"
queryKey: "tab",
})
async function initialize() {
@ -43,8 +48,8 @@ const ReleaseEditor = (props) => {
try {
let releaseData = await MusicModel.getReleaseData(release_id)
if (Array.isArray(releaseData.list)) {
releaseData.list = releaseData.list.map((item) => {
if (Array.isArray(releaseData.items)) {
releaseData.items = releaseData.items.map((item) => {
return new TrackManifest(item)
})
}
@ -53,6 +58,8 @@ const ReleaseEditor = (props) => {
...globalState,
...releaseData,
})
setInitialValues(releaseData)
} catch (error) {
setLoadError(error)
}
@ -61,6 +68,22 @@ const ReleaseEditor = (props) => {
setLoading(false)
}
function hasChanges() {
const stagedChanges = {
title: globalState.title,
type: globalState.type,
public: globalState.public,
cover: globalState.cover,
items: globalState.items,
}
return !compareObjectsByProperties(
stagedChanges,
initialValues,
Object.keys(stagedChanges),
)
}
async function renderCustomPage(page, actions) {
setCustomPage(page ?? null)
setCustomPageActions(actions ?? [])
@ -71,11 +94,15 @@ const ReleaseEditor = (props) => {
setSubmitError(null)
try {
console.log("Submitting Tracks")
// first sumbit tracks
const tracks = await MusicModel.putTrack({
list: globalState.list,
items: globalState.items,
})
console.log("Submitting release")
// then submit release
const result = await MusicModel.putRelease({
_id: globalState._id,
@ -85,7 +112,7 @@ const ReleaseEditor = (props) => {
cover: globalState.cover,
explicit: globalState.explicit,
type: globalState.type,
list: tracks.list.map((item) => item._id),
items: tracks.items.map((item) => item._id),
})
app.location.push(`/studio/music/${result._id}`)
@ -109,13 +136,15 @@ const ReleaseEditor = (props) => {
descriptionText: "This action cannot be undone.",
onConfirm: async () => {
await MusicModel.deleteRelease(globalState._id)
app.location.push(window.location.pathname.split("/").slice(0, -1).join("/"))
app.location.push(
window.location.pathname.split("/").slice(0, -1).join("/"),
)
},
})
}
async function canFinish() {
return true
function canFinish() {
return hasChanges()
}
React.useEffect(() => {
@ -123,11 +152,13 @@ const ReleaseEditor = (props) => {
}, [])
if (loadError) {
return <antd.Result
return (
<antd.Result
status="warning"
title="Error"
subTitle={loadError.message}
/>
)
}
if (loading) {
@ -139,10 +170,11 @@ const ReleaseEditor = (props) => {
const CustomPageProps = {
close: () => {
renderCustomPage(null, null)
}
},
}
return <ReleaseEditorStateContext.Provider
return (
<ReleaseEditorStateContext.Provider
value={{
...globalState,
setGlobalState,
@ -151,59 +183,67 @@ const ReleaseEditor = (props) => {
}}
>
<div className="music-studio-release-editor">
{
customPage && <div className="music-studio-release-editor-custom-page">
{
customPage.header && <div className="music-studio-release-editor-custom-page-header">
{customPage && (
<div className="music-studio-release-editor-custom-page">
{customPage.header && (
<div className="music-studio-release-editor-custom-page-header">
<div className="music-studio-release-editor-custom-page-header-title">
<antd.Button
icon={<Icons.IoIosArrowBack />}
onClick={() => renderCustomPage(null, null)}
onClick={() =>
renderCustomPage(null, null)
}
/>
<h2>{customPage.header}</h2>
</div>
{
Array.isArray(customPageActions) && customPageActions.map((action, index) => {
return <antd.Button
{Array.isArray(customPageActions) &&
customPageActions.map((action, index) => {
return (
<antd.Button
key={index}
type={action.type}
icon={createIconRender(action.icon)}
icon={createIconRender(
action.icon,
)}
onClick={async () => {
if (typeof action.onClick === "function") {
if (
typeof action.onClick ===
"function"
) {
await action.onClick()
}
if (action.fireEvent) {
app.eventBus.emit(action.fireEvent)
app.eventBus.emit(
action.fireEvent,
)
}
}}
disabled={action.disabled}
>
{action.label}
</antd.Button>
})
}
</div>
}
{
customPage.content && (React.isValidElement(customPage.content) ?
React.cloneElement(customPage.content, {
...CustomPageProps,
...customPage.props
}) :
React.createElement(customPage.content, {
...CustomPageProps,
...customPage.props
})
)
}
})}
</div>
}
{
!customPage && <>
)}
{customPage.content &&
(React.isValidElement(customPage.content)
? React.cloneElement(customPage.content, {
...CustomPageProps,
...customPage.props,
})
: React.createElement(customPage.content, {
...CustomPageProps,
...customPage.props,
}))}
</div>
)}
{!customPage && (
<>
<div className="music-studio-release-editor-menu">
<antd.Menu
onClick={(e) => setSelectedTab(e.key)}
@ -216,65 +256,77 @@ const ReleaseEditor = (props) => {
<antd.Button
type="primary"
onClick={handleSubmit}
icon={release_id !== "new" ? <Icons.FiSave /> : <Icons.MdSend />}
disabled={submitting || loading || !canFinish()}
icon={
release_id !== "new" ? (
<Icons.FiSave />
) : (
<Icons.MdSend />
)
}
disabled={
submitting || loading || !canFinish()
}
loading={submitting}
>
{release_id !== "new" ? "Save" : "Release"}
</antd.Button>
{
release_id !== "new" ? <antd.Button
{release_id !== "new" ? (
<antd.Button
icon={<Icons.IoMdTrash />}
disabled={loading}
onClick={handleDelete}
>
Delete
</antd.Button> : null
}
</antd.Button>
) : null}
{
release_id !== "new" ? <antd.Button
{release_id !== "new" ? (
<antd.Button
icon={<Icons.MdLink />}
onClick={() => app.location.push(`/music/release/${globalState._id}`)}
onClick={() =>
app.location.push(
`/music/release/${globalState._id}`,
)
}
>
Go to release
</antd.Button> : null
}
</antd.Button>
) : null}
</div>
</div>
<div className="music-studio-release-editor-content">
{
submitError && <antd.Alert
{submitError && (
<antd.Alert
message={submitError.message}
type="error"
/>
}
{
!Tab && <antd.Result
)}
{!Tab && (
<antd.Result
status="error"
title="Error"
subTitle="Tab not found"
/>
}
{
Tab && React.createElement(Tab.render, {
)}
{Tab &&
React.createElement(Tab.render, {
release: globalState,
state: globalState,
setState: setGlobalState,
references: {
basic: basicInfoRef
}
})
}
basic: basicInfoRef,
},
})}
</div>
</>
}
)}
</div>
</ReleaseEditorStateContext.Provider>
)
}
export default ReleaseEditor

View File

@ -11,13 +11,27 @@ import { ReleaseEditorStateContext } from "@contexts/MusicReleaseEditor"
import "./index.less"
const stateToString = {
uploading: "Uploading",
transmuxing: "Processing...",
uploading_s3: "Archiving...",
}
const getTitleString = ({ track, progress }) => {
if (progress) {
return stateToString[progress.state] || progress.state
}
return track.title
}
const TrackListItem = (props) => {
const context = React.useContext(ReleaseEditorStateContext)
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState(null)
const { track } = props
const { track, progress } = props
async function onClickEditTrack() {
context.renderCustomPage({
@ -33,8 +47,6 @@ const TrackListItem = (props) => {
props.onDelete(track.uid)
}
console.log("render")
return (
<div
className={classnames(
@ -50,7 +62,7 @@ const TrackListItem = (props) => {
<div
className="music-studio-release-editor-tracks-list-item-progress"
style={{
"--upload-progress": `${props.uploading.progress}%`,
"--upload-progress": `${props.progress?.percent ?? 0}%`,
}}
/>
@ -58,7 +70,7 @@ const TrackListItem = (props) => {
<span>{props.index + 1}</span>
</div>
{props.uploading.working && <Icons.LoadingOutlined />}
{progress !== null && <Icons.LoadingOutlined />}
<Image
src={track.cover}
@ -69,7 +81,7 @@ const TrackListItem = (props) => {
}}
/>
<span>{track.title}</span>
<span>{getTitleString({ track, progress })}</span>
<div className="music-studio-release-editor-tracks-list-item-actions">
<antd.Popconfirm

View File

@ -17,12 +17,12 @@ class TracksManager extends React.Component {
swapyRef = React.createRef()
state = {
list: Array.isArray(this.props.list) ? this.props.list : [],
items: Array.isArray(this.props.items) ? this.props.items : [],
pendingUploads: [],
}
componentDidUpdate = (prevProps, prevState) => {
if (prevState.list !== this.state.list) {
if (prevState.items !== this.state.items) {
if (typeof this.props.onChangeState === "function") {
this.props.onChangeState(this.state)
}
@ -55,7 +55,7 @@ class TracksManager extends React.Component {
return false
}
return this.state.list.find((item) => item.uid === uid)
return this.state.items.find((item) => item.uid === uid)
}
addTrackToList = (track) => {
@ -64,7 +64,7 @@ class TracksManager extends React.Component {
}
this.setState({
list: [...this.state.list, track],
items: [...this.state.items, track],
})
}
@ -76,18 +76,17 @@ class TracksManager extends React.Component {
this.removeTrackUIDFromPendingUploads(uid)
this.setState({
list: this.state.list.filter((item) => item.uid !== uid),
items: this.state.items.filter((item) => item.uid !== uid),
})
}
modifyTrackByUid = (uid, track) => {
console.log("modifyTrackByUid", uid, track)
if (!uid || !track) {
return false
}
this.setState({
list: this.state.list.map((item) => {
items: this.state.items.map((item) => {
if (item.uid === uid) {
return {
...item,
@ -140,7 +139,7 @@ class TracksManager extends React.Component {
)
if (uploadProgressIndex === -1) {
return 0
return null
}
return this.state.pendingUploads[uploadProgressIndex].progress
@ -159,7 +158,7 @@ class TracksManager extends React.Component {
newData[uploadProgressIndex].progress = progress
console.log(`Updating progress for [${uid}] to [${progress}]`)
console.log(`Updating progress for [${uid}] to >`, progress)
this.setState({
pendingUploads: newData,
@ -189,7 +188,7 @@ class TracksManager extends React.Component {
// remove pending file
this.removeTrackUIDFromPendingUploads(uid)
let trackManifest = this.state.list.find(
let trackManifest = this.state.items.find(
(item) => item.uid === uid,
)
@ -231,9 +230,8 @@ class TracksManager extends React.Component {
const response = await app.cores.remoteStorage
.uploadFile(req.file, {
onProgress: this.handleTrackFileUploadProgress,
service: "b2",
headers: {
transmux: "a-dash",
transformations: "a-dash",
},
})
.catch((error) => {
@ -258,17 +256,17 @@ class TracksManager extends React.Component {
this.setState((prev) => {
// move all list items by id
const orderedIds = orderedIdsArray.map((id) =>
this.state.list.find((item) => item._id === id),
this.state.items.find((item) => item._id === id),
)
console.log("orderedIds", orderedIds)
return {
list: orderedIds,
items: orderedIds,
}
})
}
render() {
console.log(`Tracks List >`, this.state.list)
console.log(`Tracks List >`, this.state.items)
return (
<div className="music-studio-release-editor-tracks">
@ -280,7 +278,7 @@ class TracksManager extends React.Component {
accept="audio/*"
multiple
>
{this.state.list.length === 0 ? (
{this.state.items.length === 0 ? (
<UploadHint />
) : (
<antd.Button
@ -296,11 +294,11 @@ class TracksManager extends React.Component {
id="editor-tracks-list"
className="music-studio-release-editor-tracks-list"
>
{this.state.list.length === 0 && (
{this.state.items.length === 0 && (
<antd.Result status="info" title="No tracks" />
)}
{this.state.list.map((track, index) => {
{this.state.items.map((track, index) => {
const progress = this.getUploadProgress(track.uid)
return (
@ -310,12 +308,7 @@ class TracksManager extends React.Component {
track={track}
onEdit={this.modifyTrackByUid}
onDelete={this.removeTrackByUid}
uploading={{
progress: progress,
working: this.state.pendingUploads.find(
(item) => item.uid === track.uid,
),
}}
progress={progress}
disabled={progress > 0}
/>
</div>
@ -336,7 +329,7 @@ const ReleaseTracks = (props) => {
<TracksManager
_id={state._id}
list={state.list}
items={state.items}
onChangeState={(managerState) => {
setState({
...state,

View File

@ -17,7 +17,7 @@ const TrackEditor = (props) => {
setTrack((prev) => {
return {
...prev,
[key]: value
[key]: value,
}
})
}
@ -28,15 +28,17 @@ const TrackEditor = (props) => {
content: EnhancedLyricsEditor,
props: {
track: track,
}
},
})
}
async function handleOnSave() {
setTrack((prev) => {
const listData = [...context.list]
const listData = [...context.items]
const trackIndex = listData.findIndex((item) => item.uid === prev.uid)
const trackIndex = listData.findIndex(
(item) => item.uid === prev.uid,
)
if (trackIndex === -1) {
return prev
@ -46,13 +48,19 @@ const TrackEditor = (props) => {
context.setGlobalState({
...context,
list: listData
items: listData,
})
props.close()
return prev
})
}
function setParentCover() {
handleChange("cover", context.cover)
}
React.useEffect(() => {
context.setCustomPageActions([
{
@ -65,7 +73,8 @@ const TrackEditor = (props) => {
])
}, [track])
return <div className="track-editor">
return (
<div className="track-editor">
<div className="track-editor-field">
<div className="track-editor-field-header">
<Icons.MdImage />
@ -76,9 +85,9 @@ const TrackEditor = (props) => {
value={track.cover}
onChange={(url) => handleChange("cover", url)}
extraActions={[
<antd.Button>
<antd.Button onClick={setParentCover}>
Use Parent
</antd.Button>
</antd.Button>,
]}
/>
</div>
@ -103,7 +112,7 @@ const TrackEditor = (props) => {
</div>
<antd.Input
value={track.artists?.join(", ")}
value={track.artist}
placeholder="Artist"
onChange={(e) => handleChange("artist", e.target.value)}
/>
@ -138,12 +147,6 @@ const TrackEditor = (props) => {
<div className="track-editor-field-header">
<Icons.MdLyrics />
<span>Enhanced Lyrics</span>
<antd.Switch
checked={track.lyrics_enabled}
onChange={(value) => handleChange("lyrics_enabled", value)}
disabled={!track.params._id}
/>
</div>
<div className="track-editor-field-actions">
@ -154,14 +157,16 @@ const TrackEditor = (props) => {
Edit
</antd.Button>
{
!track.params._id && <span>
You cannot edit Video and Lyrics without release first
{!track.params._id && (
<span>
You cannot edit Video and Lyrics without release
first
</span>
}
)}
</div>
</div>
</div>
)
}
export default TrackEditor