feat(前端):网络管理模块

master
shaot 2025-08-29 17:51:17 +08:00
parent dc54b303b7
commit 865dcdae6d
166 changed files with 52926 additions and 2327 deletions

View File

@ -19,13 +19,17 @@ export default defineConfig({
path: '/login',
component: '@/pages/login',
},
{
path: '/vncClient',
component: '@/pages/vncClient',
},
{
path: '/',
component: '@/pages/components/Layout/index',
routes: [
{
path: '/images',
component: '@/pages/images',
component: '@/pages/imagePage',
},
{
path: '/profile',
@ -39,17 +43,26 @@ export default defineConfig({
path: '/terminal',
component: '@/pages/terminal',
},
{
path: '/network',
component: '@/pages/network',
},
{
path: '/storage',
component: '@/pages/storage',
},
],
},
],
npmClient: 'pnpm',
proxy: {
'/api/nex/v1/': {
target: 'http://10.100.51.85:8113',
target: 'http://192.168.2.224:8113',
// changeOrigin: true,
},
'/api/files': {
target: 'http://10.100.51.85:8113',
target: 'http://10.100.51.86:8113',
},
},
});

View File

@ -1,4 +1,108 @@
export default {
'POST /api/nex/v1/desktopImages/query': (req: any, res: any) => {
const { page_size, page_num } = req.body;
const data = [];
for (let i = 1; i <= page_size; i++) {
const id = (page_num - 1) * page_size + i;
data.push({
id: id,
image_name: `桌面镜像${id}`,
parent_image: Math.floor(Math.random() * 5) + 1,
cpu: '4核',
memory: '8GB',
image_version: `v1.0.${Math.floor(Math.random() * 10)}`,
os_version: id % 3 === 0 ? 'Windows 11' : id % 3 === 1 ? 'Windows 10' : 'Ubuntu 22.04',
image_status: Math.random() > 0.2 ? 1 : 2,
create_time: new Date(Date.now() - Math.floor(Math.random() * 30 * 24 * 60 * 60 * 1000)).toISOString(),
description: `这是桌面镜像${id}的描述信息`,
desktopType: id % 2 === 0 ? 'standard' : 'custom',
version: `1.0.${Math.floor(Math.random() * 10)}`,
size: `${Math.floor(Math.random() * 50) + 20}GB`,
status: Math.random() > 0.1 ? 'active' : Math.random() > 0.5 ? 'inactive' : 'building',
file_name: `desktop_image_${id}.qcow2`,
file_type: id % 2 === 0 ? '中型虚拟机' : '小型虚拟机',
file_size: `${Math.floor(Math.random() * 10) + 1}TB`,
desc: `这是测试的桌面镜像${id}`,
});
}
const result = {
code: '200',
message: '操作成功',
data: {
total: 50,
page_num: page_num,
page_size: page_size,
data: data,
},
};
setTimeout(() => {
res.send(result);
}, 300);
},
'POST /api/nex/v1/virtualImages/query': (req: any, res: any) => {
const { page_size, page_num } = req.body;
const data = [];
for (let i = 1; i <= page_size; i++) {
const id = (page_num - 1) * page_size + i;
data.push({
id: id,
image_name: `工具${id}`,
description: `这是工具${id}的描述信息`,
image_system_id: '1',
image_system_name: '系统镜像1',
os_version: 'Windows 10',
storage_path: '/path/to/image',
network_module: 'Ethernet',
image_status: 'active',
});
}
const result = {
code: '200',
message: '操作成功',
data: {
total: 100,
page_num: page_num,
page_size: page_size,
data: data,
},
};
setTimeout(() => {
res.send(result);
}, 300);
},
'POST /api/nex/v1/tool/select/page': (req: any, res: any) => {
const { page_size, page_num } = req.body;
const data = [];
for (let i = 1; i <= page_size; i++) {
const id = (page_num - 1) * page_size + i;
data.push({
id: id,
tool_name: `工具${id}`,
tool_type: Math.random() > 0.5 ? 'system' : Math.random() > 0.5 ? 'virtual' : 'desktop',
file_size: `${Math.floor(Math.random() * 1000) + 100}MB`,
version: `1.0.${Math.floor(Math.random() * 10)}`,
create_time: new Date().toISOString(),
description: `这是工具${id}的描述信息`,
});
}
const result = {
code: '200',
message: '操作成功',
data: {
total: 100,
page_num: page_num,
page_size: page_size,
data: data,
},
};
setTimeout(() => {
res.send(result);
}, 300);
},
'POST /api/nex/v1/queryimagesList': (req: any, res: any) => {
const { page_size, page_num } = req.body;
const data = [];
@ -90,4 +194,45 @@ export default {
});
}
},
// 'POST /api/nex/v1/network/select/page': (req: any, res: any) => {
// const { page_size, page_num } = req.body;
// const data = [];
// const networkTypes = ['NAT', 'Isolated', 'Bridge'];
// for (let i = 1; i <= page_size; i++) {
// const id = (page_num - 1) * page_size + i;
// const networkType = networkTypes[Math.floor(Math.random() * networkTypes.length)];
// data.push({
// id: id,
// network_name: `网络${id}`,
// bridge_name: `br${id}`,
// network_type: networkType,
// ip_range: networkType !== 'Bridge' ? `192.168.${id}.0/24` : null,
// gateway: networkType !== 'Bridge' ? `192.168.${id}.1` : null,
// subnet_mask: networkType !== 'Bridge' ? '255.255.255.0' : null,
// dhcp_start: networkType !== 'Bridge' ? `192.168.${id}.100` : null,
// dhcp_end: networkType !== 'Bridge' ? `192.168.${id}.200` : null,
// dhcp_enabled: networkType !== 'Bridge' ? Math.random() > 0.3 : null,
// auto_start: Math.random() > 0.5,
// create_time: new Date(Date.now() - Math.floor(Math.random() * 30 * 24 * 60 * 60 * 1000)).toISOString(),
// description: `这是网络${id}的描述信息`,
// });
// }
// const result = {
// code: '200',
// message: '操作成功',
// data: {
// total: 50,
// page_num: page_num,
// page_size: page_size,
// data: data,
// },
// };
// setTimeout(() => {
// res.send(result);
// }, 300);
// },
};

View File

@ -1,5 +1,5 @@
export default {
'POST /api/v1/terminal/query/devicelist': (req: any, res: any) => {
'POST /api/nex/v1/device/select/page': (req: any, res: any) => {
const { page_size, page_num } = req.body;
const data = [];
@ -18,16 +18,11 @@ export default {
});
}
const result = {
error_code: '0000000000',
code: '200',
message: '操作成功',
data: {
paging: {
total: 520,
total_num: 520,
page_num: page_num,
page_size: page_size,
},
data: data,
total: data.length,
},
};
setTimeout(() => {
@ -69,8 +64,11 @@ export default {
const result = {
error_code: '0000000000',
message: '操作成功',
data: {
data: {
data: data,
total: data.length,
},
},
};
setTimeout(() => {

18585
web-fe/package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

View File

@ -12,10 +12,12 @@
"dependencies": {
"@ant-design/icons": "^5.0.1",
"@ant-design/pro-components": "^2.4.4",
"@novnc/novnc": "^1.6.0",
"@umijs/max": "^4.4.11",
"antd": "^5.4.0",
"dayjs": "^1.11.13",
"moment": "^2.30.1",
"novnc": "^1.2.0",
"spark-md5": "^3.0.2",
"uuid": "^11.1.0"
},

View File

@ -54,5 +54,37 @@ export const PRIOPRITY_OPTIONS = [
{ value: 3, label: '三级' },
];
export const DHCP_STATUS = {
1: '启用',
0: '禁用',
} as const;
export const NETWORK_STATUS = {
1: '活跃',
0: '非活跃',
} as const;
export const NETWORK_TYPE = {
nat: 'NAT(网络地址转换)',
bridge: 'Bridge(桥接)',
isolated: 'Isolated(隔离)',
} as const;
export const NETWORK_TYPE_LIST = [
{ value: "nat", label: 'NAT(网络地址转换)' },
{ value: 'bridge', label: 'Bridge(桥接)' },
{ value: 'isolated', label: 'Isolated(隔离)' },
];
export const NETWORK_STATUS_LIST = [
{ value: 1, label: '活跃' },
{ value: 0, label: '非活跃' },
];
export const DEFAULT_PASSWORD="a123456"
export const DEFAULT_BLICK_TAB="黑名单"
export const CPU_TOTAL=16
export const CPU_CORE_TOTAL=8

View File

@ -45,6 +45,8 @@ const MainLayout: React.FC = () => {
if (path.startsWith('/terminal')) return 'terminal';
if (path.startsWith('/images')) return 'images';
if (path.startsWith('/profile')) return 'profile';
if (path.startsWith('/network')) return 'network';
if (path.startsWith('/storage')) return 'storage';
return 'images'; // 默认选中镜像列表
};
@ -102,6 +104,12 @@ const MainLayout: React.FC = () => {
</Menu.Item>
<Menu.Item key="images" icon={<AppstoreOutlined />}>
</Menu.Item>
<Menu.Item key="network" icon={<AppstoreOutlined />}>
</Menu.Item>
<Menu.Item key="storage" icon={<AppstoreOutlined />}>
</Menu.Item>
<Menu.Item key="profile" icon={<UserOutlined />}>

View File

@ -0,0 +1,118 @@
import { Col, Form, Input, InputNumber, Modal, Row, Select } from 'antd';
// import { InputNumber } from 'antd/lib';
import React from 'react';
const { Option } = Select;
interface EditModalProps {
visible: boolean;
detialData: any;
onCancel: () => void;
onOk: (values: any) => void;
}
const EditModal: React.FC<EditModalProps> = ({
detialData,
visible,
onCancel,
onOk,
}) => {
const [form] = Form.useForm();
const handleOk = () => {
form
.validateFields()
.then((values) => {
onOk(values);
})
.catch((error) => {
console.error('表单验证失败:', error);
});
};
return (
<Modal
title="编辑信息"
open={visible}
onCancel={onCancel}
onOk={handleOk}
okText="确定"
cancelText="取消"
>
<Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
{/* 名称字段 */}
<Form.Item
name="name"
label="名称"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input />
</Form.Item>
{/* CPU 字段 */}
<Form.Item label="CPU">
<Row gutter={16}>
<Col span={12}>
<Form.Item
name={['cpu', 'size']}
rules={[{ required: true, message: '请输入CPU大小' }]}
noStyle
>
<InputNumber
precision={0}
addonAfter={<span></span>}
style={{ width: '190px' }}
placeholder="CPU大小"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name={['cpu', 'cores']}
rules={[{ required: true, message: '请输入CPU内核数' }]}
noStyle
>
<InputNumber
precision={0}
addonAfter={<span></span>}
style={{ width: '190px' }}
placeholder="CPU内核数"
/>
</Form.Item>
</Col>
</Row>
</Form.Item>
{/* 内存字段 */}
<Form.Item
name="memory_total"
label="内存"
rules={[{ required: true, message: '请输入内存' }]}
>
<InputNumber
precision={0}
addonAfter={<span>CB</span>}
style={{ width: '393.33px' }}
placeholder="内存"
/>
</Form.Item>
{/* 存储卷字段 */}
<Form.Item
name="storageVolume"
label="存储卷"
rules={[{ required: true, message: '请输入存储卷信息' }]}
>
<Input disabled />
</Form.Item>
{/* 描述字段 */}
<Form.Item name="description" label="描述">
<Input.TextArea />
</Form.Item>
</Form>
</Modal>
);
};
export default EditModal;

View File

@ -0,0 +1,138 @@
import type { TableProps } from 'antd';
import { useCallback, useState } from 'react';
const useTableParams = (
initialParams: DESK.DeskTableParams = {
pagination: { current: 1, pageSize: 10 },
filters: {},// 表格的搜索对象
sort: {},
search: {}, // 添加搜索参数对象
},
) => {
const [tableParams, setTableParams] =
useState<DESK.DeskTableParams>(initialParams);
const getApiParams = useCallback(() => {
const { pagination, filters, sort, search, ...rest } = tableParams;
const apiParams: Record<string, any> = {
page_size: pagination?.pageSize,
page_num: pagination?.current,
...rest,
};
if (sort?.field) {
apiParams.orderby = sort.field;
apiParams.order = sort.order === 'ascend' ? 'asc' : 'desc';
}
// 处理表格搜索参数
Object.entries(filters || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
apiParams[key] = value;
}
});
// 处理搜索参数
Object.entries(search || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
apiParams[key] = value;
}
});
console.log('getApiParams apiParams', apiParams);
return apiParams;
}, [tableParams]);
// 统一的更新方法,可以处理所有参数类型
const updateParams = useCallback(
(
newParams: Partial<DESK.DeskTableParams>,
options?: { resetPage?: boolean },
) => {
// console.log('updateParams', newParams);
setTableParams((prev) => {
// 如果是搜索或过滤相关的更新,重置到第一页
const shouldResetPage =
options?.resetPage ??
((newParams.search && Object.keys(newParams.search).length > 0) || // 有搜索值
(newParams.filters && Object.keys(newParams.filters).length > 0)); // 有过滤值
return {
...prev,
...newParams,
pagination: {
...prev.pagination,
...newParams.pagination,
...(shouldResetPage ? { current: 1 } : {}), // 根据条件决定是否重置页码
},
};
});
},
[],
);
/**
*
* @param pagination
* @param filters filters
* @param sorter
* @param extra
* @returns void
* */
const handleTableChange = useCallback<
NonNullable<TableProps<DESK.DeskItem>['onChange']>
>(
(pagination, filters, sorter) => {
// console.log('handleTableChange',pagination,filters,sorter,extra);
// 过滤掉空值的filters
const filteredFilters: Record<string, any> = {};
Object.entries(filters || {}).forEach(([key, value]) => {
if (key === 'image_type') {
if (Array.isArray(value) && value.length > 0 && value[0] !== '全部') {
filteredFilters[key] = Number(value[0]);
}
} else {
if (Array.isArray(value) && value.length > 0) {
filteredFilters[key] = value[0];
} else if (value !== undefined && value !== null) {
if (!Array.isArray(value) && value !== '') {
filteredFilters[key] = value;
}
}
}
});
const newParams: Partial<DESK.DeskTableParams> = {
pagination: {
current: pagination.current || 1,
pageSize: pagination.pageSize || 10,
},
filters: filteredFilters,
};
if (!Array.isArray(sorter)) {
newParams.sort = {
field: sorter.field as string,
order:
sorter.order === 'ascend' || sorter.order === 'descend'
? sorter.order
: undefined,
};
}
// console.log('handleTableChange', newParams);
updateParams(newParams);
},
[updateParams],
);
return {
tableParams,
getApiParams,
updateParams, // 统一的更新方法
handleTableChange,
};
};
export default useTableParams;

View File

@ -0,0 +1,172 @@
// 页面头部样式
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
}
// 镜像列表样式
.image-list {
width:100%;
height: 100%;
display: flex;
flex-direction: column;
// padding: 16px;
box-sizing: border-box;
.search-box {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
.search-input {
display: flex;
gap: 8px;
align-items: center;
}
}
.images-list-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.images-list-table {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
// 表格适应样式
.ant-table-wrapper {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-spin-nested-loading {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-spin-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-table {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-container {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-header {
flex-shrink: 0;
}
.ant-table-body {
flex: 1;
overflow: auto !important;
}
}
// 确保分页器在底部正确显示
.ant-table-pagination {
flex-shrink: 0;
// 确保分页器始终可见
position: relative;
z-index: 1;
}
}
}
}
}
}
}
.image-detail {
.detail-item {
margin-bottom: 16px;
label {
font-weight: 600;
color: #333;
display: inline-block;
width: 100px;
}
span {
color: #666;
}
p {
margin: 8px 0 0 100px;
color: #666;
line-height: 1.6;
}
}
}
}
// 个人资料样式
.profile-page {
.profile-content {
.profile-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
.profile-info {
h3 {
margin: 0 0 4px 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
p {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
.quick-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.profile-content {
.profile-header {
flex-direction: column;
text-align: center;
}
.quick-actions {
justify-content: center;
}
}
}

View File

@ -0,0 +1,482 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { CODE } from '@/constants/images.constants';
import { SettingOutlined } from '@ant-design/icons';
import {
Button,
Checkbox,
Input,
message,
Modal,
Popconfirm,
Popover,
Space,
Table,
Tooltip,
} from 'antd';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import EditModal from './editModal';
import useTableParams from './hook/hook';
import './index.less';
import { getDesktopImagesList,deleteDesktopImages } from '@/services/imagePage';
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
// 列配置定义
type ColumnConfig = {
key: string;
title: string;
dataIndex?: string;
width: number;
render?: (text: any, record: any, index: number) => React.ReactNode;
fixed?: 'left' | 'right';
align?: 'left' | 'center' | 'right';
defaultVisible: boolean; // 默认是否显示
alwaysVisible?: boolean; // 始终显示的列
ellipsis?: boolean; // 是否启用省略号
filters?: { text: string; value: string }[];
filterMultiple?: boolean; // 是否多选过滤
filterDropdown?: (props: any) => React.ReactNode;
defaultFilteredValue?: string[]; // 默认过滤值
onFilter?: (value: string, record: any) => boolean;
};
type TableColumn = {
title: string;
dataIndex?: string;
key: string;
width: number;
render?: any;
fixed?: 'left' | 'right';
hidden?: boolean;
};
// 在组件顶部添加防抖函数(支持取消)
// 增强版防抖函数,使用泛型明确函数类型
const debounce = <T extends (...args: any[]) => any>(
func: T,
delay: number,
immediate = false,
) => {
let timer: NodeJS.Timeout | null = null;
const debounced = (...args: Parameters<T>) => {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
func(...args);
}
timer = setTimeout(() => {
if (!immediate) {
func(...args);
}
timer = null;
}, delay);
};
debounced.cancel = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
return debounced;
};
const Index: React.FC<DESK.ImagesProps> = (props) => {
const { activeTabKey } = props;
const [dataSource, setDataSource] = useState<DESK.DeskItem[]>([]);
const [loading, setLoading] = useState(false);
const [selectedImage, setSelectedImage] = useState<any>(null);
const [visible, setDetailVisible] = useState(false);
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
const { tableParams, getApiParams, updateParams, handleTableChange } =
useTableParams({
pagination: {
current: 1,
pageSize: 10,
},
search: {}, // 初始化搜索参数
});
// 在组件顶部添加一个 ref 来保存最新的 tableParams
const tableParamsRef = useRef(tableParams);
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
// 表格参数变化 获取镜像列表
useEffect(() => {
if (activeTabKey === '3') {
loadImages();
}
}, [
tableParams.pagination?.current,
tableParams.pagination?.pageSize,
tableParams?.sortOrder,
tableParams?.sortField,
JSON.stringify(tableParams.filters), // 表格搜索参数
JSON.stringify(tableParams.search), // 搜索参数依赖
activeTabKey,
]);
// 定义所有列的配置
const columnConfigs: ColumnConfig[] = [
{
key: 'index',
title: '序号',
width: 60,
render: (text: any, row: any, index: number) =>
(tableParams.pagination?.current - 1) *
tableParams.pagination?.pageSize +
index +
1,
defaultVisible: true,
alwaysVisible: true,
},
{
key: 'file_name',
title: '系统镜像',
dataIndex: 'file_name',
width: 150,
defaultVisible: true,
alwaysVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'file_type',
title: '虚拟机规格',
dataIndex: 'file_type',
width: 120,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'os_version',
title: '操作系统',
dataIndex: 'os_version',
width: 150,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'desc',
// title: '模板存放路径',
title: '描述',
dataIndex: 'desc',
width: 140,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'action',
title: '操作',
width: 90,
align: 'center',
fixed: 'right' as 'right',
render: (_: any, record: IMAGES.ImageItem) => (
<Space size="small">
<Button
size="small"
type="link"
title="编辑"
onClick={() => handleViewDetail(record)}
>
</Button>
<Popconfirm
title="确定要删除这个镜像吗?"
description="删除后无法恢复,请谨慎操作。"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button size="small" type="link" title="删除">
</Button>
</Popconfirm>
</Space>
),
defaultVisible: true,
},
];
// 初始化 visibleColumns 状态
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
(acc, column) => {
if (!column.alwaysVisible) {
acc[column.key] = column.defaultVisible;
}
return acc;
},
{},
);
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
initialVisibleColumns,
);
// 重置列设置
const resetColumns = () => {
setVisibleColumns(initialVisibleColumns);
};
const loadImages = async () => {
setLoading(true);
try {
// 将搜索文本合并到API参数中
const apiParams = {
...getApiParams(),
};
const imagesRes = await getDesktopImagesList(apiParams);
if (imagesRes.code === CODE) {
setDataSource(imagesRes.data?.data || []);
setLoading(false);
// 正确处理后端返回的分页信息
updateParams({
pagination: {
...tableParams.pagination,
current: imagesRes.data?.page_num || 1,
total: imagesRes.data?.total || 0,
pageSize: tableParams.pagination?.pageSize || 10,
},
});
} else {
message.error(imagesRes.message || '获取镜像列表失败');
setLoading(false);
}
} catch (err) {
message.error('获取镜像列表失败');
setLoading(false);
}
};
const handleViewDetail = (record: DESK.DeskItem) => {
setSelectedImage(record);
setDetailVisible(true);
};
const handleDelete = (record: DESK.DeskItem) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除镜像 "${record.image_name}" 吗?`,
onOk: () => {
deleteDesktopImages({ id: record.id }).then((res) => {
if (res.code === CODE) {
message.success('删除成功');
loadImages();
} else {
message.error(res.message || '删除失败');
}
});
},
});
};
// 列设置相关函数
const handleColumnChange = (columnKey: string, checked: boolean) => {
setVisibleColumns((prev) => ({
...prev,
[columnKey]: checked,
}));
};
// 列设置内容
const columnSettingsContent = (
<div style={{ padding: '8px 0' }}>
{columnConfigs
.filter((config) => !config.alwaysVisible) // 只显示可控制的列
.map((config) => (
<div key={config.key} style={{ padding: '4px 12px' }}>
<Checkbox
checked={visibleColumns[config.key]}
onChange={(e) => handleColumnChange(config.key, e.target.checked)}
>
{config.title}
</Checkbox>
</div>
))}
<div
style={{
padding: '8px 12px',
borderTop: '1px solid #f0f0f0',
marginTop: 8,
}}
>
<Button type="link" onClick={resetColumns} style={{ padding: 0 }}>
</Button>
</div>
</div>
);
// 根据visibleColumns过滤显示的列
const filteredColumns = columnConfigs
.map((config) => {
// 对于始终显示的列
if (config.alwaysVisible) {
return {
...config,
hidden: undefined,
};
}
// 对于可控制显示/隐藏的列
return {
...config,
...(visibleColumns[config.key] ? {} : { hidden: true }),
};
})
.filter((column) => !column.hidden) as TableColumn[];
const handleRefresh = () => {
loadImages();
};
// 自定义分页配置
const paginationConfig = {
...tableParams.pagination,
showTotal: (total: number) => `${total} 条记录`,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
};
const handleSearch = useCallback(
(searchValue: string) => {
const currentTableParams = tableParamsRef.current;
updateParams({
search: {
image_name: searchValue,
},
pagination: {
current: 1,
pageSize: currentTableParams.pagination?.pageSize || 10,
},
});
},
[updateParams],
);
// 防抖版本500ms延迟不立即执行
const debouncedSearch = useRef(debounce(handleSearch, 500)).current;
// 立即执行版本(用于清空时立即搜索)
const immediateSearch = useRef(debounce(handleSearch, 0, true)).current;
const handleSearchChange = (value: string) => {
setSearchText(value);
// 取消所有未执行的防抖请求
debouncedSearch.cancel();
immediateSearch.cancel();
// 清空时立即触发搜索
if (value === '') {
immediateSearch('');
return;
}
// 正常输入时使用防抖
debouncedSearch(value);
};
// 修改回车搜索处理
const handleEnterSearch = (value: string) => {
// 回车搜索时取消未执行的防抖
debouncedSearch.cancel();
immediateSearch.cancel();
// 直接执行搜索
handleSearch(value);
};
return (
<div className="image-list">
<div className="search-box">
<div>{/* */}</div>
<div className="search-input">
<Input.Search
placeholder="请输入名称"
value={searchText}
onChange={(e) => handleSearchChange(e.target.value)}
style={{ width: 300 }}
onSearch={handleEnterSearch}
/>
<Button
onClick={handleRefresh}
loading={loading}
icon={<RefreshIcon style={{ width: 13, height: 13 }} />}
></Button>
<Popover
content={columnSettingsContent}
title="列设置"
trigger="click"
open={columnSettingsVisible}
onOpenChange={setColumnSettingsVisible}
placement="bottomRight"
>
<Button icon={<SettingOutlined />}></Button>
</Popover>
</div>
</div>
<div className="images-list-container">
<div className="images-list-table">
<Table
columns={filteredColumns}
dataSource={dataSource}
rowKey="id"
loading={loading}
pagination={paginationConfig}
onChange={handleTableChange}
scroll={{
y: 'max-content', // 关键:允许内容决定高度
}}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
/>
</div>
</div>
{visible && (
<EditModal
visible={visible}
detialData={selectedImage}
onCancel={() => {
setDetailVisible(false);
}}
onOk={() => {
setDetailVisible(false);
}}
/>
)}
</div>
);
};
export default Index;

View File

@ -0,0 +1,44 @@
.imagePage {
height: 100%;
display: flex;
flex-direction: column;
background-color: #fafafa;
}
.content {
flex: 1;
padding: 10px;
// background-color: #fafafa;
background-color: #fff;
overflow: auto;
}
.tabContent {
// padding: 24px;
background-color: #fff;
border-radius: 6px;
min-height: 400px;
}
.emptyTip {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
font-size: 14px;
}
/* 覆盖Ant Design Tabs组件的默认样式 */
.ant-tabs {
background-color: #fff;
border-radius: 6px;
}
.ant-tabs-nav {
padding: 0 24px;
}
.ant-tabs-content {
padding-top: 0;
}

View File

@ -0,0 +1,85 @@
import ToolTable from '@/pages/imagePage/tool';
import Images from '@/pages/images';
import type { TabsProps } from 'antd';
import { Tabs } from 'antd';
import React, { useState } from 'react';
import DeskImage from './deskImage';
import styles from './index.less';
import VirtualImages from './virtualImages';
const ImagePage = () => {
// 设置默认选中的Tab为系统镜像索引为0
const [activeTabKey, setActiveTabKey] = useState<string>('1');
// 处理Tab切换事件
const handleTabChange = (key: string) => {
setActiveTabKey(key);
};
// Tabs的items配置
const items: TabsProps['items'] = [
{
key: '1',
label: '系统镜像',
children: (
<div className={styles.tabContent}>
{/* 系统镜像内容区域 */}
<div className={styles.emptyTip}>
<Images activeTabKey={activeTabKey} />
</div>
</div>
),
},
{
key: '2',
label: '虚拟机镜像',
children: (
<div className={styles.tabContent}>
{/* 虚拟机镜像内容区域 */}
<div className={styles.emptyTip}>
<VirtualImages activeTabKey={activeTabKey} />
</div>
</div>
),
},
{
key: '3',
label: '桌面镜像',
children: (
<div className={styles.tabContent}>
{/* 桌面镜像内容区域 */}
<div className={styles.emptyTip}>
<DeskImage activeTabKey={activeTabKey} />
</div>
</div>
),
},
{
key: '4',
label: '工具',
children: (
<div className={styles.tabContent}>
{/* 桌面镜像内容区域 */}
<div className={styles.emptyTip}>
<ToolTable activeTabKey={activeTabKey} />
</div>
</div>
),
},
];
return (
<div className={styles.imagePage}>
<div className={styles.content}>
<Tabs
activeKey={activeTabKey}
onChange={handleTabChange}
items={items}
size="large"
/>
</div>
</div>
);
};
export default ImagePage;

View File

@ -0,0 +1,138 @@
import type { TableProps } from 'antd';
import { useCallback, useState } from 'react';
const useTableParams = (
initialParams: TOOL.TableParams = {
pagination: { current: 1, pageSize: 10 },
filters: {},// 表格的搜索对象
sort: {},
search: {}, // 添加搜索参数对象
},
) => {
const [tableParams, setTableParams] =
useState<TOOL.TableParams>(initialParams);
const getApiParams = useCallback(() => {
const { pagination, filters, sort, search, ...rest } = tableParams;
const apiParams: Record<string, any> = {
page_size: pagination?.pageSize,
page_num: pagination?.current,
...rest,
};
if (sort?.field) {
apiParams.orderby = sort.field;
apiParams.order = sort.order === 'ascend' ? 'asc' : 'desc';
}
// 处理表格搜索参数
Object.entries(filters || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
apiParams[key] = value;
}
});
// 处理搜索参数
Object.entries(search || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
apiParams[key] = value;
}
});
console.log('getApiParams apiParams', apiParams);
return apiParams;
}, [tableParams]);
// 统一的更新方法,可以处理所有参数类型
const updateParams = useCallback(
(
newParams: Partial<TOOL.TableParams>,
options?: { resetPage?: boolean },
) => {
// console.log('updateParams', newParams);
setTableParams((prev) => {
// 如果是搜索或过滤相关的更新,重置到第一页
const shouldResetPage =
options?.resetPage ??
((newParams.search && Object.keys(newParams.search).length > 0) || // 有搜索值
(newParams.filters && Object.keys(newParams.filters).length > 0)); // 有过滤值
return {
...prev,
...newParams,
pagination: {
...prev.pagination,
...newParams.pagination,
...(shouldResetPage ? { current: 1 } : {}), // 根据条件决定是否重置页码
},
};
});
},
[],
);
/**
*
* @param pagination
* @param filters filters
* @param sorter
* @param extra
* @returns void
* */
const handleTableChange = useCallback<
NonNullable<TableProps<TOOL.ToolItem>['onChange']>
>(
(pagination, filters, sorter) => {
// console.log('handleTableChange',pagination,filters,sorter,extra);
// 过滤掉空值的filters
const filteredFilters: Record<string, any> = {};
Object.entries(filters || {}).forEach(([key, value]) => {
if (key === 'image_type') {
if (Array.isArray(value) && value.length > 0 && value[0] !== '全部') {
filteredFilters[key] = Number(value[0]);
}
} else {
if (Array.isArray(value) && value.length > 0) {
filteredFilters[key] = value[0];
} else if (value !== undefined && value !== null) {
if (!Array.isArray(value) && value !== '') {
filteredFilters[key] = value;
}
}
}
});
const newParams: Partial<TOOL.TableParams> = {
pagination: {
current: pagination.current || 1,
pageSize: pagination.pageSize || 10,
},
filters: filteredFilters,
};
if (!Array.isArray(sorter)) {
newParams.sort = {
field: sorter.field as string,
order:
sorter.order === 'ascend' || sorter.order === 'descend'
? sorter.order
: undefined,
};
}
// console.log('handleTableChange', newParams);
updateParams(newParams);
},
[updateParams],
);
return {
tableParams,
getApiParams,
updateParams, // 统一的更新方法
handleTableChange,
};
};
export default useTableParams;

View File

@ -0,0 +1,172 @@
// 页面头部样式
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
}
// 镜像列表样式
.image-list {
width:100%;
height: 100%;
display: flex;
flex-direction: column;
// padding: 16px;
box-sizing: border-box;
.search-box {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
.search-input {
display: flex;
gap: 8px;
align-items: center;
}
}
.images-list-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.images-list-table {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
// 表格适应样式
.ant-table-wrapper {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-spin-nested-loading {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-spin-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-table {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-container {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-header {
flex-shrink: 0;
}
.ant-table-body {
flex: 1;
overflow: auto !important;
}
}
// 确保分页器在底部正确显示
.ant-table-pagination {
flex-shrink: 0;
// 确保分页器始终可见
position: relative;
z-index: 1;
}
}
}
}
}
}
}
.image-detail {
.detail-item {
margin-bottom: 16px;
label {
font-weight: 600;
color: #333;
display: inline-block;
width: 100px;
}
span {
color: #666;
}
p {
margin: 8px 0 0 100px;
color: #666;
line-height: 1.6;
}
}
}
}
// 个人资料样式
.profile-page {
.profile-content {
.profile-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
.profile-info {
h3 {
margin: 0 0 4px 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
p {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
.quick-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.profile-content {
.profile-header {
flex-direction: column;
text-align: center;
}
.quick-actions {
justify-content: center;
}
}
}

View File

@ -0,0 +1,523 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { CODE } from '@/constants/images.constants';
import { deleteTool, getToollList } from '@/services/imagePage';
import { SettingOutlined,PlusOutlined } from '@ant-design/icons';
import {
Button,
Checkbox,
Input,
message,
Modal,
Popconfirm,
Popover,
Space,
Table,
Tooltip,
} from 'antd';
import dayjs from 'dayjs';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import useTableParams from './hook/hook';
import './index.less';
import UploadFileModal from './uploadFileModal';
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
// 列配置定义
type ColumnConfig = {
key: string;
title: string;
dataIndex?: string;
width: number;
render?: (text: any, record: any, index: number) => React.ReactNode;
fixed?: 'left' | 'right';
align?: 'left' | 'center' | 'right';
defaultVisible: boolean; // 默认是否显示
alwaysVisible?: boolean; // 始终显示的列
ellipsis?: boolean; // 是否启用省略号
filters?: { text: string; value: string }[];
filterMultiple?: boolean; // 是否多选过滤
filterDropdown?: (props: any) => React.ReactNode;
defaultFilteredValue?: string[]; // 默认过滤值
onFilter?: (value: string, record: any) => boolean;
};
type TableColumn = {
title: string;
dataIndex?: string;
key: string;
width: number;
render?: any;
fixed?: 'left' | 'right';
hidden?: boolean;
};
// 在组件顶部添加防抖函数(支持取消)
// 增强版防抖函数,使用泛型明确函数类型
const debounce = <T extends (...args: any[]) => any>(
func: T,
delay: number,
immediate = false,
) => {
let timer: NodeJS.Timeout | null = null;
const debounced = (...args: Parameters<T>) => {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
func(...args);
}
timer = setTimeout(() => {
if (!immediate) {
func(...args);
}
timer = null;
}, delay);
};
debounced.cancel = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
return debounced;
};
const Index: React.FC<DESK.ImagesProps> = (props) => {
const { activeTabKey } = props;
const [dataSource, setDataSource] = useState<TOOL.ToolItem[]>([]);
const [loading, setLoading] = useState(false);
const [selectedImage, setSelectedImage] = useState<any>(null);
const [importModalVisible, setImportModalVisible] = useState(false);
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
const { tableParams, getApiParams, updateParams, handleTableChange } =
useTableParams({
pagination: {
current: 1,
pageSize: 10,
},
search: {}, // 初始化搜索参数
});
// 在组件顶部添加一个 ref 来保存最新的 tableParams
const tableParamsRef = useRef(tableParams);
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
// 表格参数变化 获取镜像列表
useEffect(() => {
if (activeTabKey === '4') {
loadDataSource();
}
}, [
activeTabKey,
tableParams.pagination?.current,
tableParams.pagination?.pageSize,
tableParams?.sortOrder,
tableParams?.sortField,
JSON.stringify(tableParams.filters), // 表格搜索参数
JSON.stringify(tableParams.search), // 搜索参数依赖
]);
// 定义所有列的配置
const columnConfigs: ColumnConfig[] = [
{
key: 'index',
title: '序号',
width: 60,
render: (text: any, row: any, index: number) =>
(tableParams.pagination?.current - 1) *
tableParams.pagination?.pageSize +
index +
1,
defaultVisible: true,
alwaysVisible: true,
},
{
key: 'tool_name',
// title: '镜像名称',
title: '文件名',
dataIndex: 'tool_name',
width: 150,
defaultVisible: true,
alwaysVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'tool_type',
title: '文件类型',
dataIndex: 'tool_type',
width: 100,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'file_size',
// title: '镜像版本',
title: '文件大小',
dataIndex: 'file_size',
width: 100,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'version',
// title: '模板存放路径',
title: '版本',
dataIndex: 'version',
width: 140,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'create_time',
title: '上传时间',
dataIndex: 'create_time',
width: 160,
render: (text: string) =>
text ? (
<Tooltip title={dayjs(text).format('YYYY-MM-DD HH:mm:ss')}>
{text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '--'}
</Tooltip>
) : (
'--'
),
defaultVisible: true,
ellipsis: true,
},
{
key: 'description',
title: '描述',
dataIndex: 'description',
width: 140,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'action',
title: '操作',
width: 90,
align: 'center',
fixed: 'right' as 'right',
render: (_: any, record: TOOL.ToolItem) => (
<Space size="small">
<Button
size="small"
type="link"
title="编辑"
onClick={() => handleViewDetail(record)}
>
</Button>
<Popconfirm
title="确定要删除这个镜像吗?"
description="删除后无法恢复,请谨慎操作。"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button size="small" type="link" title="删除">
</Button>
</Popconfirm>
</Space>
),
defaultVisible: true,
},
];
// 初始化 visibleColumns 状态
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
(acc, column) => {
if (!column.alwaysVisible) {
acc[column.key] = column.defaultVisible;
}
return acc;
},
{},
);
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
initialVisibleColumns,
);
// 重置列设置
const resetColumns = () => {
setVisibleColumns(initialVisibleColumns);
};
const loadDataSource = async () => {
setLoading(true);
try {
// 将搜索文本合并到API参数中
const apiParams = {
...getApiParams(),
};
const imagesRes = await getToollList(apiParams);
if (imagesRes.code === CODE) {
setDataSource(imagesRes.data?.data || []);
setLoading(false);
// 正确处理后端返回的分页信息
updateParams({
pagination: {
...tableParams.pagination,
current: imagesRes.data?.page_num || 1,
total: imagesRes.data?.total || 0,
pageSize: tableParams.pagination?.pageSize || 10,
},
});
} else {
message.error(imagesRes.message || '获取镜像列表失败');
setLoading(false);
}
} catch (err) {
message.error('获取镜像列表失败');
setLoading(false);
}
};
const handleViewDetail = (record: TOOL.ToolItem) => {
setSelectedImage(record);
setImportModalVisible(true);
};
const handleDelete = (record: TOOL.ToolItem) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除镜像 "${record.image_name}" 吗?`,
onOk: () => {
deleteTool({ id: record.id }).then((res) => {
if (res.code === CODE) {
message.success('删除成功');
loadDataSource();
} else {
message.error(res.message || '删除失败');
}
});
},
});
};
// 列设置相关函数
const handleColumnChange = (columnKey: string, checked: boolean) => {
setVisibleColumns((prev) => ({
...prev,
[columnKey]: checked,
}));
};
// 列设置内容
const columnSettingsContent = (
<div style={{ padding: '8px 0' }}>
{columnConfigs
.filter((config) => !config.alwaysVisible) // 只显示可控制的列
.map((config) => (
<div key={config.key} style={{ padding: '4px 12px' }}>
<Checkbox
checked={visibleColumns[config.key]}
onChange={(e) => handleColumnChange(config.key, e.target.checked)}
>
{config.title}
</Checkbox>
</div>
))}
<div
style={{
padding: '8px 12px',
borderTop: '1px solid #f0f0f0',
marginTop: 8,
}}
>
<Button type="link" onClick={resetColumns} style={{ padding: 0 }}>
</Button>
</div>
</div>
);
// 根据visibleColumns过滤显示的列
const filteredColumns = columnConfigs
.map((config) => {
// 对于始终显示的列
if (config.alwaysVisible) {
return {
...config,
hidden: undefined,
};
}
// 对于可控制显示/隐藏的列
return {
...config,
...(visibleColumns[config.key] ? {} : { hidden: true }),
};
})
.filter((column) => !column.hidden) as TableColumn[];
const handleRefresh = () => {
loadDataSource();
};
// 自定义分页配置
const paginationConfig = {
...tableParams.pagination,
showTotal: (total: number) => `${total} 条记录`,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
};
const handleSearch = useCallback(
(searchValue: string) => {
const currentTableParams = tableParamsRef.current;
updateParams({
search: {
image_name: searchValue,
},
pagination: {
current: 1,
pageSize: currentTableParams.pagination?.pageSize || 10,
},
});
},
[updateParams],
);
// 防抖版本500ms延迟不立即执行
const debouncedSearch = useRef(debounce(handleSearch, 500)).current;
// 立即执行版本(用于清空时立即搜索)
const immediateSearch = useRef(debounce(handleSearch, 0, true)).current;
const handleSearchChange = (value: string) => {
setSearchText(value);
// 取消所有未执行的防抖请求
debouncedSearch.cancel();
immediateSearch.cancel();
// 清空时立即触发搜索
if (value === '') {
immediateSearch('');
return;
}
// 正常输入时使用防抖
debouncedSearch(value);
};
// 修改回车搜索处理
const handleEnterSearch = (value: string) => {
// 回车搜索时取消未执行的防抖
debouncedSearch.cancel();
immediateSearch.cancel();
// 直接执行搜索
handleSearch(value);
};
const onSubmitBack = () => {
setImportModalVisible(false);
setSelectedImage(null);
handleRefresh();
};
return (
<div className="image-list">
<div className="search-box">
<Button type="primary" icon={<PlusOutlined />} onClick={() => setImportModalVisible(true)}></Button>
<div className="search-input">
<Input.Search
placeholder="请输入名称"
value={searchText}
onChange={(e) => handleSearchChange(e.target.value)}
style={{ width: 300 }}
onSearch={handleEnterSearch}
/>
<Button
onClick={handleRefresh}
loading={loading}
icon={<RefreshIcon style={{ width: 13, height: 13 }} />}
></Button>
<Popover
content={columnSettingsContent}
title="列设置"
trigger="click"
open={columnSettingsVisible}
onOpenChange={setColumnSettingsVisible}
placement="bottomRight"
>
<Button icon={<SettingOutlined />}></Button>
</Popover>
</div>
</div>
<div className="images-list-container">
<div className="images-list-table">
<Table
columns={filteredColumns}
dataSource={dataSource}
rowKey="id"
loading={loading}
pagination={paginationConfig}
onChange={handleTableChange}
scroll={{
y: 'max-content', // 关键:允许内容决定高度
}}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
/>
</div>
</div>
{importModalVisible && (
<UploadFileModal
visible={importModalVisible}
onCancel={() => {
setImportModalVisible(false);
setSelectedImage(null);
}}
onSubmit={onSubmitBack}
isEditing={selectedImage ? true : false}
initialValues={selectedImage}
/>
)}
</div>
);
};
export default Index;

View File

@ -0,0 +1,139 @@
/* 上传区域样式 */
.ant-upload-drag-icon {
color: #1890ff;
font-size: 48px;
margin-bottom: 16px;
}
/* 上传文本样式 */
.ant-upload-text {
font-size: 16px;
margin-bottom: 8px;
}
/* 提示文本样式 */
.ant-upload-hint {
color: #8c8c8c;
}
/* 表单标签样式 */
.ant-form-item-label > label {
font-weight: 500;
font-size: 14px;
}
/* 操作按钮区域样式 */
.ant-form-item-control-wrapper {
text-align: right;
}
/* 按钮间距样式 */
.ant-space-item {
margin-right: 8px;
}
/* 上传区域悬停效果 */
.ant-upload-wrapper.ant-upload-drag {
border: 2px dashed #d9d9d9;
border-radius: 4px;
padding: 40px 0;
transition: border-color 0.3s;
}
.ant-upload-wrapper.ant-upload-drag:hover {
border-color: #40a9ff;
}
/* 上传状态样式 */
.ant-upload-list-item {
margin-top: 16px;
}
/* 错误提示样式 */
.ant-form-item-explain-error {
color: #ff4d4f;
font-size: 12px;
}
/* 描述输入框样式 */
.ant-input-textarea {
min-height: 100px;
resize: vertical;
}
/* 上传进度容器样式 */
.upload-progress-container {
margin-top: 16px;
background-color: #fafafa;
padding: 16px;
border-radius: 6px;
}
.upload-progress-item {
margin-bottom: 16px;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.file-name {
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-right: 16px;
}
.cancel-button {
color: #ff4d4f;
}
.upload-actions {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.upload-actions .ant-btn {
margin-left: 8px;
}
/* 进度条样式 */
.ant-progress {
margin-bottom: 0;
}
/* 禁用状态下的上传区域 */
.ant-upload-disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
/* 多文件上传列表样式 */
.ant-upload-list {
margin-top: 12px;
}
/* 上传文件项样式 */
.ant-upload-list-item {
margin-bottom: 8px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.ant-form-item-label,
.ant-form-item-control-wrapper {
width: 100%;
text-align: left;
}
.upload-progress-container {
padding: 8px;
}
}

View File

@ -0,0 +1,435 @@
import { ERROR_CODE } from '@/constants/constants';
import { addTool, updateTool } from '@/services/imagePage';
import {
CloseOutlined,
DeleteOutlined,
ReloadOutlined,
UploadOutlined,
} from '@ant-design/icons';
import {
Button,
Form,
Input,
message,
Modal,
Progress,
Select,
Space,
Upload,
} from 'antd';
import React, { useEffect, useState } from 'react';
import './index.less';
const { Dragger } = Upload;
type UploadFile = any; // 简化类型定义,实际项目中应使用正确的类型
export interface UploadFileModalProps {
visible: boolean;
onCancel: () => void;
onSubmit: (values: any) => void;
initialValues?: any; // 编辑时的初始值
isEditing?: boolean; // 是否处于编辑状态
}
// 文件类型选项
const FILE_TYPE_OPTIONS = [
{ value: 'system', label: '系统文件' },
{ value: 'virtual', label: '虚拟机文件' },
{ value: 'desktop', label: '桌面文件' },
];
// 分片大小 (5MB)
const CHUNK_SIZE = 5 * 1024 * 1024;
const UploadFileModal: React.FC<UploadFileModalProps> = ({
visible,
onCancel,
onSubmit,
initialValues = {},
isEditing = false,
}) => {
const [form] = Form.useForm();
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState<Record<string, number>>(
{},
);
const [uploadCancelToken, setUploadCancelToken] =
useState<AbortController | null>(null);
// 当visible变化时重置表单
useEffect(() => {
if (visible) {
// 设置初始值
const { description } = initialValues || {};
if (initialValues) {
form.setFieldsValue({ description: description });
}
} else {
// 关闭时重置状态
form.resetFields();
setFileList([]);
setIsUploading(false);
setUploadProgress({});
}
}, [visible, initialValues, isEditing, form]);
// 生成文件ID
const generateFileId = () => {
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
};
// 计算文件MD5 (简化版)
const calculateFileMD5 = (file: File): Promise<string> => {
return new Promise((resolve) => {
// 实际项目中应使用 crypto 库计算真实的MD5
// 这里使用简化版,仅用于演示
const fileId = generateFileId();
resolve(fileId);
});
};
// 分片上传函数
const uploadChunk = async (
file: File,
fileId: string,
chunkIndex: number,
totalChunks: number,
chunkSize: number,
): Promise<boolean> => {
try {
const start = chunkIndex * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const chunkMD5 = await calculateFileMD5(chunk);
console.log('chunkMD5====', chunkMD5);
const formData = new FormData();
formData.append('file_id', fileId);
formData.append('file_name', file.name);
formData.append('file_size', file.size.toString());
formData.append('shard_index', chunkIndex.toString());
formData.append('shard_total', totalChunks.toString());
formData.append('chunk_size', chunkSize.toString());
formData.append('chunk_md5', chunkMD5);
formData.append('chunk', chunk);
// 创建新的取消控制器
const controller = new AbortController();
setUploadCancelToken(controller);
const response = await fetch('/api/v1/images/file/chunk/upload', {
method: 'POST',
body: formData,
signal: controller.signal,
});
const result = await response.json();
// 更新进度
const progress = Math.round(((chunkIndex + 1) / totalChunks) * 100);
setUploadProgress((prev) => ({
...prev,
[fileId]: progress,
}));
return result.success;
} catch (error) {
console.error('分片上传失败:', error);
message.error(`文件 ${file.name} 分片上传失败`);
return false;
}
};
// 处理单个文件的分片上传
const handleFileChunkUpload = async (file: File): Promise<string | null> => {
try {
setIsUploading(true);
const fileId = generateFileId();
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
setUploadProgress((prev) => ({
...prev,
[fileId]: 0,
}));
// 按顺序上传分片
for (let i = 0; i < totalChunks; i++) {
// 检查是否已取消上传
if (!uploadCancelToken) {
message.info(`文件 ${file.name} 上传已取消`);
setIsUploading(false);
return null;
}
const success = await uploadChunk(
file,
fileId,
i,
totalChunks,
CHUNK_SIZE,
);
if (!success) {
return null;
}
}
// 所有分片上传完成后模拟文件URL
const fileUrl = `/uploads/${fileId}/${file.name}`;
return fileUrl;
} catch (error) {
console.error('文件上传失败:', error);
message.error(`文件 ${file.name} 上传失败`);
return null;
} finally {
setIsUploading(false);
}
};
// 处理文件上传变化
const handleUploadChange = ({
fileList: newFileList,
file,
}: {
fileList: UploadFile[];
file?: UploadFile;
}) => {
setFileList(newFileList);
// 上传前处理
if (file && file.status === 'uploading') {
setIsUploading(true);
setUploadProgress({});
}
// 上传完成处理
if (file && file.status === 'done') {
handleFileChunkUpload(file.originFileObj);
}
// 上传错误处理
if (file && file.status === 'error') {
setIsUploading(false);
message.error('文件上传失败');
}
};
// 取消上传
const handleCancelUpload = () => {
if (uploadCancelToken) {
uploadCancelToken.abort();
setUploadCancelToken(null);
}
setIsUploading(false);
setUploadProgress({});
message.info('上传已取消');
};
// 重新上传失败的文件
const handleReUpload = (file: UploadFile) => {
if (file.originFileObj) {
handleFileChunkUpload(file.originFileObj);
}
};
// 删除已上传的文件
const handleDeleteFile = () => {
setFileList([]);
setUploadProgress({});
message.success('文件已删除');
};
// 新建
const handleCreate = (payload: any) => {
addTool(payload).then((res: any) => {
const { code } = res || {};
if (code === ERROR_CODE) {
message.success('保存成功');
onSubmit(payload);
}
});
};
const handleUpdata = (payload: any) => {
updateTool(payload).then((res: any) => {
const { code } = res || {};
if (code === ERROR_CODE) {
message.success('保存成功');
onSubmit(payload);
}
});
};
// 处理提交
const handleSubmit = async () => {
try {
// 验证表单
const values = await form.validateFields();
console.log('form表单字段=======', values);
if (isEditing) {
handleUpdata({ ...values, id: initialValues?.id });
} else {
// 上传文件
// 处理文件
if (!isEditing) {
if (fileList.length === 0 || fileList[0].status !== 'done') {
message.error('请先上传文件');
return;
}
values.fileUrl = `/uploads/${generateFileId()}/${fileList[0].name}`;
values.fileName = fileList[0].name;
handleCreate(values);
}
}
} catch (error) {
console.error('提交失败:', error);
}
};
// 上传前处理
const beforeUpload = (file: File) => {
// 允许多文件上传
return true;
};
return (
<Modal
title={isEditing ? '编辑' : '上传工具'}
open={visible}
onCancel={onCancel}
footer={null}
width={800}
>
<Form
form={form}
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
initialValues={initialValues}
>
{/* 文件类型 - 编辑时隐藏 */}
{!isEditing && (
<Form.Item
name="fileType"
label="文件类型"
rules={[{ required: true, message: '请选择文件类型' }]}
>
<Select options={FILE_TYPE_OPTIONS} placeholder="请选择文件类型" />
</Form.Item>
)}
{/* 文件上传 - 编辑时隐藏 */}
{!isEditing && (
<Form.Item
name="file"
label="文件"
valuePropName="fileList"
getValueFromEvent={({ fileList }) => fileList}
rules={[{ required: true, message: '请上传文件' }]}
>
<>
<Dragger
accept=".zip,.rar,.7z,.tar,.gz"
fileList={fileList}
beforeUpload={beforeUpload}
onChange={handleUploadChange}
multiple={false}
disabled={isUploading}
customRequest={({ file, onSuccess }) => {
// 自定义上传处理
setTimeout(() => {
if (onSuccess) {
onSuccess(undefined, file);
}
}, 0);
}}
>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
</p>
<p className="ant-upload-hint">
.zip, .rar, .7z, .tar, .gz
</p>
</Dragger>
{/* 显示上传进度和操作按钮 */}{' '}
{Object.keys(uploadProgress).length > 0 &&
fileList.length > 0 && (
<div className="upload-progress-container">
{Object.entries(uploadProgress).map(
([fileId, progress]) => (
<div key={fileId} className="upload-progress-item">
<div className="progress-info">
<span className="file-name">
{fileList[0].name}
</span>
{isUploading && (
<Button
type="text"
icon={<CloseOutlined />}
onClick={handleCancelUpload}
className="cancel-button"
>
</Button>
)}
</div>
<Progress percent={progress} status="active" />
</div>
),
)}
</div>
)}
{/* 上传完成后的删除按钮和上传失败后的重新上传按钮 */}
{fileList.length > 0 && (
<div className="upload-actions">
{fileList[0].status === 'done' && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={handleDeleteFile}
>
</Button>
)}
{fileList[0].status === 'error' && (
<Button
type="text"
icon={<ReloadOutlined />}
onClick={() => handleReUpload(fileList[0])}
>
</Button>
)}
</div>
)}
</>
</Form.Item>
)}
{/* 描述 */}
<Form.Item
name="description"
label="描述"
rules={[{ required: false }]}
>
<Input.TextArea rows={4} placeholder="请输入描述信息" />
</Form.Item>
{/* 操作按钮 */}
<Form.Item wrapperCol={{ offset: 19, span: 5 }}>
<Space size="middle">
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleSubmit} loading={isUploading}>
{isEditing ? '保存' : '提交'}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
);
};
export default UploadFileModal;

View File

@ -0,0 +1,249 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { ERROR_CODE } from '@/constants/constants';
import {
addVirtualImage,
getVirtualImageDetail,
updateVirtualImage,
} from '@/services/imagePage/index';
import {
Col,
Form,
Input,
InputNumber,
Modal,
Row,
Select,
message,
} from 'antd';
import React, { useEffect } from 'react';
const { Option } = Select;
interface EditModalProps {
visible: boolean;
detialData: any;
storagePathList: any[];
networkList: any[];
systemList: any[];
onCancel: () => void;
onOk: (values: any) => void;
}
const EditModal: React.FC<EditModalProps> = ({
visible,
detialData,
storagePathList,
networkList,
systemList,
onCancel,
onOk,
}) => {
const [form] = Form.useForm();
useEffect(() => {
if (detialData?.id) {
const params = { id: detialData.id };
getVirtualImageDetail(params).then((res: any) => {
console.log('res=======', res);
const { code, data } = res || {};
if (code === ERROR_CODE) {
const { cpu_total, cpu_core_total } = data;
const initialValues = {
...data,
cpu: { size: cpu_total, core: cpu_core_total },
};
form.setFieldsValue(initialValues);
}
});
}
}, [visible, form, detialData]);
const onAddVirtualImage = (payload: any) => {
addVirtualImage(payload).then((res: any) => {
const { code } = res || {};
if (code === ERROR_CODE) {
message.success('保存成功');
onOk(payload);
}
});
};
const onEditVirtualImage = (payload: any) => {
updateVirtualImage(payload).then((res: any) => {
const { code } = res || {};
if (code === ERROR_CODE) {
message.success('保存成功');
onOk(payload);
}
});
};
const handleOk = () => {
form
.validateFields()
.then((values) => {
console.log('表单字段值values', values);
const { cpu } = values || {};
const { size, core } = cpu || {};
const obj: any = { ...values };
delete obj.cpu;
const payload = {
cpu_total: size,
cpu_core_total: core,
...obj,
};
if (detialData.id) {
onEditVirtualImage({ id: detialData.id, ...payload });
} else {
onAddVirtualImage(payload);
}
})
.catch((error) => {
console.error('表单验证失败:', error);
});
};
return (
<Modal
title="编辑信息"
open={visible}
onCancel={onCancel}
onOk={handleOk}
okText="确定"
cancelText="取消"
>
<Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
{/* 名称字段 */}
<Form.Item
name="image_name"
label="名称"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="image_system_id"
label="系统镜像"
rules={[{ required: true, message: '请选择系统镜像' }]}
>
<Select placeholder="请选择系统镜像">
{systemList.map((item: any) => {
const { name, value } = item || {};
return (
<Option key={value} value={value}>
{name}
</Option>
);
})}
</Select>
</Form.Item>
{/* CPU 字段 */}
<Form.Item
label="CPU"
rules={[{ required: true, message: '请输入CPU数量' }]}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name={['cpu', 'size']}
rules={[{ required: true, message: '请输入CPU数量' }]}
noStyle
>
<InputNumber
precision={0}
style={{ width: '190px' }}
placeholder="CPU数量"
addonAfter={<span></span>}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name={['cpu', 'cores']}
rules={[{ required: true, message: '请输入CPU内核数' }]}
noStyle
>
<InputNumber
precision={0}
style={{ width: '190px' }}
placeholder="CPU内核数"
addonAfter={<span></span>}
/>
</Form.Item>
</Col>
</Row>
</Form.Item>
{/* 内存字段 */}
<Form.Item
name="memory_total"
label="内存"
rules={[{ required: true, message: '请输入内存' }]}
>
<InputNumber
precision={0}
addonAfter={<span>CB</span>}
style={{ width: '393.33px' }}
placeholder="内存"
/>
</Form.Item>
<Form.Item
name="system_total"
label="系统盘"
rules={[{ required: true, message: '请输入系统盘内存' }]}
>
<InputNumber
precision={0}
addonAfter={<span>CB</span>}
style={{ width: '393.33px' }}
placeholder="系统盘内存"
/>
</Form.Item>
{/* 存储卷字段 */}
<Form.Item
name="storage_path"
label="存储卷"
rules={[{ required: true, message: '请选择存储卷' }]}
>
<Select placeholder="请选择存储卷">
{storagePathList.map((item: any) => {
const { path } = item || {};
return (
<Option key={path} value={path}>
{path}
</Option>
);
})}
</Select>
</Form.Item>
<Form.Item
name="network_module"
label="网络模板"
rules={[{ required: true, message: '请选择网络模板' }]}
>
<Select placeholder="请选择网络模板">
{networkList.map((item: any) => {
const { name, value } = item || {};
return (
<Option key={value} value={value}>
{name}
</Option>
);
})}
</Select>
</Form.Item>
{/* 描述字段 */}
<Form.Item name="description" label="描述">
<Input.TextArea />
</Form.Item>
</Form>
</Modal>
);
};
export default EditModal;

View File

@ -0,0 +1,138 @@
import type { TableProps } from 'antd';
import { useCallback, useState } from 'react';
const useTableParams = (
initialParams: DESK.VirtualTableParams = {
pagination: { current: 1, pageSize: 10 },
filters: {},// 表格的搜索对象
sort: {},
search: {}, // 添加搜索参数对象
},
) => {
const [tableParams, setTableParams] =
useState<DESK.VirtualTableParams>(initialParams);
const getApiParams = useCallback(() => {
const { pagination, filters, sort, search, ...rest } = tableParams;
const apiParams: Record<string, any> = {
page_size: pagination?.pageSize,
page_num: pagination?.current,
...rest,
};
if (sort?.field) {
apiParams.orderby = sort.field;
apiParams.order = sort.order === 'ascend' ? 'asc' : 'desc';
}
// 处理表格搜索参数
Object.entries(filters || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
apiParams[key] = value;
}
});
// 处理搜索参数
Object.entries(search || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
apiParams[key] = value;
}
});
console.log('getApiParams apiParams', apiParams);
return apiParams;
}, [tableParams]);
// 统一的更新方法,可以处理所有参数类型
const updateParams = useCallback(
(
newParams: Partial<DESK.VirtualTableParams>,
options?: { resetPage?: boolean },
) => {
// console.log('updateParams', newParams);
setTableParams((prev) => {
// 如果是搜索或过滤相关的更新,重置到第一页
const shouldResetPage =
options?.resetPage ??
((newParams.search && Object.keys(newParams.search).length > 0) || // 有搜索值
(newParams.filters && Object.keys(newParams.filters).length > 0)); // 有过滤值
return {
...prev,
...newParams,
pagination: {
...prev.pagination,
...newParams.pagination,
...(shouldResetPage ? { current: 1 } : {}), // 根据条件决定是否重置页码
},
};
});
},
[],
);
/**
*
* @param pagination
* @param filters filters
* @param sorter
* @param extra
* @returns void
* */
const handleTableChange = useCallback<
NonNullable<TableProps<DESK.VirtualItem>['onChange']>
>(
(pagination, filters, sorter) => {
// console.log('handleTableChange',pagination,filters,sorter,extra);
// 过滤掉空值的filters
const filteredFilters: Record<string, any> = {};
Object.entries(filters || {}).forEach(([key, value]) => {
// if (key === 'image_type') {
// if (Array.isArray(value) && value.length > 0 && value[0] !== '全部') {
// filteredFilters[key] = Number(value[0]);
// }
// } else {
if (Array.isArray(value) && value.length > 0) {
filteredFilters[key] = value[0];
} else if (value !== undefined && value !== null) {
if (!Array.isArray(value) && value !== '') {
filteredFilters[key] = value;
}
}
// }
});
const newParams: Partial<DESK.VirtualTableParams> = {
pagination: {
current: pagination.current || 1,
pageSize: pagination.pageSize || 10,
},
filters: filteredFilters,
};
if (!Array.isArray(sorter)) {
newParams.sort = {
field: sorter.field as string,
order:
sorter.order === 'ascend' || sorter.order === 'descend'
? sorter.order
: undefined,
};
}
// console.log('handleTableChange', newParams);
updateParams(newParams);
},
[updateParams],
);
return {
tableParams,
getApiParams,
updateParams, // 统一的更新方法
handleTableChange,
};
};
export default useTableParams;

View File

@ -0,0 +1,172 @@
// 页面头部样式
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
}
// 镜像列表样式
.image-list {
width:100%;
height: 100%;
display: flex;
flex-direction: column;
// padding: 16px;
box-sizing: border-box;
.search-box {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
.search-input {
display: flex;
gap: 8px;
align-items: center;
}
}
.images-list-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.images-list-table {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
// 表格适应样式
.ant-table-wrapper {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-spin-nested-loading {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-spin-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-table {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-container {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-header {
flex-shrink: 0;
}
.ant-table-body {
flex: 1;
overflow: auto !important;
}
}
// 确保分页器在底部正确显示
.ant-table-pagination {
flex-shrink: 0;
// 确保分页器始终可见
position: relative;
z-index: 1;
}
}
}
}
}
}
}
.image-detail {
.detail-item {
margin-bottom: 16px;
label {
font-weight: 600;
color: #333;
display: inline-block;
width: 100px;
}
span {
color: #666;
}
p {
margin: 8px 0 0 100px;
color: #666;
line-height: 1.6;
}
}
}
}
// 个人资料样式
.profile-page {
.profile-content {
.profile-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
.profile-info {
h3 {
margin: 0 0 4px 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
p {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
.quick-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.profile-content {
.profile-header {
flex-direction: column;
text-align: center;
}
.quick-actions {
justify-content: center;
}
}
}

View File

@ -0,0 +1,691 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable eqeqeq */
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
import { ERROR_CODE } from '@/constants/constants';
import { CODE } from '@/constants/images.constants';
import {
cloneVirtualImage,
deleteVirtualImage,
getNetworkList,
getStorageList,
getVirtualImagesList,
operateVirtualImage,
} from '@/services/imagePage/index';
import { getImagesList } from '@/services/images';
import { DownOutlined, SettingOutlined,PlusOutlined } from '@ant-design/icons';
import {
Button,
Checkbox,
Input,
message,
Modal,
Popconfirm,
Popover,
Space,
Table,
Tooltip,
} from 'antd';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import CreatModal from './creatModal';
import useTableParams from './hook/hook';
import './index.less';
// 列配置定义
type ColumnConfig = {
key: string;
title: string;
dataIndex?: string;
width: number;
render?: (text: any, record: any, index: number) => React.ReactNode;
fixed?: 'left' | 'right';
align?: 'left' | 'right' | 'center';
defaultVisible: boolean; // 默认是否显示
alwaysVisible?: boolean; // 始终显示的列
ellipsis?: boolean; // 是否启用省略号
filters?: { text: string; value: string }[];
filterMultiple?: boolean; // 是否多选过滤
filterDropdown?: (props: any) => React.ReactNode;
defaultFilteredValue?: string[]; // 默认过滤值
onFilter?: (value: string, record: any) => boolean;
};
type TableColumn = {
title: string;
dataIndex?: string;
key: string;
width: number;
render?: any;
fixed?: 'left' | 'right';
hidden?: boolean;
};
// 在组件顶部添加防抖函数(支持取消)
// 增强版防抖函数
// 增强版防抖函数,使用泛型明确函数类型
const debounce = <T extends (...args: any[]) => any>(
func: T,
delay: number,
immediate = false,
) => {
let timer: NodeJS.Timeout | null = null;
const debounced = (...args: Parameters<T>) => {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
func(...args);
}
timer = setTimeout(() => {
if (!immediate) {
func(...args);
}
timer = null;
}, delay);
};
debounced.cancel = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
return debounced;
};
const Index: React.FC<DESK.ImagesProps> = (props) => {
const { activeTabKey } = props;
const [dataSource, setDataSource] = useState<DESK.VirtualItem[]>([]);
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
const [visible, setVisible] = useState(false); // 新增状态管理
const [detialData, setDetialData] = useState<DESK.VirtualItem | null>(null); // 新增状态管理
const [storagePathList, setStoragePathList] = useState<any>([
{ path: 'cvar/lib/libvirt/images' },
]);
const [networkList, setNetworkList] = useState<any>([
{ name: 'default', value: 'default' },
{ name: '桥接', value: 'qiaojie' },
]);
const [systemList, setSystemList] = useState<any>([
{ name: 'CentOS 7', value: 'CentOS 7' },
{ name: 'CentOS 8', value: 'CentOS 8' },
]);
// 存储清理函数的引用,用于在组件卸载时执行清理
const cleanupFunctions = useRef<Array<() => void>>([]);
const { tableParams, getApiParams, updateParams, handleTableChange } =
useTableParams({
pagination: {
current: 1,
pageSize: 10,
},
search: {}, // 初始化搜索参数
});
// 在组件顶部添加一个 ref 来保存最新的 tableParams
const tableParamsRef = useRef(tableParams);
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
// 组件卸载时执行所有清理函数
useEffect(() => {
return () => {
cleanupFunctions.current.forEach((cleanup) => cleanup());
cleanupFunctions.current = [];
};
}, []);
useEffect(() => {
onGetNetworkList();
onGetStorageList();
onGetImagesList();
}, []);
// 表格参数变化 获取镜像列表
useEffect(() => {
if (activeTabKey === '2') {
loadDataSource();
}
}, [
activeTabKey,
tableParams.pagination?.current,
tableParams.pagination?.pageSize,
tableParams?.sortOrder,
tableParams?.sortField,
JSON.stringify(tableParams.filters), // 表格搜索参数
JSON.stringify(tableParams.search), // 搜索参数依赖
]);
// 定义所有列的配置
const columnConfigs: ColumnConfig[] = [
{
key: 'index',
title: '序号',
width: 60,
render: (text: any, row: any, index: number) =>
(tableParams.pagination?.current - 1) *
tableParams.pagination?.pageSize +
index +
1,
defaultVisible: true,
alwaysVisible: true,
},
{
key: 'image_name',
title: '名称',
dataIndex: 'image_name',
width: 150,
defaultVisible: true,
alwaysVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'os_version',
title: '操作系统',
dataIndex: 'os_version',
width: 150,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'image_status',
title: '状态',
dataIndex: 'image_status',
width: 120,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'image_system_name',
title: '系统镜像',
dataIndex: 'image_system_name',
width: 120,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'description',
title: '描述',
dataIndex: 'description',
width: 140,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'action',
title: '操作',
width: 150,
align: 'center',
fixed: 'right' as 'right',
render: (_: any, record: DESK.VirtualItem) => (
<Space size="small">
<Button
size="small"
type="link"
title="控制台"
onClick={() => handleOpenVnc(record)}
>
</Button>
<Button
onClick={() => {
handleClone(record);
}}
size="small"
type="link"
title="克隆为模板"
>
</Button>
<Popover
placement="bottomRight"
content={
<div>
<div>
<Button
type="link"
onClick={() => {
handleOperate(record, 'start');
}}
>
</Button>
</div>
<div>
<Button
type="link"
onClick={() => {
handleOperate(record, 'close');
}}
>
</Button>
</div>
<div>
<Button
type="link"
onClick={() => {
handleOperate(record, 'restart');
}}
>
</Button>
</div>
<div>
<Button
type="link"
onClick={() => {
handelOpenModal(record);
}}
>
</Button>
</div>
<Popconfirm
title="确定要删除这个镜像吗?"
description="删除后无法恢复,请谨慎操作。"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button type="link" title="删除">
</Button>
</Popconfirm>
</div>
}
>
<a onClick={(e) => e.preventDefault()}>
<DownOutlined style={{ fontSize: '0.7rem' }} />
</a>
</Popover>
</Space>
),
defaultVisible: true,
},
];
// 初始化 visibleColumns 状态
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
(acc, column) => {
if (!column.alwaysVisible) {
acc[column.key] = column.defaultVisible;
}
return acc;
},
{},
);
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
initialVisibleColumns,
);
// 重置列设置
const resetColumns = () => {
setVisibleColumns(initialVisibleColumns);
};
const loadDataSource = async () => {
setLoading(true);
try {
// 将搜索文本合并到API参数中
const apiParams = {
...getApiParams(),
};
const result = await getVirtualImagesList(apiParams);
if (result.code == CODE) {
setDataSource(result.data?.data || []);
setLoading(false);
// 正确处理后端返回的分页信息
updateParams({
pagination: {
...tableParams.pagination,
current: result.data?.page_num || 1,
total: result.data?.total || 0,
pageSize: tableParams.pagination?.pageSize || 10,
},
});
} else {
message.error(result.message || '获取镜像列表失败');
setLoading(false);
}
} catch (err) {
message.error('获取镜像列表失败');
setLoading(false);
}
};
//虚拟镜像克隆为桌面镜像
const handleClone = (record: DESK.VirtualItem) => {
cloneVirtualImage({ id: record.id }).then((res) => {
if (res.code == CODE) {
message.success('克隆成功');
loadDataSource();
} else {
message.error(res.message || '克隆失败');
}
});
};
const onGetImagesList = () => {
const payload = {
page_num: 1,
page_size: 5000,
};
getImagesList(payload).then((res) => {
const { code, data } = res || {};
const { data: list } = data || {};
if (code === ERROR_CODE) {
setSystemList(list);
}
});
};
const onGetStorageList = () => {
const params = { id: detialData?.id };
getStorageList(params).then((res) => {
const { code, data } = res || {};
const { data: list } = data || {};
if (code === ERROR_CODE) {
setStoragePathList(list);
}
});
};
const onGetNetworkList = () => {
const params = { id: detialData?.id };
getNetworkList(params).then((res) => {
const { code, data } = res || {};
const { data: list } = data || {};
if (code === ERROR_CODE) {
setNetworkList(list);
}
});
};
const handleDelete = (record: DESK.VirtualItem) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除镜像 "${record.image_name}" 吗?`,
onOk: () => {
deleteVirtualImage({ id: record.id }).then((res) => {
if (res.code == CODE) {
message.success('删除成功');
loadDataSource();
} else {
message.error(res.message || '删除失败');
}
});
},
});
};
// 处理打开VNC客户端 - 在新窗口中打开
const handleOpenVnc = (record: DESK.VirtualItem) => {
try {
// 构建远程桌面页面的URL
const vncUrl = 'ws://10.100.51.118:8000/api/v1/ws/vnc/win10u';
const remotePageUrl = `/vncClient?imageId=${
record.id
}&imageName=${encodeURIComponent(
'NEXSPACE远程桌面',
)}&vncUrl=${encodeURIComponent(vncUrl)}`;
// 在新窗口中打开远程桌面
const vncWindow = window.open(
remotePageUrl,
'_blank',
'width=1200,height=800,menubar=no,toolbar=no,location=no,status=no',
);
// 检查窗口是否成功打开
if (!vncWindow) {
message.error('无法打开新窗口,可能被浏览器阻止');
return;
}
} catch (error) {
message.error('远程桌面连接失败');
}
};
// 列设置相关函数
const handleColumnChange = (columnKey: string, checked: boolean) => {
setVisibleColumns((prev) => ({
...prev,
[columnKey]: checked,
}));
};
// 列设置内容
const columnSettingsContent = (
<div style={{ padding: '8px 0' }}>
{columnConfigs
.filter((config) => !config.alwaysVisible) // 只显示可控制的列
.map((config) => (
<div key={config.key} style={{ padding: '4px 12px' }}>
<Checkbox
checked={visibleColumns[config.key]}
onChange={(e) => handleColumnChange(config.key, e.target.checked)}
>
{config.title}
</Checkbox>
</div>
))}
<div
style={{
padding: '8px 12px',
borderTop: '1px solid #f0f0f0',
marginTop: 8,
}}
>
<Button type="link" onClick={resetColumns} style={{ padding: 0 }}>
</Button>
</div>
</div>
);
// 根据visibleColumns过滤显示的列
const filteredColumns = columnConfigs
.map((config) => {
// 对于始终显示的列
if (config.alwaysVisible) {
return {
...config,
hidden: undefined,
};
}
// 对于可控制显示/隐藏的列
return {
...config,
...(visibleColumns[config.key] ? {} : { hidden: true }),
};
})
.filter((column) => !column.hidden) as TableColumn[];
const handleRefresh = () => {
loadDataSource();
};
// 自定义分页配置
const paginationConfig = {
...tableParams.pagination,
showTotal: (total: number) => `${total} 条记录`,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
};
const handleSearch = useCallback(
(searchValue: string) => {
const currentTableParams = tableParamsRef.current;
updateParams({
search: {
image_name: searchValue,
},
pagination: {
current: 1,
pageSize: currentTableParams.pagination?.pageSize || 10,
},
});
},
[updateParams],
);
// 防抖版本500ms延迟不立即执行
const debouncedSearch = useRef(debounce(handleSearch, 500)).current;
// 立即执行版本(用于清空时立即搜索)
const immediateSearch = useRef(debounce(handleSearch, 0, true)).current;
const handleSearchChange = (value: string) => {
setSearchText(value);
// 取消所有未执行的防抖请求
debouncedSearch.cancel();
immediateSearch.cancel();
// 清空时立即触发搜索
if (value === '') {
immediateSearch('');
return;
}
// 正常输入时使用防抖
debouncedSearch(value);
};
// 修改回车搜索处理
const handleEnterSearch = (value: string) => {
// 回车搜索时取消未执行的防抖
debouncedSearch.cancel();
immediateSearch.cancel();
// 直接执行搜索
handleSearch(value);
};
// 新增弹窗
const handelOpenModal = (record?: DESK.VirtualItem) => {
setDetialData(record || null);
setVisible(true);
};
// 编辑、新建保存成功回调
const onOk = () => {
setDetialData(null);
setVisible(false);
handleRefresh();
};
// 虚拟镜像关闭、开启、重启
const handleOperate = (record: DESK.VirtualItem, type: string) => {
operateVirtualImage({
id: record.id,
operate_type: type,
}).then((res: any) => {
const { code } = res || {};
if (code === ERROR_CODE) {
message.success('操作成功');
handleRefresh();
}
});
};
return (
<div className="image-list">
<div className="search-box">
<div>
<Button type="primary" icon={<PlusOutlined />} onClick={() => handelOpenModal()}></Button>
</div>
<div className="search-input">
<Input.Search
placeholder="请输入名称"
value={searchText}
onChange={(e) => handleSearchChange(e.target.value)}
style={{ width: 300 }}
onSearch={handleEnterSearch}
/>
<Button
onClick={handleRefresh}
loading={loading}
icon={<RefreshIcon style={{ width: 13, height: 13 }} />}
></Button>
<Popover
content={columnSettingsContent}
title="列设置"
trigger="click"
open={columnSettingsVisible}
onOpenChange={setColumnSettingsVisible}
placement="bottomRight"
>
<Button icon={<SettingOutlined />}></Button>
</Popover>
</div>
</div>
<div className="images-list-container">
<div className="images-list-table">
<Table
columns={filteredColumns}
dataSource={dataSource}
rowKey="id"
loading={loading}
pagination={paginationConfig}
onChange={handleTableChange}
scroll={{
y: 'max-content', // 关键:允许内容决定高度
}}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
/>
</div>
</div>
{visible && (
<CreatModal
onCancel={() => {
setVisible(false);
}}
onOk={onOk}
detialData={detialData}
visible={visible}
storagePathList={storagePathList}
networkList={networkList}
systemList={systemList}
/>
)}
</div>
);
};
export default Index;

View File

@ -15,10 +15,11 @@
// 镜像列表样式
.image-list {
width:100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 16px;
// padding: 16px;
box-sizing: border-box;
.search-box {
margin-bottom: 16px;

View File

@ -1,19 +1,16 @@
import {
CODE,
IMAGES_TYPE_MAP,
STATUS_MAP,
} from '@/constants/images.constants';
/* eslint-disable @typescript-eslint/no-use-before-define */
import { CODE, STATUS_MAP } from '@/constants/images.constants';
import { delImagesAPI, getImagesList } from '@/services/images';
import {
DeleteOutlined,
EyeOutlined,
SettingOutlined,
PlusOutlined
} from '@ant-design/icons';
import {
Button,
Checkbox,
Input,
Menu,
message,
Modal,
Popconfirm,
@ -32,6 +29,10 @@ import './index.less';
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
// interface ImagesProps {
// activeTabKey?: string;
// }
// 列配置定义
type ColumnConfig = {
key: string;
@ -40,6 +41,7 @@ type ColumnConfig = {
width: number;
render?: (text: any, record: any, index: number) => React.ReactNode;
fixed?: 'left' | 'right';
align?: 'left' | 'center' | 'right';
defaultVisible: boolean; // 默认是否显示
alwaysVisible?: boolean; // 始终显示的列
ellipsis?: boolean; // 是否启用省略号
@ -89,7 +91,8 @@ const debounce = (func: Function, delay: number, immediate = false) => {
return debounced;
};
const ImageList: React.FC = () => {
const ImageList: React.FC<DESK.ImagesProps> = (props) => {
const { activeTabKey } = props;
const [images, setImages] = useState<IMAGES.ImageItem[]>([]);
const [loading, setLoading] = useState(false);
const [selectedImage, setSelectedImage] = useState<IMAGES.ImageItem | null>(
@ -115,219 +118,6 @@ const ImageList: React.FC = () => {
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
// 表格参数变化 获取镜像列表
useEffect(() => {
loadImages();
}, [
tableParams.pagination?.current,
tableParams.pagination?.pageSize,
tableParams?.sortOrder,
tableParams?.sortField,
JSON.stringify(tableParams.filters), // 表格搜索参数
JSON.stringify(tableParams.search), // 搜索参数依赖
]);
// 定义所有列的配置
const columnConfigs: ColumnConfig[] = [
{
key: 'index',
title: '序号',
width: 60,
render: (text: any, row: any, index: number) =>
(tableParams.pagination?.current - 1) *
tableParams.pagination?.pageSize +
index +
1,
defaultVisible: true,
alwaysVisible: true,
},
{
key: 'image_name',
title: '镜像名称',
dataIndex: 'image_name',
width: 150,
defaultVisible: true,
alwaysVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'image_file_name',
title: '镜像文件',
dataIndex: 'image_file_name',
width: 150,
defaultVisible: true,
alwaysVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
// {
// key: 'image_type',
// title: '桌面类型',
// dataIndex: 'image_type',
// width: 120,
// render: (text: number) => {
// const key = text as keyof typeof IMAGES_TYPE_MAP;
// return text ? IMAGES_TYPE_MAP[key] : '--';
// },
// defaultVisible: true,
// filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => (
// <Menu
// selectedKeys={selectedKeys.length > 0 ? selectedKeys : ['全部']}
// onClick={({ key }) => {
// setSelectedKeys(key === '全部' ? [] : [key]);
// confirm({ closeDropdown: true }); // 立即触发筛选并关闭下拉菜单
// }}
// items={[
// { key: '全部', label: '全部' },
// ...Object.entries(IMAGES_TYPE_MAP).map(([key, value]) => ({
// key,
// label: value,
// })),
// ]}
// />
// ),
// filterMultiple: false,
// defaultFilteredValue: ['全部'],
// },
{
key: 'storage_path',
title: '模板存放路径',
dataIndex: 'storage_path',
width: 140,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'bt_path',
title: 'BT路径',
dataIndex: 'bt_path',
width: 140,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'image_version',
title: '镜像版本',
dataIndex: 'image_version',
width: 100,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'os_version',
title: '操作系统',
dataIndex: 'os_version',
width: 100,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'image_status',
title: '镜像状态',
dataIndex: 'image_status',
width: 90,
render: (text: number) => (text ? getStatusTag(text) : '--'),
defaultVisible: true,
},
{
key: 'create_time',
title: '上传时间',
dataIndex: 'create_time',
width: 160,
render: (text: string) =>
text ? (
<Tooltip title={dayjs(text).format('YYYY-MM-DD HH:mm:ss')}>
{text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '--'}
</Tooltip>
) : (
'--'
),
defaultVisible: true,
ellipsis: true,
},
{
key: 'action',
title: '操作',
width: 90,
fixed: 'right' as 'right',
render: (_: any, record: IMAGES.ImageItem) => (
<Space size="small">
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => handleViewDetail(record)}
title="查看详情"
/>
<Popconfirm
title="确定要删除这个镜像吗?"
description="删除后无法恢复,请谨慎操作。"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button type="text" icon={<DeleteOutlined />} title="删除" danger />
</Popconfirm>
</Space>
),
defaultVisible: true,
},
];
// 初始化 visibleColumns 状态
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
(acc, column) => {
if (!column.alwaysVisible) {
acc[column.key] = column.defaultVisible;
}
return acc;
},
{},
);
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
initialVisibleColumns,
);
// 重置列设置
const resetColumns = () => {
setVisibleColumns(initialVisibleColumns);
};
const loadImages = async () => {
setLoading(true);
try {
@ -359,6 +149,243 @@ const ImageList: React.FC = () => {
setLoading(false);
}
};
// 表格参数变化 获取镜像列表
useEffect(() => {
if (activeTabKey === '1') {
loadImages();
}
}, [
tableParams.pagination?.current,
tableParams.pagination?.pageSize,
tableParams?.sortOrder,
tableParams?.sortField,
JSON.stringify(tableParams.filters), // 表格搜索参数
JSON.stringify(tableParams.search), // 搜索参数依赖
activeTabKey,
]);
// 定义所有列的配置
const columnConfigs: ColumnConfig[] = [
{
key: 'index',
title: '序号',
width: 60,
render: (text: any, row: any, index: number) =>
(tableParams.pagination?.current - 1) *
tableParams.pagination?.pageSize +
index +
1,
defaultVisible: true,
alwaysVisible: true,
},
{
key: 'image_name',
// title: '镜像名称',
title: '名称',
dataIndex: 'image_name',
width: 150,
defaultVisible: true,
alwaysVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'os_version',
title: '操作系统',
dataIndex: 'os_version',
width: 100,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'image_version',
// title: '镜像版本',
title: '版本',
dataIndex: 'image_version',
width: 100,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'storage_path',
// title: '模板存放路径',
title: '存储位置',
dataIndex: 'storage_path',
width: 140,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
// {
// key: 'image_file_name',
// title: '镜像文件',
// dataIndex: 'image_file_name',
// width: 150,
// defaultVisible: true,
// alwaysVisible: true,
// ellipsis: true,
// render: (text: string) =>
// text ? (
// <Tooltip title={text} placement="topLeft">
// {text}
// </Tooltip>
// ) : (
// '--'
// ),
// },
// {
// key: 'image_type',
// title: '桌面类型',
// dataIndex: 'image_type',
// width: 120,
// render: (text: number) => {
// const key = text as keyof typeof IMAGES_TYPE_MAP;
// return text ? IMAGES_TYPE_MAP[key] : '--';
// },
// defaultVisible: true,
// filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => (
// <Menu
// selectedKeys={selectedKeys.length > 0 ? selectedKeys : ['全部']}
// onClick={({ key }) => {
// setSelectedKeys(key === '全部' ? [] : [key]);
// confirm({ closeDropdown: true }); // 立即触发筛选并关闭下拉菜单
// }}
// items={[
// { key: '全部', label: '全部' },
// ...Object.entries(IMAGES_TYPE_MAP).map(([key, value]) => ({
// key,
// label: value,
// })),
// ]}
// />
// ),
// filterMultiple: false,
// defaultFilteredValue: ['全部'],
// },
// {
// key: 'bt_path',
// title: 'BT路径',
// dataIndex: 'bt_path',
// width: 140,
// defaultVisible: true,
// ellipsis: true,
// render: (text: string) =>
// text ? (
// <Tooltip title={text} placement="topLeft">
// {text}
// </Tooltip>
// ) : (
// '--'
// ),
// },
// {
// key: 'image_status',
// title: '镜像状态',
// dataIndex: 'image_status',
// width: 90,
// render: (text: number) => (text ? getStatusTag(text) : '--'),
// defaultVisible: true,
// },
{
key: 'create_time',
title: '上传时间',
dataIndex: 'create_time',
width: 160,
render: (text: string) =>
text ? (
<Tooltip title={dayjs(text).format('YYYY-MM-DD HH:mm:ss')}>
{text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '--'}
</Tooltip>
) : (
'--'
),
defaultVisible: true,
ellipsis: true,
},
{
key: 'action',
title: '操作',
width: 90,
align: 'center',
fixed: 'right' as 'right',
render: (_: any, record: IMAGES.ImageItem) => (
<Space size="small">
{/* <Button
type="text"
icon={<EyeOutlined />}
onClick={() => handleViewDetail(record)}
title="查看详情"
/> */}
<Button
size="small"
type="link"
title="编辑"
onClick={() => handleViewDetail(record)}
>
</Button>
<Popconfirm
title="确定要删除这个镜像吗?"
description="删除后无法恢复,请谨慎操作。"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
{/* <Button type="text" icon={<DeleteOutlined />} title="删除" danger /> */}
<Button
size="small"
type="link"
title="删除"
>
</Button>
</Popconfirm>
</Space>
),
defaultVisible: true,
},
];
// 初始化 visibleColumns 状态
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
(acc, column) => {
if (!column.alwaysVisible) {
acc[column.key] = column.defaultVisible;
}
return acc;
},
{},
);
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
initialVisibleColumns,
);
// 重置列设置
const resetColumns = () => {
setVisibleColumns(initialVisibleColumns);
};
const getStatusTag = (status: number) => {
const config = STATUS_MAP[status as keyof typeof STATUS_MAP];
return <Tag color={config?.color}>{config.text}</Tag>;
@ -513,7 +540,7 @@ const ImageList: React.FC = () => {
return (
<div className="image-list">
<div className="search-box">
<Button onClick={() => setImportModalVisible(true)}></Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setImportModalVisible(true)}></Button>
<div className="search-input">
<Input.Search
placeholder="镜像名称"

View File

@ -0,0 +1,258 @@
import { ERROR_CODE, NETWORK_TYPE_LIST } from '@/constants/constants';
import { createNetwork, updateNetwork } from '@/services/network';
import { Form, Input, Modal, Select, Switch, message } from 'antd';
import React, { useEffect } from 'react';
const { Option } = Select;
interface NetworkFormData {
id?: number;
network_name: string;
bridge__name: string;
type: 'nat' | 'isolated' | 'bridge';
autostart: boolean;
ip_range?: string;
gateway?: string;
netmask?: string;
dhcp_start?: string;
dhcp_end?: string;
dhcp_enabled: boolean;
}
interface NetworkEditModalProps {
visible: boolean;
onCancel: () => void;
onOk: (values: NetworkFormData) => void;
initialValues?: NetworkFormData;
title?: string;
}
const NetworkEditModal: React.FC<NetworkEditModalProps> = ({
visible,
onCancel,
onOk,
initialValues,
title = '新增网络',
}) => {
const [form] = Form.useForm<NetworkFormData>();
useEffect(() => {
if (visible && initialValues) {
form.setFieldsValue(initialValues);
} else if (visible) {
form.resetFields();
}
}, [visible, initialValues, form]);
//新增网络
const onCreateNetwork = (payload: any) => {
createNetwork(payload).then((res: any) => {
console.log('res=======onCreateNetwork', res);
const { code } = res || {};
if (code === ERROR_CODE) {
message.success('网络新建成功');
onOk(payload);
form.resetFields();
} else {
message.error(res?.data || res?.message || '网络新建失败');
}
});
};
//编辑网络
const onUpdateNetwork = (payload: any) => {
updateNetwork(payload).then((res: any) => {
console.log('res=======onCreateNetwork', res);
const { code } = res || {};
if (code === ERROR_CODE) {
message.success('网络修改成功');
onOk(payload);
form.resetFields();
} else {
message.error(res?.data || res?.message || '网修改建失败');
}
});
};
const handleOk = async () => {
try {
const values = await form.validateFields();
const { autostart, dhcp_enabled } = values || {};
const params: any = { ...values };
params.autostart = autostart ? 1 : 0;
params.dhcp_enabled = dhcp_enabled ? 1 : 0;
if (initialValues?.id) {
params.id = initialValues?.id;
onUpdateNetwork(params);
} else {
onCreateNetwork(params);
}
} catch (error) {
console.error('表单验证失败:', error);
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
const networkType = Form.useWatch('type', form);
return (
<Modal
title={title}
open={visible}
onOk={handleOk}
onCancel={handleCancel}
width={600}
// unmountOnClose
>
<Form
form={form}
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
initialValues={{
autostart: true,
enable_dhcp: true,
type: 'nat',
}}
>
<Form.Item
label="网络名称"
name="network_name"
rules={[
{ required: true, message: '请输入网络名称' },
{ max: 50, message: '网络名称最多50个字符' },
]}
>
<Input
disabled={initialValues?.id ? true : false}
placeholder="请输入网络名称"
/>
</Form.Item>
<Form.Item
label="桥接名称"
name="bridge_name"
rules={[
{ required: false, message: '请输入桥接名称' },
{ max: 30, message: '桥接名称最多30个字符' },
]}
>
<Input placeholder="请输入桥接名称" />
</Form.Item>
<Form.Item
label="网络类型"
name="type"
rules={[{ required: true, message: '请选择网络类型' }]}
extra={
networkType === 'nat'
? 'NAT模式:虚拟机通过宿主机访问外网,适合大多数场景'
: networkType === 'isolated'
? '隔离模式:虚拟机之间可通信,但无法访问外网'
: '桥接模式:虚拟机直接连接到物理网络,获得独立IP'
}
>
<Select placeholder="请选择网络类型">
{NETWORK_TYPE_LIST.map((item) => {
const { value, label } = item || {};
return (
<Option key={value} value={value}>
{label}
</Option>
);
})}
</Select>
</Form.Item>
{(networkType === 'nat' || networkType === 'isolated') && (
<>
<Form.Item
label="IP范围"
name="ip_range"
rules={[
{ required: true, message: '请输入IP范围' },
{
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/,
message: '请输入有效的IP范围格式192.168.1.0/24',
},
]}
>
<Input placeholder="如192.168.1.0/24" />
</Form.Item>
<Form.Item
label="网关地址"
name="gateway"
rules={[
{ required: true, message: '请输入网关地址' },
{
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
message: '请输入有效的IP地址格式',
},
]}
>
<Input placeholder="如192.168.1.1" />
</Form.Item>
<Form.Item
label="子网掩码"
name="netmask"
rules={[
{ required: true, message: '请输入子网掩码' },
{
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
message: '请输入有效的子网掩码格式',
},
]}
>
<Input placeholder="如255.255.255.0" />
</Form.Item>
<Form.Item
label="DHCP起始地址"
name="dhcp_start"
rules={[
{ required: false, message: '请输入DHCP起始地址' },
{
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
message: '请输入有效的IP地址格式',
},
]}
>
<Input placeholder="如192.168.1.100" />
</Form.Item>
<Form.Item
label="DHCP结束地址"
name="dhcp_end"
rules={[
{ required: false, message: '请输入DHCP结束地址' },
{
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
message: '请输入有效的IP地址格式',
},
]}
>
<Input placeholder="如192.168.1.200" />
</Form.Item>
<Form.Item
label="启用DHCP"
name="enable_dhcp"
valuePropName="checked"
>
<Switch />
</Form.Item>
</>
)}
<Form.Item label="开机自启动" name="autostart" valuePropName="checked">
<Switch />
</Form.Item>
</Form>
</Modal>
);
};
export default NetworkEditModal;

View File

@ -0,0 +1,138 @@
import type { TableProps } from 'antd';
import { useCallback, useState } from 'react';
const useTableParams = (
initialParams: NETWORK.TableParams = {
pagination: { current: 1, pageSize: 10 },
filters: {},// 表格的搜索对象
sort: {},
search: {}, // 添加搜索参数对象
},
) => {
const [tableParams, setTableParams] =
useState<NETWORK.TableParams>(initialParams);
const getApiParams = useCallback(() => {
const { pagination, filters, sort, search, ...rest } = tableParams;
const apiParams: Record<string, any> = {
page_size: pagination?.pageSize,
page_num: pagination?.current,
...rest,
};
if (sort?.field) {
apiParams.orderby = sort.field;
apiParams.order = sort.order === 'ascend' ? 'asc' : 'desc';
}
// 处理表格搜索参数
Object.entries(filters || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
apiParams[key] = value;
}
});
// 处理搜索参数
Object.entries(search || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
apiParams[key] = value;
}
});
console.log('getApiParams apiParams', apiParams);
return apiParams;
}, [tableParams]);
// 统一的更新方法,可以处理所有参数类型
const updateParams = useCallback(
(
newParams: Partial<NETWORK.TableParams>,
options?: { resetPage?: boolean },
) => {
// console.log('updateParams', newParams);
setTableParams((prev) => {
// 如果是搜索或过滤相关的更新,重置到第一页
const shouldResetPage =
options?.resetPage ??
((newParams.search && Object.keys(newParams.search).length > 0) || // 有搜索值
(newParams.filters && Object.keys(newParams.filters).length > 0)); // 有过滤值
return {
...prev,
...newParams,
pagination: {
...prev.pagination,
...newParams.pagination,
...(shouldResetPage ? { current: 1 } : {}), // 根据条件决定是否重置页码
},
};
});
},
[],
);
/**
*
* @param pagination
* @param filters filters
* @param sorter
* @param extra
* @returns void
* */
const handleTableChange = useCallback<
NonNullable<TableProps<NETWORK.NetworkItem>['onChange']>
>(
(pagination, filters, sorter) => {
// console.log('handleTableChange',pagination,filters,sorter,extra);
// 过滤掉空值的filters
const filteredFilters: Record<string, any> = {};
Object.entries(filters || {}).forEach(([key, value]) => {
if (key === 'image_type') {
if (Array.isArray(value) && value.length > 0 && value[0] !== '全部') {
filteredFilters[key] = Number(value[0]);
}
} else {
if (Array.isArray(value) && value.length > 0) {
filteredFilters[key] = value[0];
} else if (value !== undefined && value !== null) {
if (!Array.isArray(value) && value !== '') {
filteredFilters[key] = value;
}
}
}
});
const newParams: Partial<NETWORK.TableParams> = {
pagination: {
current: pagination.current || 1,
pageSize: pagination.pageSize || 10,
},
filters: filteredFilters,
};
if (!Array.isArray(sorter)) {
newParams.sort = {
field: sorter.field as string,
order:
sorter.order === 'ascend' || sorter.order === 'descend'
? sorter.order
: undefined,
};
}
// console.log('handleTableChange', newParams);
updateParams(newParams);
},
[updateParams],
);
return {
tableParams,
getApiParams,
updateParams, // 统一的更新方法
handleTableChange,
};
};
export default useTableParams;

View File

@ -0,0 +1,172 @@
// 页面头部样式
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
}
// 镜像列表样式
.image-list {
width:100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 10px;
box-sizing: border-box;
.search-box {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
.search-input {
display: flex;
gap: 8px;
align-items: center;
}
}
.images-list-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.images-list-table {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
// 表格适应样式
.ant-table-wrapper {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-spin-nested-loading {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-spin-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-table {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-container {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-header {
flex-shrink: 0;
}
.ant-table-body {
flex: 1;
overflow: auto !important;
}
}
// 确保分页器在底部正确显示
.ant-table-pagination {
flex-shrink: 0;
// 确保分页器始终可见
position: relative;
z-index: 1;
}
}
}
}
}
}
}
.image-detail {
.detail-item {
margin-bottom: 16px;
label {
font-weight: 600;
color: #333;
display: inline-block;
width: 100px;
}
span {
color: #666;
}
p {
margin: 8px 0 0 100px;
color: #666;
line-height: 1.6;
}
}
}
}
// 个人资料样式
.profile-page {
.profile-content {
.profile-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
.profile-info {
h3 {
margin: 0 0 4px 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
p {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
.quick-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.profile-content {
.profile-header {
flex-direction: column;
text-align: center;
}
.quick-actions {
justify-content: center;
}
}
}

View File

@ -0,0 +1,613 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
import {
DHCP_STATUS,
NETWORK_STATUS,
NETWORK_TYPE,
NETWORK_TYPE_LIST,
NETWORK_STATUS_LIST
} from '@/constants/constants';
import { CODE } from '@/constants/images.constants';
import { getNetworkList,deleteNetwork } from '@/services/network';
import { PlusOutlined, SettingOutlined } from '@ant-design/icons';
import {
Button,
Checkbox,
Input,
message,
Modal,
Popconfirm,
Popover,
Select,
Space,
Table,
Tag,
Tooltip,
} from 'antd';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import NetworkEditModal from './components/NetworkEditModal';
import useTableParams from './hook/hook';
import './index.less';
const { Option } = Select;
// 列配置定义
type ColumnConfig = {
key: string;
title: string;
dataIndex?: string;
width: number;
render?: (text: any, record: any, index: number) => React.ReactNode;
fixed?: 'left' | 'right';
align?: 'left' | 'center' | 'right';
defaultVisible: boolean; // 默认是否显示
alwaysVisible?: boolean; // 始终显示的列
ellipsis?: boolean; // 是否启用省略号
filters?: { text: string; value: string }[];
filterMultiple?: boolean; // 是否多选过滤
filterDropdown?: (props: any) => React.ReactNode;
defaultFilteredValue?: string[]; // 默认过滤值
onFilter?: (value: string, record: any) => boolean;
};
type TableColumn = {
title: string;
dataIndex?: string;
key: string;
width: number;
render?: any;
fixed?: 'left' | 'right';
hidden?: boolean;
};
// 在组件顶部添加防抖函数(支持取消)
// 增强版防抖函数,使用泛型明确函数类型
const debounce = <T extends (...args: any[]) => any>(
func: T,
delay: number,
immediate = false,
) => {
let timer: NodeJS.Timeout | null = null;
const debounced = (...args: Parameters<T>) => {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
func(...args);
}
timer = setTimeout(() => {
if (!immediate) {
func(...args);
}
timer = null;
}, delay);
};
debounced.cancel = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
return debounced;
};
const Index = () => {
const [dataSource, setDataSource] = useState<NETWORK.NetworkItem[]>([]);
const [loading, setLoading] = useState(false);
const [selectedNetwork, setSelectedNetwork] = useState<any>(null);
const [editModalVisible, setEditModalVisible] = useState(false);
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
const [networkType, setNetworkType] = useState<string>('不限'); // 网络类型
const [status, setStatus] = useState<string>('不限'); // 网络类型
const { tableParams, getApiParams, updateParams, handleTableChange } =
useTableParams({
pagination: {
current: 1,
pageSize: 10,
},
search: {}, // 初始化搜索参数
});
// 在组件顶部添加一个 ref 来保存最新的 tableParams
const tableParamsRef = useRef(tableParams);
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
// 表格参数变化 获取镜像列表
useEffect(() => {
loadDataSource();
}, [
status,
networkType,
tableParams.pagination?.current,
tableParams.pagination?.pageSize,
tableParams?.sortOrder,
tableParams?.sortField,
JSON.stringify(tableParams.filters), // 表格搜索参数
JSON.stringify(tableParams.search), // 搜索参数依赖
]);
// 定义所有列的配置
const columnConfigs: ColumnConfig[] = [
{
key: 'index',
title: '序号',
width: 60,
render: (text: any, row: any, index: number) =>
(tableParams.pagination?.current - 1) *
tableParams.pagination?.pageSize +
index +
1,
defaultVisible: true,
alwaysVisible: true,
},
{
key: 'network_name',
title: '网络名称',
dataIndex: 'network_name',
width: 150,
defaultVisible: true,
alwaysVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'type',
title: '网络类型',
dataIndex: 'type',
width: 150,
defaultVisible: true,
ellipsis: true,
render: (text: string) => {
const key = text ? (text as keyof typeof NETWORK_TYPE) : '';
return (
<Tooltip title={key ? NETWORK_TYPE[key] : '--'}>
{key ? NETWORK_TYPE[key] : '--'}
</Tooltip>
);
},
},
{
key: 'bridge_name',
title: '桥接',
dataIndex: 'bridge_name',
width: 150,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'ip_range',
// title: '模板存放路径',
title: 'IP范围',
dataIndex: 'ip_range',
width: 140,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'dhcp_enable',
title: 'DHCP',
dataIndex: 'dhcp_enable',
width: 120,
defaultVisible: true,
ellipsis: true,
render: (text: number) => {
const key = text ? (text as keyof typeof DHCP_STATUS) : '';
return text ? (
<Tag color={text === 1 ? 'green' : 'red'}>
{' '}
{key ? DHCP_STATUS[key] : '--'}
</Tag>
) : null;
},
},
{
key: 'status',
title: '状态',
dataIndex: 'status',
width: 100,
defaultVisible: true,
ellipsis: true,
//NETWORK_STATUS
render: (text: number) => {
const key = text ? (text as keyof typeof NETWORK_STATUS) : '';
return text ? (
<Tag color={text === 1 ? 'green' : 'red'}>
{' '}
{key ? NETWORK_STATUS[key] : '--'}
</Tag>
) : null;
},
},
{
key: 'action',
title: '操作',
width: 120,
align: 'center',
fixed: 'right' as 'right',
render: (_: any, record: NETWORK.NetworkItem) => (
<Space size="small">
<Button
size="small"
type="link"
title="编辑"
onClick={() => handleEdit(record)}
>
</Button>
<Popconfirm
title="确定要删除这个网络吗?"
description="删除后无法恢复,请谨慎操作。"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button size="small" type="link" title="删除">
</Button>
</Popconfirm>
</Space>
),
defaultVisible: true,
},
];
// 初始化 visibleColumns 状态
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
(acc, column) => {
if (!column.alwaysVisible) {
acc[column.key] = column.defaultVisible;
}
return acc;
},
{},
);
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
initialVisibleColumns,
);
// 重置列设置
const resetColumns = () => {
setVisibleColumns(initialVisibleColumns);
};
const loadDataSource = async () => {
setLoading(true);
try {
// 将搜索文本合并到API参数中
const apiParams = {
...getApiParams(),
};
if(networkType!=="不限"){
apiParams.type=networkType;
}
if(status!=="不限"){
apiParams.status=status;
}
const imagesRes = await getNetworkList(apiParams);
if (imagesRes.code === CODE) {
setDataSource(imagesRes.data?.data || []);
setLoading(false);
// 正确处理后端返回的分页信息
updateParams({
pagination: {
...tableParams.pagination,
current: imagesRes.data?.page_num || 1,
total: imagesRes.data?.total || 0,
pageSize: tableParams.pagination?.pageSize || 10,
},
});
} else {
message.error(imagesRes.message || '获取网络列表失败');
setLoading(false);
}
} catch (err) {
message.error('获取网络列表失败');
setLoading(false);
}
};
const handleAdd = () => {
setSelectedNetwork(null);
setEditModalVisible(true);
};
const handleEdit = (record: NETWORK.NetworkItem) => {
setSelectedNetwork(record);
setEditModalVisible(true);
};
const handleDelete = (record: NETWORK.NetworkItem) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除网络 "${record.network_name}" 吗?`,
onOk: () => {
deleteNetwork({ id: record.id }).then((res) => {
if (res.code === CODE) {
message.success('删除成功');
loadDataSource();
} else {
message.error(res.message || '删除失败');
}
});
},
});
};
const handleEditModalOk = (values: any) => {
// 这里可以调用新增或编辑网络的API
if (selectedNetwork) {
// 编辑模式
message.success('编辑网络成功');
} else {
// 新增模式
message.success('新增网络成功');
}
setEditModalVisible(false);
loadDataSource();
};
const handleEditModalCancel = () => {
setEditModalVisible(false);
setSelectedNetwork(null);
};
// 列设置相关函数
const handleColumnChange = (columnKey: string, checked: boolean) => {
setVisibleColumns((prev) => ({
...prev,
[columnKey]: checked,
}));
};
// 列设置内容
const columnSettingsContent = (
<div style={{ padding: '8px 0' }}>
{columnConfigs
.filter((config) => !config.alwaysVisible) // 只显示可控制的列
.map((config) => (
<div key={config.key} style={{ padding: '4px 12px' }}>
<Checkbox
checked={visibleColumns[config.key]}
onChange={(e) => handleColumnChange(config.key, e.target.checked)}
>
{config.title}
</Checkbox>
</div>
))}
<div
style={{
padding: '8px 12px',
borderTop: '1px solid #f0f0f0',
marginTop: 8,
}}
>
<Button type="link" onClick={resetColumns} style={{ padding: 0 }}>
</Button>
</div>
</div>
);
// 根据visibleColumns过滤显示的列
const filteredColumns = columnConfigs
.map((config) => {
// 对于始终显示的列
if (config.alwaysVisible) {
return {
...config,
hidden: undefined,
};
}
// 对于可控制显示/隐藏的列
return {
...config,
...(visibleColumns[config.key] ? {} : { hidden: true }),
};
})
.filter((column) => !column.hidden) as TableColumn[];
const handleRefresh = () => {
loadDataSource();
};
// 自定义分页配置
const paginationConfig = {
...tableParams.pagination,
showTotal: (total: number) => `${total} 条记录`,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
};
const handleSearch = useCallback(
(searchValue: string) => {
const currentTableParams = tableParamsRef.current;
updateParams({
search: {
network_name: searchValue,
},
pagination: {
current: 1,
pageSize: currentTableParams.pagination?.pageSize || 10,
},
});
},
[updateParams],
);
// 防抖版本500ms延迟不立即执行
const debouncedSearch = useRef(debounce(handleSearch, 500)).current;
// 立即执行版本(用于清空时立即搜索)
const immediateSearch = useRef(debounce(handleSearch, 0, true)).current;
const handleSearchChange = (value: string) => {
setSearchText(value);
// 取消所有未执行的防抖请求
debouncedSearch.cancel();
immediateSearch.cancel();
// 清空时立即触发搜索
if (value === '') {
immediateSearch('');
return;
}
// 正常输入时使用防抖
debouncedSearch(value);
};
// 修改回车搜索处理
const handleEnterSearch = (value: string) => {
// 回车搜索时取消未执行的防抖
debouncedSearch.cancel();
immediateSearch.cancel();
// 直接执行搜索
handleSearch(value);
};
//类型查询
const handleTypeChange = (value: string) => {
debouncedSearch.cancel();
immediateSearch.cancel();
// 直接执行搜索
setNetworkType(value);
};
// 状态查询
const handleStatusChange = (value: string) => {
debouncedSearch.cancel();
immediateSearch.cancel();
// 直接执行搜索
setStatus(value);
};
return (
<div className="image-list">
<div className="search-box">
<div>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
</Button>
</div>
<div className="search-input">
{/* status */}
<span></span>
<Select
// defaultValue={'不限'}
value={networkType}
onChange={handleTypeChange}
style={{ width: '180px' }}
>
{[{ value: '不限', label: '不限' }, ...NETWORK_TYPE_LIST].map(
(item) => {
const { value, label } = item || {};
return (
<Option key={value} value={value}>
{label}
</Option>
);
},
)}
</Select>
<span></span>
<Select
// defaultValue={'不限'}
value={status}
onChange={handleStatusChange}
style={{ width: '180px' }}
>
{[{ value: '不限', label: '不限' }, ...NETWORK_STATUS_LIST].map(
(item) => {
const { value, label } = item || {};
return (
<Option key={value} value={value}>
{label}
</Option>
);
},
)}
</Select>
<Input.Search
placeholder="网络名称"
value={searchText}
onChange={(e) => handleSearchChange(e.target.value)}
style={{ width: 300 }}
onSearch={handleEnterSearch}
/>
<Button
onClick={handleRefresh}
loading={loading}
icon={<RefreshIcon style={{ width: 13, height: 13 }} />}
></Button>
<Popover
content={columnSettingsContent}
title="列设置"
trigger="click"
open={columnSettingsVisible}
onOpenChange={setColumnSettingsVisible}
placement="bottomRight"
>
<Button icon={<SettingOutlined />}></Button>
</Popover>
</div>
</div>
<div className="images-list-container">
<div className="images-list-table">
<Table
columns={filteredColumns}
dataSource={dataSource}
rowKey="id"
loading={loading}
pagination={paginationConfig}
onChange={handleTableChange}
scroll={{
y: 'max-content', // 关键:允许内容决定高度
}}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
/>
</div>
</div>
<NetworkEditModal
visible={editModalVisible}
onCancel={handleEditModalCancel}
onOk={handleEditModalOk}
initialValues={selectedNetwork}
title={selectedNetwork ? '编辑网络' : '新增网络'}
/>
</div>
);
};
export default Index;

View File

@ -0,0 +1,216 @@
import { NETWORK_TYPE_LIST } from '@/constants/constants';
import { Form, Input, Modal, Select, Switch } from 'antd';
import React, { useEffect } from 'react';
const { Option } = Select;
interface NetworkFormData {
network_name: string;
bridge_name: string;
network_type: 'NAT' | 'Isolated' | 'Bridge' | 'Open';
auto_start: boolean;
ip_range?: string;
gateway_address?: string;
subnet_mask?: string;
dhcp_start_address?: string;
dhcp_end_address?: string;
enable_dhcp: boolean;
}
interface NetworkEditModalProps {
visible: boolean;
onCancel: () => void;
onOk: (values: NetworkFormData) => void;
initialValues?: NetworkFormData;
title?: string;
}
const NetworkEditModal: React.FC<NetworkEditModalProps> = ({
visible,
onCancel,
onOk,
initialValues,
title = '新增网络',
}) => {
const [form] = Form.useForm<NetworkFormData>();
useEffect(() => {
if (visible && initialValues) {
form.setFieldsValue(initialValues);
} else if (visible) {
form.resetFields();
}
}, [visible, initialValues, form]);
const handleOk = async () => {
try {
const values = await form.validateFields();
onOk(values);
form.resetFields();
} catch (error) {
console.error('表单验证失败:', error);
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
const networkType = Form.useWatch('network_type', form);
return (
<Modal
title={title}
open={visible}
onOk={handleOk}
onCancel={handleCancel}
width={600}
// unmountOnClose
>
<Form
form={form}
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
initialValues={{
auto_start: true,
enable_dhcp: true,
network_type: 'NAT',
}}
>
<Form.Item
label="网络名称"
name="network_name"
rules={[
{ required: true, message: '请输入网络名称' },
{ max: 50, message: '网络名称最多50个字符' },
]}
>
<Input placeholder="请输入网络名称" />
</Form.Item>
<Form.Item
label="桥接名称"
name="bridge_name"
rules={[
{ required: false, message: '请输入桥接名称' },
{ max: 30, message: '桥接名称最多30个字符' },
]}
>
<Input placeholder="请输入桥接名称" />
</Form.Item>
<Form.Item
label="网络类型"
name="network_type"
rules={[{ required: true, message: '请选择网络类型' }]}
extra={
networkType === 'NAT'
? 'NAT模式:虚拟机通过宿主机访问外网,适合大多数场景'
: networkType === 'Isolated'
? '隔离模式:虚拟机之间可通信,但无法访问外网'
: '桥接模式:虚拟机直接连接到物理网络,获得独立IP'
}
>
<Select placeholder="请选择网络类型">
{NETWORK_TYPE_LIST.map((item) => {
const { value, label } = item || {};
return (
<Option key={value} value={value}>
{label}
</Option>
);
})}
</Select>
</Form.Item>
{(networkType === 'NAT' || networkType === 'Isolated') && (
<>
<Form.Item
label="IP范围"
name="ip_range"
rules={[
{ required: true, message: '请输入IP范围' },
{
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/,
message: '请输入有效的IP范围格式192.168.1.0/24',
},
]}
>
<Input placeholder="如192.168.1.0/24" />
</Form.Item>
<Form.Item
label="网关地址"
name="gateway_address"
rules={[
{ required: true, message: '请输入网关地址' },
{
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
message: '请输入有效的IP地址格式',
},
]}
>
<Input placeholder="如192.168.1.1" />
</Form.Item>
<Form.Item
label="子网掩码"
name="subnet_mask"
rules={[
{ required: true, message: '请输入子网掩码' },
{
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
message: '请输入有效的子网掩码格式',
},
]}
>
<Input placeholder="如255.255.255.0" />
</Form.Item>
<Form.Item
label="DHCP起始地址"
name="dhcp_start_address"
rules={[
{ required: false, message: '请输入DHCP起始地址' },
{
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
message: '请输入有效的IP地址格式',
},
]}
>
<Input placeholder="如192.168.1.100" />
</Form.Item>
<Form.Item
label="DHCP结束地址"
name="dhcp_end_address"
rules={[
{ required: false, message: '请输入DHCP结束地址' },
{
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
message: '请输入有效的IP地址格式',
},
]}
>
<Input placeholder="如192.168.1.200" />
</Form.Item>
<Form.Item
label="启用DHCP"
name="enable_dhcp"
valuePropName="checked"
>
<Switch checkedChildren="是" unCheckedChildren="否" />
</Form.Item>
</>
)}
<Form.Item label="开机自启动" name="auto_start" valuePropName="checked">
<Switch checkedChildren="是" unCheckedChildren="否" />
</Form.Item>
</Form>
</Modal>
);
};
export default NetworkEditModal;

View File

@ -0,0 +1,138 @@
import type { TableProps } from 'antd';
import { useCallback, useState } from 'react';
const useTableParams = (
initialParams: STORAGE.TableParams = {
pagination: { current: 1, pageSize: 10 },
filters: {},// 表格的搜索对象
sort: {},
search: {}, // 添加搜索参数对象
},
) => {
const [tableParams, setTableParams] =
useState<STORAGE.TableParams>(initialParams);
const getApiParams = useCallback(() => {
const { pagination, filters, sort, search, ...rest } = tableParams;
const apiParams: Record<string, any> = {
page_size: pagination?.pageSize,
page_num: pagination?.current,
...rest,
};
if (sort?.field) {
apiParams.orderby = sort.field;
apiParams.order = sort.order === 'ascend' ? 'asc' : 'desc';
}
// 处理表格搜索参数
Object.entries(filters || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
apiParams[key] = value;
}
});
// 处理搜索参数
Object.entries(search || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
apiParams[key] = value;
}
});
console.log('getApiParams apiParams', apiParams);
return apiParams;
}, [tableParams]);
// 统一的更新方法,可以处理所有参数类型
const updateParams = useCallback(
(
newParams: Partial<STORAGE.TableParams>,
options?: { resetPage?: boolean },
) => {
// console.log('updateParams', newParams);
setTableParams((prev) => {
// 如果是搜索或过滤相关的更新,重置到第一页
const shouldResetPage =
options?.resetPage ??
((newParams.search && Object.keys(newParams.search).length > 0) || // 有搜索值
(newParams.filters && Object.keys(newParams.filters).length > 0)); // 有过滤值
return {
...prev,
...newParams,
pagination: {
...prev.pagination,
...newParams.pagination,
...(shouldResetPage ? { current: 1 } : {}), // 根据条件决定是否重置页码
},
};
});
},
[],
);
/**
*
* @param pagination
* @param filters filters
* @param sorter
* @param extra
* @returns void
* */
const handleTableChange = useCallback<
NonNullable<TableProps<STORAGE.StorageItem>['onChange']>
>(
(pagination, filters, sorter) => {
// console.log('handleTableChange',pagination,filters,sorter,extra);
// 过滤掉空值的filters
const filteredFilters: Record<string, any> = {};
Object.entries(filters || {}).forEach(([key, value]) => {
if (key === 'image_type') {
if (Array.isArray(value) && value.length > 0 && value[0] !== '全部') {
filteredFilters[key] = Number(value[0]);
}
} else {
if (Array.isArray(value) && value.length > 0) {
filteredFilters[key] = value[0];
} else if (value !== undefined && value !== null) {
if (!Array.isArray(value) && value !== '') {
filteredFilters[key] = value;
}
}
}
});
const newParams: Partial<STORAGE.TableParams> = {
pagination: {
current: pagination.current || 1,
pageSize: pagination.pageSize || 10,
},
filters: filteredFilters,
};
if (!Array.isArray(sorter)) {
newParams.sort = {
field: sorter.field as string,
order:
sorter.order === 'ascend' || sorter.order === 'descend'
? sorter.order
: undefined,
};
}
// console.log('handleTableChange', newParams);
updateParams(newParams);
},
[updateParams],
);
return {
tableParams,
getApiParams,
updateParams, // 统一的更新方法
handleTableChange,
};
};
export default useTableParams;

View File

@ -0,0 +1,172 @@
// 页面头部样式
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
}
// 镜像列表样式
.image-list {
width:100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 10px;
box-sizing: border-box;
.search-box {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
.search-input {
display: flex;
gap: 8px;
align-items: center;
}
}
.images-list-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.images-list-table {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
// 表格适应样式
.ant-table-wrapper {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-spin-nested-loading {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-spin-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-table {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-container {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-header {
flex-shrink: 0;
}
.ant-table-body {
flex: 1;
overflow: auto !important;
}
}
// 确保分页器在底部正确显示
.ant-table-pagination {
flex-shrink: 0;
// 确保分页器始终可见
position: relative;
z-index: 1;
}
}
}
}
}
}
}
.image-detail {
.detail-item {
margin-bottom: 16px;
label {
font-weight: 600;
color: #333;
display: inline-block;
width: 100px;
}
span {
color: #666;
}
p {
margin: 8px 0 0 100px;
color: #666;
line-height: 1.6;
}
}
}
}
// 个人资料样式
.profile-page {
.profile-content {
.profile-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
.profile-info {
h3 {
margin: 0 0 4px 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
p {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
.quick-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.profile-content {
.profile-header {
flex-direction: column;
text-align: center;
}
.quick-actions {
justify-content: center;
}
}
}

View File

@ -0,0 +1,524 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { NETWORK_TYPE } from '@/constants/constants';
import { CODE } from '@/constants/images.constants';
import { deleteTool } from '@/services/imagePage';
import { getNetworkList } from '@/services/network';
import { SettingOutlined, PlusOutlined } from '@ant-design/icons';
import {
Button,
Checkbox,
Input,
message,
Modal,
Popconfirm,
Popover,
Space,
Table,
Tooltip,
} from 'antd';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import useTableParams from './hook/hook';
import NetworkEditModal from './components/NetworkEditModal';
import './index.less';
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
// 列配置定义
type ColumnConfig = {
key: string;
title: string;
dataIndex?: string;
width: number;
render?: (text: any, record: any, index: number) => React.ReactNode;
fixed?: 'left' | 'right';
align?: 'left' | 'center' | 'right';
defaultVisible: boolean; // 默认是否显示
alwaysVisible?: boolean; // 始终显示的列
ellipsis?: boolean; // 是否启用省略号
filters?: { text: string; value: string }[];
filterMultiple?: boolean; // 是否多选过滤
filterDropdown?: (props: any) => React.ReactNode;
defaultFilteredValue?: string[]; // 默认过滤值
onFilter?: (value: string, record: any) => boolean;
};
type TableColumn = {
title: string;
dataIndex?: string;
key: string;
width: number;
render?: any;
fixed?: 'left' | 'right';
hidden?: boolean;
};
// 在组件顶部添加防抖函数(支持取消)
// 增强版防抖函数,使用泛型明确函数类型
const debounce = <T extends (...args: any[]) => any>(
func: T,
delay: number,
immediate = false,
) => {
let timer: NodeJS.Timeout | null = null;
const debounced = (...args: Parameters<T>) => {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
func(...args);
}
timer = setTimeout(() => {
if (!immediate) {
func(...args);
}
timer = null;
}, delay);
};
debounced.cancel = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
return debounced;
};
const Index = () => {
const [dataSource, setDataSource] = useState<STORAGE.StorageItem[]>([]);
const [loading, setLoading] = useState(false);
const [selectedNetwork, setSelectedNetwork] = useState<any>(null);
const [editModalVisible, setEditModalVisible] = useState(false);
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
const { tableParams, getApiParams, updateParams, handleTableChange } =
useTableParams({
pagination: {
current: 1,
pageSize: 10,
},
search: {}, // 初始化搜索参数
});
// 在组件顶部添加一个 ref 来保存最新的 tableParams
const tableParamsRef = useRef(tableParams);
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
// 表格参数变化 获取镜像列表
useEffect(() => {
loadDataSource();
}, [
tableParams.pagination?.current,
tableParams.pagination?.pageSize,
tableParams?.sortOrder,
tableParams?.sortField,
JSON.stringify(tableParams.filters), // 表格搜索参数
JSON.stringify(tableParams.search), // 搜索参数依赖
]);
// 定义所有列的配置
const columnConfigs: ColumnConfig[] = [
{
key: 'index',
title: '序号',
width: 60,
render: (text: any, row: any, index: number) =>
(tableParams.pagination?.current - 1) *
tableParams.pagination?.pageSize +
index +
1,
defaultVisible: true,
alwaysVisible: true,
},
{
key: 'pool_name',
title: '名称',
dataIndex: 'pool_name',
width: 100,
defaultVisible: true,
ellipsis: true,
render: (text: string) => {
const key = text ? (text as keyof typeof NETWORK_TYPE) : '';
return (
<Tooltip title={key ? NETWORK_TYPE[key] : '--'}>
{key ? NETWORK_TYPE[key] : '--'}
</Tooltip>
);
},
},
{
key: 'type',
title: '类型',
dataIndex: 'type',
width: 100,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'path',
title: '路径',
dataIndex: 'ip_range',
width: 140,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'dhcp',
title: '总容量',
dataIndex: 'dhcp',
width: 160,
render: (text: string) =>
text ? <Tooltip title={text || ''}>{text || '--'}</Tooltip> : '--',
defaultVisible: true,
ellipsis: true,
},
{
key: 'dhcp',
title: '剩余容量',
dataIndex: 'dhcp',
width: 160,
render: (text: string) =>
text ? <Tooltip title={text || ''}>{text || '--'}</Tooltip> : '--',
defaultVisible: true,
ellipsis: true,
},
{
key: 'network_status',
title: '已分配容量',
dataIndex: 'network_status',
width: 160,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text || ''}>{text || '--'}</Tooltip> : '--',
},
{
key: 'action',
title: '操作',
width: 120,
align: 'center',
fixed: 'right' as 'right',
render: (_: any, record: STORAGE.StorageItem) => (
<Space size="small">
<Button
size="small"
type="link"
title="编辑"
onClick={() => handleEdit(record)}
>
</Button>
<Popconfirm
title="确定要删除这个网络吗?"
description="删除后无法恢复,请谨慎操作。"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button size="small" type="link" title="删除">
</Button>
</Popconfirm>
</Space>
),
defaultVisible: true,
},
];
// 初始化 visibleColumns 状态
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
(acc, column) => {
if (!column.alwaysVisible) {
acc[column.key] = column.defaultVisible;
}
return acc;
},
{},
);
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
initialVisibleColumns,
);
// 重置列设置
const resetColumns = () => {
setVisibleColumns(initialVisibleColumns);
};
const loadDataSource = async () => {
setLoading(true);
try {
// 将搜索文本合并到API参数中
const apiParams = {
...getApiParams(),
};
const imagesRes = await getNetworkList(apiParams);
if (imagesRes.code === CODE) {
setDataSource(imagesRes.data?.data || []);
setLoading(false);
// 正确处理后端返回的分页信息
updateParams({
pagination: {
...tableParams.pagination,
current: imagesRes.data?.page_num || 1,
total: imagesRes.data?.total || 0,
pageSize: tableParams.pagination?.pageSize || 10,
},
});
} else {
message.error(imagesRes.message || '获取网络列表失败');
setLoading(false);
}
} catch (err) {
message.error('获取网络列表失败');
setLoading(false);
}
};
const handleAdd = () => {
setSelectedNetwork(null);
setEditModalVisible(true);
};
const handleEdit = (record: STORAGE.StorageItem) => {
setSelectedNetwork(record);
setEditModalVisible(true);
};
const handleDelete = (record: STORAGE.StorageItem) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除网络 "${record.id}" 吗?`,
onOk: () => {
deleteTool({ id: record.id }).then((res) => {
if (res.code === CODE) {
message.success('删除成功');
loadDataSource();
} else {
message.error(res.message || '删除失败');
}
});
},
});
};
const handleEditModalOk = (values: any) => {
// 这里可以调用新增或编辑网络的API
if (selectedNetwork) {
// 编辑模式
message.success('编辑网络成功');
} else {
// 新增模式
message.success('新增网络成功');
}
setEditModalVisible(false);
loadDataSource();
};
const handleEditModalCancel = () => {
setEditModalVisible(false);
setSelectedNetwork(null);
};
// 列设置相关函数
const handleColumnChange = (columnKey: string, checked: boolean) => {
setVisibleColumns((prev) => ({
...prev,
[columnKey]: checked,
}));
};
// 列设置内容
const columnSettingsContent = (
<div style={{ padding: '8px 0' }}>
{columnConfigs
.filter((config) => !config.alwaysVisible) // 只显示可控制的列
.map((config) => (
<div key={config.key} style={{ padding: '4px 12px' }}>
<Checkbox
checked={visibleColumns[config.key]}
onChange={(e) => handleColumnChange(config.key, e.target.checked)}
>
{config.title}
</Checkbox>
</div>
))}
<div
style={{
padding: '8px 12px',
borderTop: '1px solid #f0f0f0',
marginTop: 8,
}}
>
<Button type="link" onClick={resetColumns} style={{ padding: 0 }}>
</Button>
</div>
</div>
);
// 根据visibleColumns过滤显示的列
const filteredColumns = columnConfigs
.map((config) => {
// 对于始终显示的列
if (config.alwaysVisible) {
return {
...config,
hidden: undefined,
};
}
// 对于可控制显示/隐藏的列
return {
...config,
...(visibleColumns[config.key] ? {} : { hidden: true }),
};
})
.filter((column) => !column.hidden) as TableColumn[];
const handleRefresh = () => {
loadDataSource();
};
// 自定义分页配置
const paginationConfig = {
...tableParams.pagination,
showTotal: (total: number) => `${total} 条记录`,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
};
const handleSearch = useCallback(
(searchValue: string) => {
const currentTableParams = tableParamsRef.current;
updateParams({
search: {
pool_name: searchValue,
},
pagination: {
current: 1,
pageSize: currentTableParams.pagination?.pageSize || 10,
},
});
},
[updateParams],
);
// 防抖版本500ms延迟不立即执行
const debouncedSearch = useRef(debounce(handleSearch, 500)).current;
// 立即执行版本(用于清空时立即搜索)
const immediateSearch = useRef(debounce(handleSearch, 0, true)).current;
const handleSearchChange = (value: string) => {
setSearchText(value);
// 取消所有未执行的防抖请求
debouncedSearch.cancel();
immediateSearch.cancel();
// 清空时立即触发搜索
if (value === '') {
immediateSearch('');
return;
}
// 正常输入时使用防抖
debouncedSearch(value);
};
// 修改回车搜索处理
const handleEnterSearch = (value: string) => {
// 回车搜索时取消未执行的防抖
debouncedSearch.cancel();
immediateSearch.cancel();
// 直接执行搜索
handleSearch(value);
};
return (
<div className="image-list">
<div className="search-box">
<div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
>
</Button>
</div>
<div className="search-input">
<Input.Search
placeholder="名称"
value={searchText}
onChange={(e) => handleSearchChange(e.target.value)}
style={{ width: 300 }}
onSearch={handleEnterSearch}
/>
<Button
onClick={handleRefresh}
loading={loading}
icon={<RefreshIcon style={{ width: 13, height: 13 }} />}
></Button>
<Popover
content={columnSettingsContent}
title="列设置"
trigger="click"
open={columnSettingsVisible}
onOpenChange={setColumnSettingsVisible}
placement="bottomRight"
>
<Button icon={<SettingOutlined />}></Button>
</Popover>
</div>
</div>
<div className="images-list-container">
<div className="images-list-table">
<Table
columns={filteredColumns}
dataSource={dataSource}
rowKey="id"
loading={loading}
pagination={paginationConfig}
onChange={handleTableChange}
scroll={{
y: 'max-content', // 关键:允许内容决定高度
}}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
/>
</div>
</div>
<NetworkEditModal
visible={editModalVisible}
onCancel={handleEditModalCancel}
onOk={handleEditModalOk}
initialValues={selectedNetwork}
title={selectedNetwork ? '编辑网络' : '新增网络'}
/>
</div>
);
};
export default Index;

View File

@ -1,12 +1,16 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import {
DEFAULT_BLICK_TAB,
DEVICE_TYPE_MAP,
// DEVICE_TYPE_MAP,
ERROR_CODE,
} from '@/constants/constants';
import CustomTree from '@/pages/components/customTree';
import CreatGroup from '@/pages/userList/mod/group';
import { deleteDevice, getTerminalList } from '@/services/terminal';
import {
deleteDevice,
getTerminalList,
getTerminalPower,
} from '@/services/terminal';
import { deleteUserGroup, getGroupTree } from '@/services/userList';
import {
DeleteOutlined,
@ -207,7 +211,8 @@ const UserListPage: React.FC = () => {
},
},
{
title: '序列号',
// title: '序列号',
title: '终端标识',
dataIndex: 'device_id',
key: 'device_id',
width: 250,
@ -232,26 +237,27 @@ const UserListPage: React.FC = () => {
);
},
},
// {
// title: '类型',
// dataIndex: 'device_type',
// key: 'device_type',
// width: 150,
// align: 'center',
// ellipsis: true,
// render: (text: number) => {
// const key = text ? (text as keyof typeof DEVICE_TYPE_MAP) : '';
// return (
// <Tooltip title={key ? DEVICE_TYPE_MAP[key] : '--'}>
// {key ? DEVICE_TYPE_MAP[key] : '--'}
// </Tooltip>
// );
// },
// },
{
title: '类型',
dataIndex: 'device_type',
key: 'device_type',
width: 150,
align: 'center',
ellipsis: true,
render: (text: number) => {
const key = text as keyof typeof DEVICE_TYPE_MAP;
return (
<Tooltip title={DEVICE_TYPE_MAP[key] || '--'}>
{DEVICE_TYPE_MAP[key] || '--'}
</Tooltip>
);
},
},
{
title: '型号',
dataIndex: 'model',
key: 'model',
title: '状态',
dataIndex: 'status',
key: 'status',
width: 150,
align: 'center',
ellipsis: true,
@ -263,6 +269,36 @@ const UserListPage: React.FC = () => {
);
},
},
{
title: '桌面镜像',
dataIndex: 'images_name',
key: 'images_name',
width: 150,
align: 'center',
ellipsis: true,
render: (text) => {
return (
<div>
<Tooltip title={text || ''}>{text || '--'}</Tooltip>
</div>
);
},
},
// {
// title: '型号',
// dataIndex: 'model',
// key: 'model',
// width: 150,
// align: 'center',
// ellipsis: true,
// render: (text) => {
// return (
// <div>
// <Tooltip title={text || ''}>{text || '--'}</Tooltip>
// </div>
// );
// },
// },
{
title: 'IP地址',
dataIndex: 'ip_addr',
@ -293,32 +329,44 @@ const UserListPage: React.FC = () => {
);
},
},
{
title: '备注',
dataIndex: 'description',
key: 'description',
ellipsis: true,
align: 'center',
width: 200,
render: (text) => {
return (
<div>
<Tooltip title={text || ''}>{text || '--'}</Tooltip>
</div>
);
},
},
// {
// title: '备注',
// dataIndex: 'description',
// key: 'description',
// ellipsis: true,
// align: 'center',
// width: 200,
// render: (text) => {
// return (
// <div>
// <Tooltip title={text || ''}>{text || '--'}</Tooltip>
// </div>
// );
// },
// },
{
title: '操作',
key: 'actions',
align: 'center',
width: 230,
width: 170,
fixed: 'right',
render: (_, record) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Button size="small" type="link" onClick={() => {onOrgChange('stop',record)}}>
</Button>
<Button size="small" type="link" onClick={() => {onOrgChange('start',record)}}>
</Button>
<Popover
placement="bottomRight"
content={
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Button
type="link"
size="small"
// size="small"
onClick={() => {
setBindUserData({
recordData: record,
@ -330,17 +378,16 @@ const UserListPage: React.FC = () => {
</Button>
<Button
type="link"
size="small"
// size="small"
onClick={() => {
setBindImageDta({ recordData: record, visible: true });
}}
>
</Button>
<Popover
placement="bottomRight"
content={
<div>
<Button type="link" onClick={() => handleEditInfo(record)}>
</Button>
<Popconfirm
title=""
description="删除操作不可逆,请确认是否删除?"
@ -351,11 +398,6 @@ const UserListPage: React.FC = () => {
>
<Button type="link"></Button>
</Popconfirm>
<div>
<Button type="link" onClick={() => handleEditInfo(record)}>
</Button>
</div>
</div>
}
>
@ -369,6 +411,27 @@ const UserListPage: React.FC = () => {
},
];
// 终端开机、关机
const onOrgChange = (value: string, record: any) => {
getTerminalPower({
id: record.id,
status: value,
}).then((res:any) => {
const { code } = res || {};
if (code === ERROR_CODE) {
const text = value === 'start'?"开机成功":"关机成功"
message.success(text);
getDataSource();
} else {
if (value === 'start') {
message.error('开机失败');
} else {
message.error('关机失败');
}
}
});
};
const onOrgSelect = (selectedKeys: React.Key[], e: any) => {
const { node } = e || {};
if (selectedKeys.length > 0) {
@ -388,9 +451,10 @@ const UserListPage: React.FC = () => {
setPageSize(size);
};
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
setSelectedRowKeys(newSelectedRowKeys as any);
};
// const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
// setSelectedRowKeys(newSelectedRowKeys as any);
// };
const onDeleteGroup = async () => {
if (selectedOrg) {
const params: any = {

View File

@ -0,0 +1,96 @@
import React, { useEffect } from 'react';
import { useLocation } from 'umi';
import VncClient from './mod/index';
/**
* VNC
* VNC
*/
const VncRemotePage: React.FC = () => {
const location = useLocation();
// 从URL参数中获取镜像ID和其他信息
const searchParams = new URLSearchParams(location.search);
console.log("location=======",location);
console.log("location=======searchParams.get('vncUrl')",searchParams.get('vncUrl'))
// const imageId = searchParams.get('imageId') || '';
const imageName = searchParams.get('imageName') || 'NEXSPACE远程桌面';
const vncUrl = searchParams.get('vncUrl') || '';
const password = searchParams.get('password') || '';
// 设置页面标题
useEffect(() => {
document.title = `${imageName} - 远程控制台`;
// 添加关闭窗口时的清理逻辑
const handleBeforeUnload = () => {
// 确保在窗口关闭前通知VNC客户端进行清理
console.log('Performing cleanup before window unload');
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [imageName]);
// 验证并修复VNC URL格式
const validateAndFixVncUrl = (url: string) => {
if (!url) return '';
// 检查URL格式修复常见问题
if (url.startsWith('ws:http')) {
// 修复错误格式 ws:http// -> ws://
return url.replace('ws:http//', 'ws://');
} else if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
// 如果没有协议前缀添加ws://
return `ws://${url}`;
}
return url;
};
// 生成默认的VNC URL如果没有提供
// const defaultVncUrl = imageId ? `ws://0.0.0.0:5091/vnc/0.0.0.0:5901` : '';
const defaultVncUrl = `ws://0.0.0.0:5091/vnc/0.0.0.0:5901`;
const finalVncUrl = validateAndFixVncUrl(vncUrl) || defaultVncUrl;
console.log('最终使用的VNC URL:', finalVncUrl);
// 当收到父窗口的消息时处理
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data === 'close-vnc-window') {
window.close();
}
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, []);
return (
<div
style={{
width: '100vw',
height: '100vh',
margin: 0,
padding: 0,
overflow: 'hidden',
backgroundColor: '#000',
}}
>
<VncClient
onClose={() => {
// 确保在关闭前有适当的清理时间
setTimeout(() => window.close(), 100);
}}
vncUrl={finalVncUrl}
password={password}
viewOnly={false}
/>
</div>
);
};
export default VncRemotePage;

View File

@ -0,0 +1,325 @@
// VNC 远程桌面组件样式
.vnc-remote-desktop {
position: relative;
width: 100%;
height: 100%;
background-color: #1a1a1a;
border: 1px solid #333;
border-radius: 4px;
display: flex;
flex-direction: column;
overflow: hidden;
// 全屏模式
&.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
margin: 0;
border: none;
border-radius: 0;
}
// 连接状态样式
&.connecting {
border-color: #ff9800;
}
&.connected {
border-color: #4caf50;
}
&.disconnected {
border-color: #f44336;
}
}
// 控制栏样式
.vnc-controls {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background-color: #2a2a2a;
border-bottom: 1px solid #333;
color: #fff;
font-size: 14px;
z-index: 100;
}
// 状态指示器
.vnc-status-indicator {
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
&.connecting {
background-color: rgba(255, 152, 0, 0.2);
color: #ff9800;
}
&.connected {
background-color: rgba(76, 175, 80, 0.2);
color: #4caf50;
}
&.disconnected {
background-color: rgba(244, 67, 54, 0.2);
color: #f44336;
}
}
// 操作按钮组
.vnc-actions {
display: flex;
align-items: center;
gap: 8px;
:where(
.css-dev-only-do-not-override-1vjf2v5
).ant-btn-variant-outlined:disabled,
:where(.css-dev-only-do-not-override-1vjf2v5).ant-btn-variant-dashed:disabled,
:where(
.css-dev-only-do-not-override-1vjf2v5
).ant-btn-variant-outlined.ant-btn-disabled,
:where(
.css-dev-only-do-not-override-1vjf2v5
).ant-btn-variant-dashed.ant-btn-disabled {
background-color: #ffffff;
}
}
.popover-content {
display: flex;
flex-direction: column;
// gap: 8px;
}
// 仅查看模式指示器
.view-only-indicator {
color: #ffeb3b;
font-size: 12px;
background-color: rgba(255, 235, 59, 0.1);
padding: 2px 6px;
border-radius: 3px;
}
// 按钮样式
.vnc-button {
width: 32px;
height: 32px;
border: none;
border-radius: 4px;
background-color: #444;
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.2s ease;
&:hover:not(:disabled) {
background-color: #555;
transform: translateY(-1px);
}
&:active:not(:disabled) {
transform: translateY(0);
}
&:disabled {
background-color: #333;
color: #666;
cursor: not-allowed;
}
&.disconnect {
background-color: #f44336;
&:hover:not(:disabled) {
background-color: #d32f2f;
}
}
}
// 内容区域
.vnc-content {
width: 100%;
height: calc(100% - 40px);
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background-color: #000;
overflow: hidden;
}
// 加载覆盖层
.vnc-loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
z-index: 10;
}
// 加载动画
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top: 4px solid #fff;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
// 错误覆盖层
.vnc-error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(244, 67, 54, 0.1);
color: #fff;
z-index: 10;
padding: 20px;
text-align: center;
}
.error-icon {
font-size: 48px;
margin-bottom: 16px;
}
.error-message {
font-size: 16px;
margin-bottom: 20px;
color: #ffcdcd;
max-width: 80%;
word-wrap: break-word;
}
.reconnect-button {
padding: 10px 20px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s ease;
&:hover {
background-color: #45a049;
}
}
// VNC 画布
.vnc-canvas {
max-width: 100%;
max-height: 100%;
cursor: default;
&:focus {
outline: 2px solid #2196f3;
outline-offset: 2px;
}
}
// 未连接时的占位符
.vnc-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #666;
.placeholder-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.3;
}
}
// 响应式设计
@media (max-width: 768px) {
.vnc-controls {
padding: 6px 12px;
font-size: 12px;
z-index: 100;
}
.vnc-button {
width: 28px;
height: 28px;
font-size: 14px;
}
.loading-spinner {
width: 30px;
height: 30px;
}
.error-icon {
font-size: 36px;
}
.error-message {
font-size: 14px;
}
.placeholder-icon {
font-size: 48px;
}
}
// 深色主题增强
@media (prefers-color-scheme: dark) {
.vnc-remote-desktop {
background-color: #0d1117;
border-color: #30363d;
}
.vnc-controls {
background-color: #161b22;
border-bottom-color: #30363d;
z-index: 100;
}
.vnc-button {
background-color: #21262d;
}
.vnc-button:hover:not(:disabled) {
background-color: #30363d;
}
}

View File

@ -0,0 +1,400 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import type { MenuProps } from 'antd';
import { Button, Dropdown } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
// import { RFB } from '@novnc/novnc/core/rfb';
import RFB from '@/public/novnc/core/rfb';
import './index.less';
interface VncRemoteDesktopProps {
vncUrl: string;
password?: string;
onConnected?: () => void;
onDisconnected?: () => void;
onError?: (error: string) => void;
loadingText?: string;
className?: string;
isFullscreen?: boolean;
viewOnly?: boolean;
autoScale?: boolean;
maxRetries?: number;
retryInterval?: number;
}
/**
* noVNC
* 使@novnc/novnc
*/
const VncRemoteDesktop: React.FC<VncRemoteDesktopProps> = ({
vncUrl,
password,
onConnected,
onDisconnected,
onError,
loadingText = '正在连接远程桌面...',
className = '',
viewOnly = false,
autoScale = true,
maxRetries = 3,
retryInterval = 2000,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const rfbRef = useRef<RFB | null>(null);
const retryCountRef = useRef(0);
const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [connectionStatus, setConnectionStatus] = useState<
'disconnected' | 'connecting' | 'connected'
>('disconnected');
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [visible, setVisible] = useState(false);
// 监听浏览器窗口关闭事件
useEffect(() => {
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault();
disconnect();
return '';
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, []);
// 清除重试定时器
const clearRetryTimeout = () => {
if (retryTimeoutRef.current) {
clearTimeout(retryTimeoutRef.current);
retryTimeoutRef.current = null;
}
};
// 连接到VNC服务器
const connect = (resetRetry = false) => {
if (!vncUrl || !canvasRef.current) {
return;
}
// 清除之前的重试定时器
clearRetryTimeout();
// 重置重试计数(如果需要)
if (resetRetry) {
retryCountRef.current = 0;
}
// 断开已有连接
if (rfbRef.current) {
disconnect();
}
setConnectionStatus('connecting');
setErrorMessage(null);
try {
// 验证URL格式
if (!vncUrl.startsWith('ws://') && !vncUrl.startsWith('wss://')) {
throw new Error('无效的VNC URL格式请使用ws://或wss://开头');
}
console.log('WebSocket URL=========', vncUrl);
console.log('尝试连接到VNC服务器:', vncUrl);
// 创建RFB实例
const rfb = new RFB(canvasRef.current, vncUrl, {
credentials: password ? { password } : undefined,
shared: true,
wsProtocols: ['binary'],
// focusOnClick: !viewOnly,
dragViewport: true,
scaleViewport: true,
resizeSession: true,
// viewOnly: viewOnly,
// background: '#000000',
});
// 保存RFB实例引用用于后续操作和事件处理
rfbRef.current = rfb;
console.log('rfbRef.current=====保存RFB实例引用', rfbRef.current);
// 监听连接事件
rfb.addEventListener('connect', () => {
console.log('VNC连接成功');
retryCountRef.current = 0; // 重置重试计数
setConnectionStatus('connected');
onConnected?.();
});
// 监听断开连接事件
rfb.addEventListener('disconnect', () => {
console.log('VNC连接断开');
setConnectionStatus('disconnected');
onDisconnected?.();
console.log('rfbRef.current=====监听断开连接事件', rfbRef.current);
});
// 监听安全失败事件
rfb.addEventListener('securityfailure', (e: any) => {
console.error('VNC安全连接失败:', e.detail);
setErrorMessage('安全连接失败: ' + e.detail);
setConnectionStatus('disconnected');
onError?.('安全连接失败: ' + e.detail);
console.log('rfbRef.current=====监听安全失败事件', rfbRef.current);
rfbRef.current = null;
});
// 监听凭证请求事件
rfb.addEventListener('credentialsrequired', () => {
console.log('需要凭证');
if (password && rfbRef.current) {
rfbRef.current.sendCredentials({ password });
}
});
// 监听错误事件
rfb.addEventListener('error', (e: any) => {
const errorDetail = e.detail || {};
const errorMsg = errorDetail.message || errorDetail || '未知错误';
console.error('VNC错误:', errorMsg);
// 提供更具体的错误信息
let userFriendlyError = '';
if (errorMsg.toString().includes('WebSocket')) {
userFriendlyError = `WebSocket连接失败: ${errorMsg}\n可能的原因: 服务器不可达、网络问题或防火墙阻止`;
} else if (errorMsg.toString().includes('401')) {
userFriendlyError = '认证失败: 用户名或密码错误';
} else if (errorMsg.toString().includes('403')) {
userFriendlyError = '权限不足: 您没有访问该远程桌面的权限';
} else if (errorMsg.toString().includes('timeout')) {
userFriendlyError = '连接超时: 服务器响应超时,请检查网络连接';
} else {
userFriendlyError = `连接错误: ${errorMsg}`;
}
setErrorMessage(userFriendlyError);
setConnectionStatus('disconnected');
onError?.(userFriendlyError);
// 自动重试连接(如果未达到最大重试次数)
if (retryCountRef.current < maxRetries) {
retryCountRef.current++;
console.log(`尝试重新连接 (${retryCountRef.current}/${maxRetries})`);
retryTimeoutRef.current = setTimeout(() => {
connect();
}, retryInterval);
}
});
console.log('RFB连接配置完成');
} catch (error) {
console.error('创建VNC连接失败:', error);
const errorMsg = error instanceof Error ? error.message : '创建连接失败';
setErrorMessage(errorMsg);
setConnectionStatus('disconnected');
onError?.(errorMsg);
}
};
// 断开连接
const disconnect = () => {
// 清除重试定时器
clearRetryTimeout();
console.log('rfbRef.current=====断开连接', rfbRef);
console.log('rfbRef.current=====断开连接', rfbRef.current);
if (rfbRef.current) {
try {
// 确保完全断开连接并释放所有资源
rfbRef.current.disconnect();
} catch (error) {
console.error('Error during disconnect/cleanup:', error);
}
}
// 重置重试计数
retryCountRef.current = 0;
};
// 连接
const reconnect = () => {
setVisible(true);
connect(true); // 重置重试计数后连接
};
// 当vncUrl或password变化时重新连接
useEffect(() => {
if (vncUrl && visible) {
connect(true); // 重置重试计数后连接
}
// 组件卸载时断开连接并清理资源
return () => {
disconnect();
clearRetryTimeout();
};
}, [visible, vncUrl, password, maxRetries, retryInterval]);
// 当autoScale或viewOnly属性变化时更新RFB实例
useEffect(() => {
if (rfbRef.current) {
rfbRef.current.viewOnly = viewOnly;
rfbRef.current.focusOnClick = !viewOnly;
rfbRef.current.dragViewport = !viewOnly;
rfbRef.current.scaleViewport = autoScale;
rfbRef.current.resizeSession = autoScale;
}
}, [viewOnly, autoScale]);
const menuItems: MenuProps = {
items: [
{
key: '1',
label: <div></div>,
// label: (
// <Button
// onClick={() => {}}
// disabled={connectionStatus === 'connecting'}
// title="挂载优化工具"
// >
// 挂载优化工具
// </Button>
// ),
},
{
key: '2', // 注意 key 应该唯一
label: <div></div>,
// label: (
// <Button
// onClick={() => {}}
// disabled={connectionStatus === 'connecting'}
// title="挂载应用软件盘"
// >
// 挂载应用软件盘
// </Button>
// ),
},
{
key: '3',
label: <div></div>,
// label: (
// <Button
// onClick={() => {}}
// disabled={connectionStatus === 'connecting'}
// title="挂载应用软件盘"
// >
// 挂载应用软件盘
// </Button>
// ),
},
],
};
return (
<div className={`vnc-remote-desktop ${className} ${connectionStatus}`}>
{/* 控制栏 */}
<div className="vnc-controls">
<span className={`vnc-status-indicator ${connectionStatus}`}>
{connectionStatus === 'connecting' && '连接中...'}
{connectionStatus === 'connected' && '已连接'}
{connectionStatus === 'disconnected' && '已断开'}
</span>
<div className="vnc-actions">
{viewOnly && <span className="view-only-indicator"></span>}
{!(connectionStatus === 'connected') ? (
<Button
onClick={reconnect}
disabled={connectionStatus === 'connecting'}
title="重新连接"
>
</Button>
) : (
<>
<Button
onClick={() => {}}
disabled={!(connectionStatus === 'connected')}
title="关机"
>
</Button>
<Button
onClick={() => {}}
disabled={!(connectionStatus === 'connected')}
title="重启"
>
</Button>
<Dropdown
menu={menuItems}
disabled={!(connectionStatus === 'connected')}
>
<Button
onClick={() => {}}
disabled={!(connectionStatus === 'connected')}
title="安装模板工具"
>
</Button>
</Dropdown>
<Button
onClick={disconnect}
disabled={!(connectionStatus === 'connected')}
title="断开连接"
type="default"
>
</Button>
</>
)}
</div>
</div>
{/* 远程桌面内容 */}
<div className="vnc-content">
{/* 加载状态 */}
{connectionStatus === 'connecting' && (
<div className="vnc-loading-overlay">
<div className="loading-spinner"></div>
<div>{loadingText}</div>
</div>
)}
{/* 错误状态 */}
{errorMessage && connectionStatus === 'disconnected' && (
<div className="vnc-error-overlay">
<div className="error-icon"></div>
<div className="error-message">{errorMessage}</div>
<Button className="reconnect-button" onClick={reconnect}>
</Button>
</div>
)}
{/* VNC画布,使用canvas远程桌面控制页面无法展示不要使用 */}
<div
ref={canvasRef}
className="vnc-canvas"
tabIndex={viewOnly ? -1 : 0}
style={{
display: connectionStatus === 'connected' ? 'block' : 'none',
}}
/>
{/* 未连接时的占位符 */}
{connectionStatus === 'disconnected' && !errorMessage && (
<div className="vnc-placeholder">
<div className="placeholder-icon">🖥</div>
<div>...</div>
</div>
)}
</div>
</div>
);
};
export default VncRemoteDesktop;

View File

@ -0,0 +1,6 @@
# README
`noVnc` 从1.5版本及以上移除了core文件夹且EMS支持有问题
解决方案:
使用1.4版本或下载1.6版本源码使用

View File

@ -0,0 +1,79 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
// Fallback for all uncought errors
function handleError(event, err) {
try {
const msg = document.getElementById('noVNC_fallback_errormsg');
// Work around Firefox bug:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1685038
if (event.message === "ResizeObserver loop completed with undelivered notifications.") {
return false;
}
// Only show the initial error
if (msg.hasChildNodes()) {
return false;
}
let div = document.createElement("div");
div.classList.add('noVNC_message');
div.appendChild(document.createTextNode(event.message));
msg.appendChild(div);
if (event.filename) {
div = document.createElement("div");
div.className = 'noVNC_location';
let text = event.filename;
if (event.lineno !== undefined) {
text += ":" + event.lineno;
if (event.colno !== undefined) {
text += ":" + event.colno;
}
}
div.appendChild(document.createTextNode(text));
msg.appendChild(div);
}
if (err && err.stack) {
div = document.createElement("div");
div.className = 'noVNC_stack';
div.appendChild(document.createTextNode(err.stack));
msg.appendChild(div);
}
document.getElementById('noVNC_fallback_error')
.classList.add("noVNC_open");
} catch (exc) {
document.write("noVNC encountered an error.");
}
// Try to disable keyboard interaction, best effort
try {
// Remove focus from the currently focused element in order to
// prevent keyboard interaction from continuing
if (document.activeElement) { document.activeElement.blur(); }
// Don't let any element be focusable when showing the error
let keyboardFocusable = 'a[href], button, input, textarea, select, details, [tabindex]';
document.querySelectorAll(keyboardFocusable).forEach((elem) => {
elem.setAttribute("tabindex", "-1");
});
} catch (exc) {
// Do nothing
}
// Don't return true since this would prevent the error
// from being printed to the browser console.
return false;
}
window.addEventListener('error', evt => handleError(evt, evt.error));
window.addEventListener('unhandledrejection', evt => handleError(evt.reason, evt.reason));

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="alt.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="18.205425"
inkscape:cy="17.531398"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<g
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:48px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text5290">
<path
d="m 9.9560547,1042.3329 -2.9394531,0 -0.4638672,1.3281 -1.8896485,0 2.7001953,-7.29 2.241211,0 2.7001958,7.29 -1.889649,0 -0.4589843,-1.3281 z m -2.4707031,-1.3526 1.9970703,0 -0.9960938,-2.9003 -1.0009765,2.9003 z"
style="font-size:10px;fill:#ffffff;fill-opacity:1"
id="path5340" />
<path
d="m 13.188477,1036.0634 1.748046,0 0,7.5976 -1.748046,0 0,-7.5976 z"
style="font-size:10px;fill:#ffffff;fill-opacity:1"
id="path5342" />
<path
d="m 18.535156,1036.6395 0,1.5528 1.801758,0 0,1.25 -1.801758,0 0,2.3193 q 0,0.3809 0.151367,0.5176 0.151368,0.1318 0.600586,0.1318 l 0.898438,0 0,1.25 -1.499024,0 q -1.035156,0 -1.469726,-0.4297 -0.429688,-0.4345 -0.429688,-1.4697 l 0,-2.3193 -0.86914,0 0,-1.25 0.86914,0 0,-1.5528 1.748047,0 z"
style="font-size:10px;fill:#ffffff;fill-opacity:1"
id="path5344" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="clipboard.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="15.366606"
inkscape:cy="16.42981"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<path
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 9,6 6,6 C 5.4459889,6 5,6.4459889 5,7 l 0,13 c 0,0.554011 0.4459889,1 1,1 l 13,0 c 0.554011,0 1,-0.445989 1,-1 L 20,7 C 20,6.4459889 19.554011,6 19,6 l -3,0"
transform="translate(0,1027.3622)"
id="rect6083"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssssssssc" />
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect6085"
width="7"
height="4"
x="9"
y="1031.3622"
ry="1.00002" />
<path
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196081"
d="m 8.5071212,1038.8622 7.9999998,0"
id="path6087"
inkscape:connector-curvature="0" />
<path
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196081"
d="m 8.5071212,1041.8622 3.9999998,0"
id="path6089"
inkscape:connector-curvature="0" />
<path
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196081"
d="m 8.5071212,1044.8622 5.9999998,0"
id="path6091"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="connect.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="37.14834"
inkscape:cy="1.9525926"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<g
id="g5103"
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,-729.15757,315.8823)">
<path
sodipodi:nodetypes="cssssc"
inkscape:connector-curvature="0"
id="rect5096"
d="m 11,1040.3622 -5,0 c -1.108,0 -2,-0.892 -2,-2 l 0,-4 c 0,-1.108 0.892,-2 2,-2 l 5,0"
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 14,1032.3622 5,0 c 1.108,0 2,0.892 2,2 l 0,4 c 0,1.108 -0.892,2 -2,2 l -5,0"
id="path5099"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssssc" />
<path
inkscape:connector-curvature="0"
id="path5101"
d="m 9,1036.3622 7,0"
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="ctrl.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="18.205425"
inkscape:cy="17.531398"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<g
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:48px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text5290">
<path
d="m 9.1210938,1043.1898 q -0.5175782,0.2686 -1.0791016,0.4053 -0.5615235,0.1367 -1.171875,0.1367 -1.8212891,0 -2.8857422,-1.0156 -1.0644531,-1.0205 -1.0644531,-2.7637 0,-1.748 1.0644531,-2.7637 1.0644531,-1.0205 2.8857422,-1.0205 0.6103515,0 1.171875,0.1368 0.5615234,0.1367 1.0791016,0.4052 l 0,1.5088 q -0.522461,-0.3564 -1.0302735,-0.5224 -0.5078125,-0.1661 -1.0693359,-0.1661 -1.0058594,0 -1.5820313,0.6446 -0.5761719,0.6445 -0.5761719,1.7773 0,1.1279 0.5761719,1.7725 0.5761719,0.6445 1.5820313,0.6445 0.5615234,0 1.0693359,-0.166 0.5078125,-0.166 1.0302735,-0.5225 l 0,1.5088 z"
style="font-size:10px;fill:#ffffff;fill-opacity:1"
id="path5370" />
<path
d="m 12.514648,1036.5687 0,1.5528 1.801758,0 0,1.25 -1.801758,0 0,2.3193 q 0,0.3809 0.151368,0.5176 0.151367,0.1318 0.600586,0.1318 l 0.898437,0 0,1.25 -1.499023,0 q -1.035157,0 -1.469727,-0.4297 -0.429687,-0.4345 -0.429687,-1.4697 l 0,-2.3193 -0.8691411,0 0,-1.25 0.8691411,0 0,-1.5528 1.748046,0 z"
style="font-size:10px;fill:#ffffff;fill-opacity:1"
id="path5372" />
<path
d="m 19.453125,1039.6107 q -0.229492,-0.1074 -0.458984,-0.1562 -0.22461,-0.054 -0.454102,-0.054 -0.673828,0 -1.040039,0.4345 -0.361328,0.4297 -0.361328,1.2354 l 0,2.5195 -1.748047,0 0,-5.4687 1.748047,0 0,0.8984 q 0.336914,-0.5371 0.771484,-0.7813 0.439453,-0.249 1.049805,-0.249 0.08789,0 0.19043,0.01 0.102539,0 0.297851,0.029 l 0.0049,1.582 z"
style="font-size:10px;fill:#ffffff;fill-opacity:1"
id="path5374" />
<path
d="m 20.332031,1035.9926 1.748047,0 0,7.5976 -1.748047,0 0,-7.5976 z"
style="font-size:10px;fill:#ffffff;fill-opacity:1"
id="path5376" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="ctrlaltdel.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="8"
inkscape:cx="11.135667"
inkscape:cy="16.407428"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5253"
width="5"
height="5.0000172"
x="16"
y="1031.3622"
ry="1.0000174" />
<rect
y="1043.3622"
x="4"
height="5.0000172"
width="5"
id="rect5255"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
ry="1.0000174" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5257"
width="5"
height="5.0000172"
x="13"
y="1043.3622"
ry="1.0000174" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="disconnect.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="25.05707"
inkscape:cy="11.594858"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="false">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<g
id="g5171"
transform="translate(-24.062499,-6.15775e-4)">
<path
id="path5110"
transform="translate(0,1027.3622)"
d="m 39.744141,3.4960938 c -0.769923,0 -1.539607,0.2915468 -2.121094,0.8730468 l -2.566406,2.5664063 1.414062,1.4140625 2.566406,-2.5664063 c 0.403974,-0.404 1.010089,-0.404 1.414063,0 l 2.828125,2.828125 c 0.40398,0.4039 0.403907,1.0101621 0,1.4140629 l -2.566406,2.566406 1.414062,1.414062 2.566406,-2.566406 c 1.163041,-1.1629 1.162968,-3.0791874 0,-4.2421874 L 41.865234,4.3691406 C 41.283747,3.7876406 40.514063,3.4960937 39.744141,3.4960938 Z M 39.017578,9.015625 a 1.0001,1.0001 0 0 0 -0.6875,0.3027344 l -0.445312,0.4453125 1.414062,1.4140621 0.445313,-0.445312 A 1.0001,1.0001 0 0 0 39.017578,9.015625 Z m -6.363281,0.7070312 a 1.0001,1.0001 0 0 0 -0.6875,0.3027348 L 28.431641,13.5625 c -1.163042,1.163 -1.16297,3.079187 0,4.242188 l 2.828125,2.828124 c 1.162974,1.163101 3.079213,1.163101 4.242187,0 l 3.535156,-3.535156 a 1.0001,1.0001 0 1 0 -1.414062,-1.414062 l -3.535156,3.535156 c -0.403974,0.404 -1.010089,0.404 -1.414063,0 l -2.828125,-2.828125 c -0.403981,-0.404 -0.403908,-1.010162 0,-1.414063 l 3.535156,-3.537109 A 1.0001,1.0001 0 0 0 32.654297,9.7226562 Z m 3.109375,2.1621098 -2.382813,2.384765 a 1.0001,1.0001 0 1 0 1.414063,1.414063 l 2.382812,-2.384766 -1.414062,-1.414062 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
inkscape:connector-curvature="0" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
y="752.29541"
x="-712.31262"
height="18.000017"
width="3"
id="rect5116"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="drag.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="22.627417"
inkscape:cx="9.8789407"
inkscape:cy="9.5008608"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="false"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 7.039733,1049.3037 c -0.4309106,-0.1233 -0.7932634,-0.4631 -0.9705434,-0.9103 -0.04922,-0.1241 -0.057118,-0.2988 -0.071321,-1.5771 l -0.015972,-1.4375 -0.328125,-0.082 c -0.7668138,-0.1927 -1.1897046,-0.4275 -1.7031253,-0.9457 -0.4586773,-0.4629 -0.6804297,-0.8433 -0.867034,-1.4875 -0.067215,-0.232 -0.068001,-0.2642 -0.078682,-3.2188 -0.012078,-3.341 -0.020337,-3.2012 0.2099452,-3.5555 0.2246623,-0.3458 0.5798271,-0.5892 0.9667343,-0.6626 0.092506,-0.017 0.531898,-0.032 0.9764271,-0.032 l 0.8082347,0 1.157e-4,1.336 c 1.125e-4,1.2779 0.00281,1.3403 0.062214,1.4378 0.091785,0.1505 0.2357707,0.226 0.4314082,0.2261 0.285389,2e-4 0.454884,-0.1352 0.5058962,-0.4042 0.019355,-0.102 0.031616,-0.982 0.031616,-2.269 0,-1.9756 0.00357,-2.1138 0.059205,-2.2926 0.1645475,-0.5287 0.6307616,-0.9246 1.19078,-1.0113 0.8000572,-0.1238 1.5711277,0.4446 1.6860387,1.2429 0.01732,0.1203 0.03177,0.8248 0.03211,1.5657 6.19e-4,1.3449 7.22e-4,1.347 0.07093,1.4499 0.108355,0.1587 0.255268,0.2248 0.46917,0.2108 0.204069,-0.013 0.316116,-0.08 0.413642,-0.2453 0.06028,-0.1024 0.06307,-0.1778 0.07862,-2.1218 0.01462,-1.8283 0.02124,-2.0285 0.07121,-2.1549 0.260673,-0.659 0.934894,-1.0527 1.621129,-0.9465 0.640523,0.099 1.152269,0.6104 1.243187,1.2421 0.01827,0.1269 0.03175,0.9943 0.03211,2.0657 l 6.19e-4,1.8469 0.07031,0.103 c 0.108355,0.1587 0.255267,0.2248 0.46917,0.2108 0.204069,-0.013 0.316115,-0.08 0.413642,-0.2453 0.05951,-0.1011 0.06329,-0.1786 0.07907,-1.6218 0.01469,-1.3438 0.02277,-1.5314 0.07121,-1.6549 0.257975,-0.6576 0.934425,-1.0527 1.620676,-0.9465 0.640522,0.099 1.152269,0.6104 1.243186,1.2421 0.0186,0.1292 0.03179,1.0759 0.03222,2.3125 7.15e-4,2.0335 0.0025,2.0966 0.06283,2.1956 0.09178,0.1505 0.235771,0.226 0.431409,0.2261 0.285388,2e-4 0.454884,-0.1352 0.505897,-0.4042 0.01874,-0.099 0.03161,-0.8192 0.03161,-1.769 0,-1.4848 0.0043,-1.6163 0.0592,-1.7926 0.164548,-0.5287 0.630762,-0.9246 1.19078,-1.0113 0.800057,-0.1238 1.571128,0.4446 1.686039,1.2429 0.04318,0.2999 0.04372,9.1764 5.78e-4,9.4531 -0.04431,0.2841 -0.217814,0.6241 -0.420069,0.8232 -0.320102,0.315 -0.63307,0.4268 -1.194973,0.4268 l -0.35281,0 -2.51e-4,1.2734 c -1.25e-4,0.7046 -0.01439,1.3642 -0.03191,1.4766 -0.06665,0.4274 -0.372966,0.8704 -0.740031,1.0702 -0.349999,0.1905 0.01748,0.18 -6.242199,0.1776 -5.3622439,0 -5.7320152,-0.01 -5.9121592,-0.057 l 1.4e-5,0 z"
id="path4379"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="error.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="14.00357"
inkscape:cy="12.443398"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 7 3 C 4.7839905 3 3 4.7839905 3 7 L 3 18 C 3 20.21601 4.7839905 22 7 22 L 18 22 C 20.21601 22 22 20.21601 22 18 L 22 7 C 22 4.7839905 20.21601 3 18 3 L 7 3 z M 7.6992188 6 A 1.6916875 1.6924297 0 0 1 8.9121094 6.5117188 L 12.5 10.101562 L 16.087891 6.5117188 A 1.6916875 1.6924297 0 0 1 17.251953 6 A 1.6916875 1.6924297 0 0 1 18.480469 8.90625 L 14.892578 12.496094 L 18.480469 16.085938 A 1.6916875 1.6924297 0 1 1 16.087891 18.478516 L 12.5 14.888672 L 8.9121094 18.478516 A 1.6916875 1.6924297 0 1 1 6.5214844 16.085938 L 10.109375 12.496094 L 6.5214844 8.90625 A 1.6916875 1.6924297 0 0 1 7.6992188 6 z "
transform="translate(0,1027.3622)"
id="rect4135" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="esc.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="18.205425"
inkscape:cy="17.531398"
inkscape:document-units="px"
inkscape:current-layer="text5290"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<g
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:48px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text5290">
<path
d="m 3.9331055,1036.1464 5.0732422,0 0,1.4209 -3.1933594,0 0,1.3574 3.0029297,0 0,1.4209 -3.0029297,0 0,1.6699 3.3007812,0 0,1.4209 -5.180664,0 0,-7.29 z"
style="font-size:10px;fill:#ffffff;fill-opacity:1"
id="path5314" />
<path
d="m 14.963379,1038.1385 0,1.3282 q -0.561524,-0.2344 -1.083984,-0.3516 -0.522461,-0.1172 -0.986329,-0.1172 -0.498046,0 -0.742187,0.127 -0.239258,0.122 -0.239258,0.3808 0,0.21 0.180664,0.3223 0.185547,0.1123 0.65918,0.166 l 0.307617,0.044 q 1.342773,0.1709 1.806641,0.5615 0.463867,0.3906 0.463867,1.2256 0,0.874 -0.644531,1.3134 -0.644532,0.4395 -1.923829,0.4395 -0.541992,0 -1.123046,-0.088 -0.576172,-0.083 -1.186524,-0.2539 l 0,-1.3281 q 0.522461,0.2539 1.069336,0.3808 0.551758,0.127 1.118164,0.127 0.512695,0 0.771485,-0.1416 0.258789,-0.1416 0.258789,-0.4199 0,-0.2344 -0.180664,-0.3467 -0.175782,-0.1172 -0.708008,-0.1807 l -0.307617,-0.039 q -1.166993,-0.1465 -1.635743,-0.542 -0.46875,-0.3955 -0.46875,-1.2012 0,-0.8691 0.595703,-1.2891 0.595704,-0.4199 1.826172,-0.4199 0.483399,0 1.015625,0.073 0.532227,0.073 1.157227,0.2294 z"
style="font-size:10px;fill:#ffffff;fill-opacity:1"
id="path5316" />
<path
d="m 21.066895,1038.1385 0,1.4258 q -0.356446,-0.2441 -0.717774,-0.3613 -0.356445,-0.1172 -0.742187,-0.1172 -0.732422,0 -1.142579,0.4297 -0.405273,0.4248 -0.405273,1.1914 0,0.7666 0.405273,1.1963 0.410157,0.4248 1.142579,0.4248 0.410156,0 0.776367,-0.1221 0.371094,-0.122 0.683594,-0.3613 l 0,1.4307 q -0.410157,0.1513 -0.834961,0.2246 -0.419922,0.078 -0.844727,0.078 -1.479492,0 -2.314453,-0.7568 -0.834961,-0.7618 -0.834961,-2.1143 0,-1.3525 0.834961,-2.1094 0.834961,-0.7617 2.314453,-0.7617 0.429688,0 0.844727,0.078 0.419921,0.073 0.834961,0.2246 z"
style="font-size:10px;fill:#ffffff;fill-opacity:1"
id="path5318" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="9"
height="10"
viewBox="0 0 9 10"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="expander.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="45.254834"
inkscape:cx="9.8737281"
inkscape:cy="6.4583132"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:snap-object-midpoints="false"
inkscape:object-nodes="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1042.3622)">
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 2.0800781,1042.3633 A 2.0002,2.0002 0 0 0 0,1044.3613 l 0,6 a 2.0002,2.0002 0 0 0 3.0292969,1.7168 l 5,-3 a 2.0002,2.0002 0 0 0 0,-3.4316 l -5,-3 a 2.0002,2.0002 0 0 0 -0.9492188,-0.2832 z"
id="path4138"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="fullscreen.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="16.400723"
inkscape:cy="15.083758"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="false"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="false">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5006"
width="17"
height="17.000017"
x="4"
y="1031.3622"
ry="3.0000174" />
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="m 7.5,1044.8622 4,0 -1.5,-1.5 1.5,-1.5 -1,-1 -1.5,1.5 -1.5,-1.5 0,4 z"
id="path5017"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path5025"
d="m 17.5,1034.8622 -4,0 1.5,1.5 -1.5,1.5 1,1 1.5,-1.5 1.5,1.5 0,-4 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="5"
height="6"
viewBox="0 0 5 6"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="handle.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="32"
inkscape:cx="1.3551778"
inkscape:cy="8.7800329"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="false"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1046.3622)">
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 4.0000803,1049.3622 -3,-2 0,4 z"
id="path4247"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="15"
height="50"
viewBox="0 0 15 50"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="handle_bg.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="-10.001409"
inkscape:cy="24.512566"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="false"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1002.3622)">
<rect
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4249"
width="1"
height="1.0000174"
x="9.5"
y="1008.8622"
ry="1.7382812e-05" />
<rect
ry="1.7382812e-05"
y="1013.8622"
x="9.5"
height="1.0000174"
width="1"
id="rect4255"
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
ry="1.7382812e-05"
y="1008.8622"
x="4.5"
height="1.0000174"
width="1"
id="rect4261"
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4263"
width="1"
height="1.0000174"
x="4.5"
y="1013.8622"
ry="1.7382812e-05" />
<rect
ry="1.7382812e-05"
y="1039.8622"
x="9.5"
height="1.0000174"
width="1"
id="rect4265"
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4267"
width="1"
height="1.0000174"
x="9.5"
y="1044.8622"
ry="1.7382812e-05" />
<rect
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4269"
width="1"
height="1.0000174"
x="4.5"
y="1039.8622"
ry="1.7382812e-05" />
<rect
ry="1.7382812e-05"
y="1044.8622"
x="4.5"
height="1.0000174"
width="1"
id="rect4271"
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4273"
width="1"
height="1.0000174"
x="9.5"
y="1018.8622"
ry="1.7382812e-05" />
<rect
ry="1.7382812e-05"
y="1018.8622"
x="4.5"
height="1.0000174"
width="1"
id="rect4275"
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4277"
width="1"
height="1.0000174"
x="9.5"
y="1034.8622"
ry="1.7382812e-05" />
<rect
ry="1.7382812e-05"
y="1034.8622"
x="4.5"
height="1.0000174"
width="1"
id="rect4279"
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -0,0 +1,42 @@
BROWSER_SIZES := 16 24 32 48 64
#ANDROID_SIZES := 72 96 144 192
# FIXME: The ICO is limited to 8 icons due to a Chrome bug:
# https://bugs.chromium.org/p/chromium/issues/detail?id=1381393
ANDROID_SIZES := 96 144 192
WEB_ICON_SIZES := $(BROWSER_SIZES) $(ANDROID_SIZES)
#IOS_1X_SIZES := 20 29 40 76 # No such devices exist anymore
IOS_2X_SIZES := 40 58 80 120 152 167
IOS_3X_SIZES := 60 87 120 180
ALL_IOS_SIZES := $(IOS_1X_SIZES) $(IOS_2X_SIZES) $(IOS_3X_SIZES)
ALL_ICONS := \
$(ALL_IOS_SIZES:%=novnc-ios-%.png) \
novnc.ico
all: $(ALL_ICONS)
# Our testing shows that the ICO file need to be sorted in largest to
# smallest to get the apporpriate behviour
WEB_ICON_SIZES_REVERSE := $(shell echo $(WEB_ICON_SIZES) | tr ' ' '\n' | sort -nr | tr '\n' ' ')
WEB_BASE_ICONS := $(WEB_ICON_SIZES_REVERSE:%=novnc-%.png)
.INTERMEDIATE: $(WEB_BASE_ICONS)
novnc.ico: $(WEB_BASE_ICONS)
convert $(WEB_BASE_ICONS) "$@"
# General conversion
novnc-%.png: novnc-icon.svg
convert -depth 8 -background transparent \
-size $*x$* "$(lastword $^)" "$@"
# iOS icons use their own SVG
novnc-ios-%.png: novnc-ios-icon.svg
convert -depth 8 -background transparent \
-size $*x$* "$(lastword $^)" "$@"
# The smallest sizes are generated using a different SVG
novnc-16.png novnc-24.png novnc-32.png: novnc-icon-sm.svg
clean:
rm -f *.png

View File

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
viewBox="0 0 16 16"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="novnc-icon-sm.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="45.254834"
inkscape:cx="9.722703"
inkscape:cy="5.5311896"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4169" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1036.3621)">
<rect
style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4167"
width="16"
height="15.999992"
x="0"
y="1036.3622"
ry="2.6666584" />
<path
style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 2.6666667,1036.3621 C 1.1893373,1036.3621 0,1037.5515 0,1039.0288 l 0,10.6666 c 0,1.4774 1.1893373,2.6667 2.6666667,2.6667 l 4,0 C 11.837333,1052.3621 16,1046.7128 16,1039.6955 l 0,-0.6667 c 0,-1.4773 -1.189337,-2.6667 -2.666667,-2.6667 l -10.6666663,0 z"
id="rect4173"
inkscape:connector-curvature="0" />
<g
id="g4381">
<g
transform="translate(0.25,0.25)"
style="fill:#000000;fill-opacity:1"
id="g4365">
<g
style="fill:#000000;fill-opacity:1"
id="g4367">
<path
inkscape:connector-curvature="0"
id="path4369"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 4.3289754,1039.3621 c 0.1846149,0 0.3419956,0.071 0.4716623,0.2121 C 4.933546,1039.7121 5,1039.8793 5,1040.0759 l 0,3.2862 -1,0 0,-2.964 c 0,-0.024 -0.011592,-0.036 -0.034038,-0.036 l -1.931924,0 C 2.011349,1040.3621 2,1040.3741 2,1040.3981 l 0,2.964 -1,0 0,-4 z"
sodipodi:nodetypes="scsccsssscccs" />
<path
inkscape:connector-curvature="0"
id="path4371"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 6.6710244,1039.3621 2.6579513,0 c 0.184775,0 0.3419957,0.071 0.471662,0.2121 C 9.933546,1039.7121 10,1039.8793 10,1040.0759 l 0,2.5724 c 0,0.1966 -0.066454,0.3655 -0.1993623,0.5069 -0.1296663,0.1379 -0.286887,0.2069 -0.471662,0.2069 l -2.6579513,0 c -0.184775,0 -0.3436164,-0.069 -0.4765247,-0.2069 C 6.0648334,1043.0138 6,1042.8449 6,1042.6483 l 0,-2.5724 c 0,-0.1966 0.064833,-0.3638 0.1944997,-0.5017 0.1329083,-0.1414 0.2917497,-0.2121 0.4765247,-0.2121 z m 2.2949386,1 -1.931926,0 C 7.011344,1040.3621 7,1040.3741 7,1040.3981 l 0,1.928 c 0,0.024 0.011347,0.036 0.034037,0.036 l 1.931926,0 c 0.02269,0 0.034037,-0.012 0.034037,-0.036 l 0,-1.928 c 0,-0.024 -0.011347,-0.036 -0.034037,-0.036 z"
sodipodi:nodetypes="sscsscsscsscssssssssss" />
</g>
<g
style="fill:#000000;fill-opacity:1"
id="g4373">
<path
inkscape:connector-curvature="0"
id="path4375"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 3,1047.1121 1,-2.75 1,0 -1.5,4 -1,0 -1.5,-4 1,0 z"
sodipodi:nodetypes="cccccccc" />
<path
inkscape:connector-curvature="0"
id="path4377"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 9,1046.8621 0,-2.5 1,0 0,4 -1,0 -2,-2.5 0,2.5 -1,0 0,-4 1,0 z"
sodipodi:nodetypes="ccccccccccc" />
<path
inkscape:connector-curvature="0"
id="path4379"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 15,1045.3621 -2.96596,0 c -0.02269,0 -0.03404,0.012 -0.03404,0.036 l 0,1.928 c 0,0.024 0.01135,0.036 0.03404,0.036 l 2.96596,0 0,1 -3.324113,0 c -0.188017,0 -0.348479,-0.068 -0.481388,-0.2037 C 11.064833,1048.0192 11,1047.8511 11,1047.6542 l 0,-2.5842 c 0,-0.1969 0.06483,-0.3633 0.194499,-0.4991 0.132909,-0.1392 0.293371,-0.2088 0.481388,-0.2088 l 3.324113,0 z"
sodipodi:nodetypes="cssssccscsscscc" />
</g>
</g>
<g
id="g4356">
<g
id="g4347">
<path
sodipodi:nodetypes="scsccsssscccs"
d="m 4.3289754,1039.3621 c 0.1846149,0 0.3419956,0.071 0.4716623,0.2121 C 4.933546,1039.7121 5,1039.8793 5,1040.0759 l 0,3.2862 -1,0 0,-2.964 c 0,-0.024 -0.011592,-0.036 -0.034038,-0.036 l -1.931924,0 c -0.022689,0 -0.034038,0.012 -0.034038,0.036 l 0,2.964 -1,0 0,-4 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4143"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="sscsscsscsscssssssssss"
d="m 6.6710244,1039.3621 2.6579513,0 c 0.184775,0 0.3419957,0.071 0.471662,0.2121 C 9.933546,1039.7121 10,1039.8793 10,1040.0759 l 0,2.5724 c 0,0.1966 -0.066454,0.3655 -0.1993623,0.5069 -0.1296663,0.1379 -0.286887,0.2069 -0.471662,0.2069 l -2.6579513,0 c -0.184775,0 -0.3436164,-0.069 -0.4765247,-0.2069 C 6.0648334,1043.0138 6,1042.8449 6,1042.6483 l 0,-2.5724 c 0,-0.1966 0.064833,-0.3638 0.1944997,-0.5017 0.1329083,-0.1414 0.2917497,-0.2121 0.4765247,-0.2121 z m 2.2949386,1 -1.931926,0 C 7.011344,1040.3621 7,1040.3741 7,1040.3981 l 0,1.928 c 0,0.024 0.011347,0.036 0.034037,0.036 l 1.931926,0 c 0.02269,0 0.034037,-0.012 0.034037,-0.036 l 0,-1.928 c 0,-0.024 -0.011347,-0.036 -0.034037,-0.036 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4145"
inkscape:connector-curvature="0" />
</g>
<g
id="g4351">
<path
sodipodi:nodetypes="cccccccc"
d="m 3,1047.1121 1,-2.75 1,0 -1.5,4 -1,0 -1.5,-4 1,0 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4147"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="ccccccccccc"
d="m 9,1046.8621 0,-2.5 1,0 0,4 -1,0 -2,-2.5 0,2.5 -1,0 0,-4 1,0 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4149"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="cssssccscsscscc"
d="m 15,1045.3621 -2.96596,0 c -0.02269,0 -0.03404,0.012 -0.03404,0.036 l 0,1.928 c 0,0.024 0.01135,0.036 0.03404,0.036 l 2.96596,0 0,1 -3.324113,0 c -0.188017,0 -0.348479,-0.068 -0.481388,-0.2037 C 11.064833,1048.0192 11,1047.8511 11,1047.6542 l 0,-2.5842 c 0,-0.1969 0.06483,-0.3633 0.194499,-0.4991 0.132909,-0.1392 0.293371,-0.2088 0.481388,-0.2088 l 3.324113,0 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4151"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48"
height="48"
viewBox="0 0 48 48.000001"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="novnc-icon.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.313708"
inkscape:cx="27.187245"
inkscape:cy="17.700974"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4169" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1004.3621)">
<rect
style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4167"
width="48"
height="48"
x="0"
y="1004.3621"
ry="7.9999785" />
<path
style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 8,1004.3621 c -4.4319881,0 -8,3.568 -8,8 l 0,32 c 0,4.432 3.5680119,8 8,8 l 12,0 c 15.512,0 28,-16.948 28,-38 l 0,-2 c 0,-4.432 -3.568012,-8 -8,-8 l -32,0 z"
id="rect4173"
inkscape:connector-curvature="0" />
<g
id="g4300"
style="fill:#000000;fill-opacity:1;stroke:none"
transform="translate(0.5,0.5)">
<g
id="g4302"
style="fill:#000000;fill-opacity:1;stroke:none">
<path
sodipodi:nodetypes="scsccsssscccs"
d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4304"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="sscsscsscsscssssssssss"
d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4306"
inkscape:connector-curvature="0" />
</g>
<g
id="g4308"
style="fill:#000000;fill-opacity:1;stroke:none">
<path
sodipodi:nodetypes="cccccccc"
d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4310"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="ccccccccccc"
d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4312"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="cssssccscsscscc"
d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4314"
inkscape:connector-curvature="0" />
</g>
</g>
<g
id="g4291"
style="stroke:none">
<g
id="g4282"
style="stroke:none">
<path
inkscape:connector-curvature="0"
id="path4143"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
sodipodi:nodetypes="scsccsssscccs" />
<path
inkscape:connector-curvature="0"
id="path4145"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
sodipodi:nodetypes="sscsscsscsscssssssssss" />
</g>
<g
id="g4286"
style="stroke:none">
<path
inkscape:connector-curvature="0"
id="path4147"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
sodipodi:nodetypes="cccccccc" />
<path
inkscape:connector-curvature="0"
id="path4149"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
sodipodi:nodetypes="ccccccccccc" />
<path
inkscape:connector-curvature="0"
id="path4151"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
sodipodi:nodetypes="cssssccscsscscc" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 48 48.000001"
id="svg2"
version="1.1"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="novnc-ios-icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.313708"
inkscape:cx="27.356195"
inkscape:cy="17.810253"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:window-width="2560"
inkscape:window-height="1371"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid4169" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1004.3621)">
<rect
style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4167"
width="48"
height="48"
x="0"
y="1004.3621"
inkscape:label="background" />
<path
style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 0,1004.3621 v 48 h 20 c 15.512,0 28,-16.948 28,-38 v -10 z"
id="rect4173"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc"
inkscape:label="darker_grey_plate" />
<g
id="g4300"
style="display:inline;fill:#000000;fill-opacity:1;stroke:none"
transform="translate(0.5,0.5)"
inkscape:label="shadows">
<g
id="g4302"
style="fill:#000000;fill-opacity:1;stroke:none"
inkscape:label="no">
<path
sodipodi:nodetypes="scsccsssscccs"
d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 v 6.8586 h -2 v -6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 H 7.1021125 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 v 6.8914 H 5 v -9 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4304"
inkscape:connector-curvature="0"
inkscape:label="n" />
<path
sodipodi:nodetypes="sscsscsscsscssssssssss"
d="m 17.013073,1016.3621 h 4.973854 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 v 4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 h -4.973854 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 v -4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 h -4.795776 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 v 4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 h 4.795776 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 v -4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4306"
inkscape:connector-curvature="0"
inkscape:label="o" />
</g>
<g
id="g4308"
style="fill:#000000;fill-opacity:1;stroke:none"
inkscape:label="VNC">
<path
sodipodi:nodetypes="cccccccc"
d="m 12,1036.9177 4.768114,-8.5556 H 19 l -6,11 h -2 l -6,-11 h 2.2318854 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4310"
inkscape:connector-curvature="0"
inkscape:label="V" />
<path
sodipodi:nodetypes="ccccccccccc"
d="m 29,1036.3621 v -8 h 2 v 11 h -2 l -7,-8 v 8 h -2 v -11 h 2 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4312"
inkscape:connector-curvature="0"
inkscape:label="N" />
<path
sodipodi:nodetypes="cssssccscsscscc"
d="m 43,1030.3621 h -8.897887 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 v 6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 H 43 v 2 h -8.972339 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 v -6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 H 43 Z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4314"
inkscape:connector-curvature="0"
inkscape:label="C" />
</g>
</g>
<g
id="g4291"
style="stroke:none"
inkscape:label="noVNC">
<g
id="g4282"
style="stroke:none"
inkscape:label="no">
<path
inkscape:connector-curvature="0"
id="path4143"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
sodipodi:nodetypes="scsccsssscccs"
inkscape:label="n" />
<path
inkscape:connector-curvature="0"
id="path4145"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
sodipodi:nodetypes="sscsscsscsscssssssssss"
inkscape:label="o" />
</g>
<g
id="g4286"
style="stroke:none"
inkscape:label="VNC">
<path
inkscape:connector-curvature="0"
id="path4147"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
sodipodi:nodetypes="cccccccc"
inkscape:label="V" />
<path
inkscape:connector-curvature="0"
id="path4149"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
sodipodi:nodetypes="ccccccccccc"
inkscape:label="N" />
<path
inkscape:connector-curvature="0"
id="path4151"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
sodipodi:nodetypes="cssssccscsscscc"
inkscape:label="C" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="info.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="15.720838"
inkscape:cy="8.9111233"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="false"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 12.5 3 A 9.5 9.4999914 0 0 0 3 12.5 A 9.5 9.4999914 0 0 0 12.5 22 A 9.5 9.4999914 0 0 0 22 12.5 A 9.5 9.4999914 0 0 0 12.5 3 z M 12.5 5 A 1.5 1.5000087 0 0 1 14 6.5 A 1.5 1.5000087 0 0 1 12.5 8 A 1.5 1.5000087 0 0 1 11 6.5 A 1.5 1.5000087 0 0 1 12.5 5 z M 10.521484 8.9785156 L 12.521484 8.9785156 A 1.50015 1.50015 0 0 1 14.021484 10.478516 L 14.021484 15.972656 A 1.50015 1.50015 0 0 1 14.498047 18.894531 C 14.498047 18.894531 13.74301 19.228309 12.789062 18.912109 C 12.312092 18.754109 11.776235 18.366625 11.458984 17.828125 C 11.141734 17.289525 11.021484 16.668469 11.021484 15.980469 L 11.021484 11.980469 L 10.521484 11.980469 A 1.50015 1.50015 0 1 1 10.521484 8.9804688 L 10.521484 8.9785156 z "
transform="translate(0,1027.3622)"
id="path4136" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="keyboard.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/keyboard.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#717171"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="31.285341"
inkscape:cy="8.8028469"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="false"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-smooth-nodes="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 7,3 C 4.8012876,3 3,4.8013 3,7 3,11.166667 3,15.333333 3,19.5 3,20.8764 4.1236413,22 5.5,22 l 14,0 C 20.876358,22 22,20.8764 22,19.5 22,15.333333 22,11.166667 22,7 22,4.8013 20.198712,3 18,3 Z m 0,2 11,0 c 1.125307,0 2,0.8747 2,2 L 20,12 5,12 5,7 C 5,5.8747 5.8746931,5 7,5 Z M 6.5,14 C 6.777,14 7,14.223 7,14.5 7,14.777 6.777,15 6.5,15 6.223,15 6,14.777 6,14.5 6,14.223 6.223,14 6.5,14 Z m 2,0 C 8.777,14 9,14.223 9,14.5 9,14.777 8.777,15 8.5,15 8.223,15 8,14.777 8,14.5 8,14.223 8.223,14 8.5,14 Z m 2,0 C 10.777,14 11,14.223 11,14.5 11,14.777 10.777,15 10.5,15 10.223,15 10,14.777 10,14.5 10,14.223 10.223,14 10.5,14 Z m 2,0 C 12.777,14 13,14.223 13,14.5 13,14.777 12.777,15 12.5,15 12.223,15 12,14.777 12,14.5 12,14.223 12.223,14 12.5,14 Z m 2,0 C 14.777,14 15,14.223 15,14.5 15,14.777 14.777,15 14.5,15 14.223,15 14,14.777 14,14.5 14,14.223 14.223,14 14.5,14 Z m 2,0 C 16.777,14 17,14.223 17,14.5 17,14.777 16.777,15 16.5,15 16.223,15 16,14.777 16,14.5 16,14.223 16.223,14 16.5,14 Z m 2,0 C 18.777,14 19,14.223 19,14.5 19,14.777 18.777,15 18.5,15 18.223,15 18,14.777 18,14.5 18,14.223 18.223,14 18.5,14 Z m -13,2 C 5.777,16 6,16.223 6,16.5 6,16.777 5.777,17 5.5,17 5.223,17 5,16.777 5,16.5 5,16.223 5.223,16 5.5,16 Z m 2,0 C 7.777,16 8,16.223 8,16.5 8,16.777 7.777,17 7.5,17 7.223,17 7,16.777 7,16.5 7,16.223 7.223,16 7.5,16 Z m 2,0 C 9.777,16 10,16.223 10,16.5 10,16.777 9.777,17 9.5,17 9.223,17 9,16.777 9,16.5 9,16.223 9.223,16 9.5,16 Z m 2,0 C 11.777,16 12,16.223 12,16.5 12,16.777 11.777,17 11.5,17 11.223,17 11,16.777 11,16.5 11,16.223 11.223,16 11.5,16 Z m 2,0 C 13.777,16 14,16.223 14,16.5 14,16.777 13.777,17 13.5,17 13.223,17 13,16.777 13,16.5 13,16.223 13.223,16 13.5,16 Z m 2,0 C 15.777,16 16,16.223 16,16.5 16,16.777 15.777,17 15.5,17 15.223,17 15,16.777 15,16.5 15,16.223 15.223,16 15.5,16 Z m 2,0 C 17.777,16 18,16.223 18,16.5 18,16.777 17.777,17 17.5,17 17.223,17 17,16.777 17,16.5 17,16.223 17.223,16 17.5,16 Z m 2,0 C 19.777,16 20,16.223 20,16.5 20,16.777 19.777,17 19.5,17 19.223,17 19,16.777 19,16.5 19,16.223 19.223,16 19.5,16 Z M 6,18 c 0.554,0 1,0.446 1,1 0,0.554 -0.446,1 -1,1 -0.554,0 -1,-0.446 -1,-1 0,-0.554 0.446,-1 1,-1 z m 2.8261719,0 7.3476561,0 C 16.631643,18 17,18.368372 17,18.826172 l 0,0.347656 C 17,19.631628 16.631643,20 16.173828,20 L 8.8261719,20 C 8.3683573,20 8,19.631628 8,19.173828 L 8,18.826172 C 8,18.368372 8.3683573,18 8.8261719,18 Z m 10.1113281,0 0.125,0 C 19.581551,18 20,18.4184 20,18.9375 l 0,0.125 C 20,19.5816 19.581551,20 19.0625,20 l -0.125,0 C 18.418449,20 18,19.5816 18,19.0625 l 0,-0.125 C 18,18.4184 18.418449,18 18.9375,18 Z"
transform="translate(0,1027.3622)"
id="rect4160"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sccssccsssssccssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" />
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
d="m 12.499929,1033.8622 -2,2 1.500071,0 0,2 1,0 0,-2 1.499929,0 z"
id="path4150"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="power.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="9.3159849"
inkscape:cy="13.436208"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 9 6.8183594 C 6.3418164 8.1213032 4.5 10.849161 4.5 14 C 4.5 18.4065 8.0935666 22 12.5 22 C 16.906433 22 20.5 18.4065 20.5 14 C 20.5 10.849161 18.658184 8.1213032 16 6.8183594 L 16 9.125 C 17.514327 10.211757 18.5 11.984508 18.5 14 C 18.5 17.3256 15.825553 20 12.5 20 C 9.1744469 20 6.5 17.3256 6.5 14 C 6.5 11.984508 7.4856727 10.211757 9 9.125 L 9 6.8183594 z "
transform="translate(0,1027.3622)"
id="path6140" />
<path
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,1031.8836 0,6.4786"
id="path6142"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="settings.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="22.627417"
inkscape:cx="14.69683"
inkscape:cy="8.8039511"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="false"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 11 3 L 11 5.1601562 A 7.5 7.5 0 0 0 8.3671875 6.2460938 L 6.84375 4.7226562 L 4.7226562 6.84375 L 6.2480469 8.3691406 A 7.5 7.5 0 0 0 5.1523438 11 L 3 11 L 3 14 L 5.1601562 14 A 7.5 7.5 0 0 0 6.2460938 16.632812 L 4.7226562 18.15625 L 6.84375 20.277344 L 8.3691406 18.751953 A 7.5 7.5 0 0 0 11 19.847656 L 11 22 L 14 22 L 14 19.839844 A 7.5 7.5 0 0 0 16.632812 18.753906 L 18.15625 20.277344 L 20.277344 18.15625 L 18.751953 16.630859 A 7.5 7.5 0 0 0 19.847656 14 L 22 14 L 22 11 L 19.839844 11 A 7.5 7.5 0 0 0 18.753906 8.3671875 L 20.277344 6.84375 L 18.15625 4.7226562 L 16.630859 6.2480469 A 7.5 7.5 0 0 0 14 5.1523438 L 14 3 L 11 3 z M 12.5 10 A 2.5 2.5 0 0 1 15 12.5 A 2.5 2.5 0 0 1 12.5 15 A 2.5 2.5 0 0 1 10 12.5 A 2.5 2.5 0 0 1 12.5 10 z "
transform="translate(0,1027.3622)"
id="rect4967" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="tab.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="11.67335"
inkscape:cy="17.881696"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="true"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 3,1031.3622 0,8 2,0 0,-4 0,-4 -2,0 z m 2,4 4,4 0,-3 13,0 0,-2 -13,0 0,-3 -4,4 z"
id="rect5194"
inkscape:connector-curvature="0" />
<path
id="path5211"
d="m 22,1048.3622 0,-8 -2,0 0,4 0,4 2,0 z m -2,-4 -4,-4 0,3 -13,0 0,2 13,0 0,3 4,-4 z"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="extrakeys.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="15.234555"
inkscape:cy="9.9710826"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="false"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="false">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 8,1031.3622 c -2.1987124,0 -4,1.8013 -4,4 l 0,8.9996 c 0,2.1987 1.8012876,4 4,4 l 9,0 c 2.198712,0 4,-1.8013 4,-4 l 0,-8.9996 c 0,-2.1987 -1.801288,-4 -4,-4 z m 0,2 9,0 c 1.125307,0 2,0.8747 2,2 l 0,7.0005 c 0,1.1253 -0.874693,2 -2,2 l -9,0 c -1.1253069,0 -2,-0.8747 -2,-2 l 0,-7.0005 c 0,-1.1253 0.8746931,-2 2,-2 z"
id="rect5006"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ssssssssssssssssss" />
<g
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text4167"
transform="matrix(0.96021948,0,0,0.96021948,0.18921715,41.80659)">
<path
d="m 14.292969,1040.6791 -2.939453,0 -0.463868,1.3281 -1.889648,0 2.700195,-7.29 2.241211,0 2.700196,7.29 -1.889649,0 -0.458984,-1.3281 z m -2.470703,-1.3526 1.99707,0 -0.996094,-2.9004 -1.000976,2.9004 z"
id="path4172"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="warning.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="16.457343"
inkscape:cy="12.179552"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:object-paths="true"
showguides="false"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 12.513672 3.0019531 C 11.751609 2.9919531 11.052563 3.4242687 10.710938 4.1054688 L 3.2109375 19.105469 C 2.5461937 20.435369 3.5132277 21.9999 5 22 L 20 22 C 21.486772 21.9999 22.453806 20.435369 21.789062 19.105469 L 14.289062 4.1054688 C 13.951849 3.4330688 13.265888 3.0066531 12.513672 3.0019531 z M 12.478516 6.9804688 A 1.50015 1.50015 0 0 1 14 8.5 L 14 14.5 A 1.50015 1.50015 0 1 1 11 14.5 L 11 8.5 A 1.50015 1.50015 0 0 1 12.478516 6.9804688 z M 12.5 17 A 1.5 1.5 0 0 1 14 18.5 A 1.5 1.5 0 0 1 12.5 20 A 1.5 1.5 0 0 1 11 18.5 A 1.5 1.5 0 0 1 12.5 17 z "
transform="translate(0,1027.3622)"
id="path4208" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg2"
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
sodipodi:docname="windows.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:version="0.92.4 (unknown)"
x="0px"
y="0px"
viewBox="-293 384 25 25"
xml:space="preserve"
width="25"
height="25"><metadata
id="metadata21"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs19" /><sodipodi:namedview
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1136"
id="namedview17"
showgrid="true"
inkscape:pagecheckerboard="false"
inkscape:zoom="32"
inkscape:cx="3.926913"
inkscape:cy="13.255959"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"><inkscape:grid
type="xygrid"
id="grid818" /></sodipodi:namedview>
<style
type="text/css"
id="style2">
.st0{fill:#FFFFFF;}
</style>
<path
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
d="M 21 4 L 11 5.1757812 L 11 12 L 21 12 L 21 4 z M 10 5.2949219 L 4 6 L 4 12 L 10 12 L 10 5.2949219 z "
transform="translate(-293,384)"
id="path853" /><path
id="path858"
d="m -272,405 -10,-1.17578 V 397 h 10 z M -283,403.70508 -289,403 v -6 h 6 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
inkscape:connector-curvature="0" /></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1 @@
DO NOT MODIFY THE FILES IN THIS FOLDER, THEY ARE AUTOMATICALLY GENERATED FROM THE PO-FILES.

View File

@ -0,0 +1,71 @@
{
"Connecting...": "Připojení...",
"Disconnecting...": "Odpojení...",
"Reconnecting...": "Obnova připojení...",
"Internal error": "Vnitřní chyba",
"Must set host": "Hostitel musí být nastavení",
"Connected (encrypted) to ": "Připojení (šifrované) k ",
"Connected (unencrypted) to ": "Připojení (nešifrované) k ",
"Something went wrong, connection is closed": "Něco se pokazilo, odpojeno",
"Failed to connect to server": "Chyba připojení k serveru",
"Disconnected": "Odpojeno",
"New connection has been rejected with reason: ": "Nové připojení bylo odmítnuto s odůvodněním: ",
"New connection has been rejected": "Nové připojení bylo odmítnuto",
"Password is required": "Je vyžadováno heslo",
"noVNC encountered an error:": "noVNC narazilo na chybu:",
"Hide/Show the control bar": "Skrýt/zobrazit ovládací panel",
"Move/Drag viewport": "Přesunout/přetáhnout výřez",
"viewport drag": "přesun výřezu",
"Active Mouse Button": "Aktivní tlačítka myši",
"No mousebutton": "Žádné",
"Left mousebutton": "Levé tlačítko myši",
"Middle mousebutton": "Prostřední tlačítko myši",
"Right mousebutton": "Pravé tlačítko myši",
"Keyboard": "Klávesnice",
"Show keyboard": "Zobrazit klávesnici",
"Extra keys": "Extra klávesy",
"Show extra keys": "Zobrazit extra klávesy",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Přepnout Ctrl",
"Alt": "Alt",
"Toggle Alt": "Přepnout Alt",
"Send Tab": "Odeslat tabulátor",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Odeslat Esc",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Poslat Ctrl-Alt-Del",
"Shutdown/Reboot": "Vypnutí/Restart",
"Shutdown/Reboot...": "Vypnutí/Restart...",
"Power": "Napájení",
"Shutdown": "Vypnout",
"Reboot": "Restart",
"Reset": "Reset",
"Clipboard": "Schránka",
"Clear": "Vymazat",
"Fullscreen": "Celá obrazovka",
"Settings": "Nastavení",
"Shared mode": "Sdílený režim",
"View only": "Pouze prohlížení",
"Clip to window": "Přizpůsobit oknu",
"Scaling mode:": "Přizpůsobení velikosti",
"None": "Žádné",
"Local scaling": "Místní",
"Remote resizing": "Vzdálené",
"Advanced": "Pokročilé",
"Repeater ID:": "ID opakovače",
"WebSocket": "WebSocket",
"Encrypt": "Šifrování:",
"Host:": "Hostitel:",
"Port:": "Port:",
"Path:": "Cesta",
"Automatic reconnect": "Automatická obnova připojení",
"Reconnect delay (ms):": "Zpoždění připojení (ms)",
"Show dot when no cursor": "Tečka místo chybějícího kurzoru myši",
"Logging:": "Logování:",
"Disconnect": "Odpojit",
"Connect": "Připojit",
"Password:": "Heslo",
"Send Password": "Odeslat heslo",
"Cancel": "Zrušit"
}

View File

@ -0,0 +1,74 @@
{
"Connecting...": "Verbinden...",
"Disconnecting...": "Verbindung trennen...",
"Reconnecting...": "Verbindung wiederherstellen...",
"Internal error": "Interner Fehler",
"Must set host": "Richten Sie den Server ein",
"Connected (encrypted) to ": "Verbunden mit (verschlüsselt) ",
"Connected (unencrypted) to ": "Verbunden mit (unverschlüsselt) ",
"Something went wrong, connection is closed": "Etwas lief schief, Verbindung wurde getrennt",
"Disconnected": "Verbindung zum Server getrennt",
"New connection has been rejected with reason: ": "Verbindung wurde aus folgendem Grund abgelehnt: ",
"New connection has been rejected": "Verbindung wurde abgelehnt",
"Password is required": "Passwort ist erforderlich",
"noVNC encountered an error:": "Ein Fehler ist aufgetreten:",
"Hide/Show the control bar": "Kontrollleiste verstecken/anzeigen",
"Move/Drag viewport": "Ansichtsfenster verschieben/ziehen",
"viewport drag": "Ansichtsfenster ziehen",
"Active Mouse Button": "Aktive Maustaste",
"No mousebutton": "Keine Maustaste",
"Left mousebutton": "Linke Maustaste",
"Middle mousebutton": "Mittlere Maustaste",
"Right mousebutton": "Rechte Maustaste",
"Keyboard": "Tastatur",
"Show keyboard": "Tastatur anzeigen",
"Extra keys": "Zusatztasten",
"Show extra keys": "Zusatztasten anzeigen",
"Ctrl": "Strg",
"Toggle Ctrl": "Strg umschalten",
"Alt": "Alt",
"Toggle Alt": "Alt umschalten",
"Send Tab": "Tab senden",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Escape senden",
"Ctrl+Alt+Del": "Strg+Alt+Entf",
"Send Ctrl-Alt-Del": "Strg+Alt+Entf senden",
"Shutdown/Reboot": "Herunterfahren/Neustarten",
"Shutdown/Reboot...": "Herunterfahren/Neustarten...",
"Power": "Energie",
"Shutdown": "Herunterfahren",
"Reboot": "Neustarten",
"Reset": "Zurücksetzen",
"Clipboard": "Zwischenablage",
"Clear": "Löschen",
"Fullscreen": "Vollbild",
"Settings": "Einstellungen",
"Shared mode": "Geteilter Modus",
"View only": "Nur betrachten",
"Clip to window": "Auf Fenster begrenzen",
"Scaling mode:": "Skalierungsmodus:",
"None": "Keiner",
"Local scaling": "Lokales skalieren",
"Remote resizing": "Serverseitiges skalieren",
"Advanced": "Erweitert",
"Repeater ID:": "Repeater ID:",
"WebSocket": "WebSocket",
"Encrypt": "Verschlüsselt",
"Host:": "Server:",
"Port:": "Port:",
"Path:": "Pfad:",
"Automatic reconnect": "Automatisch wiederverbinden",
"Reconnect delay (ms):": "Wiederverbindungsverzögerung (ms):",
"Logging:": "Protokollierung:",
"Disconnect": "Verbindung trennen",
"Connect": "Verbinden",
"Password:": "Passwort:",
"Cancel": "Abbrechen",
"Canvas not supported.": "Canvas nicht unterstützt.",
"Disconnect timeout": "Zeitüberschreitung beim Trennen",
"Local Downscaling": "Lokales herunterskalieren",
"Local Cursor": "Lokaler Mauszeiger",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "'Clipping-Modus' aktiviert, Scrollbalken in 'IE-Vollbildmodus' werden nicht unterstützt",
"True Color": "True Color"
}

View File

@ -0,0 +1,100 @@
{
"HTTPS is required for full functionality": "Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα",
"Connecting...": "Συνδέεται...",
"Disconnecting...": "Aποσυνδέεται...",
"Reconnecting...": "Επανασυνδέεται...",
"Internal error": "Εσωτερικό σφάλμα",
"Must set host": "Πρέπει να οριστεί ο διακομιστής",
"Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ",
"Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ",
"Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε",
"Failed to connect to server": "Αποτυχία στη σύνδεση με το διακομιστή",
"Disconnected": "Αποσυνδέθηκε",
"New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ",
"New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ",
"Credentials are required": "Απαιτούνται διαπιστευτήρια",
"noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:",
"Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου",
"Drag": "Σύρσιμο",
"Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου",
"Keyboard": "Πληκτρολόγιο",
"Show Keyboard": "Εμφάνιση Πληκτρολογίου",
"Extra keys": "Επιπλέον πλήκτρα",
"Show Extra Keys": "Εμφάνιση Επιπλέον Πλήκτρων",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Εναλλαγή Ctrl",
"Alt": "Alt",
"Toggle Alt": "Εναλλαγή Alt",
"Toggle Windows": "Εναλλαγή Παράθυρων",
"Windows": "Παράθυρα",
"Send Tab": "Αποστολή Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Αποστολή Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Αποστολή Ctrl-Alt-Del",
"Shutdown/Reboot": "Κλείσιμο/Επανεκκίνηση",
"Shutdown/Reboot...": "Κλείσιμο/Επανεκκίνηση...",
"Power": "Απενεργοποίηση",
"Shutdown": "Κλείσιμο",
"Reboot": "Επανεκκίνηση",
"Reset": "Επαναφορά",
"Clipboard": "Πρόχειρο",
"Edit clipboard content in the textarea below.": "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.",
"Full Screen": "Πλήρης Οθόνη",
"Settings": "Ρυθμίσεις",
"Shared Mode": "Κοινόχρηστη Λειτουργία",
"View Only": "Μόνο Θέαση",
"Clip to Window": "Αποκοπή στο όριο του Παράθυρου",
"Scaling Mode:": "Λειτουργία Κλιμάκωσης:",
"None": "Καμία",
"Local Scaling": "Τοπική Κλιμάκωση",
"Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους",
"Advanced": "Για προχωρημένους",
"Quality:": "Ποιότητα:",
"Compression level:": "Επίπεδο συμπίεσης:",
"Repeater ID:": "Repeater ID:",
"WebSocket": "WebSocket",
"Encrypt": "Κρυπτογράφηση",
"Host:": "Όνομα διακομιστή:",
"Port:": "Πόρτα διακομιστή:",
"Path:": "Διαδρομή:",
"Automatic Reconnect": "Αυτόματη επανασύνδεση",
"Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):",
"Show Dot when No Cursor": "Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας",
"Logging:": "Καταγραφή:",
"Version:": "Έκδοση:",
"Disconnect": "Αποσύνδεση",
"Connect": "Σύνδεση",
"Server identity": "Ταυτότητα Διακομιστή",
"The server has provided the following identifying information:": "Ο διακομιστής παρείχε την ακόλουθη πληροφορία ταυτοποίησης:",
"Fingerprint:": "Δακτυλικό αποτύπωμα:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Παρακαλώ επαληθεύσετε ότι η πληροφορία είναι σωστή και πιέστε \"Αποδοχή\". Αλλιώς πιέστε \"Απόρριψη\".",
"Approve": "Αποδοχή",
"Reject": "Απόρριψη",
"Credentials": "Διαπιστευτήρια",
"Username:": "Κωδικός Χρήστη:",
"Password:": "Κωδικός Πρόσβασης:",
"Send Credentials": "Αποστολή Διαπιστευτηρίων",
"Cancel": "Ακύρωση",
"Password is required": "Απαιτείται ο κωδικός πρόσβασης",
"viewport drag": "σύρσιμο θεατού πεδίου",
"Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού",
"No mousebutton": "Χωρίς Πλήκτρο Ποντικιού",
"Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού",
"Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού",
"Right mousebutton": "Δεξί Πλήκτρο Ποντικιού",
"Clear": "Καθάρισμα",
"Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas",
"Disconnect timeout": "Παρέλευση χρονικού ορίου αποσύνδεσης",
"Local Downscaling": "Τοπική Συρρίκνωση",
"Local Cursor": "Τοπικός Δρομέας",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Εφαρμογή λειτουργίας αποκοπής αφού δεν υποστηρίζονται οι λωρίδες κύλισης σε πλήρη οθόνη στον IE",
"True Color": "Πραγματικά Χρώματα",
"Style:": "Στυλ:",
"default": "προεπιλεγμένο",
"Apply": "Εφαρμογή",
"Connection": "Σύνδεση",
"Token:": "Διακριτικό:",
"Send Password": "Αποστολή Κωδικού Πρόσβασης"
}

View File

@ -0,0 +1,68 @@
{
"Connecting...": "Conectando...",
"Connected (encrypted) to ": "Conectado (con encriptación) a",
"Connected (unencrypted) to ": "Conectado (sin encriptación) a",
"Disconnecting...": "Desconectando...",
"Disconnected": "Desconectado",
"Must set host": "Se debe configurar el host",
"Reconnecting...": "Reconectando...",
"Password is required": "La contraseña es obligatoria",
"Disconnect timeout": "Tiempo de desconexión agotado",
"noVNC encountered an error:": "noVNC ha encontrado un error:",
"Hide/Show the control bar": "Ocultar/Mostrar la barra de control",
"Move/Drag viewport": "Mover/Arrastrar la ventana",
"viewport drag": "Arrastrar la ventana",
"Active Mouse Button": "Botón activo del ratón",
"No mousebutton": "Ningún botón del ratón",
"Left mousebutton": "Botón izquierdo del ratón",
"Middle mousebutton": "Botón central del ratón",
"Right mousebutton": "Botón derecho del ratón",
"Keyboard": "Teclado",
"Show keyboard": "Mostrar teclado",
"Extra keys": "Teclas adicionales",
"Show Extra Keys": "Mostrar Teclas Adicionales",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Pulsar/Soltar Ctrl",
"Alt": "Alt",
"Toggle Alt": "Pulsar/Soltar Alt",
"Send Tab": "Enviar Tabulación",
"Tab": "Tabulación",
"Esc": "Esc",
"Send Escape": "Enviar Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Enviar Ctrl+Alt+Del",
"Shutdown/Reboot": "Apagar/Reiniciar",
"Shutdown/Reboot...": "Apagar/Reiniciar...",
"Power": "Encender",
"Shutdown": "Apagar",
"Reboot": "Reiniciar",
"Reset": "Restablecer",
"Clipboard": "Portapapeles",
"Clear": "Vaciar",
"Fullscreen": "Pantalla Completa",
"Settings": "Configuraciones",
"Encrypt": "Encriptar",
"Shared Mode": "Modo Compartido",
"View only": "Solo visualización",
"Clip to window": "Recortar al tamaño de la ventana",
"Scaling mode:": "Modo de escalado:",
"None": "Ninguno",
"Local Scaling": "Escalado Local",
"Local Downscaling": "Reducción de escala local",
"Remote resizing": "Cambio de tamaño remoto",
"Advanced": "Avanzado",
"Local Cursor": "Cursor Local",
"Repeater ID:": "ID del Repetidor:",
"WebSocket": "WebSocket",
"Host:": "Host:",
"Port:": "Puerto:",
"Path:": "Ruta:",
"Automatic reconnect": "Reconexión automática",
"Reconnect delay (ms):": "Retraso en la reconexión (ms):",
"Logging:": "Registrando:",
"Disconnect": "Desconectar",
"Connect": "Conectar",
"Password:": "Contraseña:",
"Cancel": "Cancelar",
"Canvas not supported.": "Canvas no soportado."
}

View File

@ -0,0 +1,82 @@
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Lancer sans HTTPS n'est pas recommandé, crashs ou autres problèmes en vue.",
"Connecting...": "En cours de connexion...",
"Disconnecting...": "Déconnexion en cours...",
"Reconnecting...": "Reconnexion en cours...",
"Internal error": "Erreur interne",
"Failed to connect to server: ": "Échec de connexion au serveur ",
"Connected (encrypted) to ": "Connecté (chiffré) à ",
"Connected (unencrypted) to ": "Connecté (non chiffré) à ",
"Something went wrong, connection is closed": "Quelque chose s'est mal passé, la connexion a été fermée",
"Failed to connect to server": "Échec de connexion au serveur",
"Disconnected": "Déconnecté",
"New connection has been rejected with reason: ": "Une nouvelle connexion a été rejetée avec motif : ",
"New connection has been rejected": "Une nouvelle connexion a été rejetée",
"Credentials are required": "Les identifiants sont requis",
"noVNC encountered an error:": "noVNC a rencontré une erreur :",
"Hide/Show the control bar": "Masquer/Afficher la barre de contrôle",
"Drag": "Faire glisser",
"Move/Drag viewport": "Déplacer la fenêtre de visualisation",
"Keyboard": "Clavier",
"Show keyboard": "Afficher le clavier",
"Extra keys": "Touches supplémentaires",
"Show extra keys": "Afficher les touches supplémentaires",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Basculer Ctrl",
"Alt": "Alt",
"Toggle Alt": "Basculer Alt",
"Toggle Windows": "Basculer Windows",
"Windows": "Fenêtre",
"Send Tab": "Envoyer Tab",
"Tab": "Tabulation",
"Esc": "Esc",
"Send Escape": "Envoyer Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Envoyer Ctrl-Alt-Del",
"Shutdown/Reboot": "Arrêter/Redémarrer",
"Shutdown/Reboot...": "Arrêter/Redémarrer...",
"Power": "Alimentation",
"Shutdown": "Arrêter",
"Reboot": "Redémarrer",
"Reset": "Réinitialiser",
"Clipboard": "Presse-papiers",
"Edit clipboard content in the textarea below.": "Editer le contenu du presse-papier dans la zone ci-dessous.",
"Full screen": "Plein écran",
"Settings": "Paramètres",
"Shared mode": "Mode partagé",
"View only": "Afficher uniquement",
"Clip to window": "Ajuster à la fenêtre",
"Scaling mode:": "Mode mise à l'échelle :",
"None": "Aucun",
"Local scaling": "Mise à l'échelle locale",
"Remote resizing": "Redimensionnement à distance",
"Advanced": "Avancé",
"Quality:": "Qualité :",
"Compression level:": "Niveau de compression :",
"Repeater ID:": "ID Répéteur :",
"WebSocket": "WebSocket",
"Encrypt": "Chiffrer",
"Host:": "Hôte :",
"Port:": "Port :",
"Path:": "Chemin :",
"Automatic reconnect": "Reconnecter automatiquement",
"Reconnect delay (ms):": "Délai de reconnexion (ms) :",
"Show dot when no cursor": "Afficher le point lorsqu'il n'y a pas de curseur",
"Logging:": "Se connecter :",
"Version:": "Version :",
"Disconnect": "Déconnecter",
"Connect": "Connecter",
"Server identity": "Identité du serveur",
"The server has provided the following identifying information:": "Le serveur a fourni l'identification suivante :",
"Fingerprint:": "Empreinte digitale :",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "SVP, verifiez que l'information est correcte et pressez \"Accepter\". Sinon pressez \"Refuser\".",
"Approve": "Accepter",
"Reject": "Refuser",
"Credentials": "Envoyer les identifiants",
"Username:": "Nom d'utilisateur :",
"Password:": "Mot de passe :",
"Send credentials": "Envoyer les identifiants",
"Cancel": "Annuler",
"Must set host": "Doit définir l'hôte",
"Clear": "Effacer"
}

View File

@ -0,0 +1,68 @@
{
"Connecting...": "Connessione in corso...",
"Disconnecting...": "Disconnessione...",
"Reconnecting...": "Riconnessione...",
"Internal error": "Errore interno",
"Must set host": "Devi impostare l'host",
"Connected (encrypted) to ": "Connesso (crittografato) a ",
"Connected (unencrypted) to ": "Connesso (non crittografato) a",
"Something went wrong, connection is closed": "Qualcosa è andato storto, la connessione è stata chiusa",
"Failed to connect to server": "Impossibile connettersi al server",
"Disconnected": "Disconnesso",
"New connection has been rejected with reason: ": "La nuova connessione è stata rifiutata con motivo: ",
"New connection has been rejected": "La nuova connessione è stata rifiutata",
"Credentials are required": "Le credenziali sono obbligatorie",
"noVNC encountered an error:": "noVNC ha riscontrato un errore:",
"Hide/Show the control bar": "Nascondi/Mostra la barra di controllo",
"Keyboard": "Tastiera",
"Show keyboard": "Mostra tastiera",
"Extra keys": "Tasti Aggiuntivi",
"Show Extra Keys": "Mostra Tasti Aggiuntivi",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Tieni premuto Ctrl",
"Alt": "Alt",
"Toggle Alt": "Tieni premuto Alt",
"Toggle Windows": "Tieni premuto Windows",
"Windows": "Windows",
"Send Tab": "Invia Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Invia Esc",
"Ctrl+Alt+Del": "Ctrl+Alt+Canc",
"Send Ctrl-Alt-Del": "Invia Ctrl-Alt-Canc",
"Shutdown/Reboot": "Spegnimento/Riavvio",
"Shutdown/Reboot...": "Spegnimento/Riavvio...",
"Power": "Alimentazione",
"Shutdown": "Spegnimento",
"Reboot": "Riavvio",
"Reset": "Reset",
"Clipboard": "Clipboard",
"Clear": "Pulisci",
"Fullscreen": "Schermo intero",
"Settings": "Impostazioni",
"Shared mode": "Modalità condivisa",
"View Only": "Sola Visualizzazione",
"Scaling mode:": "Modalità di ridimensionamento:",
"None": "Nessuna",
"Local Scaling": "Ridimensionamento Locale",
"Remote Resizing": "Ridimensionamento Remoto",
"Advanced": "Avanzate",
"Quality:": "Qualità:",
"Compression level:": "Livello Compressione:",
"Repeater ID:": "ID Ripetitore:",
"WebSocket": "WebSocket",
"Encrypt": "Crittografa",
"Host:": "Host:",
"Port:": "Porta:",
"Path:": "Percorso:",
"Automatic Reconnect": "Riconnessione Automatica",
"Reconnect Delay (ms):": "Ritardo Riconnessione (ms):",
"Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore",
"Version:": "Versione:",
"Disconnect": "Disconnetti",
"Connect": "Connetti",
"Username:": "Utente:",
"Password:": "Password:",
"Send Credentials": "Invia Credenziale",
"Cancel": "Annulla"
}

View File

@ -0,0 +1,81 @@
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "HTTPS接続なしで実行することは推奨されません。クラッシュしたりその他の問題が発生したりする可能性があります。",
"Connecting...": "接続しています...",
"Disconnecting...": "切断しています...",
"Reconnecting...": "再接続しています...",
"Internal error": "内部エラー",
"Must set host": "ホストを設定する必要があります",
"Failed to connect to server: ": "サーバーへの接続に失敗しました: ",
"Connected (encrypted) to ": "接続しました (暗号化済み): ",
"Connected (unencrypted) to ": "接続しました (暗号化されていません): ",
"Something went wrong, connection is closed": "問題が発生したため、接続が閉じられました",
"Failed to connect to server": "サーバーへの接続に失敗しました",
"Disconnected": "切断しました",
"New connection has been rejected with reason: ": "新規接続は次の理由で拒否されました: ",
"New connection has been rejected": "新規接続は拒否されました",
"Credentials are required": "資格情報が必要です",
"noVNC encountered an error:": "noVNC でエラーが発生しました:",
"Hide/Show the control bar": "コントロールバーを隠す/表示する",
"Drag": "ドラッグ",
"Move/Drag viewport": "ビューポートを移動/ドラッグ",
"Keyboard": "キーボード",
"Show keyboard": "キーボードを表示",
"Extra keys": "追加キー",
"Show extra keys": "追加キーを表示",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl キーをトグル",
"Alt": "Alt",
"Toggle Alt": "Alt キーをトグル",
"Toggle Windows": "Windows キーをトグル",
"Windows": "Windows",
"Send Tab": "Tab キーを送信",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Escape キーを送信",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Ctrl-Alt-Del を送信",
"Shutdown/Reboot": "シャットダウン/再起動",
"Shutdown/Reboot...": "シャットダウン/再起動...",
"Power": "電源",
"Shutdown": "シャットダウン",
"Reboot": "再起動",
"Reset": "リセット",
"Clipboard": "クリップボード",
"Edit clipboard content in the textarea below.": "以下の入力欄からクリップボードの内容を編集できます。",
"Full screen": "全画面表示",
"Settings": "設定",
"Shared mode": "共有モード",
"View only": "表示専用",
"Clip to window": "ウィンドウにクリップ",
"Scaling mode:": "スケーリングモード:",
"None": "なし",
"Local scaling": "ローカルでスケーリング",
"Remote resizing": "リモートでリサイズ",
"Advanced": "高度",
"Quality:": "品質:",
"Compression level:": "圧縮レベル:",
"Repeater ID:": "リピーター ID:",
"WebSocket": "WebSocket",
"Encrypt": "暗号化",
"Host:": "ホスト:",
"Port:": "ポート:",
"Path:": "パス:",
"Automatic reconnect": "自動再接続",
"Reconnect delay (ms):": "再接続する遅延 (ミリ秒):",
"Show dot when no cursor": "カーソルがないときにドットを表示する",
"Logging:": "ロギング:",
"Version:": "バージョン:",
"Disconnect": "切断",
"Connect": "接続",
"Server identity": "サーバーの識別情報",
"The server has provided the following identifying information:": "サーバーは以下の識別情報を提供しています:",
"Fingerprint:": "フィンガープリント:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してください。",
"Approve": "承認",
"Reject": "拒否",
"Credentials": "資格情報",
"Username:": "ユーザー名:",
"Password:": "パスワード:",
"Send credentials": "資格情報を送信",
"Cancel": "キャンセル"
}

View File

@ -0,0 +1,70 @@
{
"Connecting...": "연결중...",
"Disconnecting...": "연결 해제중...",
"Reconnecting...": "재연결중...",
"Internal error": "내부 오류",
"Must set host": "호스트는 설정되어야 합니다.",
"Connected (encrypted) to ": "다음과 (암호화되어) 연결되었습니다:",
"Connected (unencrypted) to ": "다음과 (암호화 없이) 연결되었습니다:",
"Something went wrong, connection is closed": "무언가 잘못되었습니다, 연결이 닫혔습니다.",
"Failed to connect to server": "서버에 연결하지 못했습니다.",
"Disconnected": "연결이 해제되었습니다.",
"New connection has been rejected with reason: ": "새 연결이 다음 이유로 거부되었습니다:",
"New connection has been rejected": "새 연결이 거부되었습니다.",
"Password is required": "비밀번호가 필요합니다.",
"noVNC encountered an error:": "noVNC에 오류가 발생했습니다:",
"Hide/Show the control bar": "컨트롤 바 숨기기/보이기",
"Move/Drag viewport": "움직이기/드래그 뷰포트",
"viewport drag": "뷰포트 드래그",
"Active Mouse Button": "마우스 버튼 활성화",
"No mousebutton": "마우스 버튼 없음",
"Left mousebutton": "왼쪽 마우스 버튼",
"Middle mousebutton": "중간 마우스 버튼",
"Right mousebutton": "오른쪽 마우스 버튼",
"Keyboard": "키보드",
"Show keyboard": "키보드 보이기",
"Extra keys": "기타 키들",
"Show extra keys": "기타 키들 보이기",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl 켜기/끄기",
"Alt": "Alt",
"Toggle Alt": "Alt 켜기/끄기",
"Send Tab": "Tab 보내기",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Esc 보내기",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Ctrl+Alt+Del 보내기",
"Shutdown/Reboot": "셧다운/리붓",
"Shutdown/Reboot...": "셧다운/리붓...",
"Power": "전원",
"Shutdown": "셧다운",
"Reboot": "리붓",
"Reset": "리셋",
"Clipboard": "클립보드",
"Clear": "지우기",
"Fullscreen": "전체화면",
"Settings": "설정",
"Shared mode": "공유 모드",
"View only": "보기 전용",
"Clip to window": "창에 클립",
"Scaling mode:": "스케일링 모드:",
"None": "없음",
"Local scaling": "로컬 스케일링",
"Remote resizing": "원격 크기 조절",
"Advanced": "고급",
"Repeater ID:": "중계 ID",
"WebSocket": "웹소켓",
"Encrypt": "암호화",
"Host:": "호스트:",
"Port:": "포트:",
"Path:": "위치:",
"Automatic reconnect": "자동 재연결",
"Reconnect delay (ms):": "재연결 지연 시간 (ms)",
"Logging:": "로깅",
"Disconnect": "연결 해제",
"Connect": "연결",
"Password:": "비밀번호:",
"Send Password": "비밀번호 전송",
"Cancel": "취소"
}

View File

@ -0,0 +1,95 @@
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Het is niet aan te raden om zonder HTTPS te werken, crashes of andere problemen zijn dan waarschijnlijk.",
"Connecting...": "Aan het verbinden…",
"Disconnecting...": "Bezig om verbinding te verbreken...",
"Reconnecting...": "Opnieuw verbinding maken...",
"Internal error": "Interne fout",
"Failed to connect to server: ": "Verbinding maken met server is mislukt",
"Connected (encrypted) to ": "Verbonden (versleuteld) met ",
"Connected (unencrypted) to ": "Verbonden (onversleuteld) met ",
"Something went wrong, connection is closed": "Er iets fout gelopen, verbinding werd verbroken",
"Failed to connect to server": "Verbinding maken met server is mislukt",
"Disconnected": "Verbinding verbroken",
"New connection has been rejected with reason: ": "Nieuwe verbinding is geweigerd met de volgende reden: ",
"New connection has been rejected": "Nieuwe verbinding is geweigerd",
"Credentials are required": "Inloggegevens zijn nodig",
"noVNC encountered an error:": "noVNC heeft een fout bemerkt:",
"Hide/Show the control bar": "Verberg/Toon de bedieningsbalk",
"Drag": "Sleep",
"Move/Drag viewport": "Verplaats/Versleep Kijkvenster",
"Keyboard": "Toetsenbord",
"Show keyboard": "Toon Toetsenbord",
"Extra keys": "Extra toetsen",
"Show extra keys": "Toon Extra Toetsen",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl omschakelen",
"Alt": "Alt",
"Toggle Alt": "Alt omschakelen",
"Toggle Windows": "Vensters omschakelen",
"Windows": "Vensters",
"Send Tab": "Tab Sturen",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Escape Sturen",
"Ctrl+Alt+Del": "Ctrl-Alt-Del",
"Send Ctrl-Alt-Del": "Ctrl-Alt-Del Sturen",
"Shutdown/Reboot": "Uitschakelen/Herstarten",
"Shutdown/Reboot...": "Uitschakelen/Herstarten...",
"Power": "Systeem",
"Shutdown": "Uitschakelen",
"Reboot": "Herstarten",
"Reset": "Resetten",
"Clipboard": "Klembord",
"Edit clipboard content in the textarea below.": "Edit de inhoud van het klembord in het tekstveld hieronder",
"Full screen": "Volledig Scherm",
"Settings": "Instellingen",
"Shared mode": "Gedeelde Modus",
"View only": "Alleen Kijken",
"Clip to window": "Randen buiten venster afsnijden",
"Scaling mode:": "Schaalmodus:",
"None": "Geen",
"Local scaling": "Lokaal Schalen",
"Remote resizing": "Op Afstand Formaat Wijzigen",
"Advanced": "Geavanceerd",
"Quality:": "Kwaliteit:",
"Compression level:": "Compressieniveau:",
"Repeater ID:": "Repeater ID:",
"WebSocket": "WebSocket",
"Encrypt": "Versleutelen",
"Host:": "Host:",
"Port:": "Poort:",
"Path:": "Pad:",
"Automatic reconnect": "Automatisch Opnieuw Verbinden",
"Reconnect delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):",
"Show dot when no cursor": "Geef stip weer indien geen cursor",
"Logging:": "Logmeldingen:",
"Version:": "Versie:",
"Disconnect": "Verbinding verbreken",
"Connect": "Verbinden",
"Server identity": "Serveridentiteit",
"The server has provided the following identifying information:": "De server geeft de volgende identificerende informatie:",
"Fingerprint:": "Vingerafdruk:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Verifieer dat de informatie is correct en druk “OK”. Druk anders op “Afwijzen”.",
"Approve": "OK",
"Reject": "Afwijzen",
"Credentials": "Inloggegevens",
"Username:": "Gebruikersnaam:",
"Password:": "Wachtwoord:",
"Send credentials": "Stuur inloggegevens",
"Cancel": "Annuleren",
"Must set host": "Host moeten worden ingesteld",
"Password is required": "Wachtwoord is vereist",
"viewport drag": "kijkvenster slepen",
"Active Mouse Button": "Actieve Muisknop",
"No mousebutton": "Geen muisknop",
"Left mousebutton": "Linker muisknop",
"Middle mousebutton": "Middelste muisknop",
"Right mousebutton": "Rechter muisknop",
"Clear": "Wissen",
"Send Password": "Verzend Wachtwoord:",
"Disconnect timeout": "Timeout tijdens verbreken van verbinding",
"Local Downscaling": "Lokaal Neerschalen",
"Local Cursor": "Lokale Cursor",
"Canvas not supported.": "Canvas wordt niet ondersteund.",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "''Clipping mode' ingeschakeld, omdat schuifbalken in volledige-scherm-modus in IE niet worden ondersteund"
}

View File

@ -0,0 +1,80 @@
{
"Connecting...": "Łączenie...",
"Disconnecting...": "Rozłączanie...",
"Reconnecting...": "Łączenie...",
"Internal error": "Błąd wewnętrzny",
"Must set host": "Host i port są wymagane",
"Connected (encrypted) to ": "Połączenie (szyfrowane) z ",
"Connected (unencrypted) to ": "Połączenie (nieszyfrowane) z ",
"Something went wrong, connection is closed": "Coś poszło źle, połączenie zostało zamknięte",
"Disconnected": "Rozłączony",
"New connection has been rejected with reason: ": "Nowe połączenie zostało odrzucone z powodu: ",
"New connection has been rejected": "Nowe połączenie zostało odrzucone",
"Password is required": "Hasło jest wymagane",
"noVNC encountered an error:": "noVNC napotkało błąd:",
"Hide/Show the control bar": "Pokaż/Ukryj pasek ustawień",
"Move/Drag Viewport": "Ruszaj/Przeciągaj Viewport",
"viewport drag": "przeciągnij viewport",
"Active Mouse Button": "Aktywny Przycisk Myszy",
"No mousebutton": "Brak przycisku myszy",
"Left mousebutton": "Lewy przycisk myszy",
"Middle mousebutton": "Środkowy przycisk myszy",
"Right mousebutton": "Prawy przycisk myszy",
"Keyboard": "Klawiatura",
"Show keyboard": "Pokaż klawiaturę",
"Extra keys": "Przyciski dodatkowe",
"Show extra keys": "Pokaż przyciski dodatkowe",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Przełącz Ctrl",
"Alt": "Alt",
"Toggle Alt": "Przełącz Alt",
"Send Tab": "Wyślij Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Wyślij Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Wyślij Ctrl-Alt-Del",
"Shutdown/Reboot": "Wyłącz/Uruchom ponownie",
"Shutdown/Reboot...": "Wyłącz/Uruchom ponownie...",
"Power": "Włączony",
"Shutdown": "Wyłącz",
"Reboot": "Uruchom ponownie",
"Reset": "Resetuj",
"Clipboard": "Schowek",
"Clear": "Wyczyść",
"Fullscreen": "Pełny ekran",
"Settings": "Ustawienia",
"Shared Mode": "Tryb Współdzielenia",
"View Only": "Tylko Podgląd",
"Clip to Window": "Przytnij do Okna",
"Scaling Mode:": "Tryb Skalowania:",
"None": "Brak",
"Local scaling": "Skalowanie lokalne",
"Remote resizing": "Skalowanie zdalne",
"Advanced": "Zaawansowane",
"Repeater ID:": "ID Repeatera:",
"WebSocket": "WebSocket",
"Encrypt": "Szyfrowanie",
"Host:": "Host:",
"Port:": "Port:",
"Path:": "Ścieżka:",
"Automatic reconnect": "Automatycznie wznawiaj połączenie",
"Reconnect delay (ms):": "Opóźnienie wznawiania (ms):",
"Logging:": "Poziom logowania:",
"Disconnect": "Rozłącz",
"Connect": "Połącz",
"Password:": "Hasło:",
"Cancel": "Anuluj",
"Canvas not supported.": "Element Canvas nie jest wspierany.",
"Disconnect timeout": "Timeout rozłączenia",
"Local Downscaling": "Downscaling lokalny",
"Local Cursor": "Lokalny kursor",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Wymuszam clipping mode ponieważ paski przewijania nie są wspierane przez IE w trybie pełnoekranowym",
"True Color": "True Color",
"Style:": "Styl:",
"default": "domyślny",
"Apply": "Zapisz",
"Connection": "Połączenie",
"Token:": "Token:",
"Send Password": "Wyślij Hasło"
}

View File

@ -0,0 +1,72 @@
{
"Connecting...": "Conectando...",
"Disconnecting...": "Desconectando...",
"Reconnecting...": "Reconectando...",
"Internal error": "Erro interno",
"Must set host": "É necessário definir o host",
"Connected (encrypted) to ": "Conectado (com criptografia) a ",
"Connected (unencrypted) to ": "Conectado (sem criptografia) a ",
"Something went wrong, connection is closed": "Algo deu errado. A conexão foi encerrada.",
"Failed to connect to server": "Falha ao conectar-se ao servidor",
"Disconnected": "Desconectado",
"New connection has been rejected with reason: ": "A nova conexão foi rejeitada pelo motivo: ",
"New connection has been rejected": "A nova conexão foi rejeitada",
"Credentials are required": "Credenciais são obrigatórias",
"noVNC encountered an error:": "O noVNC encontrou um erro:",
"Hide/Show the control bar": "Esconder/mostrar a barra de controles",
"Drag": "Arrastar",
"Move/Drag viewport": "Mover/arrastar a janela",
"Keyboard": "Teclado",
"Show keyboard": "Mostrar teclado",
"Extra keys": "Teclas adicionais",
"Show extra keys": "Mostar teclas adicionais",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Pressionar/soltar Ctrl",
"Alt": "Alt",
"Toggle Alt": "Pressionar/soltar Alt",
"Toggle Windows": "Pressionar/soltar Windows",
"Windows": "Windows",
"Send Tab": "Enviar Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Enviar Esc",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Enviar Ctrl-Alt-Del",
"Shutdown/Reboot": "Desligar/reiniciar",
"Shutdown/Reboot...": "Desligar/reiniciar...",
"Power": "Ligar",
"Shutdown": "Desligar",
"Reboot": "Reiniciar",
"Reset": "Reiniciar (forçado)",
"Clipboard": "Área de transferência",
"Clear": "Limpar",
"Fullscreen": "Tela cheia",
"Settings": "Configurações",
"Shared mode": "Modo compartilhado",
"View only": "Apenas visualizar",
"Clip to window": "Recortar à janela",
"Scaling mode:": "Modo de dimensionamento:",
"None": "Nenhum",
"Local scaling": "Local",
"Remote resizing": "Remoto",
"Advanced": "Avançado",
"Quality:": "Qualidade:",
"Compression level:": "Nível de compressão:",
"Repeater ID:": "ID do repetidor:",
"WebSocket": "WebSocket",
"Encrypt": "Criptografar",
"Host:": "Host:",
"Port:": "Porta:",
"Path:": "Caminho:",
"Automatic reconnect": "Reconexão automática",
"Reconnect delay (ms):": "Atraso da reconexão (ms)",
"Show dot when no cursor": "Mostrar ponto quando não há cursor",
"Logging:": "Registros:",
"Version:": "Versão:",
"Disconnect": "Desconectar",
"Connect": "Conectar",
"Username:": "Nome de usuário:",
"Password:": "Senha:",
"Send credentials": "Enviar credenciais",
"Cancel": "Cancelar"
}

View File

@ -0,0 +1,72 @@
{
"Connecting...": "Подключение...",
"Disconnecting...": "Отключение...",
"Reconnecting...": "Переподключение...",
"Internal error": "Внутренняя ошибка",
"Must set host": "Задайте имя сервера или IP",
"Connected (encrypted) to ": "Подключено (с шифрованием) к ",
"Connected (unencrypted) to ": "Подключено (без шифрования) к ",
"Something went wrong, connection is closed": "Что-то пошло не так, подключение разорвано",
"Failed to connect to server": "Ошибка подключения к серверу",
"Disconnected": "Отключено",
"New connection has been rejected with reason: ": "Новое соединение отклонено по причине: ",
"New connection has been rejected": "Новое соединение отклонено",
"Credentials are required": "Требуются учетные данные",
"noVNC encountered an error:": "Ошибка noVNC: ",
"Hide/Show the control bar": "Скрыть/Показать контрольную панель",
"Drag": "Переместить",
"Move/Drag viewport": "Переместить окно",
"Keyboard": "Клавиатура",
"Show keyboard": "Показать клавиатуру",
"Extra keys": "Дополнительные Кнопки",
"Show Extra Keys": "Показать Дополнительные Кнопки",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Зажать Ctrl",
"Alt": "Alt",
"Toggle Alt": "Зажать Alt",
"Toggle Windows": "Зажать Windows",
"Windows": "Вкладка",
"Send Tab": "Передать нажатие Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Передать нажатие Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Передать нажатие Ctrl-Alt-Del",
"Shutdown/Reboot": "Выключить/Перезагрузить",
"Shutdown/Reboot...": "Выключить/Перезагрузить...",
"Power": "Питание",
"Shutdown": "Выключить",
"Reboot": "Перезагрузить",
"Reset": "Сброс",
"Clipboard": "Буфер обмена",
"Clear": "Очистить",
"Fullscreen": "Во весь экран",
"Settings": "Настройки",
"Shared mode": "Общий режим",
"View Only": "Только Просмотр",
"Clip to window": "В окно",
"Scaling mode:": "Масштаб:",
"None": "Нет",
"Local scaling": "Локальный масштаб",
"Remote resizing": "Удаленная перенастройка размера",
"Advanced": "Дополнительно",
"Quality:": "Качество",
"Compression level:": "Уровень Сжатия",
"Repeater ID:": "Идентификатор ID:",
"WebSocket": "WebSocket",
"Encrypt": "Шифрование",
"Host:": "Сервер:",
"Port:": "Порт:",
"Path:": "Путь:",
"Automatic reconnect": "Автоматическое переподключение",
"Reconnect delay (ms):": "Задержка переподключения (мс):",
"Show dot when no cursor": "Показать точку вместо курсора",
"Logging:": "Лог:",
"Version:": "Версия",
"Disconnect": "Отключение",
"Connect": "Подключение",
"Username:": "Имя Пользователя",
"Password:": "Пароль:",
"Send Credentials": "Передача Учетных Данных",
"Cancel": "Выход"
}

View File

@ -0,0 +1,83 @@
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är troliga.",
"Connecting...": "Ansluter...",
"Disconnecting...": "Kopplar ner...",
"Reconnecting...": "Återansluter...",
"Internal error": "Internt fel",
"Failed to connect to server: ": "Misslyckades att ansluta till servern: ",
"Connected (encrypted) to ": "Ansluten (krypterat) till ",
"Connected (unencrypted) to ": "Ansluten (okrypterat) till ",
"Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades",
"Failed to connect to server": "Misslyckades att ansluta till servern",
"Disconnected": "Frånkopplad",
"New connection has been rejected with reason: ": "Ny anslutning har blivit nekad med följande skäl: ",
"New connection has been rejected": "Ny anslutning har blivit nekad",
"Credentials are required": "Användaruppgifter krävs",
"noVNC encountered an error:": "noVNC stötte på ett problem:",
"Hide/Show the control bar": "Göm/Visa kontrollbaren",
"Drag": "Dra",
"Move/Drag viewport": "Flytta/Dra vyn",
"Keyboard": "Tangentbord",
"Show keyboard": "Visa tangentbord",
"Extra keys": "Extraknappar",
"Show extra keys": "Visa extraknappar",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Växla Ctrl",
"Alt": "Alt",
"Toggle Alt": "Växla Alt",
"Toggle Windows": "Växla Windows",
"Windows": "Windows",
"Send Tab": "Skicka Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Skicka Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Skicka Ctrl-Alt-Del",
"Shutdown/Reboot": "Stäng av/Boota om",
"Shutdown/Reboot...": "Stäng av/Boota om...",
"Power": "Ström",
"Shutdown": "Stäng av",
"Reboot": "Boota om",
"Reset": "Återställ",
"Clipboard": "Urklipp",
"Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.",
"Full screen": "Fullskärm",
"Settings": "Inställningar",
"Shared mode": "Delat läge",
"View only": "Endast visning",
"Clip to window": "Begränsa till fönster",
"Scaling mode:": "Skalningsläge:",
"None": "Ingen",
"Local scaling": "Lokal skalning",
"Remote resizing": "Ändra storlek",
"Advanced": "Avancerat",
"Quality:": "Kvalitet:",
"Compression level:": "Kompressionsnivå:",
"Repeater ID:": "Repeater-ID:",
"WebSocket": "WebSocket",
"Encrypt": "Kryptera",
"Host:": "Värd:",
"Port:": "Port:",
"Path:": "Sökväg:",
"Automatic reconnect": "Automatisk återanslutning",
"Reconnect delay (ms):": "Fördröjning (ms):",
"Show dot when no cursor": "Visa prick när ingen muspekare finns",
"Logging:": "Loggning:",
"Version:": "Version:",
"Disconnect": "Koppla från",
"Connect": "Anslut",
"Server identity": "Server-identitet",
"The server has provided the following identifying information:": "Servern har gett följande identifierande information:",
"Fingerprint:": "Fingeravtryck:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck annars \"Neka\".",
"Approve": "Godkänn",
"Reject": "Neka",
"Credentials": "Användaruppgifter",
"Username:": "Användarnamn:",
"Password:": "Lösenord:",
"Send credentials": "Skicka användaruppgifter",
"Cancel": "Avbryt",
"Must set host": "Du måste specifiera en värd",
"HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet",
"Clear": "Rensa"
}

View File

@ -0,0 +1,69 @@
{
"Connecting...": "Bağlanıyor...",
"Disconnecting...": "Bağlantı kesiliyor...",
"Reconnecting...": "Yeniden bağlantı kuruluyor...",
"Internal error": "İç hata",
"Must set host": "Sunucuyu kur",
"Connected (encrypted) to ": "Bağlı (şifrelenmiş)",
"Connected (unencrypted) to ": "Bağlandı (şifrelenmemiş)",
"Something went wrong, connection is closed": "Bir şeyler ters gitti, bağlantı kesildi",
"Disconnected": "Bağlantı kesildi",
"New connection has been rejected with reason: ": "Bağlantı aşağıdaki nedenlerden dolayı reddedildi: ",
"New connection has been rejected": "Bağlantı reddedildi",
"Password is required": "Şifre gerekli",
"noVNC encountered an error:": "Bir hata oluştu:",
"Hide/Show the control bar": "Denetim masasını Gizle/Göster",
"Move/Drag Viewport": "Görünümü Taşı/Sürükle",
"viewport drag": "Görüntü penceresini sürükle",
"Active Mouse Button": "Aktif Fare Düğmesi",
"No mousebutton": "Fare düğmesi yok",
"Left mousebutton": "Farenin sol düğmesi",
"Middle mousebutton": "Farenin orta düğmesi",
"Right mousebutton": "Farenin sağ düğmesi",
"Keyboard": "Klavye",
"Show Keyboard": "Klavye Düzenini Göster",
"Extra keys": "Ekstra tuşlar",
"Show extra keys": "Ekstra tuşları göster",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl Değiştir ",
"Alt": "Alt",
"Toggle Alt": "Alt Değiştir",
"Send Tab": "Sekme Gönder",
"Tab": "Sekme",
"Esc": "Esc",
"Send Escape": "Boşluk Gönder",
"Ctrl+Alt+Del": "Ctrl + Alt + Del",
"Send Ctrl-Alt-Del": "Ctrl-Alt-Del Gönder",
"Shutdown/Reboot": "Kapat/Yeniden Başlat",
"Shutdown/Reboot...": "Kapat/Yeniden Başlat...",
"Power": "Güç",
"Shutdown": "Kapat",
"Reboot": "Yeniden Başlat",
"Reset": "Sıfırla",
"Clipboard": "Pano",
"Clear": "Temizle",
"Fullscreen": "Tam Ekran",
"Settings": "Ayarlar",
"Shared Mode": "Paylaşım Modu",
"View Only": "Sadece Görüntüle",
"Clip to Window": "Pencereye Tıkla",
"Scaling Mode:": "Ölçekleme Modu:",
"None": "Bilinmeyen",
"Local Scaling": "Yerel Ölçeklendirme",
"Remote Resizing": "Uzaktan Yeniden Boyutlandırma",
"Advanced": "Gelişmiş",
"Repeater ID:": "Tekralayıcı ID:",
"WebSocket": "WebSocket",
"Encrypt": "Şifrele",
"Host:": "Ana makine:",
"Port:": "Port:",
"Path:": "Yol:",
"Automatic Reconnect": "Otomatik Yeniden Bağlan",
"Reconnect Delay (ms):": "Yeniden Bağlanma Süreci (ms):",
"Logging:": "Giriş yapılıyor:",
"Disconnect": "Bağlantıyı Kes",
"Connect": "Bağlan",
"Password:": "Parola:",
"Cancel": "Vazgeç",
"Canvas not supported.": "Tuval desteklenmiyor."
}

View File

@ -0,0 +1,93 @@
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "不建议在没有 HTTPS 的情况下运行,可能会出现崩溃或出现其他问题。",
"Connecting...": "连接中...",
"Disconnecting...": "正在断开连接...",
"Reconnecting...": "重新连接中...",
"Internal error": "内部错误",
"Must set host": "必须设置主机",
"Failed to connect to server: ": "无法连接到服务器:",
"Connected (encrypted) to ": "已连接(已加密)到",
"Connected (unencrypted) to ": "已连接(未加密)到",
"Something went wrong, connection is closed": "出了点问题,连接已关闭",
"Failed to connect to server": "无法连接到服务器",
"Disconnected": "已断开连接",
"New connection has been rejected with reason: ": "新连接被拒绝,原因如下:",
"New connection has been rejected": "新连接已被拒绝",
"Credentials are required": "需要凭证",
"noVNC encountered an error:": "noVNC 遇到一个错误:",
"Hide/Show the control bar": "显示/隐藏控制栏",
"Drag": "拖动",
"Move/Drag viewport": "移动/拖动窗口",
"Keyboard": "键盘",
"Show keyboard": "显示键盘",
"Extra keys": "额外按键",
"Show extra keys": "显示额外按键",
"Ctrl": "Ctrl",
"Toggle Ctrl": "切换 Ctrl",
"Alt": "Alt",
"Toggle Alt": "切换 Alt",
"Toggle Windows": "切换窗口",
"Windows": "窗口",
"Send Tab": "发送 Tab 键",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "发送 Escape 键",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "发送 Ctrl+Alt+Del 键",
"Shutdown/Reboot": "关机/重启",
"Shutdown/Reboot...": "关机/重启...",
"Power": "电源",
"Shutdown": "关机",
"Reboot": "重启",
"Reset": "重置",
"Clipboard": "剪贴板",
"Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。",
"Full screen": "全屏",
"Settings": "设置",
"Shared mode": "分享模式",
"View only": "仅查看",
"Clip to window": "限制/裁切窗口大小",
"Scaling mode:": "缩放模式:",
"None": "无",
"Local scaling": "本地缩放",
"Remote resizing": "远程调整大小",
"Advanced": "高级",
"Quality:": "品质:",
"Compression level:": "压缩级别:",
"Repeater ID:": "中继站 ID",
"WebSocket": "WebSocket",
"Encrypt": "加密",
"Host:": "主机:",
"Port:": "端口:",
"Path:": "路径:",
"Automatic reconnect": "自动重新连接",
"Reconnect delay (ms):": "重新连接间隔 (ms)",
"Show dot when no cursor": "无光标时显示点",
"Logging:": "日志级别:",
"Version:": "版本:",
"Disconnect": "断开连接",
"Connect": "连接",
"Server identity": "服务器身份",
"The server has provided the following identifying information:": "服务器提供了以下识别信息:",
"Fingerprint:": "指纹:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "请核实信息是否正确,并按 “同意”,否则按 “拒绝”。",
"Approve": "同意",
"Reject": "拒绝",
"Credentials": "凭证",
"Username:": "用户名:",
"Password:": "密码:",
"Send credentials": "发送凭证",
"Cancel": "取消",
"Password is required": "请提供密码",
"Disconnect timeout": "超时断开",
"viewport drag": "窗口拖动",
"Active Mouse Button": "启动鼠标按键",
"No mousebutton": "禁用鼠标按键",
"Left mousebutton": "鼠标左键",
"Middle mousebutton": "鼠标中键",
"Right mousebutton": "鼠标右键",
"Clear": "清除",
"Local Downscaling": "降低本地尺寸",
"Local Cursor": "本地光标",
"Canvas not supported.": "不支持 Canvas。"
}

View File

@ -0,0 +1,69 @@
{
"Connecting...": "連線中...",
"Disconnecting...": "正在中斷連線...",
"Reconnecting...": "重新連線中...",
"Internal error": "內部錯誤",
"Must set host": "請提供主機資訊",
"Connected (encrypted) to ": "已加密連線到",
"Connected (unencrypted) to ": "未加密連線到",
"Something went wrong, connection is closed": "發生錯誤,連線已關閉",
"Failed to connect to server": "無法連線到伺服器",
"Disconnected": "連線已中斷",
"New connection has been rejected with reason: ": "連線被拒絕,原因:",
"New connection has been rejected": "連線被拒絕",
"Password is required": "請提供密碼",
"noVNC encountered an error:": "noVNC 遇到一個錯誤:",
"Hide/Show the control bar": "顯示/隱藏控制列",
"Move/Drag viewport": "拖放顯示範圍",
"viewport drag": "顯示範圍拖放",
"Active Mouse Button": "啟用滑鼠按鍵",
"No mousebutton": "無滑鼠按鍵",
"Left mousebutton": "滑鼠左鍵",
"Middle mousebutton": "滑鼠中鍵",
"Right mousebutton": "滑鼠右鍵",
"Keyboard": "鍵盤",
"Show keyboard": "顯示鍵盤",
"Extra keys": "額外按鍵",
"Show extra keys": "顯示額外按鍵",
"Ctrl": "Ctrl",
"Toggle Ctrl": "切換 Ctrl",
"Alt": "Alt",
"Toggle Alt": "切換 Alt",
"Send Tab": "送出 Tab 鍵",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "送出 Escape 鍵",
"Ctrl+Alt+Del": "Ctrl-Alt-Del",
"Send Ctrl-Alt-Del": "送出 Ctrl-Alt-Del 快捷鍵",
"Shutdown/Reboot": "關機/重新啟動",
"Shutdown/Reboot...": "關機/重新啟動...",
"Power": "電源",
"Shutdown": "關機",
"Reboot": "重新啟動",
"Reset": "重設",
"Clipboard": "剪貼簿",
"Clear": "清除",
"Fullscreen": "全螢幕",
"Settings": "設定",
"Shared mode": "分享模式",
"View only": "僅檢視",
"Clip to window": "限制/裁切視窗大小",
"Scaling mode:": "縮放模式:",
"None": "無",
"Local scaling": "本機縮放",
"Remote resizing": "遠端調整大小",
"Advanced": "進階",
"Repeater ID:": "中繼站 ID",
"WebSocket": "WebSocket",
"Encrypt": "加密",
"Host:": "主機:",
"Port:": "連接埠:",
"Path:": "路徑:",
"Automatic reconnect": "自動重新連線",
"Reconnect delay (ms):": "重新連線間隔 (ms)",
"Logging:": "日誌級別:",
"Disconnect": "中斷連線",
"Connect": "連線",
"Password:": "密碼:",
"Cancel": "取消"
}

View File

@ -0,0 +1,206 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
/*
* Localization utilities
*/
export class Localizer {
constructor() {
// Currently configured language
this.language = 'en';
// Current dictionary of translations
this._dictionary = undefined;
}
// Configure suitable language based on user preferences
async setup(supportedLanguages, baseURL) {
this.language = 'en'; // Default: US English
this._dictionary = undefined;
this._setupLanguage(supportedLanguages);
await this._setupDictionary(baseURL);
}
_setupLanguage(supportedLanguages) {
/*
* Navigator.languages only available in Chrome (32+) and FireFox (32+)
* Fall back to navigator.language for other browsers
*/
let userLanguages;
if (typeof window.navigator.languages == 'object') {
userLanguages = window.navigator.languages;
} else {
userLanguages = [navigator.language || navigator.userLanguage];
}
for (let i = 0;i < userLanguages.length;i++) {
const userLang = userLanguages[i]
.toLowerCase()
.replace("_", "-")
.split("-");
// First pass: perfect match
for (let j = 0; j < supportedLanguages.length; j++) {
const supLang = supportedLanguages[j]
.toLowerCase()
.replace("_", "-")
.split("-");
if (userLang[0] !== supLang[0]) {
continue;
}
if (userLang[1] !== supLang[1]) {
continue;
}
this.language = supportedLanguages[j];
return;
}
// Second pass: English fallback
if (userLang[0] === 'en') {
return;
}
// Third pass pass: other fallback
for (let j = 0;j < supportedLanguages.length;j++) {
const supLang = supportedLanguages[j]
.toLowerCase()
.replace("_", "-")
.split("-");
if (userLang[0] !== supLang[0]) {
continue;
}
if (supLang[1] !== undefined) {
continue;
}
this.language = supportedLanguages[j];
return;
}
}
}
async _setupDictionary(baseURL) {
if (baseURL) {
if (!baseURL.endsWith("/")) {
baseURL = baseURL + "/";
}
} else {
baseURL = "";
}
if (this.language === "en") {
return;
}
let response = await fetch(baseURL + this.language + ".json");
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
this._dictionary = await response.json();
}
// Retrieve localised text
get(id) {
if (typeof this._dictionary !== 'undefined' &&
this._dictionary[id]) {
return this._dictionary[id];
} else {
return id;
}
}
// Traverses the DOM and translates relevant fields
// See https://html.spec.whatwg.org/multipage/dom.html#attr-translate
translateDOM() {
const self = this;
function process(elem, enabled) {
function isAnyOf(searchElement, items) {
return items.indexOf(searchElement) !== -1;
}
function translateString(str) {
// We assume surrounding whitespace, and whitespace around line
// breaks is just for source formatting
str = str.split("\n").map(s => s.trim()).join(" ").trim();
return self.get(str);
}
function translateAttribute(elem, attr) {
const str = translateString(elem.getAttribute(attr));
elem.setAttribute(attr, str);
}
function translateTextNode(node) {
const str = translateString(node.data);
node.data = str;
}
if (elem.hasAttribute("translate")) {
if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) {
enabled = true;
} else if (isAnyOf(elem.getAttribute("translate"), ["no"])) {
enabled = false;
}
}
if (enabled) {
if (elem.hasAttribute("abbr") &&
elem.tagName === "TH") {
translateAttribute(elem, "abbr");
}
if (elem.hasAttribute("alt") &&
isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) {
translateAttribute(elem, "alt");
}
if (elem.hasAttribute("download") &&
isAnyOf(elem.tagName, ["A", "AREA"])) {
translateAttribute(elem, "download");
}
if (elem.hasAttribute("label") &&
isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP",
"OPTION", "TRACK"])) {
translateAttribute(elem, "label");
}
// FIXME: Should update "lang"
if (elem.hasAttribute("placeholder") &&
isAnyOf(elem.tagName, ["INPUT", "TEXTAREA"])) {
translateAttribute(elem, "placeholder");
}
if (elem.hasAttribute("title")) {
translateAttribute(elem, "title");
}
if (elem.hasAttribute("value") &&
elem.tagName === "INPUT" &&
isAnyOf(elem.getAttribute("type"), ["reset", "button", "submit"])) {
translateAttribute(elem, "value");
}
}
for (let i = 0; i < elem.childNodes.length; i++) {
const node = elem.childNodes[i];
if (node.nodeType === node.ELEMENT_NODE) {
process(node, enabled);
} else if (node.nodeType === node.TEXT_NODE && enabled) {
translateTextNode(node);
}
}
}
process(document.body, true);
}
}
export const l10n = new Localizer();
export default l10n.get.bind(l10n);

View File

@ -0,0 +1,4 @@
bell
Copyright: Dr. Richard Boulanger et al
URL: http://www.archive.org/details/Berklee44v12
License: CC-BY Attribution 3.0 Unported

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,927 @@
/*
* noVNC base CSS
* Copyright (C) 2019 The noVNC authors
* noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
* This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
*/
/*
* Z index layers:
*
* 0: Main screen
* 10: Control bar
* 50: Transition blocker
* 60: Connection popups
* 100: Status bar
* ...
* 1000: Javascript crash
* ...
* 10000: Max (used for polyfills)
*/
/*
* State variables (set on :root):
*
* noVNC_loading: Page is still loading
* noVNC_connecting: Connecting to server
* noVNC_reconnecting: Re-establishing a connection
* noVNC_connected: Connected to server (most common state)
* noVNC_disconnecting: Disconnecting from server
*/
:root {
font-family: sans-serif;
line-height: 1.6;
}
body {
margin:0;
padding:0;
/*Background image with light grey curve.*/
background-color:#494949;
background-repeat:no-repeat;
background-position:right bottom;
height:100%;
touch-action: none;
}
html {
height:100%;
}
.noVNC_only_touch.noVNC_hidden {
display: none;
}
.noVNC_disabled {
color: var(--novnc-grey);
}
/* ----------------------------------------
* Spinner
* ----------------------------------------
*/
.noVNC_spinner {
position: relative;
}
.noVNC_spinner, .noVNC_spinner::before, .noVNC_spinner::after {
width: 10px;
height: 10px;
border-radius: 2px;
box-shadow: -60px 10px 0 rgba(255, 255, 255, 0);
animation: noVNC_spinner 1.0s linear infinite;
}
.noVNC_spinner::before {
content: "";
position: absolute;
left: 0px;
top: 0px;
animation-delay: -0.1s;
}
.noVNC_spinner::after {
content: "";
position: absolute;
top: 0px;
left: 0px;
animation-delay: 0.1s;
}
@keyframes noVNC_spinner {
0% { box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); width: 20px; }
25% { box-shadow: 20px 10px 0 rgba(255, 255, 255, 1); width: 10px; }
50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; }
}
/* ----------------------------------------
* WebKit centering hacks
* ----------------------------------------
*/
.noVNC_center {
/*
* This is a workaround because webkit misrenders transforms and
* uses non-integer coordinates, resulting in blurry content.
* Ideally we'd use "top: 50%; transform: translateY(-50%);" on
* the objects instead.
*/
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.noVNC_center > * {
pointer-events: auto;
}
.noVNC_vcenter {
display: flex !important;
flex-direction: column;
justify-content: center;
position: fixed;
top: 0;
left: 0;
height: 100%;
margin: 0 !important;
padding: 0 !important;
pointer-events: none;
}
.noVNC_vcenter > * {
pointer-events: auto;
}
/* ----------------------------------------
* Layering
* ----------------------------------------
*/
.noVNC_connect_layer {
z-index: 60;
}
/* ----------------------------------------
* Fallback error
* ----------------------------------------
*/
#noVNC_fallback_error {
z-index: 1000;
visibility: hidden;
/* Put a dark background in front of everything but the error,
and don't let mouse events pass through */
background: rgba(0, 0, 0, 0.8);
pointer-events: all;
}
#noVNC_fallback_error.noVNC_open {
visibility: visible;
}
#noVNC_fallback_error > div {
max-width: calc(100vw - 30px - 30px);
max-height: calc(100vh - 30px - 30px);
overflow: auto;
padding: 15px;
transition: 0.5s ease-in-out;
transform: translateY(-50px);
opacity: 0;
text-align: center;
font-weight: bold;
color: #fff;
border-radius: 12px;
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
background: rgba(200,55,55,0.8);
}
#noVNC_fallback_error.noVNC_open > div {
transform: translateY(0);
opacity: 1;
}
#noVNC_fallback_errormsg {
font-weight: normal;
}
#noVNC_fallback_errormsg .noVNC_message {
display: inline-block;
text-align: left;
font-family: monospace;
white-space: pre-wrap;
}
#noVNC_fallback_error .noVNC_location {
font-style: italic;
font-size: 0.8em;
color: rgba(255, 255, 255, 0.8);
}
#noVNC_fallback_error .noVNC_stack {
padding: 10px;
margin: 10px;
font-size: 0.8em;
text-align: left;
font-family: monospace;
white-space: pre;
border: 1px solid rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.2);
overflow: auto;
}
/* ----------------------------------------
* Control bar
* ----------------------------------------
*/
#noVNC_control_bar_anchor {
/* The anchor is needed to get z-stacking to work */
position: fixed;
z-index: 10;
transition: 0.5s ease-in-out;
/* Edge misrenders animations wihthout this */
transform: translateX(0);
}
:root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle {
opacity: 0.8;
}
#noVNC_control_bar_anchor.noVNC_right {
left: auto;
right: 0;
}
#noVNC_control_bar {
position: relative;
left: -100%;
transition: 0.5s ease-in-out;
background-color: var(--novnc-blue);
border-radius: 0 12px 12px 0;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none; /* Disable iOS image long-press popup */
}
#noVNC_control_bar.noVNC_open {
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
left: 0;
}
#noVNC_control_bar::before {
/* This extra element is to get a proper shadow */
content: "";
position: absolute;
z-index: -1;
height: 100%;
width: 30px;
left: -30px;
transition: box-shadow 0.5s ease-in-out;
}
#noVNC_control_bar.noVNC_open::before {
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
}
.noVNC_right #noVNC_control_bar {
left: 100%;
border-radius: 12px 0 0 12px;
}
.noVNC_right #noVNC_control_bar.noVNC_open {
left: 0;
}
.noVNC_right #noVNC_control_bar::before {
visibility: hidden;
}
#noVNC_control_bar_handle {
position: absolute;
left: -15px;
top: 0;
transform: translateY(35px);
width: calc(100% + 30px);
height: 50px;
z-index: -1;
cursor: pointer;
border-radius: 6px;
background-color: var(--novnc-darkblue);
background-image: url("../images/handle_bg.svg");
background-repeat: no-repeat;
background-position: right;
box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5);
}
#noVNC_control_bar_handle:after {
content: "";
transition: transform 0.5s ease-in-out;
background: url("../images/handle.svg");
position: absolute;
top: 22px; /* (50px-6px)/2 */
right: 5px;
width: 5px;
height: 6px;
}
#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
transform: translateX(1px) rotate(180deg);
}
:root:not(.noVNC_connected) #noVNC_control_bar_handle {
display: none;
}
.noVNC_right #noVNC_control_bar_handle {
background-position: left;
}
.noVNC_right #noVNC_control_bar_handle:after {
left: 5px;
right: 0;
transform: translateX(1px) rotate(180deg);
}
.noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
transform: none;
}
/* Larger touch area for the handle, used when a touch screen is available */
#noVNC_control_bar_handle div {
position: absolute;
right: -35px;
top: 0;
width: 50px;
height: 100%;
display: none;
}
@media (any-pointer: coarse) {
#noVNC_control_bar_handle div {
display: initial;
}
}
.noVNC_right #noVNC_control_bar_handle div {
left: -35px;
right: auto;
}
#noVNC_control_bar > .noVNC_scroll {
max-height: 100vh; /* Chrome is buggy with 100% */
overflow-x: hidden;
overflow-y: auto;
padding: 0 10px;
}
#noVNC_control_bar > .noVNC_scroll > * {
display: block;
margin: 10px auto;
}
/* Control bar hint */
#noVNC_hint_anchor {
position: fixed;
right: -50px;
left: auto;
}
#noVNC_control_bar_anchor.noVNC_right + #noVNC_hint_anchor {
left: -50px;
right: auto;
}
#noVNC_control_bar_hint {
position: relative;
transform: scale(0);
width: 100px;
height: 50%;
max-height: 600px;
visibility: hidden;
opacity: 0;
transition: 0.2s ease-in-out;
background: transparent;
box-shadow: 0 0 10px black, inset 0 0 10px 10px var(--novnc-darkblue);
border-radius: 12px;
transition-delay: 0s;
}
#noVNC_control_bar_hint.noVNC_active {
visibility: visible;
opacity: 1;
transition-delay: 0.2s;
transform: scale(1);
}
#noVNC_control_bar_hint.noVNC_notransition {
transition: none !important;
}
/* Control bar buttons */
#noVNC_control_bar .noVNC_button {
min-width: unset;
padding: 4px 4px;
vertical-align: middle;
border:1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background-color: transparent;
}
#noVNC_control_bar .noVNC_button.noVNC_selected {
border-color: rgba(0, 0, 0, 0.8);
background-color: rgba(0, 0, 0, 0.5);
}
#noVNC_control_bar .noVNC_button.noVNC_hidden {
display: none !important;
}
/* Panels */
.noVNC_panel {
transform: translateX(25px);
transition: 0.5s ease-in-out;
box-sizing: border-box; /* so max-width don't have to care about padding */
max-width: calc(100vw - 75px - 25px); /* minus left and right margins */
max-height: 100vh; /* Chrome is buggy with 100% */
overflow-x: hidden;
overflow-y: auto;
visibility: hidden;
opacity: 0;
padding: 15px;
background: #fff;
border-radius: 12px;
color: #000;
border: 2px solid #E0E0E0;
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
}
.noVNC_panel.noVNC_open {
visibility: visible;
opacity: 1;
transform: translateX(75px);
}
.noVNC_right .noVNC_vcenter {
left: auto;
right: 0;
}
.noVNC_right .noVNC_panel {
transform: translateX(-25px);
}
.noVNC_right .noVNC_panel.noVNC_open {
transform: translateX(-75px);
}
.noVNC_panel > * {
display: block;
margin: 10px auto;
}
.noVNC_panel > *:first-child {
margin-top: 0 !important;
}
.noVNC_panel > *:last-child {
margin-bottom: 0 !important;
}
.noVNC_panel hr {
border: none;
border-top: 1px solid var(--novnc-lightgrey);
width: 100%; /* <hr> inside a flexbox will otherwise be 0px wide */
}
.noVNC_panel label {
display: block;
white-space: nowrap;
margin: 5px;
}
@media (max-width: 540px) {
/* Allow wrapping on small screens */
.noVNC_panel label {
white-space: unset;
}
}
.noVNC_panel li {
margin: 5px;
}
.noVNC_panel button,
.noVNC_panel select,
.noVNC_panel textarea,
.noVNC_panel input:not([type=checkbox]):not([type=radio]) {
margin-left: 6px;
/* Prevent inputs in panels from being too wide */
max-width: calc(100% - 6px - var(--input-xpadding) * 2);
}
.noVNC_panel .noVNC_heading {
background-color: var(--novnc-blue);
border-radius: 6px;
padding: 5px 8px;
/* Compensate for padding in image */
padding-right: 11px;
display: flex;
align-items: center;
gap: 6px;
color: white;
font-size: 20px;
font-weight: bold;
white-space: nowrap;
}
.noVNC_panel .noVNC_heading img {
vertical-align: bottom;
}
.noVNC_panel form {
display: flex;
flex-direction: column;
gap: 12px
}
.noVNC_panel .button_row {
margin-top: 10px;
display: flex;
gap: 10px;
justify-content: space-between;
}
.noVNC_panel .button_row *:only-child {
margin-left: auto; /* Align single buttons to the right */
}
/* Expanders */
.noVNC_expander {
cursor: pointer;
}
.noVNC_expander::before {
content: url("../images/expander.svg");
display: inline-block;
margin-right: 5px;
transition: 0.2s ease-in-out;
}
.noVNC_expander.noVNC_open::before {
transform: rotateZ(90deg);
}
.noVNC_expander ~ * {
margin: 5px;
margin-left: 10px;
padding: 5px;
background: rgba(0, 0, 0, 0.04);
border-radius: 6px;
}
.noVNC_expander:not(.noVNC_open) ~ * {
display: none;
}
/* Control bar content */
#noVNC_control_bar .noVNC_logo {
font-size: 13px;
}
.noVNC_logo + hr {
/* Remove all but top border */
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
:root:not(.noVNC_connected) #noVNC_view_drag_button {
display: none;
}
/* noVNC Touch Device only buttons */
:root:not(.noVNC_connected) #noVNC_mobile_buttons {
display: none;
}
@media not all and (any-pointer: coarse) {
/* FIXME: The button for the virtual keyboard is the only button in this
group of "mobile buttons". It is bad to assume that no touch
devices have physical keyboards available. Hopefully we can get
a media query for this:
https://github.com/w3c/csswg-drafts/issues/3871 */
:root.noVNC_connected #noVNC_mobile_buttons {
display: none;
}
}
/* Extra manual keys */
:root:not(.noVNC_connected) #noVNC_toggle_extra_keys_button {
display: none;
}
#noVNC_modifiers {
background-color: var(--novnc-darkgrey);
border: none;
padding: 10px;
}
/* Shutdown/Reboot */
:root:not(.noVNC_connected) #noVNC_power_button {
display: none;
}
#noVNC_power {
}
#noVNC_power_buttons {
display: none;
}
#noVNC_power input[type=button] {
width: 100%;
}
/* Clipboard */
:root:not(.noVNC_connected) #noVNC_clipboard_button {
display: none;
}
#noVNC_clipboard_text {
width: 360px;
min-width: 150px;
height: 160px;
min-height: 70px;
box-sizing: border-box;
max-width: 100%;
/* minus approximate height of title, height of subtitle, and margin */
max-height: calc(100vh - 10em - 25px);
}
/* Settings */
#noVNC_settings {
}
#noVNC_settings ul {
list-style: none;
padding: 0px;
}
#noVNC_setting_port {
width: 80px;
}
#noVNC_setting_path {
width: 100px;
}
/* Version */
.noVNC_version_wrapper {
font-size: small;
}
.noVNC_version {
margin-left: 1rem;
}
/* Connection controls */
:root:not(.noVNC_connected) #noVNC_disconnect_button {
display: none;
}
/* ----------------------------------------
* Status dialog
* ----------------------------------------
*/
#noVNC_status {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
transform: translateY(-100%);
cursor: pointer;
transition: 0.5s ease-in-out;
visibility: hidden;
opacity: 0;
padding: 5px;
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
line-height: 1.6;
word-wrap: break-word;
color: #fff;
border-bottom: 1px solid rgba(0, 0, 0, 0.9);
}
#noVNC_status.noVNC_open {
transform: translateY(0);
visibility: visible;
opacity: 1;
}
#noVNC_status::before {
content: "";
display: inline-block;
width: 25px;
height: 25px;
margin-right: 5px;
}
#noVNC_status.noVNC_status_normal {
background: rgba(128,128,128,0.9);
}
#noVNC_status.noVNC_status_normal::before {
content: url("../images/info.svg") " ";
}
#noVNC_status.noVNC_status_error {
background: rgba(200,55,55,0.9);
}
#noVNC_status.noVNC_status_error::before {
content: url("../images/error.svg") " ";
}
#noVNC_status.noVNC_status_warn {
background: rgba(180,180,30,0.9);
}
#noVNC_status.noVNC_status_warn::before {
content: url("../images/warning.svg") " ";
}
/* ----------------------------------------
* Connect dialog
* ----------------------------------------
*/
#noVNC_connect_dlg {
transition: 0.5s ease-in-out;
transform: scale(0, 0);
visibility: hidden;
opacity: 0;
}
#noVNC_connect_dlg.noVNC_open {
transform: scale(1, 1);
visibility: visible;
opacity: 1;
}
#noVNC_connect_dlg .noVNC_logo {
transition: 0.5s ease-in-out;
padding: 10px;
margin-bottom: 10px;
font-size: 80px;
text-align: center;
border-radius: 6px;
}
@media (max-width: 440px) {
#noVNC_connect_dlg {
max-width: calc(100vw - 100px);
}
#noVNC_connect_dlg .noVNC_logo {
font-size: calc(25vw - 30px);
}
}
#noVNC_connect_dlg div {
padding: 18px;
background-color: var(--novnc-darkgrey);
border-radius: 12px;
text-align: center;
font-size: 20px;
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
}
#noVNC_connect_button {
width: 100%;
padding: 6px 30px;
cursor: pointer;
border-color: transparent;
border-radius: 12px;
background-color: var(--novnc-blue);
color: white;
display: flex;
justify-content: center;
place-items: center;
gap: 4px;
}
#noVNC_connect_button img {
vertical-align: bottom;
height: 1.3em;
}
/* ----------------------------------------
* Server verification dialog
* ----------------------------------------
*/
#noVNC_verify_server_dlg {
position: relative;
transform: translateY(-50px);
}
#noVNC_verify_server_dlg.noVNC_open {
transform: translateY(0);
}
#noVNC_fingerprint_block {
margin: 10px;
}
/* ----------------------------------------
* Password dialog
* ----------------------------------------
*/
#noVNC_credentials_dlg {
position: relative;
transform: translateY(-50px);
}
#noVNC_credentials_dlg.noVNC_open {
transform: translateY(0);
}
#noVNC_username_block.noVNC_hidden,
#noVNC_password_block.noVNC_hidden {
display: none;
}
/* ----------------------------------------
* Main area
* ----------------------------------------
*/
/* Transition screen */
#noVNC_transition {
transition: 0.5s ease-in-out;
display: flex;
opacity: 0;
visibility: hidden;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
color: white;
background: rgba(0, 0, 0, 0.5);
z-index: 50;
/*display: flex;*/
align-items: center;
justify-content: center;
flex-direction: column;
}
:root.noVNC_loading #noVNC_transition,
:root.noVNC_connecting #noVNC_transition,
:root.noVNC_disconnecting #noVNC_transition,
:root.noVNC_reconnecting #noVNC_transition {
opacity: 1;
visibility: visible;
}
:root:not(.noVNC_reconnecting) #noVNC_cancel_reconnect_button {
display: none;
}
#noVNC_transition_text {
font-size: 1.5em;
}
/* Main container */
#noVNC_container {
width: 100%;
height: 100%;
background-color: #313131;
border-bottom-right-radius: 800px 600px;
/*border-top-left-radius: 800px 600px;*/
/* If selection isn't disabled, long-pressing stuff in the sidebar
can accidentally select the container or the canvas. This can
happen when attempting to move the handle. */
user-select: none;
-webkit-user-select: none;
}
#noVNC_keyboardinput {
width: 1px;
height: 1px;
background-color: #fff;
color: #fff;
border: 0;
position: absolute;
left: -40px;
z-index: -1;
ime-mode: disabled;
}
/*Default noVNC logo.*/
/* From: http://fonts.googleapis.com/css?family=Orbitron:700 */
@font-face {
font-family: 'Orbitron';
font-style: normal;
font-weight: 700;
src: local('?'), url('Orbitron700.woff') format('woff'),
url('Orbitron700.ttf') format('truetype');
}
.noVNC_logo {
color: var(--novnc-yellow);
font-family: 'Orbitron', 'OrbitronTTF', sans-serif;
line-height: 0.9;
text-shadow: 0.1em 0.1em 0 black;
}
.noVNC_logo span{
color: var(--novnc-green);
}
#noVNC_bell {
display: none;
}
/* ----------------------------------------
* Media sizing
* ----------------------------------------
*/
@media screen and (max-width: 640px){
#noVNC_logo {
font-size: 150px;
}
}
@media screen and (min-width: 321px) and (max-width: 480px) {
#noVNC_logo {
font-size: 110px;
}
}
@media screen and (max-width: 320px) {
#noVNC_logo {
font-size: 90px;
}
}

View File

@ -0,0 +1,30 @@
/*
* noVNC general CSS constant variables
* Copyright (C) 2025 The noVNC authors
* noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
* This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
*/
/* ---------- COLORS ----------- */
:root {
--novnc-grey: rgb(128, 128, 128);
--novnc-lightgrey: rgb(192, 192, 192);
--novnc-darkgrey: rgb(92, 92, 92);
/* Transparent to make button colors adapt to the background */
--novnc-buttongrey: rgba(192, 192, 192, 0.5);
--novnc-blue: rgb(110, 132, 163);
--novnc-lightblue: rgb(74, 144, 217);
--novnc-darkblue: rgb(83, 99, 122);
--novnc-green: rgb(0, 128, 0);
--novnc-yellow: rgb(255, 255, 0);
}
/* ------ MISC PROPERTIES ------ */
:root {
--input-xpadding: 1em;
}

View File

@ -0,0 +1,628 @@
/*
* noVNC general input element CSS
* Copyright (C) 2025 The noVNC authors
* noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
* This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
*/
/* ------- SHARED BETWEEN INPUT ELEMENTS -------- */
input,
textarea,
button,
select,
input::file-selector-button {
padding: 0.5em var(--input-xpadding);
border-radius: 6px;
appearance: none;
text-overflow: ellipsis;
/* Respect standard font settings */
font: inherit;
line-height: 1.6;
}
input:disabled,
textarea:disabled,
button:disabled,
select:disabled,
label[disabled] {
opacity: 0.4;
}
input:focus-visible,
textarea:focus-visible,
button:focus-visible,
select:focus-visible,
input:focus-visible::file-selector-button {
outline: 2px solid var(--novnc-lightblue);
outline-offset: 1px;
}
/* ------- TEXT INPUT -------- */
input:not([type]),
input[type=date],
input[type=datetime-local],
input[type=email],
input[type=month],
input[type=number],
input[type=password],
input[type=search],
input[type=tel],
input[type=text],
input[type=time],
input[type=url],
input[type=week],
textarea {
border: 1px solid var(--novnc-lightgrey);
/* Account for borders on text inputs, buttons dont have borders */
padding: calc(0.5em - 1px) var(--input-xpadding);
}
input:not([type]):focus-visible,
input[type=date]:focus-visible,
input[type=datetime-local]:focus-visible,
input[type=email]:focus-visible,
input[type=month]:focus-visible,
input[type=number]:focus-visible,
input[type=password]:focus-visible,
input[type=search]:focus-visible,
input[type=tel]:focus-visible,
input[type=text]:focus-visible,
input[type=time]:focus-visible,
input[type=url]:focus-visible,
input[type=week]:focus-visible,
textarea:focus-visible {
outline-offset: -1px;
}
textarea {
margin: unset; /* Remove Firefox's built in margin */
/* Prevent layout from shifting when scrollbars show */
scrollbar-gutter: stable;
/* Make textareas show at minimum one line. This does not work when
using box-sizing border-box, in which case, vertical padding and
border width needs to be taken into account. */
min-height: 1lh;
vertical-align: baseline; /* Firefox gives "text-bottom" by default */
}
/* ------- NUMBER PICKERS ------- */
/* We can't style the number spinner buttons:
https://github.com/w3c/csswg-drafts/issues/8777 */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
/* Get rid of increase/decrease buttons in WebKit */
appearance: none;
}
input[type=number] {
/* Get rid of increase/decrease buttons in Firefox */
appearance: textfield;
}
/* ------- BUTTON ACTIVATIONS -------- */
/* A color overlay that depends on the activation level. The level can then be
set for different states on an element, for example hover and click on a
<button>. */
input, button, select, option,
input::file-selector-button,
.button-activations {
--button-activation-level: 0;
/* Note that CSS variables aren't functions, beware when inheriting */
--button-activation-alpha: calc(0.08 * var(--button-activation-level));
/* FIXME: We want the image() function instead of the linear-gradient()
function below. But it's not supported in the browsers yet. */
--button-activation-overlay:
linear-gradient(rgba(0, 0, 0, var(--button-activation-alpha))
100%, transparent);
--button-activation-overlay-light:
linear-gradient(rgba(255, 255, 255, calc(0.23 * var(--button-activation-level)))
100%, transparent);
}
.button-activations {
background-image: var(--button-activation-overlay);
/* Disable Chrome's touch tap highlight to avoid conflicts with overlay */
-webkit-tap-highlight-color: transparent;
}
/* When we want the light overlay on activations instead.
This is best used on elements with darker backgrounds. */
.button-activations.light-overlay {
background-image: var(--button-activation-overlay-light);
/* Can't use the normal blend mode since that gives washed out colors. */
/* FIXME: For elements with these activation overlays we'd like only
the luminosity to change. The proprty "background-blend-mode" set
to "luminosity" sounds good, but it doesn't work as intended,
see: https://bugzilla.mozilla.org/show_bug.cgi?id=1806417 */
background-blend-mode: overlay;
}
input:hover, button:hover, select:hover, option:hover,
input::file-selector-button:hover,
.button-activations:hover {
--button-activation-level: 1;
}
/* Unfortunately we have to disable the :hover effect on touch devices,
otherwise the style lingers after tapping the button. */
@media (any-pointer: coarse) {
input:hover, button:hover, select:hover, option:hover,
input::file-selector-button:hover,
.button-activations:hover {
--button-activation-level: 0;
}
}
input:active, button:active, select:active, option:active,
input::file-selector-button:active,
.button-activations:active {
--button-activation-level: 2;
}
input:disabled, button:disabled, select:disabled, select:disabled option,
input:disabled::file-selector-button,
.button-activations:disabled {
--button-activation-level: 0;
}
/* ------- BUTTONS -------- */
input[type=button],
input[type=color],
input[type=image],
input[type=reset],
input[type=submit],
input::file-selector-button,
button,
select {
min-width: 8em;
border: none;
color: black;
font-weight: bold;
background-color: var(--novnc-buttongrey);
background-image: var(--button-activation-overlay);
cursor: pointer;
/* Disable Chrome's touch tap highlight */
-webkit-tap-highlight-color: transparent;
}
input[type=button]:disabled,
input[type=color]:disabled,
input[type=image]:disabled,
input[type=reset]:disabled,
input[type=submit]:disabled,
input:disabled::file-selector-button,
button:disabled,
select:disabled {
/* See Firefox bug:
https://bugzilla.mozilla.org/show_bug.cgi?id=1798304 */
cursor: default;
}
input[type=button],
input[type=color],
input[type=reset],
input[type=submit] {
/* Workaround for text-overflow bugs in Firefox and Chromium:
https://bugzilla.mozilla.org/show_bug.cgi?id=1800077
https://bugs.chromium.org/p/chromium/issues/detail?id=1383144 */
overflow: clip;
}
/* ------- COLOR PICKERS ------- */
input[type=color] {
min-width: unset;
box-sizing: content-box;
width: 1.4em;
height: 1.4em;
}
input[type=color]::-webkit-color-swatch-wrapper {
padding: 0;
}
/* -webkit-color-swatch & -moz-color-swatch cant be in a selector list:
https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */
input[type=color]::-webkit-color-swatch {
border: none;
border-radius: 6px;
}
input[type=color]::-moz-color-swatch {
border: none;
border-radius: 6px;
}
/* -- SHARED BETWEEN CHECKBOXES, RADIOBUTTONS AND THE TOGGLE CLASS -- */
input[type=radio],
input[type=checkbox] {
display: inline-flex;
justify-content: center;
align-items: center;
background-color: var(--novnc-buttongrey);
background-image: var(--button-activation-overlay);
/* Disable Chrome's touch tap highlight to avoid conflicts with overlay */
-webkit-tap-highlight-color: transparent;
width: 16px;
--checkradio-height: 16px;
height: var(--checkradio-height);
padding: 0;
margin: 0 6px 0 0;
/* Don't have transitions for outline in order to be consistent
with other elements */
transition: all 0.2s, outline-color 0s, outline-offset 0s;
/* A transparent outline in order to work around a graphical clipping issue
in WebKit. See bug: https://bugs.webkit.org/show_bug.cgi?id=256003 */
outline: 1px solid transparent;
position: relative; /* Since ::before & ::after are absolute positioned */
/* We want to align with the middle of capital letters, this requires
a workaround. The default behavior is to align the bottom of the element
on top of the text baseline, this is too far up.
We want to push the element down half the difference in height between
it and a capital X. In our font, the height of a capital "X" is 0.698em.
*/
vertical-align: calc(0px - (var(--checkradio-height) - 0.698em) / 2);
/* FIXME: Could write 1cap instead of 0.698em, but it's only supported in
Firefox as of 2023 */
/* FIXME: We probably want to use round() here, see bug 8148 */
}
input[type=radio]:focus-visible,
input[type=checkbox]:focus-visible {
outline-color: var(--novnc-lightblue);
}
input[type=checkbox]::before,
input[type=checkbox]:not(.toggle)::after,
input[type=radio]::before,
input[type=radio]::after {
content: "";
display: block; /* width & height doesn't work on inline elements */
transition: inherit;
/* Let's prevent the pseudo-elements from taking up layout space so that
the ::before and ::after pseudo-elements can be in the same place. This
is also required for vertical-align: baseline to work like we want it to
on radio/checkboxes. If the pseudo-elements take up layout space, the
baseline of text inside them will be used instead. */
position: absolute;
}
input[type=checkbox]:not(.toggle)::after,
input[type=radio]::after {
width: 10px;
height: 2px;
background-color: transparent;
border-radius: 2px;
}
/* ------- CHECKBOXES ------- */
input[type=checkbox]:not(.toggle) {
border-radius: 4px;
}
input[type=checkbox]:not(.toggle):checked,
input[type=checkbox]:not(.toggle):indeterminate {
background-color: var(--novnc-blue);
background-image: var(--button-activation-overlay-light);
background-blend-mode: overlay;
}
input[type=checkbox]:not(.toggle)::before {
width: 25%;
height: 55%;
border-style: solid;
border-color: transparent;
border-width: 0 2px 2px 0;
border-radius: 1px;
transform: translateY(-1px) rotate(35deg);
}
input[type=checkbox]:not(.toggle):checked::before {
border-color: white;
}
input[type=checkbox]:not(.toggle):indeterminate::after {
background-color: white;
}
/* ------- RADIO BUTTONS ------- */
input[type=radio] {
border-radius: 50%;
border: 1px solid transparent; /* To ensure a smooth transition */
}
input[type=radio]:checked {
border: 4px solid var(--novnc-blue);
background-color: white;
/* button-activation-overlay should be removed from the radio
element to not interfere with button-activation-overlay-light
that is set on the ::before element. */
background-image: none;
}
input[type=radio]::before {
width: inherit;
height: inherit;
border-radius: inherit;
/* We can achieve the highlight overlay effect on border colors by
setting button-activation-overlay-light on an element that stays
on top (z-axis) of the element with a border. */
background-image: var(--button-activation-overlay-light);
mix-blend-mode: overlay;
opacity: 0;
}
input[type=radio]:checked::before {
opacity: 1;
}
input[type=radio]:indeterminate::after {
background-color: black;
}
/* ------- TOGGLE SWITCHES ------- */
/* These are meant to be used instead of checkboxes in some cases. If all of
the following critera are true you should use a toggle switch:
* The choice is a simple ON/OFF or ENABLE/DISABLE
* The choice doesn't give the feeling of "I agree" or "I confirm"
* There are not multiple related & grouped options
*/
input[type=checkbox].toggle {
display: inline-block;
--checkradio-height: 18px; /* Height value used in calc, see above */
width: 31px;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
border-radius: 9px;
}
input[type=checkbox].toggle:disabled {
cursor: default;
}
input[type=checkbox].toggle:indeterminate {
background-color: var(--novnc-buttongrey);
background-image: var(--button-activation-overlay);
}
input[type=checkbox].toggle:checked {
background-color: var(--novnc-blue);
background-image: var(--button-activation-overlay-light);
background-blend-mode: overlay;
}
input[type=checkbox].toggle::before {
--circle-diameter: 10px;
--circle-offset: 4px;
width: var(--circle-diameter);
height: var(--circle-diameter);
top: var(--circle-offset);
left: var(--circle-offset);
background: white;
border-radius: 6px;
}
input[type=checkbox].toggle:checked::before {
left: calc(100% - var(--circle-offset) - var(--circle-diameter));
}
input[type=checkbox].toggle:indeterminate::before {
left: calc(50% - var(--circle-diameter) / 2);
}
/* ------- RANGE SLIDERS ------- */
input[type=range] {
border: unset;
border-radius: 8px;
height: 15px;
padding: 0;
background: transparent;
/* Needed to get properly rounded corners on -moz-range-progress
when the thumb is all the way to the right. Without overflow
hidden, the pointy edges of the progress track shows to the
right of the thumb. */
overflow: hidden;
}
@supports selector(::-webkit-slider-thumb) {
input[type=range] {
/* Needs a fixed width to match clip-path */
width: 125px;
/* overflow: hidden is not ideal for hiding the left part of the box
shadow of -webkit-slider-thumb since it doesn't match the smaller
border-radius of the progress track. The below clip-path has two
circular sides to make the ends of the track have correctly rounded
corners. The clip path shape looks something like this:
+-------------------------------+
/---| |---\
| |
\---| |---/
+-------------------------------+
The larger middle part of the clip path is made to have room for the
thumb. By using margins on the track, we prevent the thumb from
touching the ends of the track.
*/
clip-path: path(' \
M 4.5 3 \
L 4.5 0 \
L 120.5 0 \
L 120.5 3 \
A 1 1 0 0 1 120.5 12 \
L 120.5 15 \
L 4.5 15 \
L 4.5 12 \
A 1 1 0 0 1 4.5 3 \
');
}
}
input[type=range]:hover {
cursor: grab;
}
input[type=range]:active {
cursor: grabbing;
}
input[type=range]:disabled {
cursor: default;
}
input[type=range]:focus-visible {
clip-path: none; /* Otherwise it hides the outline */
}
/* -webkit-slider.. & -moz-range.. cant be in selector lists:
https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */
input[type=range]::-webkit-slider-runnable-track {
background-color: var(--novnc-buttongrey);
height: 7px;
border-radius: 4px;
margin: 0 3px;
}
input[type=range]::-moz-range-track {
background-color: var(--novnc-buttongrey);
height: 7px;
border-radius: 4px;
}
input[type=range]::-moz-range-progress {
background-color: var(--novnc-blue);
height: 9px;
/* Needs rounded corners only on the left side. Otherwise the rounding of
the progress track starts before the thumb, when the thumb is close to
the left edge. */
border-radius: 5px 0 0 5px;
}
input[type=range]::-webkit-slider-thumb {
appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
background-color: white;
background-image: var(--button-activation-overlay);
/* Disable Chrome's touch tap highlight to avoid conflicts with overlay */
-webkit-tap-highlight-color: transparent;
border: 3px solid var(--novnc-blue);
margin-top: -4px; /* (track height / 2) - (thumb height /2) */
/* Since there is no way to style the left part of the range track in
webkit, we add a large shadow (1000px wide) to the left of the thumb and
then crop it with a clip-path shaped like this:
___
+-------------------/ \
| progress |Thumb|
+-------------------\ ___ /
The large left part of the shadow is clipped by another clip-path on on
the main range input element. */
/* FIXME: We can remove the box shadow workaround when this is standardized:
https://github.com/w3c/csswg-drafts/issues/4410 */
box-shadow: calc(-100vw - 8px) 0 0 100vw var(--novnc-blue);
clip-path: path(' \
M -1000 3 \
L 3 3 \
L 15 7.5 \
A 1 1 0 0 1 0 7.5 \
A 1 1 0 0 1 15 7.5 \
L 3 12 \
L -1000 12 Z \
');
}
input[type=range]::-moz-range-thumb {
appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
box-sizing: border-box;
background-color: white;
background-image: var(--button-activation-overlay);
border: 3px solid var(--novnc-blue);
margin-top: -7px;
}
/* ------- FILE CHOOSERS ------- */
input[type=file] {
background-image: none;
border: none;
}
input::file-selector-button {
margin-right: 6px;
}
input[type=file]:focus-visible {
outline: none; /* We outline the button instead of the entire element */
}
/* ------- SELECT BUTTONS ------- */
select {
--select-arrow: url('data:image/svg+xml;utf8, \
<svg width="11" height="6" version="1.1" viewBox="0 0 11 6" \
xmlns="http://www.w3.org/2000/svg"> \
<path d="m10.5.5-5 5-5-5" fill="none" \
stroke="black" stroke-width="1.5" \
stroke-linecap="round" stroke-linejoin="round"/> \
</svg>');
/* FIXME: A bug in Firefox, requires a workaround for the background:
https://bugzilla.mozilla.org/show_bug.cgi?id=1810958 */
/* The dropdown list will show the select element's background above and
below the options in Firefox. We want the entire dropdown to be white. */
background-color: white;
/* However, we don't want the select element to actually show a white
background, so let's place a gradient above it with the color we want. */
--grey-background: linear-gradient(var(--novnc-buttongrey) 100%,
transparent);
background-image:
var(--select-arrow),
var(--button-activation-overlay),
var(--grey-background);
background-position: calc(100% - var(--input-xpadding)), left top, left top;
background-repeat: no-repeat;
padding-right: calc(2*var(--input-xpadding) + 11px);
overflow: auto;
}
/* FIXME: :active isn't set when the <select> is opened in Firefox:
https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */
select:active {
/* Rotated arrow */
background-image: url('data:image/svg+xml;utf8, \
<svg width="11" height="6" version="1.1" viewBox="0 0 11 6" \
xmlns="http://www.w3.org/2000/svg" transform="rotate(180)"> \
<path d="m10.5.5-5 5-5-5" fill="none" \
stroke="black" stroke-width="1.5" \
stroke-linecap="round" stroke-linejoin="round"/> \
</svg>'),
var(--button-activation-overlay),
var(--grey-background);
}
select:disabled {
background-image:
var(--select-arrow),
var(--grey-background);
}
/* Note that styling for <option> doesn't work in all browsers
since its often drawn directly by the OS. We are generally very
limited in what we can change here. */
option {
/* Prevent Chrome from inheriting background-color from the <select> */
background-color: white;
color: black;
font-weight: normal;
background-image: var(--button-activation-overlay);
}
option:checked {
background-color: var(--novnc-lightgrey);
}
/* Change the look when the <select> isn't used as a dropdown. When "size"
or "multiple" are set, these elements behaves more like lists. */
select[size]:not([size="1"]), select[multiple] {
background-color: white;
background-image: unset; /* Don't show the arrow and other gradients */
border: 1px solid var(--novnc-lightgrey);
padding: 0;
font-weight: normal; /* Without this, options get bold font in WebKit. */
/* As an exception to the "list"-look, multi-selects in Chrome on Android,
and Safari on iOS, are unfortunately designed to be shown as a single
line. We can mitigate this inconsistency by at least fixing the height
here. By setting a min-height that matches other input elements, it
doesn't look too much out of place:
(1px border * 2) + (6.5px padding * 2) + 24px line-height = 39px */
min-height: 39px;
}
select[size]:not([size="1"]):focus-visible,
select[multiple]:focus-visible {
/* Text input style focus-visible highlight */
outline-offset: -1px;
}
select[size]:not([size="1"]) option, select[multiple] option {
overflow: hidden;
text-overflow: ellipsis;
padding: 4px var(--input-xpadding);
}

Some files were not shown because too many files have changed in this diff Show More