init view

This commit is contained in:
anlicheng 2025-05-12 16:27:55 +08:00
parent c09a064eff
commit 269ee4f524
12 changed files with 684 additions and 10 deletions

90
punchnet/AbortView.swift Normal file
View File

@ -0,0 +1,90 @@
//
// AbortView.swift
// sdlan
//
// Created by on 2024/6/5.
//
import Foundation
import SwiftUI
struct AbortView: View {
struct AlertShow: Identifiable {
enum ShowContent {
case error(String)
case upgrade(String, String)
}
var id: String
var content: ShowContent
}
@State private var alertShow: AlertShow?
var body: some View {
VStack {
Image("logo")
Text("sdlan")
Text("Version1.1")
Button {
Task {
guard let response = try? await SDLAPI.checkVersion(clientId: "test", version: 1, channel: "macos") else {
DispatchQueue.main.async {
self.alertShow = AlertShow(id: "network_error", content: .error("Network Error"))
}
return
}
if let result = response.result {
if result.upgrade_type == 0 {
DispatchQueue.main.async {
self.alertShow = AlertShow(id: "upgrade_0", content: .upgrade(result.upgrade_prompt, ""))
}
} else if result.upgrade_type == 1 {
DispatchQueue.main.async {
self.alertShow = AlertShow(id: "upgrade_1", content: .upgrade(result.upgrade_prompt, result.upgrade_address))
}
} else if result.upgrade_type == 2 {
DispatchQueue.main.async {
self.alertShow = AlertShow(id: "upgrade_1", content: .upgrade(result.upgrade_prompt, result.upgrade_address))
}
}
} else if let error = response.error {
DispatchQueue.main.async {
self.alertShow = AlertShow(id: "response_error", content: .error(error.message))
}
}
}
} label: {
Text("版本检测")
.font(.system(size: 16, weight: .regular))
.foregroundColor(.white)
.cornerRadius(5.0)
}
.frame(width: 138, height: 33)
.buttonStyle(PlainButtonStyle())
.background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255))
.cornerRadius(5.0)
}
.alert(item: $alertShow) { show in
switch show.content {
case .error(let errorMessage):
Alert(title: Text("错误提示"), message: Text(errorMessage))
case .upgrade(let prompt, let address):
Alert(title: Text("版本升级"), message: Text(prompt), primaryButton: .default(Text("升级版本"), action: {
if let url = URL(string: address) {
// schema: "macappstore://apps.apple.com/app/idYOUR_APP_ID"
NSWorkspace.shared.open(url)
}
}), secondaryButton: .cancel())
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "logo.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,18 +1,101 @@
//
// ContentView.swift
// punchnet
// sdlan
//
// Created by on 2025/5/12.
// Created by on 2024/1/17.
//
import SwiftUI
import SwiftData
import Combine
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
//@Environment(\.modelContext) private var modelContext
//@Query private var items: [Item]
@AppStorage("token") private var token: String = ""
@ObservedObject private var vpnManager = VPNManager.shared
@State private var showAlert = false
@State private var showStunAlert = false
@State private var message: NoticeMessage.InboundMessage = .none
@State private var cancel: AnyCancellable?
var body: some View {
VStack(alignment: .center, spacing: 10) {
Image("logo")
Text("PUNCHENT")
.font(.system(size: 46, weight: .bold))
.foregroundColor(.white)
.cornerRadius(5.0)
Spacer()
.frame(width: 1, height: 10)
TextField("邀请码", text: $token)
.multilineTextAlignment(.center)
.frame(width: 245, height: 27)
.cornerRadius(5.0)
Spacer()
.frame(width: 1, height: 10)
Button(action: {
Task {
switch self.vpnManager.vpnStatus {
case .connected:
try await vpnManager.disableVpn()
case .disconnected:
if self.token.isEmpty {
self.showAlert = true
return
}
try await vpnManager.enableVpn(options: [
"version:": SystemConfig.version as NSObject,
"installed_channel": SystemConfig.installedChannel as NSObject,
"token": self.token as NSObject
])
}
}
}, label: {
Text(vpnManager.title)
.font(.system(size: 16, weight: .regular))
.foregroundColor(vpnManager.color)
.cornerRadius(5.0)
})
.frame(width: 138, height: 33)
.buttonStyle(PlainButtonStyle())
.background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255))
.cornerRadius(5.0)
}
.frame(width: 380, height: 560)
.background(Color(red: 36 / 255, green: 38 / 255, blue: 51 / 255))
.alert(isPresented: $showAlert) {
Alert(title: Text("请输入正确的邀请码"))
}
.alert(isPresented: $showStunAlert) {
switch self.message {
case .upgradeMessage(let upgradeMessage):
Alert(title: Text(upgradeMessage.prompt))
case .alertMessage(let alertMessage):
Alert(title: Text(alertMessage.alert))
default:
Alert(title: Text(""))
}
}
.onAppear {
self.cancel = UDPNoticeCenterServer.shared.messageFlow.sink{ message in
DispatchQueue.main.async {
self.message = message
self.showStunAlert = true
}
}
}
/*
NavigationSplitView {
List {
ForEach(items) { item in
@ -35,25 +118,26 @@ struct ContentView: View {
} detail: {
Text("Select an item")
}
*/
}
private func addItem() {
withAnimation {
let newItem = Item(timestamp: Date())
modelContext.insert(newItem)
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(items[index])
}
// for index in offsets {
// modelContext.delete(items[index])
// }
}
}
}
#Preview {
ContentView()
.modelContainer(for: Item.self, inMemory: true)
//.modelContainer(for: Item.self, inMemory: true)
}

View File

@ -0,0 +1,50 @@
//
// NoticeMessage.swift
// sdlan
//
// Created by on 2024/6/3.
//
import Foundation
struct NoticeMessage {
//
enum NoticeType: UInt8 {
case upgrade = 1
case alert = 2
}
struct UpgradeMessage: Codable {
let prompt: String
let address: String
var binaryData: Data {
let json = try! JSONEncoder().encode(self)
var data = Data()
data.append(contentsOf: [NoticeType.upgrade.rawValue])
data.append(json)
return data
}
}
struct AlertMessage: Codable {
let alert: String
var binaryData: Data {
let json = try! JSONEncoder().encode(self)
var data = Data()
data.append(contentsOf: [NoticeType.alert.rawValue])
data.append(json)
return data
}
}
enum InboundMessage {
case none
case upgradeMessage(UpgradeMessage)
case alertMessage(AlertMessage)
}
}

View File

@ -0,0 +1,47 @@
//
// SDLApi.swift
// sdlan
//
// Created by on 2024/6/5.
//
import Foundation
struct JSONRPCResponse<T: Decodable>: Decodable {
let result: T?
let error: JSONRPCError?
}
struct JSONRPCError: Decodable {
let code: Int
let message: String
let data: String?
}
struct SDLAPI {
struct Upgrade: Decodable {
let upgrade_type: Int
let upgrade_prompt: String
let upgrade_address: String
}
static func checkVersion(clientId: String, version: Int, channel: String) async throws -> JSONRPCResponse<Upgrade> {
let params: [String:Any] = [
"client_id": clientId,
"version": version,
"channel": channel
]
let postData = try! JSONSerialization.data(withJSONObject: params)
var request = URLRequest(url: URL(string: "http://127.0.0.1:18082/test/upgrade")!)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = postData
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(JSONRPCResponse<Upgrade>.self, from: data)
}
}

View File

@ -0,0 +1,18 @@
//
// SystemConfig.swift
// sdlan
//
// Created by on 2024/6/3.
//
import Foundation
struct SystemConfig {
//
static let version = 1
static let version_name = "1.1"
//
static let installedChannel = "MacAppStore"
}

View File

@ -0,0 +1,86 @@
//
// UDPMessageCenterServer.swift
// sdlan
//
// Created by on 2024/5/20.
//
import Foundation
import NIOCore
import NIOPosix
import Combine
final class UDPNoticeCenterServer: ChannelInboundHandler {
public typealias InboundIn = AddressedEnvelope<ByteBuffer>
public typealias OutboundOut = AddressedEnvelope<ByteBuffer>
private var group: MultiThreadedEventLoopGroup?
private var thread: Thread?
var messageFlow = PassthroughSubject<NoticeMessage.InboundMessage, Never>()
static let shared = UDPNoticeCenterServer()
private init() {
}
func start() {
self.thread = Thread {
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let bootstrap = DatagramBootstrap(group: self.group!)
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.channelInitializer { channel in
channel.pipeline.addHandler(self)
}
let channel = try! bootstrap.bind(host: "127.0.0.1", port: 50195).wait()
try! channel.closeFuture.wait()
}
self.thread?.start()
}
func stop() {
self.thread?.cancel()
try? self.group?.syncShutdownGracefully()
}
// --MARK: ChannelInboundHandler
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let envelope = self.unwrapInboundIn(data)
var buffer = envelope.data
guard let type = buffer.readInteger(as: UInt8.self),
let noticeType = NoticeMessage.NoticeType(rawValue: type),
let bytes = buffer.readBytes(length: buffer.readableBytes) else {
return
}
switch noticeType {
case .upgrade:
if let upgradeMessage = try? JSONDecoder().decode(NoticeMessage.UpgradeMessage.self, from: Data(bytes)) {
DispatchQueue.main.async {
self.messageFlow.send(.upgradeMessage(upgradeMessage))
}
}
case .alert:
if let alertMessage = try? JSONDecoder().decode(NoticeMessage.AlertMessage.self, from: Data(bytes)) {
DispatchQueue.main.async {
self.messageFlow.send(.alertMessage(alertMessage))
}
}
}
}
public func channelReadComplete(context: ChannelHandlerContext) {
// As we are not really interested getting notified on success or failure we just pass nil as promise to
// reduce allocations.
context.flush()
}
public func errorCaught(context: ChannelHandlerContext, error: Error) {
// As we are not really interested getting notified on success or failure we just pass nil as promise to
// reduce allocations.
context.close(promise: nil)
}
}

View File

@ -0,0 +1,35 @@
//
// DataExtension.swift
// sdlan
//
// Created by on 2024/2/1.
//
import Foundation
extension Data {
mutating public func append(int32val: Int32) {
self.append(contentsOf: [
(UInt8) (int32val >> 24 & 0xFF),
(UInt8) (int32val >> 16 & 0xFF),
(UInt8) (int32val >> 8 & 0xFF),
(UInt8) (int32val & 0xFF)
])
}
mutating public func append(int16val: Int16) {
self.append(contentsOf: [
(UInt8) (int16val >> 8 & 0xFF),
(UInt8) (int16val & 0xFF)
])
}
mutating public func append(str: String) {
if let data = str.data(using: .utf8) {
self.append(contentsOf: data)
}
}
}

98
punchnet/VPNManager.swift Normal file
View File

@ -0,0 +1,98 @@
//
// VPNManager.swift
// sdlan
//
// Created by on 2024/1/17.
//
import Foundation
import NetworkExtension
import SwiftUI
// vpn
class VPNManager: ObservableObject {
static let shared = VPNManager()
@Published var vpnStatus: VPNStatus = .disconnected
@Published var title: String = "启动"
@Published var color: Color = .white
enum VPNStatus {
case connected
case disconnected
}
private init() {
}
// vpn
func enableVpn(options: [String : NSObject]? = nil) async throws {
let manager = try await loadAndCreateProviderManager()
try await manager.loadFromPreferences()
self.addVPNStatusObserver(manager)
try manager.connection.startVPNTunnel(options: options)
}
// vpn
func disableVpn() async throws {
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
managers.first?.connection.stopVPNTunnel()
}
// MARK: - Private Methods
// VPN
private func addVPNStatusObserver(_ manager: NETunnelProviderManager) {
NotificationCenter.default.removeObserver(self)
NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: manager.connection, queue: .main) { [unowned self] (notification) -> Void in
// vpn
switch manager.connection.status {
case .invalid, .disconnected, .disconnecting:
self.vpnStatus = .disconnected
self.title = "启动"
self.color = .white
case .connecting, .connected, .reasserting:
self.vpnStatus = .connected
self.title = "停止"
self.color = .red
@unknown default:
self.vpnStatus = .disconnected
self.title = "启动"
self.color = .red
}
}
}
// MARK: - Private Methods
//
private func loadAndCreateProviderManager() async throws -> NETunnelProviderManager {
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
let manager = managers.first ?? NETunnelProviderManager()
manager.localizedDescription = "sdlan"
manager.isEnabled = true
// PacketTunnel
let protocolConfiguration = NETunnelProviderProtocol()
protocolConfiguration.serverAddress = "sdlan"
protocolConfiguration.providerConfiguration = [String:AnyObject]()
protocolConfiguration.providerBundleIdentifier = "com.jihe.sdlan.Tun"
manager.protocolConfiguration = protocolConfiguration
manager.isOnDemandEnabled = false
manager.isEnabled = true
try await manager.saveToPreferences()
return manager
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.developer.networking.vpn.api</key>
<array>
<string>allow-vpn</string>
</array>
<key>com.apple.developer.system-extension.install</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>$(TeamIdentifierPrefix)</string>
</array>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

117
punchnet/sdlanApp.swift Normal file
View File

@ -0,0 +1,117 @@
//
// sdlanApp.swift
// sdlan
//
// Created by on 2024/1/17.
//
import SwiftUI
import AppKit
import SwiftData
import Combine
@main
struct sdlanApp: App {
/*
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Item.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
*/
@Environment(\.openWindow) private var openWindow
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@AppStorage("token") var token: String = ""
@ObservedObject var vpnManager = VPNManager.shared
var body: some Scene {
WindowGroup(id: "mainWindow") {
ContentView()
}
.commands {
CommandGroup(replacing: .appInfo) {
Button {
openWindow(id: "abortSDLAN")
} label: {
Text("About sdlan")
}
}
}
Window("", id: "abortSDLAN") {
AbortView()
}
//.modelContainer(sharedModelContainer)
MenuBarExtra("sdlanApp", systemImage: "hammer") {
VStack {
Button(action: {
self.menuClick()
}, label: {
Text(vpnManager.title)
})
Divider()
Button(action: {
NSApplication.shared.terminate(nil)
}, label: { Text("退出") })
}
}
.menuBarExtraStyle(.menu)
}
private func menuClick() {
switch self.vpnManager.vpnStatus {
case .disconnected:
if token.isEmpty {
let windows = NSApplication.shared.windows
if let window = windows.first(where: {$0.title == "sdlan"}) {
window.level = .floating
} else {
self.openWindow(id: "mainWindow")
}
} else {
Task {
try await vpnManager.enableVpn(options: ["token": token as NSObject])
}
}
case .connected:
Task {
try await vpnManager.disableVpn()
}
}
}
}
// APP
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationWillFinishLaunching(_ notification: Notification) {
UDPNoticeCenterServer.shared.start()
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
Task {
try await VPNManager.shared.disableVpn()
DispatchQueue.main.async {
sender.reply(toApplicationShouldTerminate: true)
}
UDPNoticeCenterServer.shared.stop()
}
return .terminateLater
}
}