diff --git a/LocalDevVPN/ContentView.swift b/LocalDevVPN/ContentView.swift index 786a60c..fe44c32 100644 --- a/LocalDevVPN/ContentView.swift +++ b/LocalDevVPN/ContentView.swift @@ -685,12 +685,13 @@ struct ContentView: View { var body: some View { NBNavigationStack { - ScrollView(showsIndicators: false) { - VStack(spacing: 20) { + ScrollView { + VStack(spacing: 16) { + TitleWithSettingsRow(showSettings: $showSettings) + StatusOverviewCard() ConnectivityControlsCard( - autoConnect: $autoConnect, action: { tunnelManager.tunnelStatus == .connected ? tunnelManager.stopVPN() : tunnelManager.startVPN() } @@ -701,24 +702,16 @@ struct ContentView: View { } } .padding(.horizontal) - .padding(.vertical, 24) + .padding(.vertical, 12) + .frame(maxWidth: .infinity, alignment: .top) } + .applyAdaptiveBounce() .background(backgroundColor.ignoresSafeArea()) - .navigationTitle("LocalDevVPN") + .navigationTitle("") #if os(iOS) - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) #endif .tvOSNavigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button { - showSettings = true - } label: { - Image(systemName: "gear") - .foregroundColor(.primary) - } - } - } .onChange(of: tunnelManager.waitingOnSettings) { finished in if tunnelManager.tunnelStatus != .connected && autoConnect && finished { tunnelManager.startVPN() @@ -742,6 +735,31 @@ struct ContentView: View { } } +struct TitleWithSettingsRow: View { + @Binding var showSettings: Bool + + var body: some View { + HStack(spacing: 10) { + Text("LocalDevVPN") + .font(.largeTitle) + .fontWeight(.bold) + .accessibilityAddTraits(.isHeader) + + Spacer() + + Button { + showSettings = true + } label: { + Image(systemName: "gear") + .font(.title2.weight(.semibold)) + .foregroundColor(.primary) + } + .accessibilityLabel(Text("settings")) + } + .padding(.top, 4) + } +} + extension View { @ViewBuilder func tvOSNavigationBarTitleDisplayMode(_ displayMode: NavigationBarItem.TitleDisplayMode) -> some View { @@ -751,10 +769,20 @@ extension View { self #endif } + + @ViewBuilder + func applyAdaptiveBounce() -> some View { + if #available(iOS 16.4, tvOS 16.4, *) { + scrollBounceBehavior(.basedOnSize) + } else { + self + } + } } struct StatusOverviewCard: View { @StateObject private var tunnelManager = TunnelManager.shared + @AppStorage("TunnelDeviceIP") private var deviceIP = "10.7.0.0" var body: some View { DashboardCard { @@ -785,9 +813,12 @@ struct StatusOverviewCard: View { Spacer() - Text(Date(), style: .time) - .font(.caption) - .foregroundColor(.secondary) + HStack(spacing: 4) { + Text("connected_at") + Text(Date(), style: .time) + } + .font(.caption) + .foregroundColor(.secondary) } } } @@ -796,9 +827,9 @@ struct StatusOverviewCard: View { private var statusTip: String { switch tunnelManager.tunnelStatus { case .connected: - return NSLocalizedString("connected_to_10.7.0.1", comment: "") + return String(format: NSLocalizedString("connected_to_ip", comment: ""), deviceIP) case .connecting: - return NSLocalizedString("macos_might_ask_you_to_approve_the_vpn", comment: "") + return NSLocalizedString("ios_might_ask_you_to_allow_the_vpn", comment: "") case .disconnecting: return NSLocalizedString("disconnecting_safely", comment: "") case .error: @@ -812,13 +843,13 @@ struct StatusOverviewCard: View { struct StatusGlyphView: View { @StateObject private var tunnelManager = TunnelManager.shared @State private var ringScale: CGFloat = 1.0 - @Environment(\.accessibilityReduceMotion) private var reduceMotion var body: some View { ZStack { Circle() .stroke(tunnelManager.tunnelStatus.color.opacity(0.25), lineWidth: 6) - .scaleEffect(reduceMotion ? 1 : ringScale, anchor: .center) + .scaleEffect(ringScale, anchor: .center) + .animation(.easeInOut(duration: 1.2).repeatForever(autoreverses: true), value: ringScale) Circle() .fill(tunnelManager.tunnelStatus.color.opacity(0.15)) @@ -828,30 +859,17 @@ struct StatusGlyphView: View { .foregroundColor(tunnelManager.tunnelStatus.color) } .frame(width: 92, height: 92) - .onAppear(perform: restartPulse) - .onChange(of: tunnelManager.tunnelStatus) { _ in - restartPulse() - } - .onChange(of: reduceMotion) { _ in - restartPulse() - } + .onAppear(perform: startPulse) } - private func restartPulse() { - guard !reduceMotion else { - ringScale = 1 - return - } - - ringScale = 1.0 - withAnimation(.easeInOut(duration: 1.2).repeatForever(autoreverses: true)) { + private func startPulse() { + DispatchQueue.main.async { ringScale = 1.08 } } } struct ConnectivityControlsCard: View { - @Binding var autoConnect: Bool let action: () -> Void var body: some View { @@ -866,24 +884,13 @@ struct ConnectivityControlsCard: View { } ConnectionButton(action: action) - - Toggle(isOn: $autoConnect) { - VStack(alignment: .leading, spacing: 2) { - Text("auto-connect_on_launch") - .fontWeight(.semibold) - Text("resume_your_last_state_automatically") - .font(.caption) - .foregroundColor(.secondary) - } - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } } } struct ConnectionInfoRow: View { - let title: String + let title: LocalizedStringKey let value: String let icon: String @@ -972,16 +979,14 @@ struct ConnectionButton: View { struct ConnectionStatsView: View { @StateObject private var tunnelManager = TunnelManager.shared - @State private var time = 0 - let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @AppStorage("TunnelDeviceIP") private var deviceIP = "10.7.0.0" @AppStorage("TunnelFakeIP") private var fakeIP = "10.7.0.1" @AppStorage("TunnelSubnetMask") private var subnetMask = "255.255.255.0" var body: some View { DashboardCard { - VStack(alignment: .leading, spacing: 18) { - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 3) { Text("session_details") .font(.headline) Text("live_stats_while_the_tunnel_is_connected") @@ -989,19 +994,6 @@ struct ConnectionStatsView: View { .foregroundColor(.secondary) } - HStack(spacing: 16) { - StatItemView( - title: "time_connected", - value: formattedTime, - icon: "clock.fill" - ) - StatItemView( - title: "status", - value: statusValue, - icon: tunnelManager.tunnelStatus.systemImage - ) - } - Divider() Text("network_configuration") @@ -1027,36 +1019,8 @@ struct ConnectionStatsView: View { ) } } - .onReceive(timer) { _ in - time += 1 - } } - var formattedTime: String { - let minutes = (time / 60) % 60 - let hours = time / 3600 - let seconds = time % 60 - - if hours > 0 { - return String(format: "%02d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%02d:%02d", minutes, seconds) - } - } - private var statusValue: String { - switch tunnelManager.tunnelStatus { - case .connected: - return NSLocalizedString("Active", comment: "") - case .connecting: - return NSLocalizedString("Connecting", comment: "") - case .disconnecting: - return NSLocalizedString("Disconnecting", comment: "") - case .error: - return NSLocalizedString("Error", comment: "") - default: - return NSLocalizedString("Idle", comment: "") - } - } } struct StatItemView: View { diff --git a/LocalDevVPN/Localization/en.lproj/Localizable.strings b/LocalDevVPN/Localization/en.lproj/Localizable.strings index cf422ad..ed6cf6b 100644 --- a/LocalDevVPN/Localization/en.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/en.lproj/Localizable.strings @@ -11,8 +11,9 @@ "local_tunnel_active" = "Local Tunnel Active"; "local_tunnel_inactive" = "Local Tunnel Inactive"; -"connected_to_10.7.0.1" = "Connected to 10.7.0.1"; -"macos_might_ask_you_to_approve_the_vpn" = "macOS might ask you to approve the VPN"; +"connected_to_ip" = "Connected to %@"; +"connected_at" = "Connected at"; +"ios_might_ask_you_to_allow_the_vpn" = "iOS might ask you to allow the VPN"; "disconnecting_safely" = "Disconnecting safely…"; "open_settings_to_review_details" = "Open Settings to review details"; "tap_connect_to_create_the_tunnel" = "Tap connect to create the tunnel"; diff --git a/LocalDevVPN/Localization/es.lproj/Localizable.strings b/LocalDevVPN/Localization/es.lproj/Localizable.strings index a44a9ee..becf323 100644 --- a/LocalDevVPN/Localization/es.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/es.lproj/Localizable.strings @@ -11,8 +11,9 @@ "local_tunnel_active" = "Túnel local activo"; "local_tunnel_inactive" = "Túnel local inactivo"; -"connected_to_10.7.0.1" = "Conectado a 10.7.0.1"; -"macos_might_ask_you_to_approve_the_vpn" = "macOS podría pedirte que apruebes la VPN"; +"connected_to_ip" = "Conectado a %@"; +"connected_at" = "Connected at"; +"ios_might_ask_you_to_allow_the_vpn" = "iOS podría pedirte que permitas la VPN"; "disconnecting_safely" = "Desconectando de forma segura…"; "open_settings_to_review_details" = "Abre Configuración para revisar los detalles"; "tap_connect_to_create_the_tunnel" = "Toca conectar para crear el túnel"; diff --git a/LocalDevVPN/Localization/it.lproj/Localizable.strings b/LocalDevVPN/Localization/it.lproj/Localizable.strings index bc7e1d9..f7b7dc8 100644 --- a/LocalDevVPN/Localization/it.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/it.lproj/Localizable.strings @@ -11,8 +11,9 @@ "local_tunnel_active" = "Tunnel locale attivo"; "local_tunnel_inactive" = "Tunnel locale inattivo"; -"connected_to_10.7.0.1" = "Connesso a 10.7.0.1"; -"macos_might_ask_you_to_approve_the_vpn" = "macOS potrebbe chiederti di approvare la VPN"; +"connected_to_ip" = "Connesso a %@"; +"connected_at" = "Connected at"; +"ios_might_ask_you_to_allow_the_vpn" = "iOS potrebbe chiederti di consentire la VPN"; "disconnecting_safely" = "Disconnessione in corso in modo sicuro…"; "open_settings_to_review_details" = "Apri Impostazioni per visualizzare i dettagli"; "tap_connect_to_create_the_tunnel" = "Tocca Connetti per creare il tunnel"; diff --git a/LocalDevVPN/Localization/ko.lproj/Localizable.strings b/LocalDevVPN/Localization/ko.lproj/Localizable.strings index 16f697b..0aa1089 100644 --- a/LocalDevVPN/Localization/ko.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/ko.lproj/Localizable.strings @@ -11,8 +11,9 @@ "local_tunnel_active" = "로컬 터널 활성화됨"; "local_tunnel_inactive" = "로컬 터널 비활성화됨"; -"connected_to_10.7.0.1" = "10.7.0.1에 연결됨"; -"macos_might_ask_you_to_approve_the_vpn" = "macOS에서 VPN 승인 요청이 나타날 수 있습니다"; +"connected_to_ip" = "%@에 연결됨"; +"connected_at" = "Connected at"; +"ios_might_ask_you_to_allow_the_vpn" = "iOS에서 VPN 허용 요청이 나타날 수 있습니다"; "disconnecting_safely" = "안전하게 연결 해제 중…"; "open_settings_to_review_details" = "자세한 내용을 보려면 설정을 여세요"; "tap_connect_to_create_the_tunnel" = "터널을 생성하려면 연결을 누르세요"; diff --git a/LocalDevVPN/Localization/pl.lproj/Localizable.strings b/LocalDevVPN/Localization/pl.lproj/Localizable.strings index d5b096f..9bf6bf9 100644 --- a/LocalDevVPN/Localization/pl.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/pl.lproj/Localizable.strings @@ -11,8 +11,9 @@ "local_tunnel_active" = "Lokalny tunel aktywny"; "local_tunnel_inactive" = "Lokalny tunel nieaktywny"; -"connected_to_10.7.0.1" = "Połączono z 10.7.0.1"; -"macos_might_ask_you_to_approve_the_vpn" = "macOS może poprosić Cię o zatwierdzenie VPN"; +"connected_to_ip" = "Połączono z %@"; +"connected_at" = "Connected at"; +"ios_might_ask_you_to_allow_the_vpn" = "iOS może poprosić Cię o pozwolenie na VPN"; "disconnecting_safely" = "Bezpieczne rozłączanie…"; "open_settings_to_review_details" = "Otwórz Ustawienia, aby zobaczyć szczegóły"; "tap_connect_to_create_the_tunnel" = "Stuknij Połącz, aby utworzyć tunel";