From 456127d32f37bc52f624194369a7c4c8145e7c42 Mon Sep 17 00:00:00 2001 From: Andrea Filice Date: Tue, 23 Dec 2025 19:16:45 +0100 Subject: [PATCH 1/6] feat: adding more infos * added more info about the status of the connection. * added "last connection at" status and dynamic change between "connected at" and "last connection at" --- .../xcschemes/TunnelProv.xcscheme | 1 + LocalDevVPN/ContentView.swift | 53 ++++++++++--------- .../Localization/en.lproj/Localizable.strings | 5 +- .../Localization/es.lproj/Localizable.strings | 5 +- .../Localization/fr.lproj/Localizable.strings | 5 +- .../Localization/it.lproj/Localizable.strings | 5 +- .../Localization/ko.lproj/Localizable.strings | 5 +- .../Localization/pl.lproj/Localizable.strings | 5 +- .../zh-Hant.lproj/Localizable.strings | 5 +- 9 files changed, 51 insertions(+), 38 deletions(-) diff --git a/LocalDevVPN.xcodeproj/xcshareddata/xcschemes/TunnelProv.xcscheme b/LocalDevVPN.xcodeproj/xcshareddata/xcschemes/TunnelProv.xcscheme index 8d51541..7c566e4 100644 --- a/LocalDevVPN.xcodeproj/xcshareddata/xcschemes/TunnelProv.xcscheme +++ b/LocalDevVPN.xcodeproj/xcshareddata/xcschemes/TunnelProv.xcscheme @@ -74,6 +74,7 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" + askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2"> diff --git a/LocalDevVPN/ContentView.swift b/LocalDevVPN/ContentView.swift index 34a41ba..8c131ac 100644 --- a/LocalDevVPN/ContentView.swift +++ b/LocalDevVPN/ContentView.swift @@ -17,7 +17,6 @@ extension Bundle { } // MARK: - Logging Utility - class VPNLogger: ObservableObject { @Published var logs: [String] = [] @@ -814,8 +813,14 @@ struct StatusOverviewCard: View { Spacer() HStack(spacing: 4) { - Text("connected_at") - Text(Date(), style: .time) + if TunnelManager.shared.tunnelStatus == .connected { + Text("connected_at") + Text(Date(), style: .time) + } + else { + Text("last_connected_at") + Text(Date(), style: .time) + } } .font(.caption) .foregroundColor(.secondary) @@ -1112,27 +1117,7 @@ struct SettingsView: View { networkConfigRow(label: "subnet_mask", text: $subnetMask) } } - - Section(header: Text("app_information")) { - Button { - UIApplication.shared.open(URL(string: "https://jkcoxson.com/cdn/LocalDevVPN/LocalDevVPNPrivacyPolicy.md")!, options: [:]) - } label: { - Label("privacy_policy", systemImage: "lock.shield") - } - NavigationLink(destination: DataCollectionInfoView()) { - Label("data_collection_policy", systemImage: "hand.raised.slash") - } - HStack { - Text("app_version") - Spacer() - Text(Bundle.main.shortVersion) - .foregroundColor(.secondary) - } - NavigationLink(destination: HelpView()) { - Text("help_and_support") - } - } - + Section(header: Text("language")) { Picker("dropdown_language", selection: $selectedLanguage) { Text("english").tag("en") @@ -1158,6 +1143,26 @@ struct SettingsView: View { ) } } + + Section(header: Text("app_information")) { + Button { + UIApplication.shared.open(URL(string: "https://jkcoxson.com/cdn/LocalDevVPN/LocalDevVPNPrivacyPolicy.md")!, options: [:]) + } label: { + Label("privacy_policy", systemImage: "lock.shield") + } + NavigationLink(destination: DataCollectionInfoView()) { + Label("data_collection_policy", systemImage: "hand.raised.slash") + } + HStack { + Text("app_version") + Spacer() + Text(Bundle.main.shortVersion) + .foregroundColor(.secondary) + } + NavigationLink(destination: HelpView()) { + Text("help_and_support") + } + } } .alert(isPresented: $showNetworkWarning) { Alert( diff --git a/LocalDevVPN/Localization/en.lproj/Localizable.strings b/LocalDevVPN/Localization/en.lproj/Localizable.strings index b08c6c0..911bf74 100644 --- a/LocalDevVPN/Localization/en.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/en.lproj/Localizable.strings @@ -12,7 +12,8 @@ "local_tunnel_inactive" = "Local Tunnel Inactive"; "connected_to_ip" = "Connected to %@"; -"connected_at" = "Connected at"; +"connected_at" = "Connected: "; +"last_connected_at" = "Last connection: "; "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"; @@ -149,4 +150,4 @@ "restart_title" = "Restart"; "restart_message" = "To apply the changes, you need to restart the application."; "confirmYes" = "Yes"; -"confirmNo" = "No"; \ No newline at end of file +"confirmNo" = "No"; diff --git a/LocalDevVPN/Localization/es.lproj/Localizable.strings b/LocalDevVPN/Localization/es.lproj/Localizable.strings index 0bc2a62..dc56cf1 100644 --- a/LocalDevVPN/Localization/es.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/es.lproj/Localizable.strings @@ -12,7 +12,8 @@ "local_tunnel_inactive" = "Túnel local inactivo"; "connected_to_ip" = "Conectado a %@"; -"connected_at" = "Conectado a"; +"connected_at" = "Conectado: "; +"last_connected_at" = "Última conexión:"; "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"; @@ -149,4 +150,4 @@ "restart_title" = "Reiniciar"; "restart_message" = "Para aplicar los cambios, es necesario reiniciar la aplicación."; "confirmYes" = "Sí"; -"confirmNo" = "No"; \ No newline at end of file +"confirmNo" = "No"; diff --git a/LocalDevVPN/Localization/fr.lproj/Localizable.strings b/LocalDevVPN/Localization/fr.lproj/Localizable.strings index 5836c8b..bfcff5e 100644 --- a/LocalDevVPN/Localization/fr.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/fr.lproj/Localizable.strings @@ -12,7 +12,8 @@ "local_tunnel_inactive" = "Tunnel local inactif"; "connected_to_ip" = "Connecté à %@"; -"connected_at" = "Connecté à"; +"connected_at" = "Connecté:"; +"last_connected_at" = "Dernière connexion:"; "ios_might_ask_you_to_allow_the_vpn" = "iOS peut vous demander d'autoriser le VPN"; "disconnecting_safely" = "Déconnexion sécurisée en cours…"; "open_settings_to_review_details" = "Ouvrir les paramètres pour voir les détails"; @@ -150,4 +151,4 @@ "restart_title" = "Redémarrer"; "restart_message" = "Pour appliquer les modifications, vous devez redémarrer l'application."; "confirmYes" = "Oui"; -"confirmNo" = "Non"; \ No newline at end of file +"confirmNo" = "Non"; diff --git a/LocalDevVPN/Localization/it.lproj/Localizable.strings b/LocalDevVPN/Localization/it.lproj/Localizable.strings index 01a8ebf..6e82be2 100644 --- a/LocalDevVPN/Localization/it.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/it.lproj/Localizable.strings @@ -12,7 +12,8 @@ "local_tunnel_inactive" = "Tunnel locale inattivo"; "connected_to_ip" = "Connesso a %@"; -"connected_at" = "Connesso a"; +"connected_at" = "Connesso:"; +"last_connected_at" = "Ultima connessione:"; "ios_might_ask_you_to_allow_the_vpn" = "iOS potrebbe chiederti di consentire la VPN"; "disconnecting_safely" = "Disconnessione in modo sicuro…"; "open_settings_to_review_details" = "Apri Impostazioni per visualizzare i dettagli"; @@ -149,4 +150,4 @@ "restart_title" = "Riavvia"; "restart_message" = "Per applicare le modifiche, è necessario riavviare l'applicazione."; "confirmYes" = "Si"; -"confirmNo" = "No"; \ No newline at end of file +"confirmNo" = "No"; diff --git a/LocalDevVPN/Localization/ko.lproj/Localizable.strings b/LocalDevVPN/Localization/ko.lproj/Localizable.strings index be731a1..598ffba 100644 --- a/LocalDevVPN/Localization/ko.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/ko.lproj/Localizable.strings @@ -12,7 +12,8 @@ "local_tunnel_inactive" = "로컬 터널 비활성화됨"; "connected_to_ip" = "%@에 연결됨"; -"connected_at" = "연결 시간 "; +"connected_at" = "연결됨:"; +"last_connected_at" = "마지막 접속:"; "ios_might_ask_you_to_allow_the_vpn" = "iOS에서 VPN 허용 요청이 나타날 수 있습니다"; "disconnecting_safely" = "안전하게 연결 해제 중…"; "open_settings_to_review_details" = "자세한 내용을 보려면 설정을 여세요"; @@ -150,4 +151,4 @@ "restart_title" = "재시작"; "restart_message" = "변경사항을 적용할려면 앱을 재시작해야 합니다."; "confirmYes" = "네"; -"confirmNo" = "아니요"; \ No newline at end of file +"confirmNo" = "아니요"; diff --git a/LocalDevVPN/Localization/pl.lproj/Localizable.strings b/LocalDevVPN/Localization/pl.lproj/Localizable.strings index f1b7f59..c04bb6b 100644 --- a/LocalDevVPN/Localization/pl.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/pl.lproj/Localizable.strings @@ -12,7 +12,8 @@ "local_tunnel_inactive" = "Lokalny tunel nieaktywny"; "connected_to_ip" = "Połączono z %@"; -"connected_at" = "Połączono z "; +"connected_at" = "Powiązane:"; +"last_connected_at" = "Ostatnie połączenie:"; "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"; @@ -150,4 +151,4 @@ "restart_title" = "Restartuj"; "restart_message" = "Aby zastosować zmiany, należy ponownie uruchomić aplikację."; "confirmYes" = "Tak"; -"confirmNo" = "Nie"; \ No newline at end of file +"confirmNo" = "Nie"; diff --git a/LocalDevVPN/Localization/zh-Hant.lproj/Localizable.strings b/LocalDevVPN/Localization/zh-Hant.lproj/Localizable.strings index 832dd82..a569c00 100644 --- a/LocalDevVPN/Localization/zh-Hant.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/zh-Hant.lproj/Localizable.strings @@ -12,7 +12,8 @@ "local_tunnel_inactive" = "本地通道未啟動"; "connected_to_ip" = "已連接至 %@"; -"connected_at" = "連接於"; +"connected_at" = "相關:"; +"last_connected_at" = "最後一次連線:"; "ios_might_ask_you_to_allow_the_vpn" = "iOS可能會要求您允許VPN"; "disconnecting_safely" = "安全地中斷連線中…"; "open_settings_to_review_details" = "開啟設定以查看詳細資訊"; @@ -150,4 +151,4 @@ "restart_title" = "重新啟動"; "restart_message" = "要套用變更,必須重新啟動應用程式。"; "confirmYes" = "是"; -"confirmNo" = "不"; \ No newline at end of file +"confirmNo" = "不"; From a2918bffb9f4265fce455d48c6180e5ea7658c46 Mon Sep 17 00:00:00 2001 From: Andrea Filice Date: Fri, 26 Dec 2025 00:57:28 +0100 Subject: [PATCH 2/6] feat: started redesign * redesign a little bit with new icons --- LocalDevVPN/ContentView.swift | 174 ++++++++++++++++-- .../Localization/en.lproj/Localizable.strings | 5 + 2 files changed, 164 insertions(+), 15 deletions(-) diff --git a/LocalDevVPN/ContentView.swift b/LocalDevVPN/ContentView.swift index 8c131ac..a1805e8 100644 --- a/LocalDevVPN/ContentView.swift +++ b/LocalDevVPN/ContentView.swift @@ -789,7 +789,7 @@ struct StatusOverviewCard: View { Text("current_status") .font(.headline) - HStack(spacing: 18) { + VStack(spacing: 18) { StatusGlyphView() Text(tunnelManager.tunnelStatus.localizedTitle) @@ -797,6 +797,12 @@ struct StatusOverviewCard: View { .fontWeight(.semibold) .multilineTextAlignment(.center) .frame(maxWidth: .infinity, alignment: .center) + + Text(localizedCaption) + .font(.caption2) + .fontWeight(.regular) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) } Divider() @@ -814,10 +820,12 @@ struct StatusOverviewCard: View { HStack(spacing: 4) { if TunnelManager.shared.tunnelStatus == .connected { + Image(systemName: "clock") Text("connected_at") Text(Date(), style: .time) } else { + Image(systemName: "clock.arrow.trianglehead.counterclockwise.rotate.90") Text("last_connected_at") Text(Date(), style: .time) } @@ -843,6 +851,21 @@ struct StatusOverviewCard: View { return NSLocalizedString("tap_connect_to_create_the_tunnel", comment: "") } } + + private var localizedCaption: String { + switch tunnelManager.tunnelStatus { + case .disconnected: + return NSLocalizedString("disconnectedCaption", comment: "") + case .connecting: + return String(format: NSLocalizedString("connectingCaption", comment: ""), deviceIP) + case .connected: + return String(format: NSLocalizedString("connectedCaption", comment: ""), deviceIP) + case .disconnecting: + return String(format: NSLocalizedString("disconnectingCaption", comment: ""), deviceIP) + case .error: + return NSLocalizedString("errorCaption", comment: "") + } + } } struct StatusGlyphView: View { @@ -881,8 +904,11 @@ struct ConnectivityControlsCard: View { DashboardCard { VStack(alignment: .leading, spacing: 18) { VStack(alignment: .leading, spacing: 4) { - Text("connection") - .font(.headline) + HStack(spacing: 6){ + Image(systemName: "network.badge.shield.half.filled") + Text("connection") + .font(.headline) + } Text("start_or_stop_the_secure_local_tunnel") .font(.footnote) .foregroundColor(.secondary) @@ -992,8 +1018,11 @@ struct ConnectionStatsView: View { DashboardCard { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 3) { - Text("session_details") - .font(.headline) + HStack(spacing: 6) { + Image(systemName: "iphone.crop.circle") + Text("session_details") + .font(.headline) + } Text("live_stats_while_the_tunnel_is_connected") .font(.footnote) .foregroundColor(.secondary) @@ -1119,14 +1148,17 @@ struct SettingsView: View { } Section(header: Text("language")) { - Picker("dropdown_language", selection: $selectedLanguage) { - Text("english").tag("en") - Text("spanish").tag("es") - Text("italian").tag("it") - Text("polish").tag("pl") - Text("korean").tag("ko") - Text("TChinese").tag("zh-Hant") - Text("french").tag("fr") + HStack{ + Image(systemName: "globe") + Picker("dropdown_language", selection: $selectedLanguage) { + Text("english").tag("en") + Text("spanish").tag("es") + Text("italian").tag("it") + Text("polish").tag("pl") + Text("korean").tag("ko") + Text("TChinese").tag("zh-Hant") + Text("french").tag("fr") + } } .onChange(of: selectedLanguage) { newValue in let languageCode = newValue @@ -1154,13 +1186,13 @@ struct SettingsView: View { Label("data_collection_policy", systemImage: "hand.raised.slash") } HStack { - Text("app_version") + Label("app_version", systemImage: "info") Spacer() Text(Bundle.main.shortVersion) .foregroundColor(.secondary) } NavigationLink(destination: HelpView()) { - Text("help_and_support") + Label("help_and_support", systemImage: "questionmark.circle") } } } @@ -1272,11 +1304,35 @@ struct ConnectionLogView: View { } struct HelpView: View { + @State var startImageTransition : Bool = false + var body: some View { List { Section(header: Text("faq_header")) { NavigationLink("faq_q1") { VStack(alignment: .leading, spacing: 15) { + if #available(iOS 18.0, *) { + Image(systemName: startImageTransition ? "network.badge.shield.half.filled" : "network") + .font(.system(size: 80)) + .foregroundColor(.blue) + .frame(maxWidth: .infinity, maxHeight: 200, alignment: .center) + .contentTransition(.symbolEffect(.replace.magic(fallback: .offUp.wholeSymbol), options: .nonRepeating)) + .onAppear{ + Task{ + try? await Task.sleep(for: .seconds(1)) + startImageTransition = true + } + } + .onDisappear{ + startImageTransition = false + } + } else { + Image(systemName: "network") + .font(.system(size: 80)) + .foregroundColor(.blue) + .frame(maxWidth: .infinity, alignment: .center) + .frame(maxHeight: 200, alignment: .center) + } Text("faq_q1_a1") .padding(.bottom, 10) Text("faq_common_use_cases") @@ -1290,6 +1346,28 @@ struct HelpView: View { } NavigationLink("faq_q2") { VStack(alignment: .leading, spacing: 15) { + if #available(iOS 18.0, *) { + Image(systemName: startImageTransition ? "antenna.radiowaves.left.and.right.slash" : "antenna.radiowaves.left.and.right") + .font(.system(size: 80)) + .foregroundColor(.blue) + .frame(maxWidth: .infinity, maxHeight: 200, alignment: .center) + .contentTransition(.symbolEffect(.replace.magic(fallback: .offUp.wholeSymbol), options: .nonRepeating)) + .onAppear{ + Task{ + try? await Task.sleep(for: .seconds(1)) + startImageTransition = true + } + } + .onDisappear{ + startImageTransition = false + } + } else { + Image(systemName: "network") + .font(.system(size: 80)) + .foregroundColor(.blue) + .frame(maxWidth: .infinity, alignment: .center) + .frame(maxHeight: 200, alignment: .center) + } Text("faq_q2_a1") .padding(.bottom, 10) .font(.headline) @@ -1304,6 +1382,28 @@ struct HelpView: View { } NavigationLink("faq_q3") { VStack(alignment: .leading, spacing: 15) { + if #available(iOS 18.0, *) { + Image(systemName: startImageTransition ? "wifi.exclamationmark" : "wifi") + .font(.system(size: 80)) + .foregroundColor(.blue) + .frame(maxWidth: .infinity, maxHeight: 200, alignment: .center) + .contentTransition(.symbolEffect(.replace.magic(fallback: .offUp.wholeSymbol), options: .nonRepeating)) + .onAppear{ + Task{ + try? await Task.sleep(for: .seconds(1)) + startImageTransition = true + } + } + .onDisappear{ + startImageTransition = false + } + } else { + Image(systemName: "network") + .font(.system(size: 80)) + .foregroundColor(.blue) + .frame(maxWidth: .infinity, alignment: .center) + .frame(maxHeight: 200, alignment: .center) + } Text("faq_q3_a1") .padding(.bottom, 10) Text("faq_troubleshoot_header") @@ -1317,6 +1417,28 @@ struct HelpView: View { } NavigationLink("faq_q4") { VStack(alignment: .leading, spacing: 15) { + if #available(iOS 18.0, *) { + Image(systemName: startImageTransition ? "apple.terminal.fill" : "hammer.fill") + .font(.system(size: 80)) + .foregroundColor(.blue) + .frame(maxWidth: .infinity, maxHeight: 200, alignment: .center) + .contentTransition(.symbolEffect(.replace.magic(fallback: .offUp.wholeSymbol), options: .nonRepeating)) + .onAppear{ + Task{ + try? await Task.sleep(for: .seconds(1)) + startImageTransition = true + } + } + .onDisappear{ + startImageTransition = false + } + } else { + Image(systemName: "network") + .font(.system(size: 80)) + .foregroundColor(.blue) + .frame(maxWidth: .infinity, alignment: .center) + .frame(maxHeight: 200, alignment: .center) + } Text("faq_q4_intro") .font(.headline) .padding(.bottom, 10) @@ -1333,6 +1455,28 @@ struct HelpView: View { Section(header: Text("business_model_header")) { NavigationLink("biz_q1") { VStack(alignment: .leading, spacing: 15) { + if #available(iOS 18.0, *) { + Image(systemName: startImageTransition ? "person.badge.shield.checkmark.fill" : "person.badge.clock.fill") + .font(.system(size: 80)) + .foregroundColor(.blue) + .frame(maxWidth: .infinity, maxHeight: 200, alignment: .center) + .contentTransition(.symbolEffect(.replace.magic(fallback: .offUp.wholeSymbol), options: .nonRepeating)) + .onAppear{ + Task{ + try? await Task.sleep(for: .seconds(1)) + startImageTransition = true + } + } + .onDisappear{ + startImageTransition = false + } + } else { + Image(systemName: "network") + .font(.system(size: 80)) + .foregroundColor(.blue) + .frame(maxWidth: .infinity, alignment: .center) + .frame(maxHeight: 200, alignment: .center) + } Text("biz_q1_a1") .padding(.bottom, 10) Text("biz_key_points_header") diff --git a/LocalDevVPN/Localization/en.lproj/Localizable.strings b/LocalDevVPN/Localization/en.lproj/Localizable.strings index 911bf74..68c2f91 100644 --- a/LocalDevVPN/Localization/en.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/en.lproj/Localizable.strings @@ -3,10 +3,15 @@ "current_status" = "Current status"; "disconnected" = "Disconnected"; +"disconnectedCaption" = "Disconnected from Tunnel."; "connecting" = "Connecting"; +"connectingCaption" = "Connecting to %@..."; "connected" = "Connected"; +"connectedCaption" = "Connected to %@."; "disconnecting" = "Disconnecting"; +"disconnectingCaption" = "Disconnecting from %@..."; "error" = "Error"; +"errorCaption" = "An unknown error occurred."; "local_tunnel_active" = "Local Tunnel Active"; "local_tunnel_inactive" = "Local Tunnel Inactive"; From 92d38a137464d227d26ff5ab48c098f79254ba96 Mon Sep 17 00:00:00 2001 From: Andrea Filice Date: Fri, 26 Dec 2025 11:21:08 +0100 Subject: [PATCH 3/6] feat: adding more animations * added icons to Setup Page * fixed some bugs with other icons --- LocalDevVPN/ContentView.swift | 71 +++++++++++++------ .../Localization/en.lproj/Localizable.strings | 2 +- .../Localization/it.lproj/Localizable.strings | 7 +- 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/LocalDevVPN/ContentView.swift b/LocalDevVPN/ContentView.swift index a1805e8..697127a 100644 --- a/LocalDevVPN/ContentView.swift +++ b/LocalDevVPN/ContentView.swift @@ -799,7 +799,7 @@ struct StatusOverviewCard: View { .frame(maxWidth: .infinity, alignment: .center) Text(localizedCaption) - .font(.caption2) + .font(.caption) .fontWeight(.regular) .multilineTextAlignment(.center) .frame(maxWidth: .infinity, alignment: .center) @@ -1019,7 +1019,7 @@ struct ConnectionStatsView: View { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 3) { HStack(spacing: 6) { - Image(systemName: "iphone.crop.circle") + Image(systemName: "wifi.badge.lock") Text("session_details") .font(.headline) } @@ -1150,6 +1150,7 @@ struct SettingsView: View { Section(header: Text("language")) { HStack{ Image(systemName: "globe") + .foregroundColor(.blue) Picker("dropdown_language", selection: $selectedLanguage) { Text("english").tag("en") Text("spanish").tag("es") @@ -1313,14 +1314,19 @@ struct HelpView: View { VStack(alignment: .leading, spacing: 15) { if #available(iOS 18.0, *) { Image(systemName: startImageTransition ? "network.badge.shield.half.filled" : "network") + .resizable() + .scaledToFit() .font(.system(size: 80)) .foregroundColor(.blue) + .frame(width: 100, height: 100) .frame(maxWidth: .infinity, maxHeight: 200, alignment: .center) .contentTransition(.symbolEffect(.replace.magic(fallback: .offUp.wholeSymbol), options: .nonRepeating)) .onAppear{ Task{ try? await Task.sleep(for: .seconds(1)) - startImageTransition = true + withAnimation{ + startImageTransition = true + } } } .onDisappear{ @@ -1348,8 +1354,11 @@ struct HelpView: View { VStack(alignment: .leading, spacing: 15) { if #available(iOS 18.0, *) { Image(systemName: startImageTransition ? "antenna.radiowaves.left.and.right.slash" : "antenna.radiowaves.left.and.right") + .resizable() + .scaledToFit() .font(.system(size: 80)) .foregroundColor(.blue) + .frame(width: 100, height: 100) .frame(maxWidth: .infinity, maxHeight: 200, alignment: .center) .contentTransition(.symbolEffect(.replace.magic(fallback: .offUp.wholeSymbol), options: .nonRepeating)) .onAppear{ @@ -1491,14 +1500,10 @@ struct HelpView: View { } } Section(header: Text("app_info_header")) { - HStack { - Image(systemName: "exclamationmark.shield") - Text("requires_ios") - } - HStack { - Image(systemName: "lock.shield") - Text("uses_network_extension") - } + Label("requires_ios", systemImage: "exclamationmark.shield") + .foregroundColor(.black) + Label("uses_network_extension", systemImage: "lock.shield") + .foregroundColor(.black) } } .navigationTitle(Text("help_and_support_nav")) @@ -1514,25 +1519,29 @@ struct SetupView: View { SetupPage( title: "setup_welcome_title", description: "setup_welcome_description", - imageName: "checkmark.shield.fill", + transitionImage: "shield.fill", + standardImage: "checkmark.shield.fill", details: "setup_welcome_details" ), SetupPage( title: "setup_why_title", description: "setup_why_description", - imageName: "person.2.fill", + transitionImage: "person.2.shield", + standardImage: "person.2.fill", details: "setup_why_details" ), SetupPage( title: "setup_easy_title", description: "setup_easy_description", - imageName: "hand.tap.fill", + transitionImage: "hand.tap.fill", + standardImage: "hand.tap.fill", details: "setup_easy_details" ), SetupPage( title: "setup_privacy_title", description: "setup_privacy_description", - imageName: "lock.shield.fill", + transitionImage: "lock.open.fill", + standardImage: "lock.fill", details: "setup_privacy_details" ), ] @@ -1598,19 +1607,41 @@ struct SetupView: View { struct SetupPage { let title: LocalizedStringKey let description: LocalizedStringKey - let imageName: String + let transitionImage: String + let standardImage : String let details: LocalizedStringKey } struct SetupPageView: View { + @State var startImageTransition : Bool = false let page: SetupPage var body: some View { VStack(spacing: tvOSSpacing) { - Image(systemName: page.imageName) - .font(.system(size: tvOSImageSize)) - .foregroundColor(.blue) - .padding(.top, tvOSTopPadding) + if #available(iOS 18.0, *){ + Image(systemName: (startImageTransition) ? page.standardImage : page.transitionImage) + .font(.system(size: tvOSImageSize)) + .foregroundColor(.blue) + .padding(.top, tvOSTopPadding) + .contentTransition(.symbolEffect(.replace)) + .onAppear{ + Task{ + try? await Task.sleep(for: .seconds(1)) + withAnimation{ + startImageTransition = true + } + } + } + .onDisappear{ + startImageTransition = false + } + } + else{ + Image(systemName: page.standardImage) + .font(.system(size: tvOSImageSize)) + .foregroundColor(.blue) + .padding(.top, tvOSTopPadding) + } Text(page.title) .font(tvOSTitleFont) diff --git a/LocalDevVPN/Localization/en.lproj/Localizable.strings b/LocalDevVPN/Localization/en.lproj/Localizable.strings index 68c2f91..6bb7f90 100644 --- a/LocalDevVPN/Localization/en.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/en.lproj/Localizable.strings @@ -3,7 +3,7 @@ "current_status" = "Current status"; "disconnected" = "Disconnected"; -"disconnectedCaption" = "Disconnected from Tunnel."; +"disconnectedCaption" = "Disconnected from the Tunnel."; "connecting" = "Connecting"; "connectingCaption" = "Connecting to %@..."; "connected" = "Connected"; diff --git a/LocalDevVPN/Localization/it.lproj/Localizable.strings b/LocalDevVPN/Localization/it.lproj/Localizable.strings index 6e82be2..2811cd0 100644 --- a/LocalDevVPN/Localization/it.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/it.lproj/Localizable.strings @@ -3,10 +3,15 @@ "current_status" = "Stato attuale"; "disconnected" = "Disconnesso"; +"disconnectedCaption" = "Disconnesso dal Tunnel."; "connecting" = "Connessione in corso"; +"connectingCaption" = "Connessione a %@..."; "connected" = "Connesso"; +"connectedCaption" = "Connesso a %@."; "disconnecting" = "Disconnessione in corso"; +"disconnectingCaption" = "Disconnessione da %@..."; "error" = "Errore"; +"errorCaption" = "Si è verificato un errore sconosciuto."; "local_tunnel_active" = "Tunnel locale attivo"; "local_tunnel_inactive" = "Tunnel locale inattivo"; @@ -128,7 +133,7 @@ "app_info_header" = "Informazioni App"; "requires_ios" = "Richiede iOS 14.0 o superiore"; "uses_network_extension" = "Usa le Network Extension API di Apple"; -"help_and_support_nav" = "Guida e Supporto"; +"help_and_support_nav" = "Guida & Supporto"; "setup_welcome_title" = "Benvenuto in LocalDevVPN"; "setup_welcome_description" = "Un semplice tunnel di rete locale per sviluppatori"; "setup_welcome_details" = "LocalDevVPN crea un'interfaccia di rete locale per sviluppo, test e accesso a server locali. Questa app NON raccoglie dati utente né instrada il traffico attraverso server esterni."; From 66a0f201276fe6e07c62d29d5b3c1a053e4aca22 Mon Sep 17 00:00:00 2001 From: Andrea Filice Date: Fri, 26 Dec 2025 12:13:34 +0100 Subject: [PATCH 4/6] fix: fixing small bugs * fixed some transitions --- LocalDevVPN/ContentView.swift | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/LocalDevVPN/ContentView.swift b/LocalDevVPN/ContentView.swift index 697127a..b41934b 100644 --- a/LocalDevVPN/ContentView.swift +++ b/LocalDevVPN/ContentView.swift @@ -102,11 +102,11 @@ class TunnelManager: ObservableObject { case .disconnected: return "disconnected" case .connecting: - return "connecting" + return "connecting_ellipsis" case .connected: return "connected" case .disconnecting: - return "disconnecting" + return "disconnecting_ellipsis" case .error: return "error" } @@ -882,9 +882,17 @@ struct StatusGlyphView: View { Circle() .fill(tunnelManager.tunnelStatus.color.opacity(0.15)) - Image(systemName: tunnelManager.tunnelStatus.systemImage) - .font(.title) - .foregroundColor(tunnelManager.tunnelStatus.color) + if #available(iOS 18.0, *){ + Image(systemName: tunnelManager.tunnelStatus.systemImage) + .font(.title) + .foregroundColor(tunnelManager.tunnelStatus.color) + .contentTransition(.symbolEffect(.replace)) + } + else{ + Image(systemName: tunnelManager.tunnelStatus.systemImage) + .font(.title) + .foregroundColor(tunnelManager.tunnelStatus.color) + } } .frame(width: 92, height: 92) .onAppear(perform: startPulse) From b22d1344ea9734d8cf08f44ad217ee88891f4df3 Mon Sep 17 00:00:00 2001 From: Andrea Filice Date: Fri, 26 Dec 2025 19:00:24 +0100 Subject: [PATCH 5/6] fix: fixing some small bugs * fixed smalll bugs --- LocalDevVPN/ContentView.swift | 36 +++++++++++++------ .../Localization/en.lproj/Localizable.strings | 4 ++- .../Localization/it.lproj/Localizable.strings | 4 ++- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/LocalDevVPN/ContentView.swift b/LocalDevVPN/ContentView.swift index b41934b..c89a67d 100644 --- a/LocalDevVPN/ContentView.swift +++ b/LocalDevVPN/ContentView.swift @@ -1133,6 +1133,7 @@ struct SettingsView: View { @AppStorage("shownTunnelAlert") private var shownTunnelAlert = false @StateObject private var tunnelManager = TunnelManager.shared @AppStorage("hasNotCompletedSetup") private var hasNotCompletedSetup = true + @AppStorage("enableAnimations") var enableAnimations = true @State private var showNetworkWarning = false @State private var showRestartPopUp = false @@ -1183,6 +1184,19 @@ struct SettingsView: View { } ) } + + if #available(iOS 18.0, *){ + Toggle("enable_animation", isOn: $enableAnimations) + } + else{ + VStack{ + Toggle("enable_animation", isOn: $enableAnimations) + .disabled(true) + Text("not_supported") + .font(.footnote) + .frame(alignment: .center) + } + } } Section(header: Text("app_information")) { @@ -1314,13 +1328,14 @@ struct ConnectionLogView: View { struct HelpView: View { @State var startImageTransition : Bool = false + @AppStorage("enableAnimations") var enableAnimations = true var body: some View { List { Section(header: Text("faq_header")) { NavigationLink("faq_q1") { VStack(alignment: .leading, spacing: 15) { - if #available(iOS 18.0, *) { + if #available(iOS 18.0, *), enableAnimations{ Image(systemName: startImageTransition ? "network.badge.shield.half.filled" : "network") .resizable() .scaledToFit() @@ -1360,7 +1375,7 @@ struct HelpView: View { } NavigationLink("faq_q2") { VStack(alignment: .leading, spacing: 15) { - if #available(iOS 18.0, *) { + if #available(iOS 18.0, *), enableAnimations{ Image(systemName: startImageTransition ? "antenna.radiowaves.left.and.right.slash" : "antenna.radiowaves.left.and.right") .resizable() .scaledToFit() @@ -1379,7 +1394,7 @@ struct HelpView: View { startImageTransition = false } } else { - Image(systemName: "network") + Image(systemName: "antenna.radiowaves.left.and.right.slash") .font(.system(size: 80)) .foregroundColor(.blue) .frame(maxWidth: .infinity, alignment: .center) @@ -1399,7 +1414,7 @@ struct HelpView: View { } NavigationLink("faq_q3") { VStack(alignment: .leading, spacing: 15) { - if #available(iOS 18.0, *) { + if #available(iOS 18.0, *), enableAnimations{ Image(systemName: startImageTransition ? "wifi.exclamationmark" : "wifi") .font(.system(size: 80)) .foregroundColor(.blue) @@ -1415,7 +1430,7 @@ struct HelpView: View { startImageTransition = false } } else { - Image(systemName: "network") + Image(systemName: "wifi.exclamationmark") .font(.system(size: 80)) .foregroundColor(.blue) .frame(maxWidth: .infinity, alignment: .center) @@ -1434,7 +1449,7 @@ struct HelpView: View { } NavigationLink("faq_q4") { VStack(alignment: .leading, spacing: 15) { - if #available(iOS 18.0, *) { + if #available(iOS 18.0, *), enableAnimations { Image(systemName: startImageTransition ? "apple.terminal.fill" : "hammer.fill") .font(.system(size: 80)) .foregroundColor(.blue) @@ -1450,7 +1465,7 @@ struct HelpView: View { startImageTransition = false } } else { - Image(systemName: "network") + Image(systemName: "apple.terminal.fill") .font(.system(size: 80)) .foregroundColor(.blue) .frame(maxWidth: .infinity, alignment: .center) @@ -1472,7 +1487,7 @@ struct HelpView: View { Section(header: Text("business_model_header")) { NavigationLink("biz_q1") { VStack(alignment: .leading, spacing: 15) { - if #available(iOS 18.0, *) { + if #available(iOS 18.0, *), enableAnimations { Image(systemName: startImageTransition ? "person.badge.shield.checkmark.fill" : "person.badge.clock.fill") .font(.system(size: 80)) .foregroundColor(.blue) @@ -1488,7 +1503,7 @@ struct HelpView: View { startImageTransition = false } } else { - Image(systemName: "network") + Image(systemName: "person.badge.shield.checkmark.fill") .font(.system(size: 80)) .foregroundColor(.blue) .frame(maxWidth: .infinity, alignment: .center) @@ -1622,11 +1637,12 @@ struct SetupPage { struct SetupPageView: View { @State var startImageTransition : Bool = false + @AppStorage("enableAnimations") var enableAnimations = true let page: SetupPage var body: some View { VStack(spacing: tvOSSpacing) { - if #available(iOS 18.0, *){ + if #available(iOS 18.0, *), enableAnimations{ Image(systemName: (startImageTransition) ? page.standardImage : page.transitionImage) .font(.system(size: tvOSImageSize)) .foregroundColor(.blue) diff --git a/LocalDevVPN/Localization/en.lproj/Localizable.strings b/LocalDevVPN/Localization/en.lproj/Localizable.strings index 6bb7f90..8146859 100644 --- a/LocalDevVPN/Localization/en.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/en.lproj/Localizable.strings @@ -71,7 +71,7 @@ "data_collection_policy" = "Data Collection Policy"; "app_version" = "App Version"; "help_and_support" = "Help and Support"; -"language" = "Set Language"; +"language" = "Graphic Settings"; "dropdown_language" = "Language"; "settings" = "Settings"; "done" = "Done"; @@ -150,6 +150,8 @@ "setup_get_started" = "Get Started"; "setup_next" = "Next"; "setup_skip" = "Skip"; +"enable_animation" = "Enable Icon Animations"; +"not_supported" = "\nNot supported for your version of iOS (18.0 or later)."; /*MARK: Restart pop-up*/ "restart_title" = "Restart"; diff --git a/LocalDevVPN/Localization/it.lproj/Localizable.strings b/LocalDevVPN/Localization/it.lproj/Localizable.strings index 2811cd0..73d9b71 100644 --- a/LocalDevVPN/Localization/it.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/it.lproj/Localizable.strings @@ -71,7 +71,7 @@ "data_collection_policy" = "Politica di Raccolta Dati"; "app_version" = "Versione"; "help_and_support" = "Guida e Supporto"; -"language" = "Impostazioni Lingua"; +"language" = "Impostazioni Grafica"; "dropdown_language" = "Lingua"; "settings" = "Impostazioni"; "done" = "Fine"; @@ -150,6 +150,8 @@ "setup_get_started" = "Inizia"; "setup_next" = "Avanti"; "setup_skip" = "Salta"; +"enable_animation" = "Attiva Animazioni Icone"; +"not_supported" = "\nNon supportate per la tua versione di iOS (18.0 o successivi)."; /*MARK: Restart pop-up*/ "restart_title" = "Riavvia"; From 81d3bf51b79bb969c77e107dddfeab4471bb9980 Mon Sep 17 00:00:00 2001 From: Andrea Filice Date: Fri, 26 Dec 2025 21:18:58 +0100 Subject: [PATCH 6/6] fix: fixed some bugs * fixed 'enableAnimations' not checking for Main Animations * performance improvements --- LocalDevVPN/ContentView.swift | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/LocalDevVPN/ContentView.swift b/LocalDevVPN/ContentView.swift index c89a67d..884691a 100644 --- a/LocalDevVPN/ContentView.swift +++ b/LocalDevVPN/ContentView.swift @@ -871,6 +871,7 @@ struct StatusOverviewCard: View { struct StatusGlyphView: View { @StateObject private var tunnelManager = TunnelManager.shared @State private var ringScale: CGFloat = 1.0 + @AppStorage("enableAnimations") var enableAnimations = true var body: some View { ZStack { @@ -882,7 +883,7 @@ struct StatusGlyphView: View { Circle() .fill(tunnelManager.tunnelStatus.color.opacity(0.15)) - if #available(iOS 18.0, *){ + if #available(iOS 18.0, *), enableAnimations{ Image(systemName: tunnelManager.tunnelStatus.systemImage) .font(.title) .foregroundColor(tunnelManager.tunnelStatus.color) @@ -912,11 +913,8 @@ struct ConnectivityControlsCard: View { DashboardCard { VStack(alignment: .leading, spacing: 18) { VStack(alignment: .leading, spacing: 4) { - HStack(spacing: 6){ - Image(systemName: "network.badge.shield.half.filled") - Text("connection") - .font(.headline) - } + Label("connection", systemImage: "network.badge.shield.half.filled") + .font(.headline) Text("start_or_stop_the_secure_local_tunnel") .font(.footnote) .foregroundColor(.secondary) @@ -1026,11 +1024,8 @@ struct ConnectionStatsView: View { DashboardCard { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 3) { - HStack(spacing: 6) { - Image(systemName: "wifi.badge.lock") - Text("session_details") - .font(.headline) - } + Label("session_details", systemImage: "wifi.badge.lock") + .font(.headline) Text("live_stats_while_the_tunnel_is_connected") .font(.footnote) .foregroundColor(.secondary)