diff --git a/.gitignore b/.gitignore index 05d49be..77cc42f 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,5 @@ package-lock.json docker/mysql/data/* + +client_dist \ No newline at end of file diff --git a/client/.vscode/settings.json b/client/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/client/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/client/package.json b/client/package.json index 31b8c91..4824ba1 100644 --- a/client/package.json +++ b/client/package.json @@ -4,7 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "dev:web": "pnpm -C packages/rtc-web dev" + "dev:web": "pnpm -C packages/rtc-web dev", + "build:web": "pnpm -C packages/rtc-web build" }, "keywords": [], "author": "", diff --git a/client/packages/rtc-web/.eslintrc.cjs b/client/packages/rtc-web/.eslintrc.cjs index 874f700..6a48d3c 100644 --- a/client/packages/rtc-web/.eslintrc.cjs +++ b/client/packages/rtc-web/.eslintrc.cjs @@ -18,10 +18,11 @@ module.exports = { }, plugins: ['vue', '@typescript-eslint', 'prettier'], rules: { - indent: ['error', 2], + indent: ['error', 2, { offsetTernaryExpressions: true }], semi: ['error', 'always'], 'vue/attribute-hyphenation': 'off', '@typescript-eslint/no-explicit-any': 'off', 'no-debugger': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', }, }; diff --git a/client/packages/rtc-web/package.json b/client/packages/rtc-web/package.json index 9b0982a..9908eab 100644 --- a/client/packages/rtc-web/package.json +++ b/client/packages/rtc-web/package.json @@ -11,10 +11,15 @@ "@types/lodash": "^4.14.195", "@vitejs/plugin-vue-jsx": "^3.0.1", "@vueuse/core": "^10.2.0", + "@vueuse/router": "^10.2.1", + "dayjs": "^1.11.9", "lodash": "^4.17.21", "nanoid": "^4.0.2", + "socket.io-client": "2.3.0", "vue": "^3.3.4", - "vue-router": "4" + "vue-router": "4", + "vue3-emoji-picker": "^1.1.7", + "vue3-popper": "^1.5.0" }, "devDependencies": { "@types/node": "^20.3.1", @@ -32,7 +37,9 @@ "prettier": "^2.8.8", "prettier-plugin-tailwindcss": "^0.3.0", "tailwindcss": "^3.3.2", + "terser": "^5.19.4", "typescript": "^5.1.3", + "vconsole": "^3.15.1", "vite": "^4.3.9", "vite-plugin-eslint": "^1.8.1", "vite-plugin-svg-icons": "^2.0.1", diff --git a/client/packages/rtc-web/src/App.vue b/client/packages/rtc-web/src/App.vue index 8853cc7..5c9710c 100644 --- a/client/packages/rtc-web/src/App.vue +++ b/client/packages/rtc-web/src/App.vue @@ -1,8 +1,20 @@ diff --git a/client/packages/rtc-web/src/assets/styles/index.css b/client/packages/rtc-web/src/assets/styles/index.css index 77d1d3c..6d49701 100644 --- a/client/packages/rtc-web/src/assets/styles/index.css +++ b/client/packages/rtc-web/src/assets/styles/index.css @@ -1 +1,21 @@ @import './tailwindcss'; + +/* width */ +::-webkit-scrollbar { + width: 4px; +} + +/* Track */ +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: #888; +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: #555; +} diff --git a/client/packages/rtc-web/src/assets/svg-icon/audio.svg b/client/packages/rtc-web/src/assets/svg-icon/audio.svg new file mode 100644 index 0000000..9c36589 --- /dev/null +++ b/client/packages/rtc-web/src/assets/svg-icon/audio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/rtc-web/src/assets/svg-icon/back.svg b/client/packages/rtc-web/src/assets/svg-icon/back.svg new file mode 100644 index 0000000..c823dcf --- /dev/null +++ b/client/packages/rtc-web/src/assets/svg-icon/back.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/rtc-web/src/assets/svg-icon/camera.svg b/client/packages/rtc-web/src/assets/svg-icon/camera.svg new file mode 100644 index 0000000..2fdee6c --- /dev/null +++ b/client/packages/rtc-web/src/assets/svg-icon/camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/rtc-web/src/assets/svg-icon/chat.svg b/client/packages/rtc-web/src/assets/svg-icon/chat.svg new file mode 100644 index 0000000..0d08497 --- /dev/null +++ b/client/packages/rtc-web/src/assets/svg-icon/chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/rtc-web/src/assets/svg-icon/count.svg b/client/packages/rtc-web/src/assets/svg-icon/count.svg new file mode 100644 index 0000000..5427670 --- /dev/null +++ b/client/packages/rtc-web/src/assets/svg-icon/count.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/rtc-web/src/assets/svg-icon/emoji.svg b/client/packages/rtc-web/src/assets/svg-icon/emoji.svg new file mode 100644 index 0000000..7126d84 --- /dev/null +++ b/client/packages/rtc-web/src/assets/svg-icon/emoji.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/rtc-web/src/assets/svg-icon/hang-up.svg b/client/packages/rtc-web/src/assets/svg-icon/hang-up.svg new file mode 100644 index 0000000..1dc9f80 --- /dev/null +++ b/client/packages/rtc-web/src/assets/svg-icon/hang-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/rtc-web/src/assets/svg-icon/member.svg b/client/packages/rtc-web/src/assets/svg-icon/member.svg new file mode 100644 index 0000000..5d824d1 --- /dev/null +++ b/client/packages/rtc-web/src/assets/svg-icon/member.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/rtc-web/src/assets/svg-icon/mirror-image.svg b/client/packages/rtc-web/src/assets/svg-icon/mirror-image.svg new file mode 100644 index 0000000..bf4be28 --- /dev/null +++ b/client/packages/rtc-web/src/assets/svg-icon/mirror-image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/rtc-web/src/assets/svg-icon/up.svg b/client/packages/rtc-web/src/assets/svg-icon/up.svg new file mode 100644 index 0000000..6445439 --- /dev/null +++ b/client/packages/rtc-web/src/assets/svg-icon/up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/rtc-web/src/assets/svg-icon/user-smail.svg b/client/packages/rtc-web/src/assets/svg-icon/user-smail.svg new file mode 100644 index 0000000..a425c7f --- /dev/null +++ b/client/packages/rtc-web/src/assets/svg-icon/user-smail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/rtc-web/src/components/back/back-previous-level.vue b/client/packages/rtc-web/src/components/back/back-previous-level.vue new file mode 100644 index 0000000..27eab48 --- /dev/null +++ b/client/packages/rtc-web/src/components/back/back-previous-level.vue @@ -0,0 +1,19 @@ + + + diff --git a/client/packages/rtc-web/src/components/back/back-title.vue b/client/packages/rtc-web/src/components/back/back-title.vue new file mode 100644 index 0000000..5fbc3a6 --- /dev/null +++ b/client/packages/rtc-web/src/components/back/back-title.vue @@ -0,0 +1,26 @@ + + + diff --git a/client/packages/rtc-web/src/components/back/index.ts b/client/packages/rtc-web/src/components/back/index.ts new file mode 100644 index 0000000..7525fb5 --- /dev/null +++ b/client/packages/rtc-web/src/components/back/index.ts @@ -0,0 +1,2 @@ +export { default as BackPreviousLevel } from './back-previous-level.vue'; +export { default as BackTitle } from './back-title.vue'; diff --git a/client/packages/rtc-web/src/components/base/dropdown.vue b/client/packages/rtc-web/src/components/base/dropdown.vue new file mode 100644 index 0000000..4c5f3e5 --- /dev/null +++ b/client/packages/rtc-web/src/components/base/dropdown.vue @@ -0,0 +1,34 @@ + + + diff --git a/client/packages/rtc-web/src/components/base/index.ts b/client/packages/rtc-web/src/components/base/index.ts index 5637d61..29d3aac 100644 --- a/client/packages/rtc-web/src/components/base/index.ts +++ b/client/packages/rtc-web/src/components/base/index.ts @@ -1,3 +1,5 @@ export { default as SvgIcon } from './svg-icon.vue'; export { default as NavIcons } from './nav-icons.vue'; export { default as Modal } from './modal.vue'; +export { default as Dropdown } from './dropdown.vue'; +export { default as ToastBox } from './toast.vue'; diff --git a/client/packages/rtc-web/src/components/base/nav-icons.vue b/client/packages/rtc-web/src/components/base/nav-icons.vue index 4ed2cbc..df87805 100644 --- a/client/packages/rtc-web/src/components/base/nav-icons.vue +++ b/client/packages/rtc-web/src/components/base/nav-icons.vue @@ -1,11 +1,15 @@ + + diff --git a/client/packages/rtc-web/src/components/chat-room/chat-content.vue b/client/packages/rtc-web/src/components/chat-room/chat-content.vue new file mode 100644 index 0000000..8aca609 --- /dev/null +++ b/client/packages/rtc-web/src/components/chat-room/chat-content.vue @@ -0,0 +1,31 @@ + + + diff --git a/client/packages/rtc-web/src/components/chat-room/chat-input.vue b/client/packages/rtc-web/src/components/chat-room/chat-input.vue new file mode 100644 index 0000000..12ff1bb --- /dev/null +++ b/client/packages/rtc-web/src/components/chat-room/chat-input.vue @@ -0,0 +1,54 @@ + + + diff --git a/client/packages/rtc-web/src/components/chat-room/chat-message.vue b/client/packages/rtc-web/src/components/chat-room/chat-message.vue new file mode 100644 index 0000000..8f73385 --- /dev/null +++ b/client/packages/rtc-web/src/components/chat-room/chat-message.vue @@ -0,0 +1,39 @@ + + + diff --git a/client/packages/rtc-web/src/components/chat-room/chat-room-user.vue b/client/packages/rtc-web/src/components/chat-room/chat-room-user.vue new file mode 100644 index 0000000..68bd5fd --- /dev/null +++ b/client/packages/rtc-web/src/components/chat-room/chat-room-user.vue @@ -0,0 +1,86 @@ + + + diff --git a/client/packages/rtc-web/src/components/chat-room/index.ts b/client/packages/rtc-web/src/components/chat-room/index.ts new file mode 100644 index 0000000..1e0007a --- /dev/null +++ b/client/packages/rtc-web/src/components/chat-room/index.ts @@ -0,0 +1,5 @@ +export { default as ChatInput } from './chat-input.vue'; +export { default as ChatMessage } from './chat-message.vue'; +export { default as ChatRoomUser } from './chat-room-user.vue'; +export { default as ChatContent } from './chat-content.vue'; +export { default as UserCard } from './user-card.vue'; diff --git a/client/packages/rtc-web/src/components/chat-room/props.ts b/client/packages/rtc-web/src/components/chat-room/props.ts new file mode 100644 index 0000000..8088b7d --- /dev/null +++ b/client/packages/rtc-web/src/components/chat-room/props.ts @@ -0,0 +1,7 @@ +export type UserCardProps = { + iconname?: string; + iconcolor?: string; + username?: string; + time?: string; + reverse?: boolean; +}; diff --git a/client/packages/rtc-web/src/components/chat-room/user-card.vue b/client/packages/rtc-web/src/components/chat-room/user-card.vue new file mode 100644 index 0000000..8a30ea9 --- /dev/null +++ b/client/packages/rtc-web/src/components/chat-room/user-card.vue @@ -0,0 +1,37 @@ + + + diff --git a/client/packages/rtc-web/src/components/emoji/index.vue b/client/packages/rtc-web/src/components/emoji/index.vue new file mode 100644 index 0000000..3f2c001 --- /dev/null +++ b/client/packages/rtc-web/src/components/emoji/index.vue @@ -0,0 +1,39 @@ + + + diff --git a/client/packages/rtc-web/src/components/error-boundary/index.vue b/client/packages/rtc-web/src/components/error-boundary/index.vue new file mode 100644 index 0000000..bd7e647 --- /dev/null +++ b/client/packages/rtc-web/src/components/error-boundary/index.vue @@ -0,0 +1,31 @@ + + + diff --git a/client/packages/rtc-web/src/components/form-room/form-room.vue b/client/packages/rtc-web/src/components/form-room/form-room.vue index 0216a9a..d1fd70c 100644 --- a/client/packages/rtc-web/src/components/form-room/form-room.vue +++ b/client/packages/rtc-web/src/components/form-room/form-room.vue @@ -2,7 +2,6 @@ import { useRoom } from '@/hooks'; import { useForm } from '../form-base'; import { computed } from 'vue'; -import { watch } from 'vue'; defineOptions({ name: 'FormRoom', @@ -16,13 +15,6 @@ const { validateRoomId } = useRoom(() => formData.value.roomId || ''); const roomIdValid = computed(() => !formErrors.value['roomId']); -watch( - () => roomIdValid.value, - (v) => { - console.log(v); - } -); - defineExpose({ onSubmit: handleSubmit, resetForm: resetFields, diff --git a/client/packages/rtc-web/src/components/lib.tsx b/client/packages/rtc-web/src/components/lib.tsx index 37351cb..a57ae15 100644 --- a/client/packages/rtc-web/src/components/lib.tsx +++ b/client/packages/rtc-web/src/components/lib.tsx @@ -1,8 +1,12 @@ import { SetupContext } from 'vue'; import { SvgIcon, NavIcons } from './base'; import { MenuSide } from './menu'; +import { resetUrl } from '@/utils'; -export const NavHeader = () => { +export const NavHeader = ( + { showSiderbar }: Partial<{ showSiderbar: boolean }>, + ctx: SetupContext +) => { return (
@@ -10,22 +14,39 @@ export const NavHeader = () => { {/* Navbar */}
-
- - -
+ + {showSiderbar ? ( +
+ + +
+ ) : undefined} ); }; @@ -47,7 +68,7 @@ export const FullHeightFlexBox = ( }; return ( -
+
{ctx.slots.default?.()}
); diff --git a/client/packages/rtc-web/src/components/list/select-list.vue b/client/packages/rtc-web/src/components/list/select-list.vue new file mode 100644 index 0000000..ac1605b --- /dev/null +++ b/client/packages/rtc-web/src/components/list/select-list.vue @@ -0,0 +1,40 @@ + + + diff --git a/client/packages/rtc-web/src/components/menu-action.vue b/client/packages/rtc-web/src/components/menu-action.vue new file mode 100644 index 0000000..66805d5 --- /dev/null +++ b/client/packages/rtc-web/src/components/menu-action.vue @@ -0,0 +1,75 @@ + + + diff --git a/client/packages/rtc-web/src/components/menu/menu-list.vue b/client/packages/rtc-web/src/components/menu/menu-list.vue index 0a7558f..f5cd5fa 100644 --- a/client/packages/rtc-web/src/components/menu/menu-list.vue +++ b/client/packages/rtc-web/src/components/menu/menu-list.vue @@ -20,6 +20,11 @@ const menuListData = ref([ icon: 'add-icon', label: '聊天', }, + { + key: 'video', + icon: 'add-icon', + label: '视频聊天', + }, ]); const currentCreateRoomByKey = ref(''); @@ -56,7 +61,10 @@ const handleClose = () => { class="flex items-center justify-between" >
{{ item.label }}
-
diff --git a/client/packages/rtc-web/src/components/menu/menu-side.vue b/client/packages/rtc-web/src/components/menu/menu-side.vue index 2da499e..68f429f 100644 --- a/client/packages/rtc-web/src/components/menu/menu-side.vue +++ b/client/packages/rtc-web/src/components/menu/menu-side.vue @@ -7,7 +7,7 @@ defineOptions({ diff --git a/client/packages/rtc-web/src/config/config-enum.ts b/client/packages/rtc-web/src/config/config-enum.ts new file mode 100644 index 0000000..db0fcc1 --- /dev/null +++ b/client/packages/rtc-web/src/config/config-enum.ts @@ -0,0 +1,3 @@ +export const enum ConfigEnum { + useRelay = 'tl-rtc-file-use-relay', +} diff --git a/client/packages/rtc-web/src/config/constant.ts b/client/packages/rtc-web/src/config/constant.ts new file mode 100644 index 0000000..6fef398 --- /dev/null +++ b/client/packages/rtc-web/src/config/constant.ts @@ -0,0 +1,53 @@ +export type MenuActionType = { + name: string; + tip: string; + color?: string; + tipDir?: string; + btn?: boolean; + disabled?: boolean; +}; + +export const ChatAction: MenuActionType[] = [ + { name: 'member', tip: '显示成员', color: undefined }, +]; + +export const ChatInputAction: MenuActionType[] = [ + { name: 'emoji', tip: '表情' }, +]; + +export const VideoMenuAction: MenuActionType[] = [ + { + name: 'member', + tip: '显示成员', + color: undefined, + tipDir: 'tooltip-top', + btn: true, + }, +]; + +export const VideoControlMenuAction: MenuActionType[] = [ + { + name: 'camera', + tip: '开启/关闭摄像头', + color: '#707070', + tipDir: 'tooltip-top', + btn: true, + disabled: false, + }, + // { + // name: 'mirror-image', + // tip: '开启镜像', + // color: '#707070', + // tipDir: 'tooltip-top', + // btn: true, + // disabled: false, + // }, + { + name: 'hang-up', + tip: '结束通话', + color: undefined, + tipDir: 'tooltip-top', + btn: true, + disabled: false, + }, +]; diff --git a/client/packages/rtc-web/src/config/index.ts b/client/packages/rtc-web/src/config/index.ts new file mode 100644 index 0000000..d62b67e --- /dev/null +++ b/client/packages/rtc-web/src/config/index.ts @@ -0,0 +1,3 @@ +export * from './config-enum'; +export * from './constant'; +export * from './socket-event-name'; diff --git a/client/packages/rtc-web/src/config/socket-event-name.ts b/client/packages/rtc-web/src/config/socket-event-name.ts new file mode 100644 index 0000000..16fa491 --- /dev/null +++ b/client/packages/rtc-web/src/config/socket-event-name.ts @@ -0,0 +1,15 @@ +export const enum SocketEventName { + Count = 'count', + CreateAndJoin = 'createAndJoin', + RoomCreated = 'created', + RoomExit = 'exit', + RoomJoin = 'joined', + RoomOffer = 'offer', + RoomAnswer = 'answer', + RoomCandidate = 'candidate', +} + +// chat 的事件名 +export const enum ChatEventName { + ChatingRoom = 'chatingRoom', +} diff --git a/client/packages/rtc-web/src/context/index.ts b/client/packages/rtc-web/src/context/index.ts new file mode 100644 index 0000000..50ff166 --- /dev/null +++ b/client/packages/rtc-web/src/context/index.ts @@ -0,0 +1,15 @@ +import { InjectionKey, Ref } from 'vue'; + +export type InitDataKeyType = Partial<{ + langMode: string; + socket: any; + logo: string; + version: string; + options: RTCOfferOptions; + config: { + iceServers: RTCIceServer[]; + }; +}>; + +export const InitDataKey: InjectionKey> = + Symbol('initDataKey'); diff --git a/client/packages/rtc-web/src/env.d.ts b/client/packages/rtc-web/src/env.d.ts new file mode 100644 index 0000000..3e25b75 --- /dev/null +++ b/client/packages/rtc-web/src/env.d.ts @@ -0,0 +1,2 @@ +declare module 'socket.io-client'; +declare module 'vue3-emoji-picker'; diff --git a/client/packages/rtc-web/src/hooks/index.ts b/client/packages/rtc-web/src/hooks/index.ts index 4eef31e..445e9c5 100644 --- a/client/packages/rtc-web/src/hooks/index.ts +++ b/client/packages/rtc-web/src/hooks/index.ts @@ -1,2 +1,9 @@ export * from './useTheme'; export * from './useRoom'; +export * from './useInitData'; +export * from './socket-utils'; +export * from './useRouterReactive'; +export * from './useDragChangeSize'; +export * from './useSwitchByUrl'; +export * from './useErrorCaptured'; +export * from './useUseragent'; diff --git a/client/packages/rtc-web/src/hooks/socket-utils/index.ts b/client/packages/rtc-web/src/hooks/socket-utils/index.ts new file mode 100644 index 0000000..3f0c43f --- /dev/null +++ b/client/packages/rtc-web/src/hooks/socket-utils/index.ts @@ -0,0 +1,2 @@ +export * from './useSocket'; +export * from './useSocketUtils'; diff --git a/client/packages/rtc-web/src/hooks/socket-utils/useSocket.ts b/client/packages/rtc-web/src/hooks/socket-utils/useSocket.ts new file mode 100644 index 0000000..6efce7d --- /dev/null +++ b/client/packages/rtc-web/src/hooks/socket-utils/useSocket.ts @@ -0,0 +1,21 @@ +import { InitDataKey } from '@/context'; +import { CommonFnType } from '@/types'; +import { inject, watch } from 'vue'; + +export const useSocket = (fn?: CommonFnType) => { + const initData = inject(InitDataKey); + + if (initData) { + watch( + () => initData.value.socket, + async (v) => { + if (v) { + await fn?.(v); + } + }, + { + immediate: true, + } + ); + } +}; diff --git a/client/packages/rtc-web/src/hooks/socket-utils/useSocketUtils.ts b/client/packages/rtc-web/src/hooks/socket-utils/useSocketUtils.ts new file mode 100644 index 0000000..7369ad8 --- /dev/null +++ b/client/packages/rtc-web/src/hooks/socket-utils/useSocketUtils.ts @@ -0,0 +1,18 @@ +import { SocketEventName } from '@/config'; +import { useSocket } from './useSocket'; +import { ref } from 'vue'; + +export const useSocketCount = () => { + const count = ref(0); + const handleSocketCount = (socket: any) => { + socket.on(SocketEventName.Count, (data: any) => { + count.value = data.mc; + }); + }; + + useSocket(handleSocketCount); + + return { + data: count, + }; +}; diff --git a/client/packages/rtc-web/src/hooks/useDragChangeSize.ts b/client/packages/rtc-web/src/hooks/useDragChangeSize.ts new file mode 100644 index 0000000..e95457e --- /dev/null +++ b/client/packages/rtc-web/src/hooks/useDragChangeSize.ts @@ -0,0 +1,128 @@ +/** + * @description 拖拽元素改变大小 + * 对于一些自由拖拽的元素会有一些问题,比如拉左边进行宽度增加,会造成 width 往右边增加;这里暂时未处理,按理说要同时改变 left + */ + +import { isItBetween } from '@/utils'; +import { + MaybeElementRef, + useMouseInElement, + useMousePressed, +} from '@vueuse/core'; +import { computed, ref, watch } from 'vue'; + +type ResizeOption = { + initialSize?: { + width?: string; + height?: string; + }; + position?: 'left' | 'right' | 'top' | 'bottom'; + persistentSize?: { + width?: string; + height?: string; + }; // 用来控制在拖拽时不会变的数据 +}; + +export const useDragChangeSize = ( + target: MaybeElementRef, + options?: ResizeOption +) => { + const offset = 5; + const { elementX, elementY, elementWidth, elementHeight } = + useMouseInElement(target); + + const style = computed(() => { + return Object.assign( + {}, + { + width: `${ + elementWidth.value + ? elementWidth.value + 'px' + : options?.initialSize?.width ?? '100%' + }`, + height: `${ + elementHeight.value + ? elementHeight.value + 'px' + : options?.initialSize?.height ?? '100%' + }`, + }, + options?.persistentSize || ({} as any) + ); + }); + + const { pressed } = useMousePressed({ target, touch: false }); + + const draggablePos = computed(() => ({ + left: { + x: [-offset, offset], + y: [0, elementHeight.value], + }, + top: { + x: [0, elementWidth.value], + y: [-offset, offset], + }, + })); + + const judgeEnterPos = () => { + const findV = Object.keys(draggablePos.value).find((key) => { + const posKey = key as keyof typeof draggablePos.value; + + return ( + isItBetween(elementX.value, draggablePos.value[posKey].x) && + isItBetween(elementY.value, draggablePos.value[posKey].y) + ); + }); + const draggable = options?.position ? options.position === findV : !!findV; + return { + draggable, + position: findV || '', + }; + }; + + // 判断点击时候是否在拖拽范围内 + const draggableByClick = ref({ + draggable: false, + position: '', + }); + + // 供外部用的判断 是否可以拖拽 + const canDragged = computed(judgeEnterPos); + + const doResize = ([posX, posY]: number[], type: string) => { + const movX = posX - offset; + const movY = posY - offset; + if (type === 'left') { + const differenceX = -movX; + elementWidth.value = elementWidth.value + differenceX; + } + + if (type === 'top') { + const differenceY = -movY; + elementHeight.value = elementHeight.value + differenceY; + } + }; + + watch([elementX, elementY], (v) => { + if (pressed.value) { + if (draggableByClick.value.draggable) { + doResize(v, draggableByClick.value.position); + } + } + }); + + watch(pressed, (v) => { + if (v) { + draggableByClick.value = judgeEnterPos(); + } else { + draggableByClick.value = { + draggable: false, + position: '', + }; + } + }); + + return { + canDragged, + style, + }; +}; diff --git a/client/packages/rtc-web/src/hooks/useErrorCaptured.ts b/client/packages/rtc-web/src/hooks/useErrorCaptured.ts new file mode 100644 index 0000000..cd5d05c --- /dev/null +++ b/client/packages/rtc-web/src/hooks/useErrorCaptured.ts @@ -0,0 +1,12 @@ +import { onErrorCaptured, ref } from 'vue'; + +// 捕获 组件error +export const useErrorCaptured = () => { + const errors = ref([]); + + onErrorCaptured((e) => { + errors.value.push(e.message); + }); + + return errors; +}; diff --git a/client/packages/rtc-web/src/hooks/useInitData.ts b/client/packages/rtc-web/src/hooks/useInitData.ts new file mode 100644 index 0000000..e71f673 --- /dev/null +++ b/client/packages/rtc-web/src/hooks/useInitData.ts @@ -0,0 +1,47 @@ +import { ConfigEnum } from '@/config'; +import { useFetch, useLocalStorage } from '@vueuse/core'; +import { ref, shallowReactive } from 'vue'; +import io from 'socket.io-client'; +import { InitDataKeyType } from '@/context'; +import { isDev } from '@/utils'; + +export const useFetchData = async () => { + const useTurn = useLocalStorage(ConfigEnum.useRelay, isDev() ? false : true); + + const { data, error } = await useFetch( + () => `/api/comm/initData?turn=${useTurn.value}` + ) + .get() + .json(); + + return { data, error }; +}; + +export const useInitData = async () => { + const { data, error } = await useFetchData(); + const initData = ref({ + langMode: 'zh', // 默认中文 + }); + + if (data.value) { + const { wsHost, logo, version, rtcConfig, options } = data.value; + initData.value = Object.assign({}, initData.value, { + socket: wsHost ? shallowReactive(io(wsHost)) : null, + logo, + version, + options: Object.keys(options).reduce( + (cur, next) => ({ ...cur, [next]: Boolean(options[next]) }), + {} + ), + config: rtcConfig, + }); + } + + if (error.value) { + throw error.value; + } + + return { + initData, + }; +}; diff --git a/client/packages/rtc-web/src/hooks/useRoom.ts b/client/packages/rtc-web/src/hooks/useRoom.ts index 80c8dc7..1934029 100644 --- a/client/packages/rtc-web/src/hooks/useRoom.ts +++ b/client/packages/rtc-web/src/hooks/useRoom.ts @@ -1,6 +1,53 @@ import { CommonFnType } from '@/types'; import { resolveRef } from '@/utils/reactive'; -import { MaybeRef, computed } from 'vue'; +import { + MaybeRef, + computed, + inject, + onBeforeUnmount, + onMounted, + ref, + shallowRef, + watch, +} from 'vue'; +import { useSocket } from './socket-utils'; +import { SocketEventName } from '@/config'; +import { useRouteParamsReactive, useUserAgent } from '.'; +import { InitDataKey } from '@/context'; +import { genNickName, isDev, resetUrl } from '@/utils'; +import { uniqBy } from 'lodash'; +import { watchArray } from '@vueuse/core'; + +export type Member = { + id: string; + nickName: string; + langMode: string; + owner: boolean; + ua: string; + joinTime: string; + userAgent: string; + ip: string; + network: string; + room?: string; + roomInfo?: OwnerInfo; +}; + +export type OwnerInfo = { + socketId: string; + roomId: string; + recoderId: string; + owner: boolean; +}; + +export type ConnectOption = { + roomJoined?: (...args: any[]) => Promise; + roomCreated?: (data: any) => Promise; + onAddRtcPeer?: (id: string, pc: any) => Promise; + onConnectComplete?: (...args: any[]) => void; + onBeforeCreateOffer?: (...args: any[]) => Promise; + onBeforeCreateAnswer?: (...args: any[]) => Promise; + onTrack?: (...args: any[]) => void; +}; export const useRoom = (value: MaybeRef | CommonFnType) => { const realValue = resolveRef(value); @@ -17,3 +64,317 @@ export const useRoom = (value: MaybeRef | CommonFnType) => { isValid, }; }; + +export const useCreateRoom = ( + type: 'password' | 'video' = 'password', + validate = true +) => { + const initData = inject(InitDataKey); + + const { getNetWorkState, isMobile } = useUserAgent(); + + const { roomId } = useRouteParamsReactive(['roomId']); + + const { isValid } = useRoom(() => roomId.value || ''); + + const emitCreateRoom = (socket: any) => { + socket.emit(SocketEventName.CreateAndJoin, { + room: roomId.value, + type: type, + password: '', + nickName: genNickName(), + langMode: initData?.value.langMode, + ua: isMobile ? 'mobile' : 'pc', + network: getNetWorkState(), + }); + }; + + const checkParams = () => { + console.log('check'); + if (validate) { + if (!isValid.value) { + resetUrl(); + } else { + useSocket(emitCreateRoom); + } + } + }; + + watch( + () => isValid.value, + () => { + checkParams(); + } + ); + + onMounted(checkParams); + + return { + roomId, + isValid, + }; +}; + +export const useRoomConnect = (option: ConnectOption = {}) => { + const roomInfo = useGetRoomInfo(); + + const { members, selfInfo } = roomInfo; + const rtcConnects = new Map(); + const dataChanelMap = new Map(); + const initData = inject(InitDataKey)!; + const socketRef = shallowRef(); + const completed = ref(false); + + watchArray( + () => members.value, + async (_, __, added, removed) => { + console.log('watch', added, removed); + + // 处理 exit + if (removed.length) { + removed.forEach(async (peer) => { + if (peer.id) { + const rtcConnect = await getRtcConnect(peer.id); + rtcConnect.close(); + rtcConnects.delete(peer.id); + } + }); + } + } + ); + + const createRtcConnect = async (id: string) => { + const pc = new RTCPeerConnection( + isDev() ? undefined : initData.value.config + ); + pc.onicecandidate = (event) => { + if (event.candidate != null) { + const message = { + from: selfInfo.value.socketId, + to: id, + room: selfInfo.value.roomId, + sdpMid: event.candidate.sdpMid, + sdpMLineIndex: event.candidate.sdpMLineIndex, + sdp: event.candidate.candidate, + }; + socketRef.value.emit('candidate', message); + } + }; + + const dataChanel = pc.createDataChannel('datachanel'); + dataChanelMap.set(id, dataChanel); + + dataChanel.onopen = () => { + // dataChanel.send('aaa'); + }; + + pc.ondatachannel = (e) => { + const chanel = e.channel; + if (chanel.label === 'datachanel') { + chanel.onmessage = () => { + // console.log('接受', e.data); + }; + } + }; + + pc.onconnectionstatechange = () => { + if (pc.connectionState === 'connected') { + // console.log('完成'); + completed.value = true; + } + }; + + pc.ontrack = (e) => { + option?.onTrack?.(e, id); + }; + + rtcConnects.set(id, pc); + await option.onAddRtcPeer?.(id, pc); + + return pc; + }; + + const getRtcConnect = async (id: string) => { + return rtcConnects.get(id) || (await createRtcConnect(id)); + }; + + const roomCreated = async (data: any) => { + await option?.roomCreated?.(data); + }; + + const roomJoined = async (data: any) => { + const peer = await getRtcConnect(data.id); + + await option?.roomJoined?.(data.id, peer); + createOffer(peer, data); + }; + + /** + * @description 这里的 createOffer + */ + const createOffer = async (pc: RTCPeerConnection, peer: any) => { + await option?.onBeforeCreateOffer?.(peer.id, pc); + const offer = await pc.createOffer(initData.value.options); + await pc.setLocalDescription(offer); + console.log('create offer - send'); + socketRef.value.emit(SocketEventName.RoomOffer, { + from: selfInfo.value.socketId, + to: peer.id, + room: selfInfo.value.roomId, + sdp: offer.sdp, + }); + }; + + /** + * @description offer 监听事件 + */ + const roomOffer = async (data: any) => { + const pc = await getRtcConnect(data.from); + await pc.setRemoteDescription( + new RTCSessionDescription({ type: 'offer', sdp: data.sdp }) + ); + await option?.onBeforeCreateAnswer?.(data.from, pc); + const answer = await pc.createAnswer(initData.value.options); + await pc.setLocalDescription(answer); + console.log('receive offer - send answer'); + socketRef.value.emit(SocketEventName.RoomAnswer, { + from: selfInfo.value.socketId, + to: data.from, + room: selfInfo.value.roomId, + sdp: answer.sdp, + }); + }; + + /** + * @description answer 监听事件 + */ + const roomAnswer = async (data: any) => { + const pc = await getRtcConnect(data.from); + console.log('receive answer'); + await pc.setRemoteDescription( + new RTCSessionDescription({ type: 'answer', sdp: data.sdp }) + ); + }; + const roomCandidate = async (data: any) => { + const pc = await getRtcConnect(data.from); + const rtcIceCandidate = new RTCIceCandidate({ + candidate: data.sdp, + sdpMid: data.sdpMid, + sdpMLineIndex: data.sdpMLineIndex, + }); + await pc.addIceCandidate(rtcIceCandidate); + }; + + const handleRoomConnect = (socket: any) => { + socketRef.value = socket; + startConnect(); + }; + + const startConnect = () => { + socketRef.value.on(SocketEventName.RoomCreated, roomCreated); + socketRef.value.on(SocketEventName.RoomJoin, roomJoined); + socketRef.value.on(SocketEventName.RoomOffer, roomOffer); + socketRef.value.on(SocketEventName.RoomAnswer, roomAnswer); + socketRef.value.on(SocketEventName.RoomCandidate, roomCandidate); + }; + useSocket(handleRoomConnect); + + return { ...roomInfo, rtcConnects, dataChanelMap, completed, startConnect }; +}; + +// 获取房间的一些信息,例如 peer 等信息 +export const useGetRoomInfo = () => { + const members = ref[]>([]); + const selfInfo = ref({ + socketId: '', + owner: false, + recoderId: '', + roomId: '', + }); + + const roomCreated = (data: any) => { + members.value = uniqBy( + [ + ...data.peers.map((peer: any) => ({ + id: peer.id, + nickName: peer.nickName, + owner: peer.owner, + })), + { + id: data.id, + owner: data.owner, + nickName: data.nickName, + }, + ], + 'id' + ); + + selfInfo.value = { + socketId: data.id, + owner: data.owner, + recoderId: data.recoderId, + roomId: data.room, + }; + console.log('created', members, data); + }; + + const roomExit = async (data: any) => { + console.log('exit', data); + members.value = members.value.filter((item) => item.id !== data.from); + }; + + const roomJoin = async (data: any) => { + console.log('join', data); + members.value = uniqBy( + [ + ...members.value, + { + id: data.id, + nickName: data.nickName, + owner: data.owner, + }, + ], + 'id' + ); + }; + + const roomOwner = computed( + () => members.value.find((item) => item.owner) || undefined + ); + + const self = computed(() => { + const info = + members.value.find((item) => item.id === selfInfo.value.socketId) || + undefined; + + if (info) { + return Object.assign({}, { ...info, roomInfo: selfInfo.value }); + } + return undefined; + }); + + const handleRoomInfo = async (socket: any) => { + socket.on(SocketEventName.RoomCreated, roomCreated); + socket.on(SocketEventName.RoomExit, roomExit); + socket.on(SocketEventName.RoomJoin, roomJoin); + + onBeforeUnmount(() => { + console.log('执行'); + socket.emit(SocketEventName.RoomExit, { + from: selfInfo.value.socketId, + room: selfInfo.value.roomId, + recoderId: selfInfo.value.recoderId, + }); + socket.removeAllListeners(); + }); + }; + + useSocket(handleRoomInfo); + + return { + roomOwner, + members, + self, + selfInfo, + }; +}; diff --git a/client/packages/rtc-web/src/hooks/useRouterReactive.ts b/client/packages/rtc-web/src/hooks/useRouterReactive.ts new file mode 100644 index 0000000..5ee865b --- /dev/null +++ b/client/packages/rtc-web/src/hooks/useRouterReactive.ts @@ -0,0 +1,57 @@ +import { useRouteParams, useRouteQuery } from '@vueuse/router'; +import { Ref, toRefs, watch } from 'vue'; +import { useRoute } from 'vue-router'; + +export const useRouteParamsReactive = (keys: T[]) => { + const params = keys.reduce( + (cur, next) => ({ ...cur, [next]: useRouteParams(next) }), + {} as Record> + ); + + const route = useRoute(); + + watch( + () => route.params, + (v) => { + Object.keys(v).forEach((key) => { + if (params[key as T]) { + params[key as T].value = v[key]; + } + }); + }, + { + immediate: true, + } + ); + + return params; +}; + +export const useRouteQueryReactive = ( + ...args: Parameters +) => { + const { query } = toRefs(useRoute()); + + const [key, defaultValue, options] = args; + const valueRef = useRouteQuery(key, defaultValue, options); + + watch( + () => query.value[key], + (v: any) => { + const nv = options?.transform?.(v); + if (options?.transform === Number) { + if (!isNaN(nv as number)) { + valueRef.value = nv; + } + } else { + valueRef.value = nv; + } + }, + { + immediate: true, + deep: true, + } + ); + + return valueRef; +}; diff --git a/client/packages/rtc-web/src/hooks/useSwitchByUrl.ts b/client/packages/rtc-web/src/hooks/useSwitchByUrl.ts new file mode 100644 index 0000000..4721686 --- /dev/null +++ b/client/packages/rtc-web/src/hooks/useSwitchByUrl.ts @@ -0,0 +1,55 @@ +import { useWindowSize } from '@vueuse/core'; +import { Ref, computed, watch } from 'vue'; +import { useRouteQueryReactive } from './useRouterReactive'; + +export const useSwitchMember = () => { + const { width } = useWindowSize(); + const showMembers = useRouteQueryReactive('showMembers', '0', { + transform: Number, + }); + + const switchMember = () => { + showMembers.value = showMembers.value === 0 ? 1 : 0; + }; + + const open = computed(() => showMembers.value === 1); + + // 是否是宽屏, max-width = 1024 + const isLgScreen = computed(() => width.value <= 1024); + + watch( + () => isLgScreen.value, + (w) => { + showMembers.value = w ? 0 : 1; + }, + { + immediate: true, + } + ); + + return { + open, + switchMember, + isLgScreen, + }; +}; + +export const useSwitchSiderbar = () => { + const showSiderbar = useRouteQueryReactive('showSiderbar', '1', { + transform: Number, + }) as Ref; + + const switchSiderbar = () => { + showSiderbar.value = showSiderbar.value === 0 ? 1 : 0; + }; + + const open = computed(() => { + return showSiderbar.value === 1; + }); + + return { + switchSiderbar, + open, + showSiderbar, + }; +}; diff --git a/client/packages/rtc-web/src/hooks/useUseragent.ts b/client/packages/rtc-web/src/hooks/useUseragent.ts new file mode 100644 index 0000000..f5fdf53 --- /dev/null +++ b/client/packages/rtc-web/src/hooks/useUseragent.ts @@ -0,0 +1,40 @@ +/* eslint-disable indent */ +export const useUserAgent = () => { + const uerAgent = window.navigator.userAgent; + + const isMobile = /Mobi|Android|iPhone/i.test(uerAgent); + + const getNetWorkState = () => { + let networkStr = uerAgent.match(/NetType\/\w+/) + ? uerAgent.match(/NetType\/\w+/)?.[0] + : 'NetType/other'; + networkStr = networkStr?.toLowerCase().replace('nettype/', ''); + if ( + networkStr && + !['wifi', '5g', '3g', '4g', '2g', '3gnet', 'slow-2g'].includes(networkStr) + ) { + if ((navigator as any).connection) { + networkStr = (navigator as any).connection.effectiveType; + } + } + switch (networkStr) { + case 'wifi': + return 'wifi'; + case '5g': + return '5g'; + case '4g': + return '4g'; + case '3g' || '3gnet': + return '3g'; + case '2g' || 'slow-2g': + return '2g'; + default: + return 'unknow'; + } + }; + + return { + isMobile, + getNetWorkState, + }; +}; diff --git a/client/packages/rtc-web/src/keys/server.crt b/client/packages/rtc-web/src/keys/server.crt new file mode 100644 index 0000000..a20e9ad --- /dev/null +++ b/client/packages/rtc-web/src/keys/server.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICVTCCAb4CCQCCDZ6FebPIqjANBgkqhkiG9w0BAQsFADBvMQswCQYDVQQGEwJV +UzENMAsGA1UECAwETWFyczETMBEGA1UEBwwKaVRyYW5zd2FycDETMBEGA1UECgwK +aVRyYW5zd2FycDETMBEGA1UECwwKaVRyYW5zd2FycDESMBAGA1UEAwwJMTI3LjAu +MC4xMB4XDTE4MDcxMDAzMTYzN1oXDTI4MDcwNzAzMTYzN1owbzELMAkGA1UEBhMC +VVMxDTALBgNVBAgMBE1hcnMxEzARBgNVBAcMCmlUcmFuc3dhcnAxEzARBgNVBAoM +CmlUcmFuc3dhcnAxEzARBgNVBAsMCmlUcmFuc3dhcnAxEjAQBgNVBAMMCTEyNy4w +LjAuMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1I6AQ6eNez85kcKjwy3g +/vcnXtw+EbP4Ab37fLhIIWG+XzmEBAqnCYjM3nmlDIGEfNylGReo9mD2OHg46a1D +wjd3pxTMit41pCTCiu8S9A2UJfbhSzQrfs+IZcNye4KR9/FzNEW6KoKQ0uc6X33E +0xe41hbRMQoKB3WmxvyN8PcCAwEAATANBgkqhkiG9w0BAQsFAAOBgQAd7GNDtKWA +0OpSCzMu0pbmss9Erh4/RC8D+wK4+TXgPDKyZ6hX4FYPvk+ryMvwxJf0o4jjx5cx +Yew7UjaKHlGXq+CNVRFYlltsbvO3oQTNkajuyYGWzMSuNxNsT3apOxH7SIu3qao8 +COSwj5FxZ2JU7O+SBVFZoJrFXEa+KJMQzQ== +-----END CERTIFICATE----- diff --git a/client/packages/rtc-web/src/keys/server.key b/client/packages/rtc-web/src/keys/server.key new file mode 100644 index 0000000..beb5a71 --- /dev/null +++ b/client/packages/rtc-web/src/keys/server.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDUjoBDp417PzmRwqPDLeD+9yde3D4Rs/gBvft8uEghYb5fOYQE +CqcJiMzeeaUMgYR83KUZF6j2YPY4eDjprUPCN3enFMyK3jWkJMKK7xL0DZQl9uFL +NCt+z4hlw3J7gpH38XM0RboqgpDS5zpffcTTF7jWFtExCgoHdabG/I3w9wIDAQAB +AoGAWsy1BjGhQrDzisy24D3NC53Q97jl2vIiU7wwnkqqpXf3tv3+4ysZx/zkZ3VX +iEwbqKso69srlnQ9OkpBJbGaa6lZe+z7BGzv2eJr+hKjjVjR122eDjAtXw+Tmt6c +iBUG9+ITC1GdhXLEgTXtYuPq8hbDhoAVI007E+5JuQoO8kECQQD3aR6zGfR7EOmn +byGkNMMPY/FoH894BpB4l7gIlNXH3pxBqukrEwnmVXSfak2PAkeLFO+bgCc6baIZ ++R4iOLn/AkEA2++dIndbY7nmGs7Q//sM7MkFMiFQ4h1nN38V9AEHaUonxlwo+Nks +PTAaVd78YIh6yBlfexm0Fxi1mEVQApqZCQJAFUPqyJglhGJqwuJxcMy8K1l6yWla +isV9q2/W+J3aViiTI63OBs7HHg4gTQd1DSK0BYdSJPp55LLBqRvZdDWN/wJBAMQa +M46exAL4p55xl9MWwyCB4LshD6B9vSGzlBx7qmMMNsjcNcAkzBhGwsScTYW5S1kN +nqABfB037/s0mjGoLRkCQF18j4MovyFaj8VAqY8YUmf86Ez9JmL9kHNA8yEoSjuI +xsRa6y5Nza5y83Mojt4W+PfS386riJ7txqrPdezyag4= +-----END RSA PRIVATE KEY----- diff --git a/client/packages/rtc-web/src/layout/index.vue b/client/packages/rtc-web/src/layout/index.vue index 6372a49..6047d07 100644 --- a/client/packages/rtc-web/src/layout/index.vue +++ b/client/packages/rtc-web/src/layout/index.vue @@ -1,20 +1,33 @@ diff --git a/client/packages/rtc-web/src/main.ts b/client/packages/rtc-web/src/main.ts index f8ccbe9..1ed4361 100644 --- a/client/packages/rtc-web/src/main.ts +++ b/client/packages/rtc-web/src/main.ts @@ -8,7 +8,16 @@ import SvgIcon from '@/components/base/svg-icon.vue'; import App from './App.vue'; +import VConsole from 'vconsole'; +import { useUserAgent } from '@/hooks'; +import { isDev } from '@/utils'; + +const { isMobile } = useUserAgent(); + function setup() { + if (isMobile && isDev()) { + new VConsole(); + } return createApp(App).use(router).component('svg-icon', SvgIcon); } diff --git a/client/packages/rtc-web/src/routes/router.ts b/client/packages/rtc-web/src/routes/router.ts index 988f641..c3768a7 100644 --- a/client/packages/rtc-web/src/routes/router.ts +++ b/client/packages/rtc-web/src/routes/router.ts @@ -1,6 +1,10 @@ export const routes = [ { path: '/', component: () => import('@/views/welcome.vue') }, { path: '/chat/:roomId', component: () => import('@/views/chat/chat.vue') }, + { + path: '/video/:roomId', + component: () => import('@/views/video/video.vue'), + }, // 将匹配所有内容并将其放在 `$route.params.pathMatch` 下 { path: '/:pathMatch(.*)*', diff --git a/client/packages/rtc-web/src/utils/common.ts b/client/packages/rtc-web/src/utils/common.ts index 9e7e973..11ef289 100644 --- a/client/packages/rtc-web/src/utils/common.ts +++ b/client/packages/rtc-web/src/utils/common.ts @@ -10,3 +10,87 @@ export function withBtnClickEvent(fn: CommonFnType) { export function preventDefault(e: Event) { e.preventDefault(); } + +export function safenExecuteConditioFn(condition: boolean, fn: CommonFnType) { + if (condition) { + fn(); + } +} + +// 将 socket on 转换为 promisify +export function transformSocketListenEvent(socket: any, ev: string) { + return new Promise((resolve) => { + const cb = (...args: any[]) => { + resolve(args); + }; + socket.on(ev, cb); + }); +} + +// 转义字符串 +export function escapeStr(str: string) { + const entityMap: any = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=', + }; + + const encodedMap: any = { + '%': '%25', + '!': '%21', + "'": '%27', + '(': '%28', + ')': '%29', + '*': '%2A', + '-': '%2D', + '.': '%2E', + _: '%5F', + '~': '%7E', + }; + + return String(str).replace(/[&<>"'`=/%!'()*\-._~]/g, function (s) { + return entityMap[s] || encodedMap[s] || ''; + }); +} + +export function unescapeStr(str: string) { + const entityMap: any = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + '/': '/', + '`': '`', + '=': '=', + }; + const encodedMap: any = { + '%25': '%', + '%21': '!', + '%27': "'", + '%28': '(', + '%29': ')', + '%2A': '*', + '%2D': '-', + '%2E': '.', + '%5F': '_', + '%7E': '~', + }; + return String(str).replace( + /&(amp|lt|gt|quot|#39|#x2F|#x60|#x3D);|%(25|21|27|28|29|2A|2D|2E|5F|7E)/g, + function (s) { + return entityMap[s] || encodedMap[s] || ''; + } + ); +} + +export const isItBetween = (num: number, arr: number[]) => { + return num >= arr[0] && num <= arr[1]; +}; + +export const resetUrl = () => window.location.replace('/'); diff --git a/client/packages/rtc-web/src/utils/env.ts b/client/packages/rtc-web/src/utils/env.ts new file mode 100644 index 0000000..0cc26f7 --- /dev/null +++ b/client/packages/rtc-web/src/utils/env.ts @@ -0,0 +1,7 @@ +export const isProd = () => { + return import.meta.env.PROD; +}; + +export const isDev = () => { + return import.meta.env.DEV; +}; diff --git a/client/packages/rtc-web/src/utils/index.ts b/client/packages/rtc-web/src/utils/index.ts index d0b9323..ac81bc6 100644 --- a/client/packages/rtc-web/src/utils/index.ts +++ b/client/packages/rtc-web/src/utils/index.ts @@ -1 +1,4 @@ export * from './common'; +export * from './user'; +export * from './reactive'; +export * from './env'; diff --git a/client/packages/rtc-web/src/utils/user.ts b/client/packages/rtc-web/src/utils/user.ts new file mode 100644 index 0000000..49d71ae --- /dev/null +++ b/client/packages/rtc-web/src/utils/user.ts @@ -0,0 +1,503 @@ +export const nameDataBase = () => { + const adjectives = [ + '幽默的', + '搞笑的', + '疯狂的', + '奇怪的', + '古怪的', + '无聊的', + '神秘的', + '魔幻的', + '风趣的', + '调皮的', + '聪明的', + '美丽的', + '可爱的', + '迷人的', + '酷的', + '萌萌的', + '潇洒的', + '霸气的', + '猛烈的', + '光芒的', + '伶俐的', + '俏皮的', + '小巧的', + '细腻的', + '娇嫩的', + '柔软的', + '亲切的', + '朴实的', + '拘谨的', + '高傲的', + '自恋的', + '浪漫的', + '单纯的', + '深情的', + '执着的', + '冷酷的', + '刁蛮的', + '天真的', + '多情的', + '成熟的', + '忧郁的', + '神经质的', + '孤独的', + '怀旧的', + '清新的', + '淡雅的', + '冷艳的', + '高冷的', + '玩世不恭的', + '逆天的', + '暴躁的', + '暴力的', + '妩媚的', + '狡猾的', + '自信的', + '自卑的', + '悲观的', + '乐观的', + '勇敢的', + '胆小的', + '快乐的', + '痛苦的', + '善良的', + '邪恶的', + '深邃的', + '神圣的', + '丰满的', + '单薄的', + '肥胖的', + '瘦弱的', + '英俊的', + '丑陋的', + '芳香的', + '臭气熏天的', + '热情的', + '冷漠的', + '朝气蓬勃的', + '干净的', + '脏兮兮的', + '无忧无虑的', + '喜怒无常的', + '平凡的', + '非凡的', + '害羞的', + '热心的', + '机智的', + '敏捷的', + '迟钝的', + '聪慧的', + '无知的', + '真诚的', + '虚伪的', + '直率的', + '谨慎的', + '大胆的', + '谦虚的', + '傲慢的', + '严肃的', + '轻松的', + '紧张的', + '勤劳的', + '懒惰的', + '守时的', + '迟到的', + '坚强的', + '软弱的', + '聪慧的', + '愚笨的', + '机灵的', + '迟钝的', + '淘气的', + '乖巧的', + '活泼的', + '沉默的', + '健康的', + '不健康的', + '高大的', + '矮小的', + '长的', + '短的', + '胖的', + '瘦的', + '美满的', + '不幸的', + '富有的', + '贫穷的', + '快乐的', + '不开心的', + '甜美的', + '苦涩的', + '精明的', + '愚蠢的', + '聪明的', + '智商高的', + '心灵手巧的', + '笨手笨脚的', + '冷静的', + '冲动的', + '踏实的', + '轻浮的', + '温柔的', + '粗暴的', + '好学的', + '讨厌学习的', + '好吃的', + '不好吃的', + '耐心的', + '急躁的', + '友善的', + '冷漠的', + '豁达的', + '固执的', + '谨慎的', + '善良的', + '狠毒的', + '平和的', + '狂躁的', + '机会主义的', + '悲观的', + '乐观的', + '心胸开阔的', + '偏狭的', + '讲义气的', + '不守信用的', + '有魅力的', + '无趣的', + '有思想的', + '无聊的', + '谋略深的', + '目光短浅的', + '善解人意的', + '自私的', + '坦率的', + '虚伪的', + '好奇的', + '不解风情的', + '喜欢交友的', + '独来独往的', + '健谈的', + '静默的', + '喜欢思考的', + '机智幽默的', + '情感丰富的', + '心地善良的', + '充满自信的', + '天真烂漫的', + '追求完美的', + '充满活力的', + '喜欢冒险的', + '充满创造力的', + '沉着冷静的', + '目标明确的', + '性格温和的', + '乐于助人的', + '聪明伶俐的', + '重情重义的', + '思维敏捷的', + '慷慨大方的', + '婉约多姿的', + '时尚前卫的', + '豁达开朗的', + '气质高雅的', + '优雅大方的', + '沉静深沉的', + '坚韧不拔的', + '独立自主的', + '外向开朗的', + '内向沉默的', + '深情专注的', + '精力旺盛的', + '富于幽默的', + '心思细腻的', + '喜怒形于色的', + '忠心耿耿的', + '玩世不恭的', + '活力四射的', + '脚踏实地的', + '注重细节的', + '保守谨慎的', + '世故圆滑的', + '梦想家的', + '勇往直前的', + '干练果敢的', + '待人友善的', + '思想开放的', + '敢于挑战的', + '感性洒脱的', + '洒脱不羁的', + '自我牺牲的', + '处事果断的', + '好奇心强的', + '待人热情的', + '热情洋溢的', + '孤独悲伤的', + '浪漫多情的', + '爱笑的', + '不羁的', + '傻气的', + '不拘小节的', + '懒散的', + '无聊的', + '低调的', + '敏感的', + '冷酷的', + '专注的', + '不屑的', + '激情的', + '忠诚的', + '神秘的', + '高傲的', + '自由的', + '文艺的', + '时尚的', + '落落大方的', + '有才华的', + '有气质的', + '阳光的', + '风趣的', + '天真浪漫的', + '爽朗开朗的', + '内敛沉静的', + '刻苦努力的', + '性格迥异的', + '个性张扬的', + '脾气火爆的', + '傲娇的', + '爱撒娇的', + '心思缜密的', + '理智果断的', + '懒惰的', + '喜欢拖延的', + '有责任感的', + '追求自由的', + '感性的', + '理性的', + '缺乏安全感的', + '追求安全感的', + '情绪化的', + '乐观的', + '悲观的', + '现实主义的', + '理想主义的', + '平易近人的', + '目中无人的', + '重视亲情的', + '重视友情的', + '重视爱情的', + '有爱心的', + '有正义感的', + '有同情心的', + '有童心的', + '有自信的', + '胆小怕事的', + '爱唠叨的', + '话多的', + '话少的', + '勇于冒险的', + '爱挑战的', + '善于发现美的', + '自我意识强的', + '不喜欢被约束的', + '慢热型的', + '热情洋溢的', + '容易受伤的', + '重视感情的', + '善于沟通的', + '不善于表达的', + '有幽默感的', + '平易近人的', + '有亲和力的', + '脸皮厚的', + '喜欢交际的', + '宅男/宅女的', + '喜欢独处的', + '有自知之明的', + '喜欢音乐的', + '喜欢阅读的', + '喜欢旅行的', + '喜欢美食的', + '喜欢运动的', + '喜欢摄影的', + '喜欢收藏的', + '喜欢购物的', + '不喜欢出门的', + '喜欢挑剔的', + '喜欢自省的', + '不喜欢评价他人的', + '喜欢评价他人的', + '有品位的', + '不讲卫生的', + '讲究卫生的', + '喜欢干净的', + '喜欢乱的', + '喜欢组织的', + '喜欢随意的', + '喜欢收纳的', + ]; + const nouns = [ + '狗子', + '猫咪', + '小鹿', + '小熊', + '小兔', + '小羊', + '小猪', + '小马', + '小狮子', + '小老虎', + '小猴子', + '小鱼儿', + '小乌龟', + '小鸟儿', + '小蚂蚁', + '小蜜蜂', + '小蝴蝶', + '小蜻蜓', + '小螃蟹', + '小章鱼', + '小海豚', + '小鲨鱼', + '小鲸鱼', + '小鳄鱼', + '小鸭子', + '小雪人', + '小皮球', + '小篮球', + '小足球', + '小排球', + '小棒球', + '小滑板', + '小冰棍', + '小雨伞', + '小手套', + '小电影', + '小蓝天', + '小公主', + '小王子', + '小玩具', + '小糖果', + '小巧克力', + '小冰淇淋', + '小蛋糕', + '小披萨', + '小汉堡', + '小炸鸡', + '小烤鸭', + '小鱼丸', + '小火锅', + '小串串', + '小煎饼', + '小油条', + '小葱油饼', + '小米粥', + '小酸奶', + '小豆腐', + '小饺子', + '小包子', + '小馄饨', + '小面条', + '小牛肉面', + '小糯米鸡', + '小蒸饺', + '小炒面', + '小蒸包', + '小烤肉', + '小烤串', + '小花生米', + '小太阳', + '小月亮', + '小星星', + '小彩虹', + '小风车', + '小气球', + '小钢琴', + '小吉他', + '小音响', + '小麦克风', + '小演员', + '小画家', + '小工程师', + '小医生', + '小警察', + '小消防员', + '小司机', + '小农民', + '小潜水员', + '小飞行员', + '小篮球', + '小游泳健将', + '小跑步冠军', + '小武术高手', + '小芭蕾舞者', + '小沙画家', + '小书法家', + '小拼图专家', + '小玩具收藏家', + '小电影制片人', + '小太空旅行家', + '超级英雄', + '无敌大魔王', + '终极霸主', + '至尊帝王', + '天降巨人', + '绝世奇才', + '神话之门', + '恐怖怪兽', + '魔法使者', + '神秘剑客', + '不朽传说', + '宇宙霸主', + '地狱火山', + '无尽黑暗', + '闪耀之星', + '璀璨之光', + '金色骑士', + '毁天灭地', + '战无不胜', + '碾压一切', + '绝世高手', + '超凡脱俗', + '万象之王', + '黑暗骑士', + '霸天战神', + '万众瞩目', + '震古烁今', + '纵横天下', + '永不磨灭', + '恒久不变', + '帝国之主', + '不屈不挠', + '狂暴之王', + '超越极限', + '魔力无边', + '星光闪耀', + '无尽追求', + '刀锋之舞', + '独步天下', + '吞噬万物', + '永恒之境', + '灭世战神', + '海量财富', + '神话传说', + '唯我独尊', + '万剑归宗', + '嗜血狂魔', + '深海之王', + '幻想之城', + '天命之子', + ]; + return { adjectives, nouns }; +}; + +export function genNickName() { + const { adjectives, nouns } = nameDataBase(); + const adjectiveIndex = Math.floor(Math.random() * adjectives.length); + const nounIndex = Math.floor(Math.random() * nouns.length); + const adjective = adjectives[adjectiveIndex]; + const noun = nouns[nounIndex]; + const randomNum = Math.floor(Math.random() * 1000); + return adjective + noun + randomNum; +} diff --git a/client/packages/rtc-web/src/views/chat/chat-room.vue b/client/packages/rtc-web/src/views/chat/chat-room.vue new file mode 100644 index 0000000..eca8a64 --- /dev/null +++ b/client/packages/rtc-web/src/views/chat/chat-room.vue @@ -0,0 +1,119 @@ + + + diff --git a/client/packages/rtc-web/src/views/chat/chat.vue b/client/packages/rtc-web/src/views/chat/chat.vue index 981df40..4c6e7e4 100644 --- a/client/packages/rtc-web/src/views/chat/chat.vue +++ b/client/packages/rtc-web/src/views/chat/chat.vue @@ -1,27 +1,67 @@ diff --git a/client/packages/rtc-web/src/views/chat/hooks/useChat.ts b/client/packages/rtc-web/src/views/chat/hooks/useChat.ts new file mode 100644 index 0000000..345d8f9 --- /dev/null +++ b/client/packages/rtc-web/src/views/chat/hooks/useChat.ts @@ -0,0 +1,57 @@ +import { ChatEventName } from '@/config'; +import { useSocket } from '@/hooks'; +import { unescapeStr } from '@/utils'; +import { ref, shallowRef } from 'vue'; + +export type SendMessage = { + content: string; + room: string; + from: string; + nickName: string; + recoderId: any; + time: string; + to?: string; +}; + +// export type MessageStatus = 'fail' | 'complete' | 'pending'; +export type MessageType = 'send' | 'receive'; + +export type MessageInfo = { + content: string; + // status: MessageStatus; + type: MessageType; +} & SendMessage; + +export const useChat = () => { + const socketRef = shallowRef(); + const msgList = ref([]); + + const handleChat = (socket: any) => { + socketRef.value = socket; + socket.on(ChatEventName.ChatingRoom, handleChatingRoom); + }; + const handleChatingRoom = (data: SendMessage) => { + console.log('receive', data); + msgList.value.push({ + ...data, + content: unescapeStr(data.content), + type: 'receive', + }); + }; + + const sendMessage = (data: SendMessage) => { + socketRef.value.emit(ChatEventName.ChatingRoom, data); + msgList.value.push({ + ...data, + content: unescapeStr(data.content), + type: 'send', + }); + }; + + useSocket(handleChat); + + return { + sendMessage, + msgList, + }; +}; diff --git a/client/packages/rtc-web/src/views/video/hooks/useVideoCall.ts b/client/packages/rtc-web/src/views/video/hooks/useVideoCall.ts new file mode 100644 index 0000000..13b9159 --- /dev/null +++ b/client/packages/rtc-web/src/views/video/hooks/useVideoCall.ts @@ -0,0 +1,243 @@ +import { useRoomConnect, useUserAgent } from '@/hooks'; +import { useUserMedia, useDevicesList } from '@vueuse/core'; +import { reject } from 'lodash'; +import { + Ref, + computed, + onBeforeUnmount, + ref, + shallowRef, + watch, + watchEffect, +} from 'vue'; + +export type VideoShareOption = { + immeately?: boolean; + audio?: Ref; + speaker?: Ref; +}; + +export const useMediaSetting = ( + option: VideoShareOption = { immeately: true } +) => { + const currentCamera = ref(); + const currentAudioInput = ref(); + const currentAudioOutput = ref(); + + const audioRef = ref(option.audio); + const speakerRef = ref(option.speaker); + + const mediaLoaded = ref(false); + + const { isMobile } = useUserAgent(); + + // 切换 麦克风 + watch( + audioRef, + (value) => { + currentAudioInput.value = value?.deviceId; + }, + { + immediate: true, + } + ); + + // 切换 扬声器 + watch( + speakerRef, + (value) => { + currentAudioOutput.value = value; + }, + { + immediate: true, + } + ); + + // 收集的错误信息 + // const errorMsg = ref({ + // audio: '', + // }); + + const { + videoInputs: cameras, + audioInputs, + audioOutputs, + } = useDevicesList({ + requestPermissions: true, + onUpdated() { + // 初始化默认 摄像头、扬声器、麦克风 + + if (!cameras.value.find((i) => i.deviceId === currentCamera.value)) + currentCamera.value = cameras.value[0]?.deviceId; + + if ( + !audioInputs.value.find((i) => i.deviceId === currentAudioInput.value) + ) + currentAudioInput.value = audioInputs.value[0]?.deviceId; + + if ( + !audioOutputs.value.find((i) => i.deviceId === currentAudioOutput.value) + ) { + currentAudioOutput.value = audioOutputs.value[0]?.deviceId; + } + }, + }); + + const video = shallowRef(); + + watch([currentAudioOutput, video], ([v1, v2]) => { + if (v1 && v2) { + setSinkId(v1); + } + }); + + const constraints = computed(() => { + const audioDevice = currentAudioInput.value + ? { deviceId: currentAudioInput.value } + : false; + + const videoDevice = currentCamera.value + ? { + deviceId: currentCamera.value, + facingMode: 'user', + frameRate: { + ideal: 30, + max: 60, + }, + width: { ideal: 1920 }, + height: { ideal: 1280 }, + } + : false; + return { + audio: audioDevice, + video: videoDevice, + }; + }); + + const { stream, stop, restart, start, isSupported } = useUserMedia({ + constraints, + }); + + const audioTracks = ref([]); + const videoTracks = ref([]); + + const audioEnabled = ref(false); + const videoEnabled = ref(false); + + // 切换 video、audio 渲染 + const switchTrackEnable = (type: 'video' | 'audio', flag: boolean) => { + if (stream.value) { + if (type === 'audio') { + if (audioTracks.value?.length) { + audioEnabled.value = flag; + audioTracks.value.forEach((item) => (item.enabled = flag)); + } + } + if (type === 'video') { + if (videoTracks.value?.length) { + videoEnabled.value = flag; + videoTracks.value.forEach((item) => { + item.enabled = flag; + }); + } + } + } + }; + + const setSinkId = (sinkId: string) => { + if (!isMobile) { + (video.value! as any).setSinkId(sinkId); + } + }; + + // 先静音和关闭视频渲染 + watch([stream, video], () => { + if (stream.value) { + if (video.value) { + video.value.srcObject = stream.value!; + if (stream.value) { + audioTracks.value = stream.value.getAudioTracks(); + videoTracks.value = stream.value.getVideoTracks(); + } + if (!videoEnabled.value) { + switchTrackEnable('video', false); + } + if (!audioEnabled.value) { + switchTrackEnable('audio', false); + } + mediaLoaded.value = true; + } + } + }); + + // 进入页面先连接 + const startGetMedia = () => { + console.log(isSupported.value); + return new Promise((resolve) => { + const watchEnableStop = watchEffect(async () => { + if ( + currentAudioInput.value && + currentCamera.value && + currentAudioOutput.value + ) { + const stream = await start(); + watchEnableStop(); + resolve(stream); + } else { + reject('请打开摄像头或者麦克风'); + } + }); + }); + }; + + if (option.immeately) { + startGetMedia(); + } + + onBeforeUnmount(() => { + stop(); + }); + + return { + startGetMedia, + stream, + video, + setSinkId, + switchTrackEnable, + restart, + stop, + audioEnabled, + videoEnabled, + mediaLoaded, + currentAudioInput, + currentAudioOutput, + }; +}; + +export const useMediaConnect = ( + stream: MediaStream, + connect: { onTrack?: (track: MediaStreamTrack, id: string) => void } = {} +) => { + useRoomConnect({ + roomJoined: async (_, pc) => { + if (pc && stream) { + stream.getTracks().forEach((track) => { + pc.addTrack(track, stream); + }); + } + }, + onBeforeCreateAnswer: async (_, pc) => { + if (pc && stream) { + stream.getTracks().forEach((track) => { + pc.addTrack(track, stream); + }); + } + }, + onTrack(e: any, id: string) { + // console.log(e); + if (e.track.kind === 'video') { + connect.onTrack?.(e.streams[0], id); + } + }, + }); +}; diff --git a/client/packages/rtc-web/src/views/video/video-control-menu.vue b/client/packages/rtc-web/src/views/video/video-control-menu.vue new file mode 100644 index 0000000..9e6e484 --- /dev/null +++ b/client/packages/rtc-web/src/views/video/video-control-menu.vue @@ -0,0 +1,158 @@ + + + diff --git a/client/packages/rtc-web/src/views/video/video-control.vue b/client/packages/rtc-web/src/views/video/video-control.vue new file mode 100644 index 0000000..6b0a6bd --- /dev/null +++ b/client/packages/rtc-web/src/views/video/video-control.vue @@ -0,0 +1,42 @@ + + + diff --git a/client/packages/rtc-web/src/views/video/video-room.vue b/client/packages/rtc-web/src/views/video/video-room.vue new file mode 100644 index 0000000..e978774 --- /dev/null +++ b/client/packages/rtc-web/src/views/video/video-room.vue @@ -0,0 +1,162 @@ + + + diff --git a/client/packages/rtc-web/src/views/video/video.vue b/client/packages/rtc-web/src/views/video/video.vue new file mode 100644 index 0000000..6e8a9e1 --- /dev/null +++ b/client/packages/rtc-web/src/views/video/video.vue @@ -0,0 +1,56 @@ + + + diff --git a/client/packages/rtc-web/src/views/welcome.vue b/client/packages/rtc-web/src/views/welcome.vue index 184e808..2e95535 100644 --- a/client/packages/rtc-web/src/views/welcome.vue +++ b/client/packages/rtc-web/src/views/welcome.vue @@ -5,7 +5,7 @@ defineOptions({ diff --git a/client/packages/rtc-web/vite.config.ts b/client/packages/rtc-web/vite.config.ts index e3cb5bc..403cfb1 100644 --- a/client/packages/rtc-web/vite.config.ts +++ b/client/packages/rtc-web/vite.config.ts @@ -4,12 +4,28 @@ import eslintPlugin from 'vite-plugin-eslint'; import { resolve } from 'path'; import vueJsx from '@vitejs/plugin-vue-jsx'; +import fs from 'fs'; + import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'; const pathResolve = (path: string) => resolve(__dirname, path); +// http://localhost:9092/api +// https://im.iamtsm.cn/api + // https://vitejs.dev/config/ export default defineConfig({ + build: { + outDir: resolve(__dirname, '../../../client_dist/rtc-web'), + minify: 'terser', + emptyOutDir: true, + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true, + }, + }, + }, resolve: { alias: [ { @@ -18,6 +34,21 @@ export default defineConfig({ }, ], }, + server: { + host: '0.0.0.0', + proxy: { + '/api': { + target: 'https://192.168.1.11:9092/api', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), + secure: false, + }, + }, + https: { + key: fs.readFileSync('./src/keys/server.key'), + cert: fs.readFileSync('./src/keys/server.crt'), + }, + }, plugins: [ vue(), vueJsx(), @@ -27,5 +58,9 @@ export default defineConfig({ // 指定symbolId格式 symbolId: 'icon-[dir]-[name]', }), + // basicSsl(), + // mkcert({ + // source: 'coding', + // }), ], }); diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index d891ee1..5c91ad3 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -11,8 +11,10 @@ importers: '@vitejs/plugin-vue': ^4.2.3 '@vitejs/plugin-vue-jsx': ^3.0.1 '@vueuse/core': ^10.2.0 + '@vueuse/router': ^10.2.1 autoprefixer: ^10.4.14 daisyui: ^3.1.6 + dayjs: ^1.11.9 eslint: ^8.43.0 eslint-config-prettier: ^8.8.0 eslint-plugin-prettier: ^4.2.1 @@ -23,8 +25,11 @@ importers: postcss-import: ^15.1.0 prettier: ^2.8.8 prettier-plugin-tailwindcss: ^0.3.0 + socket.io-client: 2.3.0 tailwindcss: ^3.3.2 + terser: ^5.19.4 typescript: ^5.1.3 + vconsole: ^3.15.1 vite: ^4.3.9 vite-plugin-eslint: ^1.8.1 vite-plugin-svg-icons: ^2.0.1 @@ -32,14 +37,21 @@ importers: vue-eslint-parser: ^9.3.1 vue-router: '4' vue-tsc: ^1.8.1 + vue3-emoji-picker: ^1.1.7 + vue3-popper: ^1.5.0 dependencies: '@types/lodash': 4.14.195 '@vitejs/plugin-vue-jsx': 3.0.1_vite@4.3.9+vue@3.3.4 '@vueuse/core': 10.2.0_vue@3.3.4 + '@vueuse/router': 10.2.1_vue-router@4.2.2+vue@3.3.4 + dayjs: 1.11.9 lodash: 4.17.21 nanoid: 4.0.2 + socket.io-client: 2.3.0 vue: 3.3.4 vue-router: 4.2.2_vue@3.3.4 + vue3-emoji-picker: 1.1.7 + vue3-popper: 1.5.0_vue@3.3.4 devDependencies: '@types/node': 20.3.2 '@typescript-eslint/eslint-plugin': 5.60.1_emhilanx7rvvqwlixwztbx5x2m @@ -56,8 +68,10 @@ importers: prettier: 2.8.8 prettier-plugin-tailwindcss: 0.3.0_prettier@2.8.8 tailwindcss: 3.3.2 + terser: 5.19.4 typescript: 5.1.3 - vite: 4.3.9_@types+node@20.3.2 + vconsole: 3.15.1 + vite: 4.3.9_66pwffvb2axzwdhxmcirxliemu vite-plugin-eslint: 1.8.1_eslint@8.43.0+vite@4.3.9 vite-plugin-svg-icons: 2.0.1_vite@4.3.9 vue-eslint-parser: 9.3.1_eslint@8.43.0 @@ -336,6 +350,13 @@ packages: - supports-color dev: false + /@babel/runtime/7.22.15: + resolution: {integrity: sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: true + /@babel/template/7.22.5: resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} engines: {node: '>=6.9.0'} @@ -620,6 +641,12 @@ packages: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} + /@jridgewell/source-map/0.3.5: + resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.18 + /@jridgewell/sourcemap-codec/1.4.14: resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} @@ -653,6 +680,10 @@ packages: fastq: 1.15.0 dev: true + /@popperjs/core/2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + dev: false + /@rollup/pluginutils/4.2.1: resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} engines: {node: '>= 8.0.0'} @@ -842,7 +873,7 @@ packages: '@babel/core': 7.22.5 '@babel/plugin-transform-typescript': 7.22.5_@babel+core@7.22.5 '@vue/babel-plugin-jsx': 1.1.4_@babel+core@7.22.5 - vite: 4.3.9_@types+node@20.3.2 + vite: 4.3.9_66pwffvb2axzwdhxmcirxliemu vue: 3.3.4 transitivePeerDependencies: - supports-color @@ -855,7 +886,7 @@ packages: vite: ^4.0.0 vue: ^3.2.25 dependencies: - vite: 4.3.9_@types+node@20.3.2 + vite: 4.3.9_66pwffvb2axzwdhxmcirxliemu vue: 3.3.4 dev: true @@ -1021,6 +1052,19 @@ packages: resolution: {integrity: sha512-IR7Mkq6QSgZ38q/2ZzOt+Zz1OpcEsnwE64WBumDQ+RGKrosFCtUA2zgRrOqDEzPBXrVB+4HhFkwDjQMu0fDBKw==} dev: false + /@vueuse/router/10.2.1_vue-router@4.2.2+vue@3.3.4: + resolution: {integrity: sha512-H/1T4fLzMmeBNEmcXlbqk6AEp0HQpzf+0eeNJ6fGrs3RWClE2i3nYEFbtxfQeSm/7nZ6nf/UhgahzUQdyMhIwQ==} + peerDependencies: + vue-router: '>=4.0.0-rc.1' + dependencies: + '@vueuse/shared': 10.2.1_vue@3.3.4 + vue-demi: 0.14.5_vue@3.3.4 + vue-router: 4.2.2_vue@3.3.4 + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: false + /@vueuse/shared/10.2.0_vue@3.3.4: resolution: {integrity: sha512-dIeA8+g9Av3H5iF4NXR/sft4V6vys76CpZ6hxwj8eMXybXk2WRl3scSsOVi+kQ9SX38COR7AH7WwY83UcuxbSg==} dependencies: @@ -1030,6 +1074,15 @@ packages: - vue dev: false + /@vueuse/shared/10.2.1_vue@3.3.4: + resolution: {integrity: sha512-QWHq2bSuGptkcxx4f4M/fBYC3Y8d3M2UYyLsyzoPgEoVzJURQ0oJeWXu79OiLlBb8gTKkqe4mO85T/sf39mmiw==} + dependencies: + vue-demi: 0.14.5_vue@3.3.4 + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: false + /acorn-jsx/5.3.2_acorn@8.9.0: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1042,7 +1095,10 @@ packages: resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==} engines: {node: '>=0.4.0'} hasBin: true - dev: true + + /after/0.8.2: + resolution: {integrity: sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==} + dev: false /ajv/6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1127,11 +1183,19 @@ packages: engines: {node: '>=0.10.0'} dev: true + /arraybuffer.slice/0.0.7: + resolution: {integrity: sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==} + dev: false + /assign-symbols/1.0.0: resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} engines: {node: '>=0.10.0'} dev: true + /async-limiter/1.0.1: + resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} + dev: false + /atob/2.1.2: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} @@ -1154,6 +1218,10 @@ packages: postcss-value-parser: 4.2.0 dev: true + /backo2/1.0.2: + resolution: {integrity: sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==} + dev: false + /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -1171,6 +1239,22 @@ packages: pascalcase: 0.1.1 dev: true + /base64-arraybuffer/0.1.4: + resolution: {integrity: sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==} + engines: {node: '>= 0.6.0'} + dev: false + + /base64-arraybuffer/0.1.5: + resolution: {integrity: sha512-437oANT9tP582zZMwSvZGy2nmSeAb8DW2me3y+Uv1Wp2Rulr8Mqlyrv3E7MLxmsiaPSMMDmiDVzgE+e8zlMx9g==} + engines: {node: '>= 0.6.0'} + dev: false + + /better-assert/1.0.2: + resolution: {integrity: sha512-bYeph2DFlpK1XmGs6fvlLRUN29QISM3GBuUwSFsMY2XRx4AvC0WNCS57j4c/xGrK2RS24C1w3YoBOsw9fT46tQ==} + dependencies: + callsite: 1.0.0 + dev: false + /big.js/5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} dev: true @@ -1180,6 +1264,10 @@ packages: engines: {node: '>=8'} dev: true + /blob/0.0.5: + resolution: {integrity: sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==} + dev: false + /bluebird/3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} dev: true @@ -1236,6 +1324,9 @@ packages: node-releases: 2.0.12 update-browserslist-db: 1.0.11_browserslist@4.21.9 + /buffer-from/1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + /cache-base/1.0.1: resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} engines: {node: '>=0.10.0'} @@ -1251,6 +1342,10 @@ packages: unset-value: 1.0.0 dev: true + /callsite/1.0.0: + resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} + dev: false + /callsites/3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1360,6 +1455,9 @@ packages: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} dev: true + /commander/2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + /commander/4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1370,9 +1468,20 @@ packages: engines: {node: '>= 10'} dev: true + /component-bind/1.0.0: + resolution: {integrity: sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==} + dev: false + + /component-emitter/1.2.1: + resolution: {integrity: sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==} + dev: false + /component-emitter/1.3.0: resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} - dev: true + + /component-inherit/0.0.3: + resolution: {integrity: sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==} + dev: false /concat-map/0.0.1: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} @@ -1387,6 +1496,16 @@ packages: engines: {node: '>=0.10.0'} dev: true + /copy-text-to-clipboard/3.2.0: + resolution: {integrity: sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==} + engines: {node: '>=12'} + dev: true + + /core-js/3.32.1: + resolution: {integrity: sha512-lqufgNn9NLnESg5mQeYsxQP5w7wrViSj0jr/kv6ECQiByzQkrn1MKvV0L3acttpDqfQrHLwr2KCMgX5b8X+lyQ==} + requiresBuild: true + dev: true + /cors/2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -1465,10 +1584,18 @@ packages: - ts-node dev: true + /dayjs/1.11.9: + resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==} + dev: false + /de-indent/1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} dev: true + /debounce/1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + dev: false + /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -1480,6 +1607,29 @@ packages: ms: 2.0.0 dev: true + /debug/3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /debug/4.1.1: + resolution: {integrity: sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==} + deprecated: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797) + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + /debug/4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -1603,6 +1753,36 @@ packages: engines: {node: '>= 4'} dev: true + /engine.io-client/3.4.4: + resolution: {integrity: sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==} + dependencies: + component-emitter: 1.3.0 + component-inherit: 0.0.3 + debug: 3.1.0 + engine.io-parser: 2.2.1 + has-cors: 1.1.0 + indexof: 0.0.1 + parseqs: 0.0.6 + parseuri: 0.0.6 + ws: 6.1.4 + xmlhttprequest-ssl: 1.5.5 + yeast: 0.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /engine.io-parser/2.2.1: + resolution: {integrity: sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==} + dependencies: + after: 0.8.2 + arraybuffer.slice: 0.0.7 + base64-arraybuffer: 0.1.4 + blob: 0.0.5 + has-binary2: 1.0.3 + dev: false + /entities/1.1.2: resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} dev: true @@ -2068,6 +2248,16 @@ packages: ansi-regex: 2.1.1 dev: true + /has-binary2/1.0.3: + resolution: {integrity: sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==} + dependencies: + isarray: 2.0.1 + dev: false + + /has-cors/1.1.0: + resolution: {integrity: sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==} + dev: false + /has-flag/1.0.0: resolution: {integrity: sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==} engines: {node: '>=0.10.0'} @@ -2142,6 +2332,10 @@ packages: readable-stream: 3.6.2 dev: true + /idb/7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + dev: false + /ignore/5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -2166,6 +2360,10 @@ packages: engines: {node: '>=0.8.19'} dev: true + /indexof/0.0.1: + resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==} + dev: false + /inflight/1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: @@ -2302,6 +2500,10 @@ packages: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} dev: true + /isarray/2.0.1: + resolution: {integrity: sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==} + dev: false + /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -2540,7 +2742,6 @@ packages: /ms/2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - dev: true /ms/2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -2549,6 +2750,10 @@ packages: resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} dev: true + /mutation-observer/1.0.3: + resolution: {integrity: sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==} + dev: true + /mz/2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} dependencies: @@ -2619,6 +2824,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /object-component/0.0.3: + resolution: {integrity: sha512-S0sN3agnVh2SZNEIGc0N1X4Z5K0JeFbGBrnuZpsxuUh5XLF0BnvWkMjRXo/zGKLd/eghvNIKcx1pQkmUjXIyrA==} + dev: false + /object-copy/0.1.0: resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} engines: {node: '>=0.10.0'} @@ -2686,6 +2895,26 @@ packages: callsites: 3.1.0 dev: true + /parseqs/0.0.5: + resolution: {integrity: sha512-B3Nrjw2aL7aI4TDujOzfA4NsEc4u1lVcIRE0xesutH8kjeWF70uk+W5cBlIQx04zUH9NTBvuN36Y9xLRPK6Jjw==} + dependencies: + better-assert: 1.0.2 + dev: false + + /parseqs/0.0.6: + resolution: {integrity: sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==} + dev: false + + /parseuri/0.0.5: + resolution: {integrity: sha512-ijhdxJu6l5Ru12jF0JvzXVPvsC+VibqeaExlNoMhWN6VQ79PGjkmc7oA4W1lp00sFkNyj0fx6ivPLdV51/UMog==} + dependencies: + better-assert: 1.0.2 + dev: false + + /parseuri/0.0.6: + resolution: {integrity: sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==} + dev: false + /pascalcase/0.1.1: resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} engines: {node: '>=0.10.0'} @@ -2976,6 +3205,10 @@ packages: picomatch: 2.3.1 dev: true + /regenerator-runtime/0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + dev: true + /regex-not/1.0.2: resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} engines: {node: '>=0.10.0'} @@ -3133,6 +3366,39 @@ packages: - supports-color dev: true + /socket.io-client/2.3.0: + resolution: {integrity: sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==} + dependencies: + backo2: 1.0.2 + base64-arraybuffer: 0.1.5 + component-bind: 1.0.0 + component-emitter: 1.2.1 + debug: 4.1.1 + engine.io-client: 3.4.4 + has-binary2: 1.0.3 + has-cors: 1.1.0 + indexof: 0.0.1 + object-component: 0.0.3 + parseqs: 0.0.5 + parseuri: 0.0.5 + socket.io-parser: 3.3.3 + to-array: 0.1.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /socket.io-parser/3.3.3: + resolution: {integrity: sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==} + dependencies: + component-emitter: 1.3.0 + debug: 3.1.0 + isarray: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + /source-map-js/1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -3148,6 +3414,12 @@ packages: urix: 0.1.0 dev: true + /source-map-support/0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + /source-map-url/0.4.1: resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} deprecated: See https://github.com/lydell/source-map-url#deprecated @@ -3161,7 +3433,6 @@ packages: /source-map/0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true /split-string/3.1.0: resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} @@ -3328,6 +3599,16 @@ packages: - ts-node dev: true + /terser/5.19.4: + resolution: {integrity: sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.5 + acorn: 8.9.0 + commander: 2.20.3 + source-map-support: 0.5.21 + /text-table/0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -3345,6 +3626,10 @@ packages: any-promise: 1.3.0 dev: true + /to-array/0.1.4: + resolution: {integrity: sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==} + dev: false + /to-fast-properties/2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -3479,6 +3764,15 @@ packages: engines: {node: '>= 0.8'} dev: true + /vconsole/3.15.1: + resolution: {integrity: sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==} + dependencies: + '@babel/runtime': 7.22.15 + copy-text-to-clipboard: 3.2.0 + core-js: 3.32.1 + mutation-observer: 1.0.3 + dev: true + /vite-plugin-eslint/1.8.1_eslint@8.43.0+vite@4.3.9: resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} peerDependencies: @@ -3489,7 +3783,7 @@ packages: '@types/eslint': 8.40.2 eslint: 8.43.0 rollup: 2.79.1 - vite: 4.3.9_@types+node@20.3.2 + vite: 4.3.9_66pwffvb2axzwdhxmcirxliemu dev: true /vite-plugin-svg-icons/2.0.1_vite@4.3.9: @@ -3505,12 +3799,12 @@ packages: pathe: 0.2.0 svg-baker: 1.7.0 svgo: 2.8.0 - vite: 4.3.9_@types+node@20.3.2 + vite: 4.3.9_66pwffvb2axzwdhxmcirxliemu transitivePeerDependencies: - supports-color dev: true - /vite/4.3.9_@types+node@20.3.2: + /vite/4.3.9_66pwffvb2axzwdhxmcirxliemu: resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -3539,6 +3833,7 @@ packages: esbuild: 0.17.19 postcss: 8.4.24 rollup: 3.25.3 + terser: 5.19.4 optionalDependencies: fsevents: 2.3.2 @@ -3612,6 +3907,25 @@ packages: '@vue/server-renderer': 3.3.4_vue@3.3.4 '@vue/shared': 3.3.4 + /vue3-emoji-picker/1.1.7: + resolution: {integrity: sha512-dKSI1NyrinYFykllwcOqBB1sw7EHdwQG4tjHYSO+khQkY8Csn4Evn5X2nAdz8Kl8o3P1J0jV4BGwbQ2dVWCxMA==} + dependencies: + '@popperjs/core': 2.11.8 + idb: 7.1.1 + vue: 3.3.4 + dev: false + + /vue3-popper/1.5.0_vue@3.3.4: + resolution: {integrity: sha512-xaEnx90YBnlSg5G2yWqm2DHWHg+DB99UVRp4VsyTF0QLXyHrqSuE1Xo5+sG0AQq/lBcrGMlk5NU5xE2MDLKViw==} + engines: {node: '>=12'} + peerDependencies: + vue: ^3.2.20 + dependencies: + '@popperjs/core': 2.11.8 + debounce: 1.2.1 + vue: 3.3.4 + dev: false + /which/2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3629,11 +3943,30 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true + /ws/6.1.4: + resolution: {integrity: sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dependencies: + async-limiter: 1.0.1 + dev: false + /xml-name-validator/4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} dev: true + /xmlhttprequest-ssl/1.5.5: + resolution: {integrity: sha512-/bFPLUgJrfGUL10AIv4Y7/CUt6so9CLtB/oFxQSHseSDNNCdC6vwwKEqwLN6wNPBg9YWXAiMu8jkf6RPRS/75Q==} + engines: {node: '>=0.4.0'} + dev: false + /yallist/3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} dev: false @@ -3647,6 +3980,10 @@ packages: engines: {node: '>= 14'} dev: true + /yeast/0.1.2: + resolution: {integrity: sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==} + dev: false + /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'}