diff --git a/PcVolumeControl/viewmodels/MainViewModel.swift b/PcVolumeControl/viewmodels/MainViewModel.swift
index c435401..9d0c192 100644
--- a/PcVolumeControl/viewmodels/MainViewModel.swift
+++ b/PcVolumeControl/viewmodels/MainViewModel.swift
@@ -577,20 +577,30 @@ final class MainViewModel: ObservableObject {
}
func deleteServerFromList(_ item: ServerListItem) {
- switch item {
- case .discovered(let server):
- // Remove discovered server from the list (it may reappear if still broadcasting)
- if let index = discoveredServers.firstIndex(where: { $0.id == server.id }) {
- discoveredServers.remove(at: index)
- }
- case .recent(let server):
- if let index = recentServers.firstIndex(where: { $0.id == server.id }) {
- recentServers.remove(at: index)
- serversRepo.saveRecent(recentServers)
- }
- case .manual:
- // The manual-entry row is not deletable.
- break
+ // The manual-entry row has no backing server and is not deletable.
+ guard let server = item.server else { return }
+
+ // A server can be both live-discovered (mDNS) and a saved recent
+ // connection. unifiedServerList dedupes those into a single row, so a
+ // single swipe-to-delete must clear BOTH representations -- otherwise the
+ // hidden recent duplicate resurfaces as a separate "recent connection" row.
+ // (A still-broadcasting server may reappear on the next discovery cycle.)
+ let isSameServer: (DiscoveredServer) -> Bool = {
+ $0.address == server.address && $0.port == server.port
+ }
+
+ discoveredServers.removeAll(where: isSameServer)
+
+ let recentCountBefore = recentServers.count
+ recentServers.removeAll(where: isSameServer)
+ if recentServers.count != recentCountBefore {
+ serversRepo.saveRecent(recentServers)
+ }
+
+ // Clear any auto-discovery bookkeeping tied to the deleted server.
+ if let current = currentDiscoveredServer, isSameServer(current) {
+ currentDiscoveredServer = nil
+ isAutoDiscovered = false
}
}
}
diff --git a/PcVolumeControlTests/MainViewModelSimpleTests.swift b/PcVolumeControlTests/MainViewModelSimpleTests.swift
index 7a1ed72..27a5d48 100644
--- a/PcVolumeControlTests/MainViewModelSimpleTests.swift
+++ b/PcVolumeControlTests/MainViewModelSimpleTests.swift
@@ -212,6 +212,45 @@ final class MainViewModelSimpleTests: XCTestCase {
XCTAssertEqual(sut.connectionStatus, .lost)
}
+ // MARK: - Server List Deletion Tests
+
+ func testDeletingServerThatIsBothDiscoveredAndRecentClearsItInOneSwipe() {
+ // Given a server that is both live-discovered over mDNS and a saved
+ // recent connection (same address:port, different DiscoveredServer
+ // instances). The unified list dedupes these into a single row.
+ let discovered = DiscoveredServer(address: "192.168.0.42", port: 3000, computerName: "CHOSS")
+ let recent = DiscoveredServer(address: "192.168.0.42", port: 3000, computerName: nil)
+ sut.discoveredServers = [discovered]
+ sut.recentServers = [recent]
+
+ let rowsBefore = sut.unifiedServerList.filter { !$0.isManual }
+ XCTAssertEqual(rowsBefore.count, 1, "Discovered and recent duplicates should collapse to one row")
+ XCTAssertTrue(rowsBefore.first?.isDiscovered ?? false)
+
+ // When the user swipes to delete that single row once
+ sut.deleteServerFromList(rowsBefore[0])
+
+ // Then the row must not resurface as a "recent connection" duplicate
+ let rowsAfter = sut.unifiedServerList.filter { !$0.isManual }
+ XCTAssertEqual(rowsAfter.count, 0, "One delete should remove both the discovered and recent representations")
+ XCTAssertTrue(sut.discoveredServers.isEmpty)
+ XCTAssertTrue(sut.recentServers.isEmpty)
+ }
+
+ func testDeletingPlainRecentServerStillWorks() {
+ // Regression: a recent-only server (no live discovery) still deletes.
+ let recent = DiscoveredServer(address: "10.0.0.5", port: 8080, computerName: nil)
+ sut.recentServers = [recent]
+
+ let rows = sut.unifiedServerList.filter { !$0.isManual }
+ XCTAssertEqual(rows.count, 1)
+
+ sut.deleteServerFromList(rows[0])
+
+ XCTAssertTrue(sut.recentServers.isEmpty)
+ XCTAssertEqual(sut.unifiedServerList.filter { !$0.isManual }.count, 0)
+ }
+
// MARK: - Integration Tests
func testConnectDisconnectCycle() async {
diff --git a/README.md b/README.md
index 6f1eea5..f0e0428 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,68 @@
-# [Demo Video](https://youtu.be/OgueVzguP3w)
+# PC Volume Control — iOS
-# Get Started!
+Control your Windows PC's application audio volumes remotely from your iPhone or iPad.
-[Download it here](https://itunes.apple.com/us/app/pcvolumecontrol/id1336171942?ls=1&mt=8) from the App Store.
+
-Head over to [our wiki](https://github.com/PcVolumeControl/PcVolumeControliOS/wiki) for more detailed instructions on installation, contributing, and using this application.
\ No newline at end of file
+## What it does
+
+PC Volume Control lets you adjust the volume of individual applications running on your Windows PC from your iOS device. This was designed primarily for remote volume control while gaming or streaming.
+
+The app connects to a free companion Windows server over your local network, displaying all running applications and their volume levels in a clean, responsive interface. Adjust volumes, mute/unmute, and switch between output devices (speakers, headphones, etc.) all from your phone or tablet.
+
+**Features:**
+- Remote application volume control
+- Per-app mute/unmute
+- Master volume and mute for the default output device
+- Auto-discovery of the server on your local network (or manual IP/port entry)
+
+## Demo
+
+[Watch a quick demo](https://www.youtube.com/shorts/yMf1cxd65t4)
+
+## Screenshots
+
+**Connection Screen** — Discover your PC server automatically over mDNS, or enter its IP and port manually.
+
+
+
+**Auto-Discovery** — PC servers broadcasting on your network appear automatically with the Wi-Fi badge.
+
+
+
+**Device Selector** — Switch between multiple audio output devices (speakers, headphones, etc.).
+
+
+
+## Getting Started
+
+### 1. Download the App
+
+[Get it on the App Store](https://itunes.apple.com/us/app/pcvolumecontrol/id1336171942?ls=1&mt=8)
+
+### 2. Install the Windows Server
+
+PC Volume Control requires the free companion server on your Windows PC.
+
+1. **Download** the latest [PC Volume Control Server release](https://github.com/PcVolumeControl/PcVolumeControlWindows/releases/latest) for Windows.
+2. **Install and run** the server on your PC. Allow it through Windows Firewall when prompted (private networks).
+3. **Note the server address** — the server window displays your PC's IP address and port (default 3000).
+
+### 3. Connect Your Device
+
+1. **Open PC Volume Control** on your iOS device.
+2. **Same Wi-Fi?** If your iPhone/iPad and PC are on the same Wi-Fi network, the server will appear automatically with a green Wi-Fi badge — just tap to connect.
+3. **Manual IP entry?** If auto-discovery doesn't find it, enter your PC's IP address and port (e.g., `192.168.1.100`) in the "Server IP or Hostname" field and tap the arrow to connect.
+
+### Troubleshooting
+
+- **Can't connect?** Check Windows Firewall isn't blocking the server.
+- **No audio output?** Use the master device selector (top of the volume screen) to switch between multiple audio outputs.
+
+## Contributing
+
+Head over to [our wiki](https://github.com/PcVolumeControl/PcVolumeControliOS/wiki) for detailed instructions on installing from source, contributing, and more.
+
+## License
+
+See LICENSE file for details.
diff --git a/meta/01_ConnectionScreen.png b/meta/01_ConnectionScreen.png
new file mode 100644
index 0000000..25846c2
Binary files /dev/null and b/meta/01_ConnectionScreen.png differ
diff --git a/meta/02_AutoDiscovery.png b/meta/02_AutoDiscovery.png
new file mode 100644
index 0000000..ce76c1a
Binary files /dev/null and b/meta/02_AutoDiscovery.png differ
diff --git a/meta/03_SliderScreen.png b/meta/03_SliderScreen.png
new file mode 100644
index 0000000..47a39dc
Binary files /dev/null and b/meta/03_SliderScreen.png differ
diff --git a/meta/04_DeviceSelector.png b/meta/04_DeviceSelector.png
new file mode 100644
index 0000000..c0f35b8
Binary files /dev/null and b/meta/04_DeviceSelector.png differ
diff --git a/meta/screenshot-connect-1242x2208.png b/meta/screenshot-connect-1242x2208.png
deleted file mode 100644
index a98ee8d..0000000
Binary files a/meta/screenshot-connect-1242x2208.png and /dev/null differ
diff --git a/meta/screenshot-full-1242x2208.png b/meta/screenshot-full-1242x2208.png
deleted file mode 100644
index 58ab128..0000000
Binary files a/meta/screenshot-full-1242x2208.png and /dev/null differ
diff --git a/meta/screenshot-select-1242x2208.png b/meta/screenshot-select-1242x2208.png
deleted file mode 100644
index 73b18fe..0000000
Binary files a/meta/screenshot-select-1242x2208.png and /dev/null differ