From 0db536bbeadf828f00404139dba01eef777431c5 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Tue, 29 Oct 2024 12:19:57 +0000 Subject: [PATCH] merge from local --- linebridge | 2 +- packages/app/index.html | 4 +- .../app/src/classes/TrackInstance/index.js | 14 ++ .../app/src/classes/TrackManifest/index.js | 3 +- .../components/Music/PlaylistView/index.jsx | 144 +++++------ .../app/src/components/Music/Track/index.jsx | 21 +- .../src/components/Player/Controls/index.jsx | 2 +- .../components/Player/ExtraActions/index.jsx | 10 +- .../components/Player/ToolBarPlayer/index.jsx | 2 +- .../PostCard/components/actions/index.jsx | 33 ++- .../app/src/components/PostCard/index.jsx | 7 + .../app/src/components/SearchButton/index.jsx | 7 +- .../src/components/TrendingsCard/index.jsx | 45 ++++ .../src/components/TrendingsCard/index.less | 35 +++ .../src/contexts/WithPlayerContext/index.jsx | 11 +- packages/app/src/cores/api/api.core.js | 7 + .../cores/player/classes/PlayerProcessors.js | 4 +- .../app/src/cores/player/classes/PlayerUI.js | 5 +- .../app/src/cores/player/classes/Presets.js | 85 ++++--- .../src/cores/player/classes/QueueManager.js | 126 ++++++++++ packages/app/src/cores/player/player.core.js | 232 +++++++----------- .../player/processors/compressorNode/index.js | 94 +------ .../cores/player/processors/eqNode/index.js | 84 +------ .../cores/remoteStorage/remoteStorage.core.js | 18 +- .../app/src/hooks/usePageWidgets/index.js | 4 +- .../src/layouts/components/toolsBar/index.jsx | 42 +++- .../layouts/components/toolsBar/index.less | 24 +- .../src/pages/@mobile-views/player/index.jsx | 2 +- .../src/pages/_debug/queuemanager/index.jsx | 115 +++++++++ .../lyrics/components/controller/index.jsx | 2 +- .../pages/lyrics/components/text/index.jsx | 4 +- .../pages/lyrics/components/video/index.jsx | 4 +- packages/app/src/pages/lyrics/index.jsx | 11 +- .../components/FeaturedPlaylist/index.jsx | 49 ++++ .../tabs/explore/components/Navbar/index.jsx | 18 ++ .../components/RecentlyPlayedList/index.jsx | 14 ++ .../components/RecentlyPlayedList/index.less | 17 ++ .../components/ReleasesList/index.jsx | 0 .../components/ReleasesList/index.less | 0 .../components/SearchResults/index.jsx | 103 ++++++++ .../src/pages/music/tabs/explore/index.jsx | 175 ++----------- .../src/pages/music/tabs/favorites/index.jsx | 56 +++-- packages/app/src/pages/timeline/index.jsx | 13 +- .../components/changePassword/index.jsx | 4 +- .../components/slidersWithPresets/index.jsx | 32 +-- packages/app/src/settings/player/index.jsx | 38 +-- .../player/items/player.compressor/index.jsx | 2 +- .../settings/player/items/player.eq/index.jsx | 2 +- packages/app/vite.config.js | 14 +- packages/server/gateway/proxy.js | 12 +- .../services/music/classes/track/index.js | 2 + .../music/classes/track/methods/get.js | 52 +++- .../classes/track/methods/isFavourite.js | 26 ++ .../classes/track/methods/toggleFavourite.js | 64 +++++ .../server/services/music/music.service.js | 1 + .../routes/music/lyrics/[track_id]/get.js | 4 + .../music/routes/music/my/folder/get.js | 73 ++++++ .../music/releases/[release_id]/data/get.js | 52 ++-- .../music/tracks/[track_id]/data/get.js | 6 +- .../music/tracks/[track_id]/favourite/post.js | 17 ++ .../tracks/[track_id]/is_favourite/get.js | 15 ++ .../posts/classes/posts/methods/fullfill.js | 2 + .../posts/routes/posts/trendings/get.js | 57 +++++ scripts/post-install.js | 10 +- evite => vessel | 0 65 files changed, 1376 insertions(+), 757 deletions(-) create mode 100644 packages/app/src/components/TrendingsCard/index.jsx create mode 100644 packages/app/src/components/TrendingsCard/index.less create mode 100644 packages/app/src/cores/player/classes/QueueManager.js create mode 100644 packages/app/src/pages/_debug/queuemanager/index.jsx create mode 100644 packages/app/src/pages/music/tabs/explore/components/FeaturedPlaylist/index.jsx create mode 100644 packages/app/src/pages/music/tabs/explore/components/Navbar/index.jsx create mode 100644 packages/app/src/pages/music/tabs/explore/components/RecentlyPlayedList/index.jsx create mode 100644 packages/app/src/pages/music/tabs/explore/components/RecentlyPlayedList/index.less rename packages/app/src/{ => pages/music/tabs/explore}/components/ReleasesList/index.jsx (100%) rename packages/app/src/{ => pages/music/tabs/explore}/components/ReleasesList/index.less (100%) create mode 100644 packages/app/src/pages/music/tabs/explore/components/SearchResults/index.jsx create mode 100644 packages/server/services/music/classes/track/methods/isFavourite.js create mode 100644 packages/server/services/music/classes/track/methods/toggleFavourite.js create mode 100644 packages/server/services/music/routes/music/my/folder/get.js create mode 100644 packages/server/services/music/routes/music/tracks/[track_id]/favourite/post.js create mode 100644 packages/server/services/music/routes/music/tracks/[track_id]/is_favourite/get.js create mode 100644 packages/server/services/posts/routes/posts/trendings/get.js rename evite => vessel (100%) 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) => { }

- {props.length ?? playlist.total_length ?? playlist.list.length} Tracks + {props.length ?? playlist.total_length ?? playlist.list.length} Items

{ @@ -332,16 +333,29 @@ 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
+
+

Recently played

+
+
+} + +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) => { }} />