From 9090ee8a94ca62eeeb030ee1e9447c5904c4281a Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sun, 5 Feb 2023 13:25:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=9C=A8=E7=BA=BF?= =?UTF-8?q?=E5=90=AC=E9=9F=B3=E4=B9=90=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/com.msgbyte.music/assets/icon.png | Bin 0 -> 1562 bytes .../plugins/com.msgbyte.music/manifest.json | 10 + .../plugins/com.msgbyte.music/package.json | 16 + .../plugins/com.msgbyte.music/src/index.tsx | 20 + .../src/panels/MusicPanel.tsx | 14 + .../com.msgbyte.music/src/translate.ts | 8 + .../plugins/com.msgbyte.music/tsconfig.json | 7 + .../com.msgbyte.music/types/tailchat.d.ts | 502 ++++++++++++++++++ client/web/registry.json | 10 + .../routes/Main/Content/Personal/index.tsx | 5 +- client/web/src/routes/Main/Content/index.tsx | 5 +- 11 files changed, 595 insertions(+), 2 deletions(-) create mode 100644 client/web/plugins/com.msgbyte.music/assets/icon.png create mode 100644 client/web/plugins/com.msgbyte.music/manifest.json create mode 100644 client/web/plugins/com.msgbyte.music/package.json create mode 100644 client/web/plugins/com.msgbyte.music/src/index.tsx create mode 100644 client/web/plugins/com.msgbyte.music/src/panels/MusicPanel.tsx create mode 100644 client/web/plugins/com.msgbyte.music/src/translate.ts create mode 100644 client/web/plugins/com.msgbyte.music/tsconfig.json create mode 100644 client/web/plugins/com.msgbyte.music/types/tailchat.d.ts diff --git a/client/web/plugins/com.msgbyte.music/assets/icon.png b/client/web/plugins/com.msgbyte.music/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..554571752acb552f8936fa58a926b1d69db629ae GIT binary patch literal 1562 zcmZ`(dpOg39R9K}n{}C$u+v#B*V;@-mWs7ybE|eHOUY$ctXl9QY5(*NgNs)<|Oxu+&b&e^T&BU&-;0w=Y5~|c|U)BF6?r5LLm$g000z&PGzbX z_X}`M)jlqfd{xn+Lym5a06dIC&iQHlGC6dn8vqhx0HhHB%%~*kAOI&w0DSfZfSe3~ z9zRXC+g@d8`nxz$!Ovfl`SezzO3~)i_Z|lTsrw6R;Ck9hm8bT2(6_W@E>1v z)~QN!8B|9WyQ=@{>o}GNrY1~T;pyQ`K<{pehab5jHLgy|$7WluuBon0m#rbj7~)4_ z4Hm!(Sxb_jcZb5&!22>lAnICWJSu1VF8zdzqmt2#H#CnEAI1z0^KY7n?@YOW>@Dw} z`gq)8uDeQ8H#DrNWBfpRdT0InNaLZ&+eJik2h$@y7G>9C$YSPQ z)B8H$pO`MrnR&nrYDGOg`-xaEGi&i+uV%r}JIz~7AEtV&a z4*&7?nc0t~oc*UQ?lz9~-Yr=Mj-UkH!^IVCtuFUtE@Jz7(!1rO&%1o(S`)BgbF@X# z3T2y%niP}StR55UrJvgG_4Mz%mgm~ncWkNbHv)~W5tVExR#wUC>d-(hf|a{pM*k(O zk->6+n3xc;n}+7G6~S*%Loui!^=AvqQ|>#$KZH}(5@WAR09DHhEy_C6VPtC3G`Ykx zWK6yxQHl4NZ~fHzd;EHX)%OrUn2_IK1~vFE5;a7FQuu1+)n>%=o`qO#QCuv)4{FNb z5ORq{^I>j<^a1yRt)T3@L(Ge(aB1SQ@l}#Mu3|jMsQ6>bSmT8iv&O<)J;<%llxTRL z;D}MHnk>dv0o)o^=9`(KaJ^NAy;+U%7MHB;B;=Ncy$AfmHHDg>Z|PD?y@$SfGGs|o zSBHH0z7cxJw-j?H6RWMW6)#hsVg0sqw6jd_iS$A+!RzS0^FJ;Y^ z9lQ*T2qmwuPqR;Pb&WAg|V2rot<5Rq9s#9p+vB~{?0nuIcSL*N)yC}gJyF;}w zZ*BWRol41O=%Po|nLSY>-9PV5e#^33JXcy9NXTNzE-=etA{UwAN=C9no4$NsGmDHz ziV{OpTe5?amHx7;mLKV*)~SNAcC126X*X2@{|A0k7`c>LAPxAf?BdSlqSjYZEJh$= zY(2HoxqyAvV(QIgXBj!_k_AaX#^+J*SX1cE$r{u_E4Ck@@HW}|IspKdad6g9P75o#(#`krt zXbl3*ZR&k^HBhYol#o__P6qk0?|{!e3+2F9X!K;; zWWL9?Rf-UKTI$5gYijmCGA#w<=;x0O1?@$r<|i_oG|J>BJp)HyaQad;=GiiJApFq&D0Nz3I{|9bK zf>pNxPYH_+G5th1zVO&VULY3-iTPX{j}LJG2uV{+=)eVgOik-{);BQvnE-}I5;;g+ zysk?k2BwRnO0BB`VE9KYTzz|MZFNI6wpy!RO3!pAg3o%(pb3gilBxnQXztW}itm|! E0qPsAPXGV_ literal 0 HcmV?d00001 diff --git a/client/web/plugins/com.msgbyte.music/manifest.json b/client/web/plugins/com.msgbyte.music/manifest.json new file mode 100644 index 00000000..fdc74504 --- /dev/null +++ b/client/web/plugins/com.msgbyte.music/manifest.json @@ -0,0 +1,10 @@ +{ + "label": "在线听音乐", + "name": "com.msgbyte.music", + "url": "/plugins/com.msgbyte.music/index.js", + "icon": "/plugins/com.msgbyte.music/assets/icon.png", + "version": "0.0.0", + "author": "moonrailgun", + "description": "提供在线听音乐服务,内容来自网络", + "requireRestart": true +} diff --git a/client/web/plugins/com.msgbyte.music/package.json b/client/web/plugins/com.msgbyte.music/package.json new file mode 100644 index 00000000..4a01744c --- /dev/null +++ b/client/web/plugins/com.msgbyte.music/package.json @@ -0,0 +1,16 @@ +{ + "name": "@plugins/com.msgbyte.music", + "main": "src/index.tsx", + "version": "0.0.0", + "description": "提供在线听音乐服务,内容来自网络", + "private": true, + "scripts": { + "sync:declaration": "tailchat declaration github" + }, + "dependencies": {}, + "devDependencies": { + "@types/styled-components": "^5.1.26", + "react": "18.2.0", + "styled-components": "^5.3.6" + } +} diff --git a/client/web/plugins/com.msgbyte.music/src/index.tsx b/client/web/plugins/com.msgbyte.music/src/index.tsx new file mode 100644 index 00000000..cd5425f1 --- /dev/null +++ b/client/web/plugins/com.msgbyte.music/src/index.tsx @@ -0,0 +1,20 @@ +import { regCustomPanel } from '@capital/common'; +import { Loadable } from '@capital/component'; +import { Translate } from './translate'; + +const PLUGIN_NAME = 'com.msgbyte.music'; + +console.log(`Plugin ${PLUGIN_NAME} is loaded`); + +regCustomPanel({ + position: 'navbar-more', + icon: 'mdi:disc-player', + name: `${PLUGIN_NAME}/musicpanel`, + label: Translate.musicpanel, + render: Loadable( + () => import('./panels/MusicPanel').then((module) => module.MusicPanel), + { + componentName: `${PLUGIN_NAME}:CustomMusicPanelRender`, + } + ), +}); diff --git a/client/web/plugins/com.msgbyte.music/src/panels/MusicPanel.tsx b/client/web/plugins/com.msgbyte.music/src/panels/MusicPanel.tsx new file mode 100644 index 00000000..0c4eecd5 --- /dev/null +++ b/client/web/plugins/com.msgbyte.music/src/panels/MusicPanel.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { WebviewKeepAlive } from '@capital/component'; + +const url = 'https://music.moonrailgun.com/'; + +/** + * 音乐面板 + */ +export const MusicPanel: React.FC = React.memo(() => { + return ( + + ); +}); +MusicPanel.displayName = 'MusicPanel'; diff --git a/client/web/plugins/com.msgbyte.music/src/translate.ts b/client/web/plugins/com.msgbyte.music/src/translate.ts new file mode 100644 index 00000000..f648055b --- /dev/null +++ b/client/web/plugins/com.msgbyte.music/src/translate.ts @@ -0,0 +1,8 @@ +import { localTrans } from '@capital/common'; + +export const Translate = { + musicpanel: localTrans({ + 'zh-CN': '在线听音乐', + 'en-US': 'YesPlayMusic', + }), +}; diff --git a/client/web/plugins/com.msgbyte.music/tsconfig.json b/client/web/plugins/com.msgbyte.music/tsconfig.json new file mode 100644 index 00000000..d9b47ed0 --- /dev/null +++ b/client/web/plugins/com.msgbyte.music/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "jsx": "react", + "importsNotUsedAsValues": "error" + } +} diff --git a/client/web/plugins/com.msgbyte.music/types/tailchat.d.ts b/client/web/plugins/com.msgbyte.music/types/tailchat.d.ts new file mode 100644 index 00000000..ceefa1fe --- /dev/null +++ b/client/web/plugins/com.msgbyte.music/types/tailchat.d.ts @@ -0,0 +1,502 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/// + +/** + * 该文件由 Tailchat 自动生成 + * 用于插件的类型声明 + * 生成命令: pnpm run plugins:declaration:generate + */ + +/** + * Tailchat 通用 + */ +declare module '@capital/common' { + export const useGroupPanelParams: any; + + /** + * 打开模态框 + * @deprecated 请从 @capital/component 引入 + */ + export const openModal: ( + content: React.ReactNode, + + props?: { + /** + * 是否显示右上角的关闭按钮 + * @default false + */ + closable?: boolean; + + /** + * 遮罩层是否可关闭 + */ + maskClosable?: boolean; + + /** + * 关闭modal的回调 + */ + onCloseModal?: () => void; + } + ) => number; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const closeModal: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const ModalWrapper: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const useModalContext: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const openConfirmModal: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const openReconfirmModal: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const Loadable: any; + + export const getGlobalState: any; + + export const useGlobalSocketEvent: ( + eventName: string, + callback: (data: T) => void + ) => void; + + export const getJWTUserInfo: () => Promise<{ + _id?: string; + nickname?: string; + email?: string; + avatar?: string; + }>; + + export const dataUrlToFile: any; + + export const urlSearchStringify: any; + + export const urlSearchParse: any; + + export const appendUrlSearch: any; + + export const getServiceWorkerRegistration: any; + + export const getServiceUrl: () => string; + + export const getCachedUserInfo: ( + userId: string, + refetch?: boolean + ) => Promise<{ + _id: string; + email: string; + nickname: string; + discriminator: string; + avatar: string | null; + temporary: boolean; + }>; + + export const getCachedConverseInfo: any; + + export const getCachedBaseGroupInfo: any; + + export const getCachedUserSettings: any; + + /** + * 本地翻译 + * @example + * localTrans({'zh-CN': '你好', 'en-US': 'Hello'}); + * + * @param trans 翻译对象 + */ + export const localTrans: (trans: Record<'zh-CN' | 'en-US', string>) => string; + + export const getLanguage: any; + + export const sharedEvent: any; + + export const useAsync: Promise>( + fn: T, + deps?: React.DependencyList + ) => { loading: boolean; value?: any; error?: Error }; + + export const useAsyncFn: Promise>( + fn: T, + deps?: React.DependencyList + ) => [{ loading: boolean; value?: any; error?: Error }, T]; + + export const useAsyncRefresh: Promise>( + fn: T, + deps?: React.DependencyList + ) => { loading: boolean; value?: any; error?: Error; refresh: () => void }; + + export const useAsyncRequest: Promise>( + fn: T, + deps?: React.DependencyList + ) => [{ loading: boolean; value?: any }, T]; + + export const uploadFile: any; + + export const showToasts: ( + message: string, + type?: 'info' | 'success' | 'error' | 'warning' + ) => void; + + export const showSuccessToasts: any; + + export const showErrorToasts: (error: any) => void; + + export const fetchAvailableServices: any; + + export const isValidStr: (str: any) => str is string; + + export const useGroupPanelInfo: any; + + export const sendMessage: any; + + export const showMessageTime: any; + + export const joinArray: any; + + export const navigate: any; + + export const useLocation: any; + + export const useNavigate: any; + + /** + * @deprecated please use createMetaFormSchema from @capital/component + */ + export const createFastFormSchema: any; + + /** + * @deprecated please use metaFormFieldSchema from @capital/component + */ + export const fieldSchema: any; + + export const useCurrentUserInfo: any; + + export const createPluginRequest: (pluginName: string) => { + get: (actionName: string, config?: any) => Promise; + post: (actionName: string, data?: any, config?: any) => Promise; + }; + + export const postRequest: any; + + export const pluginCustomPanel: any; + + export const regCustomPanel: any; + + export const pluginGroupPanel: any; + + export const regGroupPanel: any; + + export const messageInterpreter: { + name?: string; + explainMessage: (message: string) => React.ReactNode; + }[]; + + export const regMessageInterpreter: (interpreter: { + name?: string; + explainMessage: (message: string) => React.ReactNode; + }) => void; + + export const getMessageRender: (message: string) => React.ReactNode; + + export const regMessageRender: ( + render: (message: string) => React.ReactNode + ) => void; + + export const getMessageTextDecorators: any; + + export const regMessageTextDecorators: any; + + export const ChatInputActionContextProps: any; + + export const pluginChatInputActions: any; + + export const regChatInputAction: any; + + export const regSocketEventListener: (item: { + eventName: string; + eventFn: (...args: any[]) => void; + }) => void; + + export const pluginColorScheme: any; + + export const regPluginColorScheme: any; + + export const pluginInspectServices: any; + + export const regInspectService: any; + + export const pluginMessageExtraParsers: any; + + export const regMessageExtraParser: any; + + export const pluginRootRoute: any; + + export const regPluginRootRoute: any; + + export const pluginPanelActions: any; + + export const regPluginPanelAction: ( + action: + | { + name: string; + label: string; + icon: string; + position: 'group'; + onClick: (ctx: { groupId: string; panelId: string }) => void; + } + | { + name: string; + label: string; + icon: string; + position: 'dm'; + onClick: (ctx: { converseId: string }) => void; + } + ) => void; + + export const pluginPermission: any; + + export const regPluginPermission: (permission: { + /** + * 权限唯一key, 用于写入数据库 + * 如果为插件则权限点应当符合命名规范, 如: plugin.com.msgbyte.github.manage + */ + key: string; + /** + * 权限点显示名称 + */ + title: string; + /** + * 权限描述 + */ + desc: string; + /** + * 是否默认开启 + */ + default: boolean; + /** + * 是否依赖其他权限点 + */ + required?: string[]; + }) => void; + + export const pluginGroupPanelBadges: any; + + export const regGroupPanelBadge: any; + + export const pluginGroupTextPanelExtraMenus: any; + + export const regPluginGroupTextPanelExtraMenu: any; + + export const pluginUserExtraInfo: any; + + export const regUserExtraInfo: any; + + export const pluginSettings: any; + + export const regPluginSettings: any; + + export const useGroupIdContext: () => string; + + export const useGroupPanelContext: () => { + groupId: string; + panelId: string; + } | null; + + export const useSocketContext: any; +} + +/** + * Tailchat 组件 + */ +declare module '@capital/component' { + export const Button: any; + + export const Checkbox: any; + + export const Input: any; + + export const Divider: any; + + export const Space: any; + + export const Menu: any; + + export const Table: any; + + export const Switch: any; + + export const Tooltip: any; + + /** + * @link https://ant.design/components/notification-cn/ + */ + export const notification: any; + + export const Empty: React.FC< + React.PropsWithChildren<{ + prefixCls?: string; + className?: string; + style?: React.CSSProperties; + imageStyle?: React.CSSProperties; + image?: React.ReactNode; + description?: React.ReactNode; + }> + >; + + export const TextArea: any; + + export const Avatar: any; + + export const SensitiveText: React.FC<{ className?: string; text: string }>; + + export const Icon: React.FC<{ icon: string } & React.SVGProps>; + + export const CopyableText: React.FC<{ + className?: string; + style?: React.CSSProperties; + config?: + | boolean + | { + text?: string; + onCopy?: (event?: React.MouseEvent) => void; + icon?: React.ReactNode; + tooltips?: boolean | React.ReactNode; + format?: 'text/plain' | 'text/html'; + }; + }>; + + export const WebFastForm: any; + + export const WebMetaForm: any; + + export const createMetaFormSchema: any; + + export const metaFormFieldSchema: any; + + export const Link: any; + + export const MessageAckContainer: any; + + export const Image: any; + + export const IconBtn: React.FC<{ + icon: string; + className?: string; + iconClassName?: string; + size?: 'small' | 'middle' | 'large'; + shape?: 'circle' | 'square'; + title?: string; + danger?: boolean; + active?: boolean; + disabled?: boolean; + onClick?: React.MouseEventHandler; + }>; + + export const PillTabs: any; + + export const PillTabPane: any; + + export const LoadingSpinner: React.FC<{ tip?: string }>; + + export const FullModalField: any; + + export const DefaultFullModalInputEditorRender: any; + + export const DefaultFullModalTextAreaEditorRender: any; + + export const openModal: ( + content: React.ReactNode, + + props?: { + /** + * 是否显示右上角的关闭按钮 + * @default false + */ + closable?: boolean; + + /** + * 遮罩层是否可关闭 + */ + maskClosable?: boolean; + + /** + * 关闭modal的回调 + */ + onCloseModal?: () => void; + } + ) => number; + + export const closeModal: any; + + export const ModalWrapper: any; + + export const useModalContext: any; + + export const openConfirmModal: any; + + export const openReconfirmModal: any; + + export const Loadable: any; + + export const Loading: React.FC<{ + spinning: boolean; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; + }>; + + export const LoadingOnFirst: React.FC<{ + spinning: boolean; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; + }>; + + export const SidebarView: any; + + export const GroupPanelSelector: any; + + export const Emoji: any; + + export const PortalAdd: any; + + export const PortalRemove: any; + + export const ErrorBoundary: any; + + export const UserAvatar: React.FC<{ + userId: string; + className?: string; + style?: React.CSSProperties; + size?: 'large' | 'small' | 'default' | number; + }>; + + export const UserName: React.FC<{ + userId: string; + className?: string; + style?: React.CSSProperties; + }>; + + export const Markdown: any; + + export const Webview: any; + + export const WebviewKeepAlive: any; +} diff --git a/client/web/registry.json b/client/web/registry.json index ca98f539..61f80c3e 100644 --- a/client/web/registry.json +++ b/client/web/registry.json @@ -124,5 +124,15 @@ "author": "msgbyte", "description": "工具哇 —— 在线小工具", "requireRestart": false + }, + { + "label": "在线听音乐", + "name": "com.msgbyte.music", + "url": "/plugins/com.msgbyte.music/index.js", + "icon": "/plugins/com.msgbyte.music/assets/icon.png", + "version": "0.0.0", + "author": "moonrailgun", + "description": "提供在线听音乐服务,内容来自网络", + "requireRestart": true } ] diff --git a/client/web/src/routes/Main/Content/Personal/index.tsx b/client/web/src/routes/Main/Content/Personal/index.tsx index de8a584f..ba42ea3f 100644 --- a/client/web/src/routes/Main/Content/Personal/index.tsx +++ b/client/web/src/routes/Main/Content/Personal/index.tsx @@ -1,3 +1,4 @@ +import { ErrorBoundary } from '@/components/ErrorBoundary'; import { useUserSessionPreference } from '@/hooks/useUserPreference'; import { pluginCustomPanel } from '@/plugin/common'; import React, { useEffect } from 'react'; @@ -31,7 +32,9 @@ export const Personal: React.FC = React.memo(() => { {React.createElement(p.render)} + } /> ))} diff --git a/client/web/src/routes/Main/Content/index.tsx b/client/web/src/routes/Main/Content/index.tsx index 70d1cf88..0f072576 100644 --- a/client/web/src/routes/Main/Content/index.tsx +++ b/client/web/src/routes/Main/Content/index.tsx @@ -4,6 +4,7 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import { Group } from './Group'; import { Inbox } from './Inbox'; import { pluginCustomPanel } from '@/plugin/common'; +import { ErrorBoundary } from '@/components/ErrorBoundary'; export const MainContent: React.FC = React.memo(() => { return ( @@ -22,7 +23,9 @@ export const MainContent: React.FC = React.memo(() => { {React.createElement(p.render)} + } /> ))}