diff --git a/VoidLink.xcodeproj/project.pbxproj b/VoidLink.xcodeproj/project.pbxproj index 730cc37d..752c74ae 100644 --- a/VoidLink.xcodeproj/project.pbxproj +++ b/VoidLink.xcodeproj/project.pbxproj @@ -1292,7 +1292,7 @@ "$(PROJECT_DIR)/libs/SDL2/lib/iOS-Sim", "$(PROJECT_DIR)/libs/FFmpeg/lib/iOS-Sim", ); - MARKETING_VERSION = 0.0.1; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.voidlink.tf.debug10.iOS; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -1357,7 +1357,7 @@ "$(PROJECT_DIR)/libs/SDL2/lib/iOS-Sim", "$(PROJECT_DIR)/libs/FFmpeg/lib/iOS-Sim", ); - MARKETING_VERSION = 0.0.1; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.voidlink.tf.iOS; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; diff --git a/VoidLink/.DS_Store b/VoidLink/.DS_Store index d5e91d03..d331c5b6 100644 Binary files a/VoidLink/.DS_Store and b/VoidLink/.DS_Store differ diff --git a/VoidLink/AboutView.swift b/VoidLink/AboutView.swift index 206967aa..d1d2d443 100644 --- a/VoidLink/AboutView.swift +++ b/VoidLink/AboutView.swift @@ -17,9 +17,10 @@ public struct AboutView: View { VStack(spacing: 10) { // App 图标 Image(uiImage: UIImage(named: "AppIconMedium") ?? UIImage()) - .resizable() - .frame(width: 80, height: 80) - .clipShape(RoundedRectangle(cornerRadius: 36)) + // .resizable() + .resizable(capInsets:EdgeInsets(top: 3.5, leading: 3.5, bottom: 3.5, trailing: 3.5)) + .frame(width: 100, height: 100) + .clipShape(RoundedRectangle(cornerRadius: 28)) // App 名称 Text(Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "App Name") @@ -56,7 +57,7 @@ public struct AboutView: View { } // 链接按钮 if #available(iOS 14.0, *) { - Link(SwiftLocalizationHelper.localizedString(forKey: "Join us"), destination: URL(string: "https://example.com")!) + Link(SwiftLocalizationHelper.localizedString(forKey: "Join us"), destination: URL(string: SwiftLocalizationHelper.localizedString(forKey: "supportLink"))!) .padding(.top, 10) Spacer() // OK 按钮 @@ -73,7 +74,7 @@ public struct AboutView: View { HStack(spacing: 20) { Button(SwiftLocalizationHelper.localizedString(forKey: "Join us")) { // 打开链接 - if let url = URL(string: "https://example.com") { + if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "supportLink")) { UIApplication.shared.open(url) } } @@ -81,6 +82,7 @@ public struct AboutView: View { .background(Color.blue) .foregroundColor(.white) .cornerRadius(12) + .frame(height: 33) .frame(minWidth: 100) Button("OK") { @@ -90,6 +92,7 @@ public struct AboutView: View { .background(Color.blue) .foregroundColor(.white) .cornerRadius(12) + .frame(height: 33) .frame(minWidth: 100) } .padding(.top, 10) diff --git a/VoidLink/AboutViewController.swift b/VoidLink/AboutViewController.swift index b82eb1f8..be751ec2 100644 --- a/VoidLink/AboutViewController.swift +++ b/VoidLink/AboutViewController.swift @@ -28,7 +28,7 @@ import SwiftUI ]) // 🎯 设置弹窗大小 - preferredContentSize = CGSize(width: 600, height: 450) + preferredContentSize = CGSize(width: 530, height: 450) //preferredContentSize = CGSize(width:150, height:120 ) // ⚠️ 一定要设置以下样式才会生效 preferredContentSize diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index 9d879369..b5ff27ed 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -60,7 +60,7 @@ - + @@ -581,7 +581,7 @@ - + @@ -847,27 +847,6 @@ - - - - - - - - - - - - - @@ -1008,12 +987,32 @@ + + + + + + + + + + + + + - + - @@ -1048,6 +1047,7 @@ + @@ -1120,7 +1120,7 @@ - + @@ -1749,7 +1749,7 @@ - + @@ -1758,13 +1758,13 @@ - + - + - + diff --git a/VoidLink/Base.lproj/iPhone.storyboard b/VoidLink/Base.lproj/iPhone.storyboard index fc1c9a67..9fb1c0e7 100644 --- a/VoidLink/Base.lproj/iPhone.storyboard +++ b/VoidLink/Base.lproj/iPhone.storyboard @@ -13,7 +13,7 @@ - + @@ -1585,7 +1585,6 @@ - @@ -1614,6 +1613,7 @@ + diff --git a/VoidLink/HostCardView.m b/VoidLink/HostCardView.m index 85ed5a21..9081bb2b 100644 --- a/VoidLink/HostCardView.m +++ b/VoidLink/HostCardView.m @@ -248,8 +248,8 @@ static const float REFRESH_CYCLE = 2.0f; [NSLayoutConstraint activateConstraints:@[ [lockIconView.centerXAnchor constraintEqualToAnchor:_iconBackgroundView.centerXAnchor constant:0], [lockIconView.centerYAnchor constraintEqualToAnchor:_iconBackgroundView.centerYAnchor constant:computerIconMonitorCenterYOffset], - [lockIconView.widthAnchor constraintEqualToConstant:18.5*_sizeFactor], - [lockIconView.heightAnchor constraintEqualToConstant:18.5*_sizeFactor] + [lockIconView.widthAnchor constraintEqualToConstant:17.05*_sizeFactor], + [lockIconView.heightAnchor constraintEqualToConstant:17.05*_sizeFactor] ]]; lockIconView.hidden = true; diff --git a/VoidLink/Images.xcassets/icon-pc-app.imageset/icon-pc-app.svg b/VoidLink/Images.xcassets/icon-pc-app.imageset/icon-pc-app.svg index 04e56c40..0c43597f 100644 --- a/VoidLink/Images.xcassets/icon-pc-app.imageset/icon-pc-app.svg +++ b/VoidLink/Images.xcassets/icon-pc-app.imageset/icon-pc-app.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/VoidLink/Input/CustomOSC/LayoutOnScreenControls.m b/VoidLink/Input/CustomOSC/LayoutOnScreenControls.m index 566a56ed..a8cbea90 100644 --- a/VoidLink/Input/CustomOSC/LayoutOnScreenControls.m +++ b/VoidLink/Input/CustomOSC/LayoutOnScreenControls.m @@ -14,6 +14,7 @@ #import "OnScreenButtonState.h" #import "OSCProfilesManager.h" + @interface LayoutOnScreenControls () @end diff --git a/VoidLink/Input/CustomOSC/OSCProfile.h b/VoidLink/Input/CustomOSC/OSCProfile.h index ff22b639..04dc2af5 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.h +++ b/VoidLink/Input/CustomOSC/OSCProfile.h @@ -13,7 +13,8 @@ #import "OnScreenButtonState.h" NS_ASSUME_NONNULL_BEGIN -#define DEFAULT_TEMPLATE_NAME @"高级控件模板(Advanced widget templates)" +#define DEFAULT_TEMPLATE_NAME1 @"控件模板 / Widget templates" +#define DEFAULT_TEMPLATE_NAME2 @"旧版屏显控件 / Legacy on-screen controls" /** This object contains information pertaining to any of the user created, custom on screen controller layout configurations, or 'profiles.' The object contains a 'name' property for easy reference, as well as an 'isSelected' property which is used to determine whether this particular custom OSC layout should show on screen during game stream view. Only one 'OSCProfile' is set to 'isSelected' at any given time. The object also contains an array of 'OnScreenButtonStates' which provides information that allows us to move and hide/unhide each of the 19 on screen buttons. Note that the 'buttonStates' property should contain an NSMutableArray of ENCODED 'OnScreenButtonState' objects. This allows us to save the 'OSCProfile' object to NSUserDefaults. diff --git a/VoidLink/Input/CustomOSC/OSCProfilesManager.h b/VoidLink/Input/CustomOSC/OSCProfilesManager.h index 93cf5afc..58cc6bf3 100644 --- a/VoidLink/Input/CustomOSC/OSCProfilesManager.h +++ b/VoidLink/Input/CustomOSC/OSCProfilesManager.h @@ -68,7 +68,7 @@ NS_ASSUME_NONNULL_BEGIN * Lets the caller of this method know whether a profile with a given name already exists in persistent storage */ - (BOOL) profileNameAlreadyExist:(NSString*)name; -- (BOOL) profileName:(NSString*) name alreadyExistIn:(NSMutableArray*)profiles; +- (OSCProfile *) findProfileByName:(NSString*) name inProfileArray:(NSMutableArray*)profiles; @end diff --git a/VoidLink/Input/CustomOSC/OSCProfilesManager.m b/VoidLink/Input/CustomOSC/OSCProfilesManager.m index fd469676..bc1e0141 100644 --- a/VoidLink/Input/CustomOSC/OSCProfilesManager.m +++ b/VoidLink/Input/CustomOSC/OSCProfilesManager.m @@ -142,7 +142,11 @@ static CGRect streamViewBounds; - (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded { NSMutableArray* targetProfiles = [_currentProfiles mutableCopy]; //_currentProfiles is availabled as long as getAllProfiles was called before calling this method (_currentProfiles avoids frequent accessing persisted data) - if([self profileName:DEFAULT_TEMPLATE_NAME alreadyExistIn:targetProfiles] && targetProfiles.count > 1) [targetProfiles removeObjectAtIndex:1]; + OSCProfile* profile; + profile = [self findProfileByName:DEFAULT_TEMPLATE_NAME1 inProfileArray:targetProfiles]; + if(profile && targetProfiles.count > 1) [targetProfiles removeObject:profile]; + profile = [self findProfileByName:DEFAULT_TEMPLATE_NAME2 inProfileArray:targetProfiles]; + if(profile && targetProfiles.count > 1) [targetProfiles removeObject:profile]; if(targetProfiles.count > 0) [targetProfiles removeObjectAtIndex:0]; NSMutableArray* localEncodedPofiles = [self encodedProfilesFromArray:targetProfiles]; [profilesEncoded addObjectsFromArray:localEncodedPofiles]; @@ -393,14 +397,14 @@ static CGRect streamViewBounds; return NO; } -- (BOOL) profileName:(NSString*) name alreadyExistIn:(NSMutableArray*)profiles { +- (OSCProfile *) findProfileByName:(NSString*) name inProfileArray:(NSMutableArray*)profiles { /* Iterate through the decoded profiles and return 'YES' if one of the profiles' 'name' properties equals the 'name' passed into this method */ for (OSCProfile *profile in profiles) { if ([profile.name isEqualToString:name]) { - return YES; + return profile; } } - return NO; + return nil; } - (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data { diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index 2cea3abc..cca8b716 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -361,7 +361,7 @@ import UIKit if self.shape == "round"{ // we'll make custom osc buttons round & smaller NSLayoutConstraint.activate([ self.widthAnchor.constraint(equalToConstant: CGFloat(Int(60 * self.widthFactor / 2) * 2)), - self.heightAnchor.constraint(equalToConstant: CGFloat(Int(60 * self.heightFactor / 2) * 2)),]) + self.heightAnchor.constraint(equalToConstant: CGFloat(Int(60 * self.widthFactor / 2) * 2)),]) } if self.shape == "square" { NSLayoutConstraint.activate([ @@ -587,7 +587,7 @@ import UIKit // illustrate offset distance in edit mode // let offsetSign = self.selfViewOnTheRight ? -1 : 1 // dprecated // let illlustrationPoint = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMaxY(self.frame)/4) - self.crossMarkLayer.position = CGPointMake(CGRectGetMidX(self.stickBallLayer.frame), CGRectGetMidY(self.stickBallLayer.frame)-stickIndicatorOffset) + self.stickBallLayer.position = CGPointMake(CGRectGetMidX(self.crossMarkLayer.frame), CGRectGetMidY(self.crossMarkLayer.frame)-stickIndicatorOffset) } CATransaction.commit() } diff --git a/VoidLink/Input/PureNativeTouchHandler.m b/VoidLink/Input/PureNativeTouchHandler.m index 3b029709..27c58962 100644 --- a/VoidLink/Input/PureNativeTouchHandler.m +++ b/VoidLink/Input/PureNativeTouchHandler.m @@ -34,6 +34,7 @@ NSMutableSet *pointerIdPool; //pre-defined pool of pointerIds. NSMutableSet *unassignedPointerIds; NSMutableSet *blacklistedTouches; + bool doNotBlockTouchEvents; NSMutableDictionary *pointerObjDict; @@ -59,6 +60,7 @@ } self->activePointerIds = [NSMutableSet set]; self->blacklistedTouches = [NSMutableSet set]; + doNotBlockTouchEvents = true; self->asyncNativeTouch = settings.asyncNativeTouchPriority.intValue != AsyncNativeTouchOff; @@ -164,7 +166,8 @@ //check if touch point is spawned on the left or right upper half screen edges, event to remote PC. this is for better handling in-stream slide gesture CGPoint initialPoint = [touch locationInView:self->streamView]; if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) { - [blacklistedTouches addObject:touchAddrObj]; + doNotBlockTouchEvents = false; + // [blacklistedTouches addObject:touchAddrObj]; } } @@ -188,8 +191,8 @@ - (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType{ //if(touchPointSpawnedAtUpperScreenEdge && touchType != LI_TOUCH_EVENT_UP) return; // we're done here. this touch event will not be sent to the remote PC. and this must be checked after coord selector finishes populating new relative coords, or the app will crash - if([blacklistedTouches containsObject:@((uintptr_t)touch)]) return; - + // if([blacklistedTouches containsObject:@((uintptr_t)touch)]) return; + CGPoint targetCoords; //NSLog(@"selecting coords: %d", touch.phase == UITouchPhaseMoved); // NSLog(@"excluded count: %d", (uint32_t)[excludedPointerIds count]); @@ -211,6 +214,7 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchDownQos, 0), ^{ for (UITouch* touch in touches){ + // continue to the next loop if current touch is already captured by OSC. works only in regular native touch [self handleTouchDown:touch]; //generate & populate pointerId if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch]; [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN]; @@ -218,6 +222,7 @@ }); else{ for (UITouch* touch in touches){ + // continue to the next loop if current touch is already captured by OSC. works only in regular native touch [self handleTouchDown:touch]; //generate & populate pointerId if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch]; [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN]; @@ -228,14 +233,16 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchMoveQos, 0), ^{ for (UITouch* touch in touches){ + // continue to the next loop if current touch is already captured by OSC. works only in regular native touch if(self->activateCoordSelector) [self updatePointerObjInDict:touch]; - [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE]; + if(self->doNotBlockTouchEvents) [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE]; [[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service) usleep(self->touchMoveEventIntervalUs); } }); else { for (UITouch* touch in touches){ + // continue to the next loop if current touch is already captured by OSC. works only in regular native touch if(self->activateCoordSelector) [self updatePointerObjInDict:touch]; [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE]; [[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service) @@ -248,14 +255,18 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if(asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchEndQos, 0), ^{ for (UITouch* touch in touches){ + // continue to the next loop if current touch is already captured by OSC. works only in regular native touch [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId [self removePointerId:touch]; //then remove pointerId if(self->activateCoordSelector) [self removePointerObjFromDict:touch]; - [self->blacklistedTouches removeObject:@((uintptr_t)touch)]; + //[self->blacklistedTouches removeObject:@((uintptr_t)touch)]; } + self->doNotBlockTouchEvents = true; + //if(self->touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count]) }); else{ for (UITouch* touch in touches){ + // continue to the next loop if current touch is already captured by OSC. works only in regular native touch [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId [self removePointerId:touch]; //then remove pointerId if(self->activateCoordSelector) [self removePointerObjFromDict:touch]; @@ -293,7 +304,6 @@ - (CGPoint)selectCoordsFor:(UITouch *)touch{ NativeTouchPointer *pointer = [pointerObjDict objectForKey:@((uintptr_t)touch)]; if(pointer == nil) return CGPointMake(0, 0); - // NSLog(@"using relative Coords: intialX %f, divider loc %f", pointer.initialPoint.x, pointerVelocityDividerLocationByPoints); return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint; } diff --git a/VoidLink/Input/StreamView.h b/VoidLink/Input/StreamView.h index 6b7943c6..1fff07fc 100644 --- a/VoidLink/Input/StreamView.h +++ b/VoidLink/Input/StreamView.h @@ -22,7 +22,7 @@ - (void)userInteractionEnded; - (void)streamExitRequested; - (void)toggleStatsOverlay; -- (void)localMousePointerMode; +- (void)toggleMouseCapture; - (void)toggleMouseVisible; @end diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index caa1a341..ee85b678 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -828,7 +828,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; [interactionDelegate toggleStatsOverlay]; break; case UIKeyboardHIDUsageKeyboardM: - [interactionDelegate localMousePointerMode]; + [interactionDelegate toggleMouseCapture]; break; case UIKeyboardHIDUsageKeyboardC: [interactionDelegate toggleMouseVisible]; diff --git a/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents b/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents index 6170214e..bb37533b 100644 --- a/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents +++ b/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents @@ -55,7 +55,7 @@ - + diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index 07db9814..d5129bae 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -1,29 +1,6 @@ { "sourceLanguage" : "en", "strings" : { - "\n1. From %@ edge, slide %d%% screen width to open settings menu\n\n2. From %@ edge, slide %d%% screen width to open command tool\n\n3. Back to Streaming from settings menu: slide from left edge\n\n4. Slide from upper 40%% of screen edge to avoid sending touch events to PC side" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "\n1. 展开设置菜单: 从%@边缘滑动%d%%屏幕宽度\n\n2. 打开命令工具: 从%@边缘滑动%d%%屏幕宽度\n\n3. 返回串流 / 收起设置菜单: 从左边缘滑动\n\n4. 从40%%上半边缘滑动避免触控事件影响PC端" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "\n1. 展開設定選單:從%@邊緣滑動%d%%的螢幕寬度\n\n2. 開啟指令工具:從%@邊緣滑動%d%%的螢幕寬度\n\n3. 從設定選單返回串流:從左側邊緣滑動\n\n4. 從螢幕邊緣上方40%%處滑動以避免將觸控事件傳送到電腦端" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "\n1. 展開設定選單:從%@邊緣滑動%d%%的螢幕寬度\n\n2. 開啟指令工具:從%@邊緣滑動%d%%的螢幕寬度\n\n3. 從設定選單返回串流:從左側邊緣滑動\n\n4. 從螢幕邊緣上方40%%處滑動以避免將觸控事件傳送到電腦端" - } - } - } - }, "\nHost processing latency min/max/avg: %.1f/%.1f/%.1f ms" : { "extractionState" : "manual", "localizations" : { @@ -248,7 +225,7 @@ }, "zh-Hant" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "\n\n您裝置的遠端工作階段連線被阻擋。在此網路上連線時,串流可能無法運作。" } }, @@ -306,7 +283,7 @@ } } }, - "[ Open on-screen widget tool ]" : { + "[ On-screen widget tool ]" : { "extractionState" : "manual", "localizations" : { "zh-Hans" : { @@ -329,7 +306,7 @@ } } }, - "[ Switch among on-screen widget profiles ]" : { + "[ Switch on-screen widget profile ]" : { "extractionState" : "manual", "localizations" : { "zh-Hans" : { @@ -1474,6 +1451,64 @@ } } }, + "emulatedControllerTypeStackDoc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/uscwmv3vz8g30rmk?singleDoc#" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/bz6ggagp0ly4loq3?singleDoc#" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/bz6ggagp0ly4loq3?singleDoc#" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/bz6ggagp0ly4loq3?singleDoc#" + } + } + } + }, + "emulatedControllerTypeStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunshine emulates game controller via VigemBus, which support DS4 & Xbox360. \n\n- Xbox360 does not support gyro.\n\n- When “both” is selected, the app will map built-in gyro as player0 & external game controller as player1" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunshine 通过 VigemBus 模拟游戏手柄,支持 DS4 和 Xbox 360。\n\n- Xbox 360 不支持陀螺仪。\n\n- 当选择“同时映射”时,app会将内置陀螺仪映射为玩家0,外接手柄映射为玩家1。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunshine 透過 VigemBus 模擬遊戲控制器,支援 DS4 和 Xbox 360。\n\n- Xbox 360 不支援陀螺儀。\n\n- 當選擇「同時映射」時,app 會將內建陀螺儀對應為玩家0,外接控制器對應為玩家1。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunshine 透過 VigemBus 模擬遊戲控制器,支援 DS4 和 Xbox 360。\n\n- Xbox 360 不支援陀螺儀。\n\n- 當選擇「同時映射」時,app 會將內建陀螺儀對應為玩家0,外接控制器對應為玩家1。" + } + } + } + }, "Enter a new command and alias" : { "extractionState" : "manual", "localizations" : { @@ -1664,6 +1699,64 @@ } } }, + "externalDisplayModeStackDoc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/bu3hki4pezy93g6r?singleDoc# " + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/cqbse6t4o6f9hil1?singleDoc#" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/cqbse6t4o6f9hil1?singleDoc#" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/cqbse6t4o6f9hil1?singleDoc#" + } + } + } + }, + "externalDisplayModeStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select “Stage Manager” to enable full screen external display.\n\nKeyboard Shortcuts for VoidLink (Desktop Mode):\n\nCtrl + Alt + Shift + Q — Disconnect the current session\n\nCtrl + Alt + Shift + S — Toggle the statistics overlay style\n\nCtrl + Alt + Shift + M — Toggle mouse capture mode\n\nCtrl + Alt + Shift + C — Toggle mouse pointer visibility\n" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "VoidLink 桌面模式快捷键:\n\n- Ctrl + Alt + Shift + Q — 断开当前会话\n\n- Ctrl + Alt + Shift + S — 切换性能信息叠加样式\n\n- Ctrl + Alt + Shift + M — 切换鼠标指针捕获模式\n\n- Ctrl + Alt + Shift + C — 切换鼠标指针可见性" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "VoidLink 桌面模式快捷鍵:\n\nCtrl + Alt + Shift + Q — 中斷目前連線\n\nCtrl + Alt + Shift + S — 切換統計資訊覆蓋樣式\n\nCtrl + Alt + Shift + M — 切換滑鼠指標捕捉模式\n\nCtrl + Alt + Shift + C — 切換滑鼠指標可見性" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "VoidLink 桌面模式快捷鍵:\n\nCtrl + Alt + Shift + Q — 中斷目前連線\n\nCtrl + Alt + Shift + S — 切換統計資訊覆蓋樣式\n\nCtrl + Alt + Shift + M — 切換滑鼠指標捕捉模式\n\nCtrl + Alt + Shift + C — 切換滑鼠指標可見性" + } + } + } + }, "Failed to import profiles" : { "extractionState" : "manual", "localizations" : { @@ -1779,6 +1872,35 @@ } } }, + "firstLaunchTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "\n1. From %@ edge, slide %d%% screen width to open settings menu\n\n2. From %@ edge, slide %d%% screen width to open toolbox menu\n\n3. Slide from top 40%% of screen edge to avoid sending touch events to PC side" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "\n1. 展开设置菜单: 从%@边缘滑动%d%%屏幕宽度\n\n2. 打开工具箱菜单: 从%@边缘滑动%d%%屏幕宽度\n\n3. 从40%%上半边缘滑动避免触控事件影响PC端" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "\n1. 展開設定選單:從%@邊緣滑動%d%%的螢幕寬度\n\n2. 開啟工具箱選單:從%@邊緣滑動%d%%的螢幕寬度\n\n3. 從螢幕邊緣上方40%%處滑動以避免將觸控事件傳送到電腦端" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "\n1. 展開設定選單:從%@邊緣滑動%d%%的螢幕寬度\n\n2. 開啟工具箱選單:從%@邊緣滑動%d%%的螢幕寬度\n\n3. 從螢幕邊緣上方40%%處滑動以避免將觸控事件傳送到電腦端" + } + } + } + }, "FPS: %5.2f Network dropped frames: %.2f%% Network latency: %@" : { "extractionState" : "manual", "localizations" : { @@ -1894,6 +2016,93 @@ } } }, + "gyroModeStackDoc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/uscwmv3vz8g30rmk?singleDoc#" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/bz6ggagp0ly4loq3?singleDoc#" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/bz6ggagp0ly4loq3?singleDoc#" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/bz6ggagp0ly4loq3?singleDoc#" + } + } + } + }, + "gyroModeStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "- Gyro support for external game controllers is available on iOS 14+.\n\n- If a session is started with gyro mode disabled, it cannot be enabled during streaming." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "- 外接手柄的陀螺仪支持适用于 iOS 14 及以上版本。\n\n- 如果在启动会话时关闭了陀螺仪模式,则在串流过程中无法热切换开启。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "- 外接控制器的陀螺儀支援適用於 iOS 14 以上版本。\n\n- 若在啟動階段時未啟用陀螺儀模式,則無法在串流過程中熱切換開啟。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "- 外接控制器的陀螺儀支援適用於 iOS 14 以上版本。\n\n- 若在啟動階段時未啟用陀螺儀模式,則無法在串流過程中熱切換開啟。" + } + } + } + }, + "hdrStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "HDR video stream decoding is not supported on current device" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前系统不支持HDR视频流解码" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "當前裝置不支援HDR視訊串流解碼" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "當前裝置不支援HDR視訊串流解碼" + } + } + } + }, "Height" : { "extractionState" : "manual", "localizations" : { @@ -2239,6 +2448,64 @@ } } }, + "localMousePointerModeStackDoc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/zdf5l29s3vohs6iq?singleDoc#" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/yfao3s5bhl93rs01?singleDoc#" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/yfao3s5bhl93rs01?singleDoc#" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/yfao3s5bhl93rs01?singleDoc#" + } + } + } + }, + "localMousePointerModeStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "- Captured: mouse pointer captured & limited within app window\n\n- Hidden: local mouse pointer is hidden.\n\n- Visible: local mouse pointer is visible to reduce the feeling of latency\n\nDo not select hidden/visible for any game play that uses mouse for camera control." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "- 被窗口捕获:鼠标指针被限制在app窗口内。\n\n- 隐藏:不显示本地鼠标指针。\n\n- 可见:显示iOS本地鼠标指针,减少延迟感。\n\n请勿使用 隐藏/可见 模式操作任何使用鼠标控制视角的游戏。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "- 被視窗捕獲:滑鼠游標被限制在應用程式視窗內。\n\n- 隱藏:不顯示本機滑鼠游標。\n\n- 可見:顯示 iOS 本機滑鼠游標,減少延遲感。\n\n​​請勿使用「隱藏/可見」模式操作任何需用滑鼠控制視角的遊戲。​​" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "- 被視窗捕獲:滑鼠游標被限制在應用程式視窗內。\n\n- 隱藏:不顯示本機滑鼠游標。\n\n- 可見:顯示 iOS 本機滑鼠游標,減少延遲感。\n\n​​請勿使用「隱藏/可見」模式操作任何需用滑鼠控制視角的遊戲。​​" + } + } + } + }, "Manual_Add_Host_Fail" : { "extractionState" : "manual", "localizations" : { @@ -2302,7 +2569,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "无法执行网络测试. 请检查网络连接后重试." } }, @@ -2429,7 +2696,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "网络测试成功. 如果仍然无法连接远程主机, 请检查主机端防火墙设置." } }, @@ -2769,6 +3036,81 @@ } } }, + "Online Documentation" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在线支持文档" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "線上支援文件" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "線上支援文件" + } + } + } + }, + "onScreenWidgetStackDoc" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/zrg0ga93avz4s747?singleDoc# " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/zrg0ga93avz4s747?singleDoc# " + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/zrg0ga93avz4s747?singleDoc# " + } + } + } + }, + "onScreenWidgetStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "- VoidLink supports rich, powerful custom on screen widgets including keyboard buttons, gamepad button & joysticks, mouse touchpad & button. Check online document for detail.\n\n- Multi-finger tap gesture is available for launching widget layout tool during streaming." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "- VoidLink具备丰富而强大的自定义屏显控件,类型包括:键盘按键、手柄按键和摇杆、鼠标触摸板和按键等。详见在线文档。 \n\n- 可通过多指轻按手势在串流时打开控件布局工具。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "- VoidLink 提供豐富且強大的自訂螢幕顯示元件,類型包含:鍵盤按鍵、手把按鍵與搖桿、滑鼠觸控板及按鍵等。詳細資訊請參考線上說明文件。\n\n- 可透過多指輕點手勢,在串流時開啟控制元件佈局工具。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "- VoidLink 提供豐富且強大的自訂螢幕顯示元件,類型包含:鍵盤按鍵、手把按鍵與搖桿、滑鼠觸控板及按鍵等。詳細資訊請參考線上說明文件。\n\n- 可透過多指輕點手勢,在串流時開啟控制元件佈局工具。" + } + } + } + }, "Open widget tool in streaming by:\nSliding from %@ screen edge to open cmd tool.\nOr tap %d fingers on stream view, number of fingers required:" : { "extractionState" : "manual", "localizations" : { @@ -2792,6 +3134,35 @@ } } }, + "optimizeGamesStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recommended to keep this option enabled. Sunshine relies on it to load preset parameters." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunshine 依据该选项状态决定是否加载一些预置参数,建议保持开启。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunshine 依據此選項的狀態決定是否載入一些預設參數,建議保持開啟。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunshine 依據此選項的狀態決定是否載入一些預設參數,建議保持開啟。" + } + } + } + }, "Options" : { "extractionState" : "manual", "localizations" : { @@ -2907,6 +3278,35 @@ } } }, + "pipStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "- Enable auto picture-in-picture by switching on Settings -> Multitasking & Gestures -> Start PiP Automatically.\n\n- PiP can be manually started in toolbox menu.\n\n- PiP connection timeout is also defined by “Background Session” settings in “Others” section\n\n-PiP is available for iOS15+" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "- App切后台时自动开启画中画, 需要打开 iOS 设置-> 多任务与手势 -> 自动开启画中画。\n\n- 画中画可在工具箱菜单中手动开启。\n\n- 画中画时长由 “其他 -> 后台会话” 设置项控制\n\n- 仅 iOS15+ 支持画中画。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "- App 切到背景時會自動啟用子母畫面,需開啟 iOS 「設定」→「多工與手勢」→「自動啟用子母畫面」。\n\n- 子母畫面也可透過工具箱選單手動啟用。\n\n- 子母畫面顯示時長由「其他」→「背景執行」設定項目控制。\n\n- 子母畫面功能自 iOS 15 起支援。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "- App 切到背景時會自動啟用子母畫面,需開啟 iOS 「設定」→「多工與手勢」→「自動啟用子母畫面」。\n\n- 子母畫面也可透過工具箱選單手動啟用。\n\n- 子母畫面顯示時長由「其他」→「背景執行」設定項目控制。\n\n- 子母畫面功能自 iOS 15 起支援。" + } + } + } + }, "Pofiles imported" : { "extractionState" : "manual", "localizations" : { @@ -3782,6 +4182,35 @@ } } }, + "slideToSettingsDistanceStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A sufficient sliding distance ensures the menu is not triggered by unintentional touches." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "手势所要求的滑动距离, 可有效防止因误触展开菜单。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "手勢所需的滑動距離,可有效避免因誤觸而展開選單。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "手勢所需的滑動距離,可有效避免因誤觸而展開選單。" + } + } + } + }, "Slow connection to PC, Reduce your bitrate" : { "extractionState" : "manual", "localizations" : { @@ -3805,6 +4234,64 @@ } } }, + "softKeyboardGestureStackDoc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/rgc9if4xrf352555?singleDoc#" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/ok4egvu4lt37wc1f?singleDoc#" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/ok4egvu4lt37wc1f?singleDoc#" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/ok4egvu4lt37wc1f?singleDoc#" + } + } + } + }, + "softKeyboardGestureStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Soft keyboard gesture is able to locate the remote typing area by the lowest touch point of your tap gesture to lift the stream view to proper height.\nCheck the online document for details." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "软键盘手势能够根据位置最低的触点定位远程文字输入位置,并将串流视图提升到合适高度。\n详见在线文档。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "軟體鍵盤手勢可根據最低位置的觸控點,定位遠端文字輸入的位置,並將串流畫面提升至適當高度。\n詳情請參閱線上說明文件。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "軟體鍵盤手勢可根據最低位置的觸控點,定位遠端文字輸入的位置,並將串流畫面提升至適當高度。\n詳情請參閱線上說明文件。" + } + } + } + }, "Stereo (surround sound available for iOS18+)" : { "extractionState" : "manual", "localizations" : { @@ -3851,6 +4338,35 @@ } } }, + "supportLink" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/dhmg3h27ls8mwt4a?singleDoc# 《About VoidLink》" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/fdi4rxkxykhl5zpp?singleDoc# 《关于 VoidLink》" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/fdi4rxkxykhl5zpp?singleDoc# 《关于 VoidLink》" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/fdi4rxkxykhl5zpp?singleDoc# 《关于 VoidLink》" + } + } + } + }, "Tap %d Fingers to Change OSC Layout in Stream View" : { "extractionState" : "manual", "localizations" : { @@ -4266,13 +4782,42 @@ "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "- 觸控板:拖曳方式與 Windows 筆記型電腦觸控板完全相同。快速輕點兩下,從第二次輕點開始進入拖曳模式。\n\n- 原生觸控:即多點觸控傳遞,macOS 不支援。\n\n- 絕對觸控:在 macOS 上提供類似觸控螢幕的體驗。" + "value" : "- 觸控板:拖曳方式與 Windows 筆記型電腦觸控板完全相同。快速輕點兩下,從第二次輕點開始進入拖曳模式。\n\n- 多点觸控:macOS 不支援。\n\n- 單點滑鼠:在 macOS 上提供類似觸控螢幕的體驗。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "- 觸控板:拖曳方式與 Windows 筆記型電腦觸控板完全相同。快速輕點兩下,從第二次輕點開始進入拖曳模式。\n\n- 原生觸控:即多點觸控傳遞,macOS 不支援。\n\n- 絕對觸控:在 macOS 上提供類似觸控螢幕的體驗。" + "value" : "- 觸控板:拖曳方式與 Windows 筆記型電腦觸控板完全相同。快速輕點兩下,從第二次輕點開始進入拖曳模式。\n\n- 多点觸控:macOS 不支援。\n\n- 單點滑鼠:在 macOS 上提供類似觸控螢幕的體驗。" + } + } + } + }, + "unlockDisplayOrientationStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "iPad is unable to lock run-time display orientation due to stage manager support. If necessary please manually lock display orientation." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "为支持台前调度,iPad无法自动锁定显示方向。如有必要请手动锁定。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "為支援幕前調度,iPad 無法自動鎖定顯示方向。如有需要,請手動鎖定。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "為支援幕前調度,iPad 無法自動鎖定顯示方向。如有需要,請手動鎖定。" } } } diff --git a/VoidLink/PresetConfigs/widgetTemplates.bin b/VoidLink/PresetConfigs/widgetTemplates.bin index 4ca211d1..5d091aa2 100755 Binary files a/VoidLink/PresetConfigs/widgetTemplates.bin and b/VoidLink/PresetConfigs/widgetTemplates.bin differ diff --git a/VoidLink/UIAppView.m b/VoidLink/UIAppView.m index abe6d208..b02538b8 100644 --- a/VoidLink/UIAppView.m +++ b/VoidLink/UIAppView.m @@ -79,8 +79,8 @@ static UIImage* noImage; #else // Rasterizing the cell layer increases rendering performance by quite a bit // but we want it unrasterized for tvOS where it must be scaled. - self.layer.shouldRasterize = YES; - self.layer.rasterizationScale = [UIScreen mainScreen].scale; + self.layer.shouldRasterize = NO; + // self.layer.rasterizationScale = [UIScreen mainScreen].scale; if (@available(iOS 13.4.1, *)) { // Allow the button style to change when moused over diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m index 1e881a1c..fd720ff9 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m @@ -81,9 +81,8 @@ - (void) reloadOnScreenWidgetViews { NSLog(@"reloadOnScreenWidgets %f", CACurrentMediaTime()); OnScreenWidgetView.editMode = true; - [self->selectedWidgetView.stickBallLayer removeFromSuperlayer]; - [self->selectedWidgetView.crossMarkLayer removeFromSuperlayer]; - + [self clearStickIndicator]; + for (UIView *subview in self.view.subviews) { if ([subview isKindOfClass:[OnScreenWidgetView class]]) { [subview removeFromSuperview]; @@ -168,7 +167,7 @@ [profilesManager saveProfileWithName:@"Default" andButtonLayers:self.layoutOSC.OSCButtonLayers]; [profilesManager importDefaultTemplates]; }*/ - if (![profilesManager profileName:DEFAULT_TEMPLATE_NAME alreadyExistIn:allProfiles]){ + if (![profilesManager findProfileByName:DEFAULT_TEMPLATE_NAME1 inProfileArray:allProfiles]){ [profilesManager importDefaultTemplates]; } @@ -239,6 +238,11 @@ object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(OSCLayoutChanged) name:@"OSCLayoutChanged" object:nil]; // used to notifiy this view controller that the user made a change to the OSC layout so that the VC can either fade in or out its 'Undo button' which will signify to the user whether there are any OSC layout changes to undo + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillResignActive:) + name:UIApplicationWillResignActiveNotification + object:nil]; OnScreenWidgetView.editMode = true; [self handleMissingToolBarIcon:toolbarRootView]; @@ -247,6 +251,11 @@ #pragma mark - Class Helper Functions +- (void)applicationWillResignActive:(NSNotification *)notification { + [self saveTapped:nil]; +} + + /* fades the 'Undo Button' in or out depending on whether the user has any OSC layout changes to undo */ - (void) OSCLayoutChanged { if ([self.layoutOSC.layoutChanges count] > 0) { @@ -617,7 +626,7 @@ [savedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [self.oscProfilesTableViewController profileViewRefresh]; // execute this will reset layout in OSC tool! }]]; - [self presentViewController:savedAlertController animated:YES completion:nil]; + if(sender) [self presentViewController:savedAlertController animated:YES completion:nil]; } } @@ -645,6 +654,11 @@ label.numberOfLines = 1; } +- (void)clearStickIndicator{ + [self->selectedWidgetView.stickBallLayer removeFromSuperlayer]; + [self->selectedWidgetView.crossMarkLayer removeFromSuperlayer]; +} + - (void)widgetViewTapped: (NSNotification *)notification{ //self.undoButton.alpha = selectedWidgetView.layoutChanges.count>1 && !CGPointEqualToPoint(selectedWidgetView.layoutChanges.lastObject.CGPointValue, selectedWidgetView.initialCenter)? 1.0 : 0.3; @@ -653,8 +667,7 @@ OnScreenWidgetView* widgetView = (OnScreenWidgetView* )notification.object; - [self->selectedWidgetView.stickBallLayer removeFromSuperlayer]; - [self->selectedWidgetView.crossMarkLayer removeFromSuperlayer]; + [self clearStickIndicator]; self->widgetViewSelected = true; self->controllerLayerSelected = false; self->selectedWidgetView = widgetView; @@ -695,8 +708,7 @@ } if(showStickIndicatorOffsetStack){ // illustrating the indicator offset, - [selectedWidgetView.stickBallLayer removeFromSuperlayer]; - [selectedWidgetView.crossMarkLayer removeFromSuperlayer]; + [self clearStickIndicator]; selectedWidgetView.touchBeganLocation = CGPointMake(CGRectGetWidth(selectedWidgetView.frame)/2, CGRectGetHeight(selectedWidgetView.frame)/4); [selectedWidgetView showStickIndicator];// this will create the indicator CAShapeLayers [self.stickIndicatorOffsetSlider setValue:self->selectedWidgetView.stickIndicatorOffset]; @@ -733,8 +745,7 @@ - (void)legacyOscLayerTapped: (NSNotification *)notification{ [self enableCommonWidgetTools]; CALayer* controllerLayer = (CALayer* )notification.object; - [self->selectedWidgetView.stickBallLayer removeFromSuperlayer]; - [self->selectedWidgetView.crossMarkLayer removeFromSuperlayer]; + [self clearStickIndicator]; self->widgetViewSelected = false; self->selectedWidgetView = nil; @@ -1109,6 +1120,9 @@ } - (void) presentProfilesTableView{ + [self saveTapped:nil]; + [self clearStickIndicator]; + selectedWidgetView = nil; UIStoryboard *storyboard; BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone); if (isIPhone) { @@ -1133,8 +1147,6 @@ self.widgetPanelStack.hidden = YES; - [self->selectedWidgetView.stickBallLayer removeFromSuperlayer]; - [self->selectedWidgetView.crossMarkLayer removeFromSuperlayer]; _oscProfilesTableViewController.currentOSCButtonLayers = self.layoutOSC.OSCButtonLayers; [self presentViewController:_oscProfilesTableViewController animated:YES completion:nil]; @@ -1241,8 +1253,7 @@ if(!isToolbarHidden && self->selectedWidgetView != nil && [self layerIsOverlappingWithTrashcanButton:selectedWidgetView.layer]){ [self->selectedWidgetView removeFromSuperview]; [self.OnScreenWidgetViews removeObject:self->selectedWidgetView]; - [selectedWidgetView.stickBallLayer removeFromSuperlayer]; - [selectedWidgetView.crossMarkLayer removeFromSuperlayer]; + [self clearStickIndicator]; [selectedWidgetView.buttonDownVisualEffectLayer removeFromSuperlayer]; } diff --git a/VoidLink/ViewControllers/MainFrameViewController.m b/VoidLink/ViewControllers/MainFrameViewController.m index 36f84212..492fbc67 100644 --- a/VoidLink/ViewControllers/MainFrameViewController.m +++ b/VoidLink/ViewControllers/MainFrameViewController.m @@ -1133,8 +1133,8 @@ static NSMutableSet* hostList; if(self.settingsExpandedInStreamView) [revealController buttonsInStreaming]; else [revealController buttonsNotInStreaming]; - DataManager* dataMan = [[DataManager alloc] init]; - TemporarySettings* currentSettings = [dataMan getSettings]; + // DataManager* dataMan = [[DataManager alloc] init]; + // TemporarySettings* currentSettings = [dataMan getSettings]; [streamFrameViewController setUserInteractionEnabledForStreamView:!_settingsExpandedInStreamView || position == FrontViewPositionLeft]; [settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.resolutionStack]; @@ -1269,6 +1269,19 @@ static NSMutableSet* hostList; } } +- (BOOL)isFirstLaunch { + NSString *key = @"appHasLaunchedBefore"; + BOOL launchedBefore = [[NSUserDefaults standardUserDefaults] boolForKey:key]; + + if (!launchedBefore) { + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:key]; + [[NSUserDefaults standardUserDefaults] synchronize]; // iOS 12+ 可省略 + return YES; + } + return NO; +} + + - (bool)isIPhone{ return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone); } @@ -1347,7 +1360,7 @@ static NSMutableSet* hostList; button.tintColor = [ThemeManager appPrimaryColor]; [button setTitleColor:button.tintColor forState:UIControlStateNormal]; - button.frame = CGRectMake(0, 0, buttonHeight, buttonHeight); + button.frame = CGRectMake(0, 0, buttonHeight*1.3, buttonHeight*1.05); // 添加点击事件 [button addTarget:self action:@selector(helpButtonTapped) forControlEvents:UIControlEventTouchUpInside]; @@ -1774,6 +1787,7 @@ static NSMutableSet* hostList; //[self simulateSettingsButtonPress]; //force reload resolution table in the setting //[self simulateSettingsButtonPress]; [self updateResolutionAccordingly]; + if([self isFirstLaunch])[self helpButtonTapped]; } @@ -2114,7 +2128,7 @@ static NSMutableSet* hostList; if(gesture.state == UIGestureRecognizerStateBegan){ if(locationInView.x < 30) { snapshot = [[UIView alloc] init]; - snapshot.backgroundColor = [ThemeManager appPrimaryColor]; + snapshot.backgroundColor = [UIColor systemBlueColor]; screenHeight = [UIScreen mainScreen].bounds.size.height; snapshot.frame = CGRectMake(locationInSuperView.x,0, 2, screenHeight); [self.revealViewController.view addSubview:snapshot]; diff --git a/VoidLink/ViewControllers/SettingsViewController.h b/VoidLink/ViewControllers/SettingsViewController.h index 248f182f..49754f21 100644 --- a/VoidLink/ViewControllers/SettingsViewController.h +++ b/VoidLink/ViewControllers/SettingsViewController.h @@ -48,7 +48,7 @@ @property (strong, nonatomic) IBOutlet UIStackView *codecStack; @property (strong, nonatomic) IBOutlet UIStackView *yuv444Stack; @property (strong, nonatomic) IBOutlet UIStackView *pipStack; -@property (strong, nonatomic) IBOutlet UIStackView *HdrStack; +@property (strong, nonatomic) IBOutlet UIStackView *hdrStack; @property (strong, nonatomic) IBOutlet UIStackView *framepacingStack; @property (strong, nonatomic) IBOutlet UIStackView *reverseMouseWheelDirectionStack; @property (strong, nonatomic) IBOutlet UIStackView *citrixX1MouseStack; @@ -136,7 +136,7 @@ - (void)saveSettings; + (bool)isLandscapeNow; - (void)updateResolutionTable; -- (void)widget:(UISlider*)widget setEnabled:(bool)enabled; +- (void)widget:(UIView*)widget setEnabled:(bool)enabled; - (void)updateTheme; - (void)hideDynamicLabelsWhenOverlapped:(UIView* )view; - (void)setHidden:(BOOL)hidden forStack:(UIStackView* )stack; diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index 90e68dbd..dae84618 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -516,14 +516,15 @@ BOOL isCustomResolution(CGSize res) { if (@available(iOS 13.0, *)) { [videoSection setSectionWithIcon:[UIImage systemImageNamed:@"waveform"] andSize:20]; } - [self addSetting:self.resolutionStack ofId:@"resolutionStack" withInfoTag:YES withDynamicLabel:YES to:videoSection]; + [self addSetting:self.resolutionStack ofId:@"resolutionStack" withInfoTag:NO withDynamicLabel:YES to:videoSection]; [self addSetting:self.fpsStack ofId:@"fpsStack" withInfoTag:NO withDynamicLabel:NO to:videoSection]; [self addSetting:self.bitrateStack ofId:@"bitrateStack" withInfoTag:YES withDynamicLabel:YES to:videoSection]; [self addSetting:self.framepacingStack ofId:@"framepacingStack" withInfoTag:NO withDynamicLabel:NO to:videoSection]; [self addSetting:self.codecStack ofId:@"codecStack" withInfoTag:NO withDynamicLabel:NO to:videoSection]; - [self addSetting:self.HdrStack ofId:@"HdrStack" withInfoTag:NO withDynamicLabel:NO to:videoSection]; + [self addSetting:self.hdrStack ofId:@"hdrStack" withInfoTag:![self hdrSupported] withDynamicLabel:NO to:videoSection]; [self addSetting:self.yuv444Stack ofId:@"yuv444Stack" withInfoTag:YES withDynamicLabel:NO to:videoSection]; [self addSetting:self.pipStack ofId:@"pipStack" withInfoTag:YES withDynamicLabel:NO to:videoSection]; + [self addSetting:self.pipStack ofId:@"pipStack" withInfoTag:YES withDynamicLabel:NO to:videoSection]; [self addSetting:self.frameQueueSizeStack ofId:@"frameQueueSizeStack" withInfoTag:NO withDynamicLabel:NO to:videoSection]; [self addSetting:self.renderingBackendStack ofId:@"renderingBackendStack" withInfoTag:YES withDynamicLabel:NO to:videoSection]; @@ -600,10 +601,9 @@ BOOL isCustomResolution(CGSize res) { [self addSetting:self.unlockDisplayOrientationStack ofId:@"unlockDisplayOrientationStack" withInfoTag:YES withDynamicLabel:NO to:otherSection]; [self addSetting:self.backgroundSessionTimerStack ofId:@"backgroundSessionTimerStack" withInfoTag:NO withDynamicLabel:YES to:otherSection]; [self addSetting:self.optimizeGamesStack ofId:@"optimizeGamesStack" withInfoTag:YES withDynamicLabel:NO to:otherSection]; - [self addSetting:self.multiControllerStack ofId:@"multiControllerStack" withInfoTag:YES withDynamicLabel:NO to:otherSection]; - [self addSetting:self.softKeyboardToolbarStack ofId:@"softKeyboardToolbarStack" withInfoTag:YES withDynamicLabel:NO to:otherSection]; + [self addSetting:self.multiControllerStack ofId:@"multiControllerStack" withInfoTag:NO withDynamicLabel:NO to:otherSection]; + [self addSetting:self.softKeyboardToolbarStack ofId:@"softKeyboardToolbarStack" withInfoTag:NO withDynamicLabel:NO to:otherSection]; [self addSetting:self.performanceGraphStack ofId:@"performanceGraphStack" withInfoTag:YES withDynamicLabel:NO to:otherSection]; - [otherSection addToParentStack:_parentStack]; [otherSection setExpanded:YES]; @@ -614,7 +614,7 @@ BOOL isCustomResolution(CGSize res) { if (@available(iOS 13.0, *)) { [experimentalSection setSectionWithIcon:[UIImage imageNamed:@"flask"] andSize:20]; } - [self addSetting:self.touchMoveEventIntervalStack ofId:@"touchMoveEventIntervalStack" withInfoTag:YES withDynamicLabel:YES to:experimentalSection]; + [self addSetting:self.touchMoveEventIntervalStack ofId:@"touchMoveEventIntervalStack" withInfoTag:NO withDynamicLabel:YES to:experimentalSection]; [experimentalSection addToParentStack:_parentStack]; [experimentalSection setExpanded:YES]; } @@ -950,13 +950,14 @@ BOOL isCustomResolution(CGSize res) { [stack addSubview:button]; [NSLayoutConstraint activateConstraints:@[ [button.trailingAnchor constraintEqualToAnchor:stack.trailingAnchor constant:-4], - [button.bottomAnchor constraintEqualToAnchor:stack.arrangedSubviews[0].bottomAnchor constant:0], + [button.centerYAnchor constraintEqualToAnchor:stack.arrangedSubviews[0].centerYAnchor constant:0], ]]; } - (void)infoButtonTapped:(UIButton* )sender{ NSString* tipText = @""; + NSString* onlineDocLink = @""; bool showOnlineDocAction = false; tipText = sender.superview.accessibilityIdentifier; if([sender.superview.accessibilityIdentifier isEqualToString: @"bitrateStack"]){ @@ -966,6 +967,7 @@ BOOL isCustomResolution(CGSize res) { if([sender.superview.accessibilityIdentifier isEqualToString: @"yuv444Stack"]){ tipText = [LocalizationHelper localizedStringForKey:@"yuv444StackTip"]; showOnlineDocAction = true; + onlineDocLink = @"https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/koeimmrvt4o17auc?singleDoc#"; } if([sender.superview.accessibilityIdentifier isEqualToString: @"touchModeStack"]){ tipText = [LocalizationHelper localizedStringForKey:@"touchModeStackTip"]; @@ -979,10 +981,58 @@ BOOL isCustomResolution(CGSize res) { tipText = [LocalizationHelper localizedStringForKey:@"pointerVelocityFactorStackTip"]; showOnlineDocAction = true; } + if([sender.superview.accessibilityIdentifier isEqualToString: @"hdrStack"]){ + tipText = [LocalizationHelper localizedStringForKey:@"hdrStackTip"]; + showOnlineDocAction = false; + } + if([sender.superview.accessibilityIdentifier isEqualToString: @"pipStack"]){ + tipText = [LocalizationHelper localizedStringForKey:@"pipStackTip"]; + showOnlineDocAction = false; + } + if([sender.superview.accessibilityIdentifier isEqualToString: @"softKeyboardGestureStack" ]){ + tipText = [LocalizationHelper localizedStringForKey:@"softKeyboardGestureStackTip"]; + showOnlineDocAction = true; + onlineDocLink = [LocalizationHelper localizedStringForKey:@"softKeyboardGestureStackDoc"]; + } + if([sender.superview.accessibilityIdentifier isEqualToString: @"slideToSettingsDistanceStack" ]){ + tipText = [LocalizationHelper localizedStringForKey:@"slideToSettingsDistanceStackTip"]; + showOnlineDocAction = false; + } + if([sender.superview.accessibilityIdentifier isEqualToString: @"unlockDisplayOrientationStack" ]){ + tipText = [LocalizationHelper localizedStringForKey:@"unlockDisplayOrientationStackTip"]; + showOnlineDocAction = false; + } + if([sender.superview.accessibilityIdentifier isEqualToString: @"optimizeGamesStack" ]){ + tipText = [LocalizationHelper localizedStringForKey:@"optimizeGamesStackTip"]; + showOnlineDocAction = false; + } + if([sender.superview.accessibilityIdentifier isEqualToString: @"localMousePointerModeStack"]){ + tipText = [LocalizationHelper localizedStringForKey:@"localMousePointerModeStackTip"]; + showOnlineDocAction = true; + onlineDocLink = [LocalizationHelper localizedStringForKey:@"localMousePointerModeStackDoc"]; + } + if([sender.superview.accessibilityIdentifier isEqualToString: @"onScreenWidgetStack"]){ + tipText = [LocalizationHelper localizedStringForKey:@"onScreenWidgetStackTip"]; + showOnlineDocAction = true; + onlineDocLink = [LocalizationHelper localizedStringForKey:@"onScreenWidgetStackDoc"]; + } + if([sender.superview.accessibilityIdentifier isEqualToString: @"externalDisplayModeStack"]){ + tipText = [LocalizationHelper localizedStringForKey:@"externalDisplayModeStackTip"]; + showOnlineDocAction = true; + onlineDocLink = [LocalizationHelper localizedStringForKey:@"externalDisplayModeStackDoc"]; + } + if([sender.superview.accessibilityIdentifier isEqualToString: @"emulatedControllerTypeStack"]){ + tipText = [LocalizationHelper localizedStringForKey:@"emulatedControllerTypeStackTip"]; + showOnlineDocAction = true; + onlineDocLink = [LocalizationHelper localizedStringForKey:@"emulatedControllerTypeStackDoc"]; + } + if([sender.superview.accessibilityIdentifier isEqualToString: @"gyroModeStack"]){ + tipText = [LocalizationHelper localizedStringForKey:@"gyroModeStackTip"]; + showOnlineDocAction = true; + onlineDocLink = [LocalizationHelper localizedStringForKey:@"gyroModeStackDoc"]; + } - - UIAlertController *tipsAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@"Tips"] message: [LocalizationHelper localizedStringForKey:@"%@", tipText] preferredStyle:UIAlertControllerStyleAlert]; @@ -1005,7 +1055,7 @@ BOOL isCustomResolution(CGSize res) { UIAlertAction *readInstruction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Online Documentation"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ - NSURL *url = [NSURL URLWithString:@"https://b23.tv/J8qEXOr"]; + NSURL *url = [NSURL URLWithString:onlineDocLink]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; } @@ -1280,9 +1330,9 @@ BOOL isCustomResolution(CGSize res) { break; } - if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) || !(AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10)) { + if (![self hdrSupported]) { [self.hdrSwitch setOn:NO]; - [self.hdrSwitch setEnabled:NO]; + [self widget:self.hdrSwitch setEnabled:NO]; } else { [self.hdrSwitch setOn:currentSettings.enableHdr]; @@ -1324,6 +1374,7 @@ BOOL isCustomResolution(CGSize res) { [self emulatedControllerTypeChanged:self.emulatedControllerTypeSelector]; + [self.audioOnPcSwitch setOn:currentSettings.playAudioOnPC]; _lastSelectedResolutionIndex = resolution; [self.resolutionSelector setSelectedSegmentIndex:resolution]; @@ -1544,7 +1595,7 @@ BOOL isCustomResolution(CGSize res) { [_softKeyboardGestureSelector setSelectedSegmentIndex:_softKeyboardGestureSelector.selectedSegmentIndex-1]; } for (NSInteger i = 0; i < _softKeyboardGestureSelector.numberOfSegments; i++) { - [_softKeyboardGestureSelector setEnabled:![self isCustomOswEnabled] ? true : i+3 != oswLayoutFingers forSegmentAtIndex:i]; // 或 NO 来禁用 + [_softKeyboardGestureSelector setEnabled:![self isCustomOswEnabled] || i == 3 ? true : i+3 != oswLayoutFingers forSegmentAtIndex:i]; // 或 NO 来禁用 } } @@ -1643,13 +1694,21 @@ BOOL isCustomResolution(CGSize res) { return 0; } -- (void) widget:(UISlider*)widget setEnabled:(bool)enabled{ - [widget setEnabled:enabled]; - if(enabled){ - widget.alpha = 1.0; - [widget setValue:widget.value + 0.0001]; // this is for low iOS version (like iOS14), only setting this minor value change is able to make widget visibility clear +- (void) widget:(UIView*)widget setEnabled:(bool)enabled{ + if([widget isKindOfClass:[UISlider class]]){ + UISlider* widgetPtr = (UISlider* )widget; + [widgetPtr setEnabled:enabled]; + if(enabled){ + widgetPtr.alpha = 1.0; + [widgetPtr setValue:widgetPtr.value + 0.0001]; // this is for low iOS version (like iOS14), only setting this minor value change is able to make widget visibility clear + } + else widgetPtr.alpha = 0.5; // this is for updating widget visibility on low iOS version like mini5 ios14 + } + + if([widget isKindOfClass:[UISwitch class]]){ + widget.userInteractionEnabled = enabled; + widget.alpha = enabled ? 1 : 0.5; } - else widget.alpha = 0.5; // this is for updating widget visibility on low iOS version like mini5 ios14 } /* @@ -1891,6 +1950,10 @@ BOOL isCustomResolution(CGSize res) { [self updateBitrateText]; } +- (bool)hdrSupported{ + return VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) && (AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10); +} + - (void) updateBitrateText { // Display bitrate in Mbps UILabel* label = [self findDynamicLabelFromStack:self.bitrateStack]; diff --git a/VoidLink/ViewControllers/StreamFrameViewController.m b/VoidLink/ViewControllers/StreamFrameViewController.m index 669b6d0e..f4b818a9 100644 --- a/VoidLink/ViewControllers/StreamFrameViewController.m +++ b/VoidLink/ViewControllers/StreamFrameViewController.m @@ -160,11 +160,11 @@ } } -- (BOOL)isFirstLaunch { - NSString *key = @"hasLaunchedBefore"; - BOOL launchedBefore = [[NSUserDefaults standardUserDefaults] boolForKey:key]; +- (BOOL)isFirstStreaming { + NSString *key = @"hasStreamedBefore"; + BOOL streamedBefore = [[NSUserDefaults standardUserDefaults] boolForKey:key]; - if (!launchedBefore) { + if (!streamedBefore) { [[NSUserDefaults standardUserDefaults] setBool:YES forKey:key]; [[NSUserDefaults standardUserDefaults] synchronize]; // iOS 12+ 可省略 return YES; @@ -267,7 +267,7 @@ } } -- (void)presentCommandManagerViewController{ +- (void)presentToolboxViewController{ ToolboxViewController* oldToolboxVC = toolBoxViewController; toolBoxViewController = [[ToolboxViewController alloc] init]; toolBoxViewController.specialEntryDelegate = self; @@ -287,7 +287,7 @@ [self.view addGestureRecognizer:_slideToSettingsRecognizer]; - _slideToCmdToolRecognizer = [[CustomEdgeSlideGestureRecognizer alloc] initWithTarget:self action:@selector(presentCommandManagerViewController)]; + _slideToCmdToolRecognizer = [[CustomEdgeSlideGestureRecognizer alloc] initWithTarget:self action:@selector(presentToolboxViewController)]; if(_settings.slideToSettingsScreenEdge.intValue == UIRectEdgeLeft) _slideToCmdToolRecognizer.edges = UIRectEdgeRight; else _slideToCmdToolRecognizer.edges = UIRectEdgeLeft; // _commandManager triggered by sliding from another side. _slideToCmdToolRecognizer.normalizedThresholdDistance = _settings.slideToSettingsDistance.floatValue; @@ -476,8 +476,7 @@ } #endif -- (void)popFirstLaunchTip { - return; // temp disable +- (void)popFirstStreamingTip { // 初始化倒计时秒数 __block NSInteger remainingSeconds = 16; @@ -485,9 +484,25 @@ NSString* cmdToolEdgeSide = _settings.slideToSettingsScreenEdge.intValue == UIRectEdgeLeft ? [LocalizationHelper localizedStringForKey:@"right"] : [LocalizationHelper localizedStringForKey:@"left"]; uint8_t slideDist = (uint8_t)(_settings.slideToSettingsDistance.floatValue * 100); // 创建弹窗 - UIAlertController *alert = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"First Launch Tips"] - message:[LocalizationHelper localizedStringForKey:@"\n1. From %@ edge, slide %d%% screen width to open settings menu\n\n2. From %@ edge, slide %d%% screen width to open command tool\n\n3. Back to Streaming from settings menu: slide from left edge\n\n4. Slide from upper 40%% of screen edge to avoid sending touch events to PC side", settingsEdgeSide, slideDist, cmdToolEdgeSide, slideDist] - preferredStyle:UIAlertControllerStyleAlert]; + + NSString* tipText = [LocalizationHelper localizedStringForKey:@"firstLaunchTip", settingsEdgeSide, slideDist, cmdToolEdgeSide, slideDist]; + + UIAlertController *tipsAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@"First Launch Tips"] message: [LocalizationHelper localizedStringForKey:@"%@", tipText] preferredStyle:UIAlertControllerStyleAlert]; + + + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.alignment = NSTextAlignmentLeft; + + NSDictionary *attributes = @{ + NSParagraphStyleAttributeName: paragraphStyle, + NSFontAttributeName: [UIFont systemFontOfSize:14] + }; + + NSAttributedString *attributedMessage = [[NSAttributedString alloc] initWithString:tipText + attributes:attributes]; + + // 使用 KVC 设置 attributedMessage(注意审核风险) + [tipsAlertController setValue:attributedMessage forKey:@"attributedMessage"]; // 添加确认按钮(初始禁用) UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Got it! (15)"] @@ -495,10 +510,10 @@ handler:^(UIAlertAction * _Nonnull action) { }]; confirmAction.enabled = NO; - [alert addAction:confirmAction]; + [tipsAlertController addAction:confirmAction]; // 显示弹窗 - [self presentViewController:alert animated:YES completion:nil]; + [self presentViewController:tipsAlertController animated:YES completion:nil]; // 使用dispatch_source_t实现精确倒计时 dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); @@ -577,7 +592,7 @@ //[_streamView setupStreamView:_controllerSupport interactionDelegate:self config:self.streamConfig]; [self reConfigStreamViewRealtime]; // call this method again to make sure all gestures are configured & added to the superview(self.view), including the gestures added from inside the streamview. - if([self isFirstLaunch]) [self popFirstLaunchTip]; + if([self isFirstStreaming]) [self popFirstStreamingTip]; #if TARGET_OS_TV if (!_menuTapGestureRecognizer || !_menuDoubleTapGestureRecognizer || !_playPauseTapGestureRecognizer) { diff --git a/VoidLink/ViewControllers/ToolboxViewController.swift b/VoidLink/ViewControllers/ToolboxViewController.swift index 22e8d1f0..5d446728 100644 --- a/VoidLink/ViewControllers/ToolboxViewController.swift +++ b/VoidLink/ViewControllers/ToolboxViewController.swift @@ -30,8 +30,8 @@ import UIKit private let titleLabel = UILabel() @objc public var specialEntries : NSMutableArray = ["widgetSwitchTool", "widgetLayoutTool", "bringUpSoftKeyboard", "enterPip"] private let specialEntryAliasDic : [String:String] = [ - "widgetSwitchTool":SwiftLocalizationHelper.localizedString(forKey: "[ Switch among on-screen widget profiles ]"), - "widgetLayoutTool":SwiftLocalizationHelper.localizedString(forKey: "[ Open on-screen widget tool ]"), + "widgetSwitchTool":SwiftLocalizationHelper.localizedString(forKey: "[ Switch on-screen widget profile ]"), + "widgetLayoutTool":SwiftLocalizationHelper.localizedString(forKey: "[ On-screen widget tool ]"), "bringUpSoftKeyboard":SwiftLocalizationHelper.localizedString(forKey: "[ Bring up soft keyboard ]"), "enterPip":SwiftLocalizationHelper.localizedString(forKey: "[ Enter picture-in-picture mode ]") ] @@ -149,8 +149,7 @@ import UIKit NSLayoutConstraint.activate([ // Set the width and height of the view - - view.widthAnchor.constraint(equalTo: view.superview!.widthAnchor, multiplier: isIPhone() ? 0.7 : 0.8), + view.widthAnchor.constraint(equalTo: view.superview!.widthAnchor, multiplier: isIPhone() ? 0.6 : 0.8), view.heightAnchor.constraint(equalTo: view.superview!.heightAnchor, multiplier: 0.99), // Set the width and height of the view //view.leadingAnchor.constraint(equalTo: view.superview!.leadingAnchor, constant: 60), @@ -177,7 +176,7 @@ import UIKit exitButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), // EditButton constraints - editButton.leadingAnchor.constraint(equalTo: exitButton.trailingAnchor, constant: 50), + editButton.leadingAnchor.constraint(equalTo: exitButton.trailingAnchor, constant: 25), editButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), // AddButton constraints @@ -185,11 +184,11 @@ import UIKit addButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), // DeleteButton constraints - deleteButton.trailingAnchor.constraint(equalTo: addButton.leadingAnchor, constant: -50), + deleteButton.trailingAnchor.constraint(equalTo: addButton.leadingAnchor, constant: -25), deleteButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), // DeleteButton constraints - pinButton.trailingAnchor.constraint(equalTo: deleteButton.leadingAnchor, constant: -50), + pinButton.trailingAnchor.constraint(equalTo: deleteButton.leadingAnchor, constant: -25), pinButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), ]) } @@ -208,7 +207,7 @@ import UIKit pinButton.backgroundColor = highlightColor } else{ - pinButton.backgroundColor = viewBackgroundColor + pinButton.backgroundColor = .clear } } diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index ec471e46..8d567abf 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -3140,37 +3140,37 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "台前调度" + "value" : "台前调度(可全屏)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "幕前調度" + "value" : "幕前調度(可全螢幕)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "幕前調度" + "value" : "幕前調度(可全螢幕)" } } } }, "VWB-YB-Ptj.segmentTitles[1]" : { - "comment" : "Class = \"UISegmentedControl\"; VWB-YB-Ptj.segmentTitles[1] = \"AirPlay\"; ObjectID = \"VWB-YB-Ptj\";", + "comment" : "Class = \"UISegmentedControl\"; VWB-YB-Ptj.segmentTitles[1] = \"AirPlay (mirroring)\"; ObjectID = \"VWB-YB-Ptj\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "AirPlay" + "value" : "AirPlay (mirroring)" } }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "隔空播放" + "state" : "needs_review", + "value" : "隔空播放(镜像)" } }, "zh-Hant" : { @@ -3181,7 +3181,7 @@ }, "zh-HK" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "螢幕鏡像輸出" } } diff --git a/VoidLink/mul.lproj/iPhone.xcstrings b/VoidLink/mul.lproj/iPhone.xcstrings index a26d0f7f..32b2c72c 100644 --- a/VoidLink/mul.lproj/iPhone.xcstrings +++ b/VoidLink/mul.lproj/iPhone.xcstrings @@ -2834,19 +2834,19 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "台前调度" + "value" : "台前调度(可全屏)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "幕前調度" + "value" : "幕前調度(可全螢幕)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "幕前調度" + "value" : "幕前調度(可全螢幕)" } } } @@ -2864,7 +2864,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "隔空播放" + "value" : "隔空播放(镜像)" } }, "zh-Hant" : {