diff --git a/linebridge b/linebridge
index d2e6f1bc..e52925b1 160000
--- a/linebridge
+++ b/linebridge
@@ -1 +1 @@
-Subproject commit d2e6f1bc5856e3084d4fd068dec5d67ab2ef9d8d
+Subproject commit e52925b191b6e1d1415f2bb63d921ccad8c18411
diff --git a/packages/app/index.html b/packages/app/index.html
index da187bd8..8361d8ca 100755
--- a/packages/app/index.html
+++ b/packages/app/index.html
@@ -7,9 +7,11 @@
+
+
-
+
diff --git a/packages/app/src/classes/TrackInstance/index.js b/packages/app/src/classes/TrackInstance/index.js
index fa3650e7..eb9fa8f8 100644
--- a/packages/app/src/classes/TrackInstance/index.js
+++ b/packages/app/src/classes/TrackInstance/index.js
@@ -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 = {
diff --git a/packages/app/src/classes/TrackManifest/index.js b/packages/app/src/classes/TrackManifest/index.js
index 7fef97bf..70e25a32 100644
--- a/packages/app/src/classes/TrackManifest/index.js
+++ b/packages/app/src/classes/TrackManifest/index.js
@@ -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)
diff --git a/packages/app/src/components/Music/PlaylistView/index.jsx b/packages/app/src/components/Music/PlaylistView/index.jsx
index 7712a136..c659bb6e 100755
--- a/packages/app/src/components/Music/PlaylistView/index.jsx
+++ b/packages/app/src/components/Music/PlaylistView/index.jsx
@@ -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) => {
}
-
-
- Tracks
-
+ {
+ playlist.list.length > 0 &&
+
+ Tracks
+
-
+
+ }
+
+ {
+ playlist.list.length === 0 &&
+ 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 &&
- This playlist its empty!
- >
- }
- />
- }
-
{
!searchResults && playlist.list.length > 0 && {
order={index + 1}
track={item}
onClickPlayBtn={() => handleOnClickTrack(item)}
- onLike={() => handleTrackLike(item)}
+ changeState={(update) => handleTrackChangeState(item._id, update)}
/>
})
}
@@ -390,4 +394,6 @@ export default (props) => {
-}
\ No newline at end of file
+}
+
+export default PlaylistView
\ No newline at end of file
diff --git a/packages/app/src/components/Music/Track/index.jsx b/packages/app/src/components/Music/Track/index.jsx
index 5e43a366..efb5916b 100755
--- a/packages/app/src/components/Music/Track/index.jsx
+++ b/packages/app/src/components/Music/Track/index.jsx
@@ -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
)
diff --git a/packages/app/src/components/Player/Controls/index.jsx b/packages/app/src/components/Player/Controls/index.jsx
index 0681cc47..3e151ff3 100755
--- a/packages/app/src/components/Player/Controls/index.jsx
+++ b/packages/app/src/components/Player/Controls/index.jsx
@@ -35,7 +35,7 @@ const EventsHandlers = {
}
const Controls = (props) => {
- const playerState = usePlayerStateContext()
+ const [playerState] = usePlayerStateContext()
const handleAction = (event, ...args) => {
if (typeof EventsHandlers[event] !== "function") {
diff --git a/packages/app/src/components/Player/ExtraActions/index.jsx b/packages/app/src/components/Player/ExtraActions/index.jsx
index b75c876a..4e307f97 100755
--- a/packages/app/src/components/Player/ExtraActions/index.jsx
+++ b/packages/app/src/components/Player/ExtraActions/index.jsx
@@ -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
@@ -21,9 +22,10 @@ const ExtraActions = (props) => {
disabled={!playerState.track_manifest?.lyrics_enabled}
/>
}
+
{
!app.isMobile &&
}
diff --git a/packages/app/src/components/Player/ToolBarPlayer/index.jsx b/packages/app/src/components/Player/ToolBarPlayer/index.jsx
index e91bfe5e..0469f5a4 100755
--- a/packages/app/src/components/Player/ToolBarPlayer/index.jsx
+++ b/packages/app/src/components/Player/ToolBarPlayer/index.jsx
@@ -43,7 +43,7 @@ const ServiceIndicator = (props) => {
}
const Player = (props) => {
- const playerState = usePlayerStateContext()
+ const [playerState] = usePlayerStateContext()
const contentRef = React.useRef()
const titleRef = React.useRef()
diff --git a/packages/app/src/components/PostCard/components/actions/index.jsx b/packages/app/src/components/PostCard/components/actions/index.jsx
index 6cbebbfd..97576def 100755
--- a/packages/app/src/components/PostCard/components/actions/index.jsx
+++ b/packages/app/src/components/PostCard/components/actions/index.jsx
@@ -31,13 +31,13 @@ const SelfActionsItems = [
]
const MoreActionsItems = [
- {
- key: "onClickRepost",
- label: <>
-
-
Repost
- >,
- },
+ // {
+ // key: "onClickRepost",
+ // label: <>
+ //
+ //
Repost
+ // >,
+ // },
{
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)
}
}
diff --git a/packages/app/src/components/PostCard/index.jsx b/packages/app/src/components/PostCard/index.jsx
index 0e584497..13c12b2a 100755
--- a/packages/app/src/components/PostCard/index.jsx
+++ b/packages/app/src/components/PostCard/index.jsx
@@ -46,6 +46,12 @@ const messageRegexs = [
return
window.app.location.push(`/@${result[1].substr(1)}`)}>{result[1]}
}
},
+ {
+ regex: /#[a-zA-Z0-9_]+/gi,
+ fn: (key, result) => {
+ return
window.app.location.push(`/trending/${result[0].substr(1)}`)}>{result[0]}
+ }
+ }
]
export default class PostCard extends React.PureComponent {
@@ -223,6 +229,7 @@ export default class PostCard extends React.PureComponent {
{
+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}
/>
-}
\ No newline at end of file
+}
+
+export default SearchButton
\ No newline at end of file
diff --git a/packages/app/src/components/TrendingsCard/index.jsx b/packages/app/src/components/TrendingsCard/index.jsx
new file mode 100644
index 00000000..5fbe150c
--- /dev/null
+++ b/packages/app/src/components/TrendingsCard/index.jsx
@@ -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
+
+
Trendings
+
+
+
+ {
+ L_Trendings &&
+ }
+ {
+ E_Trendings &&
Something went wrong
+ }
+ {
+ !L_Trendings && !E_Trendings && R_Trendings && R_Trendings.map((trending, index) => {
+ return
window.app.location.push(`/trending/${trending.hashtag}`)}
+ >
+
+ #{index + 1} {trending.hashtag}
+
+
+
+ {trending.count} posts
+
+
+ })
+ }
+
+
+}
+
+export default TrendingsCard
\ No newline at end of file
diff --git a/packages/app/src/components/TrendingsCard/index.less b/packages/app/src/components/TrendingsCard/index.less
new file mode 100644
index 00000000..d9629d21
--- /dev/null
+++ b/packages/app/src/components/TrendingsCard/index.less
@@ -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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/contexts/WithPlayerContext/index.jsx b/packages/app/src/contexts/WithPlayerContext/index.jsx
index d447304a..862bbeb7 100755
--- a/packages/app/src/contexts/WithPlayerContext/index.jsx
+++ b/packages/app/src/contexts/WithPlayerContext/index.jsx
@@ -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({})
diff --git a/packages/app/src/cores/api/api.core.js b/packages/app/src/cores/api/api.core.js
index 7d9e278b..8ae55fc1 100755
--- a/packages/app/src/cores/api/api.core.js
+++ b/packages/app/src/cores/api/api.core.js
@@ -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")
})
diff --git a/packages/app/src/cores/player/classes/PlayerProcessors.js b/packages/app/src/cores/player/classes/PlayerProcessors.js
index b8b17c8a..f59db0aa 100644
--- a/packages/app/src/cores/player/classes/PlayerProcessors.js
+++ b/packages/app/src/cores/player/classes/PlayerProcessors.js
@@ -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)
diff --git a/packages/app/src/cores/player/classes/PlayerUI.js b/packages/app/src/cores/player/classes/PlayerUI.js
index c3bbf7f0..41c12bbd 100644
--- a/packages/app/src/cores/player/classes/PlayerUI.js
+++ b/packages/app/src/cores/player/classes/PlayerUI.js
@@ -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",
+ })
}
}
diff --git a/packages/app/src/cores/player/classes/Presets.js b/packages/app/src/cores/player/classes/Presets.js
index 48d408f3..5fb372be 100755
--- a/packages/app/src/cores/player/classes/Presets.js
+++ b/packages/app/src/cores/player/classes/Presets.js
@@ -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)
+ }
+ })
+ })
}
}
\ No newline at end of file
diff --git a/packages/app/src/cores/player/classes/QueueManager.js b/packages/app/src/cores/player/classes/QueueManager.js
new file mode 100644
index 00000000..5a7bf7ef
--- /dev/null
+++ b/packages/app/src/cores/player/classes/QueueManager.js
@@ -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
+ }
+}
diff --git a/packages/app/src/cores/player/player.core.js b/packages/app/src/cores/player/player.core.js
index 7e0dedb2..f591eaf0 100755
--- a/packages/app/src/cores/player/player.core.js
+++ b/packages/app/src/cores/player/player.core.js
@@ -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.track_next_instances.push(instance)
-
- if (index === 0) {
- this.play(this.track_next_instances[0], {
- time: time ?? 0
- })
- }
+ this.queue.add(instance)
}
+ const item = this.queue.set(startIndex)
+
+ 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
diff --git a/packages/app/src/cores/player/processors/compressorNode/index.js b/packages/app/src/cores/player/processors/compressorNode/index.js
index 62be15bd..8c372892 100755
--- a/packages/app/src/cores/player/processors/compressorNode/index.js
+++ b/packages/app/src/cores/player/processors/compressorNode/index.js
@@ -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]
})
}
}
\ No newline at end of file
diff --git a/packages/app/src/cores/player/processors/eqNode/index.js b/packages/app/src/cores/player/processors/eqNode/index.js
index a7f7d739..09103971 100755
--- a/packages/app/src/cores/player/processors/eqNode/index.js
+++ b/packages/app/src/cores/player/processors/eqNode/index.js
@@ -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]),
diff --git a/packages/app/src/cores/remoteStorage/remoteStorage.core.js b/packages/app/src/cores/remoteStorage/remoteStorage.core.js
index e94de69a..ce926f5d 100755
--- a/packages/app/src/cores/remoteStorage/remoteStorage.core.js
+++ b/packages/app/src/cores/remoteStorage/remoteStorage.core.js
@@ -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) {
diff --git a/packages/app/src/hooks/usePageWidgets/index.js b/packages/app/src/hooks/usePageWidgets/index.js
index 8850cf77..dc761344 100644
--- a/packages/app/src/hooks/usePageWidgets/index.js
+++ b/packages/app/src/hooks/usePageWidgets/index.js
@@ -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 () => {
diff --git a/packages/app/src/layouts/components/toolsBar/index.jsx b/packages/app/src/layouts/components/toolsBar/index.jsx
index 0c49541b..b8fd3724 100755
--- a/packages/app/src/layouts/components/toolsBar/index.jsx
+++ b/packages/app/src/layouts/components/toolsBar/index.jsx
@@ -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"
>
-
+
{
- 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,
+ })
})
}
+
+
+ {
+ this.state.renders.bottom.map((render, index) => {
+ return React.createElement(render.component, {
+ ...render.props,
+ key: index,
+ })
+ })
+ }
+
}}
diff --git a/packages/app/src/layouts/components/toolsBar/index.less b/packages/app/src/layouts/components/toolsBar/index.less
index b3165016..e8c1f7ad 100755
--- a/packages/app/src/layouts/components/toolsBar/index.less
+++ b/packages/app/src/layouts/components/toolsBar/index.less
@@ -7,7 +7,7 @@
right: 0;
z-index: 150;
-
+
height: 100vh;
height: 100dvh;
max-width: 420px;
@@ -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);
+ }
}
}
\ No newline at end of file
diff --git a/packages/app/src/pages/@mobile-views/player/index.jsx b/packages/app/src/pages/@mobile-views/player/index.jsx
index 60238bc8..305e6452 100755
--- a/packages/app/src/pages/@mobile-views/player/index.jsx
+++ b/packages/app/src/pages/@mobile-views/player/index.jsx
@@ -29,7 +29,7 @@ const ServiceIndicator = (props) => {
}
const AudioPlayer = (props) => {
- const playerState = usePlayerStateContext()
+ const [playerState] = usePlayerStateContext()
React.useEffect(() => {
if (app.currentDragger) {
diff --git a/packages/app/src/pages/_debug/queuemanager/index.jsx b/packages/app/src/pages/_debug/queuemanager/index.jsx
new file mode 100644
index 00000000..85967f36
--- /dev/null
+++ b/packages/app/src/pages/_debug/queuemanager/index.jsx
@@ -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 (
+
+ Queue Debugger
+
+ {current ? current.name : "None"}
+
+
+
+ (
+ handleSet(item)}>Set,
+ ]}
+ >
+ {item.name}
+
+ )}
+ />
+
+
+ (
+ handleSet(item)}>Set,
+ ,
+ ]}
+ >
+ {item.name}
+
+ )}
+ />
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default QueueDebugger
diff --git a/packages/app/src/pages/lyrics/components/controller/index.jsx b/packages/app/src/pages/lyrics/components/controller/index.jsx
index fa0e2ba8..3183edc1 100644
--- a/packages/app/src/pages/lyrics/components/controller/index.jsx
+++ b/packages/app/src/pages/lyrics/components/controller/index.jsx
@@ -47,7 +47,7 @@ const RenderAlbum = (props) => {
}
const PlayerController = React.forwardRef((props, ref) => {
- const playerState = usePlayerStateContext()
+ const [playerState] = usePlayerStateContext()
const titleRef = React.useRef()
diff --git a/packages/app/src/pages/lyrics/components/text/index.jsx b/packages/app/src/pages/lyrics/components/text/index.jsx
index 0b9bb765..b58d9b48 100644
--- a/packages/app/src/pages/lyrics/components/text/index.jsx
+++ b/packages/app/src/pages/lyrics/components/text/index.jsx
@@ -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}`)
diff --git a/packages/app/src/pages/lyrics/components/video/index.jsx b/packages/app/src/pages/lyrics/components/video/index.jsx
index ece51ac2..3d04a4c4 100644
--- a/packages/app/src/pages/lyrics/components/video/index.jsx
+++ b/packages/app/src/pages/lyrics/components/video/index.jsx
@@ -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()
}
}
diff --git a/packages/app/src/pages/lyrics/index.jsx b/packages/app/src/pages/lyrics/index.jsx
index 524592b1..6b43d79d 100644
--- a/packages/app/src/pages/lyrics/index.jsx
+++ b/packages/app/src/pages/lyrics/index.jsx
@@ -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) => {
}
-export default EnchancedLyrics
\ No newline at end of file
+export default EnchancedLyricsPage
\ No newline at end of file
diff --git a/packages/app/src/pages/music/tabs/explore/components/FeaturedPlaylist/index.jsx b/packages/app/src/pages/music/tabs/explore/components/FeaturedPlaylist/index.jsx
new file mode 100644
index 00000000..77718101
--- /dev/null
+++ b/packages/app/src/pages/music/tabs/explore/components/FeaturedPlaylist/index.jsx
@@ -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
+
+
+
+
{featuredPlaylist.title}
+
{featuredPlaylist.description}
+
+ {
+ featuredPlaylist.genre &&
+ {featuredPlaylist.genre}
+
+ }
+
+
+}
+
+export default FeaturedPlaylist
\ No newline at end of file
diff --git a/packages/app/src/pages/music/tabs/explore/components/Navbar/index.jsx b/packages/app/src/pages/music/tabs/explore/components/Navbar/index.jsx
new file mode 100644
index 00000000..0150a3bf
--- /dev/null
+++ b/packages/app/src/pages/music/tabs/explore/components/Navbar/index.jsx
@@ -0,0 +1,18 @@
+import React from "react"
+
+import Searcher from "@components/Searcher"
+import MusicModel from "@models/music"
+
+const MusicNavbar = (props) => {
+ return
+ props.setSearchResults(false)}
+ />
+
+}
+
+export default MusicNavbar
\ No newline at end of file
diff --git a/packages/app/src/pages/music/tabs/explore/components/RecentlyPlayedList/index.jsx b/packages/app/src/pages/music/tabs/explore/components/RecentlyPlayedList/index.jsx
new file mode 100644
index 00000000..4474e04e
--- /dev/null
+++ b/packages/app/src/pages/music/tabs/explore/components/RecentlyPlayedList/index.jsx
@@ -0,0 +1,14 @@
+import React from "react"
+import { Icons } from "@components/Icons"
+
+import "./index.less"
+
+const RecentlyPlayedList = (props) => {
+ return
+}
+
+export default RecentlyPlayedList
\ No newline at end of file
diff --git a/packages/app/src/pages/music/tabs/explore/components/RecentlyPlayedList/index.less b/packages/app/src/pages/music/tabs/explore/components/RecentlyPlayedList/index.less
new file mode 100644
index 00000000..c964fc53
--- /dev/null
+++ b/packages/app/src/pages/music/tabs/explore/components/RecentlyPlayedList/index.less
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/components/ReleasesList/index.jsx b/packages/app/src/pages/music/tabs/explore/components/ReleasesList/index.jsx
similarity index 100%
rename from packages/app/src/components/ReleasesList/index.jsx
rename to packages/app/src/pages/music/tabs/explore/components/ReleasesList/index.jsx
diff --git a/packages/app/src/components/ReleasesList/index.less b/packages/app/src/pages/music/tabs/explore/components/ReleasesList/index.less
similarity index 100%
rename from packages/app/src/components/ReleasesList/index.less
rename to packages/app/src/pages/music/tabs/explore/components/ReleasesList/index.less
diff --git a/packages/app/src/pages/music/tabs/explore/components/SearchResults/index.jsx b/packages/app/src/pages/music/tabs/explore/components/SearchResults/index.jsx
new file mode 100644
index 00000000..b082ee28
--- /dev/null
+++ b/packages/app/src/pages/music/tabs/explore/components/SearchResults/index.jsx
@@ -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
+ }
+ },
+ "tracks": {
+ icon: "MdMusicNote",
+ label: "Tracks",
+ renderItem: (props) => {
+ return 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
+ }
+
+ return
+ {
+ groupsKeys.map((key, index) => {
+ const decorator = ResultGroupsDecorators[key] ?? {
+ icon: null,
+ label: key,
+ renderItem: () => null
+ }
+
+ return
+
+
+ {
+ createIconRender(decorator.icon)
+ }
+
+ {(t) => t(decorator.label)}
+
+
+
+
+
+ {
+ data[key].map((item, index) => {
+ return decorator.renderItem({
+ key: index,
+ item
+ })
+ })
+ }
+
+
+ })
+ }
+
+}
+
+export default SearchResults
\ No newline at end of file
diff --git a/packages/app/src/pages/music/tabs/explore/index.jsx b/packages/app/src/pages/music/tabs/explore/index.jsx
index 93eded1e..9dc85f79 100755
--- a/packages/app/src/pages/music/tabs/explore/index.jsx
+++ b/packages/app/src/pages/music/tabs/explore/index.jsx
@@ -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
-
-
-
-
{featuredPlaylist.title}
-
{featuredPlaylist.description}
-
- {
- featuredPlaylist.genre &&
- {featuredPlaylist.genre}
-
- }
-
-
-}
-
-const MusicNavbar = (props) => {
- return
- props.setSearchResults(false)}
- />
-
-}
-
-const ResultGroupsDecorators = {
- "playlists": {
- icon: "MdPlaylistPlay",
- label: "Playlists",
- renderItem: (props) => {
- return
- }
- },
- "tracks": {
- icon: "MdMusicNote",
- label: "Tracks",
- renderItem: (props) => {
- return 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
- }
-
- return
- {
- groupsKeys.map((key, index) => {
- const decorator = ResultGroupsDecorators[key] ?? {
- icon: null,
- label: key,
- renderItem: () => null
- }
-
- return
-
-
- {
- createIconRender(decorator.icon)
- }
-
- {(t) => t(decorator.label)}
-
-
-
-
-
- {
- data[key].map((item, index) => {
- return decorator.renderItem({
- key: index,
- item
- })
- })
- }
-
-
- })
- }
-
-}
-
-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 &&
+
+
}
@@ -224,4 +75,6 @@ export default (props) => {
}
-}
\ No newline at end of file
+}
+
+export default MusicExploreTab
\ No newline at end of file
diff --git a/packages/app/src/pages/music/tabs/favorites/index.jsx b/packages/app/src/pages/music/tabs/favorites/index.jsx
index dacf23d5..1e7c9ebb 100755
--- a/packages/app/src/pages/music/tabs/favorites/index.jsx
+++ b/packages/app/src/pages/music/tabs/favorites/index.jsx
@@ -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) {
- this.setState({
- empty: true,
- })
- }
-
- return this.setState({
+ 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({
@@ -112,7 +127,7 @@ export default class FavoriteTracks extends React.Component {
}
return
}
diff --git a/packages/app/src/pages/timeline/index.jsx b/packages/app/src/pages/timeline/index.jsx
index 34e85eb1..02646888 100755
--- a/packages/app/src/pages/timeline/index.jsx
+++ b/packages/app/src/pages/timeline/index.jsx
@@ -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
-
- Trendings
-
-
-
- XD
-
-
-}
-
const TimelinePage = () => {
usePageWidgets([
{
diff --git a/packages/app/src/settings/components/changePassword/index.jsx b/packages/app/src/settings/components/changePassword/index.jsx
index 156d70aa..ec5979c2 100755
--- a/packages/app/src/settings/components/changePassword/index.jsx
+++ b/packages/app/src/settings/components/changePassword/index.jsx
@@ -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
diff --git a/packages/app/src/settings/components/slidersWithPresets/index.jsx b/packages/app/src/settings/components/slidersWithPresets/index.jsx
index 7a8d69fa..94b209b2 100755
--- a/packages/app/src/settings/components/slidersWithPresets/index.jsx
+++ b/packages/app/src/settings/components/slidersWithPresets/index.jsx
@@ -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) => {
}}
/>
}
disabled={selectedPreset === "default"}
/>
diff --git a/packages/app/src/settings/player/index.jsx b/packages/app/src/settings/player/index.jsx
index 9c67f9c0..e779c52d 100755
--- a/packages/app/src/settings/player/index.jsx
+++ b/packages/app/src/settings/player/index.jsx
@@ -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
},
diff --git a/packages/app/src/settings/player/items/player.compressor/index.jsx b/packages/app/src/settings/player/items/player.compressor/index.jsx
index 86f4aeec..395c5d81 100755
--- a/packages/app/src/settings/player/items/player.compressor/index.jsx
+++ b/packages/app/src/settings/player/items/player.compressor/index.jsx
@@ -4,7 +4,7 @@ import SlidersWithPresets from "../../../components/slidersWithPresets"
export default (props) => {
return {
return
}
\ No newline at end of file
diff --git a/packages/app/vite.config.js b/packages/app/vite.config.js
index 4af6f310..1974d3ee 100755
--- a/packages/app/vite.config.js
+++ b/packages/app/vite.config.js
@@ -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: {
@@ -42,7 +54,7 @@ export default defineConfig({
build: {
target: "esnext",
rollupOptions: {
- output:{
+ output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString();
diff --git a/packages/server/gateway/proxy.js b/packages/server/gateway/proxy.js
index a4510c76..b23546c3 100644
--- a/packages/server/gateway/proxy.js
+++ b/packages/server/gateway/proxy.js
@@ -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]}`
diff --git a/packages/server/services/music/classes/track/index.js b/packages/server/services/music/classes/track/index.js
index 280d10ff..750dea16 100644
--- a/packages/server/services/music/classes/track/index.js
+++ b/packages/server/services/music/classes/track/index.js
@@ -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
}
\ No newline at end of file
diff --git a/packages/server/services/music/classes/track/methods/get.js b/packages/server/services/music/classes/track/methods/get.js
index 963cd604..9fd4aa92 100644
--- a/packages/server/services/music/classes/track/methods/get.js
+++ b/packages/server/services/music/classes/track/methods/get.js
@@ -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
}
\ No newline at end of file
diff --git a/packages/server/services/music/classes/track/methods/isFavourite.js b/packages/server/services/music/classes/track/methods/isFavourite.js
new file mode 100644
index 00000000..6d4b1ffa
--- /dev/null
+++ b/packages/server/services/music/classes/track/methods/isFavourite.js
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/music/classes/track/methods/toggleFavourite.js b/packages/server/services/music/classes/track/methods/toggleFavourite.js
new file mode 100644
index 00000000..6b4d24bf
--- /dev/null
+++ b/packages/server/services/music/classes/track/methods/toggleFavourite.js
@@ -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(),
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/music/music.service.js b/packages/server/services/music/music.service.js
index 03e7afa0..251a3a30 100755
--- a/packages/server/services/music/music.service.js
+++ b/packages/server/services/music/music.service.js
@@ -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
diff --git a/packages/server/services/music/routes/music/lyrics/[track_id]/get.js b/packages/server/services/music/routes/music/lyrics/[track_id]/get.js
index 241af038..50a01167 100644
--- a/packages/server/services/music/routes/music/lyrics/[track_id]/get.js
+++ b/packages/server/services/music/routes/music/lyrics/[track_id]/get.js
@@ -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])
}
diff --git a/packages/server/services/music/routes/music/my/folder/get.js b/packages/server/services/music/routes/music/my/folder/get.js
new file mode 100644
index 00000000..d48e8ac1
--- /dev/null
+++ b/packages/server/services/music/routes/music/my/folder/get.js
@@ -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,
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/music/routes/music/releases/[release_id]/data/get.js b/packages/server/services/music/routes/music/releases/[release_id]/data/get.js
index 52c9a574..4c8e76f8 100644
--- a/packages/server/services/music/routes/music/releases/[release_id]/data/get.js
+++ b/packages/server/services/music/routes/music/releases/[release_id]/data/get.js
@@ -1,30 +1,34 @@
import { MusicRelease, Track } from "@db_models"
+import TrackClass from "@classes/track"
-export default async (req) => {
- const { release_id } = req.params
- const { limit = 50, offset = 0 } = req.query
+export default {
+ middlewares: ["withOptionalAuthentication"],
+ fn: async (req) => {
+ const { release_id } = req.params
+ const { limit = 50, offset = 0 } = req.query
- let release = await MusicRelease.findOne({
- _id: release_id
- })
+ let release = await MusicRelease.findOne({
+ _id: release_id
+ })
- if (!release) {
- throw new OperationError(404, "Release not found")
+ if (!release) {
+ 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
}
\ No newline at end of file
diff --git a/packages/server/services/music/routes/music/tracks/[track_id]/data/get.js b/packages/server/services/music/routes/music/tracks/[track_id]/data/get.js
index e410e18c..327a0516 100644
--- a/packages/server/services/music/routes/music/tracks/[track_id]/data/get.js
+++ b/packages/server/services/music/routes/music/tracks/[track_id]/data/get.js
@@ -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
}
diff --git a/packages/server/services/music/routes/music/tracks/[track_id]/favourite/post.js b/packages/server/services/music/routes/music/tracks/[track_id]/favourite/post.js
new file mode 100644
index 00000000..553ae294
--- /dev/null
+++ b/packages/server/services/music/routes/music/tracks/[track_id]/favourite/post.js
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/music/routes/music/tracks/[track_id]/is_favourite/get.js b/packages/server/services/music/routes/music/tracks/[track_id]/is_favourite/get.js
new file mode 100644
index 00000000..6ee9e5df
--- /dev/null
+++ b/packages/server/services/music/routes/music/tracks/[track_id]/is_favourite/get.js
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/posts/classes/posts/methods/fullfill.js b/packages/server/services/posts/classes/posts/methods/fullfill.js
index b84f593e..309428ba 100644
--- a/packages/server/services/posts/classes/posts/methods/fullfill.js
+++ b/packages/server/services/posts/classes/posts/methods/fullfill.js
@@ -126,6 +126,8 @@ export default async (payload = {}) => {
}
}
+ post.share_url = `${process.env.APP_URL}/post/${post._id}`
+
return {
...post,
user,
diff --git a/packages/server/services/posts/routes/posts/trendings/get.js b/packages/server/services/posts/routes/posts/trendings/get.js
new file mode 100644
index 00000000..6f099fc8
--- /dev/null
+++ b/packages/server/services/posts/routes/posts/trendings/get.js
@@ -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
+}
\ No newline at end of file
diff --git a/scripts/post-install.js b/scripts/post-install.js
index c6c3a7f3..4865519b 100755
--- a/scripts/post-install.js
+++ b/scripts/post-install.js
@@ -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",
})
diff --git a/evite b/vessel
similarity index 100%
rename from evite
rename to vessel