#![allow(clippy::tabs_in_doc_comments)]
use std::{collections::HashMap, fmt};
use millennium_macros::default_runtime;
use serde::de::DeserializeOwned;
use serde_json::Value as JsonValue;
use crate::{utils::config::PluginConfig, AppHandle, Invoke, InvokeHandler, PageLoadPayload, RunEvent, Runtime, Window};
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
pub trait Plugin<R: Runtime>: Send {
fn name(&self) -> &'static str;
#[allow(unused_variables)]
fn initialize(&mut self, app: &AppHandle<R>, config: JsonValue) -> Result<()> {
Ok(())
}
fn initialization_script(&self) -> Option<String> {
None
}
#[allow(unused_variables)]
fn created(&mut self, window: Window<R>) {}
#[allow(unused_variables)]
fn on_page_load(&mut self, window: Window<R>, payload: PageLoadPayload) {}
#[allow(unused_variables)]
fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {}
#[allow(unused_variables)]
fn extend_api(&mut self, invoke: Invoke<R>) {}
}
type SetupHook<R> = dyn FnOnce(&AppHandle<R>) -> Result<()> + Send;
type SetupWithConfigHook<R, T> = dyn FnOnce(&AppHandle<R>, T) -> Result<()> + Send;
type OnWebviewReady<R> = dyn FnMut(Window<R>) + Send;
type OnEvent<R> = dyn FnMut(&AppHandle<R>, &RunEvent) + Send;
type OnPageLoad<R> = dyn FnMut(Window<R>, PageLoadPayload) + Send;
pub struct Builder<R: Runtime, C: DeserializeOwned = ()> {
name: &'static str,
invoke_handler: Box<InvokeHandler<R>>,
setup: Option<Box<SetupHook<R>>>,
setup_with_config: Option<Box<SetupWithConfigHook<R, C>>>,
js_init_script: Option<String>,
on_page_load: Box<OnPageLoad<R>>,
on_webview_ready: Box<OnWebviewReady<R>>,
on_event: Box<OnEvent<R>>
}
impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
pub fn new(name: &'static str) -> Self {
Self {
name,
setup: None,
setup_with_config: None,
js_init_script: None,
invoke_handler: Box::new(|_| ()),
on_page_load: Box::new(|_, _| ()),
on_webview_ready: Box::new(|_| ()),
on_event: Box::new(|_, _| ())
}
}
#[must_use]
pub fn invoke_handler<F>(mut self, invoke_handler: F) -> Self
where
F: Fn(Invoke<R>) + Send + Sync + 'static
{
self.invoke_handler = Box::new(invoke_handler);
self
}
#[must_use]
pub fn js_init_script(mut self, js_init_script: String) -> Self {
self.js_init_script = Some(js_init_script);
self
}
#[must_use]
pub fn setup<F>(mut self, setup: F) -> Self
where
F: FnOnce(&AppHandle<R>) -> Result<()> + Send + 'static
{
self.setup.replace(Box::new(setup));
self
}
#[must_use]
pub fn setup_with_config<F>(mut self, setup_with_config: F) -> Self
where
F: FnOnce(&AppHandle<R>, C) -> Result<()> + Send + 'static
{
self.setup_with_config.replace(Box::new(setup_with_config));
self
}
#[must_use]
pub fn on_page_load<F>(mut self, on_page_load: F) -> Self
where
F: FnMut(Window<R>, PageLoadPayload) + Send + Sync + 'static
{
self.on_page_load = Box::new(on_page_load);
self
}
#[must_use]
pub fn on_webview_ready<F>(mut self, on_webview_ready: F) -> Self
where
F: FnMut(Window<R>) + Send + Sync + 'static
{
self.on_webview_ready = Box::new(on_webview_ready);
self
}
#[must_use]
pub fn on_event<F>(mut self, on_event: F) -> Self
where
F: FnMut(&AppHandle<R>, &RunEvent) + Send + Sync + 'static
{
self.on_event = Box::new(on_event);
self
}
pub fn build(self) -> MillenniumPlugin<R, C> {
MillenniumPlugin {
name: self.name,
invoke_handler: self.invoke_handler,
setup: self.setup,
setup_with_config: self.setup_with_config,
js_init_script: self.js_init_script,
on_page_load: self.on_page_load,
on_webview_ready: self.on_webview_ready,
on_event: self.on_event
}
}
}
pub struct MillenniumPlugin<R: Runtime, C: DeserializeOwned = ()> {
name: &'static str,
invoke_handler: Box<InvokeHandler<R>>,
setup: Option<Box<SetupHook<R>>>,
setup_with_config: Option<Box<SetupWithConfigHook<R, C>>>,
js_init_script: Option<String>,
on_page_load: Box<OnPageLoad<R>>,
on_webview_ready: Box<OnWebviewReady<R>>,
on_event: Box<OnEvent<R>>
}
impl<R: Runtime, C: DeserializeOwned> Plugin<R> for MillenniumPlugin<R, C> {
fn name(&self) -> &'static str {
self.name
}
fn initialize(&mut self, app: &AppHandle<R>, config: JsonValue) -> Result<()> {
if let Some(s) = self.setup.take() {
(s)(app)?;
}
if let Some(s) = self.setup_with_config.take() {
(s)(app, serde_json::from_value(config)?)?;
}
Ok(())
}
fn initialization_script(&self) -> Option<String> {
self.js_init_script.clone()
}
fn created(&mut self, window: Window<R>) {
(self.on_webview_ready)(window)
}
fn on_page_load(&mut self, window: Window<R>, payload: PageLoadPayload) {
(self.on_page_load)(window, payload)
}
fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
(self.on_event)(app, event)
}
fn extend_api(&mut self, invoke: Invoke<R>) {
(self.invoke_handler)(invoke)
}
}
#[default_runtime(crate::MillenniumWebview, millennium_webview)]
pub(crate) struct PluginStore<R: Runtime> {
store: HashMap<&'static str, Box<dyn Plugin<R>>>
}
impl<R: Runtime> fmt::Debug for PluginStore<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PluginStore").field("plugins", &self.store.keys()).finish()
}
}
impl<R: Runtime> Default for PluginStore<R> {
fn default() -> Self {
Self { store: HashMap::new() }
}
}
impl<R: Runtime> PluginStore<R> {
pub fn register<P: Plugin<R> + 'static>(&mut self, plugin: P) -> bool {
self.store.insert(plugin.name(), Box::new(plugin)).is_some()
}
pub(crate) fn initialize(&mut self, app: &AppHandle<R>, config: &PluginConfig) -> crate::Result<()> {
self.store.values_mut().try_for_each(|plugin| {
plugin
.initialize(app, config.0.get(plugin.name()).cloned().unwrap_or_default())
.map_err(|e| crate::Error::PluginInitialization(plugin.name().to_string(), e.to_string()))
})
}
pub(crate) fn initialization_script(&self) -> String {
self.store
.values()
.filter_map(|p| p.initialization_script())
.fold(String::new(), |acc, script| format!("{}\n(function () {{ {} }})();", acc, script))
}
pub(crate) fn created(&mut self, window: Window<R>) {
self.store.values_mut().for_each(|plugin| plugin.created(window.clone()))
}
pub(crate) fn on_page_load(&mut self, window: Window<R>, payload: PageLoadPayload) {
self.store
.values_mut()
.for_each(|plugin| plugin.on_page_load(window.clone(), payload.clone()))
}
pub(crate) fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
self.store.values_mut().for_each(|plugin| plugin.on_event(app, event))
}
pub(crate) fn extend_api(&mut self, mut invoke: Invoke<R>) {
let command = invoke.message.command.replace("plugin:", "");
let mut tokens = command.split('|');
let target = tokens.next().unwrap();
if let Some(plugin) = self.store.get_mut(target) {
invoke.message.command = tokens.next().map(|c| c.to_string()).unwrap_or_else(String::new);
plugin.extend_api(invoke);
} else {
invoke.resolver.reject(format!("plugin {} not found", target));
}
}
}