mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
merge from local
This commit is contained in:
parent
534463cfa2
commit
0db536bbea
@ -1 +1 @@
|
||||
Subproject commit d2e6f1bc5856e3084d4fd068dec5d67ab2ef9d8d
|
||||
Subproject commit e52925b191b6e1d1415f2bb63d921ccad8c18411
|
@ -7,9 +7,11 @@
|
||||
<meta name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="description" content="Comty, a prototype of social network." />
|
||||
<meta name="title" content="Comty" />
|
||||
<meta name="description" content="Comty, a prototype of social network." />
|
||||
<meta property="og:title" content="Comty" />
|
||||
<meta property="og:description" content="Comty, a prototype of social network." />
|
||||
<meta property="og:image" content="https://dl.ragestudio.net/branding/comty/alt/SVG/t3t3.svg" />
|
||||
<meta property="og:image" content="https://storage.ragestudio.net/rstudio/branding/comty/iso/basic_alt.svg" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -13,6 +13,8 @@ export default class TrackInstance {
|
||||
this.player = player
|
||||
this.manifest = manifest
|
||||
|
||||
this.id = this.manifest.id ?? this.manifest._id
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
@ -98,6 +100,18 @@ export default class TrackInstance {
|
||||
return this
|
||||
}
|
||||
|
||||
stop = () => {
|
||||
this.audio.pause()
|
||||
|
||||
const lastProcessor = this.attachedProcessors[this.attachedProcessors.length - 1]
|
||||
|
||||
if (lastProcessor) {
|
||||
this.attachedProcessors[this.attachedProcessors.length - 1]._destroy(this)
|
||||
}
|
||||
|
||||
this.attachedProcessors = []
|
||||
}
|
||||
|
||||
resolveManifest = async () => {
|
||||
if (typeof this.manifest === "string") {
|
||||
this.manifest = {
|
||||
|
@ -57,7 +57,6 @@ export default class TrackManifest {
|
||||
|
||||
// Extended from db
|
||||
lyrics_enabled = false
|
||||
|
||||
liked = null
|
||||
|
||||
async initialize() {
|
||||
@ -78,7 +77,7 @@ export default class TrackManifest {
|
||||
}
|
||||
|
||||
if (this.metadata.tags.picture) {
|
||||
this.cover = app.cores.remoteStorage.binaryArrayToFile(this.metadata.tags.picture, this.title)
|
||||
this.cover = app.cores.remoteStorage.binaryArrayToFile(this.metadata.tags.picture, "cover")
|
||||
|
||||
const coverUpload = await app.cores.remoteStorage.uploadFile(this.cover)
|
||||
|
||||
|
@ -73,7 +73,7 @@ const MoreMenuHandlers = {
|
||||
}
|
||||
}
|
||||
|
||||
export default (props) => {
|
||||
const PlaylistView = (props) => {
|
||||
const [playlist, setPlaylist] = React.useState(props.playlist)
|
||||
const [searchResults, setSearchResults] = React.useState(null)
|
||||
const [owningPlaylist, setOwningPlaylist] = React.useState(checkUserIdIsSelf(props.playlist?.user_id))
|
||||
@ -109,8 +109,27 @@ export default (props) => {
|
||||
|
||||
let debounceSearch = null
|
||||
|
||||
const makeSearch = (value) => {
|
||||
//TODO: Implement me using API
|
||||
return app.message.info("Not implemented yet...")
|
||||
}
|
||||
|
||||
const handleOnSearchChange = (value) => {
|
||||
debounceSearch = setTimeout(() => {
|
||||
makeSearch(value)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleOnSearchEmpty = () => {
|
||||
if (debounceSearch) {
|
||||
clearTimeout(debounceSearch)
|
||||
}
|
||||
|
||||
setSearchResults(null)
|
||||
}
|
||||
|
||||
const handleOnClickPlaylistPlay = () => {
|
||||
app.cores.player.start(playlist.list, 0)
|
||||
app.cores.player.start(playlist.list)
|
||||
}
|
||||
|
||||
const handleOnClickViewDetails = () => {
|
||||
@ -131,7 +150,7 @@ export default (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
// check if is currently playing
|
||||
// check if clicked track is currently playing
|
||||
if (app.cores.player.state.track_manifest?._id === track._id) {
|
||||
app.cores.player.playback.toggle()
|
||||
} else {
|
||||
@ -141,48 +160,7 @@ export default (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleTrackLike = async (track) => {
|
||||
return await MusicModel.toggleTrackLike(track._id)
|
||||
}
|
||||
|
||||
const makeSearch = (value) => {
|
||||
//TODO: Implement me using API
|
||||
return app.message.info("Not implemented yet...")
|
||||
|
||||
const options = {
|
||||
includeScore: true,
|
||||
keys: [
|
||||
"title",
|
||||
"artist",
|
||||
"album",
|
||||
],
|
||||
}
|
||||
|
||||
const fuseInstance = new fuse(playlist.list, options)
|
||||
const results = fuseInstance.search(value)
|
||||
|
||||
console.log(results)
|
||||
|
||||
setSearchResults(results.map((result) => {
|
||||
return result.item
|
||||
}))
|
||||
}
|
||||
|
||||
const handleOnSearchChange = (value) => {
|
||||
debounceSearch = setTimeout(() => {
|
||||
makeSearch(value)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleOnSearchEmpty = () => {
|
||||
if (debounceSearch) {
|
||||
clearTimeout(debounceSearch)
|
||||
}
|
||||
|
||||
setSearchResults(null)
|
||||
}
|
||||
|
||||
const updateTrackLike = (track_id, liked) => {
|
||||
const handleUpdateTrackLike = (track_id, liked) => {
|
||||
setPlaylist((prev) => {
|
||||
const index = prev.list.findIndex((item) => {
|
||||
return item._id === track_id
|
||||
@ -202,6 +180,29 @@ export default (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleTrackChangeState = (track_id, update) => {
|
||||
setPlaylist((prev) => {
|
||||
const index = prev.list.findIndex((item) => {
|
||||
return item._id === track_id
|
||||
})
|
||||
|
||||
if (index !== -1) {
|
||||
const newState = {
|
||||
...prev,
|
||||
}
|
||||
|
||||
newState.list[index] = {
|
||||
...newState.list[index],
|
||||
...update
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
||||
|
||||
return prev
|
||||
})
|
||||
}
|
||||
|
||||
const handleMoreMenuClick = async (e) => {
|
||||
const handler = MoreMenuHandlers[e.key]
|
||||
|
||||
@ -213,8 +214,8 @@ export default (props) => {
|
||||
}
|
||||
|
||||
useWsEvents({
|
||||
"music:self:track:toggle:like": (data) => {
|
||||
updateTrackLike(data.track_id, data.action === "liked")
|
||||
"music:track:toggle:like": (data) => {
|
||||
handleUpdateTrackLike(data.track_id, data.action === "liked")
|
||||
}
|
||||
}, {
|
||||
socketName: "music",
|
||||
@ -268,7 +269,7 @@ export default (props) => {
|
||||
}
|
||||
<div className="play_info_statistics_item">
|
||||
<p>
|
||||
<Icons.MdLibraryMusic /> {props.length ?? playlist.total_length ?? playlist.list.length} Tracks
|
||||
<Icons.MdLibraryMusic /> {props.length ?? playlist.total_length ?? playlist.list.length} Items
|
||||
</p>
|
||||
</div>
|
||||
{
|
||||
@ -332,7 +333,8 @@ export default (props) => {
|
||||
</div>
|
||||
|
||||
<div className="list">
|
||||
<div className="list_header">
|
||||
{
|
||||
playlist.list.length > 0 && <div className="list_header">
|
||||
<h1>
|
||||
<Icons.MdPlaylistPlay /> Tracks
|
||||
</h1>
|
||||
@ -340,8 +342,20 @@ export default (props) => {
|
||||
<SearchButton
|
||||
onChange={handleOnSearchChange}
|
||||
onEmpty={handleOnSearchEmpty}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
playlist.list.length === 0 && <antd.Empty
|
||||
description={
|
||||
<>
|
||||
<Icons.MdLibraryMusic /> This playlist its empty!
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
searchResults && searchResults.map((item) => {
|
||||
@ -350,21 +364,11 @@ export default (props) => {
|
||||
order={item._id}
|
||||
track={item}
|
||||
onClickPlayBtn={() => handleOnClickTrack(item)}
|
||||
onLike={() => handleTrackLike(item)}
|
||||
changeState={(update) => handleTrackChangeState(item._id, update)}
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
!searchResults && playlist.list.length === 0 && <antd.Empty
|
||||
description={
|
||||
<>
|
||||
<Icons.MdLibraryMusic /> This playlist its empty!
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!searchResults && playlist.list.length > 0 && <LoadMore
|
||||
className="list_content"
|
||||
@ -379,7 +383,7 @@ export default (props) => {
|
||||
order={index + 1}
|
||||
track={item}
|
||||
onClickPlayBtn={() => handleOnClickTrack(item)}
|
||||
onLike={() => handleTrackLike(item)}
|
||||
changeState={(update) => handleTrackChangeState(item._id, update)}
|
||||
/>
|
||||
})
|
||||
}
|
||||
@ -391,3 +395,5 @@ export default (props) => {
|
||||
</WithPlayerContext>
|
||||
</PlaylistContext.Provider>
|
||||
}
|
||||
|
||||
export default PlaylistView
|
@ -7,6 +7,8 @@ import RGBStringToValues from "@utils/rgbToValues"
|
||||
import ImageViewer from "@components/ImageViewer"
|
||||
import { Icons } from "@components/Icons"
|
||||
|
||||
import MusicModel from "@models/music"
|
||||
|
||||
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||
import { Context as PlaylistContext } from "@contexts/WithPlaylistContext"
|
||||
|
||||
@ -14,21 +16,29 @@ import "./index.less"
|
||||
|
||||
const handlers = {
|
||||
"like": async (ctx, track) => {
|
||||
app.cores.player.toggleCurrentTrackLike(true, track)
|
||||
await MusicModel.toggleItemFavourite("track", track._id, true)
|
||||
|
||||
ctx.changeState({
|
||||
liked: true,
|
||||
})
|
||||
ctx.closeMenu()
|
||||
},
|
||||
"unlike": async (ctx, track) => {
|
||||
app.cores.player.toggleCurrentTrackLike(false, track)
|
||||
await MusicModel.toggleItemFavourite("track", track._id, false)
|
||||
|
||||
ctx.changeState({
|
||||
liked: false,
|
||||
})
|
||||
ctx.closeMenu()
|
||||
},
|
||||
}
|
||||
|
||||
const Track = (props) => {
|
||||
const {
|
||||
const [{
|
||||
loading,
|
||||
track_manifest,
|
||||
playback_status,
|
||||
} = usePlayerStateContext()
|
||||
}] = usePlayerStateContext()
|
||||
|
||||
const playlist_ctx = React.useContext(PlaylistContext)
|
||||
|
||||
@ -74,7 +84,8 @@ const Track = (props) => {
|
||||
{
|
||||
closeMenu: () => {
|
||||
setMoreMenuOpened(false)
|
||||
}
|
||||
},
|
||||
changeState: props.changeState,
|
||||
},
|
||||
props.track
|
||||
)
|
||||
|
@ -35,7 +35,7 @@ const EventsHandlers = {
|
||||
}
|
||||
|
||||
const Controls = (props) => {
|
||||
const playerState = usePlayerStateContext()
|
||||
const [playerState] = usePlayerStateContext()
|
||||
|
||||
const handleAction = (event, ...args) => {
|
||||
if (typeof EventsHandlers[event] !== "function") {
|
||||
|
@ -6,11 +6,12 @@ import LikeButton from "@components/LikeButton"
|
||||
|
||||
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||
|
||||
const ExtraActions = (props) => {
|
||||
const playerState = usePlayerStateContext()
|
||||
import MusicModel from "@models/music"
|
||||
|
||||
const ExtraActions = (props) => {
|
||||
const [playerState] = usePlayerStateContext()
|
||||
const handleClickLike = async () => {
|
||||
await app.cores.player.toggleCurrentTrackLike(!playerState.track_manifest?.liked)
|
||||
await MusicModel.toggleItemFavourite("track", playerState.track_manifest._id)
|
||||
}
|
||||
|
||||
return <div className="extra_actions">
|
||||
@ -21,9 +22,10 @@ const ExtraActions = (props) => {
|
||||
disabled={!playerState.track_manifest?.lyrics_enabled}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!app.isMobile && <LikeButton
|
||||
liked={playerState.track_manifest?.liked ?? false}
|
||||
liked={playerState.track_manifest?.fetchLikeStatus}
|
||||
onClick={handleClickLike}
|
||||
/>
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ const ServiceIndicator = (props) => {
|
||||
}
|
||||
|
||||
const Player = (props) => {
|
||||
const playerState = usePlayerStateContext()
|
||||
const [playerState] = usePlayerStateContext()
|
||||
|
||||
const contentRef = React.useRef()
|
||||
const titleRef = React.useRef()
|
||||
|
@ -31,13 +31,13 @@ const SelfActionsItems = [
|
||||
]
|
||||
|
||||
const MoreActionsItems = [
|
||||
{
|
||||
key: "onClickRepost",
|
||||
label: <>
|
||||
<Icons.MdCallSplit />
|
||||
<span>Repost</span>
|
||||
</>,
|
||||
},
|
||||
// {
|
||||
// key: "onClickRepost",
|
||||
// label: <>
|
||||
// <Icons.MdCallSplit />
|
||||
// <span>Repost</span>
|
||||
// </>,
|
||||
// },
|
||||
{
|
||||
key: "onClickShare",
|
||||
label: <>
|
||||
@ -57,6 +57,19 @@ const MoreActionsItems = [
|
||||
},
|
||||
]
|
||||
|
||||
const BuiltInActions = {
|
||||
onClickShare: (post) => {
|
||||
navigator.share({
|
||||
title: post.title,
|
||||
text: `Check this post on Comty`,
|
||||
url: post.share_url
|
||||
})
|
||||
},
|
||||
onClickReport: (post) => {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const PostActions = (props) => {
|
||||
const [isSelf, setIsSelf] = React.useState(false)
|
||||
|
||||
@ -77,8 +90,10 @@ const PostActions = (props) => {
|
||||
}
|
||||
|
||||
const handleDropdownClickItem = (e) => {
|
||||
if (typeof props.actions[e.key] === "function") {
|
||||
props.actions[e.key]()
|
||||
const action = BuiltInActions[e.key] ?? props.actions[e.key]
|
||||
|
||||
if (typeof action === "function") {
|
||||
action(props.post)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,12 @@ const messageRegexs = [
|
||||
return <a key={key} onClick={() => window.app.location.push(`/@${result[1].substr(1)}`)}>{result[1]}</a>
|
||||
}
|
||||
},
|
||||
{
|
||||
regex: /#[a-zA-Z0-9_]+/gi,
|
||||
fn: (key, result) => {
|
||||
return <a key={key} onClick={() => window.app.location.push(`/trending/${result[0].substr(1)}`)}>{result[0]}</a>
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default class PostCard extends React.PureComponent {
|
||||
@ -223,6 +229,7 @@ export default class PostCard extends React.PureComponent {
|
||||
</div>
|
||||
|
||||
<PostActions
|
||||
post={this.state.data}
|
||||
user_id={this.state.data.user_id}
|
||||
|
||||
likesCount={this.state.countLikes}
|
||||
|
@ -4,7 +4,7 @@ import classnames from "classnames"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
export default (props) => {
|
||||
const SearchButton = (props) => {
|
||||
const searchBoxRef = React.useRef(null)
|
||||
|
||||
const [value, setValue] = React.useState()
|
||||
@ -51,6 +51,9 @@ export default (props) => {
|
||||
openSearchBox(false)
|
||||
}
|
||||
}}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default SearchButton
|
45
packages/app/src/components/TrendingsCard/index.jsx
Normal file
45
packages/app/src/components/TrendingsCard/index.jsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React from "react"
|
||||
|
||||
import { Skeleton } from "antd"
|
||||
import { Icons } from "@components/Icons"
|
||||
import PostsModel from "@models/post"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const TrendingsCard = (props) => {
|
||||
const [L_Trendings, R_Trendings, E_Trendings] = app.cores.api.useRequest(PostsModel.getTrendings)
|
||||
|
||||
return <div className="card">
|
||||
<div className="card-header">
|
||||
<h1><Icons.IoMdTrendingUp /> Trendings</h1>
|
||||
</div>
|
||||
|
||||
<div className="card-content trendings">
|
||||
{
|
||||
L_Trendings && <Skeleton active />
|
||||
}
|
||||
{
|
||||
E_Trendings && <span>Something went wrong</span>
|
||||
}
|
||||
{
|
||||
!L_Trendings && !E_Trendings && R_Trendings && R_Trendings.map((trending, index) => {
|
||||
return <div
|
||||
key={index}
|
||||
className="trending"
|
||||
onClick={() => window.app.location.push(`/trending/${trending.hashtag}`)}
|
||||
>
|
||||
<div className="trending-level">
|
||||
<span>#{index + 1} {trending.hashtag}</span>
|
||||
</div>
|
||||
|
||||
<div className="trending-info">
|
||||
<span>{trending.count} posts</span>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default TrendingsCard
|
35
packages/app/src/components/TrendingsCard/index.less
Normal file
35
packages/app/src/components/TrendingsCard/index.less
Normal file
@ -0,0 +1,35 @@
|
||||
.trendings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 10px;
|
||||
padding: 0 10px;
|
||||
|
||||
.trending {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
gap: 5px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.trending-level {
|
||||
font-family: "DM Mono", monospace;
|
||||
}
|
||||
|
||||
.trending-info {
|
||||
font-size: 0.7rem;
|
||||
|
||||
span {
|
||||
color: var(--background-color-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +1,19 @@
|
||||
import React from "react"
|
||||
|
||||
function deepUnproxy(obj) {
|
||||
// Verificar si es un array y hacer una copia en consecuencia
|
||||
if (Array.isArray(obj)) {
|
||||
obj = [...obj];
|
||||
obj = [...obj]
|
||||
} else {
|
||||
obj = Object.assign({}, obj);
|
||||
obj = Object.assign({}, obj)
|
||||
}
|
||||
|
||||
for (let key in obj) {
|
||||
if (obj[key] && typeof obj[key] === "object") {
|
||||
obj[key] = deepUnproxy(obj[key]); // Recursión para profundizar en objetos y arrays
|
||||
obj[key] = deepUnproxy(obj[key])
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
return obj
|
||||
}
|
||||
|
||||
export const usePlayerStateContext = (updater) => {
|
||||
@ -40,7 +39,7 @@ export const usePlayerStateContext = (updater) => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
return state
|
||||
return [state, setState]
|
||||
}
|
||||
|
||||
export const Context = React.createContext({})
|
||||
|
@ -53,6 +53,13 @@ export default class APICore extends Core {
|
||||
enableWs: true,
|
||||
})
|
||||
|
||||
this.client.eventBus.on("ws:disconnected", () => {
|
||||
app.cores.notifications.new({
|
||||
title: "Failed to connect to server",
|
||||
description: "The connection to the server was lost. Some features may not work properly.",
|
||||
})
|
||||
})
|
||||
|
||||
this.client.eventBus.on("auth:login_success", () => {
|
||||
app.eventBus.emit("auth:login_success")
|
||||
})
|
||||
|
@ -43,7 +43,7 @@ export default class PlayerProcessors {
|
||||
Object.entries(processor.exposeToPublic).forEach(([key, value]) => {
|
||||
const refName = processor.constructor.refName
|
||||
|
||||
if (typeof this.public[refName] === "undefined") {
|
||||
if (typeof this.player.public[refName] === "undefined") {
|
||||
// by default create a empty object
|
||||
this.player.public[refName] = {}
|
||||
}
|
||||
@ -55,8 +55,6 @@ export default class PlayerProcessors {
|
||||
}
|
||||
|
||||
async attachProcessorsToInstance(instance) {
|
||||
this.player.console.log(instance, this.processors)
|
||||
|
||||
for await (const [index, processor] of this.processors.entries()) {
|
||||
if (processor.constructor.node_bypass === true) {
|
||||
instance.contextElement.connect(processor.processor)
|
||||
|
@ -10,7 +10,6 @@ export default class PlayerUI {
|
||||
//
|
||||
// UI Methods
|
||||
//
|
||||
|
||||
attachPlayerComponent() {
|
||||
if (this.currentDomWindow) {
|
||||
this.player.console.warn("EmbbededMediaPlayer already attached")
|
||||
@ -18,7 +17,9 @@ export default class PlayerUI {
|
||||
}
|
||||
|
||||
if (app.layout.tools_bar) {
|
||||
this.currentDomWindow = app.layout.tools_bar.attachRender("mediaPlayer", ToolBarPlayer)
|
||||
this.currentDomWindow = app.layout.tools_bar.attachRender("mediaPlayer", ToolBarPlayer, undefined, {
|
||||
position: "bottom",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ export default class Presets {
|
||||
constructor({
|
||||
storage_key,
|
||||
defaultPresetValue,
|
||||
onApplyValues,
|
||||
}) {
|
||||
if (!storage_key) {
|
||||
throw new Error("storage_key is required")
|
||||
@ -11,6 +12,7 @@ export default class Presets {
|
||||
|
||||
this.storage_key = storage_key
|
||||
this.defaultPresetValue = defaultPresetValue
|
||||
this.onApplyValues = onApplyValues
|
||||
|
||||
return this
|
||||
}
|
||||
@ -38,14 +40,25 @@ export default class Presets {
|
||||
}
|
||||
|
||||
get currentPresetValues() {
|
||||
const presets = this.presets
|
||||
const key = this.currentPresetKey
|
||||
|
||||
if (!presets || !presets[key]) {
|
||||
if (!this.presets || !this.presets[this.currentPresetKey]) {
|
||||
return this.defaultPresetValue
|
||||
}
|
||||
|
||||
return presets[key]
|
||||
return this.presets[this.currentPresetKey]
|
||||
}
|
||||
|
||||
set currentPresetValues(values) {
|
||||
const newData = this.presets
|
||||
|
||||
newData[this.currentPresetKey] = values
|
||||
|
||||
this.presets = newData
|
||||
}
|
||||
|
||||
applyValues() {
|
||||
if (typeof this.onApplyValues === "function") {
|
||||
this.onApplyValues(this.presets)
|
||||
}
|
||||
}
|
||||
|
||||
deletePreset(key) {
|
||||
@ -54,64 +67,74 @@ export default class Presets {
|
||||
return false
|
||||
}
|
||||
|
||||
// if current preset is deleted, change to default
|
||||
if (this.currentPresetKey === key) {
|
||||
this.changePreset("default")
|
||||
}
|
||||
|
||||
let presets = this.presets
|
||||
let newData = this.presets
|
||||
|
||||
delete presets[key]
|
||||
delete newData[key]
|
||||
|
||||
this.presets = presets
|
||||
this.presets = newData
|
||||
|
||||
return presets
|
||||
this.applyValues()
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
createPreset(key, values) {
|
||||
let presets = this.presets
|
||||
|
||||
if (presets[key]) {
|
||||
if (this.presets[key]) {
|
||||
app.message.error("Preset already exists")
|
||||
return false
|
||||
}
|
||||
|
||||
presets[key] = values ?? this.defaultPresetValue
|
||||
let newData = this.presets
|
||||
|
||||
this.presets = presets
|
||||
newData[key] = values ?? this.defaultPresetValue
|
||||
|
||||
return presets[key]
|
||||
this.applyValues()
|
||||
|
||||
this.presets = newData
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
changePreset(key) {
|
||||
let presets = this.presets
|
||||
|
||||
// create new one
|
||||
if (!presets[key]) {
|
||||
presets[key] = this.defaultPresetValue
|
||||
|
||||
this.presets = presets
|
||||
if (!this.presets[key]) {
|
||||
this.presets[key] = this.defaultPresetValue
|
||||
}
|
||||
|
||||
this.currentPresetKey = key
|
||||
|
||||
return presets[key]
|
||||
this.applyValues()
|
||||
|
||||
return this.presets[key]
|
||||
}
|
||||
|
||||
setToCurrent(values) {
|
||||
let preset = this.currentPresetValues
|
||||
|
||||
preset = {
|
||||
...preset,
|
||||
this.currentPresetValues = {
|
||||
...this.currentPresetValues,
|
||||
...values,
|
||||
}
|
||||
|
||||
// update presets
|
||||
let presets = this.presets
|
||||
this.applyValues()
|
||||
|
||||
presets[this.currentPresetKey] = preset
|
||||
return this.currentPresetValues
|
||||
}
|
||||
|
||||
this.presets = presets
|
||||
async setCurrentPresetToDefault() {
|
||||
return await new Promise((resolve) => {
|
||||
app.layout.modal.confirm.confirm({
|
||||
title: "Reset to default values?",
|
||||
content: "Are you sure you want to reset to default values?",
|
||||
onOk: () => {
|
||||
this.setToCurrent(this.defaultPresetValue)
|
||||
|
||||
return preset
|
||||
resolve(this.currentPresetValues)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
126
packages/app/src/cores/player/classes/QueueManager.js
Normal file
126
packages/app/src/cores/player/classes/QueueManager.js
Normal file
@ -0,0 +1,126 @@
|
||||
export default class QueueManager {
|
||||
constructor(params = {}) {
|
||||
this.params = params
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
prevItems = []
|
||||
nextItems = []
|
||||
|
||||
currentItem = null
|
||||
|
||||
next = ({ random = false } = {}) => {
|
||||
if (this.nextItems.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.currentItem) {
|
||||
this.prevItems.push(this.currentItem)
|
||||
}
|
||||
|
||||
if (random) {
|
||||
const randomIndex = Math.floor(Math.random() * this.nextItems.length)
|
||||
|
||||
this.currentItem = this.nextItems.splice(randomIndex, 1)[0]
|
||||
} else {
|
||||
this.currentItem = this.nextItems.shift()
|
||||
}
|
||||
|
||||
return this.currentItem
|
||||
}
|
||||
|
||||
set = (item) => {
|
||||
if (typeof item === "number") {
|
||||
item = this.nextItems[item]
|
||||
}
|
||||
|
||||
if (this.currentItem && this.currentItem.id === item.id) {
|
||||
return this.currentItem
|
||||
}
|
||||
|
||||
const itemInNext = this.nextItems.findIndex((i) => i.id === item.id)
|
||||
const itemInPrev = this.prevItems.findIndex((i) => i.id === item.id)
|
||||
|
||||
if (itemInNext === -1 && itemInPrev === -1) {
|
||||
throw new Error("Item not found in the queue")
|
||||
}
|
||||
|
||||
if (itemInNext > -1) {
|
||||
if (this.currentItem) {
|
||||
this.prevItems.push(this.currentItem)
|
||||
}
|
||||
|
||||
this.prevItems.push(...this.nextItems.splice(0, itemInNext))
|
||||
|
||||
this.currentItem = this.nextItems.shift()
|
||||
}
|
||||
|
||||
if (itemInPrev > -1) {
|
||||
if (this.currentItem) {
|
||||
this.nextItems.unshift(this.currentItem)
|
||||
}
|
||||
|
||||
this.nextItems.unshift(...this.prevItems.splice(itemInPrev + 1))
|
||||
|
||||
this.currentItem = this.prevItems.pop()
|
||||
}
|
||||
|
||||
return this.currentItem
|
||||
}
|
||||
|
||||
previous = () => {
|
||||
if (this.prevItems.length === 0) {
|
||||
return this.currentItem
|
||||
}
|
||||
|
||||
if (this.currentItem) {
|
||||
this.nextItems.unshift(this.currentItem)
|
||||
}
|
||||
|
||||
this.currentItem = this.prevItems.pop()
|
||||
|
||||
return this.currentItem
|
||||
}
|
||||
|
||||
add = (items) => {
|
||||
if (!Array.isArray(items)) {
|
||||
items = [items]
|
||||
}
|
||||
|
||||
this.nextItems = [...this.nextItems, ...items]
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
remove = (item) => {
|
||||
const indexNext = this.nextItems.findIndex((i) => i.id === item.id)
|
||||
const indexPrev = this.prevItems.findIndex((i) => i.id === item.id)
|
||||
|
||||
if (indexNext > -1) {
|
||||
this.nextItems.splice(indexNext, 1)
|
||||
}
|
||||
|
||||
if (indexPrev > -1) {
|
||||
this.prevItems.splice(indexPrev, 1)
|
||||
}
|
||||
|
||||
this.consoleState()
|
||||
}
|
||||
|
||||
flush = () => {
|
||||
this.nextItems = []
|
||||
this.prevItems = []
|
||||
this.currentItem = null
|
||||
|
||||
this.consoleState()
|
||||
}
|
||||
|
||||
async load(item) {
|
||||
if (typeof this.params.loadFunction === "function") {
|
||||
return await this.params.loadFunction(item)
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import ServiceProviders from "./classes/Services"
|
||||
import PlayerState from "./classes/PlayerState"
|
||||
import PlayerUI from "./classes/PlayerUI"
|
||||
import PlayerProcessors from "./classes/PlayerProcessors"
|
||||
import QueueManager from "./classes/QueueManager"
|
||||
|
||||
import setSampleRate from "./helpers/setSampleRate"
|
||||
|
||||
@ -28,8 +29,8 @@ export default class Player extends Core {
|
||||
|
||||
state = new PlayerState(this)
|
||||
ui = new PlayerUI(this)
|
||||
service_providers = new ServiceProviders()
|
||||
native_controls = new MediaSession()
|
||||
serviceProviders = new ServiceProviders()
|
||||
nativeControls = new MediaSession()
|
||||
audioContext = new AudioContext({
|
||||
sampleRate: AudioPlayerStorage.get("sample_rate") ?? Player.defaultSampleRate,
|
||||
latencyHint: "playback"
|
||||
@ -37,9 +38,11 @@ export default class Player extends Core {
|
||||
|
||||
audioProcessors = new PlayerProcessors(this)
|
||||
|
||||
track_prev_instances = []
|
||||
track_instance = null
|
||||
track_next_instances = []
|
||||
queue = new QueueManager({
|
||||
loadFunction: this.createInstance
|
||||
})
|
||||
|
||||
currentTrackInstance = null
|
||||
|
||||
public = {
|
||||
start: this.start,
|
||||
@ -61,7 +64,7 @@ export default class Player extends Core {
|
||||
setSampleRate: setSampleRate,
|
||||
}),
|
||||
track: () => {
|
||||
return this.track_instance
|
||||
return this.queue.currentItem
|
||||
},
|
||||
eventBus: () => {
|
||||
return this.eventBus
|
||||
@ -77,7 +80,7 @@ export default class Player extends Core {
|
||||
this.state.volume = 1
|
||||
}
|
||||
|
||||
await this.native_controls.initialize()
|
||||
await this.nativeControls.initialize()
|
||||
await this.audioProcessors.initialize()
|
||||
}
|
||||
|
||||
@ -85,130 +88,75 @@ export default class Player extends Core {
|
||||
// Instance managing methods
|
||||
//
|
||||
async abortPreloads() {
|
||||
for await (const instance of this.track_next_instances) {
|
||||
for await (const instance of this.queue.nextItems) {
|
||||
if (instance.abortController?.abort) {
|
||||
instance.abortController.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async preloadAudioInstance(instance) {
|
||||
const isIndex = typeof instance === "number"
|
||||
|
||||
let index = isIndex ? instance : 0
|
||||
|
||||
if (isIndex) {
|
||||
instance = this.track_next_instances[instance]
|
||||
}
|
||||
|
||||
if (!instance) {
|
||||
this.console.error("Instance not found to preload")
|
||||
return false
|
||||
}
|
||||
|
||||
if (isIndex) {
|
||||
this.track_next_instances[index] = instance
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
async destroyCurrentInstance() {
|
||||
if (!this.track_instance) {
|
||||
return false
|
||||
}
|
||||
|
||||
// stop playback
|
||||
if (this.track_instance.audio) {
|
||||
this.track_instance.audio.pause()
|
||||
}
|
||||
|
||||
// reset track_instance
|
||||
this.track_instance = null
|
||||
async createInstance(manifest) {
|
||||
return new TrackInstance(this, manifest)
|
||||
}
|
||||
|
||||
//
|
||||
// Playback methods
|
||||
//
|
||||
async play(instance, params = {}) {
|
||||
if (typeof instance === "number") {
|
||||
if (instance < 0) {
|
||||
instance = this.track_prev_instances[instance]
|
||||
}
|
||||
|
||||
if (instance > 0) {
|
||||
instance = this.track_instances[instance]
|
||||
}
|
||||
|
||||
if (instance === 0) {
|
||||
instance = this.track_instance
|
||||
}
|
||||
}
|
||||
|
||||
if (!instance) {
|
||||
throw new Error("Audio instance is required")
|
||||
}
|
||||
|
||||
// resume audio context if needed
|
||||
if (this.audioContext.state === "suspended") {
|
||||
this.audioContext.resume()
|
||||
}
|
||||
|
||||
if (this.track_instance) {
|
||||
this.track_instance = this.track_instance.attachedProcessors[this.track_instance.attachedProcessors.length - 1]._destroy(this.track_instance)
|
||||
|
||||
this.destroyCurrentInstance()
|
||||
}
|
||||
|
||||
// chage current track instance with provided
|
||||
this.track_instance = instance
|
||||
|
||||
// initialize instance if is not
|
||||
if (this.track_instance._initialized === false) {
|
||||
this.track_instance = await instance.initialize()
|
||||
if (this.queue.currentItem._initialized === false) {
|
||||
this.queue.currentItem = await instance.initialize()
|
||||
}
|
||||
|
||||
// update manifest
|
||||
this.state.track_manifest = this.track_instance.manifest
|
||||
this.state.track_manifest = this.queue.currentItem.manifest
|
||||
|
||||
// attach processors
|
||||
this.track_instance = await this.audioProcessors.attachProcessorsToInstance(this.track_instance)
|
||||
this.queue.currentItem = await this.audioProcessors.attachProcessorsToInstance(this.queue.currentItem)
|
||||
|
||||
// reconstruct audio src if is not set
|
||||
if (this.track_instance.audio.src !== this.track_instance.manifest.source) {
|
||||
this.track_instance.audio.src = this.track_instance.manifest.source
|
||||
if (this.queue.currentItem.audio.src !== this.queue.currentItem.manifest.source) {
|
||||
this.queue.currentItem.audio.src = this.queue.currentItem.manifest.source
|
||||
}
|
||||
|
||||
// set time to provided time, if not, set to 0
|
||||
this.track_instance.audio.currentTime = params.time ?? 0
|
||||
|
||||
this.track_instance.audio.muted = this.state.muted
|
||||
this.track_instance.audio.loop = this.state.playback_mode === "repeat"
|
||||
|
||||
this.track_instance.gainNode.gain.value = this.state.volume
|
||||
// set audio properties
|
||||
this.queue.currentItem.audio.currentTime = params.time ?? 0
|
||||
this.queue.currentItem.audio.muted = this.state.muted
|
||||
this.queue.currentItem.audio.loop = this.state.playback_mode === "repeat"
|
||||
this.queue.currentItem.gainNode.gain.value = this.state.volume
|
||||
|
||||
// play
|
||||
await this.track_instance.audio.play()
|
||||
await this.queue.currentItem.audio.play()
|
||||
|
||||
this.console.debug(`Playing track >`, this.track_instance)
|
||||
this.console.debug(`Playing track >`, this.queue.currentItem)
|
||||
|
||||
// update native controls
|
||||
this.native_controls.update(this.track_instance.manifest)
|
||||
this.nativeControls.update(this.queue.currentItem.manifest)
|
||||
|
||||
return this.track_instance
|
||||
return this.queue.currentItem
|
||||
}
|
||||
|
||||
async start(manifest, { time, startIndex = 0 } = {}) {
|
||||
this.ui.attachPlayerComponent()
|
||||
|
||||
// !IMPORTANT: abort preloads before destroying current instance
|
||||
if (this.queue.currentItem) {
|
||||
await this.queue.currentItem.stop()
|
||||
}
|
||||
|
||||
await this.abortPreloads()
|
||||
await this.destroyCurrentInstance()
|
||||
await this.queue.flush()
|
||||
|
||||
this.state.loading = true
|
||||
|
||||
this.track_prev_instances = []
|
||||
this.track_next_instances = []
|
||||
|
||||
let playlist = Array.isArray(manifest) ? manifest : [manifest]
|
||||
|
||||
if (playlist.length === 0) {
|
||||
@ -217,60 +165,47 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
if (playlist.some((item) => typeof item === "string")) {
|
||||
playlist = await this.service_providers.resolveMany(playlist)
|
||||
playlist = await this.serviceProviders.resolveMany(playlist)
|
||||
}
|
||||
|
||||
playlist = playlist.slice(startIndex)
|
||||
for await (const [index, _manifest] of playlist.entries()) {
|
||||
let instance = await this.createInstance(_manifest)
|
||||
|
||||
for (const [index, _manifest] of playlist.entries()) {
|
||||
let instance = new TrackInstance(this, _manifest)
|
||||
this.queue.add(instance)
|
||||
}
|
||||
|
||||
this.track_next_instances.push(instance)
|
||||
const item = this.queue.set(startIndex)
|
||||
|
||||
if (index === 0) {
|
||||
this.play(this.track_next_instances[0], {
|
||||
this.play(item, {
|
||||
time: time ?? 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return manifest
|
||||
}
|
||||
|
||||
next() {
|
||||
if (this.track_next_instances.length > 0) {
|
||||
// move current audio instance to history
|
||||
this.track_prev_instances.push(this.track_next_instances.shift())
|
||||
if (this.queue.currentItem) {
|
||||
this.queue.currentItem.stop()
|
||||
}
|
||||
|
||||
if (this.track_next_instances.length === 0) {
|
||||
this.console.log(`No more tracks to play, stopping...`)
|
||||
//const isRandom = this.state.playback_mode === "shuffle"
|
||||
const item = this.queue.next()
|
||||
|
||||
if (!item) {
|
||||
return this.stopPlayback()
|
||||
}
|
||||
|
||||
let nextIndex = 0
|
||||
|
||||
if (this.state.playback_mode === "shuffle") {
|
||||
nextIndex = Math.floor(Math.random() * this.track_next_instances.length)
|
||||
}
|
||||
|
||||
this.play(this.track_next_instances[nextIndex])
|
||||
return this.play(item)
|
||||
}
|
||||
|
||||
previous() {
|
||||
if (this.track_prev_instances.length > 0) {
|
||||
// move current audio instance to history
|
||||
this.track_next_instances.unshift(this.track_prev_instances.pop())
|
||||
|
||||
return this.play(this.track_next_instances[0])
|
||||
if (this.queue.currentItem) {
|
||||
this.queue.currentItem.stop()
|
||||
}
|
||||
|
||||
if (this.track_prev_instances.length === 0) {
|
||||
this.console.log(`[PLAYER] No previous tracks, replying...`)
|
||||
// replay the current track
|
||||
return this.play(this.track_instance)
|
||||
}
|
||||
const item = this.queue.previous()
|
||||
|
||||
return this.play(item)
|
||||
}
|
||||
|
||||
//
|
||||
@ -290,23 +225,23 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (!this.track_instance) {
|
||||
if (!this.queue.currentItem) {
|
||||
this.console.error("No audio instance")
|
||||
return null
|
||||
}
|
||||
|
||||
// set gain exponentially
|
||||
this.track_instance.gainNode.gain.linearRampToValueAtTime(
|
||||
this.queue.currentItem.gainNode.gain.linearRampToValueAtTime(
|
||||
0.0001,
|
||||
this.audioContext.currentTime + (Player.gradualFadeMs / 1000)
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
this.track_instance.audio.pause()
|
||||
this.queue.currentItem.audio.pause()
|
||||
resolve()
|
||||
}, Player.gradualFadeMs)
|
||||
|
||||
this.native_controls.updateIsPlaying(false)
|
||||
this.nativeControls.updateIsPlaying(false)
|
||||
})
|
||||
}
|
||||
|
||||
@ -316,25 +251,25 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (!this.track_instance) {
|
||||
if (!this.queue.currentItem) {
|
||||
this.console.error("No audio instance")
|
||||
return null
|
||||
}
|
||||
|
||||
// ensure audio elemeto starts from 0 volume
|
||||
this.track_instance.gainNode.gain.value = 0.0001
|
||||
this.queue.currentItem.gainNode.gain.value = 0.0001
|
||||
|
||||
this.track_instance.audio.play().then(() => {
|
||||
this.queue.currentItem.audio.play().then(() => {
|
||||
resolve()
|
||||
})
|
||||
|
||||
// set gain exponentially
|
||||
this.track_instance.gainNode.gain.linearRampToValueAtTime(
|
||||
this.queue.currentItem.gainNode.gain.linearRampToValueAtTime(
|
||||
this.state.volume,
|
||||
this.audioContext.currentTime + (Player.gradualFadeMs / 1000)
|
||||
)
|
||||
|
||||
this.native_controls.updateIsPlaying(true)
|
||||
this.nativeControls.updateIsPlaying(true)
|
||||
})
|
||||
}
|
||||
|
||||
@ -345,8 +280,8 @@ export default class Player extends Core {
|
||||
|
||||
this.state.playback_mode = mode
|
||||
|
||||
if (this.track_instance) {
|
||||
this.track_instance.audio.loop = this.state.playback_mode === "repeat"
|
||||
if (this.queue.currentItem) {
|
||||
this.queue.currentItem.audio.loop = this.state.playback_mode === "repeat"
|
||||
}
|
||||
|
||||
AudioPlayerStorage.set("mode", mode)
|
||||
@ -355,17 +290,22 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
async stopPlayback() {
|
||||
this.destroyCurrentInstance()
|
||||
if (this.queue.currentItem) {
|
||||
this.queue.currentItem.stop()
|
||||
}
|
||||
|
||||
this.queue.flush()
|
||||
|
||||
this.abortPreloads()
|
||||
|
||||
this.state.playback_status = "stopped"
|
||||
this.state.track_manifest = null
|
||||
|
||||
this.track_instance = null
|
||||
this.queue.currentItem = null
|
||||
this.track_next_instances = []
|
||||
this.track_prev_instances = []
|
||||
|
||||
this.native_controls.destroy()
|
||||
this.nativeControls.destroy()
|
||||
}
|
||||
|
||||
//
|
||||
@ -383,7 +323,7 @@ export default class Player extends Core {
|
||||
|
||||
if (typeof to === "boolean") {
|
||||
this.state.muted = to
|
||||
this.track_instance.audio.muted = to
|
||||
this.queue.currentItem.audio.muted = to
|
||||
}
|
||||
|
||||
return this.state.muted
|
||||
@ -413,9 +353,9 @@ export default class Player extends Core {
|
||||
|
||||
AudioPlayerStorage.set("volume", volume)
|
||||
|
||||
if (this.track_instance) {
|
||||
if (this.track_instance.gainNode) {
|
||||
this.track_instance.gainNode.gain.value = this.state.volume
|
||||
if (this.queue.currentItem) {
|
||||
if (this.queue.currentItem.gainNode) {
|
||||
this.queue.currentItem.gainNode.gain.value = this.state.volume
|
||||
}
|
||||
}
|
||||
|
||||
@ -423,31 +363,31 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
seek(time) {
|
||||
if (!this.track_instance || !this.track_instance.audio) {
|
||||
if (!this.queue.currentItem || !this.queue.currentItem.audio) {
|
||||
return false
|
||||
}
|
||||
|
||||
// if time not provided, return current time
|
||||
if (typeof time === "undefined") {
|
||||
return this.track_instance.audio.currentTime
|
||||
return this.queue.currentItem.audio.currentTime
|
||||
}
|
||||
|
||||
// if time is provided, seek to that time
|
||||
if (typeof time === "number") {
|
||||
this.console.log(`Seeking to ${time} | Duration: ${this.track_instance.audio.duration}`)
|
||||
this.console.log(`Seeking to ${time} | Duration: ${this.queue.currentItem.audio.duration}`)
|
||||
|
||||
this.track_instance.audio.currentTime = time
|
||||
this.queue.currentItem.audio.currentTime = time
|
||||
|
||||
return time
|
||||
}
|
||||
}
|
||||
|
||||
duration() {
|
||||
if (!this.track_instance || !this.track_instance.audio) {
|
||||
if (!this.queue.currentItem || !this.queue.currentItem.audio) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.track_instance.audio.duration
|
||||
return this.queue.currentItem.audio.duration
|
||||
}
|
||||
|
||||
loop(to) {
|
||||
@ -458,8 +398,8 @@ export default class Player extends Core {
|
||||
|
||||
this.state.loop = to ?? !this.state.loop
|
||||
|
||||
if (this.track_instance.audio) {
|
||||
this.track_instance.audio.loop = this.state.loop
|
||||
if (this.queue.currentItem.audio) {
|
||||
this.queue.currentItem.audio.loop = this.state.loop
|
||||
}
|
||||
|
||||
return this.state.loop
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { Modal } from "antd"
|
||||
import ProcessorNode from "../node"
|
||||
import Presets from "../../classes/Presets"
|
||||
|
||||
@ -6,7 +5,7 @@ export default class CompressorProcessorNode extends ProcessorNode {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.presets_controller = new Presets({
|
||||
this.presets = new Presets({
|
||||
storage_key: "compressor",
|
||||
defaultPresetValue: {
|
||||
threshold: -50,
|
||||
@ -15,98 +14,19 @@ export default class CompressorProcessorNode extends ProcessorNode {
|
||||
attack: 0.003,
|
||||
release: 0.25,
|
||||
},
|
||||
onApplyValues: this.applyValues.bind(this),
|
||||
})
|
||||
|
||||
this.state = {
|
||||
compressorValues: this.presets_controller.currentPresetValues,
|
||||
}
|
||||
|
||||
this.exposeToPublic = {
|
||||
presets: new Proxy(this.presets_controller, {
|
||||
get: function (target, key) {
|
||||
if (!key) {
|
||||
return target
|
||||
}
|
||||
|
||||
return target[key]
|
||||
}
|
||||
}),
|
||||
deletePreset: this.deletePreset.bind(this),
|
||||
createPreset: this.createPreset.bind(this),
|
||||
changePreset: this.changePreset.bind(this),
|
||||
resetDefaultValues: this.resetDefaultValues.bind(this),
|
||||
modifyValues: this.modifyValues.bind(this),
|
||||
detach: this._detach.bind(this),
|
||||
attach: this._attach.bind(this),
|
||||
values: this.state.compressorValues,
|
||||
presets: this.presets,
|
||||
detach: this._detach,
|
||||
attach: this._attach,
|
||||
}
|
||||
}
|
||||
|
||||
static refName = "compressor"
|
||||
static dependsOnSettings = ["player.compressor"]
|
||||
|
||||
deletePreset(key) {
|
||||
this.changePreset("default")
|
||||
|
||||
this.presets_controller.deletePreset(key)
|
||||
|
||||
return this.presets_controller.presets
|
||||
}
|
||||
|
||||
createPreset(key, values) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
compressorValues: this.presets_controller.createPreset(key, values),
|
||||
}
|
||||
|
||||
this.presets_controller.changePreset(key)
|
||||
|
||||
return this.presets_controller.presets
|
||||
}
|
||||
|
||||
changePreset(key) {
|
||||
const values = this.presets_controller.changePreset(key)
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
compressorValues: values,
|
||||
}
|
||||
|
||||
this.applyValues()
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
modifyValues(values) {
|
||||
values = this.presets_controller.setToCurrent(values)
|
||||
|
||||
this.state.compressorValues = {
|
||||
...this.state.compressorValues,
|
||||
...values,
|
||||
}
|
||||
|
||||
this.applyValues()
|
||||
|
||||
return this.state.compressorValues
|
||||
}
|
||||
|
||||
async resetDefaultValues() {
|
||||
return await new Promise((resolve) => {
|
||||
Modal.confirm({
|
||||
title: "Reset to default values?",
|
||||
content: "Are you sure you want to reset to default values?",
|
||||
onOk: () => {
|
||||
this.modifyValues(this.presets_controller.defaultPresetValue)
|
||||
|
||||
resolve(this.state.compressorValues)
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(this.state.compressorValues)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async init(AudioContext) {
|
||||
if (!AudioContext) {
|
||||
throw new Error("AudioContext is required")
|
||||
@ -118,8 +38,8 @@ export default class CompressorProcessorNode extends ProcessorNode {
|
||||
}
|
||||
|
||||
applyValues() {
|
||||
Object.keys(this.state.compressorValues).forEach((key) => {
|
||||
this.processor[key].value = this.state.compressorValues[key]
|
||||
Object.keys(this.presets.currentPresetValues).forEach((key) => {
|
||||
this.processor[key].value = this.presets.currentPresetValues[key]
|
||||
})
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import { Modal } from "antd"
|
||||
import ProcessorNode from "../node"
|
||||
import Presets from "../../classes/Presets"
|
||||
|
||||
@ -6,7 +5,7 @@ export default class EqProcessorNode extends ProcessorNode {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.presets_controller = new Presets({
|
||||
this.presets = new Presets({
|
||||
storage_key: "eq",
|
||||
defaultPresetValue: {
|
||||
32: 0,
|
||||
@ -20,94 +19,21 @@ export default class EqProcessorNode extends ProcessorNode {
|
||||
8000: 0,
|
||||
16000: 0,
|
||||
},
|
||||
onApplyValues: this.applyValues.bind(this),
|
||||
})
|
||||
|
||||
this.state = {
|
||||
eqValues: this.presets_controller.currentPresetValues,
|
||||
}
|
||||
|
||||
this.exposeToPublic = {
|
||||
presets: new Proxy(this.presets_controller, {
|
||||
get: function (target, key) {
|
||||
if (!key) {
|
||||
return target
|
||||
}
|
||||
|
||||
return target[key]
|
||||
},
|
||||
}),
|
||||
deletePreset: this.deletePreset.bind(this),
|
||||
createPreset: this.createPreset.bind(this),
|
||||
changePreset: this.changePreset.bind(this),
|
||||
modifyValues: this.modifyValues.bind(this),
|
||||
resetDefaultValues: this.resetDefaultValues.bind(this),
|
||||
presets: this.presets,
|
||||
}
|
||||
}
|
||||
|
||||
static refName = "eq"
|
||||
static lock = true
|
||||
|
||||
deletePreset(key) {
|
||||
this.changePreset("default")
|
||||
|
||||
this.presets_controller.deletePreset(key)
|
||||
|
||||
return this.presets_controller.presets
|
||||
}
|
||||
|
||||
createPreset(key, values) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
eqValues: this.presets_controller.createPreset(key, values),
|
||||
}
|
||||
|
||||
this.presets_controller.changePreset(key)
|
||||
|
||||
return this.presets_controller.presets
|
||||
}
|
||||
|
||||
changePreset(key) {
|
||||
const values = this.presets_controller.changePreset(key)
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
eqValues: values,
|
||||
}
|
||||
|
||||
this.applyValues()
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
modifyValues(values) {
|
||||
values = this.presets_controller.setToCurrent(values)
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
eqValues: values,
|
||||
}
|
||||
|
||||
this.applyValues()
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
resetDefaultValues() {
|
||||
Modal.confirm({
|
||||
title: "Reset to default values?",
|
||||
content: "Are you sure you want to reset to default values?",
|
||||
onOk: () => {
|
||||
this.modifyValues(this.presets_controller.defaultPresetValue)
|
||||
}
|
||||
})
|
||||
|
||||
return this.state.eqValues
|
||||
}
|
||||
|
||||
applyValues() {
|
||||
// apply to current instance
|
||||
this.processor.eqNodes.forEach((processor) => {
|
||||
const gainValue = this.state.eqValues[processor.frequency.value]
|
||||
const gainValue = this.presets.currentPresetValues[processor.frequency.value]
|
||||
|
||||
if (processor.gain.value !== gainValue) {
|
||||
console.debug(`[EQ] Applying values to ${processor.frequency.value} Hz frequency with gain ${gainValue}`)
|
||||
@ -127,7 +53,7 @@ export default class EqProcessorNode extends ProcessorNode {
|
||||
|
||||
this.processor.eqNodes = []
|
||||
|
||||
const values = Object.entries(this.state.eqValues).map((entry) => {
|
||||
const values = Object.entries(this.presets.currentPresetValues).map((entry) => {
|
||||
return {
|
||||
freq: parseFloat(entry[0]),
|
||||
gain: parseFloat(entry[1]),
|
||||
|
@ -8,7 +8,23 @@ export default class RemoteStorage extends Core {
|
||||
static depends = ["api", "tasksQueue"]
|
||||
|
||||
public = {
|
||||
uploadFile: this.uploadFile.bind(this),
|
||||
uploadFile: this.uploadFile,
|
||||
getFileHash: this.getFileHash,
|
||||
binaryArrayToFile: this.binaryArrayToFile,
|
||||
}
|
||||
|
||||
binaryArrayToFile(bin, filename) {
|
||||
const { format, data } = bin
|
||||
|
||||
const filenameExt = format.split("/")[1]
|
||||
filename = `${filename}.${filenameExt}`
|
||||
|
||||
const byteArray = new Uint8Array(data)
|
||||
const blob = new Blob([byteArray], { type: data.type })
|
||||
|
||||
return new File([blob], filename, {
|
||||
type: format,
|
||||
})
|
||||
}
|
||||
|
||||
async getFileHash(file) {
|
||||
|
@ -3,7 +3,9 @@ import React from "react"
|
||||
const usePageWidgets = (widgets = []) => {
|
||||
React.useEffect(() => {
|
||||
for (const widget of widgets) {
|
||||
app.layout.tools_bar.attachRender(widget.id, widget.component, widget.props)
|
||||
app.layout.tools_bar.attachRender(widget.id, widget.component, widget.props, {
|
||||
position: "top",
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
@ -1,8 +1,6 @@
|
||||
import React from "react"
|
||||
import classnames from "classnames"
|
||||
import { Motion, spring } from "react-motion"
|
||||
import { Translation } from "react-i18next"
|
||||
import { Icons } from "@components/Icons"
|
||||
|
||||
import WidgetsWrapper from "@components/WidgetsWrapper"
|
||||
|
||||
@ -11,7 +9,10 @@ import "./index.less"
|
||||
export default class ToolsBar extends React.Component {
|
||||
state = {
|
||||
visible: false,
|
||||
renders: [],
|
||||
renders: {
|
||||
top: [],
|
||||
bottom: [],
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -34,20 +35,25 @@ export default class ToolsBar extends React.Component {
|
||||
visible: to ?? !this.state.visible,
|
||||
})
|
||||
},
|
||||
attachRender: (id, component, props) => {
|
||||
this.setState({
|
||||
renders: [...this.state.renders, {
|
||||
attachRender: (id, component, props, { position = "bottom" } = {}) => {
|
||||
this.setState((prev) => {
|
||||
prev.renders[position].push({
|
||||
id: id,
|
||||
component: component,
|
||||
props: props,
|
||||
}],
|
||||
})
|
||||
|
||||
return prev
|
||||
})
|
||||
|
||||
return component
|
||||
},
|
||||
detachRender: (id) => {
|
||||
this.setState({
|
||||
renders: this.state.renders.filter((render) => render.id !== id),
|
||||
renders: {
|
||||
top: this.state.renders.top.filter((render) => render.id !== id),
|
||||
bottom: this.state.renders.bottom.filter((render) => render.id !== id),
|
||||
},
|
||||
})
|
||||
|
||||
return true
|
||||
@ -78,15 +84,29 @@ export default class ToolsBar extends React.Component {
|
||||
id="tools_bar"
|
||||
className="tools-bar"
|
||||
>
|
||||
<div className="attached_renders">
|
||||
<div className="attached_renders top">
|
||||
{
|
||||
this.state.renders.map((render) => {
|
||||
return React.createElement(render.component, render.props)
|
||||
this.state.renders.top.map((render, index) => {
|
||||
return React.createElement(render.component, {
|
||||
...render.props,
|
||||
key: index,
|
||||
})
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<WidgetsWrapper />
|
||||
|
||||
<div className="attached_renders bottom">
|
||||
{
|
||||
this.state.renders.bottom.map((render, index) => {
|
||||
return React.createElement(render.component, {
|
||||
...render.props,
|
||||
key: index,
|
||||
})
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}}
|
||||
|
@ -26,6 +26,8 @@
|
||||
}
|
||||
|
||||
.tools-bar {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -46,14 +48,32 @@
|
||||
flex: 0;
|
||||
|
||||
.attached_renders {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: fit-content;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
&.bottom {
|
||||
position: absolute;
|
||||
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ const ServiceIndicator = (props) => {
|
||||
}
|
||||
|
||||
const AudioPlayer = (props) => {
|
||||
const playerState = usePlayerStateContext()
|
||||
const [playerState] = usePlayerStateContext()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (app.currentDragger) {
|
||||
|
115
packages/app/src/pages/_debug/queuemanager/index.jsx
Normal file
115
packages/app/src/pages/_debug/queuemanager/index.jsx
Normal file
@ -0,0 +1,115 @@
|
||||
import React, { useState } from "react"
|
||||
import { Button, Card, List, Typography, Space, Divider, notification } from "antd"
|
||||
import QueueManager from "@cores/player/classes/QueueManager"
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
const QueueDebugger = () => {
|
||||
const queueManager = React.useRef(new QueueManager())
|
||||
|
||||
const [current, setCurrent] = useState(queueManager.current.currentItem)
|
||||
const [prevItems, setPrevItems] = useState([...queueManager.current.prevItems])
|
||||
const [nextItems, setNextItems] = useState([...queueManager.current.nextItems])
|
||||
|
||||
const updateQueueState = () => {
|
||||
setCurrent(queueManager.current.currentItem)
|
||||
setPrevItems([...queueManager.current.prevItems])
|
||||
setNextItems([...queueManager.current.nextItems])
|
||||
}
|
||||
|
||||
const handleNext = (random = false) => {
|
||||
queueManager.current.next(random)
|
||||
updateQueueState()
|
||||
}
|
||||
|
||||
const handlePrevious = () => {
|
||||
queueManager.current.previous()
|
||||
updateQueueState()
|
||||
}
|
||||
|
||||
const handleSet = (item) => {
|
||||
try {
|
||||
queueManager.current.set(item)
|
||||
updateQueueState()
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: "Error",
|
||||
description: error.message,
|
||||
placement: "bottomRight",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
const newItem = {
|
||||
id: (nextItems.length + prevItems.length + 2).toString(),
|
||||
name: `Item ${nextItems.length + prevItems.length + 2}`
|
||||
}
|
||||
queueManager.current.add(newItem)
|
||||
updateQueueState()
|
||||
}
|
||||
|
||||
const handleRemove = (item) => {
|
||||
queueManager.current.remove(item)
|
||||
updateQueueState()
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
queueManager.current.add({ id: "1", name: "Item 1" })
|
||||
queueManager.current.add({ id: "2", name: "Item 2" })
|
||||
queueManager.current.add({ id: "3", name: "Item 3" })
|
||||
queueManager.current.add({ id: "4", name: "Item 4" })
|
||||
|
||||
updateQueueState()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: "100%", padding: "20px" }}>
|
||||
<Title level={2}>Queue Debugger</Title>
|
||||
<Card title="Current Item">
|
||||
<Text>{current ? current.name : "None"}</Text>
|
||||
</Card>
|
||||
<Divider />
|
||||
<Card title="Previous Items">
|
||||
<List
|
||||
bordered
|
||||
dataSource={prevItems}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button type="link" onClick={() => handleSet(item)}>Set</Button>,
|
||||
]}
|
||||
>
|
||||
{item.name}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
<Card title="Next Items">
|
||||
<List
|
||||
bordered
|
||||
dataSource={nextItems}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button type="link" onClick={() => handleSet(item)}>Set</Button>,
|
||||
<Button type="link" danger onClick={() => handleRemove(item)}>Remove</Button>,
|
||||
]}
|
||||
>
|
||||
{item.name}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
<Divider />
|
||||
<Space>
|
||||
<Button onClick={handlePrevious}>Previous</Button>
|
||||
<Button onClick={() => handleNext(false)}>Next</Button>
|
||||
<Button onClick={() => handleNext(true)}>Next (Random)</Button>
|
||||
<Button type="primary" onClick={handleAdd}>Add Item</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
export default QueueDebugger
|
@ -47,7 +47,7 @@ const RenderAlbum = (props) => {
|
||||
}
|
||||
|
||||
const PlayerController = React.forwardRef((props, ref) => {
|
||||
const playerState = usePlayerStateContext()
|
||||
const [playerState] = usePlayerStateContext()
|
||||
|
||||
const titleRef = React.useRef()
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { Motion, spring } from "react-motion"
|
||||
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||
|
||||
const LyricsText = React.forwardRef((props, textRef) => {
|
||||
const playerState = usePlayerStateContext()
|
||||
const [playerState] = usePlayerStateContext()
|
||||
|
||||
const { lyrics } = props
|
||||
|
||||
@ -55,7 +55,7 @@ const LyricsText = React.forwardRef((props, textRef) => {
|
||||
setVisible(false)
|
||||
} else {
|
||||
setVisible(true)
|
||||
console.log(`Scrolling to line ${currentLineIndex}`)
|
||||
|
||||
// find line element by id
|
||||
const lineElement = textRef.current.querySelector(`#lyrics-line-${currentLineIndex}`)
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||
const maxLatencyInMs = 55
|
||||
|
||||
const LyricsVideo = React.forwardRef((props, videoRef) => {
|
||||
const playerState = usePlayerStateContext()
|
||||
const [playerState] = usePlayerStateContext()
|
||||
|
||||
const { lyrics } = props
|
||||
|
||||
@ -57,12 +57,10 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
||||
setCurrentVideoLatency(currentVideoTimeDiff)
|
||||
|
||||
if (syncingVideo === true) {
|
||||
console.log(`Syncing video...`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (currentVideoTimeDiff > maxOffset) {
|
||||
console.warn(`Video offset exceeds`, maxOffset)
|
||||
seekVideoToSyncAudio()
|
||||
}
|
||||
}
|
||||
|
@ -22,8 +22,8 @@ function getDominantColorStr(track_manifest) {
|
||||
return `${values[0]}, ${values[1]}, ${values[2]}`
|
||||
}
|
||||
|
||||
const EnchancedLyrics = (props) => {
|
||||
const playerState = usePlayerStateContext()
|
||||
const EnchancedLyricsPage = () => {
|
||||
const [playerState] = usePlayerStateContext()
|
||||
|
||||
const [initialized, setInitialized] = React.useState(false)
|
||||
const [lyrics, setLyrics] = React.useState(null)
|
||||
@ -71,11 +71,6 @@ const EnchancedLyrics = (props) => {
|
||||
}
|
||||
}, [playerState.track_manifest])
|
||||
|
||||
//* Handle when lyrics data change
|
||||
React.useEffect(() => {
|
||||
console.log(lyrics)
|
||||
}, [lyrics])
|
||||
|
||||
React.useEffect(() => {
|
||||
setInitialized(true)
|
||||
}, [])
|
||||
@ -129,4 +124,4 @@ const EnchancedLyrics = (props) => {
|
||||
</div>
|
||||
}
|
||||
|
||||
export default EnchancedLyrics
|
||||
export default EnchancedLyricsPage
|
@ -0,0 +1,49 @@
|
||||
import React from "react"
|
||||
|
||||
import Image from "@components/Image"
|
||||
|
||||
import MusicModel from "@models/music"
|
||||
|
||||
const FeaturedPlaylist = (props) => {
|
||||
const [featuredPlaylist, setFeaturedPlaylist] = React.useState(false)
|
||||
|
||||
const onClick = () => {
|
||||
if (!featuredPlaylist) {
|
||||
return
|
||||
}
|
||||
|
||||
app.navigation.goToPlaylist(featuredPlaylist.playlist_id)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
MusicModel.getFeaturedPlaylists().then((data) => {
|
||||
if (data[0]) {
|
||||
console.log(`Loaded featured playlist >`, data[0])
|
||||
setFeaturedPlaylist(data[0])
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!featuredPlaylist) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className="featured_playlist" onClick={onClick}>
|
||||
<Image
|
||||
src={featuredPlaylist.cover_url}
|
||||
/>
|
||||
|
||||
<div className="featured_playlist_content">
|
||||
<h1>{featuredPlaylist.title}</h1>
|
||||
<p>{featuredPlaylist.description}</p>
|
||||
|
||||
{
|
||||
featuredPlaylist.genre && <div className="featured_playlist_genre">
|
||||
<span>{featuredPlaylist.genre}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default FeaturedPlaylist
|
@ -0,0 +1,18 @@
|
||||
import React from "react"
|
||||
|
||||
import Searcher from "@components/Searcher"
|
||||
import MusicModel from "@models/music"
|
||||
|
||||
const MusicNavbar = (props) => {
|
||||
return <div className="music_navbar">
|
||||
<Searcher
|
||||
useUrlQuery
|
||||
renderResults={false}
|
||||
model={MusicModel.search}
|
||||
onSearchResult={props.setSearchResults}
|
||||
onEmpty={() => props.setSearchResults(false)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default MusicNavbar
|
@ -0,0 +1,14 @@
|
||||
import React from "react"
|
||||
import { Icons } from "@components/Icons"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const RecentlyPlayedList = (props) => {
|
||||
return <div className="recently_played">
|
||||
<div className="recently_played-header">
|
||||
<h1>Recently played</h1>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default RecentlyPlayedList
|
@ -0,0 +1,17 @@
|
||||
.recently_played {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
.recently_played-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
import { Translation } from "react-i18next"
|
||||
|
||||
import { createIconRender } from "@components/Icons"
|
||||
import MusicTrack from "@components/Music/Track"
|
||||
import PlaylistItem from "@components/Music/PlaylistItem"
|
||||
|
||||
const ResultGroupsDecorators = {
|
||||
"playlists": {
|
||||
icon: "MdPlaylistPlay",
|
||||
label: "Playlists",
|
||||
renderItem: (props) => {
|
||||
return <PlaylistItem
|
||||
key={props.key}
|
||||
playlist={props.item}
|
||||
/>
|
||||
}
|
||||
},
|
||||
"tracks": {
|
||||
icon: "MdMusicNote",
|
||||
label: "Tracks",
|
||||
renderItem: (props) => {
|
||||
return <MusicTrack
|
||||
key={props.key}
|
||||
track={props.item}
|
||||
onClickPlayBtn={() => app.cores.player.start(props.item)}
|
||||
onClick={() => app.location.push(`/play/${props.item._id}`)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SearchResults = ({
|
||||
data
|
||||
}) => {
|
||||
if (typeof data !== "object") {
|
||||
return null
|
||||
}
|
||||
|
||||
let groupsKeys = Object.keys(data)
|
||||
|
||||
// filter out empty groups
|
||||
groupsKeys = groupsKeys.filter((key) => {
|
||||
return data[key].length > 0
|
||||
})
|
||||
|
||||
if (groupsKeys.length === 0) {
|
||||
return <div className="music-explorer_search_results no_results">
|
||||
<antd.Result
|
||||
status="info"
|
||||
title="No results"
|
||||
subTitle="We are sorry, but we could not find any results for your search."
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div
|
||||
className={classnames(
|
||||
"music-explorer_search_results",
|
||||
{
|
||||
["one_column"]: groupsKeys.length === 1,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{
|
||||
groupsKeys.map((key, index) => {
|
||||
const decorator = ResultGroupsDecorators[key] ?? {
|
||||
icon: null,
|
||||
label: key,
|
||||
renderItem: () => null
|
||||
}
|
||||
|
||||
return <div className="music-explorer_search_results_group" key={index}>
|
||||
<div className="music-explorer_search_results_group_header">
|
||||
<h1>
|
||||
{
|
||||
createIconRender(decorator.icon)
|
||||
}
|
||||
<Translation>
|
||||
{(t) => t(decorator.label)}
|
||||
</Translation>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="music-explorer_search_results_group_list">
|
||||
{
|
||||
data[key].map((item, index) => {
|
||||
return decorator.renderItem({
|
||||
key: index,
|
||||
item
|
||||
})
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default SearchResults
|
@ -1,175 +1,27 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
import { Translation } from "react-i18next"
|
||||
|
||||
import Image from "@components/Image"
|
||||
import Searcher from "@components/Searcher"
|
||||
import { Icons, createIconRender } from "@components/Icons"
|
||||
import MusicTrack from "@components/Music/Track"
|
||||
import PlaylistItem from "@components/Music/PlaylistItem"
|
||||
|
||||
import ReleasesList from "@components/ReleasesList"
|
||||
import { Icons } from "@components/Icons"
|
||||
|
||||
import FeedModel from "@models/feed"
|
||||
import MusicModel from "@models/music"
|
||||
|
||||
import Navbar from "./components/Navbar"
|
||||
import RecentlyPlayedList from "./components/RecentlyPlayedList"
|
||||
import SearchResults from "./components/SearchResults"
|
||||
import ReleasesList from "./components/ReleasesList"
|
||||
import FeaturedPlaylist from "./components/FeaturedPlaylist"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const FeaturedPlaylist = (props) => {
|
||||
const [featuredPlaylist, setFeaturedPlaylist] = React.useState(false)
|
||||
|
||||
const onClick = () => {
|
||||
if (!featuredPlaylist) {
|
||||
return
|
||||
}
|
||||
|
||||
app.navigation.goToPlaylist(featuredPlaylist.playlist_id)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
MusicModel.getFeaturedPlaylists().then((data) => {
|
||||
if (data[0]) {
|
||||
console.log(`Loaded featured playlist >`, data[0])
|
||||
setFeaturedPlaylist(data[0])
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!featuredPlaylist) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className="featured_playlist" onClick={onClick}>
|
||||
<Image
|
||||
src={featuredPlaylist.cover_url}
|
||||
/>
|
||||
|
||||
<div className="featured_playlist_content">
|
||||
<h1>{featuredPlaylist.title}</h1>
|
||||
<p>{featuredPlaylist.description}</p>
|
||||
|
||||
{
|
||||
featuredPlaylist.genre && <div className="featured_playlist_genre">
|
||||
<span>{featuredPlaylist.genre}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const MusicNavbar = (props) => {
|
||||
return <div className="music_navbar">
|
||||
<Searcher
|
||||
useUrlQuery
|
||||
renderResults={false}
|
||||
model={MusicModel.search}
|
||||
onSearchResult={props.setSearchResults}
|
||||
onEmpty={() => props.setSearchResults(false)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
const ResultGroupsDecorators = {
|
||||
"playlists": {
|
||||
icon: "MdPlaylistPlay",
|
||||
label: "Playlists",
|
||||
renderItem: (props) => {
|
||||
return <PlaylistItem
|
||||
key={props.key}
|
||||
playlist={props.item}
|
||||
/>
|
||||
}
|
||||
},
|
||||
"tracks": {
|
||||
icon: "MdMusicNote",
|
||||
label: "Tracks",
|
||||
renderItem: (props) => {
|
||||
return <MusicTrack
|
||||
key={props.key}
|
||||
track={props.item}
|
||||
onClickPlayBtn={() => app.cores.player.start(props.item)}
|
||||
onClick={() => app.location.push(`/play/${props.item._id}`)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SearchResults = ({
|
||||
data
|
||||
}) => {
|
||||
if (typeof data !== "object") {
|
||||
return null
|
||||
}
|
||||
|
||||
let groupsKeys = Object.keys(data)
|
||||
|
||||
// filter out empty groups
|
||||
groupsKeys = groupsKeys.filter((key) => {
|
||||
return data[key].length > 0
|
||||
})
|
||||
|
||||
if (groupsKeys.length === 0) {
|
||||
return <div className="music-explorer_search_results no_results">
|
||||
<antd.Result
|
||||
status="info"
|
||||
title="No results"
|
||||
subTitle="We are sorry, but we could not find any results for your search."
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div
|
||||
className={classnames(
|
||||
"music-explorer_search_results",
|
||||
{
|
||||
["one_column"]: groupsKeys.length === 1,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{
|
||||
groupsKeys.map((key, index) => {
|
||||
const decorator = ResultGroupsDecorators[key] ?? {
|
||||
icon: null,
|
||||
label: key,
|
||||
renderItem: () => null
|
||||
}
|
||||
|
||||
return <div className="music-explorer_search_results_group" key={index}>
|
||||
<div className="music-explorer_search_results_group_header">
|
||||
<h1>
|
||||
{
|
||||
createIconRender(decorator.icon)
|
||||
}
|
||||
<Translation>
|
||||
{(t) => t(decorator.label)}
|
||||
</Translation>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="music-explorer_search_results_group_list">
|
||||
{
|
||||
data[key].map((item, index) => {
|
||||
return decorator.renderItem({
|
||||
key: index,
|
||||
item
|
||||
})
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default (props) => {
|
||||
const MusicExploreTab = (props) => {
|
||||
const [searchResults, setSearchResults] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
app.layout.toggleCenteredContent(true)
|
||||
|
||||
app.layout.page_panels.attachComponent("music_navbar", MusicNavbar, {
|
||||
app.layout.page_panels.attachComponent("music_navbar", Navbar, {
|
||||
props: {
|
||||
setSearchResults: setSearchResults,
|
||||
}
|
||||
@ -192,9 +44,6 @@ export default (props) => {
|
||||
useUrlQuery
|
||||
renderResults={false}
|
||||
model={MusicModel.search}
|
||||
modelParams={{
|
||||
useTidal: app.cores.sync.getActiveLinkedServices().tidal,
|
||||
}}
|
||||
onSearchResult={setSearchResults}
|
||||
onEmpty={() => setSearchResults(false)}
|
||||
/>
|
||||
@ -210,6 +59,8 @@ export default (props) => {
|
||||
!searchResults && <div className="feed_main">
|
||||
<FeaturedPlaylist />
|
||||
|
||||
<RecentlyPlayedList />
|
||||
|
||||
<ReleasesList
|
||||
headerTitle="From your following artists"
|
||||
headerIcon={<Icons.MdPerson />}
|
||||
@ -225,3 +76,5 @@ export default (props) => {
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default MusicExploreTab
|
@ -49,47 +49,62 @@ export default class FavoriteTracks extends React.Component {
|
||||
loading: true,
|
||||
})
|
||||
|
||||
const result = await MusicModel.getFavoriteTracks({
|
||||
useTidal: app.cores.sync.getActiveLinkedServices().tidal,
|
||||
const result = await MusicModel.getFavouriteFolder({
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
}).catch((err) => {
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
error: err.message,
|
||||
error: error.message,
|
||||
})
|
||||
return false
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
console.log("Loaded favorites => ", result)
|
||||
|
||||
if (result) {
|
||||
const { tracks, total_length } = result
|
||||
const {
|
||||
tracks,
|
||||
releases,
|
||||
playlists,
|
||||
total_length,
|
||||
} = result
|
||||
|
||||
this.setState({
|
||||
total_length
|
||||
})
|
||||
const data = [
|
||||
...tracks.list,
|
||||
...releases.list,
|
||||
...playlists.list,
|
||||
]
|
||||
|
||||
if (tracks.length === 0) {
|
||||
if (offset === 0) {
|
||||
if (total_length === 0) {
|
||||
this.setState({
|
||||
empty: true,
|
||||
hasMore: false,
|
||||
initialLoading: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return this.setState({
|
||||
empty: false,
|
||||
hasMore: false,
|
||||
initialLoading: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (replace) {
|
||||
this.setState({
|
||||
list: tracks,
|
||||
list: data,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
list: [...this.state.list, ...tracks],
|
||||
list: [...this.state.list, ...data],
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
total_length
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
@ -122,7 +137,6 @@ export default class FavoriteTracks extends React.Component {
|
||||
centered={app.isMobile}
|
||||
onLoadMore={this.onLoadMore}
|
||||
hasMore={this.state.hasMore}
|
||||
empty={this.state.empty}
|
||||
length={this.state.total_length}
|
||||
/>
|
||||
}
|
||||
|
@ -2,23 +2,12 @@ import React from "react"
|
||||
import { Translation } from "react-i18next"
|
||||
|
||||
import { PagePanelWithNavMenu } from "@components/PagePanels"
|
||||
import TrendingsCard from "@components/TrendingsCard"
|
||||
|
||||
import usePageWidgets from "@hooks/usePageWidgets"
|
||||
|
||||
import Tabs from "./tabs"
|
||||
|
||||
const TrendingsCard = () => {
|
||||
return <div className="card">
|
||||
<div className="card-header">
|
||||
<span>Trendings</span>
|
||||
</div>
|
||||
|
||||
<div className="card-content">
|
||||
<span>XD</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const TimelinePage = () => {
|
||||
usePageWidgets([
|
||||
{
|
||||
|
@ -3,7 +3,7 @@ import * as antd from "antd"
|
||||
|
||||
import { Icons } from "@components/Icons"
|
||||
|
||||
import UserModel from "@models/user"
|
||||
import AuthModel from "@models/auth"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
@ -32,7 +32,7 @@ const ChangePasswordComponent = (props) => {
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
|
||||
const result = await UserModel.changePassword({ currentPassword, newPassword }).catch((err) => {
|
||||
const result = await AuthModel.changePassword({ currentPassword, newPassword }).catch((err) => {
|
||||
console.error(err)
|
||||
setError(err.response.data.message)
|
||||
return null
|
||||
|
@ -6,14 +6,23 @@ import { Icons } from "@components/Icons"
|
||||
import Sliders from "../sliderValues"
|
||||
|
||||
export default (props) => {
|
||||
const [selectedPreset, setSelectedPreset] = React.useState(props.controller.presets.currentPresetKey)
|
||||
const [presets, setPresets] = React.useState(props.controller.presets.presets ?? {})
|
||||
const [selectedPreset, setSelectedPreset] = React.useState(props.controller.currentPresetKey)
|
||||
const [presets, setPresets] = React.useState(props.controller.presets ?? {})
|
||||
|
||||
const createPreset = (key) => {
|
||||
setPresets(props.controller.createPreset(key))
|
||||
const presets = props.controller.createPreset(key)
|
||||
|
||||
setPresets(presets)
|
||||
setSelectedPreset(key)
|
||||
}
|
||||
|
||||
const deletePreset = (key) => {
|
||||
const presets = props.controller.deletePreset(key)
|
||||
|
||||
setPresets(presets)
|
||||
setSelectedPreset(props.controller.currentPresetKey)
|
||||
}
|
||||
|
||||
const handleCreateNewPreset = () => {
|
||||
app.layout.modal.open("create_preset", (props) => {
|
||||
const [presetKey, setPresetKey] = React.useState("")
|
||||
@ -51,15 +60,11 @@ export default (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeletePreset = () => {
|
||||
const handleDeleteCurrentPreset = () => {
|
||||
Modal.confirm({
|
||||
title: "Delete preset",
|
||||
content: "Are you sure you want to delete this preset?",
|
||||
onOk: () => {
|
||||
props.controller.deletePreset(selectedPreset)
|
||||
setPresets(props.controller.presets.presets ?? {})
|
||||
setSelectedPreset(props.controller.presets.currentPresetKey)
|
||||
}
|
||||
onOk: () => deletePreset(selectedPreset)
|
||||
})
|
||||
}
|
||||
|
||||
@ -77,14 +82,13 @@ export default (props) => {
|
||||
]
|
||||
|
||||
React.useEffect(() => {
|
||||
const presets = props.controller.presets.presets ?? {}
|
||||
const preset = presets[selectedPreset]
|
||||
const presetValues = props.controller.presets[selectedPreset]
|
||||
|
||||
if (props.controller.presets.currentPresetKey !== selectedPreset) {
|
||||
if (props.controller.currentPresetKey !== selectedPreset) {
|
||||
props.controller.changePreset(selectedPreset)
|
||||
}
|
||||
|
||||
props.ctx.updateCurrentValue(preset)
|
||||
props.ctx.updateCurrentValue(presetValues)
|
||||
}, [selectedPreset])
|
||||
|
||||
return <>
|
||||
@ -152,7 +156,7 @@ export default (props) => {
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleDeletePreset}
|
||||
onClick={handleDeleteCurrentPreset}
|
||||
icon={<Icons.MdDelete />}
|
||||
disabled={selectedPreset === "default"}
|
||||
/>
|
||||
|
@ -64,7 +64,7 @@ export default {
|
||||
return app.cores.player.audioContext.sampleRate
|
||||
},
|
||||
onUpdate: async (value) => {
|
||||
const sampleRate = await app.cores.player.setSampleRate(value)
|
||||
const sampleRate = await app.cores.player.controls.setSampleRate(value)
|
||||
|
||||
return sampleRate
|
||||
},
|
||||
@ -93,6 +93,18 @@ export default {
|
||||
app.cores.player.compressor.detach()
|
||||
}
|
||||
},
|
||||
extraActions: [
|
||||
{
|
||||
id: "reset",
|
||||
title: "Default",
|
||||
icon: "MdRefresh",
|
||||
onClick: async (ctx) => {
|
||||
const values = await app.cores.player.compressor.presets.setCurrentPresetToDefault()
|
||||
|
||||
ctx.updateCurrentValue(values)
|
||||
}
|
||||
}
|
||||
],
|
||||
props: {
|
||||
valueFormat: (value) => `${value}dB`,
|
||||
sliders: [
|
||||
@ -131,20 +143,8 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
extraActions: [
|
||||
{
|
||||
id: "reset",
|
||||
title: "Default",
|
||||
icon: "MdRefresh",
|
||||
onClick: async (ctx) => {
|
||||
const values = await app.cores.player.compressor.resetDefaultValues()
|
||||
|
||||
ctx.updateCurrentValue(values)
|
||||
}
|
||||
}
|
||||
],
|
||||
onUpdate: (value) => {
|
||||
app.cores.player.compressor.modifyValues(value)
|
||||
app.cores.player.compressor.presets.setToCurrent(value)
|
||||
|
||||
return value
|
||||
},
|
||||
@ -164,15 +164,15 @@ export default {
|
||||
title: "Reset",
|
||||
icon: "MdRefresh",
|
||||
onClick: (ctx) => {
|
||||
const values = app.cores.player.eq.resetDefaultValues()
|
||||
const values = app.cores.player.eq.presets.setCurrentPresetToDefault()
|
||||
|
||||
ctx.updateCurrentValue(values)
|
||||
}
|
||||
},
|
||||
],
|
||||
dependsOn: {
|
||||
"player.equalizer": true
|
||||
},
|
||||
// dependsOn: {
|
||||
// "player.equalizer": true
|
||||
// },
|
||||
props: {
|
||||
valueFormat: (value) => `${value}dB`,
|
||||
marks: [
|
||||
@ -251,7 +251,7 @@ export default {
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
app.cores.player.eq.modifyValues(values)
|
||||
app.cores.player.eq.presets.setToCurrent(values)
|
||||
|
||||
return value
|
||||
},
|
||||
|
@ -4,7 +4,7 @@ import SlidersWithPresets from "../../../components/slidersWithPresets"
|
||||
export default (props) => {
|
||||
return <SlidersWithPresets
|
||||
{...props}
|
||||
controller={app.cores.player.compressor}
|
||||
controller={app.cores.player.compressor.presets}
|
||||
extraHeaderItems={[
|
||||
<Switch
|
||||
onChange={props.onEnabledChange}
|
||||
|
@ -3,6 +3,6 @@ import SlidersWithPresets from "../../../components/slidersWithPresets"
|
||||
export default (props) => {
|
||||
return <SlidersWithPresets
|
||||
{...props}
|
||||
controller={app.cores.player.eq}
|
||||
controller={app.cores.player.eq.presets}
|
||||
/>
|
||||
}
|
@ -26,6 +26,18 @@ export default defineConfig({
|
||||
headers: {
|
||||
"Strict-Transport-Security": `max-age=${oneYearInSeconds}`
|
||||
},
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "https://0.0.0.0:9000",
|
||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||
hostRewrite: true,
|
||||
changeOrigin: true,
|
||||
xfwd: true,
|
||||
//ws: true,
|
||||
toProxy: true,
|
||||
secure: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
|
@ -160,13 +160,11 @@ export default class Proxy {
|
||||
}
|
||||
|
||||
if (sanitizedUrl === "/") {
|
||||
return res.end(`
|
||||
{
|
||||
"name": "${pkg.name}",
|
||||
"version": "${pkg.version}",
|
||||
"lb_version": "${defaults.version}"
|
||||
}
|
||||
`)
|
||||
return res.end(JSON.stringify({
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
lb_version: defaults.version
|
||||
}))
|
||||
}
|
||||
|
||||
const namespace = `/${sanitizedUrl.split("/")[1]}`
|
||||
|
@ -2,4 +2,6 @@ export default class Track {
|
||||
static create = require("./methods/create").default
|
||||
static delete = require("./methods/delete").default
|
||||
static get = require("./methods/get").default
|
||||
static toggleFavourite = require("./methods/toggleFavourite").default
|
||||
static isFavourite = require("./methods/isFavourite").default
|
||||
}
|
@ -1,32 +1,66 @@
|
||||
import { Track } from "@db_models"
|
||||
import { Track, TrackLike } from "@db_models"
|
||||
|
||||
export default async (track_id, { limit = 50, offset = 0 } = {}) => {
|
||||
export default async (track_id, { user_id = null, onlyList = false } = {}) => {
|
||||
if (!track_id) {
|
||||
throw new OperationError(400, "Missing track_id")
|
||||
}
|
||||
|
||||
const isMultiple = track_id.includes(",")
|
||||
const isMultiple = Array.isArray(track_id) || track_id.includes(",")
|
||||
|
||||
if (isMultiple) {
|
||||
const track_ids = track_id.split(",")
|
||||
const track_ids = Array.isArray(track_id) ? track_id : track_id.split(",")
|
||||
|
||||
const tracks = await Track.find({ _id: { $in: track_ids } })
|
||||
.limit(limit)
|
||||
.skip(offset)
|
||||
const tracks = await Track.find({
|
||||
_id: { $in: track_ids }
|
||||
}).lean()
|
||||
|
||||
if (user_id) {
|
||||
const trackLikes = await TrackLike.find({
|
||||
user_id: user_id,
|
||||
track_id: { $in: track_ids }
|
||||
})
|
||||
|
||||
// FIXME: this could be a performance issue when there are a lot of likes
|
||||
// Array.find may not be a good idea
|
||||
for (const trackLike of trackLikes) {
|
||||
const track = tracks.find(track => track._id.toString() === trackLike.track_id.toString())
|
||||
|
||||
if (track) {
|
||||
track.liked_at = trackLike.created_at
|
||||
track.liked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onlyList) {
|
||||
return tracks
|
||||
}
|
||||
|
||||
return {
|
||||
total_count: await Track.countDocuments({ _id: { $in: track_ids } }),
|
||||
list: tracks.map(track => track.toObject()),
|
||||
list: tracks,
|
||||
}
|
||||
}
|
||||
|
||||
const track = await Track.findOne({
|
||||
_id: track_id
|
||||
})
|
||||
}).lean()
|
||||
|
||||
if (!track) {
|
||||
throw new OperationError(404, "Track not found")
|
||||
}
|
||||
|
||||
if (user_id) {
|
||||
const trackLike = await TrackLike.findOne({
|
||||
user_id: user_id,
|
||||
track_id: track_id,
|
||||
})
|
||||
|
||||
if (trackLike) {
|
||||
track.liked_at = trackLike.created_at
|
||||
track.liked = true
|
||||
}
|
||||
}
|
||||
|
||||
return track
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { Track, TrackLike } from "@db_models"
|
||||
|
||||
export default async (user_id, track_id, to) => {
|
||||
if (!user_id) {
|
||||
throw new OperationError(400, "Missing user_id")
|
||||
}
|
||||
|
||||
if (!track_id) {
|
||||
throw new OperationError(400, "Missing track_id")
|
||||
}
|
||||
|
||||
const track = await Track.findById(track_id)
|
||||
|
||||
if (!track) {
|
||||
throw new OperationError(404, "Track not found")
|
||||
}
|
||||
|
||||
let trackLike = await TrackLike.findOne({
|
||||
user_id: user_id,
|
||||
track_id: track_id,
|
||||
}).catch(() => null)
|
||||
|
||||
return {
|
||||
liked: !!trackLike
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
import { Track, TrackLike } from "@db_models"
|
||||
|
||||
export default async (user_id, track_id, to) => {
|
||||
if (!user_id) {
|
||||
throw new OperationError(400, "Missing user_id")
|
||||
}
|
||||
|
||||
if (!track_id) {
|
||||
throw new OperationError(400, "Missing track_id")
|
||||
}
|
||||
|
||||
const track = await Track.findById(track_id)
|
||||
|
||||
if (!track) {
|
||||
throw new OperationError(404, "Track not found")
|
||||
}
|
||||
|
||||
let trackLike = await TrackLike.findOne({
|
||||
user_id: user_id,
|
||||
track_id: track_id,
|
||||
}).catch(() => null)
|
||||
|
||||
if (typeof to === "undefined") {
|
||||
to = !!!trackLike
|
||||
}
|
||||
|
||||
if (to) {
|
||||
if (!trackLike) {
|
||||
trackLike = new TrackLike({
|
||||
user_id: user_id,
|
||||
track_id: track_id,
|
||||
created_at: Date.now(),
|
||||
})
|
||||
|
||||
await trackLike.save()
|
||||
}
|
||||
} else {
|
||||
if (trackLike) {
|
||||
await TrackLike.deleteOne({
|
||||
user_id: user_id,
|
||||
track_id: track_id,
|
||||
})
|
||||
|
||||
trackLike = null
|
||||
}
|
||||
}
|
||||
|
||||
console.log(global.websocket.find)
|
||||
|
||||
const targetSocket = await global.websocket.find.socketByUserId(user_id)
|
||||
|
||||
if (targetSocket) {
|
||||
await targetSocket.emit("music:track:toggle:like", {
|
||||
track_id: track_id,
|
||||
action: trackLike ? "liked" : "unliked"
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
liked: trackLike ? true : false,
|
||||
track_like_id: trackLike ? trackLike._id : null,
|
||||
track_id: track._id.toString(),
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import LimitsClass from "@shared-classes/Limits"
|
||||
|
||||
export default class API extends Server {
|
||||
static refName = "music"
|
||||
static enableWebsockets = true
|
||||
static routesPath = `${__dirname}/routes`
|
||||
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3003
|
||||
|
||||
|
@ -61,6 +61,10 @@ export default async (req) => {
|
||||
if (typeof trackLyrics.lrc === "object") {
|
||||
trackLyrics.translated_lang = translate_lang
|
||||
|
||||
if (!trackLyrics.lrc[translate_lang]) {
|
||||
translate_lang = "original"
|
||||
}
|
||||
|
||||
if (trackLyrics.lrc[translate_lang]) {
|
||||
trackLyrics.synced_lyrics = await remoteLcrToSyncedLyrics(trackLyrics.lrc[translate_lang])
|
||||
}
|
||||
|
73
packages/server/services/music/routes/music/my/folder/get.js
Normal file
73
packages/server/services/music/routes/music/my/folder/get.js
Normal file
@ -0,0 +1,73 @@
|
||||
import {
|
||||
TrackLike,
|
||||
} from "@db_models"
|
||||
|
||||
import TrackClass from "@classes/track"
|
||||
|
||||
//
|
||||
// A endpoint to fetch track & playlists & releases likes
|
||||
//
|
||||
export default {
|
||||
middlewares: ["withAuthentication"],
|
||||
fn: async (req) => {
|
||||
const user_id = req.auth.session.user_id
|
||||
const { limit, offset } = req.query
|
||||
|
||||
const [
|
||||
totalTrackLikes,
|
||||
totalReleasesLikes,
|
||||
totalPlaylistsLikes,
|
||||
] = await Promise.all([
|
||||
TrackLike.countDocuments({ user_id }),
|
||||
0,
|
||||
0,
|
||||
])
|
||||
|
||||
let [
|
||||
trackLikes,
|
||||
releasesLikes,
|
||||
playlistsLikes
|
||||
] = await Promise.all([
|
||||
TrackLike.find({
|
||||
user_id
|
||||
})
|
||||
.limit(limit)
|
||||
.skip(offset),
|
||||
[],
|
||||
[],
|
||||
])
|
||||
|
||||
let [
|
||||
Tracks,
|
||||
Releases,
|
||||
Playlists,
|
||||
] = await Promise.all([
|
||||
TrackClass.get(trackLikes.map(trackLike => trackLike.track_id), {
|
||||
user_id,
|
||||
onlyList: true,
|
||||
}),
|
||||
[],
|
||||
[],
|
||||
])
|
||||
|
||||
Tracks = Tracks.sort((a, b) => b.liked_at - a.liked_at)
|
||||
// Releases = Releases.sort((a, b) => b.liked_at - a.liked_at)
|
||||
// Playlists = Playlists.sort((a, b) => b.liked_at - a.liked_at)
|
||||
|
||||
return {
|
||||
tracks: {
|
||||
list: Tracks,
|
||||
total_items: totalTrackLikes,
|
||||
},
|
||||
releases: {
|
||||
list: Releases,
|
||||
total_items: totalReleasesLikes,
|
||||
},
|
||||
playlists: {
|
||||
list: Playlists,
|
||||
total_items: totalPlaylistsLikes,
|
||||
},
|
||||
total_length: totalTrackLikes + totalReleasesLikes + totalPlaylistsLikes,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
import { MusicRelease, Track } from "@db_models"
|
||||
import TrackClass from "@classes/track"
|
||||
|
||||
export default async (req) => {
|
||||
export default {
|
||||
middlewares: ["withOptionalAuthentication"],
|
||||
fn: async (req) => {
|
||||
const { release_id } = req.params
|
||||
const { limit = 50, offset = 0 } = req.query
|
||||
|
||||
@ -17,14 +20,15 @@ export default async (req) => {
|
||||
const totalTracks = await Track.countDocuments({
|
||||
_id: release.list
|
||||
})
|
||||
const tracks = await Track.find({
|
||||
_id: { $in: release.list }
|
||||
|
||||
const tracks = await TrackClass.get(release.list, {
|
||||
user_id: req.auth?.session?.user_id,
|
||||
onlyList: true
|
||||
})
|
||||
.limit(limit)
|
||||
.skip(offset)
|
||||
|
||||
release.listLength = totalTracks
|
||||
release.list = tracks
|
||||
|
||||
return release
|
||||
}
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
import TrackClass from "@classes/track"
|
||||
|
||||
export default {
|
||||
middlewares: ["withOptionalAuthentication"],
|
||||
fn: async (req) => {
|
||||
const { track_id } = req.params
|
||||
const user_id = req.auth?.session?.user_id
|
||||
|
||||
const track = await TrackClass.get(track_id)
|
||||
const track = await TrackClass.get(track_id, {
|
||||
user_id
|
||||
})
|
||||
|
||||
return track
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
import TrackClass from "@classes/track"
|
||||
|
||||
export default {
|
||||
middlewares: ["withAuthentication"],
|
||||
fn: async (req) => {
|
||||
const { track_id } = req.params
|
||||
const { to } = req.body
|
||||
|
||||
const track = await TrackClass.toggleFavourite(
|
||||
req.auth.session.user_id,
|
||||
track_id,
|
||||
to,
|
||||
)
|
||||
|
||||
return track
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import TrackClass from "@classes/track"
|
||||
|
||||
export default {
|
||||
middlewares: ["withAuthentication"],
|
||||
fn: async (req) => {
|
||||
const { track_id } = req.params
|
||||
|
||||
const likeStatus = await TrackClass.isFavourite(
|
||||
req.auth.session.user_id,
|
||||
track_id,
|
||||
)
|
||||
|
||||
return likeStatus
|
||||
}
|
||||
}
|
@ -126,6 +126,8 @@ export default async (payload = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
post.share_url = `${process.env.APP_URL}/post/${post._id}`
|
||||
|
||||
return {
|
||||
...post,
|
||||
user,
|
||||
|
57
packages/server/services/posts/routes/posts/trendings/get.js
Normal file
57
packages/server/services/posts/routes/posts/trendings/get.js
Normal file
@ -0,0 +1,57 @@
|
||||
import { Post } from "@db_models"
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
const maxDaysOld = 30
|
||||
|
||||
export default async (req) => {
|
||||
// fetch all posts that contain in message an #, with a maximun of 5 diferent hashtags
|
||||
let posts = await Post.find({
|
||||
message: {
|
||||
$regex: /#/gi
|
||||
},
|
||||
created_at: {
|
||||
$gte: DateTime.local().minus({ days: maxDaysOld }).toISO()
|
||||
}
|
||||
})
|
||||
.lean()
|
||||
|
||||
// get the hastag content
|
||||
posts = posts.map((post) => {
|
||||
post.hashtags = post.message.match(/#[a-zA-Z0-9_]+/gi)
|
||||
|
||||
post.hashtags = post.hashtags.map((hashtag) => {
|
||||
return hashtag.substring(1)
|
||||
})
|
||||
|
||||
return post
|
||||
})
|
||||
|
||||
// build trendings
|
||||
let trendings = posts.reduce((acc, post) => {
|
||||
post.hashtags.forEach((hashtag) => {
|
||||
if (acc.find((trending) => trending.hashtag === hashtag)) {
|
||||
acc = acc.map((trending) => {
|
||||
if (trending.hashtag === hashtag) {
|
||||
trending.count++
|
||||
}
|
||||
|
||||
return trending
|
||||
})
|
||||
} else {
|
||||
acc.push({
|
||||
hashtag,
|
||||
count: 1
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
// sort by count
|
||||
trendings = trendings.sort((a, b) => {
|
||||
return b.count - a.count
|
||||
})
|
||||
|
||||
return trendings
|
||||
}
|
@ -67,18 +67,18 @@ async function linkInternalSubmodules(packages) {
|
||||
const appPath = path.resolve(rootPath, pkgjson._web_app_path)
|
||||
|
||||
const comtyjsPath = path.resolve(rootPath, "comty.js")
|
||||
const evitePath = path.resolve(rootPath, "evite")
|
||||
const vesselPath = path.resolve(rootPath, "vessel")
|
||||
const linebridePath = path.resolve(rootPath, "linebridge")
|
||||
|
||||
//* EVITE LINKING
|
||||
console.log(`Linking Evite to app...`)
|
||||
//* APP RUNTIME LINKING
|
||||
console.log(`Linking Vessel to app...`)
|
||||
|
||||
await child_process.execSync("yarn link", {
|
||||
cwd: evitePath,
|
||||
cwd: vesselPath,
|
||||
stdio: "inherit",
|
||||
})
|
||||
|
||||
await child_process.execSync(`yarn link "evite"`, {
|
||||
await child_process.execSync(`yarn link "vessel"`, {
|
||||
cwd: appPath,
|
||||
stdio: "inherit",
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user