mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-10 02:54:15 +00:00
rewrites to lb cores v2
This commit is contained in:
parent
0e5604a466
commit
2efcc8a3cd
@ -1,7 +1,8 @@
|
||||
import Core from "evite/src/core"
|
||||
import config from "config"
|
||||
import { Bridge } from "linebridge/dist/client"
|
||||
import { Session } from "models"
|
||||
|
||||
import config from "config"
|
||||
import { SessionModel } from "models"
|
||||
|
||||
function generateWSFunctionHandler(socket, type = "listen") {
|
||||
if (!socket) {
|
||||
@ -47,15 +48,23 @@ function generateWSFunctionHandler(socket, type = "listen") {
|
||||
}
|
||||
|
||||
export default class ApiCore extends Core {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
static namespace = "api"
|
||||
|
||||
this.namespaces = Object()
|
||||
excludedExpiredExceptionURL = ["/session/regenerate"]
|
||||
|
||||
this.onExpiredExceptionEvent = false
|
||||
this.excludedExpiredExceptionURL = ["/regenerate_session_token"]
|
||||
onExpiredExceptionEvent = false
|
||||
|
||||
this.ctx.registerPublicMethod("api", this)
|
||||
namespaces = Object()
|
||||
|
||||
public = {
|
||||
namespaces: this.namespaces,
|
||||
customRequest: this.customRequest,
|
||||
request: this.request,
|
||||
withEndpoints: this.withEndpoints,
|
||||
attachBridge: this.attachBridge,
|
||||
detachBridge: this.detachBridge,
|
||||
createBridge: this.createBridge,
|
||||
autenticateWS: this.autenticateWS,
|
||||
}
|
||||
|
||||
async customRequest(
|
||||
@ -83,7 +92,7 @@ export default class ApiCore extends Core {
|
||||
payload.headers = {}
|
||||
}
|
||||
|
||||
const sessionToken = await Session.token
|
||||
const sessionToken = await SessionModel.token
|
||||
|
||||
if (sessionToken) {
|
||||
payload.headers["Authorization"] = `Bearer ${sessionToken}`
|
||||
@ -95,7 +104,7 @@ export default class ApiCore extends Core {
|
||||
return await this.namespaces[namepace].httpInterface(payload, ...args)
|
||||
}
|
||||
|
||||
request = (namespace = "main", method, endpoint, ...args) => {
|
||||
request(namespace = "main", method, endpoint, ...args) {
|
||||
if (!this.namespaces[namespace]) {
|
||||
throw new Error(`Namespace ${namespace} not found`)
|
||||
}
|
||||
@ -111,7 +120,7 @@ export default class ApiCore extends Core {
|
||||
return this.namespaces[namespace].endpoints[method][endpoint](...args)
|
||||
}
|
||||
|
||||
withEndpoints = (namespace = "main") => {
|
||||
withEndpoints(namespace = "main") {
|
||||
if (!this.namespaces[namespace]) {
|
||||
throw new Error(`Namespace ${namespace} not found`)
|
||||
}
|
||||
@ -119,7 +128,7 @@ export default class ApiCore extends Core {
|
||||
return this.namespaces[namespace].endpoints
|
||||
}
|
||||
|
||||
handleBeforeRequest = async (request) => {
|
||||
async handleBeforeRequest(request) {
|
||||
if (this.onExpiredExceptionEvent) {
|
||||
if (this.excludedExpiredExceptionURL.includes(request.url)) return
|
||||
|
||||
@ -132,19 +141,21 @@ export default class ApiCore extends Core {
|
||||
}
|
||||
}
|
||||
|
||||
handleRegenerationEvent = async (refreshToken, makeRequest) => {
|
||||
async handleRegenerationEvent(refreshToken, makeRequest) {
|
||||
window.app.eventBus.emit("session.expiredExceptionEvent", refreshToken)
|
||||
|
||||
this.onExpiredExceptionEvent = true
|
||||
|
||||
const expiredToken = await Session.token
|
||||
|
||||
// exclude regeneration endpoint
|
||||
const expiredToken = await SessionModel.token
|
||||
|
||||
// send request to regenerate token
|
||||
const response = await this.request("main", "post", "regenerateSessionToken", {
|
||||
expiredToken: expiredToken,
|
||||
refreshToken,
|
||||
const response = await this.customRequest("main", {
|
||||
method: "POST",
|
||||
url: "/session/regenerate",
|
||||
data: {
|
||||
expiredToken: expiredToken,
|
||||
refreshToken,
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error(`Failed to regenerate token: ${error.message}`)
|
||||
return false
|
||||
@ -155,7 +166,7 @@ export default class ApiCore extends Core {
|
||||
}
|
||||
|
||||
// set new token
|
||||
Session.token = response.token
|
||||
SessionModel.token = response.token
|
||||
|
||||
//this.namespaces["main"].internalAbortController.abort()
|
||||
|
||||
@ -165,18 +176,18 @@ export default class ApiCore extends Core {
|
||||
window.app.eventBus.emit("session.regenerated")
|
||||
}
|
||||
|
||||
attachBridge = (key, params) => {
|
||||
attachBridge(key, params) {
|
||||
return this.namespaces[key] = this.createBridge(params)
|
||||
}
|
||||
|
||||
detachBridge = (key) => {
|
||||
detachBridge(key) {
|
||||
return delete this.namespaces[key]
|
||||
}
|
||||
|
||||
createBridge(params = {}) {
|
||||
const getSessionContext = async () => {
|
||||
const obj = {}
|
||||
const token = await Session.token
|
||||
const token = await SessionModel.token
|
||||
|
||||
if (token) {
|
||||
// append token to context
|
||||
@ -224,7 +235,7 @@ export default class ApiCore extends Core {
|
||||
|
||||
const bridge = new Bridge(bridgeOptions)
|
||||
|
||||
// handle main ws events
|
||||
// handle main ws onEvents
|
||||
const mainWSSocket = bridge.wsInterface.sockets["main"]
|
||||
|
||||
mainWSSocket.on("authenticated", () => {
|
||||
@ -236,16 +247,23 @@ export default class ApiCore extends Core {
|
||||
})
|
||||
|
||||
mainWSSocket.on("connect", () => {
|
||||
this.ctx.eventBus.emit(`api.ws.main.connect`)
|
||||
if (this.ctx.eventBus) {
|
||||
this.ctx.eventBus.emit(`api.ws.main.connect`)
|
||||
}
|
||||
|
||||
this.autenticateWS(mainWSSocket)
|
||||
})
|
||||
|
||||
mainWSSocket.on("disconnect", (...context) => {
|
||||
this.ctx.eventBus.emit(`api.ws.main.disconnect`, ...context)
|
||||
if (this.ctx.eventBus) {
|
||||
this.ctx.eventBus.emit(`api.ws.main.disconnect`, ...context)
|
||||
}
|
||||
})
|
||||
|
||||
mainWSSocket.on("connect_error", (...context) => {
|
||||
this.ctx.eventBus.emit(`api.ws.main.connect_error`, ...context)
|
||||
if (this.ctx.eventBus) {
|
||||
this.ctx.eventBus.emit(`api.ws.main.connect_error`, ...context)
|
||||
}
|
||||
})
|
||||
|
||||
// generate functions
|
||||
@ -256,8 +274,8 @@ export default class ApiCore extends Core {
|
||||
return bridge
|
||||
}
|
||||
|
||||
autenticateWS = async (socket) => {
|
||||
const token = await Session.token
|
||||
async autenticateWS(socket) {
|
||||
const token = await SessionModel.token
|
||||
|
||||
if (token) {
|
||||
socket.emit("authenticate", {
|
||||
|
@ -1,271 +0,0 @@
|
||||
import Core from "evite/src/core"
|
||||
|
||||
import React from "react"
|
||||
import { Howl } from "howler"
|
||||
|
||||
import { EmbbededMediaPlayer } from "components"
|
||||
import { DOMWindow } from "components/RenderWindow"
|
||||
|
||||
export default class AudioPlayerCore extends Core {
|
||||
audioMuted = false
|
||||
audioVolume = 1
|
||||
|
||||
audioQueueHistory = []
|
||||
audioQueue = []
|
||||
currentAudio = null
|
||||
|
||||
currentDomWindow = null
|
||||
|
||||
preloadAudioDebounce = null
|
||||
|
||||
publicMethods = {
|
||||
AudioPlayer: this,
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
app.eventBus.on("audioPlayer.end", () => {
|
||||
this.nextAudio()
|
||||
})
|
||||
}
|
||||
|
||||
toogleMute() {
|
||||
this.audioMuted = !this.audioMuted
|
||||
|
||||
if (this.currentAudio) {
|
||||
this.currentAudio.instance.mute(this.audioMuted)
|
||||
}
|
||||
|
||||
// apply to all audio in queue
|
||||
this.audioQueue.forEach((audio) => {
|
||||
audio.instance.mute(this.audioMuted)
|
||||
})
|
||||
|
||||
app.eventBus.emit("audioPlayer.muted", this.audioMuted)
|
||||
|
||||
return this.audioMuted
|
||||
}
|
||||
|
||||
setVolume(volume) {
|
||||
if (typeof volume !== "number") {
|
||||
console.warn("Volume must be a number")
|
||||
return false
|
||||
}
|
||||
|
||||
if (volume > 1) {
|
||||
volume = 1
|
||||
}
|
||||
|
||||
if (volume < 0) {
|
||||
volume = 0
|
||||
}
|
||||
|
||||
this.audioVolume = volume
|
||||
|
||||
if (this.currentAudio) {
|
||||
this.currentAudio.instance.volume(volume)
|
||||
}
|
||||
|
||||
// apply to all audio in queue
|
||||
this.audioQueue.forEach((audio) => {
|
||||
audio.instance.volume(volume)
|
||||
})
|
||||
|
||||
app.eventBus.emit("audioPlayer.volumeChanged", volume)
|
||||
|
||||
return volume
|
||||
}
|
||||
|
||||
async preloadAudio() {
|
||||
// debounce to prevent multiple preload
|
||||
if (this.preloadAudioDebounce) {
|
||||
clearTimeout(this.preloadAudioDebounce)
|
||||
}
|
||||
|
||||
this.preloadAudioDebounce = setTimeout(async () => {
|
||||
// load the first 2 audio in queue
|
||||
const audioToLoad = this.audioQueue.slice(0, 2)
|
||||
|
||||
// filter undefined
|
||||
const audioToLoadFiltered = audioToLoad.filter((audio) => audio.instance)
|
||||
|
||||
audioToLoad.forEach(async (audio) => {
|
||||
const audioState = audio.instance.state()
|
||||
|
||||
if (audioState !== "loaded" && audioState !== "loading") {
|
||||
await audio.instance.load()
|
||||
}
|
||||
})
|
||||
}, 600)
|
||||
}
|
||||
|
||||
startPlaylist = async (data) => {
|
||||
if (typeof data === "undefined") {
|
||||
console.warn("No data provided")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
data = [data]
|
||||
}
|
||||
|
||||
await this.clearAudioQueues()
|
||||
|
||||
this.attachEmbbededMediaPlayer()
|
||||
|
||||
for await (const item of data) {
|
||||
const audioInstance = await this.createAudioInstance(item)
|
||||
|
||||
await this.audioQueue.push({
|
||||
data: item,
|
||||
instance: audioInstance,
|
||||
})
|
||||
}
|
||||
await this.preloadAudio()
|
||||
|
||||
this.currentAudio = this.audioQueue.shift()
|
||||
|
||||
this.playCurrentAudio()
|
||||
}
|
||||
|
||||
clearAudioQueues() {
|
||||
if (this.currentAudio) {
|
||||
this.currentAudio.instance.stop()
|
||||
}
|
||||
|
||||
this.audioQueueHistory = []
|
||||
this.audioQueue = []
|
||||
this.currentAudio = null
|
||||
}
|
||||
|
||||
async playCurrentAudio() {
|
||||
if (!this.currentAudio) {
|
||||
console.warn("No audio playing")
|
||||
return false
|
||||
}
|
||||
|
||||
const audioState = this.currentAudio.instance.state()
|
||||
|
||||
console.log(`Current Audio State: ${audioState}`)
|
||||
|
||||
// check if the instance is loaded
|
||||
if (audioState !== "loaded") {
|
||||
console.warn("Audio not loaded")
|
||||
|
||||
app.eventBus.emit("audioPlayer.loading", this.currentAudio)
|
||||
|
||||
await this.currentAudio.instance.load()
|
||||
|
||||
app.eventBus.emit("audioPlayer.loaded", this.currentAudio)
|
||||
}
|
||||
|
||||
this.currentAudio.instance.play()
|
||||
}
|
||||
|
||||
pauseAudioQueue() {
|
||||
if (!this.currentAudio) {
|
||||
console.warn("No audio playing")
|
||||
return false
|
||||
}
|
||||
|
||||
this.currentAudio.instance.pause()
|
||||
}
|
||||
|
||||
previousAudio() {
|
||||
// check if there is audio playing
|
||||
if (this.currentAudio) {
|
||||
this.currentAudio.instance.stop()
|
||||
}
|
||||
|
||||
// check if there is audio in queue
|
||||
if (!this.audioQueueHistory[0]) {
|
||||
console.warn("No audio in queue")
|
||||
return false
|
||||
}
|
||||
|
||||
// move current audio to queue
|
||||
this.audioQueue.unshift(this.currentAudio)
|
||||
|
||||
this.currentAudio = this.audioQueueHistory.pop()
|
||||
|
||||
this.playCurrentAudio()
|
||||
}
|
||||
|
||||
nextAudio() {
|
||||
// check if there is audio playing
|
||||
if (this.currentAudio) {
|
||||
this.currentAudio.instance.stop()
|
||||
}
|
||||
|
||||
// check if there is audio in queue
|
||||
if (!this.audioQueue[0]) {
|
||||
console.warn("No audio in queue")
|
||||
|
||||
this.currentAudio = null
|
||||
|
||||
// if there is no audio in queue, close the embbeded media player
|
||||
this.destroyPlayer()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// move current audio to history
|
||||
this.audioQueueHistory.push(this.currentAudio)
|
||||
|
||||
this.currentAudio = this.audioQueue.shift()
|
||||
|
||||
this.playCurrentAudio()
|
||||
|
||||
this.preloadAudio()
|
||||
}
|
||||
|
||||
destroyPlayer() {
|
||||
this.currentDomWindow.destroy()
|
||||
this.currentDomWindow = null
|
||||
}
|
||||
|
||||
async createAudioInstance(data) {
|
||||
const audio = new Howl({
|
||||
src: data.src,
|
||||
preload: false,
|
||||
//html5: true,
|
||||
mute: this.audioMuted,
|
||||
volume: this.audioVolume,
|
||||
onplay: () => {
|
||||
app.eventBus.emit("audioPlayer.playing", data)
|
||||
},
|
||||
onend: () => {
|
||||
app.eventBus.emit("audioPlayer.end", data)
|
||||
},
|
||||
onload: () => {
|
||||
app.eventBus.emit("audioPlayer.preloaded", data)
|
||||
},
|
||||
onpause: () => {
|
||||
app.eventBus.emit("audioPlayer.paused", data)
|
||||
},
|
||||
onstop: () => {
|
||||
app.eventBus.emit("audioPlayer.stopped", data)
|
||||
},
|
||||
onseek: () => {
|
||||
app.eventBus.emit("audioPlayer.seeked", data)
|
||||
},
|
||||
onvolume: () => {
|
||||
app.eventBus.emit("audioPlayer.volumeChanged", data)
|
||||
},
|
||||
})
|
||||
|
||||
return audio
|
||||
}
|
||||
|
||||
attachEmbbededMediaPlayer() {
|
||||
if (this.currentDomWindow) {
|
||||
console.warn("EmbbededMediaPlayer already attached")
|
||||
return false
|
||||
}
|
||||
|
||||
this.currentDomWindow = new DOMWindow({
|
||||
id: "mediaPlayer"
|
||||
})
|
||||
|
||||
this.currentDomWindow.render(<EmbbededMediaPlayer />)
|
||||
}
|
||||
}
|
@ -42,9 +42,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
|
||||
padding: 5px 10px 5px 20px;
|
||||
padding: 10px 10px 10px 20px;
|
||||
|
||||
transition: all 50ms ease-in-out;
|
||||
|
||||
@ -58,7 +56,7 @@
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--background-color-primary2);
|
||||
background-color: var(--background-color-primary-2);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export default class ContextMenuCore extends Core {
|
||||
clickOutsideToClose: true,
|
||||
})
|
||||
|
||||
async initialize() {
|
||||
async onInitialize() {
|
||||
document.addEventListener("contextmenu", this.handleEvent)
|
||||
}
|
||||
|
||||
|
@ -15,14 +15,14 @@ export function extractLocaleFromPath(path = "") {
|
||||
const messageImports = import.meta.glob("schemas/translations/*.json")
|
||||
|
||||
export default class I18nCore extends Core {
|
||||
events = {
|
||||
onEvents = {
|
||||
"changeLanguage": (locale) => {
|
||||
this.loadAsyncLanguage(locale)
|
||||
}
|
||||
}
|
||||
|
||||
initialize = async () => {
|
||||
let locale = app.settings.get("language") ?? DEFAULT_LOCALE
|
||||
onInitialize = async () => {
|
||||
let locale = app.cores.settings.get("language") ?? DEFAULT_LOCALE
|
||||
|
||||
if (!SUPPORTED_LOCALES.includes(locale)) {
|
||||
locale = DEFAULT_LOCALE
|
||||
|
@ -2,7 +2,6 @@ import SettingsCore from "./settings"
|
||||
import APICore from "./api"
|
||||
import StyleCore from "./style"
|
||||
import PermissionsCore from "./permissions"
|
||||
import SearchCore from "./search"
|
||||
import ContextMenuCore from "./contextMenu"
|
||||
|
||||
import I18nCore from "./i18n"
|
||||
@ -10,13 +9,12 @@ import NotificationsCore from "./notifications"
|
||||
import ShortcutsCore from "./shortcuts"
|
||||
import SoundCore from "./sound"
|
||||
|
||||
import AudioPlayer from "./audioPlayer"
|
||||
import Player from "./player"
|
||||
|
||||
// DEFINE LOAD ORDER HERE
|
||||
export default [
|
||||
SettingsCore,
|
||||
APICore,
|
||||
SearchCore,
|
||||
PermissionsCore,
|
||||
StyleCore,
|
||||
I18nCore,
|
||||
@ -24,6 +22,6 @@ export default [
|
||||
NotificationsCore,
|
||||
ShortcutsCore,
|
||||
|
||||
AudioPlayer,
|
||||
Player,
|
||||
ContextMenuCore,
|
||||
]
|
@ -6,7 +6,7 @@ import { Translation } from "react-i18next"
|
||||
import { Haptics } from "@capacitor/haptics"
|
||||
|
||||
export default class NotificationCore extends Core {
|
||||
events = {
|
||||
onEvents = {
|
||||
"changeNotificationsSoundVolume": (value) => {
|
||||
this.playAudio({ soundVolume: value })
|
||||
},
|
||||
@ -17,12 +17,12 @@ export default class NotificationCore extends Core {
|
||||
}
|
||||
}
|
||||
|
||||
publicMethods = {
|
||||
registerToApp = {
|
||||
notification: this
|
||||
}
|
||||
|
||||
getSoundVolume = () => {
|
||||
return (window.app.settings.get("notifications_sound_volume") ?? 50) / 100
|
||||
return (window.app.cores.settings.get("notifications_sound_volume") ?? 50) / 100
|
||||
}
|
||||
|
||||
new = (notification, options = {}) => {
|
||||
@ -31,7 +31,9 @@ export default class NotificationCore extends Core {
|
||||
this.playAudio(options)
|
||||
}
|
||||
|
||||
notify = (notification, options = {}) => {
|
||||
notify = (notification, options = {
|
||||
type: "info"
|
||||
}) => {
|
||||
if (typeof notification === "string") {
|
||||
notification = {
|
||||
title: "New notification",
|
||||
@ -39,20 +41,69 @@ export default class NotificationCore extends Core {
|
||||
}
|
||||
}
|
||||
|
||||
Notf.open({
|
||||
message: <Translation>
|
||||
{(t) => t(notification.title)}
|
||||
</Translation>,
|
||||
description: <Translation>
|
||||
{(t) => t(notification.description)}
|
||||
</Translation>,
|
||||
const notfObj = {
|
||||
duration: notification.duration ?? 4,
|
||||
icon: React.isValidElement(notification.icon) ? notification.icon : (createIconRender(notification.icon) ?? <Icons.Bell />),
|
||||
})
|
||||
}
|
||||
|
||||
if (notification.message) {
|
||||
switch (typeof notification.message) {
|
||||
case "function": {
|
||||
notfObj.message = React.createElement(notification.message)
|
||||
|
||||
break
|
||||
}
|
||||
case "object": {
|
||||
notfObj.message = notification.message
|
||||
|
||||
break
|
||||
}
|
||||
default: {
|
||||
notfObj.message = <Translation>
|
||||
{(t) => t(notification.message)}
|
||||
</Translation>
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (notification.description) {
|
||||
switch (typeof notification.description) {
|
||||
case "function": {
|
||||
notfObj.description = React.createElement(notification.description)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "object": {
|
||||
notfObj.description = notification.description
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
notfObj.description = <Translation>
|
||||
{(t) => t(notification.description)}
|
||||
</Translation>
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (notification.icon) {
|
||||
notfObj.icon = React.isValidElement(notification.icon) ? notification.icon : (createIconRender(notification.icon) ?? <Icons.Bell />)
|
||||
}
|
||||
|
||||
if (typeof Notf[options.type] !== "function") {
|
||||
options.type = "info"
|
||||
}
|
||||
|
||||
return Notf[options.type](notfObj)
|
||||
}
|
||||
|
||||
playHaptic = async (options = {}) => {
|
||||
const vibrationEnabled = options.vibrationEnabled ?? window.app.settings.get("notifications_vibrate")
|
||||
const vibrationEnabled = options.vibrationEnabled ?? window.app.cores.settings.get("notifications_vibrate")
|
||||
|
||||
if (vibrationEnabled) {
|
||||
await Haptics.vibrate()
|
||||
@ -60,7 +111,7 @@ export default class NotificationCore extends Core {
|
||||
}
|
||||
|
||||
playAudio = (options = {}) => {
|
||||
const soundEnabled = options.soundEnabled ?? window.app.settings.get("notifications_sound")
|
||||
const soundEnabled = options.soundEnabled ?? window.app.cores.settings.get("notifications_sound")
|
||||
const soundVolume = options.soundVolume ? options.soundVolume / 100 : this.getSoundVolume()
|
||||
|
||||
if (soundEnabled) {
|
||||
|
@ -6,20 +6,22 @@ import SessionModel from "models/session"
|
||||
export default class PermissionsCore extends Core {
|
||||
static namespace = "permissions"
|
||||
static dependencies = ["api"]
|
||||
static public = ["hasAdmin", "checkUserIdIsSelf", "hasPermission"]
|
||||
|
||||
userData = null
|
||||
isUserAdmin = null
|
||||
public = {
|
||||
hasAdmin: this.hasAdmin,
|
||||
checkUserIdIsSelf: this.checkUserIdIsSelf,
|
||||
hasPermission: this.hasPermission,
|
||||
}
|
||||
|
||||
hasAdmin = async () => {
|
||||
async hasAdmin() {
|
||||
return await UserModel.hasAdmin()
|
||||
}
|
||||
|
||||
checkUserIdIsSelf = (userId) => {
|
||||
return SessionModel.user_id === userId
|
||||
checkUserIdIsSelf(user_id) {
|
||||
return SessionModel.user_id === user_id
|
||||
}
|
||||
|
||||
hasPermission = async (permission) => {
|
||||
async hasPermission(permission) {
|
||||
let query = []
|
||||
|
||||
if (Array.isArray(permission)) {
|
||||
|
715
packages/app/src/cores/player/index.jsx
Normal file
715
packages/app/src/cores/player/index.jsx
Normal file
@ -0,0 +1,715 @@
|
||||
import Core from "evite/src/core"
|
||||
import { Observable } from "object-observer"
|
||||
import store from "store"
|
||||
// import { createRealTimeBpmProcessor } from "realtime-bpm-analyzer"
|
||||
|
||||
import { EmbbededMediaPlayer } from "components"
|
||||
import { DOMWindow } from "components/RenderWindow"
|
||||
|
||||
class AudioPlayerStorage {
|
||||
static storeKey = "audioPlayer"
|
||||
|
||||
static get(key) {
|
||||
const data = store.get(AudioPlayerStorage.storeKey)
|
||||
|
||||
if (data) {
|
||||
return data[key]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
static set(key, value) {
|
||||
const data = store.get(AudioPlayerStorage.storeKey) ?? {}
|
||||
|
||||
data[key] = value
|
||||
|
||||
store.set(AudioPlayerStorage.storeKey, data)
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
export default class Player extends Core {
|
||||
static namespace = "player"
|
||||
|
||||
currentDomWindow = null
|
||||
|
||||
audioContext = new AudioContext()
|
||||
|
||||
audioQueueHistory = []
|
||||
audioQueue = []
|
||||
audioProcessors = []
|
||||
|
||||
currentAudioInstance = null
|
||||
|
||||
state = Observable.from({
|
||||
loading: false,
|
||||
audioMuted: AudioPlayerStorage.get("mute") ?? false,
|
||||
playbackMode: AudioPlayerStorage.get("mode") ?? "repeat",
|
||||
audioVolume: AudioPlayerStorage.get("volume") ?? 0.3,
|
||||
velocity: AudioPlayerStorage.get("velocity") ?? 1,
|
||||
|
||||
currentAudioManifest: null,
|
||||
playbackStatus: "stopped",
|
||||
crossfading: false,
|
||||
trackBPM: 0,
|
||||
})
|
||||
|
||||
public = {
|
||||
audioContext: this.audioContext,
|
||||
attachPlayerComponent: this.attachPlayerComponent.bind(this),
|
||||
detachPlayerComponent: this.detachPlayerComponent.bind(this),
|
||||
toogleMute: this.toogleMute.bind(this),
|
||||
volume: this.volume.bind(this),
|
||||
start: this.start.bind(this),
|
||||
startPlaylist: this.startPlaylist.bind(this),
|
||||
playback: {
|
||||
mode: function (mode) {
|
||||
if (mode) {
|
||||
this.state.playbackMode = mode
|
||||
}
|
||||
|
||||
return this.state.playbackMode
|
||||
}.bind(this),
|
||||
toogle: function () {
|
||||
if (!this.currentAudioInstance) {
|
||||
console.error("No audio instance")
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.currentAudioInstance.audioElement.paused) {
|
||||
this.public.playback.play()
|
||||
} else {
|
||||
this.public.playback.pause()
|
||||
}
|
||||
}.bind(this),
|
||||
play: function () {
|
||||
if (!this.currentAudioInstance) {
|
||||
console.error("No audio instance")
|
||||
return null
|
||||
}
|
||||
|
||||
// set gain exponentially
|
||||
this.currentAudioInstance.gainNode.gain.linearRampToValueAtTime(
|
||||
this.state.audioVolume,
|
||||
this.audioContext.currentTime + 0.1
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
this.currentAudioInstance.audioElement.play()
|
||||
}, 100)
|
||||
|
||||
}.bind(this),
|
||||
pause: function () {
|
||||
if (!this.currentAudioInstance) {
|
||||
console.error("No audio instance")
|
||||
return null
|
||||
}
|
||||
|
||||
// set gain exponentially
|
||||
this.currentAudioInstance.gainNode.gain.linearRampToValueAtTime(
|
||||
0.0001,
|
||||
this.audioContext.currentTime + 0.1
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
this.currentAudioInstance.audioElement.pause()
|
||||
}, 100)
|
||||
}.bind(this),
|
||||
next: this.next.bind(this),
|
||||
previous: this.previous.bind(this),
|
||||
stop: this.stop.bind(this),
|
||||
status: function () {
|
||||
return this.state.playbackStatus
|
||||
}.bind(this),
|
||||
},
|
||||
getState: function (key) {
|
||||
if (key) {
|
||||
return this.state[key]
|
||||
}
|
||||
|
||||
return this.state
|
||||
}.bind(this),
|
||||
seek: this.seek.bind(this),
|
||||
duration: this.duration.bind(this),
|
||||
velocity: this.velocity.bind(this),
|
||||
}
|
||||
|
||||
async onInitialize() {
|
||||
Observable.observe(this.state, (changes) => {
|
||||
changes.forEach((change) => {
|
||||
if (change.type === "update") {
|
||||
switch (change.path[0]) {
|
||||
case "trackBPM": {
|
||||
app.eventBus.emit("player.bpm.update", change.object.trackBPM)
|
||||
|
||||
break
|
||||
}
|
||||
case "crossfading": {
|
||||
app.eventBus.emit("player.crossfading.update", change.object.crossfading)
|
||||
|
||||
console.log("crossfading", change.object.crossfading)
|
||||
|
||||
break
|
||||
}
|
||||
case "loading": {
|
||||
app.eventBus.emit("player.loading.update", change.object.loading)
|
||||
|
||||
break
|
||||
}
|
||||
case "currentAudioManifest": {
|
||||
app.eventBus.emit("player.current.update", change.object.currentAudioManifest)
|
||||
|
||||
break
|
||||
}
|
||||
case "audioMuted": {
|
||||
AudioPlayerStorage.set("muted", change.object.audioMuted)
|
||||
|
||||
app.eventBus.emit("player.mute.update", change.object.audioMuted)
|
||||
|
||||
break
|
||||
}
|
||||
case "audioVolume": {
|
||||
AudioPlayerStorage.set("volume", change.object.audioVolume)
|
||||
|
||||
app.eventBus.emit("player.volume.update", change.object.audioVolume)
|
||||
|
||||
break
|
||||
}
|
||||
case "velocity": {
|
||||
AudioPlayerStorage.set("velocity", change.object.velocity)
|
||||
|
||||
app.eventBus.emit("player.velocity.update", change.object.velocity)
|
||||
|
||||
break
|
||||
}
|
||||
case "playbackMode": {
|
||||
AudioPlayerStorage.set("mode", change.object.playbackMode)
|
||||
|
||||
this.currentAudioInstance.audioElement.loop = change.object.playbackMode === "repeat"
|
||||
|
||||
app.eventBus.emit("player.mode.update", change.object.playbackMode)
|
||||
|
||||
break
|
||||
}
|
||||
case "playbackStatus": {
|
||||
app.eventBus.emit("player.status.update", change.object.playbackStatus)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// async instanciateRealtimeAnalyzerNode() {
|
||||
// if (this.realtimeAnalyzerNode) {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// this.realtimeAnalyzerNode = await createRealTimeBpmProcessor(this.audioContext)
|
||||
|
||||
// this.realtimeAnalyzerNode.port.onmessage = (event) => {
|
||||
// if (event.data.result.bpm[0]) {
|
||||
// if (this.state.trackBPM != event.data.result.bpm[0].tempo) {
|
||||
// this.state.trackBPM = event.data.result.bpm[0].tempo
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (event.data.message === "BPM_STABLE") {
|
||||
// console.log("BPM STABLE", event.data.result)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
attachPlayerComponent() {
|
||||
if (this.currentDomWindow) {
|
||||
console.warn("EmbbededMediaPlayer already attached")
|
||||
return false
|
||||
}
|
||||
|
||||
this.currentDomWindow = new DOMWindow({
|
||||
id: "mediaPlayer"
|
||||
})
|
||||
|
||||
this.currentDomWindow.render(<EmbbededMediaPlayer />)
|
||||
}
|
||||
|
||||
detachPlayerComponent() {
|
||||
if (!this.currentDomWindow) {
|
||||
console.warn("EmbbededMediaPlayer not attached")
|
||||
return false
|
||||
}
|
||||
|
||||
this.currentDomWindow.close()
|
||||
this.currentDomWindow = null
|
||||
}
|
||||
|
||||
destroyCurrentInstance() {
|
||||
if (!this.currentAudioInstance) {
|
||||
return false
|
||||
}
|
||||
|
||||
// stop playback
|
||||
if (this.currentAudioInstance.audioElement) {
|
||||
this.currentAudioInstance.audioElement.pause()
|
||||
}
|
||||
|
||||
this.currentAudioInstance = null
|
||||
}
|
||||
|
||||
async createInstance(manifest) {
|
||||
if (!manifest) {
|
||||
console.error("Manifest is required")
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof manifest === "string") {
|
||||
manifest = {
|
||||
src: manifest,
|
||||
}
|
||||
}
|
||||
|
||||
if (!manifest.src && !manifest.source) {
|
||||
console.error("Manifest source is required")
|
||||
return false
|
||||
}
|
||||
|
||||
const audioSource = manifest.src ?? manifest.source
|
||||
|
||||
if (!manifest.title) {
|
||||
manifest.title = audioSource.split("/").pop()
|
||||
}
|
||||
|
||||
let instanceObj = {
|
||||
audioElement: new Audio(audioSource),
|
||||
audioSource: audioSource,
|
||||
manifest: manifest,
|
||||
track: null,
|
||||
gainNode: null,
|
||||
crossfadeInterval: null,
|
||||
crossfading: false
|
||||
}
|
||||
|
||||
instanceObj.audioElement.loop = this.state.playbackMode === "repeat"
|
||||
instanceObj.audioElement.crossOrigin = "anonymous"
|
||||
instanceObj.audioElement.preload = "metadata"
|
||||
|
||||
const createCrossfadeInterval = () => {
|
||||
console.warn("Crossfader is not supported yet")
|
||||
return false
|
||||
|
||||
const crossfadeDuration = app.cores.settings.get("player.crossfade")
|
||||
|
||||
if (crossfadeDuration === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (instanceObj.crossfadeInterval) {
|
||||
clearInterval(instanceObj.crossfadeInterval)
|
||||
}
|
||||
|
||||
// fix audioElement.duration to be the duration of the audio minus the crossfade time
|
||||
const crossfadeTime = Number.parseFloat(instanceObj.audioElement.duration).toFixed(0) - crossfadeDuration
|
||||
|
||||
const crossfaderTick = () => {
|
||||
// check the if current audio has reached the crossfade time
|
||||
if (instanceObj.audioElement.currentTime >= crossfadeTime) {
|
||||
instanceObj.crossfading = true
|
||||
|
||||
this.next({
|
||||
crossfading: crossfadeDuration,
|
||||
instance: instanceObj
|
||||
})
|
||||
|
||||
clearInterval(instanceObj.crossfadeInterval)
|
||||
}
|
||||
}
|
||||
|
||||
crossfaderTick()
|
||||
|
||||
instanceObj.crossfadeInterval = setInterval(() => {
|
||||
crossfaderTick()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// handle on end
|
||||
instanceObj.audioElement.addEventListener("ended", () => {
|
||||
// cancel if is crossfading
|
||||
if (this.state.crossfading || instanceObj.crossfading) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.next()
|
||||
})
|
||||
|
||||
instanceObj.audioElement.addEventListener("play", () => {
|
||||
this.state.loading = false
|
||||
|
||||
this.state.playbackStatus = "playing"
|
||||
|
||||
instanceObj.audioElement.loop = this.state.playbackMode === "repeat"
|
||||
})
|
||||
|
||||
instanceObj.audioElement.addEventListener("playing", () => {
|
||||
this.state.loading = false
|
||||
|
||||
this.state.playbackStatus = "playing"
|
||||
|
||||
if (this.waitUpdateTimeout) {
|
||||
clearTimeout(this.waitUpdateTimeout)
|
||||
this.waitUpdateTimeout = null
|
||||
}
|
||||
|
||||
createCrossfadeInterval()
|
||||
})
|
||||
|
||||
instanceObj.audioElement.addEventListener("pause", () => {
|
||||
if (this.state.crossfading || instanceObj.crossfading) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.state.playbackStatus = "paused"
|
||||
|
||||
if (instanceObj.crossfadeInterval) {
|
||||
clearInterval(instanceObj.crossfadeInterval)
|
||||
}
|
||||
})
|
||||
|
||||
instanceObj.audioElement.addEventListener("durationchange", (duration) => {
|
||||
if (instanceObj.audioElement.paused) {
|
||||
return
|
||||
}
|
||||
|
||||
app.eventBus.emit("player.duration.update", duration)
|
||||
})
|
||||
|
||||
instanceObj.audioElement.addEventListener("waiting", () => {
|
||||
if (instanceObj.audioElement.paused) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.waitUpdateTimeout) {
|
||||
clearTimeout(this.waitUpdateTimeout)
|
||||
this.waitUpdateTimeout = null
|
||||
}
|
||||
|
||||
// if takes more than 200ms to load, update loading state
|
||||
this.waitUpdateTimeout = setTimeout(() => {
|
||||
this.state.loading = true
|
||||
}, 200)
|
||||
})
|
||||
|
||||
instanceObj.audioElement.addEventListener("seeked", () => {
|
||||
app.eventBus.emit("player.seek.update", instanceObj.audioElement.currentTime)
|
||||
createCrossfadeInterval()
|
||||
})
|
||||
|
||||
//await this.instanciateRealtimeAnalyzerNode()
|
||||
|
||||
instanceObj.track = this.audioContext.createMediaElementSource(instanceObj.audioElement)
|
||||
|
||||
instanceObj.gainNode = this.audioContext.createGain()
|
||||
|
||||
instanceObj.gainNode.gain.value = this.state.audioVolume
|
||||
|
||||
const processorsList = [
|
||||
instanceObj.gainNode,
|
||||
...this.audioProcessors,
|
||||
]
|
||||
|
||||
let lastProcessor = null
|
||||
|
||||
processorsList.forEach((processor) => {
|
||||
if (lastProcessor) {
|
||||
lastProcessor.connect(processor)
|
||||
} else {
|
||||
instanceObj.track.connect(processor)
|
||||
}
|
||||
|
||||
lastProcessor = processor
|
||||
})
|
||||
|
||||
lastProcessor.connect(this.audioContext.destination)
|
||||
|
||||
return instanceObj
|
||||
}
|
||||
|
||||
play(instance, params = {}) {
|
||||
if (typeof instance === "number") {
|
||||
instance = this.audioQueue[instance]
|
||||
}
|
||||
|
||||
if (!instance) {
|
||||
throw new Error("Audio instance is required")
|
||||
}
|
||||
|
||||
if (this.audioContext.state === "suspended") {
|
||||
this.audioContext.resume()
|
||||
}
|
||||
|
||||
this.currentAudioInstance = instance
|
||||
this.state.currentAudioManifest = instance.manifest
|
||||
|
||||
// set time to 0
|
||||
this.currentAudioInstance.audioElement.currentTime = 0
|
||||
|
||||
if (params.time >= 0) {
|
||||
this.currentAudioInstance.audioElement.currentTime = params.time
|
||||
}
|
||||
|
||||
if (params.volume >= 0) {
|
||||
this.currentAudioInstance.gainNode.gain.value = params.volume
|
||||
}
|
||||
|
||||
if (this.realtimeAnalyzerNode) {
|
||||
const filter = this.audioContext.createBiquadFilter()
|
||||
|
||||
filter.type = "lowpass"
|
||||
|
||||
this.currentAudioInstance.track.connect(filter).connect(this.realtimeAnalyzerNode)
|
||||
}
|
||||
|
||||
instance.audioElement.play()
|
||||
|
||||
if (!this.currentDomWindow) {
|
||||
// FIXME: i gonna attach the player component after 500ms to avoid error calculating the player position and duration on the first play
|
||||
setTimeout(() => {
|
||||
this.attachPlayerComponent()
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
async startPlaylist(playlist, startIndex = 0) {
|
||||
// playlist is an array of audio manifests
|
||||
if (!playlist || !Array.isArray(playlist)) {
|
||||
throw new Error("Playlist is required")
|
||||
}
|
||||
|
||||
this.destroyCurrentInstance()
|
||||
|
||||
// clear current queue
|
||||
this.audioQueue = []
|
||||
|
||||
this.audioQueueHistory = []
|
||||
|
||||
this.state.loading = true
|
||||
|
||||
for await (const [index, manifest] of playlist.entries()) {
|
||||
const instance = await this.createInstance(manifest)
|
||||
|
||||
if (index < startIndex) {
|
||||
this.audioQueueHistory.push(instance)
|
||||
} else {
|
||||
this.audioQueue.push(instance)
|
||||
}
|
||||
}
|
||||
|
||||
// play first audio
|
||||
this.play(this.audioQueue[0])
|
||||
}
|
||||
|
||||
async start(manifest) {
|
||||
this.destroyCurrentInstance()
|
||||
|
||||
const instance = await this.createInstance(manifest)
|
||||
|
||||
this.audioQueue = [instance]
|
||||
|
||||
this.audioQueueHistory = []
|
||||
|
||||
this.state.loading = true
|
||||
|
||||
this.play(this.audioQueue[0])
|
||||
}
|
||||
|
||||
next(params = {}) {
|
||||
if (this.audioQueue.length > 0) {
|
||||
// move current audio instance to history
|
||||
this.audioQueueHistory.push(this.audioQueue.shift())
|
||||
}
|
||||
|
||||
// check if there is a next audio in queue
|
||||
if (this.audioQueue.length === 0) {
|
||||
console.log("no more audio on queue, stopping playback")
|
||||
|
||||
this.destroyCurrentInstance()
|
||||
|
||||
this.state.playbackStatus = "stopped"
|
||||
this.state.currentAudioManifest = null
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const nextParams = {}
|
||||
let nextIndex = 0
|
||||
|
||||
if (params.crossfading && params.crossfading > 0 && this.state.playbackStatus === "playing" && params.instance) {
|
||||
this.state.crossfading = true
|
||||
|
||||
// calculate the current audio context time with the current audio duration (subtracting time offset)
|
||||
const linearFadeoutTime = Number(
|
||||
this.audioContext.currentTime +
|
||||
Number(params.crossfading.toFixed(2))
|
||||
)
|
||||
|
||||
console.log("linearFadeoutTime", this.audioContext.currentTime, linearFadeoutTime)
|
||||
|
||||
console.log("crossfading offset", ( this.currentAudioInstance.audioElement.duration - this.currentAudioInstance.audioElement.currentTime) - Number(params.crossfading.toFixed(2)))
|
||||
|
||||
params.instance.gainNode.gain.linearRampToValueAtTime(0.00001, linearFadeoutTime)
|
||||
|
||||
nextParams.volume = 0
|
||||
|
||||
setTimeout(() => {
|
||||
this.state.crossfading = false
|
||||
}, params.crossfading)
|
||||
} else {
|
||||
this.destroyCurrentInstance()
|
||||
}
|
||||
|
||||
// if is in shuffle mode, play a random audio
|
||||
if (this.state.playbackMode === "shuffle") {
|
||||
nextIndex = Math.floor(Math.random() * this.audioQueue.length)
|
||||
}
|
||||
|
||||
// play next audio
|
||||
this.play(this.audioQueue[nextIndex], nextParams)
|
||||
|
||||
if (this.state.crossfading) {
|
||||
// calculate the current audio context time (fixing times) with the crossfading duration
|
||||
const linearFadeinTime = Number(this.audioContext.currentTime + Number(params.crossfading.toFixed(2)))
|
||||
|
||||
console.log("linearFadeinTime", this.audioContext.currentTime, linearFadeinTime)
|
||||
|
||||
// set a linear ramp to 1
|
||||
this.currentAudioInstance.gainNode.gain.linearRampToValueAtTime(
|
||||
this.state.audioVolume,
|
||||
linearFadeinTime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
previous() {
|
||||
this.destroyCurrentInstance()
|
||||
|
||||
if (this.audioQueueHistory.length > 0) {
|
||||
// move current audio instance to queue
|
||||
this.audioQueue.unshift(this.audioQueueHistory.pop())
|
||||
|
||||
// play previous audio
|
||||
this.play(this.audioQueue[0])
|
||||
}
|
||||
|
||||
// check if there is a previous audio in history
|
||||
if (this.audioQueueHistory.length === 0) {
|
||||
// if there is no previous audio, start again from the first audio
|
||||
this.play(this.audioQueue[0])
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.destroyCurrentInstance()
|
||||
|
||||
this.state.playbackStatus = "stopped"
|
||||
this.state.currentAudioManifest = null
|
||||
|
||||
this.audioQueue = []
|
||||
}
|
||||
|
||||
toogleMute(to) {
|
||||
this.state.audioMuted = to ?? !this.state.audioMuted
|
||||
|
||||
if (this.currentAudioInstance) {
|
||||
this.currentAudioInstance.audioElement.muted = this.state.audioMuted
|
||||
}
|
||||
|
||||
return this.state.audioMuted
|
||||
}
|
||||
|
||||
volume(volume) {
|
||||
if (typeof volume !== "number") {
|
||||
return this.state.audioVolume
|
||||
}
|
||||
|
||||
if (volume > 1) {
|
||||
console.log(app.cores.settings.get("player.allowVolumeOver100"))
|
||||
|
||||
if (!app.cores.settings.get("player.allowVolumeOver100")) {
|
||||
volume = 1
|
||||
}
|
||||
}
|
||||
|
||||
if (volume < 0) {
|
||||
volume = 0
|
||||
}
|
||||
|
||||
this.state.audioVolume = volume
|
||||
|
||||
if (this.currentAudioInstance) {
|
||||
if (this.currentAudioInstance.gainNode) {
|
||||
this.currentAudioInstance.gainNode.gain.value = this.state.audioVolume
|
||||
}
|
||||
}
|
||||
|
||||
return this.state.audioVolume
|
||||
}
|
||||
|
||||
seek(time) {
|
||||
if (!this.currentAudioInstance) {
|
||||
return false
|
||||
}
|
||||
|
||||
// if time not provided, return current time
|
||||
if (typeof time === "undefined") {
|
||||
return this.currentAudioInstance.audioElement.currentTime
|
||||
}
|
||||
|
||||
// if time is provided, seek to that time
|
||||
if (typeof time === "number") {
|
||||
this.currentAudioInstance.audioElement.currentTime = time
|
||||
|
||||
return time
|
||||
}
|
||||
}
|
||||
|
||||
duration() {
|
||||
if (!this.currentAudioInstance) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.currentAudioInstance.audioElement.duration
|
||||
}
|
||||
|
||||
loop(to) {
|
||||
if (typeof to !== "boolean") {
|
||||
console.warn("Loop must be a boolean")
|
||||
return false
|
||||
}
|
||||
|
||||
this.state.loop = to ?? !this.state.loop
|
||||
|
||||
if (this.currentAudioInstance) {
|
||||
this.currentAudioInstance.audioElement.loop = this.state.loop
|
||||
}
|
||||
|
||||
return this.state.loop
|
||||
}
|
||||
|
||||
velocity(to) {
|
||||
if (typeof to !== "number") {
|
||||
console.warn("Velocity must be a number")
|
||||
return false
|
||||
}
|
||||
|
||||
this.state.velocity = to
|
||||
|
||||
if (this.currentAudioInstance) {
|
||||
this.currentAudioInstance.audioElement.playbackRate = this.state.velocity
|
||||
}
|
||||
|
||||
return this.state.velocity
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ export default class RemoteStorage extends Core {
|
||||
|
||||
connection = null
|
||||
|
||||
async initialize() {
|
||||
async onInitialize() {
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,17 +0,0 @@
|
||||
import Core from "evite/src/core"
|
||||
|
||||
export default class Search extends Core {
|
||||
static namespace = "searchEngine"
|
||||
static dependencies = ["api"]
|
||||
static public = ["search"]
|
||||
|
||||
apiBridge = null
|
||||
|
||||
search = async (keywords, params = {}) => {
|
||||
if (!this.apiBridge) {
|
||||
this.apiBridge = app.api.withEndpoints("main")
|
||||
}
|
||||
|
||||
return await this.apiBridge.get.search(undefined, { keywords: keywords, params })
|
||||
}
|
||||
}
|
@ -4,52 +4,60 @@ import defaultSettings from "schemas/defaultSettings.json"
|
||||
import { Observable } from "rxjs"
|
||||
|
||||
export default class SettingsCore extends Core {
|
||||
storeKey = "app_settings"
|
||||
static namespace = "settings"
|
||||
|
||||
settings = store.get(this.storeKey) ?? {}
|
||||
static storeKey = "app_settings"
|
||||
|
||||
publicMethods = {
|
||||
settings: this
|
||||
public = {
|
||||
is: this.is,
|
||||
set: this.set,
|
||||
get: this.get,
|
||||
getDefaults: this.getDefaults,
|
||||
withEvent: this.withEvent,
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.fulfillUndefinedWithDefaults()
|
||||
}
|
||||
onInitialize() {
|
||||
const settings = this.get()
|
||||
|
||||
fulfillUndefinedWithDefaults = () => {
|
||||
// fulfillUndefinedWithDefaults
|
||||
Object.keys(defaultSettings).forEach((key) => {
|
||||
const value = defaultSettings[key]
|
||||
|
||||
// Only set default if value is undefined
|
||||
if (typeof this.settings[key] === "undefined") {
|
||||
this.settings[key] = value
|
||||
if (typeof settings[key] === "undefined") {
|
||||
this.set(key, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
is = (key, value) => {
|
||||
return this.settings[key] === value
|
||||
is(key, value) {
|
||||
return this.get(key) === value
|
||||
}
|
||||
|
||||
set = (key, value) => {
|
||||
this.settings[key] = value
|
||||
store.set(this.storeKey, this.settings)
|
||||
set(key, value) {
|
||||
const settings = this.get()
|
||||
|
||||
settings[key] = value
|
||||
|
||||
store.set(SettingsCore.storeKey, settings)
|
||||
|
||||
window.app.eventBus.emit("setting.update", { key, value })
|
||||
window.app.eventBus.emit(`setting.update.${key}`, value)
|
||||
|
||||
return this.settings
|
||||
return settings
|
||||
}
|
||||
|
||||
get = (key) => {
|
||||
get(key) {
|
||||
const settings = store.get(SettingsCore.storeKey) ?? {}
|
||||
|
||||
if (typeof key === "undefined") {
|
||||
return this.settings
|
||||
return settings
|
||||
}
|
||||
|
||||
return this.settings[key]
|
||||
return settings[key]
|
||||
}
|
||||
|
||||
getDefaults = (key) => {
|
||||
getDefaults(key) {
|
||||
if (typeof key === "undefined") {
|
||||
return defaultSettings
|
||||
}
|
||||
@ -57,8 +65,8 @@ export default class SettingsCore extends Core {
|
||||
return defaultSettings[key]
|
||||
}
|
||||
|
||||
withEvent = (listenEvent, defaultValue) => {
|
||||
let value = defaultValue ?? this.settings[key] ?? false
|
||||
withEvent(listenEvent, defaultValue) {
|
||||
let value = defaultValue ?? false
|
||||
|
||||
const observable = new Observable((subscriber) => {
|
||||
subscriber.next(value)
|
||||
|
@ -3,7 +3,7 @@ import Core from "evite/src/core"
|
||||
export default class ShortcutsCore extends Core {
|
||||
shortcutsRegister = []
|
||||
|
||||
publicMethods = {
|
||||
registerToApp = {
|
||||
shortcuts: this
|
||||
}
|
||||
|
||||
|
@ -3,36 +3,29 @@ import { Howl } from "howler"
|
||||
import config from "config"
|
||||
|
||||
export default class SoundCore extends Core {
|
||||
sounds = {}
|
||||
static namespace = "sound"
|
||||
|
||||
publicMethods = {
|
||||
sound: this,
|
||||
public = {
|
||||
play: this.play,
|
||||
getSounds: this.getSounds,
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.sounds = await this.getSounds()
|
||||
}
|
||||
|
||||
getSounds = async () => {
|
||||
async getSounds() {
|
||||
// TODO: Load custom soundpacks manifests
|
||||
let soundPack = config.defaultSoundPack ?? {}
|
||||
|
||||
Object.keys(soundPack).forEach((key) => {
|
||||
const src = soundPack[key]
|
||||
|
||||
soundPack[key] = (options) => new Howl({
|
||||
volume: window.app.settings.get("generalAudioVolume") ?? 0.5,
|
||||
...options,
|
||||
src: [src],
|
||||
})
|
||||
})
|
||||
|
||||
return soundPack
|
||||
}
|
||||
|
||||
play = (name, options) => {
|
||||
if (this.sounds[name]) {
|
||||
return this.sounds[name](options).play()
|
||||
async play(name, options) {
|
||||
let soundPack = await this.getSounds()
|
||||
|
||||
if (soundPack[name]) {
|
||||
return new Howl({
|
||||
volume: window.app.cores.settings.get("generalAudioVolume") ?? 0.5,
|
||||
...options,
|
||||
src: [soundPack[name]],
|
||||
}).play()
|
||||
} else {
|
||||
console.error(`Sound [${name}] not found or is not available.`)
|
||||
return false
|
||||
|
@ -1,87 +1,118 @@
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom"
|
||||
import SVG from "react-inlinesvg"
|
||||
|
||||
import Core from "evite/src/core"
|
||||
import config from "config"
|
||||
import store from "store"
|
||||
import { ConfigProvider } from "antd"
|
||||
import { ConfigProvider, theme } from "antd"
|
||||
import RemoteSVGToComponent from "components/RemoteSVGToComponent"
|
||||
|
||||
const variantToAlgorithm = {
|
||||
light: theme.defaultAlgorithm,
|
||||
dark: theme.darkAlgorithm,
|
||||
}
|
||||
|
||||
export class ThemeProvider extends React.Component {
|
||||
state = {
|
||||
useAlgorigthm: app.cores.style.currentVariant ?? "dark",
|
||||
useCompactMode: app.cores.style.getValue("compact-mode"),
|
||||
}
|
||||
|
||||
handleUpdate = (update) => {
|
||||
console.log("[THEME] Update", update)
|
||||
|
||||
if (update.themeVariant) {
|
||||
this.setState({
|
||||
useAlgorigthm: update.themeVariant
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
useCompactMode: update["compact-mode"]
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
app.eventBus.on("style.update", this.handleUpdate)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
app.eventBus.off("style.update", this.handleUpdate)
|
||||
}
|
||||
|
||||
render() {
|
||||
const themeAlgorithms = [
|
||||
variantToAlgorithm[this.state.useAlgorigthm ?? "dark"],
|
||||
]
|
||||
|
||||
if (this.state.useCompactMode) {
|
||||
themeAlgorithms.push(theme.compactAlgorithm)
|
||||
}
|
||||
|
||||
return <ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
...app.cores.style.getValue(),
|
||||
},
|
||||
algorithm: themeAlgorithms,
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
</ConfigProvider>
|
||||
}
|
||||
}
|
||||
|
||||
export default class StyleCore extends Core {
|
||||
themeManifestStorageKey = "theme"
|
||||
modificationStorageKey = "themeModifications"
|
||||
static namespace = "style"
|
||||
|
||||
theme = null
|
||||
mutation = null
|
||||
currentVariant = null
|
||||
static themeManifestStorageKey = "theme"
|
||||
static modificationStorageKey = "themeModifications"
|
||||
|
||||
events = {
|
||||
"style.compactMode": (value = !window.app.settings.get("style.compactMode")) => {
|
||||
if (value) {
|
||||
return this.update({
|
||||
layoutMargin: 0,
|
||||
layoutPadding: 0,
|
||||
})
|
||||
}
|
||||
static get rootVariables() {
|
||||
let attributes = document.documentElement.getAttribute("style").trim().split(";")
|
||||
|
||||
return this.update({
|
||||
layoutMargin: this.getValue("layoutMargin"),
|
||||
layoutPadding: this.getValue("layoutPadding"),
|
||||
})
|
||||
},
|
||||
"style.autoDarkModeToogle": (value) => {
|
||||
if (value === true) {
|
||||
this.handleAutoColorScheme()
|
||||
} else {
|
||||
this.applyVariant(this.getStoragedVariant())
|
||||
}
|
||||
},
|
||||
"theme.applyVariant": (value) => {
|
||||
this.applyVariant(value)
|
||||
this.setVariant(value)
|
||||
},
|
||||
"modifyTheme": (value) => {
|
||||
this.update(value)
|
||||
this.setModifications(this.mutation)
|
||||
},
|
||||
"resetTheme": () => {
|
||||
this.resetDefault()
|
||||
}
|
||||
attributes = attributes.slice(0, (attributes.length - 1))
|
||||
|
||||
attributes = attributes.map((variable) => {
|
||||
let [key, value] = variable.split(":")
|
||||
key = key.split("--")[1]
|
||||
|
||||
return [key, value]
|
||||
})
|
||||
|
||||
return Object.fromEntries(attributes)
|
||||
}
|
||||
|
||||
publicMethods = {
|
||||
style: this
|
||||
static get storagedTheme() {
|
||||
return store.get(StyleCore.themeManifestStorageKey)
|
||||
}
|
||||
|
||||
static get currentVariant() {
|
||||
return document.documentElement.style.getPropertyValue("--themeVariant")
|
||||
static get storagedVariant() {
|
||||
return app.cores.settings.get("style.darkMode") ? "dark" : "light"
|
||||
}
|
||||
|
||||
handleAutoColorScheme() {
|
||||
const prefered = window.matchMedia("(prefers-color-scheme: light)")
|
||||
static set storagedModifications(modifications) {
|
||||
return store.set(StyleCore.modificationStorageKey, modifications)
|
||||
}
|
||||
|
||||
if (!prefered.matches) {
|
||||
this.applyVariant("dark")
|
||||
static get storagedModifications() {
|
||||
return store.get(StyleCore.modificationStorageKey) ?? {}
|
||||
}
|
||||
|
||||
async onInitialize() {
|
||||
if (StyleCore.storagedTheme) {
|
||||
// TODO: Start remote theme loader
|
||||
} else {
|
||||
this.applyVariant("light")
|
||||
}
|
||||
}
|
||||
|
||||
initialize = async () => {
|
||||
let theme = this.getStoragedTheme()
|
||||
|
||||
const modifications = this.getStoragedModifications()
|
||||
const variantKey = this.getStoragedVariant()
|
||||
|
||||
if (!theme) {
|
||||
// load default theme
|
||||
theme = this.getDefaultTheme()
|
||||
} else {
|
||||
// load URL and initialize theme
|
||||
this.public.theme = config.defaultTheme
|
||||
}
|
||||
|
||||
// set global theme
|
||||
this.theme = theme
|
||||
const modifications = StyleCore.storagedModifications
|
||||
const variantKey = StyleCore.storagedVariant
|
||||
|
||||
// override with static vars
|
||||
if (theme.staticVars) {
|
||||
this.update(theme.staticVars)
|
||||
if (this.public.theme.defaultVars) {
|
||||
this.update(this.public.theme.defaultVars)
|
||||
}
|
||||
|
||||
// override theme with modifications
|
||||
@ -96,104 +127,130 @@ export default class StyleCore extends Core {
|
||||
window.matchMedia("(prefers-color-scheme: light)").addListener(() => {
|
||||
console.log(`[THEME] Auto color scheme changed`)
|
||||
|
||||
if (window.app.settings.get("auto_darkMode")) {
|
||||
if (window.app.cores.settings.get("auto_darkMode")) {
|
||||
this.handleAutoColorScheme()
|
||||
}
|
||||
})
|
||||
|
||||
if (window.app.settings.get("auto_darkMode")) {
|
||||
if (window.app.cores.settings.get("auto_darkMode")) {
|
||||
this.handleAutoColorScheme()
|
||||
}
|
||||
}
|
||||
|
||||
getRootVariables = () => {
|
||||
let attributes = document.documentElement.getAttribute("style").trim().split(";")
|
||||
attributes = attributes.slice(0, (attributes.length - 1))
|
||||
attributes = attributes.map((variable) => {
|
||||
let [key, value] = variable.split(":")
|
||||
key = key.split("--")[1]
|
||||
|
||||
return [key, value]
|
||||
})
|
||||
|
||||
return Object.fromEntries(attributes)
|
||||
onEvents = {
|
||||
"style.autoDarkModeToogle": (value) => {
|
||||
if (value === true) {
|
||||
this.handleAutoColorScheme()
|
||||
} else {
|
||||
this.applyVariant(StyleCore.storagedVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultTheme = () => {
|
||||
// TODO: Use evite CONSTANTS_API
|
||||
return config.defaultTheme
|
||||
public = {
|
||||
theme: null,
|
||||
mutation: null,
|
||||
currentVariant: "dark",
|
||||
|
||||
getValue: (...args) => this.getValue(...args),
|
||||
setDefault: () => this.setDefault(),
|
||||
update: (...args) => this.update(...args),
|
||||
applyVariant: (...args) => this.applyVariant(...args),
|
||||
compactMode: (value = !window.app.cores.settings.get("style.compactMode")) => {
|
||||
if (value) {
|
||||
return this.update({
|
||||
layoutMargin: 0,
|
||||
layoutPadding: 0,
|
||||
})
|
||||
}
|
||||
|
||||
return this.update({
|
||||
layoutMargin: this.getValue("layoutMargin"),
|
||||
layoutPadding: this.getValue("layoutPadding"),
|
||||
})
|
||||
},
|
||||
modify: (value) => {
|
||||
this.public.update(value)
|
||||
|
||||
this.applyVariant(this.public.mutation.themeVariant ?? this.public.currentVariant)
|
||||
|
||||
StyleCore.storagedModifications = this.public.mutation
|
||||
},
|
||||
defaultVar: (key) => {
|
||||
if (!key) {
|
||||
return this.public.theme.defaultVars
|
||||
}
|
||||
|
||||
return this.public.theme.defaultVars[key]
|
||||
},
|
||||
storagedVariant: StyleCore.storagedVariant,
|
||||
storagedModifications: StyleCore.storagedModifications,
|
||||
}
|
||||
|
||||
getStoragedTheme = () => {
|
||||
return store.get(this.themeManifestStorageKey)
|
||||
}
|
||||
|
||||
getStoragedModifications = () => {
|
||||
return store.get(this.modificationStorageKey) ?? {}
|
||||
}
|
||||
|
||||
getStoragedVariant = () => {
|
||||
return app.settings.get("themeVariant")
|
||||
}
|
||||
|
||||
getValue = (key) => {
|
||||
const storagedModifications = this.getStoragedModifications()
|
||||
const staticValues = this.theme.staticVars
|
||||
|
||||
getValue(key) {
|
||||
if (typeof key === "undefined") {
|
||||
return {
|
||||
...staticValues,
|
||||
...storagedModifications
|
||||
...this.public.theme.defaultVars,
|
||||
...StyleCore.storagedModifications
|
||||
}
|
||||
}
|
||||
|
||||
return storagedModifications[key] || staticValues[key]
|
||||
return StyleCore.storagedModifications[key] || this.public.theme.defaultVars[key]
|
||||
}
|
||||
|
||||
setVariant = (variationKey) => {
|
||||
return app.settings.set("themeVariant", variationKey)
|
||||
setDefault() {
|
||||
store.remove(StyleCore.themeManifestStorageKey)
|
||||
store.remove(StyleCore.modificationStorageKey)
|
||||
|
||||
app.cores.settings.set("colorPrimary", this.public.theme.defaultVars.colorPrimary)
|
||||
|
||||
this.onInitialize()
|
||||
}
|
||||
|
||||
setModifications = (modifications) => {
|
||||
return store.set(this.modificationStorageKey, modifications)
|
||||
}
|
||||
|
||||
resetDefault = () => {
|
||||
store.remove(this.themeManifestStorageKey)
|
||||
store.remove(this.modificationStorageKey)
|
||||
|
||||
window.app.settings.set("primaryColor", this.theme.staticVars.primaryColor)
|
||||
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
update = (update) => {
|
||||
update(update) {
|
||||
if (typeof update !== "object") {
|
||||
return false
|
||||
}
|
||||
|
||||
this.mutation = {
|
||||
...this.theme.staticVars,
|
||||
...this.mutation,
|
||||
this.public.mutation = {
|
||||
...this.public.theme.defaultVars,
|
||||
...this.public.mutation,
|
||||
...update
|
||||
}
|
||||
|
||||
Object.keys(this.mutation).forEach(key => {
|
||||
document.documentElement.style.setProperty(`--${key}`, this.mutation[key])
|
||||
Object.keys(this.public.mutation).forEach(key => {
|
||||
document.documentElement.style.setProperty(`--${key}`, this.public.mutation[key])
|
||||
})
|
||||
|
||||
document.documentElement.className = `theme-${this.currentVariant}`
|
||||
document.documentElement.style.setProperty(`--themeVariant`, this.currentVariant)
|
||||
app.eventBus.emit("style.update", {
|
||||
...this.public.mutation,
|
||||
})
|
||||
|
||||
ConfigProvider.config({ theme: this.mutation })
|
||||
ConfigProvider.config({ theme: this.public.mutation })
|
||||
}
|
||||
|
||||
applyVariant = (variant = (this.theme.defaultVariant ?? "light")) => {
|
||||
const values = this.theme.variants[variant]
|
||||
applyVariant(variant = (this.public.theme.defaultVariant ?? "light")) {
|
||||
const values = this.public.theme.variants[variant]
|
||||
|
||||
if (values) {
|
||||
this.currentVariant = variant
|
||||
this.update(values)
|
||||
if (!values) {
|
||||
console.error(`Variant [${variant}] not found`)
|
||||
return false
|
||||
}
|
||||
|
||||
values.themeVariant = variant
|
||||
|
||||
this.public.currentVariant = variant
|
||||
|
||||
this.update(values)
|
||||
}
|
||||
|
||||
handleAutoColorScheme() {
|
||||
const prefered = window.matchMedia("(prefers-color-scheme: light)")
|
||||
|
||||
if (!prefered.matches) {
|
||||
this.applyVariant("dark")
|
||||
} else {
|
||||
this.applyVariant("light")
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user