mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
Add change tracking and update to use "items" property
This commit is contained in:
parent
d738995054
commit
74021f38b6
@ -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
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user