Improve On Demand Settings

This commit is contained in:
世界
2026-01-01 19:37:36 +08:00
parent e2c378d42b
commit 9b179fe5cd
2 changed files with 519 additions and 134 deletions
@@ -2,12 +2,35 @@ import Foundation
import Library
import SwiftUI
private enum OnDemandMode: String, CaseIterable, Identifiable {
case disabled
case alwaysOn
case enabled
var id: String { rawValue }
var name: String {
switch self {
case .disabled: "Disabled"
case .alwaysOn: "Always On"
case .enabled: "Enabled"
}
}
var description: String {
switch self {
case .disabled: "VPN will not connect automatically."
case .alwaysOn: "Automatically connect VPN on any network."
case .enabled: "Automatically connect or disconnect VPN based on rules."
}
}
}
public struct OnDemandRulesView: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@State private var isLoading = true
@State private var alert: AlertState?
@State private var alwaysOn = false
@State private var onDemandEnabled = false
@State private var mode: OnDemandMode = .disabled
@State private var rules: [OnDemandRule] = []
@State private var editingRule: OnDemandRule?
@State private var isAddingRule = false
@@ -27,10 +50,9 @@ public struct OnDemandRulesView: View {
}
} else {
FormView {
alwaysOnToggle
enableToggle
modePicker
if !alwaysOn, onDemandEnabled {
if mode == .enabled {
rulesSection
}
@@ -53,6 +75,7 @@ public struct OnDemandRulesView: View {
}
.environment(\.editMode, $editMode)
#endif
#if !os(tvOS)
.platformSheet(isPresented: $isAddingRule) {
OnDemandRuleEditView(rule: OnDemandRule(), isNew: true) { newRule in
rules.append(newRule)
@@ -71,36 +94,31 @@ public struct OnDemandRulesView: View {
}
}
}
#endif
}
private var alwaysOnToggle: some View {
FormToggle("Always On", """
Automatically connect VPN on any network.
When enabled, VPN connects automatically when network is available. Custom rules below will be disabled.
""", $alwaysOn) { newValue in
await SharedPreferences.alwaysOn.set(newValue)
await updateService()
private var modePicker: some View {
Section {
Picker("Mode", selection: $mode) {
ForEach(OnDemandMode.allCases) { m in
Text(m.name).tag(m)
}
}
.onChange(of: mode) { newValue in
Task {
await saveMode(newValue)
}
}
} footer: {
Text(mode.description)
}
}
private var enableToggle: some View {
FormToggle("Custom Rules", """
Automatically connect or disconnect VPN based on custom rules.
When enabled, iOS manages VPN state automatically. You may need to use the in-app interface to stop the service.
""", $onDemandEnabled) { newValue in
await SharedPreferences.onDemandEnabled.set(newValue)
await updateService()
}
.disabled(alwaysOn)
}
@ViewBuilder
private var rulesSection: some View {
Section {
if rules.isEmpty {
Text("No rules configured. Add a rule to specify when VPN should connect or disconnect.")
Text("Empty rules")
.foregroundStyle(.secondary)
.font(.callout)
} else {
@@ -124,13 +142,26 @@ public struct OnDemandRulesView: View {
HStack {
Text("Rules")
Spacer()
Button {
isAddingRule = true
} label: {
Image(systemName: "plus.circle.fill")
}
#if os(macOS)
.buttonStyle(.plain)
#if os(tvOS)
FormNavigationLink {
OnDemandRuleEditView(rule: OnDemandRule(), isNew: true) { newRule in
rules.append(newRule)
Task {
await saveRules()
}
}
} label: {
Image(systemName: "plus.circle.fill")
}
#else
Button {
isAddingRule = true
} label: {
Image(systemName: "plus.circle.fill")
}
#if os(macOS)
.buttonStyle(.plain)
#endif
#endif
}
} footer: {
@@ -140,54 +171,52 @@ public struct OnDemandRulesView: View {
@ViewBuilder
private func ruleRow(_ rule: OnDemandRule) -> some View {
Button {
editingRule = rule
} label: {
HStack {
VStack(alignment: .leading, spacing: 4) {
HStack {
actionIcon(rule.action)
Text(rule.action.name)
.fontWeight(.medium)
#if os(tvOS)
FormNavigationLink {
OnDemandRuleEditView(rule: rule, isNew: false) { updatedRule in
if let index = rules.firstIndex(where: { $0.id == updatedRule.id }) {
rules[index] = updatedRule
Task {
await saveRules()
}
}
}
} label: {
VStack(alignment: .leading, spacing: 4) {
Text(rule.action.name)
.fontWeight(.medium)
Text(ruleDescription(rule))
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
#if !os(tvOS)
}
#else
Button {
editingRule = rule
} label: {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(rule.action.name)
.fontWeight(.medium)
Text(ruleDescription(rule))
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(.secondary)
.font(.caption)
#endif
}
.contentShape(Rectangle())
}
.contentShape(Rectangle())
}
#if os(macOS)
.buttonStyle(.plain)
#elseif os(iOS)
.foregroundStyle(.primary)
#if os(macOS)
.buttonStyle(.plain)
#elseif os(iOS)
.foregroundStyle(.primary)
#endif
#endif
}
@ViewBuilder
private func actionIcon(_ action: OnDemandRuleAction) -> some View {
switch action {
case .connect:
Image(systemName: "arrow.up.circle.fill")
.foregroundStyle(.green)
case .disconnect:
Image(systemName: "arrow.down.circle.fill")
.foregroundStyle(.red)
case .evaluateConnection:
Image(systemName: "questionmark.circle.fill")
.foregroundStyle(.orange)
case .ignore:
Image(systemName: "minus.circle.fill")
.foregroundStyle(.gray)
}
}
private func ruleDescription(_ rule: OnDemandRule) -> String {
var parts: [String] = []
@@ -243,15 +272,21 @@ public struct OnDemandRulesView: View {
.foregroundStyle(.red)
}
private func saveMode(_ newMode: OnDemandMode) async {
let alwaysOn = newMode == .alwaysOn
let onDemandEnabled = newMode == .enabled
await SharedPreferences.alwaysOn.set(alwaysOn)
await SharedPreferences.onDemandEnabled.set(onDemandEnabled)
await updateService()
}
private func updateService() async {
guard let profile = environments.extensionProfile, profile.status.isConnected else {
return
}
do {
let alwaysOnValue = await SharedPreferences.alwaysOn.get()
let onDemandEnabledValue = await SharedPreferences.onDemandEnabled.get()
let enabled = alwaysOnValue || onDemandEnabledValue
try await profile.updateOnDemand(enabled: enabled, useDefaultRules: alwaysOnValue)
let enabled = mode != .disabled
try await profile.updateOnDemand(enabled: enabled, useDefaultRules: mode == .alwaysOn)
} catch {
alert = AlertState(error: error)
}
@@ -268,8 +303,15 @@ public struct OnDemandRulesView: View {
}
private func loadSettings() async {
alwaysOn = await SharedPreferences.alwaysOn.get()
onDemandEnabled = await SharedPreferences.onDemandEnabled.get()
let alwaysOn = await SharedPreferences.alwaysOn.get()
let onDemandEnabled = await SharedPreferences.onDemandEnabled.get()
if alwaysOn {
mode = .alwaysOn
} else if onDemandEnabled {
mode = .enabled
} else {
mode = .disabled
}
rules = await SharedPreferences.onDemandRules.get()
isLoading = false
}
@@ -305,11 +347,21 @@ private struct OnDemandRuleEditView: View {
if rule.action == .evaluateConnection {
connectionRulesSection
}
#if os(tvOS)
Section {
Button(isNew ? "Create" : "Save") {
onSave(rule)
dismiss()
}
.disabled(!isProbeURLValid)
}
#endif
}
.navigationTitle(isNew ? "New Rule" : "Edit Rule")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
#if !os(tvOS)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
@@ -317,13 +369,14 @@ private struct OnDemandRuleEditView: View {
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
Button(isNew ? "Create" : "Save") {
onSave(rule)
dismiss()
}
.disabled(!isProbeURLValid)
}
}
#endif
#if os(macOS)
.formStyle(.grouped)
#endif