fix token history
This commit is contained in:
parent
9f1eadeaaa
commit
78f8752aa7
2
dmg.sh
2
dmg.sh
@ -1,3 +1,3 @@
|
||||
#! /bin/sh
|
||||
|
||||
create-dmg --volname "punchnet" --window-pos 200 120 --window-size 800 400 --icon "punchnet.app" 200 190 --hide-extension "punchnet.app" --app-drop-link 600 185 ~/Desktop/punchnet.dmg /Users/anlicheng/Desktop/punchnetv6
|
||||
create-dmg --volname "punchnet" --window-pos 200 120 --window-size 800 400 --icon "punchnet.app" 200 190 --hide-extension "punchnet.app" --app-drop-link 600 185 ~/Desktop/punchnet.dmg /Users/anlicheng/Desktop/punchnetv10
|
||||
|
||||
@ -12,8 +12,17 @@ struct AppContextError: Error {
|
||||
let message: String
|
||||
}
|
||||
|
||||
struct TokenHistoryItem: Codable, Hashable {
|
||||
let token: String
|
||||
let networkName: String?
|
||||
}
|
||||
|
||||
@Observable
|
||||
class AppContext {
|
||||
private let cachedTokenAccount = "token"
|
||||
private let cachedTokenHistoryAccount = "tokenHistory"
|
||||
private let maxCachedTokenCount = 10
|
||||
|
||||
private var vpnManager = VPNManager.shared
|
||||
|
||||
// 调用 "/connect" 之后的网络信息
|
||||
@ -77,10 +86,7 @@ class AppContext {
|
||||
let networkSession = try await AuthService.loginWithToken(token: token)
|
||||
self.loginCredit = .token(token: token, session: networkSession)
|
||||
self.selectedExitNodeIp = self.loadExitNodeIp(networkId: networkSession.networkId)
|
||||
// 将数据缓存到keychain
|
||||
if let data = token.data(using: .utf8) {
|
||||
try KeychainStore.shared.save(data, account: "token")
|
||||
}
|
||||
try self.saveCacheToken(token, networkName: networkSession.networkName)
|
||||
}
|
||||
|
||||
func loginWith(username: String, password: String) async throws {
|
||||
@ -158,12 +164,87 @@ class AppContext {
|
||||
}
|
||||
|
||||
func loadCacheToken() -> String? {
|
||||
if let data = try? KeychainStore.shared.load(account: "token") {
|
||||
return String(data: data, encoding: .utf8)
|
||||
if let item = self.loadCacheTokenHistory().first {
|
||||
return item.token
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadCacheTokenHistory() -> [TokenHistoryItem] {
|
||||
if let data = try? KeychainStore.shared.load(account: cachedTokenHistoryAccount),
|
||||
let items = try? JSONDecoder().decode([TokenHistoryItem].self, from: data) {
|
||||
let normalizedItems = self.normalizedTokenHistory(items)
|
||||
if !normalizedItems.isEmpty {
|
||||
return normalizedItems
|
||||
}
|
||||
}
|
||||
|
||||
if let data = try? KeychainStore.shared.load(account: cachedTokenHistoryAccount),
|
||||
let tokens = try? JSONDecoder().decode([String].self, from: data) {
|
||||
let normalizedItems = self.normalizedTokenHistory(tokens.map {
|
||||
TokenHistoryItem(token: $0, networkName: nil)
|
||||
})
|
||||
|
||||
if !normalizedItems.isEmpty {
|
||||
return normalizedItems
|
||||
}
|
||||
}
|
||||
|
||||
if let data = try? KeychainStore.shared.load(account: cachedTokenAccount),
|
||||
let token = String(data: data, encoding: .utf8) {
|
||||
return self.normalizedTokenHistory([
|
||||
TokenHistoryItem(token: token, networkName: nil)
|
||||
])
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
private func saveCacheToken(_ token: String, networkName: String) throws {
|
||||
let normalizedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !normalizedToken.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let normalizedNetworkName = networkName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
var items = self.loadCacheTokenHistory()
|
||||
items.removeAll { $0.token == normalizedToken }
|
||||
items.insert(
|
||||
TokenHistoryItem(
|
||||
token: normalizedToken,
|
||||
networkName: normalizedNetworkName.isEmpty ? nil : normalizedNetworkName
|
||||
),
|
||||
at: 0
|
||||
)
|
||||
items = Array(items.prefix(maxCachedTokenCount))
|
||||
|
||||
let historyData = try JSONEncoder().encode(items)
|
||||
try KeychainStore.shared.save(historyData, account: cachedTokenHistoryAccount)
|
||||
|
||||
if let data = normalizedToken.data(using: .utf8) {
|
||||
try KeychainStore.shared.save(data, account: cachedTokenAccount)
|
||||
}
|
||||
}
|
||||
|
||||
private func normalizedTokenHistory(_ items: [TokenHistoryItem]) -> [TokenHistoryItem] {
|
||||
var seen = Set<String>()
|
||||
|
||||
return items.compactMap { item in
|
||||
let normalizedToken = item.token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !normalizedToken.isEmpty, !seen.contains(normalizedToken) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen.insert(normalizedToken)
|
||||
let normalizedNetworkName = item.networkName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return TokenHistoryItem(
|
||||
token: normalizedToken,
|
||||
networkName: normalizedNetworkName?.isEmpty == true ? nil : normalizedNetworkName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func loadCacheUsernameAndPassword() -> (String, String)? {
|
||||
if let data = try? KeychainStore.shared.load(account: "accountAndPasword"),
|
||||
let str = String(data: data, encoding: .utf8) {
|
||||
|
||||
@ -191,16 +191,50 @@ struct LoginTokenView: View {
|
||||
@Environment(AppContext.self) var appContext: AppContext
|
||||
|
||||
@State private var token = ""
|
||||
@State private var tokenHistory: [TokenHistoryItem] = []
|
||||
@State private var wantsTokenHistory = false
|
||||
@State private var showTokenHistory = false
|
||||
@State private var isLoading = false
|
||||
@FocusState private var isTokenFocused: Bool
|
||||
|
||||
// 错误提示
|
||||
@State private var showAlert: Bool = false
|
||||
@State private var errorMessage: String = ""
|
||||
|
||||
private var normalizedToken: String {
|
||||
token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
private var visibleTokenHistory: [TokenHistoryItem] {
|
||||
guard !normalizedToken.isEmpty else {
|
||||
return tokenHistory
|
||||
}
|
||||
|
||||
let matchedItems = tokenHistory.filter {
|
||||
$0.token.localizedCaseInsensitiveContains(normalizedToken)
|
||||
|| ($0.networkName?.localizedCaseInsensitiveContains(normalizedToken) == true)
|
||||
}
|
||||
|
||||
return matchedItems
|
||||
}
|
||||
|
||||
private var tokenHistoryPopoverBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: {
|
||||
showTokenHistory
|
||||
},
|
||||
set: { isPresented in
|
||||
showTokenHistory = isPresented
|
||||
if !isPresented {
|
||||
wantsTokenHistory = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
CustomTextField(title: "请输入认证密钥 (Token)", text: $token, icon: "key.fill")
|
||||
.frame(width: 280)
|
||||
tokenInputField
|
||||
|
||||
Button(action: {
|
||||
Task { @MainActor in
|
||||
@ -214,26 +248,121 @@ struct LoginTokenView: View {
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.frame(width: 280)
|
||||
.disabled(token.isEmpty || isLoading)
|
||||
.disabled(normalizedToken.isEmpty || isLoading)
|
||||
}
|
||||
.alert(isPresented: $showAlert) {
|
||||
Alert(title: Text("提示"), message: Text(self.errorMessage))
|
||||
}
|
||||
.onAppear {
|
||||
self.tokenHistory = self.appContext.loadCacheTokenHistory()
|
||||
if let cacheToken = self.appContext.loadCacheToken() {
|
||||
self.token = cacheToken
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var tokenInputField: some View {
|
||||
HStack {
|
||||
Image(systemName: "key.fill")
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: 20)
|
||||
|
||||
TextField("请输入认证密钥 (Token)", text: $token)
|
||||
.textFieldStyle(.plain)
|
||||
.focused($isTokenFocused)
|
||||
}
|
||||
.padding(8)
|
||||
.frame(width: 280)
|
||||
.background(Color(NSColor.controlBackgroundColor).opacity(0.5))
|
||||
.cornerRadius(6)
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.secondary.opacity(0.2), lineWidth: 1))
|
||||
.onTapGesture {
|
||||
self.presentTokenHistory()
|
||||
}
|
||||
.onChange(of: isTokenFocused) {
|
||||
if isTokenFocused {
|
||||
self.presentTokenHistory()
|
||||
} else if !showTokenHistory {
|
||||
self.wantsTokenHistory = false
|
||||
}
|
||||
}
|
||||
.onChange(of: token) {
|
||||
if wantsTokenHistory {
|
||||
self.refreshTokenHistoryPopover()
|
||||
}
|
||||
}
|
||||
.popover(isPresented: tokenHistoryPopoverBinding, arrowEdge: .bottom) {
|
||||
tokenHistoryPopover
|
||||
}
|
||||
}
|
||||
|
||||
private var tokenHistoryPopover: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(visibleTokenHistory, id: \.token) { item in
|
||||
Button(action: {
|
||||
self.token = item.token
|
||||
self.dismissTokenHistory()
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: item.token == normalizedToken ? "checkmark" : "key.fill")
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: 14)
|
||||
|
||||
Text(tokenHistoryTitle(item))
|
||||
.font(.system(size: 12))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.frame(width: 280)
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
|
||||
private func presentTokenHistory() {
|
||||
self.wantsTokenHistory = true
|
||||
self.refreshTokenHistoryPopover()
|
||||
}
|
||||
|
||||
private func dismissTokenHistory() {
|
||||
self.wantsTokenHistory = false
|
||||
self.showTokenHistory = false
|
||||
}
|
||||
|
||||
private func refreshTokenHistoryPopover() {
|
||||
self.tokenHistory = self.appContext.loadCacheTokenHistory()
|
||||
self.showTokenHistory = wantsTokenHistory && !visibleTokenHistory.isEmpty
|
||||
}
|
||||
|
||||
private func tokenHistoryTitle(_ item: TokenHistoryItem) -> String {
|
||||
guard let networkName = item.networkName, !networkName.isEmpty else {
|
||||
return item.token
|
||||
}
|
||||
|
||||
return "\(item.token) (\(networkName))"
|
||||
}
|
||||
|
||||
private func login() async {
|
||||
let finalToken = self.normalizedToken
|
||||
guard !finalToken.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isLoading = true
|
||||
self.dismissTokenHistory()
|
||||
defer {
|
||||
self.isLoading = false
|
||||
}
|
||||
|
||||
do {
|
||||
_ = try await appContext.loginWith(token: token)
|
||||
_ = try await appContext.loginWith(token: finalToken)
|
||||
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
|
||||
self.appContext.appScene = .logined
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user