This commit is contained in:
2026-01-30 11:49:04 +03:00
commit f8bb05a652
48 changed files with 9538 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
import React from "react";
import { useDispatch } from 'react-redux';
import { Provider } from 'react-redux';
import manifest from '../manifest';
import usePlugin from '../utils/usePlugin';
const RootComponent: React.FC = () => {
const dispatch = useDispatch();
const plugin = usePlugin();
return (
<div>
{/* Add your root component content here */}
<p>Template Plugin Root Component</p>
</div>
)
}
export default RootComponent
+1
View File
@@ -0,0 +1 @@
// Add your global styles here
+14
View File
@@ -0,0 +1,14 @@
import { getPluginAssetsPath } from '../utils/utils';
export const basePath = getPluginAssetsPath();
// Add your assets configuration here
export type AssetsType = {
// Define your asset types
}
const assets: AssetsType = {
// Add your assets here
} as const
export default assets
+5
View File
@@ -0,0 +1,5 @@
import Svgs from './svgs.js';
export {
Svgs,
};
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+17
View File
@@ -0,0 +1,17 @@
import * as React from 'react';
type Props = {
icon: string,
className?: string,
} & React.HTMLAttributes<HTMLElement>
export default function CompassIcon({icon, className, ...rest}: Props): JSX.Element {
// All compass icon classes start with icon,
// so not expecting that prefix in props.
return (
<i
className={`CompassIcon icon-${icon} ${className}`}
{...rest}
/>
);
}
+26
View File
@@ -0,0 +1,26 @@
import { PluginRegistry } from 'loop-plugin-sdk';
import { Store, Action } from 'redux';
import { GlobalState } from 'loop-plugin-sdk/loop/types/store';
import 'loop-plugin-sdk/window'
import manifest from './manifest';
import registerApp from './registerApp';
export default class Plugin {
// @ts-ignore
public store: Store;
// @ts-ignore
public registry: PluginRegistry
public onStoreChanged: any;
public uninitialize: any;
public async initialize(registry: PluginRegistry, store: Store<GlobalState, Action<Record<string, unknown>>>) {
this.store = store;
this.registry = registry;
await registerApp(this, registry, store)
}
}
window.registerPlugin(manifest.id, new Plugin());
+49
View File
@@ -0,0 +1,49 @@
import { PluginRegistry } from 'loop-plugin-sdk';
import { getCurrentUserLocale } from 'loop-plugin-sdk/loop/redux/selectors/entities/i18n';
import { Action, Store } from 'redux';
import RootComponent from './components/RootComponent';
import Plugin from './index';
import manifest from './manifest';
import reducer from './store/reducers';
import * as React from 'react';
import { getTranslations } from './utils/utils';
import { Provider, useSelector } from 'react-redux';
import { IntlProvider } from 'react-intl';
import { GlobalState } from 'loop-plugin-sdk/loop/types/store';
export const pluginStoreId = `plugins-${manifest.id}`;
export default async function Initialize(plugin: Plugin, registry: PluginRegistry, store: Store<GlobalState, Action<Record<string, unknown>>>) {
const Providers: React.FC<React.PropsWithChildren<any>> = ({ children }) => {
const locale = useSelector((state) => getCurrentUserLocale(state as GlobalState) || 'en');
return (
<IntlProvider locale={locale} key={locale} messages={getTranslations(locale)}>
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
};
const regIds: string[] = [];
registry.registerReducer(reducer);
registry.registerTranslations(getTranslations);
plugin.uninitialize = () => {
// Add cleanup logic here
};
const reinit = () => {
if (regIds.length > 0) {
regIds.forEach(regId => registry.unregisterComponent(regId));
regIds.splice(0, regIds.length);
}
regIds.push(registry.registerRootComponent(() => (
<Providers>
<RootComponent />
</Providers>
)));
};
reinit();
}
+13
View File
@@ -0,0 +1,13 @@
import { DispatchFunc } from '../types/store';
import { ACTIONS } from './reducers';
import { AnyAction } from 'redux';
// Add your action creators here
export function exampleAction(data: string) {
return ((dispatch: DispatchFunc) => {
dispatch({
type: ACTIONS.EXAMPLE_ACTION,
data: { example: data }
});
}) as unknown as AnyAction;
}
+37
View File
@@ -0,0 +1,37 @@
import { combineReducers } from 'redux';
import { PluginStore } from '../types/store';
export type ActionType = {
type: ACTIONS
data: PluginActionData
}
export enum ACTIONS {
// Add your action types here
EXAMPLE_ACTION = 'EXAMPLE_ACTION',
}
export type PluginActionData = {
// Add your action data types here
example?: string;
}
const EmptyState: PluginStore = {
// Add your initial state here
}
function pluginState(state: PluginStore = EmptyState, { type, data }: ActionType): PluginStore {
switch (type) {
case ACTIONS.EXAMPLE_ACTION:
return {
...state,
// Handle action
}
default:
return state;
}
}
export default combineReducers({
pluginState,
});
+15
View File
@@ -0,0 +1,15 @@
import Plugin from '../index';
declare module "*.module.css";
declare module "*.module.scss";
declare global {
const __PLUGIN_DEV__: boolean;
const __PLUGIN_COMPONENTS_HOST__: string | null;
interface Window {
plugins: Record<string, Plugin | unknown>;
basename: string;
desktopAPI?: any;
}
}
+16
View File
@@ -0,0 +1,16 @@
import { GlobalState } from 'loop-plugin-sdk/loop/types/store';
import { ActionType } from '../store/reducers';
export type PluginStore = {
// Add your store state here
}
export type GlobalStatePlugin = GlobalState & {
[name: string]: {
pluginState: PluginStore
}
}
export type GetStateType = () => GlobalStatePlugin
export type DispatchFunc = (action: ActionType) => any;
+1
View File
@@ -0,0 +1 @@
// trackEvent: (event: Telemetry.Event, source: Telemetry.Source, props?: Record<string, string>) => void,
+39
View File
@@ -0,0 +1,39 @@
import Client4 from 'loop-plugin-sdk/loop/client/client4';
import manifest from '../manifest';
export type StdApiResp<T = undefined> = {
status: string
error?: string
data?: T
}
class ApiClient {
client = new Client4();
async exampleRequest(): Promise<StdApiResp<{ message: string }>> {
try {
// @ts-ignore
return await this.client.doFetch<StdApiResp<{ message: string }>>(`/plugins/${manifest.id}/example`, {
method: 'GET',
credentials: 'include',
})
} catch (error) {
console.error(error);
return {status: 'error', error: error as string};
}
}
async apiPingServerStatus(): Promise<boolean> {
try {
const response = await this.client.ping();
return response.status === 'OK';
} catch (err) {
console.error('Ошибка пинга сервера:', err);
return false;
}
}
}
const apiClient = new ApiClient();
export default apiClient
+18
View File
@@ -0,0 +1,18 @@
import { getTheme } from 'loop-plugin-sdk/loop/redux/selectors/entities/preferences';
import { GlobalState } from 'loop-plugin-sdk/loop/types/store';
import React, { useEffect } from "react";
import { useSelector } from 'react-redux';
import { isDarkTheme } from './colorUtils';
const useIsDarkTheme = () => {
const [isDark, setIsDark] = React.useState(false);
const theme = useSelector((state: GlobalState) => getTheme(state))
useEffect(() => {
setIsDark(isDarkTheme());
}, [theme])
return isDark
}
export default useIsDarkTheme
+6
View File
@@ -0,0 +1,6 @@
import Plugin from '../index';
import manifest from '../manifest';
export default function usePlugin() {
return window.plugins[manifest.id] as Plugin
}
+97
View File
@@ -0,0 +1,97 @@
import { WebSocketClient } from 'loop-plugin-sdk/loop/client';
import { DispatchFunc } from 'loop-plugin-sdk/loop/redux/types/actions';
import { GlobalState } from 'loop-plugin-sdk/loop/types/store';
import { AnyAction } from 'redux';
import en from '../../i18n/en.json';
import ru from '../../i18n/ru.json';
import Plugin from '../index';
import manifest from '../manifest';
import { pluginStoreId } from '../registerApp';
import { GlobalStatePlugin } from '../types/store';
export function getPluginAssetsPath() {
return `${window.basename || ''}/plugins/${manifest.id}/assets`;
}
export function getPlugin(): Plugin {
return window.plugins[manifest.id] as Plugin;
}
export function isDesktopApp(): boolean {
return window.navigator.userAgent.indexOf('Electron') !== -1;
}
export function getWebappUtils() {
let utils;
try {
utils = window.opener ? window.opener.WebappUtils : window.WebappUtils;
} catch (err) {
console.error(err);
}
return utils;
}
export type ProductApi = {
selectRhsPost: (postId: string) => (dispatch: DispatchFunc) => AnyAction
closeRhs: () => (dispatch: DispatchFunc) => AnyAction
getIsRhsOpen: (state: GlobalState) => boolean
getRhsSelectedPostId: (state: GlobalState) => string
useWebSocketClient: () => WebSocketClient
}
export function getProductApi(): ProductApi {
let utils: any;
try {
// @ts-ignore
utils = window.opener ? window.opener.ProductApi : window['ProductApi'];
} catch (err) {
console.error(err);
}
return utils;
}
export function getPluginStoreFromState(state: GlobalStatePlugin) {
return state[pluginStoreId].pluginState;
}
export function getTranslations(locale: string) {
switch (locale) {
case "en":
return en;
case "ru":
return ru;
}
return en;
}
export function getRandomInt(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export async function waitForServer(
pingFn: () => Promise<any>,
interval = 2000,
maxAttempts = 10
): Promise<void> {
let attempts = 0;
return new Promise((resolve, reject) => {
const check = async () => {
attempts++;
const ok = await pingFn();
if (ok) {
resolve();
return;
}
if (attempts >= maxAttempts) {
reject(new Error("Не удалось подключиться после нескольких попыток"));
return;
}
setTimeout(check, interval);
};
check();
});
}