merge from local

This commit is contained in:
SrGooglo 2024-10-29 12:19:57 +00:00
parent 534463cfa2
commit 0db536bbea
65 changed files with 1376 additions and 757 deletions

@ -1 +1 @@
Subproject commit d2e6f1bc5856e3084d4fd068dec5d67ab2ef9d8d Subproject commit e52925b191b6e1d1415f2bb63d921ccad8c18411

View File

@ -7,9 +7,11 @@
<meta name="viewport" <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" /> 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="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:title" content="Comty" />
<meta property="og:description" content="Comty, a prototype of social network." /> <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> </head>
<body> <body>

View File

@ -13,6 +13,8 @@ export default class TrackInstance {
this.player = player this.player = player
this.manifest = manifest this.manifest = manifest
this.id = this.manifest.id ?? this.manifest._id
return this return this
} }
@ -98,6 +100,18 @@ export default class TrackInstance {
return this 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 () => { resolveManifest = async () => {
if (typeof this.manifest === "string") { if (typeof this.manifest === "string") {
this.manifest = { this.manifest = {

View File

@ -57,7 +57,6 @@ export default class TrackManifest {
// Extended from db // Extended from db
lyrics_enabled = false lyrics_enabled = false
liked = null liked = null
async initialize() { async initialize() {
@ -78,7 +77,7 @@ export default class TrackManifest {
} }
if (this.metadata.tags.picture) { 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) const coverUpload = await app.cores.remoteStorage.uploadFile(this.cover)

View File

@ -73,7 +73,7 @@ const MoreMenuHandlers = {
} }
} }
export default (props) => { const PlaylistView = (props) => {
const [playlist, setPlaylist] = React.useState(props.playlist) const [playlist, setPlaylist] = React.useState(props.playlist)
const [searchResults, setSearchResults] = React.useState(null) const [searchResults, setSearchResults] = React.useState(null)
const [owningPlaylist, setOwningPlaylist] = React.useState(checkUserIdIsSelf(props.playlist?.user_id)) const [owningPlaylist, setOwningPlaylist] = React.useState(checkUserIdIsSelf(props.playlist?.user_id))
@ -109,8 +109,27 @@ export default (props) => {
let debounceSearch = null 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 = () => { const handleOnClickPlaylistPlay = () => {
app.cores.player.start(playlist.list, 0) app.cores.player.start(playlist.list)
} }
const handleOnClickViewDetails = () => { const handleOnClickViewDetails = () => {
@ -131,7 +150,7 @@ export default (props) => {
return return
} }
// check if is currently playing // check if clicked track is currently playing
if (app.cores.player.state.track_manifest?._id === track._id) { if (app.cores.player.state.track_manifest?._id === track._id) {
app.cores.player.playback.toggle() app.cores.player.playback.toggle()
} else { } else {
@ -141,48 +160,7 @@ export default (props) => {
} }
} }
const handleTrackLike = async (track) => { const handleUpdateTrackLike = (track_id, liked) => {
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) => {
setPlaylist((prev) => { setPlaylist((prev) => {
const index = prev.list.findIndex((item) => { const index = prev.list.findIndex((item) => {
return item._id === track_id 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 handleMoreMenuClick = async (e) => {
const handler = MoreMenuHandlers[e.key] const handler = MoreMenuHandlers[e.key]
@ -213,8 +214,8 @@ export default (props) => {
} }
useWsEvents({ useWsEvents({
"music:self:track:toggle:like": (data) => { "music:track:toggle:like": (data) => {
updateTrackLike(data.track_id, data.action === "liked") handleUpdateTrackLike(data.track_id, data.action === "liked")
} }
}, { }, {
socketName: "music", socketName: "music",
@ -268,7 +269,7 @@ export default (props) => {
} }
<div className="play_info_statistics_item"> <div className="play_info_statistics_item">
<p> <p>
<Icons.MdLibraryMusic /> {props.length ?? playlist.total_length ?? playlist.list.length} Tracks <Icons.MdLibraryMusic /> {props.length ?? playlist.total_length ?? playlist.list.length} Items
</p> </p>
</div> </div>
{ {
@ -332,16 +333,29 @@ export default (props) => {
</div> </div>
<div className="list"> <div className="list">
<div className="list_header"> {
<h1> playlist.list.length > 0 && <div className="list_header">
<Icons.MdPlaylistPlay /> Tracks <h1>
</h1> <Icons.MdPlaylistPlay /> Tracks
</h1>
<SearchButton <SearchButton
onChange={handleOnSearchChange} onChange={handleOnSearchChange}
onEmpty={handleOnSearchEmpty} onEmpty={handleOnSearchEmpty}
disabled
/>
</div>
}
{
playlist.list.length === 0 && <antd.Empty
description={
<>
<Icons.MdLibraryMusic /> This playlist its empty!
</>
}
/> />
</div> }
{ {
searchResults && searchResults.map((item) => { searchResults && searchResults.map((item) => {
@ -350,21 +364,11 @@ export default (props) => {
order={item._id} order={item._id}
track={item} track={item}
onClickPlayBtn={() => handleOnClickTrack(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 !searchResults && playlist.list.length > 0 && <LoadMore
className="list_content" className="list_content"
@ -379,7 +383,7 @@ export default (props) => {
order={index + 1} order={index + 1}
track={item} track={item}
onClickPlayBtn={() => handleOnClickTrack(item)} onClickPlayBtn={() => handleOnClickTrack(item)}
onLike={() => handleTrackLike(item)} changeState={(update) => handleTrackChangeState(item._id, update)}
/> />
}) })
} }
@ -390,4 +394,6 @@ export default (props) => {
</div> </div>
</WithPlayerContext> </WithPlayerContext>
</PlaylistContext.Provider> </PlaylistContext.Provider>
} }
export default PlaylistView

View File

@ -7,6 +7,8 @@ import RGBStringToValues from "@utils/rgbToValues"
import ImageViewer from "@components/ImageViewer" import ImageViewer from "@components/ImageViewer"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
import MusicModel from "@models/music"
import { usePlayerStateContext } from "@contexts/WithPlayerContext" import { usePlayerStateContext } from "@contexts/WithPlayerContext"
import { Context as PlaylistContext } from "@contexts/WithPlaylistContext" import { Context as PlaylistContext } from "@contexts/WithPlaylistContext"
@ -14,21 +16,29 @@ import "./index.less"
const handlers = { const handlers = {
"like": async (ctx, track) => { "like": async (ctx, track) => {
app.cores.player.toggleCurrentTrackLike(true, track) await MusicModel.toggleItemFavourite("track", track._id, true)
ctx.changeState({
liked: true,
})
ctx.closeMenu() ctx.closeMenu()
}, },
"unlike": async (ctx, track) => { "unlike": async (ctx, track) => {
app.cores.player.toggleCurrentTrackLike(false, track) await MusicModel.toggleItemFavourite("track", track._id, false)
ctx.changeState({
liked: false,
})
ctx.closeMenu() ctx.closeMenu()
}, },
} }
const Track = (props) => { const Track = (props) => {
const { const [{
loading, loading,
track_manifest, track_manifest,
playback_status, playback_status,
} = usePlayerStateContext() }] = usePlayerStateContext()
const playlist_ctx = React.useContext(PlaylistContext) const playlist_ctx = React.useContext(PlaylistContext)
@ -74,7 +84,8 @@ const Track = (props) => {
{ {
closeMenu: () => { closeMenu: () => {
setMoreMenuOpened(false) setMoreMenuOpened(false)
} },
changeState: props.changeState,
}, },
props.track props.track
) )

View File

@ -35,7 +35,7 @@ const EventsHandlers = {
} }
const Controls = (props) => { const Controls = (props) => {
const playerState = usePlayerStateContext() const [playerState] = usePlayerStateContext()
const handleAction = (event, ...args) => { const handleAction = (event, ...args) => {
if (typeof EventsHandlers[event] !== "function") { if (typeof EventsHandlers[event] !== "function") {

View File

@ -6,11 +6,12 @@ import LikeButton from "@components/LikeButton"
import { usePlayerStateContext } from "@contexts/WithPlayerContext" import { usePlayerStateContext } from "@contexts/WithPlayerContext"
const ExtraActions = (props) => { import MusicModel from "@models/music"
const playerState = usePlayerStateContext()
const ExtraActions = (props) => {
const [playerState] = usePlayerStateContext()
const handleClickLike = async () => { 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"> return <div className="extra_actions">
@ -21,9 +22,10 @@ const ExtraActions = (props) => {
disabled={!playerState.track_manifest?.lyrics_enabled} disabled={!playerState.track_manifest?.lyrics_enabled}
/> />
} }
{ {
!app.isMobile && <LikeButton !app.isMobile && <LikeButton
liked={playerState.track_manifest?.liked ?? false} liked={playerState.track_manifest?.fetchLikeStatus}
onClick={handleClickLike} onClick={handleClickLike}
/> />
} }

View File

@ -43,7 +43,7 @@ const ServiceIndicator = (props) => {
} }
const Player = (props) => { const Player = (props) => {
const playerState = usePlayerStateContext() const [playerState] = usePlayerStateContext()
const contentRef = React.useRef() const contentRef = React.useRef()
const titleRef = React.useRef() const titleRef = React.useRef()

View File

@ -31,13 +31,13 @@ const SelfActionsItems = [
] ]
const MoreActionsItems = [ const MoreActionsItems = [
{ // {
key: "onClickRepost", // key: "onClickRepost",
label: <> // label: <>
<Icons.MdCallSplit /> // <Icons.MdCallSplit />
<span>Repost</span> // <span>Repost</span>
</>, // </>,
}, // },
{ {
key: "onClickShare", key: "onClickShare",
label: <> 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 PostActions = (props) => {
const [isSelf, setIsSelf] = React.useState(false) const [isSelf, setIsSelf] = React.useState(false)
@ -77,8 +90,10 @@ const PostActions = (props) => {
} }
const handleDropdownClickItem = (e) => { const handleDropdownClickItem = (e) => {
if (typeof props.actions[e.key] === "function") { const action = BuiltInActions[e.key] ?? props.actions[e.key]
props.actions[e.key]()
if (typeof action === "function") {
action(props.post)
} }
} }

View File

@ -46,6 +46,12 @@ const messageRegexs = [
return <a key={key} onClick={() => window.app.location.push(`/@${result[1].substr(1)}`)}>{result[1]}</a> 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 { export default class PostCard extends React.PureComponent {
@ -223,6 +229,7 @@ export default class PostCard extends React.PureComponent {
</div> </div>
<PostActions <PostActions
post={this.state.data}
user_id={this.state.data.user_id} user_id={this.state.data.user_id}
likesCount={this.state.countLikes} likesCount={this.state.countLikes}

View File

@ -4,7 +4,7 @@ import classnames from "classnames"
import "./index.less" import "./index.less"
export default (props) => { const SearchButton = (props) => {
const searchBoxRef = React.useRef(null) const searchBoxRef = React.useRef(null)
const [value, setValue] = React.useState() const [value, setValue] = React.useState()
@ -51,6 +51,9 @@ export default (props) => {
openSearchBox(false) openSearchBox(false)
} }
}} }}
disabled={props.disabled}
/> />
</div> </div>
} }
export default SearchButton

View 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

View 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);
}
}
}
}

View File

@ -1,20 +1,19 @@
import React from "react" import React from "react"
function deepUnproxy(obj) { function deepUnproxy(obj) {
// Verificar si es un array y hacer una copia en consecuencia
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
obj = [...obj]; obj = [...obj]
} else { } else {
obj = Object.assign({}, obj); obj = Object.assign({}, obj)
} }
for (let key in obj) { for (let key in obj) {
if (obj[key] && typeof obj[key] === "object") { 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) => { export const usePlayerStateContext = (updater) => {
@ -40,7 +39,7 @@ export const usePlayerStateContext = (updater) => {
} }
}, []) }, [])
return state return [state, setState]
} }
export const Context = React.createContext({}) export const Context = React.createContext({})

View File

@ -53,6 +53,13 @@ export default class APICore extends Core {
enableWs: true, 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", () => { this.client.eventBus.on("auth:login_success", () => {
app.eventBus.emit("auth:login_success") app.eventBus.emit("auth:login_success")
}) })

View File

@ -43,7 +43,7 @@ export default class PlayerProcessors {
Object.entries(processor.exposeToPublic).forEach(([key, value]) => { Object.entries(processor.exposeToPublic).forEach(([key, value]) => {
const refName = processor.constructor.refName const refName = processor.constructor.refName
if (typeof this.public[refName] === "undefined") { if (typeof this.player.public[refName] === "undefined") {
// by default create a empty object // by default create a empty object
this.player.public[refName] = {} this.player.public[refName] = {}
} }
@ -55,8 +55,6 @@ export default class PlayerProcessors {
} }
async attachProcessorsToInstance(instance) { async attachProcessorsToInstance(instance) {
this.player.console.log(instance, this.processors)
for await (const [index, processor] of this.processors.entries()) { for await (const [index, processor] of this.processors.entries()) {
if (processor.constructor.node_bypass === true) { if (processor.constructor.node_bypass === true) {
instance.contextElement.connect(processor.processor) instance.contextElement.connect(processor.processor)

View File

@ -10,7 +10,6 @@ export default class PlayerUI {
// //
// UI Methods // UI Methods
// //
attachPlayerComponent() { attachPlayerComponent() {
if (this.currentDomWindow) { if (this.currentDomWindow) {
this.player.console.warn("EmbbededMediaPlayer already attached") this.player.console.warn("EmbbededMediaPlayer already attached")
@ -18,7 +17,9 @@ export default class PlayerUI {
} }
if (app.layout.tools_bar) { 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",
})
} }
} }

View File

@ -4,6 +4,7 @@ export default class Presets {
constructor({ constructor({
storage_key, storage_key,
defaultPresetValue, defaultPresetValue,
onApplyValues,
}) { }) {
if (!storage_key) { if (!storage_key) {
throw new Error("storage_key is required") throw new Error("storage_key is required")
@ -11,6 +12,7 @@ export default class Presets {
this.storage_key = storage_key this.storage_key = storage_key
this.defaultPresetValue = defaultPresetValue this.defaultPresetValue = defaultPresetValue
this.onApplyValues = onApplyValues
return this return this
} }
@ -38,14 +40,25 @@ export default class Presets {
} }
get currentPresetValues() { get currentPresetValues() {
const presets = this.presets if (!this.presets || !this.presets[this.currentPresetKey]) {
const key = this.currentPresetKey
if (!presets || !presets[key]) {
return this.defaultPresetValue 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) { deletePreset(key) {
@ -54,64 +67,74 @@ export default class Presets {
return false return false
} }
// if current preset is deleted, change to default
if (this.currentPresetKey === key) { if (this.currentPresetKey === key) {
this.changePreset("default") 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) { createPreset(key, values) {
let presets = this.presets if (this.presets[key]) {
if (presets[key]) {
app.message.error("Preset already exists") app.message.error("Preset already exists")
return false 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) { changePreset(key) {
let presets = this.presets
// create new one // create new one
if (!presets[key]) { if (!this.presets[key]) {
presets[key] = this.defaultPresetValue this.presets[key] = this.defaultPresetValue
this.presets = presets
} }
this.currentPresetKey = key this.currentPresetKey = key
return presets[key] this.applyValues()
return this.presets[key]
} }
setToCurrent(values) { setToCurrent(values) {
let preset = this.currentPresetValues this.currentPresetValues = {
...this.currentPresetValues,
preset = {
...preset,
...values, ...values,
} }
// update presets this.applyValues()
let presets = this.presets
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)
}
})
})
} }
} }

View 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
}
}

View File

@ -6,6 +6,7 @@ import ServiceProviders from "./classes/Services"
import PlayerState from "./classes/PlayerState" import PlayerState from "./classes/PlayerState"
import PlayerUI from "./classes/PlayerUI" import PlayerUI from "./classes/PlayerUI"
import PlayerProcessors from "./classes/PlayerProcessors" import PlayerProcessors from "./classes/PlayerProcessors"
import QueueManager from "./classes/QueueManager"
import setSampleRate from "./helpers/setSampleRate" import setSampleRate from "./helpers/setSampleRate"
@ -28,8 +29,8 @@ export default class Player extends Core {
state = new PlayerState(this) state = new PlayerState(this)
ui = new PlayerUI(this) ui = new PlayerUI(this)
service_providers = new ServiceProviders() serviceProviders = new ServiceProviders()
native_controls = new MediaSession() nativeControls = new MediaSession()
audioContext = new AudioContext({ audioContext = new AudioContext({
sampleRate: AudioPlayerStorage.get("sample_rate") ?? Player.defaultSampleRate, sampleRate: AudioPlayerStorage.get("sample_rate") ?? Player.defaultSampleRate,
latencyHint: "playback" latencyHint: "playback"
@ -37,9 +38,11 @@ export default class Player extends Core {
audioProcessors = new PlayerProcessors(this) audioProcessors = new PlayerProcessors(this)
track_prev_instances = [] queue = new QueueManager({
track_instance = null loadFunction: this.createInstance
track_next_instances = [] })
currentTrackInstance = null
public = { public = {
start: this.start, start: this.start,
@ -61,7 +64,7 @@ export default class Player extends Core {
setSampleRate: setSampleRate, setSampleRate: setSampleRate,
}), }),
track: () => { track: () => {
return this.track_instance return this.queue.currentItem
}, },
eventBus: () => { eventBus: () => {
return this.eventBus return this.eventBus
@ -77,7 +80,7 @@ export default class Player extends Core {
this.state.volume = 1 this.state.volume = 1
} }
await this.native_controls.initialize() await this.nativeControls.initialize()
await this.audioProcessors.initialize() await this.audioProcessors.initialize()
} }
@ -85,130 +88,75 @@ export default class Player extends Core {
// Instance managing methods // Instance managing methods
// //
async abortPreloads() { async abortPreloads() {
for await (const instance of this.track_next_instances) { for await (const instance of this.queue.nextItems) {
if (instance.abortController?.abort) { if (instance.abortController?.abort) {
instance.abortController.abort() instance.abortController.abort()
} }
} }
} }
async preloadAudioInstance(instance) { async createInstance(manifest) {
const isIndex = typeof instance === "number" return new TrackInstance(this, manifest)
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
} }
// //
// Playback methods // Playback methods
// //
async play(instance, params = {}) { 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) { if (!instance) {
throw new Error("Audio instance is required") throw new Error("Audio instance is required")
} }
// resume audio context if needed
if (this.audioContext.state === "suspended") { if (this.audioContext.state === "suspended") {
this.audioContext.resume() 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 // initialize instance if is not
if (this.track_instance._initialized === false) { if (this.queue.currentItem._initialized === false) {
this.track_instance = await instance.initialize() this.queue.currentItem = await instance.initialize()
} }
// update manifest // update manifest
this.state.track_manifest = this.track_instance.manifest this.state.track_manifest = this.queue.currentItem.manifest
// attach processors // 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 // reconstruct audio src if is not set
if (this.track_instance.audio.src !== this.track_instance.manifest.source) { if (this.queue.currentItem.audio.src !== this.queue.currentItem.manifest.source) {
this.track_instance.audio.src = this.track_instance.manifest.source this.queue.currentItem.audio.src = this.queue.currentItem.manifest.source
} }
// set time to provided time, if not, set to 0 // set audio properties
this.track_instance.audio.currentTime = params.time ?? 0 this.queue.currentItem.audio.currentTime = params.time ?? 0
this.queue.currentItem.audio.muted = this.state.muted
this.track_instance.audio.muted = this.state.muted this.queue.currentItem.audio.loop = this.state.playback_mode === "repeat"
this.track_instance.audio.loop = this.state.playback_mode === "repeat" this.queue.currentItem.gainNode.gain.value = this.state.volume
this.track_instance.gainNode.gain.value = this.state.volume
// play // 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 // 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 } = {}) { async start(manifest, { time, startIndex = 0 } = {}) {
this.ui.attachPlayerComponent() this.ui.attachPlayerComponent()
// !IMPORTANT: abort preloads before destroying current instance if (this.queue.currentItem) {
await this.queue.currentItem.stop()
}
await this.abortPreloads() await this.abortPreloads()
await this.destroyCurrentInstance() await this.queue.flush()
this.state.loading = true this.state.loading = true
this.track_prev_instances = []
this.track_next_instances = []
let playlist = Array.isArray(manifest) ? manifest : [manifest] let playlist = Array.isArray(manifest) ? manifest : [manifest]
if (playlist.length === 0) { if (playlist.length === 0) {
@ -217,60 +165,47 @@ export default class Player extends Core {
} }
if (playlist.some((item) => typeof item === "string")) { 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()) { this.queue.add(instance)
let instance = new TrackInstance(this, _manifest)
this.track_next_instances.push(instance)
if (index === 0) {
this.play(this.track_next_instances[0], {
time: time ?? 0
})
}
} }
const item = this.queue.set(startIndex)
this.play(item, {
time: time ?? 0
})
return manifest return manifest
} }
next() { next() {
if (this.track_next_instances.length > 0) { if (this.queue.currentItem) {
// move current audio instance to history this.queue.currentItem.stop()
this.track_prev_instances.push(this.track_next_instances.shift())
} }
if (this.track_next_instances.length === 0) { //const isRandom = this.state.playback_mode === "shuffle"
this.console.log(`No more tracks to play, stopping...`) const item = this.queue.next()
if (!item) {
return this.stopPlayback() return this.stopPlayback()
} }
let nextIndex = 0 return this.play(item)
if (this.state.playback_mode === "shuffle") {
nextIndex = Math.floor(Math.random() * this.track_next_instances.length)
}
this.play(this.track_next_instances[nextIndex])
} }
previous() { previous() {
if (this.track_prev_instances.length > 0) { if (this.queue.currentItem) {
// move current audio instance to history this.queue.currentItem.stop()
this.track_next_instances.unshift(this.track_prev_instances.pop())
return this.play(this.track_next_instances[0])
} }
if (this.track_prev_instances.length === 0) { const item = this.queue.previous()
this.console.log(`[PLAYER] No previous tracks, replying...`)
// replay the current track return this.play(item)
return this.play(this.track_instance)
}
} }
// //
@ -290,23 +225,23 @@ export default class Player extends Core {
} }
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
if (!this.track_instance) { if (!this.queue.currentItem) {
this.console.error("No audio instance") this.console.error("No audio instance")
return null return null
} }
// set gain exponentially // set gain exponentially
this.track_instance.gainNode.gain.linearRampToValueAtTime( this.queue.currentItem.gainNode.gain.linearRampToValueAtTime(
0.0001, 0.0001,
this.audioContext.currentTime + (Player.gradualFadeMs / 1000) this.audioContext.currentTime + (Player.gradualFadeMs / 1000)
) )
setTimeout(() => { setTimeout(() => {
this.track_instance.audio.pause() this.queue.currentItem.audio.pause()
resolve() resolve()
}, Player.gradualFadeMs) }, 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) => { return await new Promise((resolve, reject) => {
if (!this.track_instance) { if (!this.queue.currentItem) {
this.console.error("No audio instance") this.console.error("No audio instance")
return null return null
} }
// ensure audio elemeto starts from 0 volume // 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() resolve()
}) })
// set gain exponentially // set gain exponentially
this.track_instance.gainNode.gain.linearRampToValueAtTime( this.queue.currentItem.gainNode.gain.linearRampToValueAtTime(
this.state.volume, this.state.volume,
this.audioContext.currentTime + (Player.gradualFadeMs / 1000) 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 this.state.playback_mode = mode
if (this.track_instance) { if (this.queue.currentItem) {
this.track_instance.audio.loop = this.state.playback_mode === "repeat" this.queue.currentItem.audio.loop = this.state.playback_mode === "repeat"
} }
AudioPlayerStorage.set("mode", mode) AudioPlayerStorage.set("mode", mode)
@ -355,17 +290,22 @@ export default class Player extends Core {
} }
async stopPlayback() { async stopPlayback() {
this.destroyCurrentInstance() if (this.queue.currentItem) {
this.queue.currentItem.stop()
}
this.queue.flush()
this.abortPreloads() this.abortPreloads()
this.state.playback_status = "stopped" this.state.playback_status = "stopped"
this.state.track_manifest = null this.state.track_manifest = null
this.track_instance = null this.queue.currentItem = null
this.track_next_instances = [] this.track_next_instances = []
this.track_prev_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") { if (typeof to === "boolean") {
this.state.muted = to this.state.muted = to
this.track_instance.audio.muted = to this.queue.currentItem.audio.muted = to
} }
return this.state.muted return this.state.muted
@ -413,9 +353,9 @@ export default class Player extends Core {
AudioPlayerStorage.set("volume", volume) AudioPlayerStorage.set("volume", volume)
if (this.track_instance) { if (this.queue.currentItem) {
if (this.track_instance.gainNode) { if (this.queue.currentItem.gainNode) {
this.track_instance.gainNode.gain.value = this.state.volume this.queue.currentItem.gainNode.gain.value = this.state.volume
} }
} }
@ -423,31 +363,31 @@ export default class Player extends Core {
} }
seek(time) { seek(time) {
if (!this.track_instance || !this.track_instance.audio) { if (!this.queue.currentItem || !this.queue.currentItem.audio) {
return false return false
} }
// if time not provided, return current time // if time not provided, return current time
if (typeof time === "undefined") { 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 time is provided, seek to that time
if (typeof time === "number") { 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 return time
} }
} }
duration() { duration() {
if (!this.track_instance || !this.track_instance.audio) { if (!this.queue.currentItem || !this.queue.currentItem.audio) {
return false return false
} }
return this.track_instance.audio.duration return this.queue.currentItem.audio.duration
} }
loop(to) { loop(to) {
@ -458,8 +398,8 @@ export default class Player extends Core {
this.state.loop = to ?? !this.state.loop this.state.loop = to ?? !this.state.loop
if (this.track_instance.audio) { if (this.queue.currentItem.audio) {
this.track_instance.audio.loop = this.state.loop this.queue.currentItem.audio.loop = this.state.loop
} }
return this.state.loop return this.state.loop

View File

@ -1,4 +1,3 @@
import { Modal } from "antd"
import ProcessorNode from "../node" import ProcessorNode from "../node"
import Presets from "../../classes/Presets" import Presets from "../../classes/Presets"
@ -6,7 +5,7 @@ export default class CompressorProcessorNode extends ProcessorNode {
constructor(props) { constructor(props) {
super(props) super(props)
this.presets_controller = new Presets({ this.presets = new Presets({
storage_key: "compressor", storage_key: "compressor",
defaultPresetValue: { defaultPresetValue: {
threshold: -50, threshold: -50,
@ -15,98 +14,19 @@ export default class CompressorProcessorNode extends ProcessorNode {
attack: 0.003, attack: 0.003,
release: 0.25, release: 0.25,
}, },
onApplyValues: this.applyValues.bind(this),
}) })
this.state = {
compressorValues: this.presets_controller.currentPresetValues,
}
this.exposeToPublic = { this.exposeToPublic = {
presets: new Proxy(this.presets_controller, { presets: this.presets,
get: function (target, key) { detach: this._detach,
if (!key) { attach: this._attach,
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,
} }
} }
static refName = "compressor" static refName = "compressor"
static dependsOnSettings = ["player.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) { async init(AudioContext) {
if (!AudioContext) { if (!AudioContext) {
throw new Error("AudioContext is required") throw new Error("AudioContext is required")
@ -118,8 +38,8 @@ export default class CompressorProcessorNode extends ProcessorNode {
} }
applyValues() { applyValues() {
Object.keys(this.state.compressorValues).forEach((key) => { Object.keys(this.presets.currentPresetValues).forEach((key) => {
this.processor[key].value = this.state.compressorValues[key] this.processor[key].value = this.presets.currentPresetValues[key]
}) })
} }
} }

View File

@ -1,4 +1,3 @@
import { Modal } from "antd"
import ProcessorNode from "../node" import ProcessorNode from "../node"
import Presets from "../../classes/Presets" import Presets from "../../classes/Presets"
@ -6,7 +5,7 @@ export default class EqProcessorNode extends ProcessorNode {
constructor(props) { constructor(props) {
super(props) super(props)
this.presets_controller = new Presets({ this.presets = new Presets({
storage_key: "eq", storage_key: "eq",
defaultPresetValue: { defaultPresetValue: {
32: 0, 32: 0,
@ -20,94 +19,21 @@ export default class EqProcessorNode extends ProcessorNode {
8000: 0, 8000: 0,
16000: 0, 16000: 0,
}, },
onApplyValues: this.applyValues.bind(this),
}) })
this.state = {
eqValues: this.presets_controller.currentPresetValues,
}
this.exposeToPublic = { this.exposeToPublic = {
presets: new Proxy(this.presets_controller, { presets: this.presets,
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),
} }
} }
static refName = "eq" static refName = "eq"
static lock = true 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() { applyValues() {
// apply to current instance // apply to current instance
this.processor.eqNodes.forEach((processor) => { 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) { if (processor.gain.value !== gainValue) {
console.debug(`[EQ] Applying values to ${processor.frequency.value} Hz frequency with gain ${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 = [] this.processor.eqNodes = []
const values = Object.entries(this.state.eqValues).map((entry) => { const values = Object.entries(this.presets.currentPresetValues).map((entry) => {
return { return {
freq: parseFloat(entry[0]), freq: parseFloat(entry[0]),
gain: parseFloat(entry[1]), gain: parseFloat(entry[1]),

View File

@ -8,7 +8,23 @@ export default class RemoteStorage extends Core {
static depends = ["api", "tasksQueue"] static depends = ["api", "tasksQueue"]
public = { 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) { async getFileHash(file) {

View File

@ -3,7 +3,9 @@ import React from "react"
const usePageWidgets = (widgets = []) => { const usePageWidgets = (widgets = []) => {
React.useEffect(() => { React.useEffect(() => {
for (const widget of widgets) { 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 () => { return () => {

View File

@ -1,8 +1,6 @@
import React from "react" import React from "react"
import classnames from "classnames" import classnames from "classnames"
import { Motion, spring } from "react-motion" import { Motion, spring } from "react-motion"
import { Translation } from "react-i18next"
import { Icons } from "@components/Icons"
import WidgetsWrapper from "@components/WidgetsWrapper" import WidgetsWrapper from "@components/WidgetsWrapper"
@ -11,7 +9,10 @@ import "./index.less"
export default class ToolsBar extends React.Component { export default class ToolsBar extends React.Component {
state = { state = {
visible: false, visible: false,
renders: [], renders: {
top: [],
bottom: [],
},
} }
componentDidMount() { componentDidMount() {
@ -34,20 +35,25 @@ export default class ToolsBar extends React.Component {
visible: to ?? !this.state.visible, visible: to ?? !this.state.visible,
}) })
}, },
attachRender: (id, component, props) => { attachRender: (id, component, props, { position = "bottom" } = {}) => {
this.setState({ this.setState((prev) => {
renders: [...this.state.renders, { prev.renders[position].push({
id: id, id: id,
component: component, component: component,
props: props, props: props,
}], })
return prev
}) })
return component return component
}, },
detachRender: (id) => { detachRender: (id) => {
this.setState({ 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 return true
@ -78,15 +84,29 @@ export default class ToolsBar extends React.Component {
id="tools_bar" id="tools_bar"
className="tools-bar" className="tools-bar"
> >
<div className="attached_renders"> <div className="attached_renders top">
{ {
this.state.renders.map((render) => { this.state.renders.top.map((render, index) => {
return React.createElement(render.component, render.props) return React.createElement(render.component, {
...render.props,
key: index,
})
}) })
} }
</div> </div>
<WidgetsWrapper /> <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>
</div> </div>
}} }}

View File

@ -7,7 +7,7 @@
right: 0; right: 0;
z-index: 150; z-index: 150;
height: 100vh; height: 100vh;
height: 100dvh; height: 100dvh;
max-width: 420px; max-width: 420px;
@ -26,6 +26,8 @@
} }
.tools-bar { .tools-bar {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -46,14 +48,32 @@
flex: 0; flex: 0;
.attached_renders { .attached_renders {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 100%; width: 100%;
height: 100%; height: fit-content;
gap: 10px; gap: 10px;
&.bottom {
position: absolute;
bottom: 0;
left: 0;
padding: 10px;
}
.card {
width: 100%;
height: fit-content;
background-color: var(--background-color-primary);
}
} }
} }

View File

@ -29,7 +29,7 @@ const ServiceIndicator = (props) => {
} }
const AudioPlayer = (props) => { const AudioPlayer = (props) => {
const playerState = usePlayerStateContext() const [playerState] = usePlayerStateContext()
React.useEffect(() => { React.useEffect(() => {
if (app.currentDragger) { if (app.currentDragger) {

View 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

View File

@ -47,7 +47,7 @@ const RenderAlbum = (props) => {
} }
const PlayerController = React.forwardRef((props, ref) => { const PlayerController = React.forwardRef((props, ref) => {
const playerState = usePlayerStateContext() const [playerState] = usePlayerStateContext()
const titleRef = React.useRef() const titleRef = React.useRef()

View File

@ -5,7 +5,7 @@ import { Motion, spring } from "react-motion"
import { usePlayerStateContext } from "@contexts/WithPlayerContext" import { usePlayerStateContext } from "@contexts/WithPlayerContext"
const LyricsText = React.forwardRef((props, textRef) => { const LyricsText = React.forwardRef((props, textRef) => {
const playerState = usePlayerStateContext() const [playerState] = usePlayerStateContext()
const { lyrics } = props const { lyrics } = props
@ -55,7 +55,7 @@ const LyricsText = React.forwardRef((props, textRef) => {
setVisible(false) setVisible(false)
} else { } else {
setVisible(true) setVisible(true)
console.log(`Scrolling to line ${currentLineIndex}`)
// find line element by id // find line element by id
const lineElement = textRef.current.querySelector(`#lyrics-line-${currentLineIndex}`) const lineElement = textRef.current.querySelector(`#lyrics-line-${currentLineIndex}`)

View File

@ -7,7 +7,7 @@ import { usePlayerStateContext } from "@contexts/WithPlayerContext"
const maxLatencyInMs = 55 const maxLatencyInMs = 55
const LyricsVideo = React.forwardRef((props, videoRef) => { const LyricsVideo = React.forwardRef((props, videoRef) => {
const playerState = usePlayerStateContext() const [playerState] = usePlayerStateContext()
const { lyrics } = props const { lyrics } = props
@ -57,12 +57,10 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
setCurrentVideoLatency(currentVideoTimeDiff) setCurrentVideoLatency(currentVideoTimeDiff)
if (syncingVideo === true) { if (syncingVideo === true) {
console.log(`Syncing video...`)
return false return false
} }
if (currentVideoTimeDiff > maxOffset) { if (currentVideoTimeDiff > maxOffset) {
console.warn(`Video offset exceeds`, maxOffset)
seekVideoToSyncAudio() seekVideoToSyncAudio()
} }
} }

View File

@ -22,8 +22,8 @@ function getDominantColorStr(track_manifest) {
return `${values[0]}, ${values[1]}, ${values[2]}` return `${values[0]}, ${values[1]}, ${values[2]}`
} }
const EnchancedLyrics = (props) => { const EnchancedLyricsPage = () => {
const playerState = usePlayerStateContext() const [playerState] = usePlayerStateContext()
const [initialized, setInitialized] = React.useState(false) const [initialized, setInitialized] = React.useState(false)
const [lyrics, setLyrics] = React.useState(null) const [lyrics, setLyrics] = React.useState(null)
@ -71,11 +71,6 @@ const EnchancedLyrics = (props) => {
} }
}, [playerState.track_manifest]) }, [playerState.track_manifest])
//* Handle when lyrics data change
React.useEffect(() => {
console.log(lyrics)
}, [lyrics])
React.useEffect(() => { React.useEffect(() => {
setInitialized(true) setInitialized(true)
}, []) }, [])
@ -129,4 +124,4 @@ const EnchancedLyrics = (props) => {
</div> </div>
} }
export default EnchancedLyrics export default EnchancedLyricsPage

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -1,175 +1,27 @@
import React from "react" import React from "react"
import * as antd from "antd"
import classnames from "classnames" import classnames from "classnames"
import { Translation } from "react-i18next"
import Image from "@components/Image"
import Searcher from "@components/Searcher" import Searcher from "@components/Searcher"
import { Icons, createIconRender } from "@components/Icons" import { Icons } from "@components/Icons"
import MusicTrack from "@components/Music/Track"
import PlaylistItem from "@components/Music/PlaylistItem"
import ReleasesList from "@components/ReleasesList"
import FeedModel from "@models/feed" import FeedModel from "@models/feed"
import MusicModel from "@models/music" 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" import "./index.less"
const FeaturedPlaylist = (props) => { const MusicExploreTab = (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 [searchResults, setSearchResults] = React.useState(false) const [searchResults, setSearchResults] = React.useState(false)
React.useEffect(() => { React.useEffect(() => {
app.layout.toggleCenteredContent(true) app.layout.toggleCenteredContent(true)
app.layout.page_panels.attachComponent("music_navbar", MusicNavbar, { app.layout.page_panels.attachComponent("music_navbar", Navbar, {
props: { props: {
setSearchResults: setSearchResults, setSearchResults: setSearchResults,
} }
@ -192,9 +44,6 @@ export default (props) => {
useUrlQuery useUrlQuery
renderResults={false} renderResults={false}
model={MusicModel.search} model={MusicModel.search}
modelParams={{
useTidal: app.cores.sync.getActiveLinkedServices().tidal,
}}
onSearchResult={setSearchResults} onSearchResult={setSearchResults}
onEmpty={() => setSearchResults(false)} onEmpty={() => setSearchResults(false)}
/> />
@ -210,6 +59,8 @@ export default (props) => {
!searchResults && <div className="feed_main"> !searchResults && <div className="feed_main">
<FeaturedPlaylist /> <FeaturedPlaylist />
<RecentlyPlayedList />
<ReleasesList <ReleasesList
headerTitle="From your following artists" headerTitle="From your following artists"
headerIcon={<Icons.MdPerson />} headerIcon={<Icons.MdPerson />}
@ -224,4 +75,6 @@ export default (props) => {
</div> </div>
} }
</div> </div>
} }
export default MusicExploreTab

View File

@ -49,47 +49,62 @@ export default class FavoriteTracks extends React.Component {
loading: true, loading: true,
}) })
const result = await MusicModel.getFavoriteTracks({ const result = await MusicModel.getFavouriteFolder({
useTidal: app.cores.sync.getActiveLinkedServices().tidal,
offset: offset, offset: offset,
limit: limit, limit: limit,
}).catch((err) => { }).catch((error) => {
this.setState({ this.setState({
error: err.message, error: error.message,
}) })
return false
return null
}) })
console.log("Loaded favorites => ", result) console.log("Loaded favorites => ", result)
if (result) { if (result) {
const { tracks, total_length } = result const {
tracks,
releases,
playlists,
total_length,
} = result
this.setState({ const data = [
total_length ...tracks.list,
}) ...releases.list,
...playlists.list,
]
if (tracks.length === 0) { if (total_length === 0) {
if (offset === 0) { this.setState({
this.setState({ empty: true,
empty: true,
})
}
return this.setState({
hasMore: false, hasMore: false,
initialLoading: false,
})
}
if (data.length === 0) {
return this.setState({
empty: false,
hasMore: false,
initialLoading: false,
}) })
} }
if (replace) { if (replace) {
this.setState({ this.setState({
list: tracks, list: data,
}) })
} else { } else {
this.setState({ this.setState({
list: [...this.state.list, ...tracks], list: [...this.state.list, ...data],
}) })
} }
this.setState({
total_length
})
} }
this.setState({ this.setState({
@ -112,7 +127,7 @@ export default class FavoriteTracks extends React.Component {
} }
return <PlaylistView return <PlaylistView
favorite favorite
type="vertical" type="vertical"
playlist={{ playlist={{
title: "Your favorites", title: "Your favorites",
@ -122,7 +137,6 @@ export default class FavoriteTracks extends React.Component {
centered={app.isMobile} centered={app.isMobile}
onLoadMore={this.onLoadMore} onLoadMore={this.onLoadMore}
hasMore={this.state.hasMore} hasMore={this.state.hasMore}
empty={this.state.empty}
length={this.state.total_length} length={this.state.total_length}
/> />
} }

View File

@ -2,23 +2,12 @@ import React from "react"
import { Translation } from "react-i18next" import { Translation } from "react-i18next"
import { PagePanelWithNavMenu } from "@components/PagePanels" import { PagePanelWithNavMenu } from "@components/PagePanels"
import TrendingsCard from "@components/TrendingsCard"
import usePageWidgets from "@hooks/usePageWidgets" import usePageWidgets from "@hooks/usePageWidgets"
import Tabs from "./tabs" 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 = () => { const TimelinePage = () => {
usePageWidgets([ usePageWidgets([
{ {

View File

@ -3,7 +3,7 @@ import * as antd from "antd"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
import UserModel from "@models/user" import AuthModel from "@models/auth"
import "./index.less" import "./index.less"
@ -32,7 +32,7 @@ const ChangePasswordComponent = (props) => {
setError(null) setError(null)
setLoading(true) setLoading(true)
const result = await UserModel.changePassword({ currentPassword, newPassword }).catch((err) => { const result = await AuthModel.changePassword({ currentPassword, newPassword }).catch((err) => {
console.error(err) console.error(err)
setError(err.response.data.message) setError(err.response.data.message)
return null return null

View File

@ -6,14 +6,23 @@ import { Icons } from "@components/Icons"
import Sliders from "../sliderValues" import Sliders from "../sliderValues"
export default (props) => { export default (props) => {
const [selectedPreset, setSelectedPreset] = React.useState(props.controller.presets.currentPresetKey) const [selectedPreset, setSelectedPreset] = React.useState(props.controller.currentPresetKey)
const [presets, setPresets] = React.useState(props.controller.presets.presets ?? {}) const [presets, setPresets] = React.useState(props.controller.presets ?? {})
const createPreset = (key) => { const createPreset = (key) => {
setPresets(props.controller.createPreset(key)) const presets = props.controller.createPreset(key)
setPresets(presets)
setSelectedPreset(key) setSelectedPreset(key)
} }
const deletePreset = (key) => {
const presets = props.controller.deletePreset(key)
setPresets(presets)
setSelectedPreset(props.controller.currentPresetKey)
}
const handleCreateNewPreset = () => { const handleCreateNewPreset = () => {
app.layout.modal.open("create_preset", (props) => { app.layout.modal.open("create_preset", (props) => {
const [presetKey, setPresetKey] = React.useState("") const [presetKey, setPresetKey] = React.useState("")
@ -51,15 +60,11 @@ export default (props) => {
}) })
} }
const handleDeletePreset = () => { const handleDeleteCurrentPreset = () => {
Modal.confirm({ Modal.confirm({
title: "Delete preset", title: "Delete preset",
content: "Are you sure you want to delete this preset?", content: "Are you sure you want to delete this preset?",
onOk: () => { onOk: () => deletePreset(selectedPreset)
props.controller.deletePreset(selectedPreset)
setPresets(props.controller.presets.presets ?? {})
setSelectedPreset(props.controller.presets.currentPresetKey)
}
}) })
} }
@ -77,14 +82,13 @@ export default (props) => {
] ]
React.useEffect(() => { React.useEffect(() => {
const presets = props.controller.presets.presets ?? {} const presetValues = props.controller.presets[selectedPreset]
const preset = presets[selectedPreset]
if (props.controller.presets.currentPresetKey !== selectedPreset) { if (props.controller.currentPresetKey !== selectedPreset) {
props.controller.changePreset(selectedPreset) props.controller.changePreset(selectedPreset)
} }
props.ctx.updateCurrentValue(preset) props.ctx.updateCurrentValue(presetValues)
}, [selectedPreset]) }, [selectedPreset])
return <> return <>
@ -152,7 +156,7 @@ export default (props) => {
}} }}
/> />
<Button <Button
onClick={handleDeletePreset} onClick={handleDeleteCurrentPreset}
icon={<Icons.MdDelete />} icon={<Icons.MdDelete />}
disabled={selectedPreset === "default"} disabled={selectedPreset === "default"}
/> />

View File

@ -64,7 +64,7 @@ export default {
return app.cores.player.audioContext.sampleRate return app.cores.player.audioContext.sampleRate
}, },
onUpdate: async (value) => { onUpdate: async (value) => {
const sampleRate = await app.cores.player.setSampleRate(value) const sampleRate = await app.cores.player.controls.setSampleRate(value)
return sampleRate return sampleRate
}, },
@ -93,6 +93,18 @@ export default {
app.cores.player.compressor.detach() 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: { props: {
valueFormat: (value) => `${value}dB`, valueFormat: (value) => `${value}dB`,
sliders: [ 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) => { onUpdate: (value) => {
app.cores.player.compressor.modifyValues(value) app.cores.player.compressor.presets.setToCurrent(value)
return value return value
}, },
@ -164,15 +164,15 @@ export default {
title: "Reset", title: "Reset",
icon: "MdRefresh", icon: "MdRefresh",
onClick: (ctx) => { onClick: (ctx) => {
const values = app.cores.player.eq.resetDefaultValues() const values = app.cores.player.eq.presets.setCurrentPresetToDefault()
ctx.updateCurrentValue(values) ctx.updateCurrentValue(values)
} }
}, },
], ],
dependsOn: { // dependsOn: {
"player.equalizer": true // "player.equalizer": true
}, // },
props: { props: {
valueFormat: (value) => `${value}dB`, valueFormat: (value) => `${value}dB`,
marks: [ marks: [
@ -251,7 +251,7 @@ export default {
return acc return acc
}, {}) }, {})
app.cores.player.eq.modifyValues(values) app.cores.player.eq.presets.setToCurrent(values)
return value return value
}, },

View File

@ -4,7 +4,7 @@ import SlidersWithPresets from "../../../components/slidersWithPresets"
export default (props) => { export default (props) => {
return <SlidersWithPresets return <SlidersWithPresets
{...props} {...props}
controller={app.cores.player.compressor} controller={app.cores.player.compressor.presets}
extraHeaderItems={[ extraHeaderItems={[
<Switch <Switch
onChange={props.onEnabledChange} onChange={props.onEnabledChange}

View File

@ -3,6 +3,6 @@ import SlidersWithPresets from "../../../components/slidersWithPresets"
export default (props) => { export default (props) => {
return <SlidersWithPresets return <SlidersWithPresets
{...props} {...props}
controller={app.cores.player.eq} controller={app.cores.player.eq.presets}
/> />
} }

View File

@ -26,6 +26,18 @@ export default defineConfig({
headers: { headers: {
"Strict-Transport-Security": `max-age=${oneYearInSeconds}` "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: { css: {
preprocessorOptions: { preprocessorOptions: {
@ -42,7 +54,7 @@ export default defineConfig({
build: { build: {
target: "esnext", target: "esnext",
rollupOptions: { rollupOptions: {
output:{ output: {
manualChunks(id) { manualChunks(id) {
if (id.includes('node_modules')) { if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString(); return id.toString().split('node_modules/')[1].split('/')[0].toString();

View File

@ -160,13 +160,11 @@ export default class Proxy {
} }
if (sanitizedUrl === "/") { if (sanitizedUrl === "/") {
return res.end(` return res.end(JSON.stringify({
{ name: pkg.name,
"name": "${pkg.name}", version: pkg.version,
"version": "${pkg.version}", lb_version: defaults.version
"lb_version": "${defaults.version}" }))
}
`)
} }
const namespace = `/${sanitizedUrl.split("/")[1]}` const namespace = `/${sanitizedUrl.split("/")[1]}`

View File

@ -2,4 +2,6 @@ export default class Track {
static create = require("./methods/create").default static create = require("./methods/create").default
static delete = require("./methods/delete").default static delete = require("./methods/delete").default
static get = require("./methods/get").default static get = require("./methods/get").default
static toggleFavourite = require("./methods/toggleFavourite").default
static isFavourite = require("./methods/isFavourite").default
} }

View File

@ -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) { if (!track_id) {
throw new OperationError(400, "Missing 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) { 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 } }) const tracks = await Track.find({
.limit(limit) _id: { $in: track_ids }
.skip(offset) }).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 { return {
total_count: await Track.countDocuments({ _id: { $in: track_ids } }), total_count: await Track.countDocuments({ _id: { $in: track_ids } }),
list: tracks.map(track => track.toObject()), list: tracks,
} }
} }
const track = await Track.findOne({ const track = await Track.findOne({
_id: track_id _id: track_id
}) }).lean()
if (!track) { if (!track) {
throw new OperationError(404, "Track not found") 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 return track
} }

View File

@ -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
}
}

View File

@ -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(),
}
}

View File

@ -7,6 +7,7 @@ import LimitsClass from "@shared-classes/Limits"
export default class API extends Server { export default class API extends Server {
static refName = "music" static refName = "music"
static enableWebsockets = true
static routesPath = `${__dirname}/routes` static routesPath = `${__dirname}/routes`
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3003 static listen_port = process.env.HTTP_LISTEN_PORT ?? 3003

View File

@ -61,6 +61,10 @@ export default async (req) => {
if (typeof trackLyrics.lrc === "object") { if (typeof trackLyrics.lrc === "object") {
trackLyrics.translated_lang = translate_lang trackLyrics.translated_lang = translate_lang
if (!trackLyrics.lrc[translate_lang]) {
translate_lang = "original"
}
if (trackLyrics.lrc[translate_lang]) { if (trackLyrics.lrc[translate_lang]) {
trackLyrics.synced_lyrics = await remoteLcrToSyncedLyrics(trackLyrics.lrc[translate_lang]) trackLyrics.synced_lyrics = await remoteLcrToSyncedLyrics(trackLyrics.lrc[translate_lang])
} }

View 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,
}
}
}

View File

@ -1,30 +1,34 @@
import { MusicRelease, Track } from "@db_models" import { MusicRelease, Track } from "@db_models"
import TrackClass from "@classes/track"
export default async (req) => { export default {
const { release_id } = req.params middlewares: ["withOptionalAuthentication"],
const { limit = 50, offset = 0 } = req.query fn: async (req) => {
const { release_id } = req.params
const { limit = 50, offset = 0 } = req.query
let release = await MusicRelease.findOne({ let release = await MusicRelease.findOne({
_id: release_id _id: release_id
}) })
if (!release) { if (!release) {
throw new OperationError(404, "Release not found") throw new OperationError(404, "Release not found")
}
release = release.toObject()
const totalTracks = await Track.countDocuments({
_id: release.list
})
const tracks = await TrackClass.get(release.list, {
user_id: req.auth?.session?.user_id,
onlyList: true
})
release.listLength = totalTracks
release.list = tracks
return release
} }
release = release.toObject()
const totalTracks = await Track.countDocuments({
_id: release.list
})
const tracks = await Track.find({
_id: { $in: release.list }
})
.limit(limit)
.skip(offset)
release.listLength = totalTracks
release.list = tracks
return release
} }

View File

@ -1,10 +1,14 @@
import TrackClass from "@classes/track" import TrackClass from "@classes/track"
export default { export default {
middlewares: ["withOptionalAuthentication"],
fn: async (req) => { fn: async (req) => {
const { track_id } = req.params 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 return track
} }

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -126,6 +126,8 @@ export default async (payload = {}) => {
} }
} }
post.share_url = `${process.env.APP_URL}/post/${post._id}`
return { return {
...post, ...post,
user, user,

View 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
}

View File

@ -67,18 +67,18 @@ async function linkInternalSubmodules(packages) {
const appPath = path.resolve(rootPath, pkgjson._web_app_path) const appPath = path.resolve(rootPath, pkgjson._web_app_path)
const comtyjsPath = path.resolve(rootPath, "comty.js") 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") const linebridePath = path.resolve(rootPath, "linebridge")
//* EVITE LINKING //* APP RUNTIME LINKING
console.log(`Linking Evite to app...`) console.log(`Linking Vessel to app...`)
await child_process.execSync("yarn link", { await child_process.execSync("yarn link", {
cwd: evitePath, cwd: vesselPath,
stdio: "inherit", stdio: "inherit",
}) })
await child_process.execSync(`yarn link "evite"`, { await child_process.execSync(`yarn link "vessel"`, {
cwd: appPath, cwd: appPath,
stdio: "inherit", stdio: "inherit",
}) })

View File