reimplement with evite extension v2

This commit is contained in:
srgooglo 2022-03-16 04:28:44 +01:00
parent e41cf70b51
commit 2745104b7c
21 changed files with 543 additions and 836 deletions

View File

@ -0,0 +1,166 @@
import { Extension } from "evite"
import config from "config"
import { Bridge } from "linebridge/dist/client"
import { Session } from "models"
export default class ApiExtension extends Extension {
constructor(app, main) {
super(app, main)
this.apiBridge = this.createBridge()
this.WSInterface = this.apiBridge.wsInterface
this.WSInterface.request = this.WSRequest
this.WSInterface.listen = this.handleWSListener
this.WSSockets = this.WSInterface.sockets
this.WSInterface.mainSocketConnected = false
}
initializers = [
async () => {
this.WSSockets.main.on("authenticated", () => {
console.debug("[WS] Authenticated")
})
this.WSSockets.main.on("authenticateFailed", (error) => {
console.error("[WS] Authenticate Failed", error)
})
this.WSSockets.main.on("connect", () => {
window.app.eventBus.emit("websocket_connected")
this.WSInterface.mainSocketConnected = true
})
this.WSSockets.main.on("disconnect", (...context) => {
window.app.eventBus.emit("websocket_disconnected", ...context)
this.WSInterface.mainSocketConnected = false
})
this.WSSockets.main.on("connect_error", (...context) => {
window.app.eventBus.emit("websocket_connection_error", ...context)
this.WSInterface.mainSocketConnected = false
})
this.mainContext.setToWindowContext("api", this.apiBridge)
this.mainContext.setToWindowContext("ws", this.WSInterface)
this.mainContext.setToWindowContext("request", this.apiBridge.endpoints)
this.mainContext.setToWindowContext("WSRequest", this.WSInterface.wsEndpoints)
}
]
createBridge() {
const getSessionContext = async () => {
const obj = {}
const token = await Session.token
if (token) {
// append token to context
obj.headers = {
Authorization: `Bearer ${token ?? null}`,
}
}
return obj
}
const handleResponse = async (data) => {
if (data.headers?.regenerated_token) {
Session.token = data.headers.regenerated_token
console.debug("[REGENERATION] New token generated")
}
if (data instanceof Error) {
if (data.response.status === 401) {
window.app.eventBus.emit("invalid_session")
}
}
}
return new Bridge({
origin: config.api.address,
wsOrigin: config.ws.address,
wsOptions: {
autoConnect: false,
},
onRequest: getSessionContext,
onResponse: handleResponse,
})
}
async attachWSConnection() {
if (!this.WSInterface.sockets.main.connected) {
await this.WSInterface.sockets.main.connect()
}
let startTime = null
let latency = null
let latencyWarning = false
let pingInterval = setInterval(() => {
if (!this.WSInterface.mainSocketConnected) {
return clearTimeout(pingInterval)
}
startTime = Date.now()
this.WSInterface.sockets.main.emit("ping")
}, 2000)
this.WSInterface.sockets.main.on("pong", () => {
latency = Date.now() - startTime
if (latency > 800 && this.WSInterface.mainSocketConnected) {
latencyWarning = true
console.error("[WS] Latency is too high > 800ms", latency)
window.app.eventBus.emit("websocket_latency_too_high", latency)
} else if (latencyWarning && this.WSInterface.mainSocketConnected) {
latencyWarning = false
window.app.eventBus.emit("websocket_latency_normal", latency)
}
})
}
async attachAPIConnection() {
await this.apiBridge.initialize()
}
handleWSListener = (to, fn) => {
if (typeof to === "undefined") {
console.error("handleWSListener: to must be defined")
return false
}
if (typeof fn !== "function") {
console.error("handleWSListener: fn must be function")
return false
}
let ns = "main"
let event = null
if (typeof to === "string") {
event = to
} else if (typeof to === "object") {
ns = to.ns
event = to.event
}
return window.app.ws.sockets[ns].on(event, async (...context) => {
return await fn(...context)
})
}
WSRequest = (socket = "main", channel, ...args) => {
return new Promise(async (resolve, reject) => {
const request = await window.app.ws.sockets[socket].emit(channel, ...args)
request.on("responseError", (...errors) => {
return reject(...errors)
})
request.on("response", (...responses) => {
return resolve(...responses)
})
})
}
window = {
ApiController: this
}
}

View File

@ -1,187 +0,0 @@
import config from "config"
import { Bridge } from "linebridge/dist/client"
import { Session } from "models"
import io from "socket.io-client"
class WSInterface {
constructor(params = {}) {
this.params = params
this.manager = new io.Manager(this.params.origin, {
autoConnect: true,
transports: ["websocket"],
...this.params.managerOptions,
})
this.sockets = {}
this.register("/", "main")
}
register = (socket, as) => {
if (typeof socket !== "string") {
console.error("socket must be string")
return false
}
socket = this.manager.socket(socket)
return this.sockets[as ?? socket] = socket
}
}
export default {
key: "apiBridge",
expose: [
{
initialization: [
async (app, main) => {
app.apiBridge = await app.createApiBridge()
app.WSInterface = app.apiBridge.wsInterface
app.WSInterface.request = app.WSRequest
app.WSInterface.listen = app.handleWSListener
app.WSSockets = app.WSInterface.sockets
app.WSInterface.mainSocketConnected = false
app.WSSockets.main.on("authenticated", () => {
console.debug("[WS] Authenticated")
})
app.WSSockets.main.on("authenticateFailed", (error) => {
console.error("[WS] Authenticate Failed", error)
})
app.WSSockets.main.on("connect", () => {
window.app.eventBus.emit("websocket_connected")
app.WSInterface.mainSocketConnected = true
})
app.WSSockets.main.on("disconnect", (...context) => {
window.app.eventBus.emit("websocket_disconnected", ...context)
app.WSInterface.mainSocketConnected = false
})
app.WSSockets.main.on("connect_error", (...context) => {
window.app.eventBus.emit("websocket_connection_error", ...context)
app.WSInterface.mainSocketConnected = false
})
window.app.api = app.apiBridge
window.app.ws = app.WSInterface
window.app.request = app.apiBridge.endpoints
window.app.wsRequest = app.apiBridge.wsEndpoints
},
],
mutateContext: {
async attachWSConnection() {
if (!this.WSInterface.sockets.main.connected) {
await this.WSInterface.sockets.main.connect()
}
let startTime = null
let latency = null
let latencyWarning = false
let pingInterval = setInterval(() => {
if (!this.WSInterface.mainSocketConnected) {
return clearTimeout(pingInterval)
}
startTime = Date.now()
this.WSInterface.sockets.main.emit("ping")
}, 2000)
this.WSInterface.sockets.main.on("pong", () => {
latency = Date.now() - startTime
if (latency > 800 && this.WSInterface.mainSocketConnected) {
latencyWarning = true
console.error("[WS] Latency is too high > 800ms", latency)
window.app.eventBus.emit("websocket_latency_too_high", latency)
} else if (latencyWarning && this.WSInterface.mainSocketConnected) {
latencyWarning = false
window.app.eventBus.emit("websocket_latency_normal", latency)
}
})
},
async attachAPIConnection() {
await this.apiBridge.initialize()
},
handleWSListener: (to, fn) => {
if (typeof to === "undefined") {
console.error("handleWSListener: to must be defined")
return false
}
if (typeof fn !== "function") {
console.error("handleWSListener: fn must be function")
return false
}
let ns = "main"
let event = null
if (typeof to === "string") {
event = to
} else if (typeof to === "object") {
ns = to.ns
event = to.event
}
return window.app.ws.sockets[ns].on(event, async (...context) => {
return await fn(...context)
})
},
createApiBridge: async () => {
const getSessionContext = async () => {
const obj = {}
const token = await Session.token
if (token) {
// append token to context
obj.headers = {
Authorization: `Bearer ${token ?? null}`,
}
}
return obj
}
const handleResponse = async (data) => {
if (data.headers?.regenerated_token) {
Session.token = data.headers.regenerated_token
console.debug("[REGENERATION] New token generated")
}
if (data instanceof Error) {
if (data.response.status === 401) {
window.app.eventBus.emit("invalid_session")
}
}
}
const bridge = new Bridge({
origin: config.api.address,
wsOrigin: config.ws.address,
wsOptions: {
autoConnect: false,
},
onRequest: getSessionContext,
onResponse: handleResponse,
})
return bridge
},
WSRequest: (socket = "main", channel, ...args) => {
return new Promise(async (resolve, reject) => {
const request = await window.app.ws.sockets[socket].emit(channel, ...args)
request.on("responseError", (...errors) => {
return reject(...errors)
})
request.on("response", (...responses) => {
return resolve(...responses)
})
})
}
},
},
],
}

View File

@ -1,3 +1,4 @@
import { Extension } from "evite"
import React from "react"
import { Window } from "components"
import { Skeleton, Tabs } from "antd"
@ -121,15 +122,8 @@ class Debugger {
}
}
export default {
key: "visualDebugger",
expose: [
{
initialization: [
async (app, main) => {
main.setToWindowContext("debug", new Debugger(main))
},
],
},
],
export default class VisualDebugger extends Extension {
window = {
debug: new Debugger(this.mainContext)
}
}

View File

@ -1,68 +0,0 @@
import Evite from "evite"
import { Haptics, ImpactStyle } from "@capacitor/haptics"
// This is a temporal workaround to make the extension work with the new evite extension system.
export default class HapticExtensionV2 extends Evite.Extension {
static id = "hapticsEngine"
static compatible = ["mobile"]
static extendsWith = ["SettingsController"]
statement = {
test: "macarronie",
}
initialization = [
async (app, main) => {
console.log(this.statement.test)
}
]
debug = {
testVibrate: () => {
},
testSelectionStart: () => {
},
testSelectionChanged: () => {
},
testSelectionEnd: () => {
},
}
public = {
vibrate: async function () {
const enabled = this.extended.SettingsController.get("haptic_feedback")
if (enabled) {
await Haptics.vibrate()
}
},
selectionStart: async function () {
const enabled = this.extended.SettingsController.get("haptic_feedback")
if (enabled) {
await Haptics.selectionStart()
}
},
selectionChanged: async function () {
const enabled = this.extended.SettingsController.get("haptic_feedback")
if (enabled) {
await Haptics.selectionChanged()
}
},
selectionEnd: async function () {
const enabled = this.extended.SettingsController.get("haptic_feedback")
if (enabled) {
await Haptics.selectionEnd()
}
},
}
}

View File

@ -0,0 +1,73 @@
import { Extension } from "evite"
import config from "config"
import i18n from "i18next"
import { initReactI18next } from "react-i18next"
export const SUPPORTED_LANGUAGES = config.i18n?.languages ?? {}
export const SUPPORTED_LOCALES = SUPPORTED_LANGUAGES.map((l) => l.locale)
export const DEFAULT_LOCALE = config.i18n?.defaultLocale
export function extractLocaleFromPath(path = "") {
const [_, maybeLocale] = path.split("/")
return SUPPORTED_LOCALES.includes(maybeLocale) ? maybeLocale : DEFAULT_LOCALE
}
const messageImports = import.meta.glob("./translations/*.json")
export default class I18nExtension extends Extension {
depends = ["SettingsExtension"]
importLocale = async (locale) => {
const [, importLocale] =
Object.entries(messageImports).find(([key]) =>
key.includes(`/${locale}.`)
) || []
return importLocale && importLocale()
}
loadAsyncLanguage = async function (locale) {
locale = locale ?? DEFAULT_LOCALE
try {
const result = await this.importLocale(locale)
if (result) {
i18n.addResourceBundle(locale, "translation", result.default || result)
i18n.changeLanguage(locale)
}
} catch (error) {
console.error(error)
}
}
initializers = [
async () => {
let locale = app.settings.get("language") ?? DEFAULT_LOCALE
if (!SUPPORTED_LOCALES.includes(locale)) {
locale = DEFAULT_LOCALE
}
const messages = await this.importLocale(locale)
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
// debug: true,
resources: {
[locale]: { translation: messages.default || messages },
},
lng: locale,
//fallbackLng: DEFAULT_LOCALE,
interpolation: {
escapeValue: false, // react already safes from xss
},
})
this.mainContext.eventBus.on("changeLanguage", (locale) => {
this.loadAsyncLanguage(locale)
})
},
]
}

View File

@ -1,78 +0,0 @@
import config from "config"
import i18n from "i18next"
import { initReactI18next } from "react-i18next"
export const SUPPORTED_LANGUAGES = config.i18n?.languages ?? {}
export const SUPPORTED_LOCALES = SUPPORTED_LANGUAGES.map((l) => l.locale)
export const DEFAULT_LOCALE = config.i18n?.defaultLocale
export function extractLocaleFromPath(path = "") {
const [_, maybeLocale] = path.split("/")
return SUPPORTED_LOCALES.includes(maybeLocale) ? maybeLocale : DEFAULT_LOCALE
}
const messageImports = import.meta.glob("./translations/*.json")
export const extension = {
key: "i18n",
expose: [
{
initialization: [
async (app, main) => {
let locale = app.settingsController.get("language") ?? DEFAULT_LOCALE
if (!SUPPORTED_LOCALES.includes(locale)) {
locale = DEFAULT_LOCALE
}
const messages = await app.importLocale(locale)
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
// debug: true,
resources: {
[locale]: { translation: messages.default || messages },
},
lng: locale,
//fallbackLng: DEFAULT_LOCALE,
interpolation: {
escapeValue: false, // react already safes from xss
},
})
main.eventBus.on("changeLanguage", (locale) => {
app.loadAsyncLanguage(locale)
})
},
],
mutateContext: {
importLocale: async (locale) => {
const [, importLocale] =
Object.entries(messageImports).find(([key]) =>
key.includes(`/${locale}.`)
) || []
return importLocale && importLocale()
},
loadAsyncLanguage: async function (locale) {
locale = locale ?? DEFAULT_LOCALE
try {
const result = await this.importLocale(locale)
if (result) {
i18n.addResourceBundle(locale, "translation", result.default || result)
i18n.changeLanguage(locale)
}
} catch (error) {
console.error(error)
}
}
},
},
],
}
export default extension

View File

@ -1,11 +1,2 @@
export * as Render from "./render"
export * as Splash from "./splash"
export * as Sound from "./sound"
export * as Theme from "./theme"
export * as i18n from "./i18n"
export * as Notifications from "./notifications"
export { default as SettingsController } from "./settings"
export { default as API } from "./api"
export { default as Debug } from "./debug"
export { default as Shortcuts } from "./shortcuts"
export * as Render from "./render.extension.jsx"
export * as Splash from "./splash"

View File

@ -1,10 +1,11 @@
import { Extension } from "evite"
import React from "react"
import { notification as Notf } from "antd"
import { Icons, createIconRender } from "components/Icons"
import { Translation } from "react-i18next"
import { Haptics } from "@capacitor/haptics"
class NotificationController {
export default class NotificationController extends Extension {
getSoundVolume = () => {
return (window.app.settings.get("notifications_sound_volume") ?? 50) / 100
}
@ -53,34 +54,21 @@ class NotificationController {
})
}
}
}
const extension = {
key: "notification",
expose: [
{
initialization: [
async (app, main) => {
app.NotificationController = new NotificationController()
initializers = [
function () {
this.eventBus.on("changeNotificationsSoundVolume", (value) => {
app.notifications.playAudio({ soundVolume: value })
})
this.eventBus.on("changeNotificationsVibrate", (value) => {
app.notifications.playHaptic({
vibrationEnabled: value,
})
})
}
]
main.eventBus.on("changeNotificationsSoundVolume", (value) => {
app.NotificationController.playAudio({ soundVolume: value })
})
main.eventBus.on("changeNotificationsVibrate", (value) => {
app.NotificationController.playHaptic({
vibrationEnabled: value,
})
})
main.setToWindowContext("notifications", app.NotificationController)
},
],
},
],
}
export {
extension,
NotificationController,
}
export default extension
window = {
notifications: this
}
}

View File

@ -0,0 +1,194 @@
import React from "react"
import { EvitePureComponent, Extension } from "evite"
import progressBar from "nprogress"
import routes from "virtual:generated-pages"
import NotFoundRender from "./staticsRenders/404"
import CrashRender from "./staticsRenders/crash"
export const ConnectWithApp = (component) => {
return window.app.bindContexts(component)
}
export function GetRoutesComponentMap() {
return routes.reduce((acc, route) => {
const { path, component } = route
acc[path] = component
return acc
}, {})
}
export class RouteRender extends EvitePureComponent {
state = {
renderInitialization: true,
renderComponent: null,
renderError: null,
//pageStatement: new PageStatement(),
routes: GetRoutesComponentMap() ?? {},
crash: null,
}
handleBusEvents = {
"render_initialization": () => {
this.setState({ renderInitialization: true })
},
"render_initialization_done": () => {
this.setState({ renderInitialization: false })
},
"crash": (message, error) => {
this.setState({ crash: { message, error } })
},
"locationChange": (event) => {
this.loadRender()
},
}
componentDidMount() {
this._ismounted = true
this._loadBusEvents()
this.loadRender()
}
componentWillUnmount() {
this._ismounted = false
this._unloadBusEvents()
}
loadRender = (path) => {
if (!this._ismounted) {
console.warn("RouteRender is not mounted, skipping render load")
return false
}
let componentModule = this.state.routes[path ?? this.props.path ?? window.location.pathname] ?? this.props.staticRenders?.NotFound ?? NotFoundRender
// TODO: in a future use, we can use `pageStatement` class for managing statement
window.app.pageStatement = Object.freeze(componentModule.pageStatement) ?? Object.freeze({})
return this.setState({ renderComponent: componentModule })
}
componentDidCatch(info, stack) {
this.setState({ renderError: { info, stack } })
}
render() {
if (this.state.crash) {
const StaticCrashRender = this.props.staticRenders?.Crash ?? CrashRender
return <StaticCrashRender crash={this.state.crash} />
}
if (this.state.renderError) {
if (this.props.staticRenders?.RenderError) {
return React.createElement(this.props.staticRenders?.RenderError, { error: this.state.renderError })
}
return JSON.stringify(this.state.renderError)
}
if (this.state.renderInitialization) {
const StaticInitializationRender = this.props.staticRenders?.initialization ?? null
return <StaticInitializationRender />
}
if (!this.state.renderComponent) {
return null
}
return React.createElement(ConnectWithApp(this.state.renderComponent), this.props)
}
}
export class RenderExtension extends Extension {
initializers = [
async function () {
const defaultTransitionDelay = 150
this.progressBar = progressBar.configure({ parent: "html", showSpinner: false })
this.history.listen((event) => {
this.eventBus.emit("transitionDone", event)
this.eventBus.emit("locationChange", event)
this.progressBar.done()
})
this.history.setLocation = (to, state, delay) => {
const lastLocation = this.history.lastLocation
if (typeof lastLocation !== "undefined" && lastLocation?.pathname === to && lastLocation?.state === state) {
return false
}
this.progressBar.start()
this.eventBus.emit("transitionStart", delay)
setTimeout(() => {
this.history.push({
pathname: to,
}, state)
this.history.lastLocation = this.history.location
}, delay ?? defaultTransitionDelay)
}
this.setToWindowContext("setLocation", this.history.setLocation)
},
]
expose = {
validateLocationSlash: (location) => {
let key = location ?? window.location.pathname
while (key[0] === "/") {
key = key.slice(1, key.length)
}
return key
},
}
window = {
isAppCapacitor: () => window.navigator.userAgent === "capacitor",
bindContexts: (component) => {
let contexts = {
main: {},
app: {},
}
if (typeof component.bindApp === "string") {
if (component.bindApp === "all") {
Object.keys(app).forEach((key) => {
contexts.app[key] = app[key]
})
}
} else {
if (Array.isArray(component.bindApp)) {
component.bindApp.forEach((key) => {
contexts.app[key] = app[key]
})
}
}
if (typeof component.bindMain === "string") {
if (component.bindMain === "all") {
Object.keys(main).forEach((key) => {
contexts.main[key] = main[key]
})
}
} else {
if (Array.isArray(component.bindMain)) {
component.bindMain.forEach((key) => {
contexts.main[key] = main[key]
})
}
}
return (props) => React.createElement(component, { ...props, contexts })
},
}
}
export default RenderExtension

View File

@ -1,225 +0,0 @@
import React from "react"
import { EvitePureComponent } from "evite"
import routes from "virtual:generated-pages"
import progressBar from "nprogress"
import NotFoundRender from "./statics/404"
import CrashRender from "./statics/crash"
export const ConnectWithApp = (component) => {
return window.app.bindContexts(component)
}
export function GetRoutesMap() {
return routes.map((route) => {
const { path } = route
route.name =
path
.replace(/^\//, "")
.replace(/:/, "")
.replace(/\//, "-")
.replace("all(.*)", "not-found") || "home"
route.path = route.path.includes("*") ? "*" : route.path
return route
})
}
export function GetRoutesComponentMap() {
return routes.reduce((acc, route) => {
const { path, component } = route
acc[path] = component
return acc
}, {})
}
// class PageStatement {
// constructor() {
// this.state = {}
// }
// getProxy() {
// }
// }
export class RouteRender extends EvitePureComponent {
state = {
renderInitialization: true,
renderComponent: null,
renderError: null,
//pageStatement: new PageStatement(),
routes: GetRoutesComponentMap() ?? {},
crash: null,
}
handleBusEvents = {
"render_initialization": () => {
this.setState({ renderInitialization: true })
},
"render_initialization_done": () => {
this.setState({ renderInitialization: false })
},
"crash": (message, error) => {
this.setState({ crash: { message, error } })
},
"locationChange": (event) => {
this.loadRender()
},
}
componentDidMount() {
this._ismounted = true
this._loadBusEvents()
this.loadRender()
}
componentWillUnmount() {
this._ismounted = false
this._unloadBusEvents()
}
loadRender = (path) => {
if (!this._ismounted) {
console.warn("RouteRender is not mounted, skipping render load")
return false
}
let componentModule = this.state.routes[path ?? this.props.path ?? window.location.pathname] ?? this.props.staticRenders?.NotFound ?? NotFoundRender
// TODO: in a future use, we can use `pageStatement` class for managing statement
window.app.pageStatement = Object.freeze(componentModule.pageStatement) ?? Object.freeze({})
return this.setState({ renderComponent: componentModule })
}
componentDidCatch(info, stack) {
this.setState({ renderError: { info, stack } })
}
render() {
if (this.state.crash) {
const StaticCrashRender = this.props.staticRenders?.Crash ?? CrashRender
return <StaticCrashRender crash={this.state.crash} />
}
if (this.state.renderError) {
if (this.props.staticRenders?.RenderError) {
return React.createElement(this.props.staticRenders?.RenderError, { error: this.state.renderError })
}
return JSON.stringify(this.state.renderError)
}
if (this.state.renderInitialization) {
const StaticInitializationRender = this.props.staticRenders?.initialization ?? null
return <StaticInitializationRender />
}
if (!this.state.renderComponent) {
return null
}
return React.createElement(ConnectWithApp(this.state.renderComponent), this.props)
}
}
export const extension = {
key: "customRender",
expose: [
{
initialization: [
async (app, main) => {
app.bindContexts = (component) => {
let contexts = {
main: {},
app: {},
}
if (typeof component.bindApp === "string") {
if (component.bindApp === "all") {
Object.keys(app).forEach((key) => {
contexts.app[key] = app[key]
})
}
} else {
if (Array.isArray(component.bindApp)) {
component.bindApp.forEach((key) => {
contexts.app[key] = app[key]
})
}
}
if (typeof component.bindMain === "string") {
if (component.bindMain === "all") {
Object.keys(main).forEach((key) => {
contexts.main[key] = main[key]
})
}
} else {
if (Array.isArray(component.bindMain)) {
component.bindMain.forEach((key) => {
contexts.main[key] = main[key]
})
}
}
return (props) => React.createElement(component, { ...props, contexts })
}
main.setToWindowContext("bindContexts", app.bindContexts)
},
async (app, main) => {
const defaultTransitionDelay = 150
main.progressBar = progressBar.configure({ parent: "html", showSpinner: false })
main.history.listen((event) => {
main.eventBus.emit("transitionDone", event)
main.eventBus.emit("locationChange", event)
main.progressBar.done()
})
main.history.setLocation = (to, state, delay) => {
const lastLocation = main.history.lastLocation
if (typeof lastLocation !== "undefined" && lastLocation?.pathname === to && lastLocation?.state === state) {
return false
}
main.progressBar.start()
main.eventBus.emit("transitionStart", delay)
setTimeout(() => {
main.history.push({
pathname: to,
}, state)
main.history.lastLocation = main.history.location
}, delay ?? defaultTransitionDelay)
}
main.setToWindowContext("setLocation", main.history.setLocation)
},
],
mutateContext: {
validateLocationSlash: (location) => {
let key = location ?? window.location.pathname
while (key[0] === "/") {
key = key.slice(1, key.length)
}
return key
},
},
},
],
}
export default extension

View File

@ -1,8 +1,10 @@
import { Extension } from "evite"
import store from "store"
import defaultSettings from "schemas/defaultSettings.json"
class SettingsController {
constructor() {
export default class SettingsExtension extends Extension {
constructor(app, main) {
super(app, main)
this.storeKey = "app_settings"
this.settings = store.get(this.storeKey) ?? {}
@ -49,18 +51,8 @@ class SettingsController {
return this.settings[key]
}
}
export default {
key: "settings",
expose: [
{
initialization: [
(app, main) => {
app.settingsController = new SettingsController()
window.app.settings = app.settingsController
}
]
},
]
window = {
"settings": this
}
}

View File

@ -1,5 +1,9 @@
export class ShortcutsController {
constructor() {
import { Extension } from "evite"
export default class ShortcutsExtension extends Extension {
constructor(app, main) {
super(app, main)
this.shortcuts = {}
document.addEventListener("keydown", (event) => {
@ -15,15 +19,15 @@ export class ShortcutsController {
if (typeof shortcut.shift === "boolean" && event.shiftKey !== shortcut.shift) {
return
}
if (typeof shortcut.alt === "boolean" && event.altKey !== shortcut.alt) {
return
}
if (typeof shortcut.meta === "boolean" && event.metaKey !== shortcut.meta) {
return
}
if (shortcut.preventDefault) {
event.preventDefault()
}
@ -58,21 +62,8 @@ export class ShortcutsController {
delete this.shortcuts[key]
})
}
}
export const extension = {
key: "shortcuts",
expose: [
{
initialization: [
(app, main) => {
app.ShortcutsController = new ShortcutsController()
main.setToWindowContext("ShortcutsController", app.ShortcutsController)
}
],
},
]
}
export default extension
window = {
ShortcutsController: this
}
}

View File

@ -1,14 +1,9 @@
import { Extension } from "evite"
import { Howl } from "howler"
import config from "config"
export class SoundEngine {
constructor() {
this.sounds = {}
}
initialize = async () => {
this.sounds = await this.getSounds()
}
export default class SoundEngineExtension extends Extension {
sounds = {}
getSounds = async () => {
// TODO: Load custom soundpacks manifests
@ -35,19 +30,14 @@ export class SoundEngine {
return false
}
}
}
export const extension = {
key: "soundEngine",
expose: [
{
initialization: [
async (app, main) => {
app.SoundEngine = new SoundEngine()
main.setToWindowContext("SoundEngine", app.SoundEngine)
await app.SoundEngine.initialize()
}
]
initializers = [
async () => {
this.sounds = await this.getSounds()
}
]
window = {
SoundEngine: this
}
}

View File

@ -1,57 +0,0 @@
import React from "react"
import ReactDOM from "react-dom"
import "./index.less"
export const SplashComponent = ({ props = {}, logo }) => {
return (
<div className="splash_wrapper">
<div {...props.logo} className="splash_logo">
<img src={logo} />
</div>
</div>
)
}
export const extension = (params = {}) => {
return {
key: "splash",
expose: [
{
initialization: [
async (app, main) => {
const fadeOutVelocity = params.velocity ?? 1000 //on milliseconds
const splashElement = document.createElement("div")
splashElement.style = `
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
`
const show = () => {
document.body.appendChild(splashElement)
ReactDOM.render(<SplashComponent logo={params.logo} props={params.props} />, splashElement)
}
const removeSplash = () => {
splashElement.style.animation = `${params.preset ?? "fadeOut"} ${fadeOutVelocity}ms`
setTimeout(() => {
splashElement.remove()
}, fadeOutVelocity)
}
main.eventBus.on("splash_show", show)
main.eventBus.on("splash_close", removeSplash)
},
],
},
],
}
}
export default extension

View File

@ -1,44 +0,0 @@
.splash_wrapper {
overflow: hidden;
//background-color: rgba(240, 242, 245, 0.8);
backdrop-filter: blur(10px);
--webkit-backdrop-filter: blur(10px);
width: 100%;
height: 100%;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.splash_logo {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
img {
width: fit-content;
max-width: 50%;
max-height: 50%;
filter: drop-shadow(14px 10px 10px rgba(128, 128, 128, 0.5));
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

View File

@ -1,10 +1,11 @@
import { Extension } from "evite"
import config from "config"
import store from "store"
import { ConfigProvider } from "antd"
export class ThemeController {
constructor(params) {
this.params = { ...params }
export default class ThemeExtension extends Extension {
constructor(app, main) {
super(app, main)
this.themeManifestStorageKey = "theme"
this.modificationStorageKey = "themeModifications"
@ -14,45 +15,59 @@ export class ThemeController {
this.mutation = null
this.currentVariant = null
this.init()
return this
}
initializers = [
async () => {
this.mainContext.eventBus.on("darkMode", (value) => {
if (value) {
this.applyVariant("dark")
} else {
this.applyVariant("light")
}
})
this.mainContext.eventBus.on("modifyTheme", (value) => {
this.update(value)
this.setModifications(this.mutation)
})
this.mainContext.eventBus.on("resetTheme", () => {
this.resetDefault()
})
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
}
// set global theme
this.theme = theme
// override with static vars
if (theme.staticVars) {
this.update(theme.staticVars)
}
// override theme with modifications
if (modifications) {
this.update(modifications)
}
// apply variation
this.applyVariant(variantKey)
},
]
static get currentVariant() {
return document.documentElement.style.getPropertyValue("--themeVariant")
}
init = () => {
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
}
// set global theme
this.theme = theme
// override with static vars
if (theme.staticVars) {
this.update(theme.staticVars)
}
// override theme with modifications
if (modifications) {
this.update(modifications)
}
// apply variation
this.applyVariant(variantKey)
}
getRootVariables = () => {
let attributes = document.documentElement.getAttribute("style").trim().split(";")
attributes = attributes.slice(0, (attributes.length - 1))
@ -130,36 +145,8 @@ export class ThemeController {
this.setVariant(variant)
}
}
}
export const extension = {
key: "theme",
expose: [
{
initialization: [
async (app, main) => {
app.ThemeController = new ThemeController()
main.eventBus.on("darkMode", (value) => {
if (value) {
app.ThemeController.applyVariant("dark")
} else {
app.ThemeController.applyVariant("light")
}
})
main.eventBus.on("modifyTheme", (value) => {
app.ThemeController.update(value)
app.ThemeController.setModifications(app.ThemeController.mutation)
})
main.eventBus.on("resetTheme", () => {
app.ThemeController.resetDefault()
})
main.setToWindowContext("ThemeController", app.ThemeController)
},
],
},
],
}
export default extension
window = {
ThemeController: this
}
}