From 8c3e9a504b7eef5ca75baa0b3ad597315cc580b7 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Fri, 25 Oct 2024 09:39:35 +0000 Subject: [PATCH] merge from local --- packages/app/package.json | 2 +- packages/app/src/App.jsx | 16 +- .../TrackInstance/index.js} | 114 ++- .../app/src/classes/TrackManifest/index.js | 150 +++ .../index.less => BadgeCard/index.jsx} | 0 .../app/src/components/Music/Track/index.jsx | 12 +- .../app/src/components/Music/Track/index.less | 10 +- .../components/LyricsEditor/index.jsx | 97 ++ .../components/LyricsEditor/index.less | 11 + .../components/VideoEditor/index.jsx | 106 ++ .../components/VideoEditor/index.less | 25 + .../EnhancedLyricsEditor/index.jsx | 120 +++ .../EnhancedLyricsEditor/index.less | 6 + .../MusicStudio/LyricsEditor/index.jsx | 116 --- .../MusicStudio/LyricsTextView/index.jsx | 31 +- .../MusicStudio/LyricsTextView/index.less | 15 + .../MusicStudio/ReleaseEditor/index.jsx | 108 +- .../Tracks/components/TrackListItem/index.jsx | 35 +- .../components/TrackListItem/index.less | 15 + .../ReleaseEditor/tabs/Tracks/index.jsx | 185 ++-- .../MusicStudio/TrackEditor/index.jsx | 122 +-- .../MusicStudio/TrackEditor/index.less | 4 +- .../MusicStudio/VideoEditor/index.jsx | 14 - .../Player/BackgroundMediaPlayer/index.jsx | 190 ---- .../Player/BackgroundMediaPlayer/index.less | 239 ----- .../src/components/Player/Controls/index.jsx | 169 ++-- .../components/Player/ExtraActions/index.jsx | 10 +- .../src/components/Player/SeekBar/index.jsx | 22 +- .../components/Player/ToolBarPlayer/index.jsx | 40 +- .../PostCard/components/actions/index.jsx | 6 +- .../app/src/components/UploadButton/index.jsx | 1 + .../app/src/components/VideoPlayer/index.jsx | 79 ++ .../app/src/components/VideoPlayer/index.less | 16 + .../src/contexts/WithPlayerContext/index.jsx | 78 +- .../components/contextMenu/index.less | 2 +- .../MediaSession.js} | 0 .../cores/player/classes/PlayerProcessors.js | 85 ++ .../src/cores/player/classes/PlayerState.js | 37 + .../app/src/cores/player/classes/PlayerUI.js | 40 + .../player/{presets.js => classes/Presets.js} | 2 +- .../{services.js => classes/Services.js} | 0 .../src/cores/player/helpers/setSampleRate.js | 37 + packages/app/src/cores/player/player.bkp.js | 946 ------------------ packages/app/src/cores/player/player.core.js | 517 ++-------- .../player/processors/compressorNode/index.js | 2 +- .../cores/player/processors/eqNode/index.js | 2 +- .../cores/player/processors/gainNode/index.js | 2 +- .../src/cores/remoteStorage/chunkedUpload.js | 207 ++-- .../cores/remoteStorage/remoteStorage.core.js | 15 +- .../app/src/hooks/usePageWidgets/index.js | 17 + .../components/@mobile/bottomBar/index.jsx | 16 +- .../components/draggableDrawer/index.jsx | 10 +- .../src/layouts/components/drawer/index.less | 4 + .../src/layouts/components/modals/index.jsx | 2 +- .../src/layouts/components/sidebar/index.jsx | 7 +- .../src/layouts/components/sidebar/index.less | 5 + .../src/layouts/components/toolsBar/index.jsx | 28 +- .../layouts/components/toolsBar/index.less | 54 - packages/app/src/layouts/default/index.jsx | 3 +- .../src/pages/@mobile-views/player/index.jsx | 30 +- .../src/pages/account/tabs/details/index.jsx | 4 +- .../src/pages/auth/forms/selector/index.jsx | 2 +- .../app/src/pages/badge/[user_id]/index.jsx | 12 + .../lyrics/components/controller/index.jsx | 36 +- .../pages/lyrics/components/text/index.jsx | 26 +- .../pages/lyrics/components/video/index.jsx | 72 +- packages/app/src/pages/lyrics/index.jsx | 52 +- packages/app/src/pages/lyrics/index.less | 46 +- .../components/SettingItemComponent/index.jsx | 2 +- .../tv/components/EditableText/index.less | 5 + .../tv/components/ProfileData/index.jsx | 43 +- .../tv/components/ProfileData/index.less | 12 +- .../tv/components/ProfileSelector/index.jsx | 6 +- packages/app/src/pages/studio/tv/index.jsx | 35 +- packages/app/src/pages/studio/tv/index.less | 19 +- packages/app/src/pages/timeline/index.jsx | 73 +- packages/app/src/settings/about/index.jsx | 4 +- packages/app/src/settings/apparence/index.jsx | 4 + .../settings/tap_share/badge_editor/index.jsx | 15 + .../tap_share/badge_editor}/index.less | 0 .../tap_share/steps/data_editor/index.jsx | 110 +- .../tap_share/steps/data_editor/index.less | 15 + packages/app/src/styles/animations.less | 13 + packages/app/src/styles/fixments.less | 14 +- packages/app/src/styles/fonts.less | 1 + packages/app/src/styles/index.less | 85 +- packages/app/src/styles/layout.less | 67 ++ packages/server/boot | 42 +- .../server/classes/ChunkFileUpload/index.js | 78 +- .../classes/MultiqualityHLSJob/index.js | 147 +++ .../server/classes/StorageClient/index.js | 8 +- packages/server/db_models/NFCTags/index.js | 4 + .../server/db_models/activationCode/index.js | 21 + packages/server/db_models/post/index.js | 38 +- packages/server/db_models/track/index.js | 4 + .../{musicLyrics => trackLyrics}/index.js | 7 + packages/server/db_models/user/index.js | 82 +- packages/server/db_models/votePoll/index.js | 18 + packages/server/gateway/index.js | 13 +- packages/server/gateway/proxy.js | 2 +- packages/server/gateway/utils/spawnService.js | 9 +- packages/server/package.json | 9 +- packages/server/services/auth/auth.service.js | 2 +- .../services/auth/classes/account/index.js | 2 + .../account/methods/activateAccount.js | 50 + .../auth/classes/account/methods/create.js | 10 +- .../classes/account/methods/deleteSession.js | 4 +- .../account/methods/sendActivationCode.js | 51 + .../auth/routes/auth/activate/post.js | 5 + .../server/services/auth/routes/auth/post.js | 7 + .../auth/resend-activation-code/post.js | 18 + .../services/auth/routes/auth/token/post.js | 22 + .../server/services/chats/chats.service.js | 4 +- packages/server/services/ems/ems.service.js | 4 +- .../ems/ipcEvents/accountActivation.js | 19 + .../account_activation/index.handlebars | 328 ++++++ .../server/services/ems/templates/index.js | 1 + .../ems/templates/mfa_code/index.handlebars | 17 +- .../server/services/files/file.service.js | 6 +- packages/server/services/files/package.json | 7 +- .../services/files/routes/transcode/get.js | 88 ++ .../files/routes/upload/chunk/post.js | 2 + .../files/services/remoteUpload/index.js | 142 ++- .../remoteUpload/providers/b2/index.js | 39 + .../remoteUpload/providers/standard/index.js | 58 ++ .../services/files/services/transmux/index.js | 89 ++ .../files/utils/downloadFFMPEG/index.js | 20 + packages/server/services/main/main.service.js | 4 +- .../routes/nfc/tag/id/[id]/execute/get.js | 20 +- .../routes/nfc/tag/register/[serial]/post.js | 4 +- .../marketplace/marketplace.service.js | 3 +- .../music/classes/track/methods/create.js | 1 + .../music/controllers/featured/index.js | 21 - .../featured/routes/get/playlists.js | 19 - .../music/controllers/lyrics/index.js | 21 - .../lyrics/routes/get/[track_id].js | 56 -- .../music/controllers/playlists/index.js | 21 - .../playlists/routes/delete/[playlist_id].js | 40 - .../routes/get/[playlist_id]/data.js | 65 -- .../playlists/routes/get/search.js | 44 - .../controllers/playlists/routes/get/self.js | 68 -- .../controllers/playlists/routes/post/new.js | 47 - .../playlists/services/getTrackById.js | 19 - .../music/controllers/releases/index.js | 21 - .../releases/routes/delete/[release_id].js | 40 - .../releases/routes/get/[release_id]/data.js | 57 -- .../controllers/releases/routes/get/self.js | 54 - .../releases/routes/get/user/[user_id].js | 27 - .../releases/routes/put/release.js | 186 ---- .../music/controllers/search/index.js | 86 -- .../music/controllers/tracks/index.js | 21 - .../tracks/routes/delete/[track_id]/like.js | 36 - .../tracks/routes/get/[track_id]/data.js | 19 - .../tracks/routes/get/[track_id]/stream.js | 43 - .../controllers/tracks/routes/get/liked.js | 60 -- .../controllers/tracks/routes/get/many.js | 23 - .../tracks/routes/post/[track_id]/like.js | 42 - .../routes/post/[track_id]/refresh-cache.js | 36 - .../server/services/music/music.service.js | 3 +- .../routes/music/lyrics/[track_id]/get.js | 109 +- .../routes/music/lyrics/[track_id]/put.js | 57 ++ .../notifications/notifications.service.js | 4 +- .../services/posts/classes/posts/index.js | 2 + .../posts/classes/posts/methods/create.js | 13 +- .../classes/posts/methods/deletePollVote.js | 33 + .../posts/classes/posts/methods/fullfill.js | 50 +- .../posts/classes/posts/methods/update.js | 10 + .../posts/classes/posts/methods/votePoll.js | 46 + .../server/services/posts/posts.service.js | 4 +- .../routes/posts/[post_id]/update/put.js | 2 +- .../[post_id]/vote_poll/[option_id]/delete.js | 14 + .../[post_id]/vote_poll/[option_id]/post.js | 14 + .../users/classes/users/method/update.js | 2 +- .../users/routes/users/self/update/post.js | 10 +- .../server/services/users/users.service.js | 2 +- 175 files changed, 3757 insertions(+), 4306 deletions(-) rename packages/app/src/{cores/player/classes/TrackInstance.js => classes/TrackInstance/index.js} (91%) create mode 100644 packages/app/src/classes/TrackManifest/index.js rename packages/app/src/components/{MusicStudio/LyricsEditor/index.less => BadgeCard/index.jsx} (100%) create mode 100644 packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/LyricsEditor/index.jsx create mode 100644 packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/LyricsEditor/index.less create mode 100644 packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/VideoEditor/index.jsx create mode 100644 packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/VideoEditor/index.less create mode 100644 packages/app/src/components/MusicStudio/EnhancedLyricsEditor/index.jsx create mode 100644 packages/app/src/components/MusicStudio/EnhancedLyricsEditor/index.less delete mode 100644 packages/app/src/components/MusicStudio/LyricsEditor/index.jsx create mode 100644 packages/app/src/components/MusicStudio/LyricsTextView/index.less delete mode 100644 packages/app/src/components/MusicStudio/VideoEditor/index.jsx delete mode 100755 packages/app/src/components/Player/BackgroundMediaPlayer/index.jsx delete mode 100755 packages/app/src/components/Player/BackgroundMediaPlayer/index.less create mode 100644 packages/app/src/components/VideoPlayer/index.jsx create mode 100644 packages/app/src/components/VideoPlayer/index.less rename packages/app/src/cores/player/{mediaSession.js => classes/MediaSession.js} (100%) create mode 100644 packages/app/src/cores/player/classes/PlayerProcessors.js create mode 100644 packages/app/src/cores/player/classes/PlayerState.js create mode 100644 packages/app/src/cores/player/classes/PlayerUI.js rename packages/app/src/cores/player/{presets.js => classes/Presets.js} (97%) rename packages/app/src/cores/player/{services.js => classes/Services.js} (100%) create mode 100644 packages/app/src/cores/player/helpers/setSampleRate.js delete mode 100755 packages/app/src/cores/player/player.bkp.js create mode 100644 packages/app/src/hooks/usePageWidgets/index.js create mode 100644 packages/app/src/pages/badge/[user_id]/index.jsx create mode 100644 packages/app/src/settings/tap_share/badge_editor/index.jsx rename packages/app/src/{components/MusicStudio/VideoEditor => settings/tap_share/badge_editor}/index.less (100%) create mode 100644 packages/app/src/settings/tap_share/steps/data_editor/index.less create mode 100644 packages/app/src/styles/layout.less create mode 100644 packages/server/classes/MultiqualityHLSJob/index.js create mode 100644 packages/server/db_models/activationCode/index.js rename packages/server/db_models/{musicLyrics => trackLyrics}/index.js (61%) create mode 100644 packages/server/db_models/votePoll/index.js create mode 100644 packages/server/services/auth/classes/account/methods/activateAccount.js create mode 100644 packages/server/services/auth/classes/account/methods/sendActivationCode.js create mode 100644 packages/server/services/auth/routes/auth/activate/post.js create mode 100644 packages/server/services/auth/routes/auth/resend-activation-code/post.js create mode 100644 packages/server/services/auth/routes/auth/token/post.js create mode 100644 packages/server/services/ems/ipcEvents/accountActivation.js create mode 100644 packages/server/services/ems/templates/account_activation/index.handlebars create mode 100644 packages/server/services/files/routes/transcode/get.js create mode 100644 packages/server/services/files/services/remoteUpload/providers/b2/index.js create mode 100644 packages/server/services/files/services/remoteUpload/providers/standard/index.js create mode 100644 packages/server/services/files/services/transmux/index.js create mode 100644 packages/server/services/files/utils/downloadFFMPEG/index.js delete mode 100755 packages/server/services/music/controllers/featured/index.js delete mode 100755 packages/server/services/music/controllers/featured/routes/get/playlists.js delete mode 100755 packages/server/services/music/controllers/lyrics/index.js delete mode 100755 packages/server/services/music/controllers/lyrics/routes/get/[track_id].js delete mode 100755 packages/server/services/music/controllers/playlists/index.js delete mode 100755 packages/server/services/music/controllers/playlists/routes/delete/[playlist_id].js delete mode 100755 packages/server/services/music/controllers/playlists/routes/get/[playlist_id]/data.js delete mode 100755 packages/server/services/music/controllers/playlists/routes/get/search.js delete mode 100755 packages/server/services/music/controllers/playlists/routes/get/self.js delete mode 100755 packages/server/services/music/controllers/playlists/routes/post/new.js delete mode 100755 packages/server/services/music/controllers/playlists/services/getTrackById.js delete mode 100755 packages/server/services/music/controllers/releases/index.js delete mode 100755 packages/server/services/music/controllers/releases/routes/delete/[release_id].js delete mode 100755 packages/server/services/music/controllers/releases/routes/get/[release_id]/data.js delete mode 100755 packages/server/services/music/controllers/releases/routes/get/self.js delete mode 100755 packages/server/services/music/controllers/releases/routes/get/user/[user_id].js delete mode 100755 packages/server/services/music/controllers/releases/routes/put/release.js delete mode 100755 packages/server/services/music/controllers/search/index.js delete mode 100755 packages/server/services/music/controllers/tracks/index.js delete mode 100755 packages/server/services/music/controllers/tracks/routes/delete/[track_id]/like.js delete mode 100755 packages/server/services/music/controllers/tracks/routes/get/[track_id]/data.js delete mode 100755 packages/server/services/music/controllers/tracks/routes/get/[track_id]/stream.js delete mode 100755 packages/server/services/music/controllers/tracks/routes/get/liked.js delete mode 100755 packages/server/services/music/controllers/tracks/routes/get/many.js delete mode 100755 packages/server/services/music/controllers/tracks/routes/post/[track_id]/like.js delete mode 100755 packages/server/services/music/controllers/tracks/routes/post/[track_id]/refresh-cache.js create mode 100644 packages/server/services/music/routes/music/lyrics/[track_id]/put.js create mode 100644 packages/server/services/posts/classes/posts/methods/deletePollVote.js create mode 100644 packages/server/services/posts/classes/posts/methods/votePoll.js create mode 100644 packages/server/services/posts/routes/posts/[post_id]/vote_poll/[option_id]/delete.js create mode 100644 packages/server/services/posts/routes/posts/[post_id]/vote_poll/[option_id]/post.js diff --git a/packages/app/package.json b/packages/app/package.json index ee5bca54..d7bdc6fb 100755 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -46,7 +46,7 @@ "capacitor-music-controls-plugin-v3": "^1.1.0", "classnames": "2.3.1", "dompurify": "^3.0.0", - "evite": "^0.17.0", + "vessel": "^0.18.0", "fast-average-color": "^9.2.0", "framer-motion": "^10.12.17", "fuse.js": "6.5.3", diff --git a/packages/app/src/App.jsx b/packages/app/src/App.jsx index 8903c8fe..54777471 100755 --- a/packages/app/src/App.jsx +++ b/packages/app/src/App.jsx @@ -2,7 +2,7 @@ import "./patches" import config from "@config" import React from "react" -import { EviteRuntime } from "evite" +import { Runtime } from "vessel" import { Helmet } from "react-helmet" import { Translation } from "react-i18next" import * as Sentry from "@sentry/browser" @@ -109,16 +109,12 @@ class ComtyApp extends React.Component { }, openLoginForm: async (options = {}) => { app.layout.draggable.open("login", Login, { - defaultLocked: options.defaultLocked ?? false, - componentProps: { - sessionController: this.sessionController, - }, props: { - fillEnd: true, - bodyStyle: { - height: "100%", + sessionController: this.sessionController, + onDone: () => { + app.layout.draggable.destroy("login") } - } + }, }) }, openAppsMenu: () => { @@ -464,4 +460,4 @@ class ComtyApp extends React.Component { } } -export default new EviteRuntime(ComtyApp) \ No newline at end of file +export default new Runtime(ComtyApp) \ No newline at end of file diff --git a/packages/app/src/cores/player/classes/TrackInstance.js b/packages/app/src/classes/TrackInstance/index.js similarity index 91% rename from packages/app/src/cores/player/classes/TrackInstance.js rename to packages/app/src/classes/TrackInstance/index.js index 7915308e..fa3650e7 100644 --- a/packages/app/src/cores/player/classes/TrackInstance.js +++ b/packages/app/src/classes/TrackInstance/index.js @@ -1,3 +1,5 @@ +import TrackManifest from "../TrackManifest" + export default class TrackInstance { constructor(player, manifest) { if (!player) { @@ -14,6 +16,8 @@ export default class TrackInstance { return this } + _initialized = false + audio = null contextElement = null @@ -24,57 +28,6 @@ export default class TrackInstance { waitUpdateTimeout = null - resolveManifest = async () => { - if (typeof this.manifest === "string") { - this.manifest = { - src: this.manifest, - } - } - - if (this.manifest.service) { - if (!this.player.service_providers.has(manifest.service)) { - throw new Error(`Service ${manifest.service} is not supported`) - } - - // try to resolve source file - if (this.manifest.service !== "inherit" && !this.manifest.source) { - this.manifest = await this.player.service_providers.resolve(this.manifest.service, this.manifest) - } - } - - if (!this.manifest.source) { - throw new Error("Manifest `source` is required") - } - - if (!this.manifest.metadata) { - this.manifest.metadata = {} - } - - if (!this.manifest.metadata.title) { - this.manifest.metadata.title = this.manifest.source.split("/").pop() - } - - return this.manifest - } - - initialize = async () => { - this.manifest = await this.resolveManifest() - - this.audio = new Audio(this.manifest.source) - - this.audio.signal = this.abortController.signal - this.audio.crossOrigin = "anonymous" - this.audio.preload = "metadata" - - for (const [key, value] of Object.entries(this.mediaEvents)) { - this.audio.addEventListener(key, value) - } - - this.contextElement = this.player.audioContext.createMediaElementSource(this.audio) - - return this - } - mediaEvents = { "ended": () => { this.player.next() @@ -124,4 +77,63 @@ export default class TrackInstance { this.player.eventBus.emit(`player.seeked`, this.audio.currentTime) }, } + + initialize = async () => { + this.manifest = await this.resolveManifest() + + this.audio = new Audio(this.manifest.source) + + this.audio.signal = this.abortController.signal + this.audio.crossOrigin = "anonymous" + this.audio.preload = "metadata" + + for (const [key, value] of Object.entries(this.mediaEvents)) { + this.audio.addEventListener(key, value) + } + + this.contextElement = this.player.audioContext.createMediaElementSource(this.audio) + + this._initialized = true + + return this + } + + resolveManifest = async () => { + if (typeof this.manifest === "string") { + this.manifest = { + src: this.manifest, + } + } + + this.manifest = new TrackManifest(this.manifest) + + this.manifest = await this.manifest.analyzeCoverColor() + + if (this.manifest.service) { + if (!this.player.service_providers.has(manifest.service)) { + throw new Error(`Service ${manifest.service} is not supported`) + } + + // try to resolve source file + if (this.manifest.service !== "inherit" && !this.manifest.source) { + this.manifest = await this.player.service_providers.resolve(this.manifest.service, this.manifest) + } + } + + if (!this.manifest.source) { + throw new Error("Manifest `source` is required") + } + + // set empty metadata if not provided + if (!this.manifest.metadata) { + this.manifest.metadata = {} + } + + // auto name if a title is not provided + if (!this.manifest.metadata.title) { + this.manifest.metadata.title = this.manifest.source.split("/").pop() + } + + return this.manifest + } } \ No newline at end of file diff --git a/packages/app/src/classes/TrackManifest/index.js b/packages/app/src/classes/TrackManifest/index.js new file mode 100644 index 00000000..db75288b --- /dev/null +++ b/packages/app/src/classes/TrackManifest/index.js @@ -0,0 +1,150 @@ +import jsmediatags from "jsmediatags/dist/jsmediatags.min.js" +import { FastAverageColor } from "fast-average-color" + +async function uploadBinaryArrayToStorage(bin, args) { + const { format, data } = bin + + const filenameExt = format.split("/")[1] + const filename = `cover.${filenameExt}` + + const byteArray = new Uint8Array(data) + const blob = new Blob([byteArray], { type: data.type }) + + // create a file object + const file = new File([blob], filename, { + type: format, + }) + + return await app.cores.remoteStorage.uploadFile(file, args) +} + +export default class TrackManifest { + constructor(params) { + this.params = params + + this.uid = params.uid ?? params._id + this._id = params._id + + if (typeof params.cover !== "undefined") { + this.cover = params.cover + } + + if (typeof params.title !== "undefined") { + this.title = params.title + } + + if (typeof params.album !== "undefined") { + this.album = params.album + } + + if (typeof params.artist !== "undefined") { + this.artist = params.artist + } + + if (typeof params.artists !== "undefined" || Array.isArray(params.artists)) { + this.artistStr = params.artists.join(", ") + } + + if (typeof params.source !== "undefined") { + this.source = params.source + } + + if (typeof params.metadata !== "undefined") { + this.metadata = params.metadata + } + + if (typeof params.lyrics_enabled !== "undefined") { + this.lyrics_enabled = params.lyrics_enabled + } + + if (params.cover.startsWith("http")) { + try { + this.analyzeCoverColor() + } catch (error) { + // so bad... + } + } + + return this + } + + uid = null + + cover = "https://storage.ragestudio.net/comty-static-assets/default_song.png" + + title = "Untitled" + + album = "Unknown" + + artist = "Unknown" + + source = null + + metadata = {} + + lyrics_enabled = false + + analyzedMetadata = null + + async initialize() { + if (this.params.file) { + this.metadata = await this.analyzeMetadata(this.params.file.originFileObj) + + if (this.metadata.tags) { + if (this.metadata.tags.title) { + this.title = this.metadata.tags.title + } + + if (this.metadata.tags.artist) { + this.artist = this.metadata.tags.artist + } + + if (this.metadata.tags.album) { + this.album = this.metadata.tags.album + } + + if (this.metadata.tags.picture) { + const coverUpload = await uploadBinaryArrayToStorage(this.metadata.tags.picture) + + this.cover = coverUpload.url + } + + this.handleChanges({ + cover: this.cover, + title: this.title, + artist: this.artist, + album: this.album, + }) + } + } + + return this + } + + handleChanges = (changes) => { + if (typeof this.params.onChange === "function") { + this.params.onChange(this.uid, changes) + } + } + + analyzeMetadata = async (file) => { + return new Promise((resolve, reject) => { + jsmediatags.read(file, { + onSuccess: (data) => { + return resolve(data) + }, + onError: (error) => { + return reject(error) + } + }) + }) + } + + analyzeCoverColor = async () => { + const fac = new FastAverageColor() + + this.cover_analysis = await fac.getColorAsync(this.cover) + + return this + } +} \ No newline at end of file diff --git a/packages/app/src/components/MusicStudio/LyricsEditor/index.less b/packages/app/src/components/BadgeCard/index.jsx similarity index 100% rename from packages/app/src/components/MusicStudio/LyricsEditor/index.less rename to packages/app/src/components/BadgeCard/index.jsx diff --git a/packages/app/src/components/Music/Track/index.jsx b/packages/app/src/components/Music/Track/index.jsx index f7e0c25d..5e43a366 100755 --- a/packages/app/src/components/Music/Track/index.jsx +++ b/packages/app/src/components/Music/Track/index.jsx @@ -7,7 +7,7 @@ import RGBStringToValues from "@utils/rgbToValues" import ImageViewer from "@components/ImageViewer" import { Icons } from "@components/Icons" -import { Context as PlayerContext } from "@contexts/WithPlayerContext" +import { usePlayerStateContext } from "@contexts/WithPlayerContext" import { Context as PlaylistContext } from "@contexts/WithPlaylistContext" import "./index.less" @@ -28,7 +28,7 @@ const Track = (props) => { loading, track_manifest, playback_status, - } = React.useContext(PlayerContext) + } = usePlayerStateContext() const playlist_ctx = React.useContext(PlaylistContext) @@ -186,12 +186,16 @@ const Track = (props) => { { props.track.service === "tidal" && } - {props.track.title} + { + props.track.title + }
- {props.track.artist} + { + Array.isArray(props.track.artists) ? props.track.artists.join(", ") : props.track.artist + }
diff --git a/packages/app/src/components/Music/Track/index.less b/packages/app/src/components/Music/Track/index.less index 4df899ad..043694e2 100755 --- a/packages/app/src/components/Music/Track/index.less +++ b/packages/app/src/components/Music/Track/index.less @@ -99,7 +99,7 @@ html { align-items: center; - padding: 10px; + padding: 6px; } .music-track_actions { @@ -182,11 +182,11 @@ html { overflow: hidden; - width: 50px; - height: 50px; + width: 35px; + height: 35px; - min-width: 50px; - min-height: 50px; + min-width: 35px; + min-height: 35px; img { width: 100%; diff --git a/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/LyricsEditor/index.jsx b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/LyricsEditor/index.jsx new file mode 100644 index 00000000..a85907bd --- /dev/null +++ b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/LyricsEditor/index.jsx @@ -0,0 +1,97 @@ +import React from "react" +import * as antd from "antd" + +import LyricsTextView from "@components/MusicStudio/LyricsTextView" +import UploadButton from "@components/UploadButton" +import { Icons } from "@components/Icons" + +import Languages from "@config/languages" + +import "./index.less" + +const LanguagesMap = Object.entries(Languages).map(([key, value]) => { + return { + label: value, + value: key, + } +}) + +const LyricsEditor = (props) => { + const { langs = {} } = props + const [selectedLang, setSelectedLang] = React.useState("original") + + function handleChange(key, value) { + if (typeof props.onChange !== "function") { + return false + } + + props.onChange(key, value) + } + + function updateCurrentLang(url) { + handleChange("langs", { + ...langs, + [selectedLang]: url + }) + } + + return
+
+

+ + Lyrics +

+ +
+ + Language: + + + (option?.label.toLowerCase() ?? '').includes(input.toLowerCase())} + filterSort={(optionA, optionB) => + (optionA?.label.toLowerCase() ?? '').toLowerCase().localeCompare((optionB?.label.toLowerCase() ?? '').toLowerCase()) + } + onChange={setSelectedLang} + /> + + { + selectedLang && { + updateCurrentLang(data.url) + }} + accept={[ + "text/*" + ]} + /> + } +
+
+ + { + !langs[selectedLang] && + No lyrics uploaded for this language + + } + + { + langs[selectedLang] && + } +
+} + +export default LyricsEditor \ No newline at end of file diff --git a/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/LyricsEditor/index.less b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/LyricsEditor/index.less new file mode 100644 index 00000000..be94fc7b --- /dev/null +++ b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/LyricsEditor/index.less @@ -0,0 +1,11 @@ +.lyrics-editor { + display: flex; + flex-direction: column; + + gap: 20px; + padding: 15px; + + border-radius: 12px; + + background-color: var(--background-color-accent); +} \ No newline at end of file diff --git a/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/VideoEditor/index.jsx b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/VideoEditor/index.jsx new file mode 100644 index 00000000..f8907f5f --- /dev/null +++ b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/VideoEditor/index.jsx @@ -0,0 +1,106 @@ +import React from "react" +import * as antd from "antd" +import dayjs from "dayjs" +import customParseFormat from "dayjs/plugin/customParseFormat" + +import UploadButton from "@components/UploadButton" +import { Icons } from "@components/Icons" +import VideoPlayer from "@components/VideoPlayer" + +import "./index.less" + +dayjs.extend(customParseFormat) + +const VideoEditor = (props) => { + function handleChange(key, value) { + if (typeof props.onChange !== "function") { + return false + } + + props.onChange(key, value) + } + + return
+

+ + Video +

+ + { + (!props.videoSourceURL) && } + description="No video" + /> + } + + { + props.videoSourceURL &&
+ +
+ } + +
+
+ + + Start video sync at + + + {props.startSyncAt ?? "not set"} +
+ +
+ Set to: + + { + handleChange("startSyncAt", str) + }} + /> +
+
+ +
+ { + handleChange("videoSourceURL", response.url) + }} + accept={[ + "video/*", + ]} + headers={{ + "transmux": "mq-hls", + }} + disabled={props.loading} + > + Upload video + + + or + + { + handleChange("videoSourceURL", e.target.value) + }} + value={props.videoSourceURL} + disabled={props.loading} + /> +
+
+} + +export default VideoEditor \ No newline at end of file diff --git a/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/VideoEditor/index.less b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/VideoEditor/index.less new file mode 100644 index 00000000..6def957e --- /dev/null +++ b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/VideoEditor/index.less @@ -0,0 +1,25 @@ +.video-editor { + display: flex; + flex-direction: column; + + gap: 20px; + padding: 15px; + + border-radius: 12px; + + background-color: var(--background-color-accent); + + .video-editor-actions { + display: flex; + flex-direction: row; + + align-items: center; + + gap: 10px; + } + + .video-editor-preview { + width: 100%; + height: 350px; + } +} \ No newline at end of file diff --git a/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/index.jsx b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/index.jsx new file mode 100644 index 00000000..16bcad2b --- /dev/null +++ b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/index.jsx @@ -0,0 +1,120 @@ +import React from "react" +import { Skeleton } from "antd" + +import VideoEditor from "./components/VideoEditor" +import LyricsEditor from "./components/LyricsEditor" + +import MusicModel from "@models/music" + +import ReleaseEditorStateContext from "@contexts/MusicReleaseEditor" + +import "./index.less" + +class EnhancedLyricsEditor extends React.Component { + static contextType = ReleaseEditorStateContext + + state = { + data: {}, + loading: true, + submitting: false, + videoOptions: {}, + lyricsOptions: {} + } + + componentDidMount = async () => { + this.setState({ + loading: true + }) + + this.context.setCustomPageActions([ + { + label: "Save", + icon: "FiSave", + onClick: this.submitChanges, + } + ]) + + const data = await MusicModel.getTrackLyrics(this.props.track._id).catch((err) => { + return null + }) + + if (data) { + this.setState({ + videoOptions: { + videoSourceURL: data.video_source, + startSyncAt: data.sync_audio_at + }, + lyricsOptions: { + langs: data.lrc + } + }) + } + + this.setState({ + loading: false + }) + } + + submitChanges = async () => { + this.setState({ + submitting: true + }) + + console.log(`Submitting changes with values >`, { + ...this.state.videoOptions, + ...this.state.lyricsOptions + }) + + await MusicModel.putTrackLyrics(this.props.track._id, { + video_source: this.state.videoOptions.videoSourceURL, + sync_audio_at: this.state.videoOptions.startSyncAt, + lrc: this.state.lyricsOptions.langs + }).catch((err) => { + console.error(err) + app.message.error("Failed to update enhanced lyrics") + }) + + this.setState({ + submitting: false + }) + } + + render() { + if (this.state.loading) { + return + } + + return
+

{this.props.track.title}

+ + { + this.setState({ + videoOptions: { + ...this.state.videoOptions, + [key]: value + } + }) + }} + /> + + { + this.setState({ + lyricsOptions: { + ...this.state.lyricsOptions, + [key]: value + } + }) + }} + /> +
+ } +} + +export default EnhancedLyricsEditor \ No newline at end of file diff --git a/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/index.less b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/index.less new file mode 100644 index 00000000..84319bb9 --- /dev/null +++ b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/index.less @@ -0,0 +1,6 @@ +.enhanced_lyrics_editor-wrapper { + display: flex; + flex-direction: column; + + gap: 20px; +} \ No newline at end of file diff --git a/packages/app/src/components/MusicStudio/LyricsEditor/index.jsx b/packages/app/src/components/MusicStudio/LyricsEditor/index.jsx deleted file mode 100644 index e46bbd81..00000000 --- a/packages/app/src/components/MusicStudio/LyricsEditor/index.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from "react" -import * as antd from "antd" - -import LyricsTextView from "../LyricsTextView" -import UploadButton from "@components/UploadButton" -import { Icons } from "@components/Icons" - -import MusicService from "@models/music" - -import Languages from "@config/languages" - -const LanguagesMap = Object.entries(Languages).map(([key, value]) => { - return { - label: value, - value: key, - } -}) - -import "./index.less" - -const LyricsEditor = (props) => { - const [L_TrackLyrics, R_TrackLyrics, E_TrackLyrics, F_TrackLyrics] = app.cores.api.useRequest(MusicService.getTrackLyrics, props.track._id) - - const [langs, setLangs] = React.useState([]) - const [selectedLang, setSelectedLang] = React.useState("original") - - async function onUploadLRC(uid, data) { - const { url } = data - - setLangs((prev) => { - const index = prev.findIndex((lang) => { - return lang.id === selectedLang - }) - - console.log(`Replacing value for id [${selectedLang}] at index [${index}]`) - - if (index !== -1) { - prev[index].value = url - } else { - const lang = LanguagesMap.find((lang) => { - return lang.value === selectedLang - }) - - prev.push({ - id: lang.value, - name: lang.label, - value: url - }) - } - - console.log(`new value =>`, prev) - - return prev - }) - } - - React.useEffect(() => { - if (R_TrackLyrics) { - if (R_TrackLyrics.available_langs) { - setLangs(R_TrackLyrics.available_langs) - } - } - console.log(R_TrackLyrics) - }, [R_TrackLyrics]) - - const currentLangData = selectedLang && langs.find((lang) => { - return lang.id === selectedLang - }) - - console.log(langs, currentLangData) - - return
-

Lyrics

- - (option?.label.toLowerCase() ?? '').includes(input.toLowerCase())} - filterSort={(optionA, optionB) => - (optionA?.label.toLowerCase() ?? '').toLowerCase().localeCompare((optionB?.label.toLowerCase() ?? '').toLowerCase()) - } - onChange={setSelectedLang} - /> - - - {selectedLang} - - - { - selectedLang && - } - - { - currentLangData && currentLangData?.value && - } - { - !currentLangData || !currentLangData?.value && - } -
-} - -export default LyricsEditor \ No newline at end of file diff --git a/packages/app/src/components/MusicStudio/LyricsTextView/index.jsx b/packages/app/src/components/MusicStudio/LyricsTextView/index.jsx index 05f5f728..b30828b7 100644 --- a/packages/app/src/components/MusicStudio/LyricsTextView/index.jsx +++ b/packages/app/src/components/MusicStudio/LyricsTextView/index.jsx @@ -2,8 +2,10 @@ import React from "react" import * as antd from "antd" import axios from "axios" +import "./index.less" + const LyricsTextView = (props) => { - const { lang, track } = props + const { lrcURL } = props const [loading, setLoading] = React.useState(false) const [error, setError] = React.useState(null) @@ -24,19 +26,19 @@ const LyricsTextView = (props) => { return null }) - + if (data) { - setLyrics(data.data) + setLyrics(data.data.split("\n")) } setLoading(false) } React.useEffect(() => { - getLyrics(lang.value) - }, [lang]) + getLyrics(lrcURL) + }, [lrcURL]) - if (!lang) { + if (!lrcURL) { return null } @@ -52,8 +54,21 @@ const LyricsTextView = (props) => { return } - return
-

{lyrics}

+ if (!lyrics) { + return

No lyrics provided

+ } + + return
+ { + lyrics?.map((line, index) => { + return
+ {line} +
+ }) + }
} diff --git a/packages/app/src/components/MusicStudio/LyricsTextView/index.less b/packages/app/src/components/MusicStudio/LyricsTextView/index.less new file mode 100644 index 00000000..9abe5f1b --- /dev/null +++ b/packages/app/src/components/MusicStudio/LyricsTextView/index.less @@ -0,0 +1,15 @@ +.lyrics-text-view { + display: flex; + flex-direction: column; + + gap: 10px; + + .lyrics-text-view-line { + display: flex; + flex-direction: row; + + align-items: center; + + gap: 10px; + } +} \ No newline at end of file diff --git a/packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx b/packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx index 28987f69..f8f7a60a 100644 --- a/packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx +++ b/packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx @@ -1,31 +1,39 @@ import React from "react" import * as antd from "antd" -import { Icons } from "@components/Icons" +import { Icons, createIconRender } from "@components/Icons" import MusicModel from "@models/music" +import useUrlQueryActiveKey from "@hooks/useUrlQueryActiveKey" + +import TrackManifest from "@classes/TrackManifest" + import { DefaultReleaseEditorState, ReleaseEditorStateContext } from "@contexts/MusicReleaseEditor" import Tabs from "./tabs" import "./index.less" -console.log(MusicModel.deleteRelease) - const ReleaseEditor = (props) => { const { release_id } = props const basicInfoRef = React.useRef() const [submitting, setSubmitting] = React.useState(false) + const [loading, setLoading] = React.useState(true) const [submitError, setSubmitError] = React.useState(null) - const [loading, setLoading] = React.useState(true) const [loadError, setLoadError] = React.useState(null) const [globalState, setGlobalState] = React.useState(DefaultReleaseEditorState) - const [selectedTab, setSelectedTab] = React.useState("info") + const [customPage, setCustomPage] = React.useState(null) + const [customPageActions, setCustomPageActions] = React.useState([]) + + const [selectedTab, setSelectedTab] = useUrlQueryActiveKey({ + defaultKey: "info", + queryKey: "tab" + }) async function initialize() { setLoading(true) @@ -33,7 +41,13 @@ const ReleaseEditor = (props) => { if (release_id !== "new") { try { - const releaseData = await MusicModel.getReleaseData(release_id) + let releaseData = await MusicModel.getReleaseData(release_id) + + if (Array.isArray(releaseData.list)) { + releaseData.list = releaseData.list.map((item) => { + return new TrackManifest(item) + }) + } setGlobalState({ ...globalState, @@ -47,21 +61,23 @@ const ReleaseEditor = (props) => { setLoading(false) } + async function renderCustomPage(page, actions) { + setCustomPage(page ?? null) + setCustomPageActions(actions ?? []) + } + async function handleSubmit() { setSubmitting(true) setSubmitError(null) try { // first sumbit tracks - console.time("submit:tracks:") const tracks = await MusicModel.putTrack({ list: globalState.list, }) - console.timeEnd("submit:tracks:") // then submit release - console.time("submit:release:") - await MusicModel.putRelease({ + const result = await MusicModel.putRelease({ _id: globalState._id, title: globalState.title, description: globalState.description, @@ -71,7 +87,8 @@ const ReleaseEditor = (props) => { type: globalState.type, list: tracks.list, }) - console.timeEnd("submit:release:") + + app.location.push(`/studio/music/${result._id}`) } catch (error) { console.error(error) app.message.error(error.message) @@ -84,8 +101,6 @@ const ReleaseEditor = (props) => { setSubmitting(false) app.message.success("Release saved") - - return release } async function handleDelete() { @@ -99,10 +114,6 @@ const ReleaseEditor = (props) => { }) } - async function onFinish(values) { - console.log(values) - } - async function canFinish() { return true } @@ -125,10 +136,18 @@ const ReleaseEditor = (props) => { const Tab = Tabs.find(({ key }) => key === selectedTab) + const CustomPageProps = { + close: () => { + renderCustomPage(null, null) + } + } + return
@@ -139,29 +158,47 @@ const ReleaseEditor = (props) => {
} - onClick={() => setCustomPage(null)} + onClick={() => renderCustomPage(null, null)} />

{customPage.header}

{ - customPage.props?.onSave && } - onClick={() => customPage.props.onSave()} - > - Save - + Array.isArray(customPageActions) && customPageActions.map((action, index) => { + return { + if (typeof action.onClick === "function") { + await action.onClick() + } + + if (action.fireEvent) { + app.eventBus.emit(action.fireEvent) + } + }} + disabled={action.disabled} + > + {action.label} + + }) }
} { - React.cloneElement(customPage.content, { - ...customPage.props, - close: () => setCustomPage(null), - }) + customPage.content && (React.isValidElement(customPage.content) ? + React.cloneElement(customPage.content, { + ...CustomPageProps, + ...customPage.props + }) : + React.createElement(customPage.content, { + ...CustomPageProps, + ...customPage.props + }) + ) }
} @@ -179,11 +216,11 @@ const ReleaseEditor = (props) => { } + icon={release_id !== "new" ? : } disabled={submitting || loading || !canFinish()} loading={submitting} > - Save + {release_id !== "new" ? "Save" : "Release"} { @@ -208,6 +245,12 @@ const ReleaseEditor = (props) => {
+ { + submitError && + } { !Tab && { { Tab && React.createElement(Tab.render, { release: globalState, - onFinish: onFinish, state: globalState, setState: setGlobalState, diff --git a/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/components/TrackListItem/index.jsx b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/components/TrackListItem/index.jsx index 9071c577..3f75fa4a 100644 --- a/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/components/TrackListItem/index.jsx +++ b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/components/TrackListItem/index.jsx @@ -20,17 +20,19 @@ const TrackListItem = (props) => { const { track } = props async function onClickEditTrack() { - context.setCustomPage({ + context.renderCustomPage({ header: "Track Editor", - content: , + content: , props: { - onSave: (newTrackData) => { - console.log("Saving track", newTrackData) - }, + track: track, } }) } + async function onClickRemoveTrack() { + props.onDelete(track.uid) + } + return { "music-studio-release-editor-tracks-list-item", { ["loading"]: loading, - ["failed"]: !!error + ["failed"]: !!error, + ["disabled"]: props.disabled, } )} ref={provided.innerRef} {...provided.draggableProps} > +
+
{props.index + 1}
@@ -65,10 +75,23 @@ const TrackListItem = (props) => { {track.title}
+ + } + disabled={props.disabled} + /> + } onClick={onClickEditTrack} + disabled={props.disabled} />
{ - return new Promise((resolve, reject) => { - jsmediatags.read(file, { - onSuccess: (data) => { - return resolve(data) - }, - onError: (error) => { - return reject(error) - } - }) - }) - } -} - class TracksManager extends React.Component { state = { - list: [], + list: Array.isArray(this.props.list) ? this.props.list : [], pendingUploads: [], } - componentDidMount() { - if (typeof this.props.list !== "undefined" && Array.isArray(this.props.list)) { - this.setState({ - list: this.props.list - }) - } - } - componentDidUpdate = (prevProps, prevState) => { - if (prevState.list !== this.state.list || prevState.pendingUploads !== this.state.pendingUploads) { + if (prevState.list !== this.state.list) { if (typeof this.props.onChangeState === "function") { this.props.onChangeState(this.state) } @@ -158,12 +49,16 @@ class TracksManager extends React.Component { return false } + this.removeTrackUIDFromPendingUploads(uid) + this.setState({ list: this.state.list.filter((item) => item.uid !== uid), }) + } modifyTrackByUid = (uid, track) => { + console.log("modifyTrackByUid", uid, track) if (!uid || !track) { return false } @@ -187,9 +82,17 @@ class TracksManager extends React.Component { return false } - if (!this.state.pendingUploads.includes(uid)) { + const pendingUpload = this.state.pendingUploads.find((item) => item.uid === uid) + + if (!pendingUpload) { this.setState({ - pendingUploads: [...this.state.pendingUploads, uid], + pendingUploads: [ + ...this.state.pendingUploads, + { + uid: uid, + progress: 0 + } + ], }) } } @@ -200,13 +103,43 @@ class TracksManager extends React.Component { } this.setState({ - pendingUploads: this.state.pendingUploads.filter((item) => item !== uid), + pendingUploads: this.state.pendingUploads.filter((item) => item.uid !== uid), + }) + } + + getUploadProgress = (uid) => { + const uploadProgressIndex = this.state.pendingUploads.findIndex((item) => item.uid === uid) + + if (uploadProgressIndex === -1) { + return 0 + } + + return this.state.pendingUploads[uploadProgressIndex].progress + } + + updateUploadProgress = (uid, progress) => { + const uploadProgressIndex = this.state.pendingUploads.findIndex((item) => item.uid === uid) + + if (uploadProgressIndex === -1) { + return false + } + + const newData = [...this.state.pendingUploads] + + newData[uploadProgressIndex].progress = progress + + console.log(`Updating progress for [${uid}] to [${progress}]`) + + this.setState({ + pendingUploads: newData, }) } handleUploaderStateChange = async (change) => { const uid = change.file.uid + console.log("handleUploaderStateChange", change) + switch (change.file.status) { case "uploading": { this.addTrackUIDToPendingUploads(uid) @@ -214,23 +147,20 @@ class TracksManager extends React.Component { const trackManifest = new TrackManifest({ uid: uid, file: change.file, + onChange: this.modifyTrackByUid }) this.addTrackToList(trackManifest) - const trackData = await trackManifest.initialize() - - this.modifyTrackByUid(uid, trackData) - break } case "done": { // remove pending file this.removeTrackUIDFromPendingUploads(uid) - const trackIndex = this.state.list.findIndex((item) => item.uid === uid) + const trackManifest = this.state.list.find((item) => item.uid === uid) - if (trackIndex === -1) { + if (!trackManifest) { console.error(`Track with uid [${uid}] not found!`) break } @@ -240,6 +170,8 @@ class TracksManager extends React.Component { source: change.file.response.url }) + await trackManifest.initialize() + break } case "error": { @@ -278,7 +210,7 @@ class TracksManager extends React.Component { } handleTrackFileUploadProgress = async (file, progress) => { - console.log(file, progress) + this.updateUploadProgress(file.uid, progress) } orderTrackList = (result) => { @@ -301,6 +233,7 @@ class TracksManager extends React.Component { render() { console.log(`Tracks List >`, this.state.list) + return
{ + const progress = this.getUploadProgress(track.uid) + return 0} /> }) } diff --git a/packages/app/src/components/MusicStudio/TrackEditor/index.jsx b/packages/app/src/components/MusicStudio/TrackEditor/index.jsx index a3faa8cc..d73f48ba 100644 --- a/packages/app/src/components/MusicStudio/TrackEditor/index.jsx +++ b/packages/app/src/components/MusicStudio/TrackEditor/index.jsx @@ -3,9 +3,7 @@ import * as antd from "antd" import CoverEditor from "@components/CoverEditor" import { Icons } from "@components/Icons" - -import LyricsEditor from "@components/MusicStudio/LyricsEditor" -import VideoEditor from "@components/MusicStudio/VideoEditor" +import EnhancedLyricsEditor from "@components/MusicStudio/EnhancedLyricsEditor" import { ReleaseEditorStateContext } from "@contexts/MusicReleaseEditor" @@ -24,43 +22,48 @@ const TrackEditor = (props) => { }) } - async function openLyricsEditor() { - context.setCustomPage({ - header: "Lyrics Editor", - content: , + async function openEnhancedLyricsEditor() { + context.renderCustomPage({ + header: "Enhanced Lyrics", + content: EnhancedLyricsEditor, props: { - onSave: () => { - console.log("Saved lyrics") - }, + track: track, } }) } - async function openVideoEditor() { - context.setCustomPage({ - header: "Video Editor", - content: , - props: { - onSave: () => { - console.log("Saved video") - }, + async function handleOnSave() { + setTrack((prev) => { + const listData = [...context.list] + + const trackIndex = listData.findIndex((item) => item.uid === prev.uid) + + if (trackIndex === -1) { + return prev } + + listData[trackIndex] = prev + + context.setGlobalState({ + ...context, + list: listData + }) + + return prev }) } - async function onClose() { - if (typeof props.close === "function") { - props.close() - } - } - - async function onSave() { - await props.onSave(track) - - if (typeof props.close === "function") { - props.close() - } - } + React.useEffect(() => { + context.setCustomPageActions([ + { + label: "Save", + icon: "FiSave", + type: "primary", + onClick: handleOnSave, + disabled: props.track === track, + }, + ]) + }, [track]) return
@@ -131,49 +134,32 @@ const TrackEditor = (props) => { />
- -
- - Edit Video + + Enhanced Lyrics + + handleChange("lyrics_enabled", value)} + disabled={!track.params._id} + />
- - Edit - -
+
+ + Edit + -
-
- - Edit Lyrics + { + !track.params._id && + You cannot edit Video and Lyrics without release first + + }
- - - Edit - -
- -
-
- - Timestamps -
- - - Edit -
} diff --git a/packages/app/src/components/MusicStudio/TrackEditor/index.less b/packages/app/src/components/MusicStudio/TrackEditor/index.less index f6570bb8..1b8bdacb 100644 --- a/packages/app/src/components/MusicStudio/TrackEditor/index.less +++ b/packages/app/src/components/MusicStudio/TrackEditor/index.less @@ -35,6 +35,8 @@ justify-content: flex-start; align-items: center; + gap: 7px; + width: 100%; h3 { @@ -46,8 +48,8 @@ display: flex; flex-direction: row; + justify-content: flex-start; align-items: center; - justify-content: center; gap: 10px; diff --git a/packages/app/src/components/MusicStudio/VideoEditor/index.jsx b/packages/app/src/components/MusicStudio/VideoEditor/index.jsx deleted file mode 100644 index f95f1ef4..00000000 --- a/packages/app/src/components/MusicStudio/VideoEditor/index.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react" -import * as antd from "antd" - -import { Icons } from "@components/Icons" - -import "./index.less" - -const VideoEditor = (props) => { - return
- -
-} - -export default VideoEditor \ No newline at end of file diff --git a/packages/app/src/components/Player/BackgroundMediaPlayer/index.jsx b/packages/app/src/components/Player/BackgroundMediaPlayer/index.jsx deleted file mode 100755 index 4726edff..00000000 --- a/packages/app/src/components/Player/BackgroundMediaPlayer/index.jsx +++ /dev/null @@ -1,190 +0,0 @@ -import React from "react" -import * as antd from "antd" -import classnames from "classnames" -import Marquee from "react-fast-marquee" - -import { Icons } from "@components/Icons" - -import { WithPlayerContext, Context } from "@contexts/WithPlayerContext" - -import "./index.less" - -function RGBStringToValues(rgbString) { - if (!rgbString) { - return [0, 0, 0] - } - - const rgb = rgbString.replace("rgb(", "").replace(")", "").split(",").map((v) => parseInt(v)) - - return [rgb[0], rgb[1], rgb[2]] -} - -export default (props) => { - return - - -} - -export class BackgroundMediaPlayer extends React.Component { - static contextType = Context - - state = { - expanded: false, - } - - events = { - "sidebar.expanded": (to) => { - if (!to) { - this.toggleExpand(false) - } - } - } - - onClickMinimize = () => { - app.cores.player.minimize() - } - - toggleExpand = (to) => { - if (typeof to !== "boolean") { - to = !this.state.expanded - } - - this.setState({ - expanded: to - }) - } - - componentDidMount = async () => { - for (const [event, handler] of Object.entries(this.events)) { - app.eventBus.on(event, handler) - } - } - - componentWillUnmount() { - for (const [event, handler] of Object.entries(this.events)) { - app.eventBus.off(event, handler) - } - } - - render() { - return
  • -
    - -
    - - - -
    -
    - } - onClick={app.cores.player.playback.previous} - /> - - : } - onClick={app.cores.player.playback.toggle} - /> - - } - onClick={app.cores.player.playback.next} - /> - - } - onClick={this.onClickMinimize} - /> -
    -
  • - } -} \ No newline at end of file diff --git a/packages/app/src/components/Player/BackgroundMediaPlayer/index.less b/packages/app/src/components/Player/BackgroundMediaPlayer/index.less deleted file mode 100755 index eaf1dc7a..00000000 --- a/packages/app/src/components/Player/BackgroundMediaPlayer/index.less +++ /dev/null @@ -1,239 +0,0 @@ -@import "@styles/animations.less"; - -.background_media_player { - position: relative; - - display: flex; - flex-direction: column; - - align-items: center; - justify-content: center; - - width: 100%; - height: 40px; - - padding: 10px; - - overflow: hidden; - - border-radius: 12px; - - transition: all 150ms ease-in-out; - - line-height: 40px; - - color: var(--text-color-white); - - background-repeat: no-repeat; - - background-size: cover; - - &.expanded { - height: 120px; - - .background_media_player__background { - opacity: 0.4; - } - - .background_media_player__icon { - width: 0; - opacity: 0; - } - - .background_media_player__title { - padding: 10px 0; - - h4 { - word-break: break-all; - word-wrap: break-word; - white-space: break-spaces; - - font-size: 1rem; - } - - p { - height: 100%; - } - } - - .background_media_player__controls { - margin-top: 10px; - background-color: var(--text-color-white); - } - } - - &.lightBackground { - color: var(--text-color-black); - - .background_media_player__icon { - svg { - color: var(--text-color-black); - } - } - - .background_media_player__title { - color: var(--text-color-black); - - h4, - p { - color: var(--text-color-black); - } - } - - .background_media_player__controls { - color: var(--text-color-black); - - .ant-btn { - color: var(--text-color-black); - - svg { - color: var(--text-color-black); - } - } - } - } - - .background_media_player__background { - pointer-events: none; - - position: absolute; - top: 0; - left: 0; - - width: 100%; - height: 100%; - - border-radius: 12px; - - background-position: center; - background-size: cover; - background-repeat: no-repeat; - - transition: all 150ms ease-in-out; - - opacity: 0; - } - - .background_media_player__row { - display: flex; - flex-direction: row; - - align-items: center; - justify-content: center; - - z-index: 350; - - width: 100%; - - &.hidden { - pointer-events: none; - opacity: 0; - height: 0; - } - } - - .background_media_player__icon { - svg { - margin-right: 0 !important; - color: var(--text-color-white); - } - - &.bounce { - animation: bounce 1s infinite; - } - } - - .background_media_player__title { - display: flex; - flex-direction: column; - - justify-content: center; - - max-height: 67px; - - height: 100%; - - transition: all 150ms ease-in-out; - - color: var(--text-color-white); - - width: 100%; - height: 100%; - - overflow: hidden; - - //gap: 16px; - - h4 { - line-height: 1rem; - - font-size: 0.8rem; - font-weight: 600; - - margin: 0 0 0 10px; - - height: 100%; - - overflow: hidden; - - white-space: nowrap; - text-overflow: ellipsis; - - font-family: "Space Grotesk", sans-serif; - color: var(--text-color-white); - } - - p { - line-height: 0.8rem; - - font-size: 0.7rem; - font-weight: 400; - - margin: 0 0 0 10px; - - height: 0; - - justify-content: flex-end; - - font-family: "Space Grotesk", sans-serif; - color: var(--text-color-white); - } - - .marquee-container { - width: 100%; - - .overlay { - width: 100%; - } - } - } - - .background_media_player__controls { - display: flex; - flex-direction: row; - - align-self: flex-end; - align-items: center; - justify-content: space-evenly; - - width: 100%; - - color: var(--text-color-white); - - background-color: var(--text-color-black); - - border-radius: 12px; - - opacity: 0.5; - - .ant-btn { - color: var(--text-color-black); - - background-color: transparent; - - svg { - color: var(--text-color-black); - } - } - } -} \ No newline at end of file diff --git a/packages/app/src/components/Player/Controls/index.jsx b/packages/app/src/components/Player/Controls/index.jsx index fc9a09af..0681cc47 100755 --- a/packages/app/src/components/Player/Controls/index.jsx +++ b/packages/app/src/components/Player/Controls/index.jsx @@ -9,7 +9,7 @@ import LikeButton from "@components/LikeButton" import AudioVolume from "@components/Player/AudioVolume" import AudioPlayerChangeModeButton from "@components/Player/ChangeModeButton" -import { Context } from "@contexts/WithPlayerContext" +import { usePlayerStateContext } from "@contexts/WithPlayerContext" import "./index.less" @@ -17,9 +17,6 @@ const EventsHandlers = { "playback": () => { return app.cores.player.playback.toggle() }, - "like": async (ctx) => { - await app.cores.player.toggleCurrentTrackLike(!ctx.track_manifest?.liked) - }, "previous": () => { return app.cores.player.playback.previous() }, @@ -27,98 +24,96 @@ const EventsHandlers = { return app.cores.player.playback.next() }, "volume": (ctx, value) => { - return app.cores.player.volume(value) + return app.cores.player.controls.volume(value) }, "mute": () => { - return app.cores.player.toggleMute() - } + return app.cores.player.controls.mute("toggle") + }, + "like": async (ctx) => { + await app.cores.player.toggleCurrentTrackLike(!ctx.track_manifest?.liked) + }, } const Controls = (props) => { - try { - const ctx = React.useContext(Context) + const playerState = usePlayerStateContext() - const handleAction = (event, ...args) => { - if (typeof EventsHandlers[event] !== "function") { - throw new Error(`Unknown event "${event}"`) - } - - return EventsHandlers[event](ctx, ...args) + const handleAction = (event, ...args) => { + if (typeof EventsHandlers[event] !== "function") { + throw new Error(`Unknown event "${event}"`) } - return
    - - } - onClick={() => handleAction("previous")} - disabled={ctx.control_locked} - /> - : ctx.playback_status === "playing" ? : } - onClick={() => handleAction("playback")} - className="playButton" - disabled={ctx.control_locked} - > - { - ctx.loading &&
    - -
    - } -
    - } - onClick={() => handleAction("next")} - disabled={ctx.control_locked} - /> - { - app.isMobile && handleAction("like")} - liked={ctx.track_manifest?.liked} - /> - } - { - !app.isMobile && handleAction("volume", value), - defaultValue: ctx.volume - } - )} - trigger="hover" - > - - - } -
    - } catch (error) { - console.error(error) - return null + return EventsHandlers[event](playerState, ...args) } + + return
    + + } + onClick={() => handleAction("previous")} + disabled={playerState.control_locked} + /> + : playerState.playback_status === "playing" ? : } + onClick={() => handleAction("playback")} + className="playButton" + disabled={playerState.control_locked} + > + { + playerState.loading &&
    + +
    + } +
    + } + onClick={() => handleAction("next")} + disabled={playerState.control_locked} + /> + { + app.isMobile && handleAction("like")} + liked={playerState.track_manifest?.liked} + /> + } + { + !app.isMobile && handleAction("volume", value), + defaultValue: playerState.volume + } + )} + trigger="hover" + > + + + } +
    } export default Controls \ No newline at end of file diff --git a/packages/app/src/components/Player/ExtraActions/index.jsx b/packages/app/src/components/Player/ExtraActions/index.jsx index 4f14285e..b75c876a 100755 --- a/packages/app/src/components/Player/ExtraActions/index.jsx +++ b/packages/app/src/components/Player/ExtraActions/index.jsx @@ -4,13 +4,13 @@ import { Button } from "antd" import { Icons } from "@components/Icons" import LikeButton from "@components/LikeButton" -import { Context } from "@contexts/WithPlayerContext" +import { usePlayerStateContext } from "@contexts/WithPlayerContext" const ExtraActions = (props) => { - const ctx = React.useContext(Context) + const playerState = usePlayerStateContext() const handleClickLike = async () => { - await app.cores.player.toggleCurrentTrackLike(!ctx.track_manifest?.liked) + await app.cores.player.toggleCurrentTrackLike(!playerState.track_manifest?.liked) } return
    @@ -18,12 +18,12 @@ const ExtraActions = (props) => { app.isMobile &&
    diff --git a/packages/app/src/components/Player/ToolBarPlayer/index.jsx b/packages/app/src/components/Player/ToolBarPlayer/index.jsx index c042180c..e91bfe5e 100755 --- a/packages/app/src/components/Player/ToolBarPlayer/index.jsx +++ b/packages/app/src/components/Player/ToolBarPlayer/index.jsx @@ -4,7 +4,7 @@ import Marquee from "react-fast-marquee" import classnames from "classnames" import { Icons } from "@components/Icons" -import { WithPlayerContext, Context } from "@contexts/WithPlayerContext" +import { usePlayerStateContext } from "@contexts/WithPlayerContext" import SeekBar from "@components/Player/SeekBar" import Controls from "@components/Player/Controls" @@ -43,7 +43,7 @@ const ServiceIndicator = (props) => { } const Player = (props) => { - const ctx = React.useContext(Context) + const playerState = usePlayerStateContext() const contentRef = React.useRef() const titleRef = React.useRef() @@ -64,16 +64,16 @@ const Player = (props) => { const { title, album, - artist, + artistStr, liked, service, - lyricsEnabled, + lyrics_enabled, cover_analysis, cover, - } = ctx.track_manifest ?? {} + } = playerState.track_manifest ?? {} - const playing = ctx.playback_status === "playing" - const stopped = ctx.playback_status === "stopped" + const playing = playerState.playback_status === "playing" + const stopped = playerState.playback_status === "stopped" const titleText = (!playing && stopped) ? "Stopped" : (title ?? "Untitled") const subtitleText = "" @@ -89,7 +89,7 @@ const Player = (props) => { "toolbar_player_wrapper", { "hover": topActionsVisible, - "minimized": ctx.minimized, + "minimized": playerState.minimized, "cover_light": cover_analysis?.isLight, } )} @@ -106,7 +106,7 @@ const Player = (props) => { )} > { - !ctx.control_locked && } shape="circle" @@ -114,7 +114,7 @@ const Player = (props) => { } { - lyricsEnabled && } shape="circle" onClick={() => app.location.push("/lyrics")} @@ -170,7 +170,7 @@ const Player = (props) => { titleOverflown &&

    { }

    - {artist ?? ""} + {artistStr ?? ""}

    @@ -193,10 +193,10 @@ const Player = (props) => { @@ -206,10 +206,4 @@ const Player = (props) => { } -const PlayerContextHandler = () => { - return - - -} - -export default PlayerContextHandler \ No newline at end of file +export default Player \ No newline at end of file diff --git a/packages/app/src/components/PostCard/components/actions/index.jsx b/packages/app/src/components/PostCard/components/actions/index.jsx index ed07a105..6cbebbfd 100755 --- a/packages/app/src/components/PostCard/components/actions/index.jsx +++ b/packages/app/src/components/PostCard/components/actions/index.jsx @@ -57,7 +57,7 @@ const MoreActionsItems = [ }, ] -export default (props) => { +const PostActions = (props) => { const [isSelf, setIsSelf] = React.useState(false) const { @@ -128,4 +128,6 @@ export default (props) => { -} \ No newline at end of file +} + +export default PostActions \ No newline at end of file diff --git a/packages/app/src/components/UploadButton/index.jsx b/packages/app/src/components/UploadButton/index.jsx index 9163993f..0a7caee1 100755 --- a/packages/app/src/components/UploadButton/index.jsx +++ b/packages/app/src/components/UploadButton/index.jsx @@ -41,6 +41,7 @@ export default (props) => { handleOnStart(req.file.uid, req.file) await app.cores.remoteStorage.uploadFile(req.file, { + headers: props.headers, onProgress: (file, progress) => { setProgess(progress) handleOnProgress(file.uid, progress) diff --git a/packages/app/src/components/VideoPlayer/index.jsx b/packages/app/src/components/VideoPlayer/index.jsx new file mode 100644 index 00000000..4c4bdf7d --- /dev/null +++ b/packages/app/src/components/VideoPlayer/index.jsx @@ -0,0 +1,79 @@ +import React from "react" +import HLS from "hls.js" +import Plyr from "plyr" + +import "plyr-react/dist/plyr.css" + +import "./index.less" + +const VideoPlayer = (props) => { + const videoRef = React.createRef() + + const [initializing, setInitializing] = React.useState(true) + const [player, setPlayer] = React.useState(null) + const [hls, setHls] = React.useState(null) + + React.useEffect(() => { + setInitializing(true) + + const hlsInstance = new HLS() + const plyrInstance = new Plyr(videoRef.current, { + controls: props.controls ?? [ + "current-time", + "mute", + "volume", + "captions", + "settings", + "pip", + "airplay", + "fullscreen" + ], + settings: ["quality", "speed"], + quality: { + default: 1080, + options: [ + { label: "Auto", value: "auto" }, + { label: "1080p", value: 1080 }, + { label: "720p", value: 720 }, + { label: "480p", value: 480 }, + { label: "360p", value: 360 }, + { label: "240p", value: 240 }, + ] + } + }) + + setHls(hlsInstance) + setPlayer(plyrInstance) + + hlsInstance.attachMedia(videoRef.current) + hlsInstance.loadSource(props.src) + + hlsInstance.on(HLS.Events.MANIFEST_PARSED, (event, data) => { + console.log(event, data) + + plyrInstance.set + }) + + setInitializing(false) + + return () => { + hlsInstance.destroy() + } + }, []) + + React.useEffect(() => { + if (hls) { + hls.loadSource(props.src) + } + }, [props.src]) + + return
    +
    +} + +export default VideoPlayer \ No newline at end of file diff --git a/packages/app/src/components/VideoPlayer/index.less b/packages/app/src/components/VideoPlayer/index.less new file mode 100644 index 00000000..607e09ae --- /dev/null +++ b/packages/app/src/components/VideoPlayer/index.less @@ -0,0 +1,16 @@ +.video-player { + display: flex; + flex-direction: column; + + width: 100%; + height: 100%; + + overflow: hidden; + + border-radius: 12px; + + .video-player-component { + width: 100%; + height: 100%; + } +} \ 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 245e4da0..d447304a 100755 --- a/packages/app/src/contexts/WithPlayerContext/index.jsx +++ b/packages/app/src/contexts/WithPlayerContext/index.jsx @@ -1,42 +1,52 @@ import React from "react" -export const DefaultContextValues = { - loading: false, - minimized: false, +function deepUnproxy(obj) { + // Verificar si es un array y hacer una copia en consecuencia + if (Array.isArray(obj)) { + obj = [...obj]; + } else { + obj = Object.assign({}, obj); + } - muted: false, - volume: 1, + 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 + } + } - sync_mode: false, - livestream_mode: false, - control_locked: false, - - track_cover_analysis: null, - track_metadata: null, - - playback_mode: "repeat", - playback_status: null, + return obj; } -export const Context = React.createContext(DefaultContextValues) +export const usePlayerStateContext = (updater) => { + const [state, setState] = React.useState({ ...app.cores.player.state }) + + function handleStateChange(newState) { + newState = deepUnproxy(newState) + + setState(newState) + + if (typeof updater === "function") { + updater(newState) + } + } + + React.useEffect(() => { + handleStateChange(app.cores.player.state) + + app.cores.player.eventBus().on("player.state.update", handleStateChange) + + return () => { + app.cores.player.eventBus().off("player.state.update", handleStateChange) + } + }, []) + + return state +} + +export const Context = React.createContext({}) export class WithPlayerContext extends React.Component { - state = { - loading: app.cores.player.state["loading"], - minimized: app.cores.player.state["minimized"], - - muted: app.cores.player.state["muted"], - volume: app.cores.player.state["volume"], - - sync_mode: app.cores.player.state["sync_mode"], - livestream_mode: app.cores.player.state["livestream_mode"], - control_locked: app.cores.player.state["control_locked"], - - track_manifest: app.cores.player.state["track_manifest"], - - playback_mode: app.cores.player.state["playback_mode"], - playback_status: app.cores.player.state["playback_status"], - } + state = app.cores.player.state events = { "player.state.update": (state) => { @@ -44,17 +54,15 @@ export class WithPlayerContext extends React.Component { }, } - eventBus = app.cores.player.eventBus - componentDidMount() { for (const [event, handler] of Object.entries(this.events)) { - this.eventBus.on(event, handler) + app.cores.player.eventBus().on(event, handler) } } componentWillUnmount() { for (const [event, handler] of Object.entries(this.events)) { - this.eventBus.off(event, handler) + app.cores.player.eventBus().off(event, handler) } } diff --git a/packages/app/src/cores/contextMenu/components/contextMenu/index.less b/packages/app/src/cores/contextMenu/components/contextMenu/index.less index d4c04735..73bba3b4 100755 --- a/packages/app/src/cores/contextMenu/components/contextMenu/index.less +++ b/packages/app/src/cores/contextMenu/components/contextMenu/index.less @@ -26,7 +26,7 @@ padding: 4px; - font-weight: 600; + font-weight: 450; font-family: var(--fontFamily); font-size: 0.8rem; color: var(--text-color); diff --git a/packages/app/src/cores/player/mediaSession.js b/packages/app/src/cores/player/classes/MediaSession.js similarity index 100% rename from packages/app/src/cores/player/mediaSession.js rename to packages/app/src/cores/player/classes/MediaSession.js diff --git a/packages/app/src/cores/player/classes/PlayerProcessors.js b/packages/app/src/cores/player/classes/PlayerProcessors.js new file mode 100644 index 00000000..b8b17c8a --- /dev/null +++ b/packages/app/src/cores/player/classes/PlayerProcessors.js @@ -0,0 +1,85 @@ +import defaultAudioProccessors from "../processors" + +export default class PlayerProcessors { + constructor(player) { + this.player = player + } + + processors = [] + + public = {} + + async initialize() { + // if already exists audio processors, destroy all before create new + if (this.processors.length > 0) { + this.player.console.log("Destroying audio processors") + + this.processors.forEach((processor) => { + this.player.console.log(`Destroying audio processor ${processor.constructor.name}`, processor) + processor._destroy() + }) + + this.processors = [] + } + + // instanciate default audio processors + for await (const defaultProccessor of defaultAudioProccessors) { + this.processors.push(new defaultProccessor(this.player)) + } + + // initialize audio processors + for await (const processor of this.processors) { + if (typeof processor._init === "function") { + try { + await processor._init(this.player.audioContext) + } catch (error) { + this.player.console.error(`Failed to initialize audio processor ${processor.constructor.name} >`, error) + continue + } + } + + // check if processor has exposed public methods + if (processor.exposeToPublic) { + Object.entries(processor.exposeToPublic).forEach(([key, value]) => { + const refName = processor.constructor.refName + + if (typeof this.public[refName] === "undefined") { + // by default create a empty object + this.player.public[refName] = {} + } + + this.player.public[refName][key] = value + }) + } + } + } + + 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) + + processor.processor.connect(this.player.audioContext.destination) + + continue + } + + if (typeof processor._attach !== "function") { + this.player.console.error(`Processor ${processor.constructor.refName} not support attach`) + + continue + } + + instance = await processor._attach(instance, index) + } + + const lastProcessor = instance.attachedProcessors[instance.attachedProcessors.length - 1].processor + + // now attach to destination + lastProcessor.connect(this.player.audioContext.destination) + + return instance + } +} \ No newline at end of file diff --git a/packages/app/src/cores/player/classes/PlayerState.js b/packages/app/src/cores/player/classes/PlayerState.js new file mode 100644 index 00000000..c2d71115 --- /dev/null +++ b/packages/app/src/cores/player/classes/PlayerState.js @@ -0,0 +1,37 @@ +import { Observable } from "object-observer" +import AudioPlayerStorage from "../player.storage" + +export default class PlayerState { + static defaultState = { + loading: false, + playback_status: "stopped", + track_manifest: null, + + muted: app.isMobile ? false : (AudioPlayerStorage.get("mute") ?? false), + volume: app.isMobile ? 1 : (AudioPlayerStorage.get("volume") ?? 0.3), + playback_mode: AudioPlayerStorage.get("mode") ?? "normal", + } + + constructor(player) { + this.player = player + + this.state = Observable.from(PlayerState.defaultState) + + Observable.observe(this.state, async (changes) => { + try { + changes.forEach((change) => { + if (change.type === "update") { + const stateKey = change.path[0] + + this.player.eventBus.emit(`player.state.update:${stateKey}`, change.object[stateKey]) + this.player.eventBus.emit("player.state.update", change.object) + } + }) + } catch (error) { + this.player.console.error(`Failed to dispatch state updater >`, error) + } + }) + + return this.state + } +} \ No newline at end of file diff --git a/packages/app/src/cores/player/classes/PlayerUI.js b/packages/app/src/cores/player/classes/PlayerUI.js new file mode 100644 index 00000000..c3bbf7f0 --- /dev/null +++ b/packages/app/src/cores/player/classes/PlayerUI.js @@ -0,0 +1,40 @@ +import ToolBarPlayer from "@components/Player/ToolBarPlayer" + +export default class PlayerUI { + constructor(player) { + this.player = player + } + + currentDomWindow = null + + // + // UI Methods + // + + attachPlayerComponent() { + if (this.currentDomWindow) { + this.player.console.warn("EmbbededMediaPlayer already attached") + return false + } + + if (app.layout.tools_bar) { + this.currentDomWindow = app.layout.tools_bar.attachRender("mediaPlayer", ToolBarPlayer) + } + } + + detachPlayerComponent() { + if (!this.currentDomWindow) { + this.player.console.warn("EmbbededMediaPlayer not attached") + return false + } + + if (!app.layout.tools_bar) { + this.player.console.error("Tools bar not found") + return false + } + + app.layout.tools_bar.detachRender("mediaPlayer") + + this.currentDomWindow = null + } +} \ No newline at end of file diff --git a/packages/app/src/cores/player/presets.js b/packages/app/src/cores/player/classes/Presets.js similarity index 97% rename from packages/app/src/cores/player/presets.js rename to packages/app/src/cores/player/classes/Presets.js index a8fa40cb..48d408f3 100755 --- a/packages/app/src/cores/player/presets.js +++ b/packages/app/src/cores/player/classes/Presets.js @@ -1,4 +1,4 @@ -import AudioPlayerStorage from "./player.storage" +import AudioPlayerStorage from "../player.storage" export default class Presets { constructor({ diff --git a/packages/app/src/cores/player/services.js b/packages/app/src/cores/player/classes/Services.js similarity index 100% rename from packages/app/src/cores/player/services.js rename to packages/app/src/cores/player/classes/Services.js diff --git a/packages/app/src/cores/player/helpers/setSampleRate.js b/packages/app/src/cores/player/helpers/setSampleRate.js new file mode 100644 index 00000000..daa94b31 --- /dev/null +++ b/packages/app/src/cores/player/helpers/setSampleRate.js @@ -0,0 +1,37 @@ +import AudioPlayerStorage from "../player.storage.js" + +export default async (sampleRate) => { + // must be a integer + if (typeof sampleRate !== "number") { + this.console.error("Sample rate must be a number") + return null + } + + // must be a integer + if (!Number.isInteger(sampleRate)) { + this.console.error("Sample rate must be a integer") + return null + } + + return await new Promise((resolve) => { + app.confirm({ + title: "Change sample rate", + content: `To change the sample rate, the app needs to be reloaded. Do you want to continue?`, + onOk: () => { + try { + AudioPlayerStorage.set("sample_rate", sampleRate) + + app.navigation.reload() + + return resolve(sampleRate) + } catch (error) { + app.message.error(`Failed to change sample rate, ${error.message}`) + return resolve(null) + } + }, + onCancel: () => { + return resolve(null) + } + }) + }) +} \ No newline at end of file diff --git a/packages/app/src/cores/player/player.bkp.js b/packages/app/src/cores/player/player.bkp.js deleted file mode 100755 index 2af3d41b..00000000 --- a/packages/app/src/cores/player/player.bkp.js +++ /dev/null @@ -1,946 +0,0 @@ -import { Core, EventBus } from "vessel" -import { Observable } from "object-observer" -import { FastAverageColor } from "fast-average-color" - -import MusicModel from "comty.js/models/music" - -import ToolBarPlayer from "@components/Player/ToolBarPlayer" -import BackgroundMediaPlayer from "@components/Player/BackgroundMediaPlayer" - -import AudioPlayerStorage from "./player.storage" - -import defaultAudioProccessors from "./processors" - -import MediaSession from "./mediaSession" -import ServiceProviders from "./services" - -export default class Player extends Core { - static dependencies = [ - "api", - "settings" - ] - - static namespace = "player" - - static bgColor = "aquamarine" - static textColor = "black" - - static defaultSampleRate = 48000 - - static gradualFadeMs = 150 - - // buffer & precomputation - static maxManifestPrecompute = 3 - - service_providers = new ServiceProviders() - - native_controls = new MediaSession() - - currentDomWindow = null - - audioContext = new AudioContext({ - sampleRate: AudioPlayerStorage.get("sample_rate") ?? Player.defaultSampleRate, - latencyHint: "playback" - }) - - audioProcessors = [] - - eventBus = new EventBus() - - fac = new FastAverageColor() - - track_prev_instances = [] - track_instance = null - track_next_instances = [] - - state = Observable.from({ - loading: false, - minimized: false, - - muted: app.isMobile ? false : (AudioPlayerStorage.get("mute") ?? false), - volume: app.isMobile ? 1 : (AudioPlayerStorage.get("volume") ?? 0.3), - - sync_mode: false, - livestream_mode: false, - control_locked: false, - - track_manifest: null, - - playback_mode: AudioPlayerStorage.get("mode") ?? "normal", - playback_status: "stopped", - }) - - public = { - audioContext: this.audioContext, - setSampleRate: this.setSampleRate, - start: this.start.bind(this), - close: this.close.bind(this), - playback: { - mode: this.playbackMode.bind(this), - stop: this.stop.bind(this), - toggle: this.togglePlayback.bind(this), - pause: this.pausePlayback.bind(this), - play: this.resumePlayback.bind(this), - next: this.next.bind(this), - previous: this.previous.bind(this), - seek: this.seek.bind(this), - }, - _setLoading: function (to) { - this.state.loading = !!to - }.bind(this), - duration: this.duration.bind(this), - volume: this.volume.bind(this), - mute: this.mute.bind(this), - toggleMute: this.toggleMute.bind(this), - seek: this.seek.bind(this), - minimize: this.toggleMinimize.bind(this), - collapse: this.toggleCollapse.bind(this), - state: new Proxy(this.state, { - get: (target, prop) => { - return target[prop] - }, - set: (target, prop, value) => { - return false - } - }), - eventBus: new Proxy(this.eventBus, { - get: (target, prop) => { - return target[prop] - }, - set: (target, prop, value) => { - return false - } - }), - gradualFadeMs: Player.gradualFadeMs, - trackInstance: () => { - return this.track_instance - } - } - - internalEvents = { - "player.state.update:loading": () => { - //app.cores.sync.music.dispatchEvent("music.player.state.update", this.state) - }, - "player.state.update:track_manifest": () => { - //app.cores.sync.music.dispatchEvent("music.player.state.update", this.state) - }, - "player.state.update:playback_status": () => { - //app.cores.sync.music.dispatchEvent("music.player.state.update", this.state) - }, - "player.seeked": (to) => { - //app.cores.sync.music.dispatchEvent("music.player.seek", to) - }, - } - - async onInitialize() { - this.native_controls.initialize() - - this.initializeAudioProcessors() - - for (const [eventName, eventHandler] of Object.entries(this.internalEvents)) { - this.eventBus.on(eventName, eventHandler) - } - - Observable.observe(this.state, async (changes) => { - try { - changes.forEach((change) => { - if (change.type === "update") { - const stateKey = change.path[0] - - this.eventBus.emit(`player.state.update:${stateKey}`, change.object[stateKey]) - this.eventBus.emit("player.state.update", change.object) - } - }) - } catch (error) { - this.console.error(`Failed to dispatch state updater >`, error) - } - }) - } - - async initializeBeforeRuntimeInitialize() { - for (const [eventName, eventHandler] of Object.entries(this.wsEvents)) { - app.cores.api.listenEvent(eventName, eventHandler, Player.websocketListen) - } - - if (app.isMobile) { - this.state.audioVolume = 1 - } - } - - async initializeAudioProcessors() { - if (this.audioProcessors.length > 0) { - this.console.log("Destroying audio processors") - - this.audioProcessors.forEach((processor) => { - this.console.log(`Destroying audio processor ${processor.constructor.name}`, processor) - processor._destroy() - }) - - this.audioProcessors = [] - } - - for await (const defaultProccessor of defaultAudioProccessors) { - this.audioProcessors.push(new defaultProccessor(this)) - } - - for await (const processor of this.audioProcessors) { - if (typeof processor._init === "function") { - try { - await processor._init(this.audioContext) - } catch (error) { - this.console.error(`Failed to initialize audio processor ${processor.constructor.name} >`, error) - continue - } - } - - // check if processor has exposed public methods - if (processor.exposeToPublic) { - Object.entries(processor.exposeToPublic).forEach(([key, value]) => { - const refName = processor.constructor.refName - - if (typeof this.public[refName] === "undefined") { - // by default create a empty object - this.public[refName] = {} - } - - this.public[refName][key] = value - }) - } - } - } - - // - // UI Methods - // - - attachPlayerComponent() { - if (this.currentDomWindow) { - this.console.warn("EmbbededMediaPlayer already attached") - return false - } - - if (app.layout.tools_bar) { - this.currentDomWindow = app.layout.tools_bar.attachRender("mediaPlayer", ToolBarPlayer) - } - - } - - detachPlayerComponent() { - if (!this.currentDomWindow) { - this.console.warn("EmbbededMediaPlayer not attached") - return false - } - - if (!app.layout.tools_bar) { - this.console.error("Tools bar not found") - return false - } - - app.layout.tools_bar.detachRender("mediaPlayer") - - this.currentDomWindow = null - } - - // - // Instance managing methods - // - async abortPreloads() { - for await (const instance of this.track_next_instances) { - 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 (!instance.manifest.cover_analysis) { - const cover_analysis = await this.fac.getColorAsync(`https://corsproxy.io/?${encodeURIComponent(instance.manifest.cover ?? instance.manifest.thumbnail)}`) - .catch((err) => { - this.console.error(err) - - return false - }) - - instance.manifest.cover_analysis = cover_analysis - } - - if (!instance._preloaded) { - instance.media.preload = "metadata" - instance._preloaded = true - } - - if (isIndex) { - this.track_next_instances[index] = instance - } - - return instance - } - - async destroyCurrentInstance({ sync = false } = {}) { - if (!this.track_instance) { - return false - } - - // stop playback - if (this.track_instance.media) { - this.track_instance.media.pause() - } - - // reset track_instance - this.track_instance = null - - // reset livestream mode - this.state.livestream_mode = false - } - - async createInstance(manifest) { - if (!manifest) { - this.console.error("Manifest is required") - return false - } - - if (typeof manifest === "string") { - manifest = { - src: manifest, - } - } - - // check if manifest has `manifest` property, if is and not inherit or missing source, resolve - if (manifest.service) { - if (!this.service_providers.has(manifest.service)) { - this.console.error(`Service ${manifest.service} is not supported`) - return false - } - - if (manifest.service !== "inherit" && !manifest.source) { - manifest = await this.service_providers.resolve(manifest.service, manifest) - } - } - - if (!manifest.src && !manifest.source) { - this.console.error("Manifest source is required") - return false - } - - const source = manifest.src ?? manifest.source - - if (!manifest.metadata) { - manifest.metadata = {} - } - - // if title is not set, use the audio source filename - if (!manifest.metadata.title) { - manifest.metadata.title = source.split("/").pop() - } - - let instance = { - manifest: manifest, - attachedProcessors: [], - abortController: new AbortController(), - source: source, - media: new Audio(source), - duration: null, - seek: 0, - track: null, - } - - instance.media.signal = instance.abortController.signal - instance.media.crossOrigin = "anonymous" - instance.media.preload = "metadata" - - instance.media.loop = this.state.playback_mode === "repeat" - instance.media.volume = this.state.volume - - // handle on end - instance.media.addEventListener("ended", () => { - this.next() - }) - - instance.media.addEventListener("loadeddata", () => { - this.state.loading = false - }) - - // update playback status - instance.media.addEventListener("play", () => { - this.state.playback_status = "playing" - }) - - instance.media.addEventListener("playing", () => { - this.state.loading = false - - this.state.playback_status = "playing" - - if (this.waitUpdateTimeout) { - clearTimeout(this.waitUpdateTimeout) - this.waitUpdateTimeout = null - } - }) - - instance.media.addEventListener("pause", () => { - this.state.playback_status = "paused" - }) - - instance.media.addEventListener("durationchange", (duration) => { - if (instance.media.paused) { - return false - } - - instance.duration = duration - }) - - instance.media.addEventListener("waiting", () => { - if (instance.media.paused) { - return false - } - - if (this.waitUpdateTimeout) { - clearTimeout(this.waitUpdateTimeout) - this.waitUpdateTimeout = null - } - - // if takes more than 150ms to load, update loading state - this.waitUpdateTimeout = setTimeout(() => { - this.state.loading = true - }, 150) - }) - - instance.media.addEventListener("seeked", () => { - this.console.log(`Seeked to ${instance.seek}`) - - this.eventBus.emit(`player.seeked`, instance.seek) - }) - - instance.media.addEventListener("loadedmetadata", () => { - if (instance.media.duration === Infinity) { - instance.manifest.stream = true - - this.state.livestream_mode = true - } - }, { once: true }) - - instance.track = this.audioContext.createMediaElementSource(instance.media) - - return instance - } - - async attachProcessorsToInstance(instance) { - for await (const [index, processor] of this.audioProcessors.entries()) { - if (processor.constructor.node_bypass === true) { - instance.track.connect(processor.processor) - - processor.processor.connect(this.audioContext.destination) - - continue - } - - if (typeof processor._attach !== "function") { - this.console.error(`Processor ${processor.constructor.refName} not support attach`) - - continue - } - - instance = await processor._attach(instance, index) - } - - const lastProcessor = instance.attachedProcessors[instance.attachedProcessors.length - 1].processor - - // now attach to destination - lastProcessor.connect(this.audioContext.destination) - - return instance - } - - // - // 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") - } - - 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() - } - - // attach processors - instance = await this.attachProcessorsToInstance(instance) - - // now set the current instance - this.track_instance = await this.preloadAudioInstance(instance) - - // reconstruct audio src if is not set - if (this.track_instance.media.src !== instance.source) { - this.track_instance.media.src = instance.source - } - - // set time to 0 - this.track_instance.media.currentTime = 0 - - if (params.time >= 0) { - this.track_instance.media.currentTime = params.time - } - - this.track_instance.media.muted = this.state.muted - this.track_instance.media.loop = this.state.playback_mode === "repeat" - - // try to preload next audio - // TODO: Use a better way to preload queues - if (this.track_next_instances.length > 0) { - this.preloadAudioInstance(1) - } - - // play - await this.track_instance.media.play() - - this.console.debug(`Playing track >`, this.track_instance) - - // update manifest - this.state.track_manifest = instance.manifest - - this.native_controls.update(instance.manifest) - - return this.track_instance - } - - async start(manifest, { sync = false, time, startIndex = 0 } = {}) { - if (this.state.control_locked && !sync) { - this.console.warn("Controls are locked, cannot do this action") - return false - } - - this.attachPlayerComponent() - - // !IMPORTANT: abort preloads before destroying current instance - await this.abortPreloads() - await this.destroyCurrentInstance({ - sync - }) - - this.state.loading = true - - this.track_prev_instances = [] - this.track_next_instances = [] - - let playlist = Array.isArray(manifest) ? manifest : [manifest] - - if (playlist.length === 0) { - this.console.warn(`[PLAYER] Playlist is empty, aborting...`) - return false - } - - if (playlist.some((item) => typeof item === "string")) { - playlist = await this.service_providers.resolveMany(playlist) - } - - playlist = playlist.slice(startIndex) - - for await (const [index, _manifest] of playlist.entries()) { - const instance = await this.createInstance(_manifest) - - this.track_next_instances.push(instance) - - if (index === 0) { - this.play(this.track_next_instances[0], { - time: time ?? 0 - }) - } - } - - return manifest - } - - next({ sync = false } = {}) { - if (this.state.control_locked && !sync) { - //this.console.warn("Sync mode is locked, cannot do this action") - return false - } - - 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.track_next_instances.length === 0) { - this.console.log(`[PLAYER] No more tracks to play, stopping...`) - - return this.stop() - } - - 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]) - } - - previous({ sync = false } = {}) { - if (this.state.control_locked && !sync) { - //this.console.warn("Sync mode is locked, cannot do this action") - return false - } - - 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.track_prev_instances.length === 0) { - this.console.log(`[PLAYER] No previous tracks, replying...`) - // replay the current track - return this.play(this.track_instance) - } - } - - async togglePlayback() { - if (this.state.playback_status === "paused") { - await this.resumePlayback() - } else { - await this.pausePlayback() - } - } - - async pausePlayback() { - return await new Promise((resolve, reject) => { - if (!this.track_instance) { - this.console.error("No audio instance") - return null - } - - // set gain exponentially - this.track_instance.gainNode.gain.linearRampToValueAtTime( - 0.0001, - this.audioContext.currentTime + (Player.gradualFadeMs / 1000) - ) - - setTimeout(() => { - this.track_instance.media.pause() - resolve() - }, Player.gradualFadeMs) - - this.native_controls.updateIsPlaying(false) - }) - } - - async resumePlayback() { - if (!this.state.playback_status === "playing") { - return true - } - - return await new Promise((resolve, reject) => { - if (!this.track_instance) { - this.console.error("No audio instance") - return null - } - - // ensure audio elemeto starts from 0 volume - this.track_instance.gainNode.gain.value = 0.0001 - - this.track_instance.media.play().then(() => { - resolve() - }) - - // set gain exponentially - this.track_instance.gainNode.gain.linearRampToValueAtTime( - this.state.volume, - this.audioContext.currentTime + (Player.gradualFadeMs / 1000) - ) - - this.native_controls.updateIsPlaying(true) - }) - } - - stop() { - this.destroyCurrentInstance() - this.abortPreloads() - - this.state.playback_status = "stopped" - this.state.track_manifest = null - - this.state.livestream_mode = false - - this.track_instance = null - this.track_next_instances = [] - this.track_prev_instances = [] - - this.native_controls.destroy() - } - - mute(to) { - if (app.isMobile && typeof to !== "boolean") { - this.console.warn("Cannot mute on mobile") - return false - } - - if (typeof to === "boolean") { - this.state.muted = to - this.track_instance.media.muted = to - } - - return this.state.muted - } - - volume(volume) { - if (typeof volume !== "number") { - return this.state.volume - } - - if (app.isMobile) { - this.console.warn("Cannot change volume on mobile") - return false - } - - if (volume > 1) { - if (!app.cores.settings.get("player.allowVolumeOver100")) { - volume = 1 - } - } - - if (volume < 0) { - volume = 0 - } - - this.state.volume = volume - - AudioPlayerStorage.set("volume", volume) - - if (this.track_instance) { - if (this.track_instance.gainNode) { - this.track_instance.gainNode.gain.value = this.state.volume - } - } - - return this.state.volume - } - - seek(time, { sync = false } = {}) { - if (!this.track_instance || !this.track_instance.media) { - return false - } - - // if time not provided, return current time - if (typeof time === "undefined") { - return this.track_instance.media.currentTime - } - - if (this.state.control_locked && !sync) { - this.console.warn("Sync mode is locked, cannot do this action") - return false - } - - - // if time is provided, seek to that time - if (typeof time === "number") { - this.console.log(`Seeking to ${time} | Duration: ${this.track_instance.media.duration}`) - - this.track_instance.media.currentTime = time - - return time - } - } - - playbackMode(mode) { - if (typeof mode !== "string") { - return this.state.playback_mode - } - - this.state.playback_mode = mode - - if (this.track_instance) { - this.track_instance.media.loop = this.state.playback_mode === "repeat" - } - - AudioPlayerStorage.set("mode", mode) - - return mode - } - - duration() { - if (!this.track_instance) { - return false - } - - return this.track_instance.media.duration - } - - loop(to) { - if (typeof to !== "boolean") { - this.console.warn("Loop must be a boolean") - return false - } - - this.state.loop = to ?? !this.state.loop - - if (this.track_instance.media) { - this.track_instance.media.loop = this.state.loop - } - - return this.state.loop - } - - close() { - this.stop() - this.detachPlayerComponent() - } - - toggleMinimize(to) { - this.state.minimized = to ?? !this.state.minimized - - if (this.state.minimized) { - app.layout.sidebar.attachBottomItem("player", BackgroundMediaPlayer, { - noContainer: true - }) - } else { - app.layout.sidebar.removeBottomItem("player") - } - - return this.state.minimized - } - - toggleCollapse(to) { - if (typeof to !== "boolean") { - this.console.warn("Collapse must be a boolean") - return false - } - - this.state.collapsed = to ?? !this.state.collapsed - - return this.state.collapsed - } - - toggleSyncMode(to, lock) { - if (typeof to !== "boolean") { - this.console.warn("Sync mode must be a boolean") - return false - } - - this.state.syncMode = to ?? !this.state.syncMode - - this.state.syncModeLocked = lock ?? false - - this.console.log(`Sync mode is now ${this.state.syncMode ? "enabled" : "disabled"} | Locked: ${this.state.syncModeLocked ? "yes" : "no"}`) - - return this.state.syncMode - } - - toggleMute(to) { - if (typeof to !== "boolean") { - to = !this.state.muted - } - - return this.mute(to) - } - - async getTracksByIds(list) { - if (!Array.isArray(list)) { - this.console.warn("List must be an array") - return false - } - - let ids = [] - - list.forEach((item) => { - if (typeof item === "string") { - ids.push(item) - } - }) - - if (ids.length === 0) { - return list - } - - const fetchedTracks = await MusicModel.getTracksData(ids).catch((err) => { - this.console.error(err) - return false - }) - - if (!fetchedTracks) { - return list - } - - // replace fetched tracks with the ones in the list - fetchedTracks.forEach((fetchedTrack) => { - const index = list.findIndex((item) => item === fetchedTrack._id) - - if (index !== -1) { - list[index] = fetchedTrack - } - }) - - return list - } - - async setSampleRate(to) { - // must be a integer - if (typeof to !== "number") { - this.console.error("Sample rate must be a number") - return this.audioContext.sampleRate - } - - // must be a integer - if (!Number.isInteger(to)) { - this.console.error("Sample rate must be a integer") - return this.audioContext.sampleRate - } - - return await new Promise((resolve, reject) => { - app.confirm({ - title: "Change sample rate", - content: `To change the sample rate, the app needs to be reloaded. Do you want to continue?`, - onOk: () => { - try { - this.audioContext = new AudioContext({ sampleRate: to }) - - AudioPlayerStorage.set("sample_rate", to) - - app.navigation.reload() - - return resolve(this.audioContext.sampleRate) - } catch (error) { - app.message.error(`Failed to change sample rate, ${error.message}`) - return resolve(this.audioContext.sampleRate) - } - }, - onCancel: () => { - return resolve(this.audioContext.sampleRate) - } - }) - }) - } -} \ No newline at end of file diff --git a/packages/app/src/cores/player/player.core.js b/packages/app/src/cores/player/player.core.js index 066528e4..7e0dedb2 100755 --- a/packages/app/src/cores/player/player.core.js +++ b/packages/app/src/cores/player/player.core.js @@ -1,243 +1,84 @@ -import { Core, EventBus } from "vessel" -import { Observable } from "object-observer" -import { FastAverageColor } from "fast-average-color" +import { Core } from "vessel" -import ToolBarPlayer from "@components/Player/ToolBarPlayer" -import BackgroundMediaPlayer from "@components/Player/BackgroundMediaPlayer" +import TrackInstance from "@classes/TrackInstance" +import MediaSession from "./classes/MediaSession" +import ServiceProviders from "./classes/Services" +import PlayerState from "./classes/PlayerState" +import PlayerUI from "./classes/PlayerUI" +import PlayerProcessors from "./classes/PlayerProcessors" + +import setSampleRate from "./helpers/setSampleRate" import AudioPlayerStorage from "./player.storage" -import TrackInstanceClass from "./classes/TrackInstance" -import defaultAudioProccessors from "./processors" - -import MediaSession from "./mediaSession" -import ServiceProviders from "./services" - export default class Player extends Core { + // core config static dependencies = [ "api", "settings" ] - static namespace = "player" - static bgColor = "aquamarine" static textColor = "black" + // player config static defaultSampleRate = 48000 - static gradualFadeMs = 150 - - // buffer & precomputation static maxManifestPrecompute = 3 + state = new PlayerState(this) + ui = new PlayerUI(this) service_providers = new ServiceProviders() - native_controls = new MediaSession() - - currentDomWindow = null - audioContext = new AudioContext({ sampleRate: AudioPlayerStorage.get("sample_rate") ?? Player.defaultSampleRate, latencyHint: "playback" }) - audioProcessors = [] - - eventBus = new EventBus() - - fac = new FastAverageColor() + audioProcessors = new PlayerProcessors(this) track_prev_instances = [] track_instance = null track_next_instances = [] - state = Observable.from({ - loading: false, - minimized: false, - - muted: app.isMobile ? false : (AudioPlayerStorage.get("mute") ?? false), - volume: app.isMobile ? 1 : (AudioPlayerStorage.get("volume") ?? 0.3), - - sync_mode: false, - livestream_mode: false, - control_locked: false, - - track_manifest: null, - - playback_mode: AudioPlayerStorage.get("mode") ?? "normal", - playback_status: "stopped", - }) - public = { - audioContext: this.audioContext, - setSampleRate: this.setSampleRate, - start: this.start.bind(this), - close: this.close.bind(this), - playback: { - mode: this.playbackMode.bind(this), - stop: this.stop.bind(this), - toggle: this.togglePlayback.bind(this), - pause: this.pausePlayback.bind(this), - play: this.resumePlayback.bind(this), - next: this.next.bind(this), - previous: this.previous.bind(this), - seek: this.seek.bind(this), - }, - _setLoading: function (to) { - this.state.loading = !!to - }.bind(this), - duration: this.duration.bind(this), - volume: this.volume.bind(this), - mute: this.mute.bind(this), - toggleMute: this.toggleMute.bind(this), - seek: this.seek.bind(this), - minimize: this.toggleMinimize.bind(this), - collapse: this.toggleCollapse.bind(this), - state: new Proxy(this.state, { - get: (target, prop) => { - return target[prop] - }, - set: (target, prop, value) => { - return false - } + start: this.start, + close: this.close, + playback: this.bindableReadOnlyProxy({ + toggle: this.togglePlayback, + play: this.resumePlayback, + pause: this.pausePlayback, + stop: this.stopPlayback, + previous: this.previous, + next: this.next, + mode: this.playbackMode, }), - eventBus: new Proxy(this.eventBus, { - get: (target, prop) => { - return target[prop] - }, - set: (target, prop, value) => { - return false - } + controls: this.bindableReadOnlyProxy({ + duration: this.duration, + volume: this.volume, + mute: this.mute, + seek: this.seek, + setSampleRate: setSampleRate, }), - gradualFadeMs: Player.gradualFadeMs, - trackInstance: () => { + track: () => { return this.track_instance - } + }, + eventBus: () => { + return this.eventBus + }, + state: this.state, + ui: this.ui.public, + audioContext: this.audioContext, + gradualFadeMs: Player.gradualFadeMs, } - internalEvents = { - "player.state.update:loading": () => { - //app.cores.sync.music.dispatchEvent("music.player.state.update", this.state) - }, - "player.state.update:track_manifest": () => { - //app.cores.sync.music.dispatchEvent("music.player.state.update", this.state) - }, - "player.state.update:playback_status": () => { - //app.cores.sync.music.dispatchEvent("music.player.state.update", this.state) - }, - "player.seeked": (to) => { - //app.cores.sync.music.dispatchEvent("music.player.seek", to) - }, - } - - async onInitialize() { - this.native_controls.initialize() - - this.initializeAudioProcessors() - - for (const [eventName, eventHandler] of Object.entries(this.internalEvents)) { - this.eventBus.on(eventName, eventHandler) - } - - Observable.observe(this.state, async (changes) => { - try { - changes.forEach((change) => { - if (change.type === "update") { - const stateKey = change.path[0] - - this.eventBus.emit(`player.state.update:${stateKey}`, change.object[stateKey]) - this.eventBus.emit("player.state.update", change.object) - } - }) - } catch (error) { - this.console.error(`Failed to dispatch state updater >`, error) - } - }) - } - - async initializeBeforeRuntimeInitialize() { - for (const [eventName, eventHandler] of Object.entries(this.wsEvents)) { - app.cores.api.listenEvent(eventName, eventHandler, Player.websocketListen) - } - + async initializeAfterCoresInit() { if (app.isMobile) { - this.state.audioVolume = 1 - } - } - - async initializeAudioProcessors() { - if (this.audioProcessors.length > 0) { - this.console.log("Destroying audio processors") - - this.audioProcessors.forEach((processor) => { - this.console.log(`Destroying audio processor ${processor.constructor.name}`, processor) - processor._destroy() - }) - - this.audioProcessors = [] + this.state.volume = 1 } - for await (const defaultProccessor of defaultAudioProccessors) { - this.audioProcessors.push(new defaultProccessor(this)) - } - - for await (const processor of this.audioProcessors) { - if (typeof processor._init === "function") { - try { - await processor._init(this.audioContext) - } catch (error) { - this.console.error(`Failed to initialize audio processor ${processor.constructor.name} >`, error) - continue - } - } - - // check if processor has exposed public methods - if (processor.exposeToPublic) { - Object.entries(processor.exposeToPublic).forEach(([key, value]) => { - const refName = processor.constructor.refName - - if (typeof this.public[refName] === "undefined") { - // by default create a empty object - this.public[refName] = {} - } - - this.public[refName][key] = value - }) - } - } - } - - // - // UI Methods - // - - attachPlayerComponent() { - if (this.currentDomWindow) { - this.console.warn("EmbbededMediaPlayer already attached") - return false - } - - if (app.layout.tools_bar) { - this.currentDomWindow = app.layout.tools_bar.attachRender("mediaPlayer", ToolBarPlayer) - } - - } - - detachPlayerComponent() { - if (!this.currentDomWindow) { - this.console.warn("EmbbededMediaPlayer not attached") - return false - } - - if (!app.layout.tools_bar) { - this.console.error("Tools bar not found") - return false - } - - app.layout.tools_bar.detachRender("mediaPlayer") - - this.currentDomWindow = null + await this.native_controls.initialize() + await this.audioProcessors.initialize() } // @@ -265,22 +106,6 @@ export default class Player extends Core { return false } - if (!instance.manifest.cover_analysis) { - const cover_analysis = await this.fac.getColorAsync(`https://corsproxy.io/?${encodeURIComponent(instance.manifest.cover ?? instance.manifest.thumbnail)}`) - .catch((err) => { - this.console.error(err) - - return false - }) - - instance.manifest.cover_analysis = cover_analysis - } - - if (!instance._preloaded) { - instance.audio.preload = "metadata" - instance._preloaded = true - } - if (isIndex) { this.track_next_instances[index] = instance } @@ -288,7 +113,7 @@ export default class Player extends Core { return instance } - async destroyCurrentInstance({ sync = false } = {}) { + async destroyCurrentInstance() { if (!this.track_instance) { return false } @@ -300,36 +125,6 @@ export default class Player extends Core { // reset track_instance this.track_instance = null - - // reset livestream mode - this.state.livestream_mode = false - } - - async attachProcessorsToInstance(instance) { - for await (const [index, processor] of this.audioProcessors.entries()) { - if (processor.constructor.node_bypass === true) { - instance.contextElement.connect(processor.processor) - - processor.processor.connect(this.audioContext.destination) - - continue - } - - if (typeof processor._attach !== "function") { - this.console.error(`Processor ${processor.constructor.refName} not support attach`) - - continue - } - - instance = await processor._attach(instance, index) - } - - const lastProcessor = instance.attachedProcessors[instance.attachedProcessors.length - 1].processor - - // now attach to destination - lastProcessor.connect(this.audioContext.destination) - - return instance } // @@ -364,61 +159,50 @@ export default class Player extends Core { this.destroyCurrentInstance() } - // attach processors - instance = await this.attachProcessorsToInstance(instance) + // chage current track instance with provided + this.track_instance = instance - // now set the current instance - this.track_instance = await this.preloadAudioInstance(instance) + // initialize instance if is not + if (this.track_instance._initialized === false) { + this.track_instance = await instance.initialize() + } + + // update manifest + this.state.track_manifest = this.track_instance.manifest + + // attach processors + this.track_instance = await this.audioProcessors.attachProcessorsToInstance(this.track_instance) // reconstruct audio src if is not set - if (this.track_instance.audio.src !== instance.manifest.source) { - this.track_instance.audio.src = instance.manifest.source + if (this.track_instance.audio.src !== this.track_instance.manifest.source) { + this.track_instance.audio.src = this.track_instance.manifest.source } - // set time to 0 - this.track_instance.audio.currentTime = 0 - - if (params.time >= 0) { - this.track_instance.audio.currentTime = params.time - } + // 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 - // try to preload next audio - // TODO: Use a better way to preload queues - if (this.track_next_instances.length > 0) { - this.preloadAudioInstance(1) - } - // play await this.track_instance.audio.play() this.console.debug(`Playing track >`, this.track_instance) - // update manifest - this.state.track_manifest = instance.manifest - - this.native_controls.update(instance.manifest) + // update native controls + this.native_controls.update(this.track_instance.manifest) return this.track_instance } - async start(manifest, { sync = false, time, startIndex = 0 } = {}) { - if (this.state.control_locked && !sync) { - this.console.warn("Controls are locked, cannot do this action") - return false - } - - this.attachPlayerComponent() + async start(manifest, { time, startIndex = 0 } = {}) { + this.ui.attachPlayerComponent() // !IMPORTANT: abort preloads before destroying current instance await this.abortPreloads() - await this.destroyCurrentInstance({ - sync - }) + await this.destroyCurrentInstance() this.state.loading = true @@ -438,9 +222,8 @@ export default class Player extends Core { playlist = playlist.slice(startIndex) - for await (const [index, _manifest] of playlist.entries()) { - let instance = new TrackInstanceClass(this, _manifest) - instance = await instance.initialize() + for (const [index, _manifest] of playlist.entries()) { + let instance = new TrackInstance(this, _manifest) this.track_next_instances.push(instance) @@ -454,12 +237,7 @@ export default class Player extends Core { return manifest } - next({ sync = false } = {}) { - if (this.state.control_locked && !sync) { - //this.console.warn("Sync mode is locked, cannot do this action") - return false - } - + next() { if (this.track_next_instances.length > 0) { // move current audio instance to history this.track_prev_instances.push(this.track_next_instances.shift()) @@ -468,7 +246,7 @@ export default class Player extends Core { if (this.track_next_instances.length === 0) { this.console.log(`No more tracks to play, stopping...`) - return this.stop() + return this.stopPlayback() } let nextIndex = 0 @@ -480,12 +258,7 @@ export default class Player extends Core { this.play(this.track_next_instances[nextIndex]) } - previous({ sync = false } = {}) { - if (this.state.control_locked && !sync) { - //this.console.warn("Sync mode is locked, cannot do this action") - return false - } - + previous() { if (this.track_prev_instances.length > 0) { // move current audio instance to history this.track_next_instances.unshift(this.track_prev_instances.pop()) @@ -500,6 +273,9 @@ export default class Player extends Core { } } + // + // Playback Control + // async togglePlayback() { if (this.state.playback_status === "paused") { await this.resumePlayback() @@ -509,6 +285,10 @@ export default class Player extends Core { } async pausePlayback() { + if (!this.state.playback_status === "paused") { + return true + } + return await new Promise((resolve, reject) => { if (!this.track_instance) { this.console.error("No audio instance") @@ -558,15 +338,29 @@ export default class Player extends Core { }) } - stop() { + async playbackMode(mode) { + if (typeof mode !== "string") { + return this.state.playback_mode + } + + this.state.playback_mode = mode + + if (this.track_instance) { + this.track_instance.audio.loop = this.state.playback_mode === "repeat" + } + + AudioPlayerStorage.set("mode", mode) + + return mode + } + + async stopPlayback() { this.destroyCurrentInstance() this.abortPreloads() this.state.playback_status = "stopped" this.state.track_manifest = null - this.state.livestream_mode = false - this.track_instance = null this.track_next_instances = [] this.track_prev_instances = [] @@ -574,12 +368,19 @@ export default class Player extends Core { this.native_controls.destroy() } + // + // Audio Control + // mute(to) { if (app.isMobile && typeof to !== "boolean") { this.console.warn("Cannot mute on mobile") return false } + if (to === "toggle") { + to = !this.state.muted + } + if (typeof to === "boolean") { this.state.muted = to this.track_instance.audio.muted = to @@ -621,7 +422,7 @@ export default class Player extends Core { return this.state.volume } - seek(time, { sync = false } = {}) { + seek(time) { if (!this.track_instance || !this.track_instance.audio) { return false } @@ -631,12 +432,6 @@ export default class Player extends Core { return this.track_instance.audio.currentTime } - if (this.state.control_locked && !sync) { - this.console.warn("Sync mode is locked, cannot do this action") - return false - } - - // if time is provided, seek to that time if (typeof time === "number") { this.console.log(`Seeking to ${time} | Duration: ${this.track_instance.audio.duration}`) @@ -647,24 +442,8 @@ export default class Player extends Core { } } - playbackMode(mode) { - if (typeof mode !== "string") { - return this.state.playback_mode - } - - this.state.playback_mode = mode - - if (this.track_instance) { - this.track_instance.audio.loop = this.state.playback_mode === "repeat" - } - - AudioPlayerStorage.set("mode", mode) - - return mode - } - duration() { - if (!this.track_instance) { + if (!this.track_instance || !this.track_instance.audio) { return false } @@ -687,93 +466,7 @@ export default class Player extends Core { } close() { - this.stop() - this.detachPlayerComponent() - } - - toggleMinimize(to) { - this.state.minimized = to ?? !this.state.minimized - - if (this.state.minimized) { - app.layout.sidebar.attachBottomItem("player", BackgroundMediaPlayer, { - noContainer: true - }) - } else { - app.layout.sidebar.removeBottomItem("player") - } - - return this.state.minimized - } - - toggleCollapse(to) { - if (typeof to !== "boolean") { - this.console.warn("Collapse must be a boolean") - return false - } - - this.state.collapsed = to ?? !this.state.collapsed - - return this.state.collapsed - } - - toggleSyncMode(to, lock) { - if (typeof to !== "boolean") { - this.console.warn("Sync mode must be a boolean") - return false - } - - this.state.syncMode = to ?? !this.state.syncMode - - this.state.syncModeLocked = lock ?? false - - this.console.log(`Sync mode is now ${this.state.syncMode ? "enabled" : "disabled"} | Locked: ${this.state.syncModeLocked ? "yes" : "no"}`) - - return this.state.syncMode - } - - toggleMute(to) { - if (typeof to !== "boolean") { - to = !this.state.muted - } - - return this.mute(to) - } - - async setSampleRate(to) { - // must be a integer - if (typeof to !== "number") { - this.console.error("Sample rate must be a number") - return this.audioContext.sampleRate - } - - // must be a integer - if (!Number.isInteger(to)) { - this.console.error("Sample rate must be a integer") - return this.audioContext.sampleRate - } - - return await new Promise((resolve, reject) => { - app.confirm({ - title: "Change sample rate", - content: `To change the sample rate, the app needs to be reloaded. Do you want to continue?`, - onOk: () => { - try { - this.audioContext = new AudioContext({ sampleRate: to }) - - AudioPlayerStorage.set("sample_rate", to) - - app.navigation.reload() - - return resolve(this.audioContext.sampleRate) - } catch (error) { - app.message.error(`Failed to change sample rate, ${error.message}`) - return resolve(this.audioContext.sampleRate) - } - }, - onCancel: () => { - return resolve(this.audioContext.sampleRate) - } - }) - }) + this.stopPlayback() + this.ui.detachPlayerComponent() } } \ No newline at end of file diff --git a/packages/app/src/cores/player/processors/compressorNode/index.js b/packages/app/src/cores/player/processors/compressorNode/index.js index 26439949..62be15bd 100755 --- a/packages/app/src/cores/player/processors/compressorNode/index.js +++ b/packages/app/src/cores/player/processors/compressorNode/index.js @@ -1,6 +1,6 @@ import { Modal } from "antd" import ProcessorNode from "../node" -import Presets from "../../presets" +import Presets from "../../classes/Presets" export default class CompressorProcessorNode extends ProcessorNode { constructor(props) { diff --git a/packages/app/src/cores/player/processors/eqNode/index.js b/packages/app/src/cores/player/processors/eqNode/index.js index 88f4066e..a7f7d739 100755 --- a/packages/app/src/cores/player/processors/eqNode/index.js +++ b/packages/app/src/cores/player/processors/eqNode/index.js @@ -1,6 +1,6 @@ import { Modal } from "antd" import ProcessorNode from "../node" -import Presets from "../../presets" +import Presets from "../../classes/Presets" export default class EqProcessorNode extends ProcessorNode { constructor(props) { diff --git a/packages/app/src/cores/player/processors/gainNode/index.js b/packages/app/src/cores/player/processors/gainNode/index.js index 1098d753..dc00fa59 100755 --- a/packages/app/src/cores/player/processors/gainNode/index.js +++ b/packages/app/src/cores/player/processors/gainNode/index.js @@ -35,7 +35,7 @@ export default class GainProcessorNode extends ProcessorNode { applyValues() { // apply to current instance - this.processor.gain.value = app.cores.player.volume() * this.state.gain + this.processor.gain.value = app.cores.player.state.volume * this.state.gain } async init() { diff --git a/packages/app/src/cores/remoteStorage/chunkedUpload.js b/packages/app/src/cores/remoteStorage/chunkedUpload.js index 7a934d0a..b17f4605 100644 --- a/packages/app/src/cores/remoteStorage/chunkedUpload.js +++ b/packages/app/src/cores/remoteStorage/chunkedUpload.js @@ -1,178 +1,139 @@ import { EventBus } from "vessel" -import SessionModel from "@models/session" export default class ChunkedUpload { constructor(params) { - this.endpoint = params.endpoint - this.file = params.file - this.headers = params.headers || {} - this.postParams = params.postParams - this.service = params.service ?? "default" - this.retries = params.retries ?? app.cores.settings.get("uploader.retries") ?? 3 - this.delayBeforeRetry = params.delayBeforeRetry || 5 + const { + endpoint, + file, + headers = {}, + splitChunkSize = 1024 * 1024 * 10, + maxRetries = 3, + delayBeforeRetry = 5, + } = params + + if (!endpoint) { + throw new Error("Missing endpoint") + } + + if (!file instanceof File) { + throw new Error("Invalid or missing file") + } + + if (typeof headers !== "object") { + throw new Error("Invalid headers") + } + + if (splitChunkSize <= 0) { + throw new Error("Invalid splitChunkSize") + } - this.start = 0 - this.chunk = null this.chunkCount = 0 - - this.splitChunkSize = params.splitChunkSize || 1024 * 1024 * 10 - this.totalChunks = Math.ceil(this.file.size / this.splitChunkSize) - this.retriesCount = 0 - this.offline = false - this.paused = false - this.headers["Authorization"] = `Bearer ${SessionModel.token}` - this.headers["uploader-original-name"] = encodeURIComponent(this.file.name) - this.headers["uploader-file-id"] = this.uniqid(this.file) - this.headers["uploader-chunks-total"] = this.totalChunks - this.headers["provider-type"] = this.service - this.headers["chunk-size"] = this.splitChunkSize + this.splitChunkSize = splitChunkSize + this.totalChunks = Math.ceil(file.size / splitChunkSize) - this._reader = new FileReader() - this.eventBus = new EventBus() + this.maxRetries = maxRetries + this.delayBeforeRetry = delayBeforeRetry + this.offline = this.paused = false - this.validateParams() + this.endpoint = endpoint + this.file = file + this.headers = { + ...headers, + "uploader-original-name": encodeURIComponent(file.name), + "uploader-file-id": this.getFileUID(file), + "uploader-chunks-total": this.totalChunks, + "chunk-size": splitChunkSize + } + + this.setupListeners() this.nextSend() console.debug("[Uploader] Created", { - splitChunkSize: this.splitChunkSize, + splitChunkSize: splitChunkSize, totalChunks: this.totalChunks, - totalSize: this.file.size, - }) - - // restart sync when back online - // trigger events when offline/back online - window.addEventListener("online", () => { - if (!this.offline) return - - this.offline = false - this.eventBus.emit("online") - this.nextSend() - }) - - window.addEventListener("offline", () => { - this.offline = true - this.eventBus.emit("offline") + totalSize: file.size }) } - on(event, fn) { - this.eventBus.on(event, fn) + _reader = new FileReader() + events = new EventBus() + + setupListeners() { + window.addEventListener("online", () => !this.offline && (this.offline = false, this.events.emit("online"), this.nextSend())) + window.addEventListener("offline", () => (this.offline = true, this.events.emit("offline"))) } - validateParams() { - if (!this.endpoint || !this.endpoint.length) throw new TypeError("endpoint must be defined") - if (this.file instanceof File === false) throw new TypeError("file must be a File object") - if (this.headers && typeof this.headers !== "object") throw new TypeError("headers must be null or an object") - if (this.postParams && typeof this.postParams !== "object") throw new TypeError("postParams must be null or an object") - if (this.splitChunkSize && (typeof this.splitChunkSize !== "number" || this.splitChunkSize === 0)) throw new TypeError("splitChunkSize must be a positive number") - if (this.retries && (typeof this.retries !== "number" || this.retries === 0)) throw new TypeError("retries must be a positive number") - if (this.delayBeforeRetry && (typeof this.delayBeforeRetry !== "number")) throw new TypeError("delayBeforeRetry must be a positive number") - } - - uniqid(file) { - return Math.floor(Math.random() * 100000000) + Date.now() + this.file.size + "_tmp" + getFileUID(file) { + return Math.floor(Math.random() * 100000000) + Date.now() + file.size + "_tmp" } loadChunk() { return new Promise((resolve) => { - const length = this.totalChunks === 1 ? this.file.size : this.splitChunkSize - const start = length * this.chunkCount + const start = this.chunkCount * this.splitChunkSize + const end = Math.min(start + this.splitChunkSize, this.file.size) - this._reader.onload = () => { - this.chunk = new Blob([this._reader.result], { type: "application/octet-stream" }) - resolve() - } - - this._reader.readAsArrayBuffer(this.file.slice(start, start + length)) + this._reader.onload = () => resolve(new Blob([this._reader.result], { type: "application/octet-stream" })) + this._reader.readAsArrayBuffer(this.file.slice(start, end)) }) } - sendChunk() { + async sendChunk() { const form = new FormData() - // send post fields on last request - if (this.chunkCount + 1 === this.totalChunks && this.postParams) Object.keys(this.postParams).forEach(key => form.append(key, this.postParams[key])) - form.append("file", this.chunk) this.headers["uploader-chunk-number"] = this.chunkCount - return fetch(this.endpoint, { method: "POST", headers: this.headers, body: form }) + try { + const res = await fetch( + this.endpoint, + { + method: "POST", + headers: this.headers, + body: form + }) + + return res + } catch (error) { + this.manageRetries() + } } manageRetries() { - if (this.retriesCount++ < this.retries) { + if (++this.retriesCount < this.maxRetries) { setTimeout(() => this.nextSend(), this.delayBeforeRetry * 1000) - this.eventBus.emit("fileRetry", { - message: `An error occured uploading chunk ${this.chunkCount}. ${this.retries - this.retriesCount} retries left`, - chunk: this.chunkCount, - retriesLeft: this.retries - this.retriesCount - }) - - return + this.events.emit("fileRetry", { message: `Retrying chunk ${this.chunkCount}`, chunk: this.chunkCount, retriesLeft: this.retries - this.retriesCount }) + } else { + this.events.emit("error", { message: `No more retries for chunk ${this.chunkCount}` }) } - - this.eventBus.emit("error", { - message: `An error occured uploading chunk ${this.chunkCount}. No more retries, stopping upload` - }) } async nextSend() { if (this.paused || this.offline) { - return + return null } - await this.loadChunk() + this.chunk = await this.loadChunk() + const res = await this.sendChunk() - .catch((err) => { - if (this.paused || this.offline) return - this.console.error(err) - - // this type of error can happen after network disconnection on CORS setup - this.manageRetries() - }) - - if (res.status === 200 || res.status === 201 || res.status === 204) { + if ([200, 201, 204].includes(res.status)) { if (++this.chunkCount < this.totalChunks) { this.nextSend() } else { - res.json().then((body) => { - this.eventBus.emit("finish", body) - }) + res.json().then((body) => this.events.emit("finish", body)) } - const percentProgress = Math.round((100 / this.totalChunks) * this.chunkCount) - - this.eventBus.emit("progress", { - percentProgress + this.events.emit("progress", { + percentProgress: Math.round((100 / this.totalChunks) * this.chunkCount) }) - } - - // errors that might be temporary, wait a bit then retry - else if ([408, 502, 503, 504].includes(res.status)) { - if (this.paused || this.offline) return - + } else if ([408, 502, 503, 504].includes(res.status)) { this.manageRetries() - } - - else { - if (this.paused || this.offline) return - - try { - res.json().then((body) => { - this.eventBus.emit("error", { - message: `[${res.status}] ${body.error ?? body.message}` - }) - }) - } catch (error) { - this.eventBus.emit("error", { - message: `[${res.status}] ${res.statusText}` - }) - } + } else { + res.json().then((body) => this.events.emit("error", { message: `[${res.status}] ${body.error ?? body.message}` })) } } @@ -180,7 +141,7 @@ export default class ChunkedUpload { this.paused = !this.paused if (!this.paused) { - this.nextSend() + return this.nextSend() } } -} \ No newline at end of file +} diff --git a/packages/app/src/cores/remoteStorage/remoteStorage.core.js b/packages/app/src/cores/remoteStorage/remoteStorage.core.js index a8d0590e..e94de69a 100755 --- a/packages/app/src/cores/remoteStorage/remoteStorage.core.js +++ b/packages/app/src/cores/remoteStorage/remoteStorage.core.js @@ -1,6 +1,7 @@ import { Core } from "vessel" import ChunkedUpload from "./chunkedUpload" +import SessionModel from "@models/session" export default class RemoteStorage extends Core { static namespace = "remoteStorage" @@ -26,18 +27,24 @@ export default class RemoteStorage extends Core { onFinish = () => { }, onError = () => { }, service = "standard", + headers = {}, } = {}, ) { return await new Promise((_resolve, _reject) => { const fn = async () => new Promise((resolve, reject) => { const uploader = new ChunkedUpload({ endpoint: `${app.cores.api.client().mainOrigin}/upload/chunk`, - splitChunkSize: 5 * 1024 * 1024, + splitChunkSize: 5 * 1024 * 1024, file: file, service: service, + headers: { + ...headers, + "provider-type": service, + "Authorization": `Bearer ${SessionModel.token}`, + }, }) - uploader.on("error", ({ message }) => { + uploader.events.on("error", ({ message }) => { this.console.error("[Uploader] Error", message) app.cores.notifications.new({ @@ -55,13 +62,13 @@ export default class RemoteStorage extends Core { _reject(message) }) - uploader.on("progress", ({ percentProgress }) => { + uploader.events.on("progress", ({ percentProgress }) => { if (typeof onProgress === "function") { onProgress(file, percentProgress) } }) - uploader.on("finish", (data) => { + uploader.events.on("finish", (data) => { this.console.debug("[Uploader] Finish", data) app.cores.notifications.new({ diff --git a/packages/app/src/hooks/usePageWidgets/index.js b/packages/app/src/hooks/usePageWidgets/index.js new file mode 100644 index 00000000..8850cf77 --- /dev/null +++ b/packages/app/src/hooks/usePageWidgets/index.js @@ -0,0 +1,17 @@ +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) + } + + return () => { + for (const widget of widgets) { + app.layout.tools_bar.detachRender(widget.id) + } + } + }) +} + +export default usePageWidgets \ No newline at end of file diff --git a/packages/app/src/layouts/components/@mobile/bottomBar/index.jsx b/packages/app/src/layouts/components/@mobile/bottomBar/index.jsx index 623a2a71..a2f10c02 100755 --- a/packages/app/src/layouts/components/@mobile/bottomBar/index.jsx +++ b/packages/app/src/layouts/components/@mobile/bottomBar/index.jsx @@ -120,14 +120,6 @@ const AccountButton = React.forwardRef((props, ref) => { }) -export default (props) => { - return - - -} - export class BottomBar extends React.Component { static contextType = Context @@ -418,4 +410,12 @@ export class BottomBar extends React.Component { } +} + +export default (props) => { + return + + } \ No newline at end of file diff --git a/packages/app/src/layouts/components/draggableDrawer/index.jsx b/packages/app/src/layouts/components/draggableDrawer/index.jsx index 8f6df9a9..0dea3bd0 100644 --- a/packages/app/src/layouts/components/draggableDrawer/index.jsx +++ b/packages/app/src/layouts/components/draggableDrawer/index.jsx @@ -11,7 +11,7 @@ export class DraggableDrawerController extends React.Component { this.interface = { open: this.open, - close: this.close, + destroy: this.destroy, actions: this.actions, } @@ -100,9 +100,15 @@ export class DraggableDrawerController extends React.Component { this.setState({ drawers: drawers }) - app.cores.window_mng.close(drawer.winId) + app.cores.window_mng.close(drawer.id ?? id) } + /** + * This lifecycle method is called after the component has been updated. + * It will toggle the root scale effect based on the amount of drawers. + * If there are no drawers, the root scale effect is disabled. + * If there are one or more drawers, the root scale effect is enabled. + */ componentDidUpdate() { if (this.state.drawers.length === 0) { app.layout.toggleRootScaleEffect(false) diff --git a/packages/app/src/layouts/components/drawer/index.less b/packages/app/src/layouts/components/drawer/index.less index f0ce4227..e407cf9b 100644 --- a/packages/app/src/layouts/components/drawer/index.less +++ b/packages/app/src/layouts/components/drawer/index.less @@ -17,6 +17,10 @@ height: 100dvh; height: 100vh; + + &.hidden { + display: none; + } } .drawers-mask { diff --git a/packages/app/src/layouts/components/modals/index.jsx b/packages/app/src/layouts/components/modals/index.jsx index 21c43b65..60af8c2b 100755 --- a/packages/app/src/layouts/components/modals/index.jsx +++ b/packages/app/src/layouts/components/modals/index.jsx @@ -97,7 +97,7 @@ export default () => { confirmOnClickContent={confirmOnClickContent} > { - React.createElement(render, props) + React.isValidElement(render) ? React.cloneElement(render, props) : React.createElement(render, props) } ) } diff --git a/packages/app/src/layouts/components/sidebar/index.jsx b/packages/app/src/layouts/components/sidebar/index.jsx index 98e18fd2..0b0405c0 100755 --- a/packages/app/src/layouts/components/sidebar/index.jsx +++ b/packages/app/src/layouts/components/sidebar/index.jsx @@ -336,7 +336,12 @@ export default class Sidebar extends React.Component { const selectedKeyId = this.state.selectedMenuItem?.id return
    diff --git a/packages/app/src/layouts/components/sidebar/index.less b/packages/app/src/layouts/components/sidebar/index.less index 80a3f7c6..07925180 100755 --- a/packages/app/src/layouts/components/sidebar/index.less +++ b/packages/app/src/layouts/components/sidebar/index.less @@ -26,6 +26,11 @@ gap: 10px; padding: 10px; + + &.hidden { + padding: 0; + width: 0px; + } } .app_sidebar { diff --git a/packages/app/src/layouts/components/toolsBar/index.jsx b/packages/app/src/layouts/components/toolsBar/index.jsx index ee9c8270..0c49541b 100755 --- a/packages/app/src/layouts/components/toolsBar/index.jsx +++ b/packages/app/src/layouts/components/toolsBar/index.jsx @@ -78,32 +78,6 @@ export default class ToolsBar extends React.Component { id="tools_bar" className="tools-bar" > - {/*
    -
    -

    - - {(t) => t("Trendings")} -

    -
    - - -
    - -
    -
    -

    - - {(t) => t("Online Friends")} -

    -
    - - -
    - - */} - - -
    { this.state.renders.map((render) => { @@ -111,6 +85,8 @@ export default class ToolsBar extends React.Component { }) }
    + +
    }} diff --git a/packages/app/src/layouts/components/toolsBar/index.less b/packages/app/src/layouts/components/toolsBar/index.less index 96933b7f..b3165016 100755 --- a/packages/app/src/layouts/components/toolsBar/index.less +++ b/packages/app/src/layouts/components/toolsBar/index.less @@ -45,67 +45,13 @@ flex: 0; - .card { - display: flex; - flex-direction: column; - - background-color: var(--background-color-primary); - border-radius: 12px; - - padding: 20px; - - isolation: isolate; - - h1, - h2 { - width: fit-content; - margin: 0; - } - - &.header { - position: relative; - - display: flex; - flex-direction: row; - - justify-content: space-between; - align-items: center; - - width: 100%; - - margin-bottom: 10px; - - z-index: 150; - - -webkit-box-shadow: 0 0 0 1px rgba(63, 63, 68, 0.05), 0 1px 3px 0 var(--shadow-color); - -moz-box-shadow: 0 0 0 1px rgba(63, 63, 68, 0.05), 0 1px 3px 0 var(--shadow-color); - box-shadow: 0 0 0 1px rgba(63, 63, 68, 0.05), 0 1px 3px 0 var(--shadow-color); - } - - &.content { - position: relative; - - transform: translateY(-30px); - padding-top: 35px; - - z-index: 45; - } - } - .attached_renders { - position: sticky; - - bottom: 0; - right: 0; - display: flex; flex-direction: column; align-items: center; - justify-content: flex-end; width: 100%; - height: 100%; gap: 10px; diff --git a/packages/app/src/layouts/default/index.jsx b/packages/app/src/layouts/default/index.jsx index 4e7c33b8..e8040ec5 100755 --- a/packages/app/src/layouts/default/index.jsx +++ b/packages/app/src/layouts/default/index.jsx @@ -1,6 +1,6 @@ import React from "react" import classnames from "classnames" -import { Layout, Alert } from "antd" +import { Layout } from "antd" import Sidebar from "@layouts/components/sidebar" import ToolsBar from "@layouts/components/toolsBar" @@ -20,6 +20,7 @@ const DesktopLayout = (props) => { return <> + diff --git a/packages/app/src/pages/@mobile-views/player/index.jsx b/packages/app/src/pages/@mobile-views/player/index.jsx index 357918b3..60238bc8 100755 --- a/packages/app/src/pages/@mobile-views/player/index.jsx +++ b/packages/app/src/pages/@mobile-views/player/index.jsx @@ -6,7 +6,7 @@ import SeekBar from "@components/Player/SeekBar" import Controls from "@components/Player/Controls" import ExtraActions from "@components/Player/ExtraActions" -import { WithPlayerContext, Context } from "@contexts/WithPlayerContext" +import { usePlayerStateContext } from "@contexts/WithPlayerContext" import RGBStringToValues from "@utils/rgbToValues" import "./index.less" @@ -29,22 +29,14 @@ const ServiceIndicator = (props) => { } const AudioPlayer = (props) => { - return - - -} - -const AudioPlayerComponent = (props) => { - const ctx = React.useContext(Context) + const playerState = usePlayerStateContext() React.useEffect(() => { if (app.currentDragger) { - app.currentDragger.setBackgroundColorValues(RGBStringToValues(ctx.track_manifest?.cover_analysis?.rgb)) + app.currentDragger.setBackgroundColorValues(RGBStringToValues(playerState.track_manifest?.cover_analysis?.rgb)) } - }, [ctx.track_manifest?.cover_analysis]) + }, [playerState.track_manifest?.cover_analysis]) const { title, @@ -54,10 +46,10 @@ const AudioPlayerComponent = (props) => { lyricsEnabled, cover_analysis, cover, - } = ctx.track_manifest ?? {} + } = playerState.track_manifest ?? {} - const playing = ctx.playback_status === "playing" - const stopped = ctx.playback_status === "stopped" + const playing = playerState.playback_status === "playing" + const stopped = playerState.playback_status === "stopped" const titleText = (!playing && stopped) ? "Stopped" : (title ?? "Untitled") const subtitleText = `${artist} | ${album?.title ?? album}` @@ -107,10 +99,10 @@ const AudioPlayerComponent = (props) => { diff --git a/packages/app/src/pages/account/tabs/details/index.jsx b/packages/app/src/pages/account/tabs/details/index.jsx index c0270faa..4d0a26f9 100755 --- a/packages/app/src/pages/account/tabs/details/index.jsx +++ b/packages/app/src/pages/account/tabs/details/index.jsx @@ -33,8 +33,8 @@ const DroppableField = (props) => {
    { collapsed - ? - : + ? + : }
    diff --git a/packages/app/src/pages/auth/forms/selector/index.jsx b/packages/app/src/pages/auth/forms/selector/index.jsx index 3f4dfcf0..3c09e0cc 100755 --- a/packages/app/src/pages/auth/forms/selector/index.jsx +++ b/packages/app/src/pages/auth/forms/selector/index.jsx @@ -54,7 +54,7 @@ const MainSelector = (props) => { Create a Comty™ Account -

    +

    Registering a new account accepts the app.location.push("/terms")}>Terms and Conditions and app.location.push("/privacy")}>Privacy policy for the services provided by {config.author}

    diff --git a/packages/app/src/pages/badge/[user_id]/index.jsx b/packages/app/src/pages/badge/[user_id]/index.jsx new file mode 100644 index 00000000..6f5b1bbf --- /dev/null +++ b/packages/app/src/pages/badge/[user_id]/index.jsx @@ -0,0 +1,12 @@ +import React from "react" + +const BadgePage = (props) => { + const user_id = props.params.user_id + + return
    + Badge Page + {user_id} +
    +} + +export default BadgePage \ No newline at end of file diff --git a/packages/app/src/pages/lyrics/components/controller/index.jsx b/packages/app/src/pages/lyrics/components/controller/index.jsx index 5018317b..fa0e2ba8 100644 --- a/packages/app/src/pages/lyrics/components/controller/index.jsx +++ b/packages/app/src/pages/lyrics/components/controller/index.jsx @@ -8,7 +8,7 @@ import useHideOnMouseStop from "@hooks/useHideOnMouseStop" import { Icons } from "@components/Icons" import Controls from "@components/Player/Controls" -import { Context } from "@contexts/WithPlayerContext" +import { usePlayerStateContext } from "@contexts/WithPlayerContext" function isOverflown(element) { if (!element) { @@ -47,7 +47,7 @@ const RenderAlbum = (props) => { } const PlayerController = React.forwardRef((props, ref) => { - const context = React.useContext(Context) + const playerState = usePlayerStateContext() const titleRef = React.useRef() @@ -63,42 +63,42 @@ const PlayerController = React.forwardRef((props, ref) => { async function onDragEnd(seekTime) { setDraggingTime(false) - app.cores.player.seek(seekTime) + app.cores.player.controls.seek(seekTime) syncPlayback() } async function syncPlayback() { - if (!context.track_manifest) { + if (!playerState.track_manifest) { return false } - const currentTrackTime = app.cores.player.seek() + const currentTrackTime = app.cores.player.controls.seek() setCurrentTime(currentTrackTime) } //* Handle when playback status change React.useEffect(() => { - if (context.playback_status === "playing") { + if (playerState.playback_status === "playing") { setSyncInterval(setInterval(syncPlayback, 1000)) } else { if (syncInterval) { clearInterval(syncInterval) } } - }, [context.playback_status]) + }, [playerState.playback_status]) React.useEffect(() => { setTitleIsOverflown(isOverflown(titleRef.current)) - setTrackDuration(app.cores.player.duration()) - }, [context.track_manifest]) + setTrackDuration(app.cores.player.controls.duration()) + }, [playerState.track_manifest]) React.useEffect(() => { syncPlayback() }, []) - const isStopped = context.playback_status === "stopped" + const isStopped = playerState.playback_status === "stopped" return
    { )} > { - context.playback_status === "stopped" ? "Nothing is playing" : <> - {context.track_manifest?.title ?? "Nothing is playing"} + playerState.playback_status === "stopped" ? "Nothing is playing" : <> + {playerState.track_manifest?.title ?? "Nothing is playing"} }

    @@ -143,7 +143,7 @@ const PlayerController = React.forwardRef((props, ref) => { isStopped ? "Nothing is playing" : <> - {context.track_manifest?.title ?? "Untitled"} + {playerState.track_manifest?.title ?? "Untitled"} } @@ -152,9 +152,9 @@ const PlayerController = React.forwardRef((props, ref) => {
    - + - - +
    @@ -189,7 +189,7 @@ const PlayerController = React.forwardRef((props, ref) => {
    { - context.track_manifest?.metadata.lossless && } bordered={false} > @@ -197,7 +197,7 @@ const PlayerController = React.forwardRef((props, ref) => { } { - context.track_manifest?.explicit && Explicit @@ -212,7 +212,7 @@ const PlayerController = React.forwardRef((props, ref) => { } { - props.lyrics?.available_langs &&