rewrites to lb cores v2

This commit is contained in:
SrGooglo 2023-02-24 14:37:14 +00:00
parent 0e5604a466
commit 2efcc8a3cd
15 changed files with 1076 additions and 524 deletions

View File

@ -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", {

View File

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

View File

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

View File

@ -18,7 +18,7 @@ export default class ContextMenuCore extends Core {
clickOutsideToClose: true,
})
async initialize() {
async onInitialize() {
document.addEventListener("contextmenu", this.handleEvent)
}

View File

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

View File

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

View File

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

View File

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

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

View File

@ -8,7 +8,7 @@ export default class RemoteStorage extends Core {
connection = null
async initialize() {
async onInitialize() {
}

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import Core from "evite/src/core"
export default class ShortcutsCore extends Core {
shortcutsRegister = []
publicMethods = {
registerToApp = {
shortcuts: this
}

View File

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

View File

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