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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions browser/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { Keyboard } from '@/components/keyboard';
import { Menu } from '@/components/menu';
import { Mouse } from '@/components/mouse';
import { VirtualKeyboard } from '@/components/virtual-keyboard';
import { resolutionAtom, serialStateAtom, videoStateAtom } from '@/jotai/device.ts';
import {
resolutionAtom,
serialStateAtom,
videoScaleAtom,
videoStateAtom
} from '@/jotai/device.ts';
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
import { mouseStyleAtom } from '@/jotai/mouse.ts';
import { camera } from '@/libs/camera';
Expand All @@ -23,6 +28,7 @@ const App = () => {
const isBigScreen = useMediaQuery({ minWidth: 850 });

const mouseStyle = useAtomValue(mouseStyleAtom);
const videoScale = useAtomValue(videoScaleAtom);
const videoState = useAtomValue(videoStateAtom);
const serialState = useAtomValue(serialStateAtom);
const isKeyboardEnable = useAtomValue(isKeyboardEnableAtom);
Expand Down Expand Up @@ -107,7 +113,13 @@ const App = () => {
<video
id="video"
className={clsx('block min-h-[480px] min-w-[640px] select-none', mouseStyle)}
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'scale-down' }}
style={{
transform: `scale(${videoScale})`,
transformOrigin: 'center',
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'scale-down'
}}
autoPlay
playsInline
/>
Expand Down
2 changes: 2 additions & 0 deletions browser/src/components/menu/video/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { MonitorIcon } from 'lucide-react';

import { Device } from './device.tsx';
import { Resolution } from './resolution.tsx';
import { Scale } from './scale.tsx';

export const Video = () => {
const content = (
<div className="flex flex-col space-y-1">
<Resolution />
<Scale />
<Device />
</div>
);
Expand Down
58 changes: 58 additions & 0 deletions browser/src/components/menu/video/scale.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ReactElement, useEffect } from 'react'
import { Popover, Slider } from 'antd'
import { useAtom } from 'jotai'
import { ScalingIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'

import { videoScaleAtom } from '@/jotai/device.ts';
import * as storage from '@/libs/storage';

export const Scale = (): ReactElement => {
const { t } = useTranslation()

const [videoScale, setVideoScale] = useAtom(videoScaleAtom)

useEffect(() => {
const scale = storage.getVideoScale()
if (scale) {
setVideoScale(scale)
}
}, [])

async function updateScale(scale: number): Promise<void> {
setVideoScale(scale)
storage.setVideoScale(scale)
}

const content = (
<div className="h-[150px] w-[60px] py-3">
<Slider
vertical
marks={{
0.5: <span>x0.5</span>,
1: <span>x1.0</span>,
1.5: <span>x1.5</span>,
2: <span>x2.0</span>
}}
range={false}
included={false}
min={0.5}
max={2}
step={0.1}
defaultValue={videoScale}
onChange={updateScale}
/>
</div>
)

return (
<Popover content={content} placement="rightTop" arrow={false} align={{ offset: [13, 0] }}>
<div className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/50">
<div className="flex h-[14px] w-[20px] items-end">
<ScalingIcon size={16} />
</div>
<span>{t('video.scale')}</span>
</div>
</Popover>
)
}
3 changes: 2 additions & 1 deletion browser/src/i18n/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const languages = [
{ key: 'zh', name: '中文' },
{ key: 'de', name: 'Deutsch' },
{ key: 'nl', name: 'Nederlands' },
{ key: 'be', name: 'België' }
{ key: 'be', name: 'België' },
{ key: 'ko', name: '한국어' }
];

languages.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }));
Expand Down
5 changes: 3 additions & 2 deletions browser/src/i18n/locales/be.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const be = {
},
video: {
resolution: 'Resolutie',
scale: 'Schaal',
customResolution: 'Aangepast',
device: 'Toestel',
custom: {
Expand Down Expand Up @@ -62,5 +63,5 @@ const be = {
}
}
};
export default be;

export default be;
5 changes: 3 additions & 2 deletions browser/src/i18n/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const de = {
},
video: {
resolution: 'Auflösung',
scale: 'Skalierung',
customResolution: 'Benutzerdefiniert',
device: 'Gerät',
custom: {
Expand Down Expand Up @@ -62,5 +63,5 @@ const de = {
}
}
};
export default de;

export default de;
1 change: 1 addition & 0 deletions browser/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const en = {
},
video: {
resolution: 'Resolution',
scale: 'Scale',
customResolution: 'Custom',
device: 'Device',
custom: {
Expand Down
70 changes: 70 additions & 0 deletions browser/src/i18n/locales/ko.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const ko = {
translation: {
serial: {
notSupported:
'시리얼이 지원되지 않습니다. 마우스와 키보드를 사용하려면 Chrome 브라우저를 사용하세요.',
failed: '시리얼 연결에 실패했습니다. 다시 시도해 주세요.'
},
camera: {
tip: '권한을 기다리는 중...',
denied: '권한이 거부되었습니다.',
authorize:
'Target PC 연결에 카메라 권한이 필요합니다. 브라우저 설정에서 카메라 권한을 허용해 주세요.',
failed: '카메라 연결에 실패했습니다. 다시 시도해 주세요.'
},
modal: {
title: 'USB 장치 선택',
selectVideo: '비디오 입력 장치를 선택해 주세요.',
selectSerial: '시리얼 장치를 선택해 주세요.'
},
menu: {
serial: '시리얼',
keyboard: '키보드',
mouse: '마우스'
},
video: {
resolution: '해상도',
scale: '배율',
customResolution: '사용자 정의',
device: '장치',
custom: {
title: '사용자 정의 해상도',
width: '가로',
height: '세로',
confirm: '확인',
cancel: '취소'
}
},
keyboard: {
paste: '붙여넣기',
virtualKeyboard: '가상 키보드',
ctrlAltDel: 'Ctrl + Alt + Delete'
},
mouse: {
cursor: {
title: '커서 모양',
pointer: '포인터',
grab: '손',
cell: '플러스',
hide: '숨기기'
},
mode: '마우스 모드',
absolute: '절대 모드',
relative: '상대 모드',
direction: '휠 방향',
scrollUp: '위로 스크롤',
scrollDown: '아래로 스크롤',
speed: '휠 속도',
fast: '빠르게',
slow: '느리게',
requestPointer: '상대 모드를 사용 중입니다. 마우스 포인터를 가져오려면 스크린을 클릭하세요.'
},
settings: {
language: '언어',
document: '문서',
download: '다운로드'
}
}
};

export default ko;
5 changes: 3 additions & 2 deletions browser/src/i18n/locales/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const nl = {
},
video: {
resolution: 'Resolutie',
scale: 'Schaal',
customResolution: 'Aangepast',
device: 'Apparaat',
custom: {
Expand Down Expand Up @@ -62,5 +63,5 @@ const nl = {
}
}
};
export default nl;

export default nl;
1 change: 1 addition & 0 deletions browser/src/i18n/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const ru = {
},
video: {
resolution: 'Разрешение',
scale: 'Масштаб',
customResolution: 'Пользовательское',
device: 'Видеоустройство',
custom: {
Expand Down
1 change: 1 addition & 0 deletions browser/src/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const zh = {
},
video: {
resolution: '分辨率',
scale: '缩放',
customResolution: '自定义',
device: '设备',
custom: {
Expand Down
2 changes: 2 additions & 0 deletions browser/src/jotai/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const resolutionAtom = atom<Resolution>({
height: 1080
});

export const videoScaleAtom = atom<number>(1.0)

export const videoDeviceIdAtom = atom('');
export const videoStateAtom = atom<VideoState>('disconnected');

Expand Down
13 changes: 13 additions & 0 deletions browser/src/libs/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const LANGUAGE_KEY = 'nanokvm-usb-language';
const VIDEO_DEVICE_ID_KEY = 'nanokvm-usb-video-device-id';
const VIDEO_RESOLUTION_KEY = 'nanokvm-usb-video-resolution';
const CUSTOM_RESOLUTION_KEY = 'nanokvm-usb-custom-resolution';
const VIDEO_SCALE_KEY = 'nanokvm-usb-video-scale'
const IS_MENU_OPEN_KEY = 'nanokvm-is-menu-open';
const MOUSE_STYLE_KEY = 'nanokvm-usb-mouse-style';
const MOUSE_MODE_KEY = 'nanokvm-usb-mouse-mode';
Expand Down Expand Up @@ -56,6 +57,18 @@ export function removeCustomResolutions() {
localStorage.removeItem(CUSTOM_RESOLUTION_KEY);
}

export function getVideoScale(): number | null {
const scale = localStorage.getItem(VIDEO_SCALE_KEY)
if (scale && Number(scale)) {
return Number(scale)
}
return null
}

export function setVideoScale(scale: number): void {
localStorage.setItem(VIDEO_SCALE_KEY, String(scale))
}

export function getIsMenuOpen(): boolean {
const state = localStorage.getItem(IS_MENU_OPEN_KEY);
if (!state) {
Expand Down
16 changes: 14 additions & 2 deletions desktop/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { Keyboard } from '@renderer/components/keyboard'
import { Menu } from '@renderer/components/menu'
import { Mouse } from '@renderer/components/mouse'
import { VirtualKeyboard } from '@renderer/components/virtual-keyboard'
import { resolutionAtom, serialPortStateAtom, videoStateAtom } from '@renderer/jotai/device'
import {
resolutionAtom,
serialPortStateAtom,
videoScaleAtom,
videoStateAtom
} from '@renderer/jotai/device'
import { isKeyboardEnableAtom } from '@renderer/jotai/keyboard'
import { mouseStyleAtom } from '@renderer/jotai/mouse'
import { camera } from '@renderer/libs/camera'
Expand All @@ -24,6 +29,7 @@ const App = (): ReactElement => {
const { t } = useTranslation()
const isBigScreen = useMediaQuery({ minWidth: 850 })

const videoScale = useAtomValue(videoScaleAtom)
const videoState = useAtomValue(videoStateAtom)
const serialPortState = useAtomValue(serialPortStateAtom)
const mouseStyle = useAtomValue(mouseStyleAtom)
Expand Down Expand Up @@ -115,7 +121,13 @@ const App = (): ReactElement => {
<video
id="video"
className={clsx('block min-h-[480px] min-w-[640px] select-none', mouseStyle)}
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'scale-down' }}
style={{
transform: `scale(${videoScale})`,
transformOrigin: 'center',
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'scale-down'
}}
autoPlay
playsInline
/>
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/renderer/src/components/menu/video/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { MonitorIcon } from 'lucide-react'

import { Device } from './device'
import { Resolution } from './resolution'
import { Scale } from './scale'

export const Video = (): ReactElement => {
const content = (
<div className="flex flex-col space-y-1">
<Resolution />
<Scale />
<Device />
</div>
)
Expand Down
Loading