mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-23 23:57:22 +08:00
✨ feat: 添加远程控制功能,支持远程控制音乐播放操作
This commit is contained in:
@@ -26,6 +26,7 @@ export default {
|
||||
delete: 'Delete',
|
||||
refresh: 'Refresh',
|
||||
retry: 'Retry',
|
||||
reset: 'Reset',
|
||||
validation: {
|
||||
required: 'This field is required',
|
||||
invalidInput: 'Invalid input',
|
||||
|
||||
@@ -83,7 +83,9 @@ export default {
|
||||
unlimitedDownload: 'Unlimited Download',
|
||||
unlimitedDownloadDesc: 'Enable unlimited download mode for music , default limit 300 songs',
|
||||
downloadPath: 'Download Directory',
|
||||
downloadPathDesc: 'Choose download location for music files'
|
||||
downloadPathDesc: 'Choose download location for music files',
|
||||
remoteControl: 'Remote Control',
|
||||
remoteControlDesc: 'Set remote control function'
|
||||
},
|
||||
network: {
|
||||
apiPort: 'Music API Port',
|
||||
@@ -230,5 +232,15 @@ export default {
|
||||
disableAll: 'All shortcuts disabled, please save to apply',
|
||||
enableAll: 'All shortcuts enabled, please save to apply'
|
||||
}
|
||||
},
|
||||
remoteControl: {
|
||||
title: 'Remote Control',
|
||||
enable: 'Enable Remote Control',
|
||||
port: 'Port',
|
||||
allowedIps: 'Allowed IPs',
|
||||
addIp: 'Add IP',
|
||||
emptyListHint: 'Empty list means allow all IPs',
|
||||
saveSuccess: 'Remote control settings saved',
|
||||
accessInfo: 'Remote control access address:',
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ export default {
|
||||
delete: '删除',
|
||||
refresh: '刷新',
|
||||
retry: '重试',
|
||||
reset: '重置',
|
||||
validation: {
|
||||
required: '此项是必填的',
|
||||
invalidInput: '输入无效',
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
"playback": {
|
||||
"musicSources": "音源设置",
|
||||
"musicSourcesDesc": "选择音乐解析使用的音源平台",
|
||||
"musicSourcesWarning": "至少需要选择一个音源平台"
|
||||
}
|
||||
@@ -83,7 +83,9 @@ export default {
|
||||
unlimitedDownload: '无限制下载',
|
||||
unlimitedDownloadDesc: '开启后将无限制下载音乐(可能出现下载失败的情况), 默认限制 300 首',
|
||||
downloadPath: '下载目录',
|
||||
downloadPathDesc: '选择音乐文件的下载位置'
|
||||
downloadPathDesc: '选择音乐文件的下载位置',
|
||||
remoteControl: '远程控制',
|
||||
remoteControlDesc: '设置远程控制功能'
|
||||
},
|
||||
network: {
|
||||
apiPort: '音乐API端口',
|
||||
@@ -230,5 +232,15 @@ export default {
|
||||
disableAll: '已禁用所有快捷键,请记得保存',
|
||||
enableAll: '已启用所有快捷键,请记得保存'
|
||||
}
|
||||
},
|
||||
remoteControl: {
|
||||
title: '远程控制',
|
||||
enable: '启用远程控制',
|
||||
port: '服务端口',
|
||||
allowedIps: '允许的IP地址',
|
||||
addIp: '添加IP',
|
||||
emptyListHint: '空列表表示允许所有IP访问',
|
||||
saveSuccess: '远程控制设置已保存',
|
||||
accessInfo: '远程控制访问地址:',
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { loadLyricWindow } from './lyric';
|
||||
import { initializeConfig } from './modules/config';
|
||||
import { initializeFileManager } from './modules/fileManager';
|
||||
import { initializeFonts } from './modules/fonts';
|
||||
import { initializeRemoteControl } from './modules/remoteControl';
|
||||
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
||||
import { initializeStats, setupStatsHandlers } from './modules/statsService';
|
||||
import { initializeTray, updateCurrentSong, updatePlayState, updateTrayMenu } from './modules/tray';
|
||||
@@ -66,6 +67,9 @@ function initialize() {
|
||||
// 初始化快捷键
|
||||
initializeShortcuts(mainWindow);
|
||||
|
||||
// 初始化远程控制服务
|
||||
initializeRemoteControl(mainWindow);
|
||||
|
||||
// 初始化更新处理程序
|
||||
setupUpdateHandlers(mainWindow);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import os from 'os';
|
||||
import { getStore } from './config';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// 定义远程控制相关接口
|
||||
export interface RemoteControlConfig {
|
||||
enabled: boolean;
|
||||
port: number;
|
||||
allowedIps: string[];
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
export const defaultRemoteControlConfig: RemoteControlConfig = {
|
||||
enabled: false,
|
||||
port: 31888,
|
||||
allowedIps: []
|
||||
};
|
||||
|
||||
let app: express.Application | null = null;
|
||||
let server: any = null;
|
||||
let mainWindowRef: Electron.BrowserWindow | null = null;
|
||||
let currentSong: any = null;
|
||||
let isPlaying: boolean = false;
|
||||
|
||||
// 获取本地IP地址
|
||||
function getLocalIpAddresses(): string[] {
|
||||
const interfaces = os.networkInterfaces();
|
||||
const addresses: string[] = [];
|
||||
|
||||
for (const key in interfaces) {
|
||||
const iface = interfaces[key];
|
||||
if (iface) {
|
||||
for (const alias of iface) {
|
||||
if (alias.family === 'IPv4' && !alias.internal) {
|
||||
addresses.push(alias.address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
// 初始化远程控制服务
|
||||
export function initializeRemoteControl(mainWindow: Electron.BrowserWindow) {
|
||||
mainWindowRef = mainWindow;
|
||||
const store = getStore() as any;
|
||||
let config = store.get('remoteControl') as RemoteControlConfig;
|
||||
|
||||
// 如果配置不存在,使用默认配置
|
||||
if (!config) {
|
||||
config = defaultRemoteControlConfig;
|
||||
store.set('remoteControl', config);
|
||||
}
|
||||
|
||||
// 监听当前歌曲变化
|
||||
ipcMain.on('update-current-song', (_, song: any) => {
|
||||
currentSong = song;
|
||||
});
|
||||
|
||||
// 监听播放状态变化
|
||||
ipcMain.on('update-play-state', (_, playing: boolean) => {
|
||||
isPlaying = playing;
|
||||
});
|
||||
|
||||
// 监听远程控制配置变化
|
||||
ipcMain.on('update-remote-control-config', (_, newConfig: RemoteControlConfig) => {
|
||||
if (server) {
|
||||
stopServer();
|
||||
}
|
||||
|
||||
store.set('remoteControl', newConfig);
|
||||
|
||||
if (newConfig.enabled) {
|
||||
startServer(newConfig);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取远程控制配置
|
||||
ipcMain.handle('get-remote-control-config', () => {
|
||||
const config = store.get('remoteControl') as RemoteControlConfig;
|
||||
return config || defaultRemoteControlConfig;
|
||||
});
|
||||
|
||||
// 获取本地IP地址
|
||||
ipcMain.handle('get-local-ip-addresses', () => {
|
||||
return getLocalIpAddresses();
|
||||
});
|
||||
|
||||
// 如果启用了远程控制,启动服务器
|
||||
if (config.enabled) {
|
||||
startServer(config);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动远程控制服务器
|
||||
function startServer(config: RemoteControlConfig) {
|
||||
if (!mainWindowRef) {
|
||||
console.error('主窗口未初始化,无法启动远程控制服务');
|
||||
return;
|
||||
}
|
||||
|
||||
app = express();
|
||||
|
||||
// 跨域配置
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// IP 过滤中间件
|
||||
app.use((req, res, next) => {
|
||||
const clientIp = req.ip || req.socket.remoteAddress || '';
|
||||
const cleanIp = clientIp.replace(/^::ffff:/, ''); // 移除IPv6前缀
|
||||
console.log('config',config)
|
||||
if (config.allowedIps.length === 0 || config.allowedIps.includes(cleanIp)) {
|
||||
next();
|
||||
} else {
|
||||
res.status(403).json({ error: '未授权的IP地址' });
|
||||
}
|
||||
});
|
||||
|
||||
// 路由配置
|
||||
setupRoutes(app);
|
||||
|
||||
// 启动服务器
|
||||
try {
|
||||
server = app.listen(config.port, () => {
|
||||
console.log(`远程控制服务已启动,监听端口: ${config.port}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('启动远程控制服务失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 停止远程控制服务器
|
||||
function stopServer() {
|
||||
if (server) {
|
||||
server.close();
|
||||
server = null;
|
||||
app = null;
|
||||
console.log('远程控制服务已停止');
|
||||
}
|
||||
}
|
||||
|
||||
// 设置路由
|
||||
function setupRoutes(app: express.Application) {
|
||||
// 获取当前播放状态
|
||||
app.get('/api/status', (_, res) => {
|
||||
res.json({
|
||||
isPlaying,
|
||||
currentSong
|
||||
});
|
||||
});
|
||||
|
||||
// 播放/暂停
|
||||
app.post('/api/toggle-play', (_, res) => {
|
||||
if (!mainWindowRef) {
|
||||
return res.status(500).json({ error: '主窗口未初始化' });
|
||||
}
|
||||
mainWindowRef.webContents.send('global-shortcut', 'togglePlay');
|
||||
res.json({ success: true, message: '已发送播放/暂停指令' });
|
||||
});
|
||||
|
||||
// 上一首
|
||||
app.post('/api/prev', (_, res) => {
|
||||
if (!mainWindowRef) {
|
||||
return res.status(500).json({ error: '主窗口未初始化' });
|
||||
}
|
||||
mainWindowRef.webContents.send('global-shortcut', 'prevPlay');
|
||||
res.json({ success: true, message: '已发送上一首指令' });
|
||||
});
|
||||
|
||||
// 下一首
|
||||
app.post('/api/next', (_, res) => {
|
||||
if (!mainWindowRef) {
|
||||
return res.status(500).json({ error: '主窗口未初始化' });
|
||||
}
|
||||
mainWindowRef.webContents.send('global-shortcut', 'nextPlay');
|
||||
res.json({ success: true, message: '已发送下一首指令' });
|
||||
});
|
||||
|
||||
// 音量加
|
||||
app.post('/api/volume-up', (_, res) => {
|
||||
if (!mainWindowRef) {
|
||||
return res.status(500).json({ error: '主窗口未初始化' });
|
||||
}
|
||||
mainWindowRef.webContents.send('global-shortcut', 'volumeUp');
|
||||
res.json({ success: true, message: '已发送音量加指令' });
|
||||
});
|
||||
|
||||
// 音量减
|
||||
app.post('/api/volume-down', (_, res) => {
|
||||
if (!mainWindowRef) {
|
||||
return res.status(500).json({ error: '主窗口未初始化' });
|
||||
}
|
||||
mainWindowRef.webContents.send('global-shortcut', 'volumeDown');
|
||||
res.json({ success: true, message: '已发送音量减指令' });
|
||||
});
|
||||
|
||||
// 收藏/取消收藏
|
||||
app.post('/api/toggle-favorite', (_, res) => {
|
||||
if (!mainWindowRef) {
|
||||
return res.status(500).json({ error: '主窗口未初始化' });
|
||||
}
|
||||
mainWindowRef.webContents.send('global-shortcut', 'toggleFavorite');
|
||||
res.json({ success: true, message: '已发送收藏/取消收藏指令' });
|
||||
});
|
||||
|
||||
// 提供远程控制界面HTML
|
||||
app.get('/', (_, res) => {
|
||||
try {
|
||||
const htmlPath = path.join(process.cwd(), 'resources', 'html', 'remote-control.html');
|
||||
if (fs.existsSync(htmlPath)) {
|
||||
res.sendFile(htmlPath);
|
||||
} else {
|
||||
res.status(404).send('远程控制界面文件未找到');
|
||||
console.error('远程控制界面文件不存在:', htmlPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载远程控制界面失败:', error);
|
||||
res.status(500).send('加载远程控制界面失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
Vendored
+3
@@ -19,8 +19,10 @@ declare module 'vue' {
|
||||
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
|
||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
@@ -50,6 +52,7 @@ declare module 'vue' {
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NText: typeof import('naive-ui')['NText']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
|
||||
@@ -168,7 +168,6 @@ import {
|
||||
setBackgroundImg
|
||||
} from '@/utils';
|
||||
import { getArtistDetail } from '@/api/artist';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const playerStore = usePlayerStore();
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDateFormat, useThrottleFn } from '@vueuse/core';
|
||||
import { useDateFormat } from '@vueuse/core';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
@@ -271,6 +271,16 @@
|
||||
}}</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">{{ t('settings.application.remoteControl') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.application.remoteControlDesc') }}</div>
|
||||
</div>
|
||||
<n-button size="small" @click="showRemoteControlModal = true">{{
|
||||
t('common.configure')
|
||||
}}</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -548,6 +558,9 @@
|
||||
</div>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
|
||||
<!-- 远程控制设置弹窗 -->
|
||||
<remote-control-setting v-model:visible="showRemoteControlModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -569,6 +582,7 @@ import { useUserStore } from '@/store/modules/user';
|
||||
import { isElectron, isMobile } from '@/utils';
|
||||
import { openDirectory, selectDirectory } from '@/utils/fileOperation';
|
||||
import { checkUpdate, UpdateResult } from '@/utils/update';
|
||||
import RemoteControlSetting from '@/views/setting/ServerSetting.vue';
|
||||
|
||||
import config from '../../../../package.json';
|
||||
|
||||
@@ -1096,6 +1110,9 @@ const getSourceLabel = (source: Platform) => {
|
||||
const sourceLabel = musicSourceOptions.value.find(s => s.value === source)?.label;
|
||||
return sourceLabel || source;
|
||||
};
|
||||
|
||||
// 远程控制设置弹窗
|
||||
const showRemoteControlModal = ref(false);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="visible"
|
||||
preset="card"
|
||||
:title="t('settings.remoteControl.title')"
|
||||
class="remote-control-modal"
|
||||
style="max-width: 650px; width: 100%"
|
||||
>
|
||||
<n-scrollbar>
|
||||
<div class="remote-control-setting">
|
||||
<n-form label-placement="left" label-width="auto" :style="{ maxWidth: '640px' }">
|
||||
<n-form-item :label="t('settings.remoteControl.enable')">
|
||||
<n-switch v-model:value="remoteControlConfig.enabled" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item :label="t('settings.remoteControl.port')">
|
||||
<n-input-number
|
||||
v-model:value="remoteControlConfig.port"
|
||||
:min="1024"
|
||||
:max="65535"
|
||||
:disabled="!remoteControlConfig.enabled"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item :label="t('settings.remoteControl.allowedIps')">
|
||||
<div class="allowed-ips-container">
|
||||
<div v-for="(_, index) in remoteControlConfig.allowedIps" :key="index" class="ip-item">
|
||||
<n-input v-model:value="remoteControlConfig.allowedIps[index]" :disabled="!remoteControlConfig.enabled" />
|
||||
<n-button
|
||||
quaternary
|
||||
circle
|
||||
type="error"
|
||||
:disabled="!remoteControlConfig.enabled"
|
||||
@click="removeIp(index)"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><i class="ri-delete-bin-line"></i></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
<n-button
|
||||
secondary
|
||||
size="small"
|
||||
:disabled="!remoteControlConfig.enabled"
|
||||
@click="addIp"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><i class="ri-add-line"></i></n-icon>
|
||||
</template>
|
||||
{{ t('settings.remoteControl.addIp') }}
|
||||
</n-button>
|
||||
<n-text depth="3" size="small" class="allow-all-hint">
|
||||
{{ t('settings.remoteControl.emptyListHint') }}
|
||||
</n-text>
|
||||
</div>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!remoteControlConfig.enabled"
|
||||
@click="saveConfig"
|
||||
>
|
||||
{{ t('common.save') }}
|
||||
</n-button>
|
||||
<n-button @click="resetConfig">
|
||||
{{ t('common.reset') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
|
||||
<n-collapse-transition :show="remoteControlConfig.enabled">
|
||||
<div class="remote-info">
|
||||
<n-alert type="info">
|
||||
<template #icon>
|
||||
<n-icon><i class="ri-information-line"></i></n-icon>
|
||||
</template>
|
||||
<p>{{ t('settings.remoteControl.accessInfo') }}</p>
|
||||
<div class="access-url">
|
||||
<n-tag type="success">
|
||||
http://localhost:{{ remoteControlConfig.port }}/
|
||||
</n-tag>
|
||||
</div>
|
||||
<div v-if="localIpAddresses.length" class="local-ips">
|
||||
<div v-for="ip in localIpAddresses" :key="ip" class="ip-address">
|
||||
<n-tag type="info">
|
||||
http://{{ ip }}:{{ remoteControlConfig.port }}/
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</n-alert>
|
||||
</div>
|
||||
</n-collapse-transition>
|
||||
</n-form>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
|
||||
// 控制弹窗显示的属性
|
||||
const visible = defineModel('visible', { default: false });
|
||||
|
||||
// 默认配置
|
||||
const defaultConfig:{
|
||||
enabled: boolean,
|
||||
port: number,
|
||||
allowedIps: string[]
|
||||
} = {
|
||||
enabled: false,
|
||||
port: 31888,
|
||||
allowedIps: []
|
||||
};
|
||||
|
||||
// 远程控制配置
|
||||
const remoteControlConfig = ref({...defaultConfig});
|
||||
|
||||
// 本地IP地址列表
|
||||
const localIpAddresses = ref<string[]>([]);
|
||||
|
||||
// 获取本地IP地址
|
||||
const getLocalIpAddresses = () => {
|
||||
if (window.electron) {
|
||||
window.electron.ipcRenderer.invoke('get-local-ip-addresses').then((ips: string[]) => {
|
||||
localIpAddresses.value = ips;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 添加IP地址
|
||||
const addIp = () => {
|
||||
remoteControlConfig.value.allowedIps.push('');
|
||||
};
|
||||
|
||||
// 删除IP地址
|
||||
const removeIp = (index: number) => {
|
||||
remoteControlConfig.value.allowedIps.splice(index, 1);
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = () => {
|
||||
// 过滤空IP
|
||||
remoteControlConfig.value.allowedIps = remoteControlConfig.value.allowedIps.filter(ip => ip.trim() !== '');
|
||||
|
||||
if (window.electron) {
|
||||
window.electron.ipcRenderer.send('update-remote-control-config', cloneDeep(remoteControlConfig.value));
|
||||
message.success(t('settings.remoteControl.saveSuccess'));
|
||||
}
|
||||
};
|
||||
|
||||
// 重置配置
|
||||
const resetConfig = () => {
|
||||
if (window.electron) {
|
||||
window.electron.ipcRenderer.invoke('get-remote-control-config').then((config) => {
|
||||
if (config) {
|
||||
remoteControlConfig.value = config;
|
||||
} else {
|
||||
remoteControlConfig.value = { ...defaultConfig };
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时,获取当前配置
|
||||
onMounted(async () => {
|
||||
if (window.electron) {
|
||||
try {
|
||||
const config = await window.electron.ipcRenderer.invoke('get-remote-control-config');
|
||||
if (config) {
|
||||
remoteControlConfig.value = config;
|
||||
}
|
||||
// 获取本地IP地址
|
||||
getLocalIpAddresses();
|
||||
} catch (error) {
|
||||
console.error('获取远程控制配置失败:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.remote-control-setting {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.allowed-ips-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
|
||||
.ip-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.allow-all-hint {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.remote-info {
|
||||
margin-top: 16px;
|
||||
|
||||
.access-url {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.local-ips {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user