Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion airsync-mac/Components/Buttons/SaveAndRestartButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ struct SaveAndRestartButton: View {
WebSocketServer.shared.start(port: portNumber)
onRestart?(portNumber)

appState.shouldRefreshQR = true
// Delay QR refresh to ensure server has restarted
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
appState.shouldRefreshQR = true
}
}
.controlSize(.large)
.applyGlassIfAvailable()
Expand Down
49 changes: 48 additions & 1 deletion airsync-mac/Core/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ class AppState: ObservableObject {
// Initialize persisted UI toggles
self.isMusicCardHidden = UserDefaults.standard.bool(forKey: "isMusicCardHidden")

// Load and validate saved network adapter
let savedAdapterName = UserDefaults.standard.string(forKey: "selectedNetworkAdapterName")
self.selectedNetworkAdapterName = validateAndGetNetworkAdapter(savedName: savedAdapterName)

self.myDevice = Device(
name: name,
ipAddress: WebSocketServer.shared
Expand Down Expand Up @@ -119,7 +123,11 @@ class AppState: ObservableObject {
@Published var adbConnected: Bool = false
@Published var adbConnecting: Bool = false
@Published var currentDeviceWallpaperBase64: String? = nil
@Published var selectedNetworkAdapterName: String? // e.g., "en0"
@Published var selectedNetworkAdapterName: String? { // e.g., "en0"
didSet {
UserDefaults.standard.set(selectedNetworkAdapterName, forKey: "selectedNetworkAdapterName")
}
}
@Published var showMenubarText: Bool {
didSet {
UserDefaults.standard.set(showMenubarText, forKey: "showMenubarText")
Expand Down Expand Up @@ -598,4 +606,43 @@ class AppState: ObservableObject {
}
}
}

/// Revalidates the current network adapter selection and falls back to auto if no longer valid
func revalidateNetworkAdapter() {
let currentSelection = selectedNetworkAdapterName
let validated = validateAndGetNetworkAdapter(savedName: currentSelection)

if currentSelection != validated {
print("[state] Network adapter changed from '\(currentSelection ?? "auto")' to '\(validated ?? "auto")'")
selectedNetworkAdapterName = validated
shouldRefreshQR = true
}
}

/// Validates a saved network adapter name and returns it if available with valid IP, otherwise returns nil (auto)
private func validateAndGetNetworkAdapter(savedName: String?) -> String? {
guard let savedName = savedName else {
print("[state] No saved network adapter, using auto selection")
return nil // Auto mode
}

// Get available adapters from WebSocketServer
let availableAdapters = WebSocketServer.shared.getAvailableNetworkAdapters()

// Check if the saved adapter is still available
guard let matchingAdapter = availableAdapters.first(where: { $0.name == savedName }) else {
print("[state] Saved network adapter '\(savedName)' not found, falling back to auto")
return nil // Fall back to auto
}

// Verify the adapter has a valid IP address
let ipAddress = WebSocketServer.shared.getLocalIPAddress(adapterName: savedName)
guard let validIP = ipAddress, !validIP.isEmpty, validIP != "127.0.0.1" else {
print("[state] Saved network adapter '\(savedName)' has no valid IP (\(ipAddress ?? "nil")), falling back to auto")
return nil // Fall back to auto
}

print("[state] Using saved network adapter: \(savedName) -> \(validIP)")
return savedName
}
}
3 changes: 3 additions & 0 deletions airsync-mac/Core/WebSocket/WebSocketServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,9 @@ class WebSocketServer: ObservableObject {
if adapterAddresses != lastAddresses {
lastKnownAdapters = adapters

// Revalidate the current network adapter selection
AppState.shared.revalidateNetworkAdapter()

for adapter in adapters {
let activeMark = (adapter.address == chosenIP) ? " [ACTIVE]" : ""
print("[websocket] (network) \(adapter.name) -> \(adapter.address)\(activeMark)")
Expand Down
2 changes: 1 addition & 1 deletion airsync-mac/Localization/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"apps.tab": "Applicazioni",
"transfers.tab": "Trasferimenti",
"settings.tab": "Impostazioni",
"qr.tab": "Scan QR",
"qr.tab": "Scansiona QR",
"transfers.empty": "Non è ancora stato trasferito nulla",
"plus.title": "AirSync+",
"plus.why": "Perché Plus?",
Expand Down
44 changes: 22 additions & 22 deletions airsync-mac/Localization/zh-Hant.json
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
{
"app.name": "AirSync",
"menu.mirror": "Mirror",
"menu.about": "About AirSync",
"menu.checkUpdates": "Check for Updates…",
"menu.quit": "Quit",
"menu.mirror": "鏡像",
"menu.about": "關於 AirSync",
"menu.checkUpdates": "檢查更新…",
"menu.quit": "結束",
"home.title": "AirSync",
"home.sidebar.ready": "Ready",
"onboarding.title": "Welcome to AirSync",
"notifications.empty.title": "You're all caught up!",
"home.sidebar.ready": "準備就緒",
"onboarding.title": "歡迎使用 AirSync",
"notifications.empty.title": "沒有未讀通知!",
"notifications.empty.emoji": "└(=^‥^=)┐",
"notifications.actions.hide": "Hide",
"notifications.actions.dismiss": "Dismiss",
"notifications.tab": "Notifications",
"apps.tab": "Apps",
"transfers.tab": "Transfers",
"settings.tab": "Settings",
"qr.tab": "Scan QR",
"transfers.empty": "Nothing transferred yet",
"notifications.actions.hide": "隱藏",
"notifications.actions.dismiss": "關閉",
"notifications.tab": "通知",
"apps.tab": "應用程式",
"transfers.tab": "傳輸",
"settings.tab": "設定",
"qr.tab": "掃描二維碼",
"transfers.empty": "未有傳輸記錄",
"plus.title": "AirSync+",
"plus.why": "Why Plus?",
"plus.why.body": "Keeps me inspired to continue and maybe even to publish to the Apple app store and Google Play Store. Think of it as a little donation to keep this project alive and evolving.\nThat said, I know not everyone who wants the full experience can afford it. If that’s you, please don’t hesitate to reach out. 😊\n\nThe source code is available on GitHub, and you're more than welcome to build with all Plus features free — for personal use which also opens for contributions which is a win-win!.\nAs a thank-you for supporting the app, AirSync+ unlocks some nice extras: media controls, synced widgets, low battery alerts, wireless ADB, and more to come as I keep adding new features.\n\nEnjoy the app!\n(っ◕‿◕)っ",
"license.enterKey": "Enter license key",
"status.battery": "Battery",
"status.volume": "Volume",
"media.play": "Play",
"media.pause": "Pause"
"plus.why": "為什麼 Plus",
"plus.why.body": "這份支持讓我保持創作動力,甚至考慮將應用程式上架至 Apple App Store 與 Google Play 商店。不妨將此視為一筆小額捐款,讓這個專案得以延續並不斷進化。\n當然,我明白並非所有渴望完整體驗的使用者都能負擔這筆費用。若您正處於這種情況,歡迎隨時與我聯繫。😊\n\n原始碼已開放於 GitHub,歡迎免費自行建構並使用所有 Plus 功能作個人用途,更歡迎您的貢獻,實現雙贏局面!\n\n為感謝您對本應用程式的支持,AirSync+ 特別解鎖多項實用功能:媒體控制、同步小工具、低電量提醒、無線 ADB 連線,未來我將陸續開發新功能。\n\n祝您使用愉快!\n(っ◕‿◕)っ",
"license.enterKey": "輸入授權碼",
"status.battery": "電池",
"status.volume": "音量",
"media.play": "播放",
"media.pause": "暫停"
}
5 changes: 4 additions & 1 deletion airsync-mac/Screens/HomeScreen/AppContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ struct AppContentView: View {
Button("Refresh", systemImage: "repeat"){
WebSocketServer.shared.stop()
WebSocketServer.shared.start()
appState.shouldRefreshQR = true
// Delay QR refresh to ensure server has started
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
appState.shouldRefreshQR = true
}
}
.help("Refresh server")
}
Expand Down
60 changes: 48 additions & 12 deletions airsync-mac/Screens/HomeScreen/PhoneView/DeviceStatusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,20 @@ struct DeviceStatusView: View {

if let title = appState.status?.music.title.trimmingCharacters(in: .whitespacesAndNewlines),
!title.isEmpty && showMediaToggle {
GlassButtonView(
label: "Music Player",
systemImage: appState.status?.music.isPlaying == true ? "play.rectangle" : "music.note",
iconOnly: true,
primary: false,
action: {
withAnimation(.easeInOut(duration: 0.28)) {
appState.isMusicCardHidden.toggle()
}

GlassButtonView(
label: "Music Player",
systemImage: appState.status?.music.isPlaying == true ? "play.rectangle" : "music.note",
iconOnly: true,
primary: false,
action: {
withAnimation(.easeInOut(duration: 0.28)) {
appState.isMusicCardHidden.toggle()
}
)
.help("Show player")
.transition(.opacity.combined(with: .scale))
}
)
.help(appState.isMusicCardHidden ? "Show player" : "Hide player")
.transition(.opacity.combined(with: .scale))
}
}
.padding(.bottom, appState.isMusicCardHidden ? 0 : 8)
Expand All @@ -115,6 +116,41 @@ struct DeviceStatusView: View {
.easeInOut(duration: 0.25),
value: "\(appState.status?.battery.level ?? 0)-\(appState.status?.music.volume ?? 0)"
)
.onAppear {
// Ensure media card is collapsed at startup if no media is present
checkAndCollapseIfNoMedia()
}
.onChange(of: appState.status?.music.title) { _, newTitle in
// Auto-collapse music card when there's no media playing
checkAndCollapseIfNoMedia()
}
.onChange(of: appState.status?.music.artist) { _, newArtist in
// Also check when artist changes (could become empty)
checkAndCollapseIfNoMedia()
}
}

// Helper function to check if there's valid media info and collapse if not
private func checkAndCollapseIfNoMedia() {
let hasValidMedia = {
// Check if we have status and music info
guard let music = appState.status?.music else {
return false
}

// Check if title and artist are non-empty after trimming
let title = music.title.trimmingCharacters(in: .whitespacesAndNewlines)
let artist = music.artist.trimmingCharacters(in: .whitespacesAndNewlines)

return !title.isEmpty && !artist.isEmpty
}()

// If no valid media info and card is currently expanded, collapse it
if !hasValidMedia && !appState.isMusicCardHidden {
withAnimation(.easeInOut(duration: 0.28)) {
appState.isMusicCardHidden = true
}
}
}

// Battery icon helper
Expand Down
2 changes: 1 addition & 1 deletion airsync-mac/Screens/HomeScreen/PhoneView/TimeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ struct TimeView: View {
let minute = String(format: "%02d", components.minute ?? 0)

// Desired size and weight
let fontSize: CGFloat = 90
let fontSize: CGFloat = 85
// Use a rounded NSFont for the liquid glass path
let roundedNSFont = roundedFont(ofSize: fontSize, weight: .black)

Expand Down
12 changes: 12 additions & 0 deletions airsync-mac/Screens/ScannerView/ScannerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@ struct ScannerView: View {
appState.shouldRefreshQR = false
}
}
.onChange(of: appState.selectedNetworkAdapterName) { _, _ in
// Network adapter changed, regenerate QR with new IP
generateQRAsync()
}
.onChange(of: appState.myDevice?.port) { _, _ in
// Port changed, regenerate QR
generateQRAsync()
}
.onChange(of: appState.myDevice?.name) { _, _ in
// Device name changed, regenerate QR
generateQRAsync()
}

}

Expand Down
9 changes: 8 additions & 1 deletion airsync-mac/Screens/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct SettingsView: View {
@State private var deviceName: String = ""
@State private var port: String = "6996"
@State private var availableAdapters: [(name: String, address: String)] = []
@State private var currentIPAddress: String = "N/A"


var body: some View {
Expand Down Expand Up @@ -36,20 +37,26 @@ struct SettingsView: View {
}
.onAppear {
availableAdapters = WebSocketServer.shared.getAvailableNetworkAdapters()
currentIPAddress = WebSocketServer.shared.getLocalIPAddress(adapterName: appState.selectedNetworkAdapterName) ?? "N/A"
}
.onChange(of: appState.selectedNetworkAdapterName) { _, _ in
// Update IP address immediately
currentIPAddress = WebSocketServer.shared.getLocalIPAddress(adapterName: appState.selectedNetworkAdapterName) ?? "N/A"

WebSocketServer.shared.stop()
if let port = UInt16(port) {
WebSocketServer.shared.start(port: port)
} else {
WebSocketServer.shared.start()
}
// Refresh QR code since IP address may have changed
appState.shouldRefreshQR = true
}

ConnectionInfoText(
label: "IP Address",
icon: "wifi",
text: WebSocketServer.shared.getLocalIPAddress(adapterName: appState.selectedNetworkAdapterName) ?? "N/A"
text: currentIPAddress
)

HStack {
Expand Down