diff --git a/server/proto/vm.go b/server/proto/vm.go index 3467e13..3408353 100644 --- a/server/proto/vm.go +++ b/server/proto/vm.go @@ -163,6 +163,17 @@ type InputRegion struct { Height int `json:"height"` Resolutions []OriginalResolution `json:"resolutions,omitempty"` SelectedResolution string `json:"selectedResolution"` + Regions []ManualRegion `json:"regions,omitempty"` + SelectedRegion string `json:"selectedRegion"` +} + +type ManualRegion struct { + FrameWidth int `json:"frameWidth"` + FrameHeight int `json:"frameHeight"` + Left int `json:"left"` + Top int `json:"top"` + Width int `json:"width"` + Height int `json:"height"` } type OriginalResolution struct { @@ -180,6 +191,8 @@ type SetInputRegionReq struct { Height *int `json:"height,omitempty"` Resolutions *[]OriginalResolution `json:"resolutions,omitempty"` SelectedResolution *string `json:"selectedResolution,omitempty"` + Regions *[]ManualRegion `json:"regions,omitempty"` + SelectedRegion *string `json:"selectedRegion,omitempty"` } type GetInputRegionRsp struct { diff --git a/server/service/vm/input_region.go b/server/service/vm/input_region.go index aa87c3b..11d0470 100644 --- a/server/service/vm/input_region.go +++ b/server/service/vm/input_region.go @@ -99,7 +99,20 @@ func (s *Service) SetInputRegion(c *gin.Context) { if req.SelectedResolution != nil { region.SelectedResolution = *req.SelectedResolution } - if err := validateSelectedResolution(region.SelectedResolution, region.Resolutions); err != nil { + if req.Regions != nil { + if err := validateManualRegions(*req.Regions); err != nil { + rsp.ErrRsp(c, -1, "invalid manual regions") + return + } + region.Regions = *req.Regions + if len(region.Regions) == 0 { + clearLegacyManualRegion(region) + } + } + if req.SelectedRegion != nil { + region.SelectedRegion = *req.SelectedRegion + } + if err := validateSelections(*region); err != nil { rsp.ErrRsp(c, -1, "invalid selected resolution") return } @@ -127,6 +140,8 @@ func (s *Service) SetInputRegion(c *gin.Context) { if previous != nil { region.Resolutions = previous.Resolutions region.SelectedResolution = previous.SelectedResolution + region.Regions = previous.Regions + region.SelectedRegion = previous.SelectedRegion } if req.Resolutions != nil { if err := validateOriginalResolutions(*req.Resolutions); err != nil { @@ -138,7 +153,20 @@ func (s *Service) SetInputRegion(c *gin.Context) { if req.SelectedResolution != nil { region.SelectedResolution = *req.SelectedResolution } - if err := validateSelectedResolution(region.SelectedResolution, region.Resolutions); err != nil { + if req.Regions != nil { + if err := validateManualRegions(*req.Regions); err != nil { + rsp.ErrRsp(c, -1, "invalid manual regions") + return + } + region.Regions = *req.Regions + if len(region.Regions) == 0 { + clearLegacyManualRegion(®ion) + } + } + if req.SelectedRegion != nil { + region.SelectedRegion = *req.SelectedRegion + } + if err := validateSelections(region); err != nil { rsp.ErrRsp(c, -1, "invalid selected resolution") return } @@ -176,10 +204,68 @@ func validateSelectedResolution(selected string, resolutions []proto.OriginalRes return errors.New("selected resolution not found") } +func validateManualRegions(regions []proto.ManualRegion) error { + seen := make(map[string]struct{}, len(regions)) + for _, region := range regions { + if err := validateManualRegion(region); err != nil { + return err + } + key := manualRegionKey(region) + if _, ok := seen[key]; ok { + return errors.New("duplicate manual region") + } + seen[key] = struct{}{} + } + return nil +} + +func validateManualRegion(region proto.ManualRegion) error { + return validateInputRegion(proto.InputRegion{ + Mode: "manual", + FrameWidth: region.FrameWidth, + FrameHeight: region.FrameHeight, + Left: region.Left, + Top: region.Top, + Width: region.Width, + Height: region.Height, + }) +} + +func validateSelections(region proto.InputRegion) error { + if region.SelectedResolution != "" && region.SelectedRegion != "" { + return errors.New("only one input region source may be selected") + } + if err := validateSelectedResolution(region.SelectedResolution, region.Resolutions); err != nil { + return err + } + if region.SelectedRegion == "" { + return nil + } + for _, item := range region.Regions { + if region.SelectedRegion == manualRegionKey(item) { + return nil + } + } + return errors.New("selected manual region not found") +} + func resolutionKey(resolution proto.OriginalResolution) string { return fmt.Sprintf("%dx%d", resolution.Width, resolution.Height) } +func manualRegionKey(region proto.ManualRegion) string { + return fmt.Sprintf("%dx%d", region.Width, region.Height) +} + +func clearLegacyManualRegion(region *proto.InputRegion) { + region.FrameWidth = 0 + region.FrameHeight = 0 + region.Left = 0 + region.Top = 0 + region.Width = 0 + region.Height = 0 +} + func inputRegionFromRequest(req proto.SetInputRegionReq) (proto.InputRegion, error) { if req.FrameWidth == nil || req.FrameHeight == nil || req.Left == nil || req.Top == nil || req.Width == nil || req.Height == nil { @@ -236,9 +322,29 @@ func readInputRegion(path string) (*proto.InputRegion, error) { if region.Mode == "" { region.Mode = "manual" } + if len(region.Regions) == 0 && region.FrameWidth > 0 && region.FrameHeight > 0 { + manual := proto.ManualRegion{ + FrameWidth: region.FrameWidth, + FrameHeight: region.FrameHeight, + Left: region.Left, + Top: region.Top, + Width: region.Width, + Height: region.Height, + } + region.Regions = []proto.ManualRegion{manual} + if region.SelectedResolution == "" { + region.SelectedRegion = manualRegionKey(manual) + } + } if err := validateInputRegion(*region); err != nil { return nil, nil } + if err := validateManualRegions(region.Regions); err != nil { + return nil, nil + } + if err := validateSelections(*region); err != nil { + return nil, nil + } return region, nil } diff --git a/web/src/api/vm.ts b/web/src/api/vm.ts index 47af831..a11fbaa 100644 --- a/web/src/api/vm.ts +++ b/web/src/api/vm.ts @@ -48,11 +48,17 @@ export function setInputRegion(region: InputRegion) { return http.post('/api/vm/input-region', { mode: 'manual', ...region }); } -export function setInputRegionConfig(region: InputRegion, selectedResolution: string) { +export function setInputRegionConfig( + region: InputRegion, + regions: InputRegion[], + selectedRegion: string +) { return http.post('/api/vm/input-region', { mode: 'manual', ...region, - selectedResolution + regions, + selectedRegion, + selectedResolution: '' }); } @@ -66,17 +72,36 @@ export function setOriginalResolutions(mode: ControlRegionMode, resolutions: Ori export function setOriginalResolutionConfig( resolutions: OriginalResolution[], - selectedResolution: string + selectedResolution: string, + selectedRegion: string ) { return http.post('/api/vm/input-region', { mode: 'manual', resolutions, - selectedResolution + selectedResolution, + selectedRegion }); } export function setSelectedOriginalResolution(selectedResolution: string) { - return http.post('/api/vm/input-region', { mode: 'manual', selectedResolution }); + return http.post('/api/vm/input-region', { + mode: 'manual', + selectedResolution, + selectedRegion: '' + }); +} + +export function setManualRegions( + regions: InputRegion[], + selectedRegion: string, + selectedResolution = '' +) { + return http.post('/api/vm/input-region', { + mode: 'manual', + regions, + selectedRegion, + selectedResolution + }); } // get memory limit diff --git a/web/src/i18n/locales/ca.ts b/web/src/i18n/locales/ca.ts index 30f2e04..b1cf41c 100644 --- a/web/src/i18n/locales/ca.ts +++ b/web/src/i18n/locales/ca.ts @@ -73,9 +73,10 @@ const ca = { autoWarning: "El calibratge pot fallar quan l'aplicació de l'usuari tingui un fons completament negre.", manual: 'Manual', + selectedResolution: "Resolució de l'àrea seleccionada", + unused: 'No utilitzada', originalResolution: 'Resolució original', selectResolution: 'Selecciona la resolució original', - useSelectedArea: "Utilitza la resolució de l'àrea seleccionada", addResolution: 'Afegeix una resolució personalitzada', add: 'Afegeix', duplicateResolution: 'Aquesta resolució ja existeix.', diff --git a/web/src/i18n/locales/cz.ts b/web/src/i18n/locales/cz.ts index 7d35fdf..d571e79 100644 --- a/web/src/i18n/locales/cz.ts +++ b/web/src/i18n/locales/cz.ts @@ -73,9 +73,10 @@ const cz = { auto: 'Automaticky', autoWarning: 'Kalibrace může selhat, pokud má uživatelská aplikace zcela černé pozadí.', manual: 'Ručně', + selectedResolution: 'Rozlišení vybrané oblasti', + unused: 'Nepoužito', originalResolution: 'Původní rozlišení', selectResolution: 'Vyberte původní rozlišení', - useSelectedArea: 'Použít rozlišení vybrané oblasti', addResolution: 'Přidat vlastní rozlišení', add: 'Přidat', duplicateResolution: 'Toto rozlišení již existuje.', diff --git a/web/src/i18n/locales/da.ts b/web/src/i18n/locales/da.ts index 614c7d8..9eabeda 100644 --- a/web/src/i18n/locales/da.ts +++ b/web/src/i18n/locales/da.ts @@ -73,9 +73,10 @@ const da = { autoWarning: 'Kalibreringen kan mislykkes, hvis brugerprogrammet har en helt sort baggrund.', manual: 'Manuel', + selectedResolution: 'Valgt områdeopløsning', + unused: 'Ikke i brug', originalResolution: 'Oprindelig opløsning', selectResolution: 'Vælg oprindelig opløsning', - useSelectedArea: 'Brug det valgte områdes opløsning', addResolution: 'Tilføj brugerdefineret opløsning', add: 'Tilføj', duplicateResolution: 'Denne opløsning findes allerede.', diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index 181764c..2df4ce4 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -76,9 +76,10 @@ const de = { autoWarning: 'Die Kalibrierung kann fehlschlagen, wenn die Benutzeranwendung einen vollständig schwarzen Hintergrund hat.', manual: 'Manuell', + selectedResolution: 'Auflösung des ausgewählten Bereichs', + unused: 'Nicht verwendet', originalResolution: 'Originalauflösung', selectResolution: 'Originalauflösung auswählen', - useSelectedArea: 'Auflösung des ausgewählten Bereichs verwenden', addResolution: 'Benutzerdefinierte Auflösung hinzufügen', add: 'Hinzufügen', duplicateResolution: 'Diese Auflösung ist bereits vorhanden.', diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index b4bbe9b..1e2e676 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -74,9 +74,10 @@ const en = { auto: 'Auto', autoWarning: 'Calibration may fail when the user application has a pure black background.', manual: 'Manual', + selectedResolution: 'Selected Area Resolution', + unused: 'Not used', originalResolution: 'Original Resolution', selectResolution: 'Select original resolution', - useSelectedArea: 'Use selected area resolution', addResolution: 'Add custom resolution', add: 'Add', duplicateResolution: 'This resolution already exists.', diff --git a/web/src/i18n/locales/es.ts b/web/src/i18n/locales/es.ts index 928aca4..76d25c1 100644 --- a/web/src/i18n/locales/es.ts +++ b/web/src/i18n/locales/es.ts @@ -74,9 +74,10 @@ const es = { autoWarning: 'La calibración puede fallar si la aplicación del usuario tiene un fondo completamente negro.', manual: 'Manual', + selectedResolution: 'Resolución del área seleccionada', + unused: 'Sin usar', originalResolution: 'Resolución original', selectResolution: 'Seleccionar resolución original', - useSelectedArea: 'Usar la resolución del área seleccionada', addResolution: 'Añadir resolución personalizada', add: 'Añadir', duplicateResolution: 'Esta resolución ya existe.', diff --git a/web/src/i18n/locales/fr.ts b/web/src/i18n/locales/fr.ts index a28aa11..699985a 100644 --- a/web/src/i18n/locales/fr.ts +++ b/web/src/i18n/locales/fr.ts @@ -76,9 +76,10 @@ const fr = { autoWarning: "L'étalonnage peut échouer si l'application utilisateur présente un arrière-plan entièrement noir.", manual: 'Manuel', + selectedResolution: 'Résolution de la zone sélectionnée', + unused: 'Non utilisée', originalResolution: "Résolution d'origine", selectResolution: "Sélectionner la résolution d'origine", - useSelectedArea: 'Utiliser la résolution de la zone sélectionnée', addResolution: 'Ajouter une résolution personnalisée', add: 'Ajouter', duplicateResolution: 'Cette résolution existe déjà.', diff --git a/web/src/i18n/locales/hu.ts b/web/src/i18n/locales/hu.ts index a6a2a0d..904f29b 100644 --- a/web/src/i18n/locales/hu.ts +++ b/web/src/i18n/locales/hu.ts @@ -75,9 +75,10 @@ const hu = { autoWarning: 'A kalibrálás sikertelen lehet, ha a felhasználói alkalmazás háttere teljesen fekete.', manual: 'Kézi', + selectedResolution: 'Kijelölt terület felbontása', + unused: 'Nincs használatban', originalResolution: 'Eredeti felbontás', selectResolution: 'Válassza ki az eredeti felbontást', - useSelectedArea: 'A kijelölt terület felbontásának használata', addResolution: 'Egyéni felbontás hozzáadása', add: 'Hozzáadás', duplicateResolution: 'Ez a felbontás már létezik.', diff --git a/web/src/i18n/locales/id.ts b/web/src/i18n/locales/id.ts index 9a484af..9bbb89e 100644 --- a/web/src/i18n/locales/id.ts +++ b/web/src/i18n/locales/id.ts @@ -73,9 +73,10 @@ const id = { autoWarning: 'Kalibrasi mungkin gagal jika aplikasi pengguna menggunakan latar belakang hitam pekat.', manual: 'Manual', + selectedResolution: 'Resolusi Area Terpilih', + unused: 'Tidak digunakan', originalResolution: 'Resolusi Asli', selectResolution: 'Pilih resolusi asli', - useSelectedArea: 'Gunakan resolusi area yang dipilih', addResolution: 'Tambahkan resolusi khusus', add: 'Tambah', duplicateResolution: 'Resolusi ini sudah ada.', diff --git a/web/src/i18n/locales/it.ts b/web/src/i18n/locales/it.ts index 1e419ad..1dd9c6c 100644 --- a/web/src/i18n/locales/it.ts +++ b/web/src/i18n/locales/it.ts @@ -75,9 +75,10 @@ const it = { autoWarning: "La calibrazione potrebbe non riuscire se l'applicazione utente ha uno sfondo completamente nero.", manual: 'Manuale', + selectedResolution: 'Risoluzione area selezionata', + unused: 'Non utilizzata', originalResolution: 'Risoluzione originale', selectResolution: 'Seleziona la risoluzione originale', - useSelectedArea: "Usa la risoluzione dell'area selezionata", addResolution: 'Aggiungi una risoluzione personalizzata', add: 'Aggiungi', duplicateResolution: 'Questa risoluzione esiste già.', diff --git a/web/src/i18n/locales/ja.ts b/web/src/i18n/locales/ja.ts index c305acd..290a119 100644 --- a/web/src/i18n/locales/ja.ts +++ b/web/src/i18n/locales/ja.ts @@ -73,9 +73,10 @@ const ja = { autoWarning: 'ユーザーアプリケーションの背景が完全な黒の場合、補正に失敗することがあります。', manual: '手動', + selectedResolution: '選択領域の解像度', + unused: '未使用', originalResolution: '元の解像度', selectResolution: '元の解像度を選択', - useSelectedArea: '選択領域の解像度を使用', addResolution: 'カスタム解像度を追加', add: '追加', duplicateResolution: 'この解像度はすでに存在します。', diff --git a/web/src/i18n/locales/ko.ts b/web/src/i18n/locales/ko.ts index e1b2d61..5d24176 100644 --- a/web/src/i18n/locales/ko.ts +++ b/web/src/i18n/locales/ko.ts @@ -71,9 +71,10 @@ const ko = { auto: '자동', autoWarning: '사용자 애플리케이션의 배경이 완전히 검은색이면 보정에 실패할 수 있습니다.', manual: '수동', + selectedResolution: '선택 영역 해상도', + unused: '사용 안 함', originalResolution: '원본 해상도', selectResolution: '원본 해상도 선택', - useSelectedArea: '선택한 영역의 해상도 사용', addResolution: '사용자 지정 해상도 추가', add: '추가', duplicateResolution: '이미 존재하는 해상도입니다.', diff --git a/web/src/i18n/locales/nb.ts b/web/src/i18n/locales/nb.ts index cad7c37..3fcf9b6 100644 --- a/web/src/i18n/locales/nb.ts +++ b/web/src/i18n/locales/nb.ts @@ -74,9 +74,10 @@ const nb = { autoWarning: 'Kalibreringen kan mislykkes hvis brukerprogrammet har en helt svart bakgrunn.', manual: 'Manuell', + selectedResolution: 'Oppløsning for valgt område', + unused: 'Ikke i bruk', originalResolution: 'Opprinnelig oppløsning', selectResolution: 'Velg opprinnelig oppløsning', - useSelectedArea: 'Bruk oppløsningen til det valgte området', addResolution: 'Legg til egendefinert oppløsning', add: 'Legg til', duplicateResolution: 'Denne oppløsningen finnes allerede.', diff --git a/web/src/i18n/locales/nl.ts b/web/src/i18n/locales/nl.ts index 37e22d6..1e7c6d5 100644 --- a/web/src/i18n/locales/nl.ts +++ b/web/src/i18n/locales/nl.ts @@ -75,9 +75,10 @@ const nl = { autoWarning: 'De kalibratie kan mislukken als de gebruikerstoepassing een volledig zwarte achtergrond heeft.', manual: 'Handmatig', + selectedResolution: 'Resolutie van geselecteerd gebied', + unused: 'Niet gebruikt', originalResolution: 'Oorspronkelijke resolutie', selectResolution: 'Oorspronkelijke resolutie selecteren', - useSelectedArea: 'Resolutie van geselecteerd gebied gebruiken', addResolution: 'Aangepaste resolutie toevoegen', add: 'Toevoegen', duplicateResolution: 'Deze resolutie bestaat al.', diff --git a/web/src/i18n/locales/pl.ts b/web/src/i18n/locales/pl.ts index 514e07e..8613e41 100644 --- a/web/src/i18n/locales/pl.ts +++ b/web/src/i18n/locales/pl.ts @@ -74,9 +74,10 @@ const pl = { autoWarning: 'Kalibracja może się nie powieść, gdy aplikacja użytkownika ma całkowicie czarne tło.', manual: 'Ręczna', + selectedResolution: 'Rozdzielczość zaznaczonego obszaru', + unused: 'Nieużywane', originalResolution: 'Oryginalna rozdzielczość', selectResolution: 'Wybierz oryginalną rozdzielczość', - useSelectedArea: 'Użyj rozdzielczości zaznaczonego obszaru', addResolution: 'Dodaj niestandardową rozdzielczość', add: 'Dodaj', duplicateResolution: 'Ta rozdzielczość już istnieje.', diff --git a/web/src/i18n/locales/pt_br.ts b/web/src/i18n/locales/pt_br.ts index a99433f..8309dca 100644 --- a/web/src/i18n/locales/pt_br.ts +++ b/web/src/i18n/locales/pt_br.ts @@ -73,9 +73,10 @@ const pt_br = { autoWarning: 'A calibração pode falhar se o aplicativo do usuário tiver um fundo totalmente preto.', manual: 'Manual', + selectedResolution: 'Resolução da área selecionada', + unused: 'Não utilizado', originalResolution: 'Resolução original', selectResolution: 'Selecionar resolução original', - useSelectedArea: 'Usar a resolução da área selecionada', addResolution: 'Adicionar resolução personalizada', add: 'Adicionar', duplicateResolution: 'Esta resolução já existe.', diff --git a/web/src/i18n/locales/ru.ts b/web/src/i18n/locales/ru.ts index 2fdb7b2..ab15241 100644 --- a/web/src/i18n/locales/ru.ts +++ b/web/src/i18n/locales/ru.ts @@ -73,9 +73,10 @@ const ru = { autoWarning: 'Калибровка может завершиться неудачей, если фон пользовательского приложения полностью чёрный.', manual: 'Вручную', + selectedResolution: 'Разрешение выбранной области', + unused: 'Не используется', originalResolution: 'Исходное разрешение', selectResolution: 'Выберите исходное разрешение', - useSelectedArea: 'Использовать разрешение выбранной области', addResolution: 'Добавить пользовательское разрешение', add: 'Добавить', duplicateResolution: 'Такое разрешение уже существует.', diff --git a/web/src/i18n/locales/se.ts b/web/src/i18n/locales/se.ts index 2e07934..968dd45 100644 --- a/web/src/i18n/locales/se.ts +++ b/web/src/i18n/locales/se.ts @@ -70,9 +70,10 @@ const se = { auto: 'Automatisk', autoWarning: 'Kalibreringen kan misslyckas om användarprogrammet har en helsvart bakgrund.', manual: 'Manuell', + selectedResolution: 'Valt områdesupplösning', + unused: 'Används inte', originalResolution: 'Ursprunglig upplösning', selectResolution: 'Välj ursprunglig upplösning', - useSelectedArea: 'Använd upplösningen för det valda området', addResolution: 'Lägg till anpassad upplösning', add: 'Lägg till', duplicateResolution: 'Den här upplösningen finns redan.', diff --git a/web/src/i18n/locales/th.ts b/web/src/i18n/locales/th.ts index 929f2a9..f4d2f8f 100644 --- a/web/src/i18n/locales/th.ts +++ b/web/src/i18n/locales/th.ts @@ -70,9 +70,10 @@ const th = { auto: 'อัตโนมัติ', autoWarning: 'การปรับเทียบอาจล้มเหลวเมื่อแอปพลิเคชันของผู้ใช้มีพื้นหลังสีดำสนิท', manual: 'กำหนดเอง', + selectedResolution: 'ความละเอียดพื้นที่ที่เลือก', + unused: 'ไม่ได้ใช้', originalResolution: 'ความละเอียดต้นฉบับ', selectResolution: 'เลือกความละเอียดต้นฉบับ', - useSelectedArea: 'ใช้ความละเอียดของพื้นที่ที่เลือก', addResolution: 'เพิ่มความละเอียดแบบกำหนดเอง', add: 'เพิ่ม', duplicateResolution: 'มีความละเอียดนี้อยู่แล้ว', diff --git a/web/src/i18n/locales/tr.ts b/web/src/i18n/locales/tr.ts index 80abb96..9e6361c 100644 --- a/web/src/i18n/locales/tr.ts +++ b/web/src/i18n/locales/tr.ts @@ -74,9 +74,10 @@ const tr = { autoWarning: 'Kullanıcı uygulamasının arka planı tamamen siyah olduğunda kalibrasyon başarısız olabilir.', manual: 'Manuel', + selectedResolution: 'Seçili Alan Çözünürlüğü', + unused: 'Kullanılmıyor', originalResolution: 'Orijinal Çözünürlük', selectResolution: 'Orijinal çözünürlüğü seçin', - useSelectedArea: 'Seçili alanın çözünürlüğünü kullan', addResolution: 'Özel çözünürlük ekle', add: 'Ekle', duplicateResolution: 'Bu çözünürlük zaten mevcut.', diff --git a/web/src/i18n/locales/uk.ts b/web/src/i18n/locales/uk.ts index 10e3706..c5b76d5 100644 --- a/web/src/i18n/locales/uk.ts +++ b/web/src/i18n/locales/uk.ts @@ -74,9 +74,10 @@ const uk = { autoWarning: 'Калібрування може завершитися невдало, якщо користувацька програма має повністю чорне тло.', manual: 'Вручну', + selectedResolution: 'Роздільна здатність вибраної області', + unused: 'Не використовується', originalResolution: 'Початкова роздільна здатність', selectResolution: 'Виберіть початкову роздільну здатність', - useSelectedArea: 'Використати роздільну здатність вибраної області', addResolution: 'Додати власну роздільну здатність', add: 'Додати', duplicateResolution: 'Така роздільна здатність уже існує.', diff --git a/web/src/i18n/locales/vi.ts b/web/src/i18n/locales/vi.ts index 0ca2f1d..223554e 100644 --- a/web/src/i18n/locales/vi.ts +++ b/web/src/i18n/locales/vi.ts @@ -73,9 +73,10 @@ const vi = { autoWarning: 'Hiệu chỉnh có thể thất bại khi ứng dụng của người dùng có nền hoàn toàn màu đen.', manual: 'Thủ công', + selectedResolution: 'Độ phân giải vùng đã chọn', + unused: 'Không sử dụng', originalResolution: 'Độ phân giải gốc', selectResolution: 'Chọn độ phân giải gốc', - useSelectedArea: 'Sử dụng độ phân giải của vùng đã chọn', addResolution: 'Thêm độ phân giải tùy chỉnh', add: 'Thêm', duplicateResolution: 'Độ phân giải này đã tồn tại.', diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index 7cce56a..4feecf9 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -72,9 +72,10 @@ const zh = { auto: '自动', autoWarning: '若用户程序为纯黑色背景,则可能校准出错。', manual: '手动', + selectedResolution: '框选分辨率', + unused: '未使用', originalResolution: '原始分辨率', selectResolution: '选择原始分辨率', - useSelectedArea: '使用框选分辨率', addResolution: '添加自定义分辨率', add: '添加', duplicateResolution: '该分辨率已存在。', diff --git a/web/src/i18n/locales/zh_tw.ts b/web/src/i18n/locales/zh_tw.ts index 8176c8f..d120728 100644 --- a/web/src/i18n/locales/zh_tw.ts +++ b/web/src/i18n/locales/zh_tw.ts @@ -69,9 +69,10 @@ const zh_tw = { auto: '自動', autoWarning: '當使用者應用程式使用全黑背景時,校正可能會失敗。', manual: '手動', + selectedResolution: '框選解析度', + unused: '未使用', originalResolution: '原始解析度', selectResolution: '選擇原始解析度', - useSelectedArea: '使用所選區域的解析度', addResolution: '新增自訂解析度', add: '新增', duplicateResolution: '此解析度已存在。', diff --git a/web/src/jotai/screen.ts b/web/src/jotai/screen.ts index d5b26ba..0cbf525 100644 --- a/web/src/jotai/screen.ts +++ b/web/src/jotai/screen.ts @@ -18,6 +18,8 @@ export const resolutionAtom = atom(null); // currently effective absolute mouse input region export const inputRegionAtom = atom(null); export const manualInputRegionAtom = atom(null); +export const manualRegionsAtom = atom([]); +export const selectedManualRegionAtom = atom(''); export const selectedOriginalResolutionAtom = atom(''); // device-level control region mode; disabled by default diff --git a/web/src/pages/desktop/index.tsx b/web/src/pages/desktop/index.tsx index a533a52..bcb1b07 100644 --- a/web/src/pages/desktop/index.tsx +++ b/web/src/pages/desktop/index.tsx @@ -13,7 +13,9 @@ import { controlRegionModeAtom, inputRegionAtom, manualInputRegionAtom, + manualRegionsAtom, resolutionAtom, + selectedManualRegionAtom, selectedOriginalResolutionAtom, videoModeAtom } from '@/jotai/screen.ts'; @@ -42,14 +44,11 @@ function getVideoMode() { const defaultVideoMode = window.RTCPeerConnection ? 'h264' : 'mjpeg'; const cookieVideoMode = storage.getVideoMode(); - if (cookieVideoMode) { - if (cookieVideoMode === 'direct' && !window.VideoDecoder) { - return defaultVideoMode; - } - return cookieVideoMode; + if (!cookieVideoMode || (cookieVideoMode === 'direct' && !window.VideoDecoder)) { + return defaultVideoMode; } - return defaultVideoMode; + return ['direct', 'h264', 'mjpeg'].includes(cookieVideoMode) ? cookieVideoMode : defaultVideoMode; } export const Desktop = () => { @@ -64,6 +63,8 @@ export const Desktop = () => { const [inputRegion, setInputRegion] = useAtom(inputRegionAtom); const [controlRegionMode, setControlRegionModeState] = useAtom(controlRegionModeAtom); const setManualInputRegion = useSetAtom(manualInputRegionAtom); + const setManualRegions = useSetAtom(manualRegionsAtom); + const setSelectedManualRegion = useSetAtom(selectedManualRegionAtom); const setSelectedOriginalResolution = useSetAtom(selectedOriginalResolutionAtom); const isPicoclawChatOpen = useAtomValue(picoclawChatOpenAtom); @@ -76,6 +77,8 @@ export const Desktop = () => { setResolution(res); setInputRegion(null); setManualInputRegion(null); + setManualRegions([]); + setSelectedManualRegion(''); setSelectedOriginalResolution(''); setControlRegionModeState('off'); @@ -87,15 +90,24 @@ export const Desktop = () => { ? (config as InputRegion) : null; const selectedResolution = config?.selectedResolution || ''; + const regions = config?.regions || []; + const selectedRegion = config?.selectedRegion || ''; + const selectedManualRegion = regions.find( + (region) => `${region.width}x${region.height}` === selectedRegion + ); setManualInputRegion(manualRegion); + setManualRegions(regions); + setSelectedManualRegion(selectedRegion); setSelectedOriginalResolution(selectedResolution); - setInputRegion(mode === 'manual' && !selectedResolution ? manualRegion : null); + setInputRegion(mode === 'manual' && selectedRegion ? selectedManualRegion || null : null); setControlRegionModeState(mode); }) .catch(() => { setControlRegionModeState('off'); setInputRegion(null); setManualInputRegion(null); + setManualRegions([]); + setSelectedManualRegion(''); setSelectedOriginalResolution(''); }); @@ -107,7 +119,9 @@ export const Desktop = () => { setControlRegionModeState, setInputRegion, setManualInputRegion, + setManualRegions, setResolution, + setSelectedManualRegion, setSelectedOriginalResolution, setVideoMode ]); @@ -135,7 +149,7 @@ export const Desktop = () => { clearTimeout(validationTimer); } validationTimer = setTimeout(() => { - const mediaSize = getMediaSize(target); + const mediaSize = getMediaSize(target, resolution); if (!mediaSize || !isMediaReady(target) || isInputRegionCompatible(region, mediaSize)) { return; } diff --git a/web/src/pages/desktop/menu/screen/constants.ts b/web/src/pages/desktop/menu/screen/constants.ts index 64d45e7..9fac0cf 100644 --- a/web/src/pages/desktop/menu/screen/constants.ts +++ b/web/src/pages/desktop/menu/screen/constants.ts @@ -11,3 +11,23 @@ export const BitRateMap = new Map([ [3, 2000], [4, 1000] ]); + +export function getQualityMap(videoMode: string) { + if (videoMode === 'mjpeg') { + return QualityMap; + } + if (videoMode === 'direct' || videoMode === 'h264') { + return BitRateMap; + } + return null; +} + +export function getScreenType(videoMode: string) { + if (videoMode === 'mjpeg') { + return 0; + } + if (videoMode === 'direct' || videoMode === 'h264') { + return 1; + } + return null; +} diff --git a/web/src/pages/desktop/menu/screen/index.tsx b/web/src/pages/desktop/menu/screen/index.tsx index f8b2fbb..c910706 100644 --- a/web/src/pages/desktop/menu/screen/index.tsx +++ b/web/src/pages/desktop/menu/screen/index.tsx @@ -8,7 +8,7 @@ import * as ls from '@/lib/localstorage'; import { resolutionAtom, videoModeAtom } from '@/jotai/screen.ts'; import { MenuItem } from '@/components/menu-item.tsx'; -import { BitRateMap, QualityMap } from './constants.ts'; +import { getQualityMap, getScreenType } from './constants.ts'; import { Fps } from './fps'; import { FrameDetect } from './frame-detect'; import { Gop } from './gop.tsx'; @@ -23,12 +23,16 @@ export const Screen = () => { const videoMode = useAtomValue(videoModeAtom); const resolution = useAtomValue(resolutionAtom); + const isMjpeg = videoMode === 'mjpeg'; const [fps, setFps] = useState(30); const [quality, setQuality] = useState(2); const [gop, setGop] = useState(30); useEffect(() => { - updateScreen('type', videoMode === 'mjpeg' ? 0 : 1); + const screenType = getScreenType(videoMode); + if (screenType !== null) { + updateScreen('type', screenType); + } updateScreen('resolution', resolution!.height); updateQuality(); updateFps(); @@ -40,7 +44,9 @@ export const Screen = () => { if (!cookieQuality) return; const key = cookieQuality >= 1 && cookieQuality <= 4 ? cookieQuality : 2; - const value = videoMode === 'mjpeg' ? QualityMap.get(key)! : BitRateMap.get(key)!; + const qualityMap = getQualityMap(videoMode); + const value = qualityMap?.get(key); + if (value === undefined) return; const rsp = await updateScreen('quality', value); if (rsp.code === 0) { @@ -75,8 +81,8 @@ export const Screen = () => { - {videoMode !== 'mjpeg' && } - {videoMode === 'mjpeg' && } + {!isMjpeg && } + {isMjpeg && } ); diff --git a/web/src/pages/desktop/menu/screen/original-resolution.tsx b/web/src/pages/desktop/menu/screen/original-resolution.tsx index bcad02b..fa88818 100644 --- a/web/src/pages/desktop/menu/screen/original-resolution.tsx +++ b/web/src/pages/desktop/menu/screen/original-resolution.tsx @@ -12,12 +12,13 @@ import { Tooltip, Typography } from 'antd'; -import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { useAtom, useSetAtom } from 'jotai'; import { ScanSearchIcon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { getInputRegion, + setManualRegions as saveManualRegions, setControlRegionMode, setOriginalResolutionConfig, setSelectedOriginalResolution @@ -34,6 +35,8 @@ import { inputRegionAtom, inputRegionSelectingAtom, manualInputRegionAtom, + manualRegionsAtom, + selectedManualRegionAtom, selectedOriginalResolutionAtom } from '@/jotai/screen.ts'; import { menuCloseSignalAtom } from '@/jotai/settings.ts'; @@ -44,9 +47,12 @@ const resolutionKey = ({ width, height }: ResolutionPreset) => `${width}x${heigh export const OriginalResolution = () => { const { t } = useTranslation(); const setInputRegion = useSetAtom(inputRegionAtom); - const manualInputRegion = useAtomValue(manualInputRegionAtom); const setManualInputRegion = useSetAtom(manualInputRegionAtom); - const [selectedResolution, setSelectedResolution] = useAtom(selectedOriginalResolutionAtom); + const [manualRegions, setManualRegions] = useAtom(manualRegionsAtom); + const [selectedManualRegion, setSelectedManualRegion] = useAtom(selectedManualRegionAtom); + const [selectedOriginalResolution, setSelectedOriginalResolutionState] = useAtom( + selectedOriginalResolutionAtom + ); const [mode, setMode] = useAtom(controlRegionModeAtom); const setSelecting = useSetAtom(inputRegionSelectingAtom); const setKeyboardLock = useSetAtom(keyboardLockAtom); @@ -74,7 +80,9 @@ export const OriginalResolution = () => { if (rsp.code === 0) { const config = rsp.data as ControlRegionConfig; setResolutions(config?.resolutions || []); - setSelectedResolution(config?.selectedResolution || ''); + setManualRegions(config?.regions || []); + setSelectedManualRegion(config?.selectedRegion || ''); + setSelectedOriginalResolutionState(config?.selectedResolution || ''); setManualInputRegion( isValidInputRegion(config as InputRegion) ? (config as InputRegion) : null ); @@ -82,18 +90,41 @@ export const OriginalResolution = () => { return rsp; } - async function applyResolution(key: string) { + async function applyOriginalResolution(key: string) { const rsp = await setSelectedOriginalResolution(key); if (rsp.code !== 0) { messageApi.error(t('screen.controlRegion.saveFailed')); return; } - setSelectedResolution(key); + setSelectedOriginalResolutionState(key); + setSelectedManualRegion(''); + if (!key) setInputRegion(null); setIsPopoverOpen(true); } - async function saveResolutions(next: ResolutionPreset[], nextSelected = selectedResolution) { - const rsp = await setOriginalResolutionConfig(next, nextSelected); + async function applyManualRegion(key: string) { + const rsp = await saveManualRegions(manualRegions, key); + if (rsp.code !== 0) { + messageApi.error(t('screen.controlRegion.saveFailed')); + return; + } + setSelectedManualRegion(key); + setSelectedOriginalResolutionState(''); + setInputRegion( + manualRegions.find((region) => `${region.width}x${region.height}` === key) || null + ); + setIsPopoverOpen(true); + } + + async function saveResolutions( + next: ResolutionPreset[], + nextSelected = selectedOriginalResolution + ) { + const rsp = await setOriginalResolutionConfig( + next, + nextSelected, + nextSelected ? '' : selectedManualRegion + ); if (rsp.code !== 0) { messageApi.error(t('screen.controlRegion.saveFailed')); return false; @@ -122,7 +153,7 @@ export const OriginalResolution = () => { async function deleteResolution(index: number) { if (index < 0 || index >= resolutions.length) return; const deleted = resolutionKey(resolutions[index]); - const nextSelected = selectedResolution === deleted ? '' : selectedResolution; + const nextSelected = selectedOriginalResolution === deleted ? '' : selectedOriginalResolution; if ( await saveResolutions( resolutions.filter((_, itemIndex) => itemIndex !== index), @@ -130,12 +161,29 @@ export const OriginalResolution = () => { ) ) { if (!nextSelected) { - setSelectedResolution(''); - setInputRegion(manualInputRegion); + setSelectedOriginalResolutionState(''); + setInputRegion(null); } } } + async function deleteManualRegion(index: number) { + if (index < 0 || index >= manualRegions.length) return; + const deleted = `${manualRegions[index].width}x${manualRegions[index].height}`; + const nextSelected = selectedManualRegion === deleted ? '' : selectedManualRegion; + const next = manualRegions.filter((_, itemIndex) => itemIndex !== index); + const rsp = await saveManualRegions(next, nextSelected, selectedOriginalResolution); + if (rsp.code !== 0) { + messageApi.error(t('screen.controlRegion.saveFailed')); + return; + } + setManualRegions(next); + if (!nextSelected) { + setSelectedManualRegion(''); + setInputRegion(null); + } + } + async function updateMode(nextMode: ControlRegionMode) { const rsp = await setControlRegionMode(nextMode); if (rsp.code !== 0) { @@ -148,9 +196,14 @@ export const OriginalResolution = () => { const manualRegion = isValidInputRegion(config.data as InputRegion) ? (config.data as InputRegion) : null; + const selectedRegion = (config.data as ControlRegionConfig)?.selectedRegion || ''; + const selectedResolution = (config.data as ControlRegionConfig)?.selectedResolution || ''; + const regions = (config.data as ControlRegionConfig)?.regions || []; setManualInputRegion(manualRegion); setInputRegion( - (config.data as ControlRegionConfig)?.selectedResolution ? null : manualRegion + selectedResolution + ? null + : regions.find((region) => `${region.width}x${region.height}` === selectedRegion) || null ); } else { setInputRegion(null); @@ -195,19 +248,58 @@ export const OriginalResolution = () => { + {t('screen.controlRegion.selectedResolution')} + trigger.parentElement || document.body} - options={resolutions - .map((resolution) => ({ + options={[ + { label: t('screen.controlRegion.unused'), value: '' }, + ...resolutions.map((resolution) => ({ label: resolutionKey(resolution), value: resolutionKey(resolution) })) - .concat([{ label: t('screen.controlRegion.useSelectedArea'), value: '' }])} + ]} optionRender={(option) => (
{option.label} @@ -233,7 +325,7 @@ export const OriginalResolution = () => { )}
)} - onChange={applyResolution} + onChange={applyOriginalResolution} />