Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions Web/web-vite-vue3/package.json
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
{
"name": "@tencentcloud/live-uikit-vue",
"version": "5.6.0",
"version": "5.8.0",
"scripts": {
"dev": "vite --force",
"dev:business": "cross-env STYLE_PRESET=business vite --force",
"dev:deep": "vite",
"dev:local": "vite --force",
"upload-server": "npm --prefix ./upload-server run start",
"upload-server:standalone": "npm --prefix ./upload-server run start",
"upload-server:bootstrap": "npm --prefix ./upload-server install",
"dev:with-upload-server": "concurrently \"npm run dev\" \"npm run upload-server\"",
"build": "vite build",
"build:business": "cross-env STYLE_PRESET=business vite build",
"build:local": "vite build",
"preview": "vite preview",
"lint": "./node_modules/.bin/eslint ./src --no-error-on-unmatched-pattern"
},
"dependencies": {
"@tencentcloud/tuiroom-engine-js": "~4.0.2",
"@tencentcloud/uikit-base-component-vue3": "1.3.8",
"@tencentcloud/tuiroom-engine-js": "~4.0.3",
"@tencentcloud/uikit-base-component-vue3": "1.4.0",
"@tencentcloud/universal-api": "^2.0.9",
"axios": "^0.27.2",
"js-cookie": "^3.0.1",
"mitt": "^3.0.0",
"pinia": "^2.0.13",
"qs": "^6.10.3",
"rtc-detect": "^1.0.3",
"tuikit-atomicx-vue3": "5.6.0",
"tuikit-atomicx-vue3": "5.8.1",
"vue": "^3.2.25",
"vue-i18n": "^9.10.2",
"vue-router": "^4.0.14"
Expand Down
4 changes: 2 additions & 2 deletions Web/web-vite-vue3/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<UIKitProvider theme="dark">
<UIKitProvider theme="dark" style-preset="business">
<router-view />
</UIKitProvider>
</template>
Expand All @@ -15,7 +15,7 @@ const { language } = useUIKit();
TUIRoomEngine.once('ready', () => {
watch(language, () => {
initRoomEngineLanguage();
}, { immediate: true })
}, { immediate: true });
});
</script>

Expand Down
8 changes: 7 additions & 1 deletion Web/web-vite-vue3/src/TUILiveKit/LivePusherView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ import OrientationSwitch from './component/OrientationSwitch.vue';
import SettingButton from './component/SettingButton.vue';
import SpeakerVolumeSetting from './component/SpeakerVolumeSetting.vue';
import LivePusherNotification from './component/LivePusherNotification.vue';
import { copyToClipboard } from './utils/utils';
import { copyToClipboard, isSvgCoverUrl } from './utils/utils';
import { errorHandler } from './utils/errorHandler';
import { initRoomEngineLanguage } from '../utils/utils';

Expand Down Expand Up @@ -279,6 +279,12 @@ const handleLiveSettingConfirm = async (form: { liveName: string; coverUrl?: str
liveName: form.liveName.trim(),
coverUrl: (form.coverUrl || '').trim(),
};
if (isSvgCoverUrl(updatedForm.coverUrl)) {
TUIToast.error({
message: t('Unsupported image format'),
});
return;
}

if (!isInLive.value || !currentLive.value?.liveId) {
liveParamsEditForm.value = updatedForm;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<div class="message-input">
<BarrageInput
width="158px"
:autoFocus="false"
:auto-focus="false"
:disabled="!isInLive"
:placeholder="isInLive ? '' : t('Live not started')"
@focus="handleBarrageInputFocus"
Expand Down Expand Up @@ -109,7 +109,7 @@
<script setup lang="ts">
import { ref, onMounted, computed, onUnmounted, watch, Teleport } from 'vue';
import TUIRoomEngine, { TUIAutoPlayCallbackInfo, TUIRoomEvents } from '@tencentcloud/tuiroom-engine-js';
import { TUIButton, IconClose, IconLike, TUIDialog, useUIKit, TUIMessageBox } from '@tencentcloud/uikit-base-component-vue3';
import { TUIButton, IconClose, IconLike, TUIDialog, TUIToast, useUIKit, TUIMessageBox } from '@tencentcloud/uikit-base-component-vue3';
import {
LiveAudienceList,
LiveCoreView,
Expand Down Expand Up @@ -137,6 +137,18 @@ const { audienceList, fetchAudienceList } = useLiveAudienceState();
const { currentLive, joinLive, leaveLive, subscribeEvent, unsubscribeEvent } = useLiveListState();
const { loginUserInfo } = useLoginState();
const isInLive = computed(() => !!currentLive.value?.liveId);

// Mute detection: show toast when the current user is muted by the host
const localAudience = computed(() => audienceList.value.find(item => item.userId === loginUserInfo.value?.userId));
const isMessageMuted = computed(() => !!localAudience.value?.isMessageDisabled);
watch(isMessageMuted, (newVal, oldVal) => {
if (newVal && !oldVal) {
TUIToast.info({ message: t('You have been muted in this room') });
}
if (!newVal && oldVal) {
TUIToast.info({ message: t('You have been unmuted in this room') });
}
});
const { canvas } = useLiveSeatState();
const { giftInfoList, sendLikes, subscribeEvent: subscribeGiftEvent, unsubscribeEvent: unsubscribeGiftEvent } = useLiveGiftState();
const roomEngine = useRoomEngine();
Expand Down Expand Up @@ -267,7 +279,7 @@ function handleAutoPlayFailed(event: TUIAutoPlayCallbackInfo) {
callback: () => {
autoPlayFailedHandled.value = false;
event.resume();
}
},
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@
<div class="main-left">
<div class="main-left-top">
<IconArrowStrokeBack class="icon-back" size="20" @click="handleLeaveLive" />
<Avatar
:src="currentLive?.liveOwner.avatarUrl" :size="32"
:style="{ border: '1px solid var(--uikit-color-white-7)' }"
/>
<span> {{ currentLive?.liveOwner.userName || currentLive?.liveOwner.userId }}</span>
<template v-if="liveEndedOverlayVisible">
<div class="top-ended-avatar">
<IconUser size="24" />
</div>
<span>{{ t('The host is not currently live') }}</span>
</template>
<template v-else>
<Avatar
:src="currentLive?.liveOwner.avatarUrl" :size="32"
:style="{ border: '1px solid var(--uikit-color-white-7)' }"
/>
<span> {{ currentLive?.liveOwner.userName || currentLive?.liveOwner.userId }}</span>
</template>
</div>
<div class="main-left-center">
<LiveView @empty-seat-click="handleApplyForSeat" />
Expand Down Expand Up @@ -90,10 +98,11 @@
</template>

<script setup lang="ts">
import { ref, onMounted, computed, onUnmounted } from 'vue';
import { ref, onMounted, computed, onUnmounted, watch } from 'vue';
import TUIRoomEngine, { TUIAutoPlayCallbackInfo, TUIRoomEvents } from '@tencentcloud/tuiroom-engine-js';
import {
IconArrowStrokeBack,
IconUser,
TUIButton,
TUIMessageBox,
TUIToast,
Expand All @@ -107,6 +116,7 @@ import {
useLiveAudienceState,
LiveView,
useLiveListState,
useLoginState,
Avatar,
useRoomEngine,
LiveListEvent,
Expand All @@ -122,9 +132,22 @@ import { initRoomEngineLanguage } from '../../../utils/utils';
const { t } = useUIKit();
const { audienceList } = useLiveAudienceState();
const { currentLive, joinLive, leaveLive, subscribeEvent, unsubscribeEvent } = useLiveListState();
const { loginUserInfo } = useLoginState();
const isInLive = computed(() => !!currentLive.value?.liveId);
const roomEngine = useRoomEngine();

// Mute detection: show toast when the current user is muted by the host
const localAudience = computed(() => audienceList.value.find(item => item.userId === loginUserInfo.value?.userId));
const isMessageMuted = computed(() => !!localAudience.value?.isMessageDisabled);
watch(isMessageMuted, (newVal, oldVal) => {
if (newVal && !oldVal) {
TUIToast.info({ message: t('You have been muted in this room') });
}
if (!newVal && oldVal) {
TUIToast.info({ message: t('You have been unmuted in this room') });
}
});

TUIRoomEngine.once('ready', () => {
roomEngine.instance?.on(TUIRoomEvents.onAutoPlayFailed, handleAutoPlayFailed);
});
Expand Down Expand Up @@ -259,7 +282,7 @@ function handleAutoPlayFailed(event: TUIAutoPlayCallbackInfo) {
callback: () => {
autoPlayFailedHandled.value = false;
event.resume();
}
},
});
}
</script>
Expand Down Expand Up @@ -316,6 +339,18 @@ function handleAutoPlayFailed(event: TUIAutoPlayCallbackInfo) {
cursor: pointer;
}
}

.top-ended-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: rgba(255, 255, 255, 0.55);
}
}

.main-left-center {
Expand All @@ -325,7 +360,6 @@ function handleAutoPlayFailed(event: TUIAutoPlayCallbackInfo) {
min-height: 0;
background-color: black;
overflow: hidden;
border: 1px solid var(--bg-color-operate);

.live-ended-overlay {
position: absolute;
Expand Down
6 changes: 6 additions & 0 deletions Web/web-vite-vue3/src/TUILiveKit/i18n/en-US/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ export const resource = {
'Seat does not support link mic': 'Seat does not support link mic',
'Empty seat list': 'Empty seat list',
'End Link': 'End Link',
'BarrageList.ComeIn': 'came in',
'BarrageList.Leave': 'left',
'BarrageList.SendGift': 'sent',
// Business theme additions (reuse existing keys when possible: 'Online viewers', 'Exit')
'Search audience...': 'Search audience...',
'No results found': 'No results found',
Expand All @@ -169,6 +172,7 @@ export const resource = {
'Turn on sound': 'Turn on sound',
'Mute sound': 'Mute sound',
'Exit picture in picture': 'Exit picture in picture',
'Not allow to enter cinema mode in fullscreen': 'Not allow to enter cinema mode in fullscreen',
'Enter cinema mode': 'Enter cinema mode',
'Exit cinema mode': 'Exit cinema mode',
'Enter full screen': 'Enter full screen',
Expand All @@ -192,4 +196,6 @@ export const resource = {
More: 'More',
'joined the live': 'joined the live',
'No audience yet': 'No audience yet',
'You have been muted in this room': 'You have been muted in this room',
'You have been unmuted in this room': 'You have been unmuted in this room',
};
6 changes: 6 additions & 0 deletions Web/web-vite-vue3/src/TUILiveKit/i18n/zh-CN/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ export const resource = {
'Seat does not support link mic': '该房间不支持连麦',
'Empty seat list': '连麦列表为空',
'End Link': '结束连麦',
'BarrageList.ComeIn': '进入了直播间',
'BarrageList.Leave': '离开了直播间',
'BarrageList.SendGift': '送出了',
// Business theme additions (reuse existing keys when possible: 'Online viewers', 'Exit')
'Search audience...': '搜索观众...',
'No results found': '无匹配结果',
Expand All @@ -169,6 +172,7 @@ export const resource = {
'Turn on sound': '取消静音',
'Mute sound': '静音',
'Exit picture in picture': '退出画中画',
'Not allow to enter cinema mode in fullscreen': '全屏状态下不允许进入影院模式',
'Enter cinema mode': '进入影院模式',
'Exit cinema mode': '退出影院模式',
'Enter full screen': '进入全屏',
Expand All @@ -192,4 +196,6 @@ export const resource = {
More: '更多',
'joined the live': '加入了直播',
'No audience yet': '暂无观众',
'You have been muted in this room': '当前房间内,您已被禁言',
'You have been unmuted in this room': '当前房间内,您已被解除禁言',
};
15 changes: 15 additions & 0 deletions Web/web-vite-vue3/src/TUILiveKit/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ export async function copyToClipboard(text: string): Promise<void> {
await navigator.clipboard.writeText(text);
}

export function isSvgCoverUrl(coverUrl: string): boolean {
if (!coverUrl) {
return false;
}
const normalizedUrl = coverUrl.trim().toLowerCase();
if (!normalizedUrl) {
return false;
}
if (normalizedUrl.startsWith('data:image/svg+xml')) {
return true;
}
const urlWithoutQuery = normalizedUrl.split('#')[0].split('?')[0];
return urlWithoutQuery.endsWith('.svg') || urlWithoutQuery.endsWith('.svgz');
}

export function isSafariBrowser(): boolean {
// Safari has several unique features
const isSafari =
Expand Down
5 changes: 3 additions & 2 deletions Web/web-vite-vue3/src/api/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export const UPLOAD_ALLOWED_MIME_TYPES = [
'image/png',
'image/gif',
'image/webp',
'image/svg+xml',
];

const uploadHttp = axios.create({
Expand Down Expand Up @@ -51,8 +50,10 @@ export async function uploadImageFile(params: {
type?: 'cover' | 'gift-icon' | 'gift-animation';
}): Promise<UploadResponseData> {
const formData = new FormData();
formData.append('file', params.file);
// Keep `type` before `file` so the server can resolve per-type MIME rules
// as early as possible during multipart parsing.
formData.append('type', params.type || 'cover');
formData.append('file', params.file);

const response = await uploadHttp.post('/api/upload/image', formData, {
headers: {
Expand Down
9 changes: 5 additions & 4 deletions Web/web-vite-vue3/src/business/components/BusinessHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { IconUsers, IconShare } from '@tencentcloud/uikit-base-component-vue3';
import {
IconBusinessUsers as IconUsers,
IconBusinessShare as IconShare,
} from '@tencentcloud/uikit-base-component-vue3';

const props = defineProps<{
title?: string;
Expand All @@ -38,9 +41,7 @@ const props = defineProps<{
startTime?: number;
}>();

defineEmits<{
(e: 'action-click'): void;
}>();
defineEmits(['action-click']);

// Duration counter
const elapsed = ref(0);
Expand Down
Loading