Skip to content
Open
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
12 changes: 12 additions & 0 deletions boringNotch.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@
5955950D2E900ED800C66711 /* ApplicationRelauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5955950C2E900ED800C66711 /* ApplicationRelauncher.swift */; };
59D8C23C2E589FAA00147B33 /* VolumeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59D8C23B2E589FAA00147B33 /* VolumeManager.swift */; };
64FA50FB2F4D6F9E00008A28 /* WebcamSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA50FA2F4D6F9E00008A28 /* WebcamSettingsView.swift */; };
6A1000012F5B3B5100A1B2C3 /* NetworkActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A1000022F5B3B5100A1B2C3 /* NetworkActivityManager.swift */; };
6A1000032F5B3B5100A1B2C3 /* NetworkStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A1000042F5B3B5100A1B2C3 /* NetworkStatusViewModel.swift */; };
6A1000052F5B3B5100A1B2C3 /* BoringNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A1000062F5B3B5100A1B2C3 /* BoringNetwork.swift */; };
9A0887322C7A693000C160EA /* TabButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A0887312C7A693000C160EA /* TabButton.swift */; };
9A0887352C7AFF8E00C160EA /* TabSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A0887342C7AFF8E00C160EA /* TabSelectionView.swift */; };
9A987A0D2C73CA66005CA465 /* ShelfView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A987A032C73CA66005CA465 /* ShelfView.swift */; };
Expand Down Expand Up @@ -312,6 +315,9 @@
5955950C2E900ED800C66711 /* ApplicationRelauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationRelauncher.swift; sourceTree = "<group>"; };
59D8C23B2E589FAA00147B33 /* VolumeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeManager.swift; sourceTree = "<group>"; };
64FA50FA2F4D6F9E00008A28 /* WebcamSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebcamSettingsView.swift; sourceTree = "<group>"; };
6A1000022F5B3B5100A1B2C3 /* NetworkActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkActivityManager.swift; sourceTree = "<group>"; };
6A1000042F5B3B5100A1B2C3 /* NetworkStatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStatusViewModel.swift; sourceTree = "<group>"; };
6A1000062F5B3B5100A1B2C3 /* BoringNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringNetwork.swift; sourceTree = "<group>"; };
9A0887312C7A693000C160EA /* TabButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabButton.swift; sourceTree = "<group>"; };
9A0887342C7AFF8E00C160EA /* TabSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSelectionView.swift; sourceTree = "<group>"; };
9A987A032C73CA66005CA465 /* ShelfView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShelfView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -609,6 +615,7 @@
11DB26652EDD0BE1001EA0CF /* LyricsService.swift */,
11D58EA12E760AE100FA8377 /* ImageService.swift */,
F38DE6472D8243E2008B5C6D /* BatteryActivityManager.swift */,
6A1000022F5B3B5100A1B2C3 /* NetworkActivityManager.swift */,
112FB7342CCF16F70015238C /* NotchSpaceManager.swift */,
147163992C5D35FF0068B555 /* MusicManager.swift */,
F1F2A0A200000000000000F2 /* AudioCaptureManager.swift */,
Expand Down Expand Up @@ -729,6 +736,7 @@
B19016232CC15B4D00E3F12E /* Constants.swift */,
14D570C82C5F38890011E668 /* BoringViewModel.swift */,
14D570CA2C5F4B2C0011E668 /* BatteryStatusViewModel.swift */,
6A1000042F5B3B5100A1B2C3 /* NetworkStatusViewModel.swift */,
1153BD902D986DB300979FB0 /* PlaybackState.swift */,
);
path = models;
Expand Down Expand Up @@ -827,6 +835,7 @@
isa = PBXGroup;
children = (
14D570CC2C5F4BB70011E668 /* BoringBattery.swift */,
6A1000062F5B3B5100A1B2C3 /* BoringNetwork.swift */,
B1C974332C642B6D0000E707 /* MarqueeTextView.swift */,
B1D365CD2C6A979C0047BDBC /* LiveActivityModifier.swift */,
14E9FEA92C70BF610062E83F /* DownloadView.swift */,
Expand Down Expand Up @@ -1008,6 +1017,7 @@
1194E87C2EA19E09009C82D6 /* ImageProcessingService.swift in Sources */,
1194E8872EA6DDA7009C82D6 /* BoringNotchSkyLightWindow.swift in Sources */,
14D570CB2C5F4B2C0011E668 /* BatteryStatusViewModel.swift in Sources */,
6A1000032F5B3B5100A1B2C3 /* NetworkStatusViewModel.swift in Sources */,
9A0887322C7A693000C160EA /* TabButton.swift in Sources */,
1153BD9C2D98853B00979FB0 /* NowPlayingController.swift in Sources */,
11985BEF2F37E48900F81585 /* OSDIconView.swift in Sources */,
Expand Down Expand Up @@ -1072,6 +1082,7 @@
14D570C62C5F38210011E668 /* BoringHeader.swift in Sources */,
14C08BB62C8DE42D000F8AA0 /* CalendarManager.swift in Sources */,
14D570CD2C5F4BB70011E668 /* BoringBattery.swift in Sources */,
6A1000052F5B3B5100A1B2C3 /* BoringNetwork.swift in Sources */,
B10F84A32C6C9596009F3026 /* TestView.swift in Sources */,
1443E7F32C609DCE0027C1FC /* matters.swift in Sources */,
11C5E3162DFE88510065821E /* SettingsView.swift in Sources */,
Expand All @@ -1092,6 +1103,7 @@
118EBE3B2E9720C500D54B5A /* ShelfStateViewModel.swift in Sources */,
11F747CE2EC75CEA00F841DB /* DragPreviewView.swift in Sources */,
F38DE6482D8243E7008B5C6D /* BatteryActivityManager.swift in Sources */,
6A1000012F5B3B5100A1B2C3 /* NetworkActivityManager.swift in Sources */,
B10348D92C74E56000475897 /* ConditionalModifier.swift in Sources */,
118D1FD12E98FF5F00A2FF63 /* SharingStateManager.swift in Sources */,
11985BE62F37A3FC00F81585 /* BetterDisplayNotificationModels.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions boringNotch/BoringViewCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ enum SneakContentType {
case music
case mic
case battery
case network
case download
}

Expand Down
16 changes: 16 additions & 0 deletions boringNotch/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct ContentView: View {
@ObservedObject var coordinator = BoringViewCoordinator.shared
@ObservedObject var musicManager = MusicManager.shared
@ObservedObject var batteryModel = BatteryStatusViewModel.shared
@ObservedObject var networkModel = NetworkStatusViewModel.shared
@ObservedObject var brightnessManager = BrightnessManager.shared
@ObservedObject var volumeManager = VolumeManager.shared
@State private var hoverTask: Task<Void, Never>?
Expand Down Expand Up @@ -92,6 +93,10 @@ struct ContentView: View {
&& vm.notchState == .closed && Defaults[.showPowerStatusNotifications]
{
chinWidth = 640
} else if coordinator.expandingView.type == .network && coordinator.expandingView.show
&& vm.notchState == .closed
{
chinWidth = networkModel.preferredNotificationWidth
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .music)
&& vm.notchState == .closed && (musicManager.isPlaying || !musicManager.isPlayerIdle)
&& coordinator.musicLiveActivityEnabled && !vm.hideOnClosed
Expand Down Expand Up @@ -321,6 +326,17 @@ struct ContentView: View {
.frame(width: 76, alignment: .trailing)
}
.frame(height: displayClosedNotchHeight, alignment: .center)
} else if coordinator.expandingView.type == .network && coordinator.expandingView.show
&& vm.notchState == .closed
{
BoringNetworkActivityView(
statusText: networkModel.statusText,
symbolName: networkModel.symbolName,
isConnected: networkModel.isConnected,
textWidth: networkModel.textWidth,
centerWidth: vm.closedNotchSize.width + 10
)
.frame(height: displayClosedNotchHeight, alignment: .center)
} else if coordinator.shouldShowSneakPeek(on: vm.screenUUID) && Defaults[.inlineOSD] && (coordinator.sneakPeekState(for: vm.screenUUID).type != .music) && (coordinator.sneakPeekState(for: vm.screenUUID).type != .battery) && vm.notchState == .closed {
InlineOSD(
type: coordinator.binding(for: vm.screenUUID).type,
Expand Down
34 changes: 34 additions & 0 deletions boringNotch/components/Live activities/BoringNetwork.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import SwiftUI

struct BoringNetworkActivityView: View {
var statusText: String
var symbolName: String
var isConnected: Bool
var textWidth: CGFloat
var centerWidth: CGFloat

var body: some View {
HStack(spacing: 0) {
HStack {
Text(statusText)
.font(.subheadline)
.foregroundStyle(.white)
.lineLimit(1)
.minimumScaleFactor(0.9)
}
.frame(width: textWidth, alignment: .leading)

Rectangle()
.fill(.black)
.frame(width: centerWidth)

HStack {
Image(systemName: symbolName)
.foregroundStyle(isConnected ? .white : .gray)
.frame(width: 18, height: 18)
.contentTransition(.interpolate)
}
.frame(width: 76, alignment: .trailing)
}
}
}
96 changes: 96 additions & 0 deletions boringNotch/managers/NetworkActivityManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Foundation
import Network

@MainActor
final class NetworkActivityManager {

struct NetworkState: Equatable {
enum Status: Equatable {
case connected
case disconnected
case requiresConnection
}

let status: Status
let isConstrained: Bool
let isExpensive: Bool
}

enum NetworkEvent {
case stateChanged(NetworkState, isInitial: Bool)
}

static let shared = NetworkActivityManager()

private let monitor = NWPathMonitor(requiredInterfaceType: .wifi)
private let monitorQueue = DispatchQueue(label: "com.boringnotch.network-activity")
private var observers: [Int: (NetworkEvent) -> Void] = [:]
private var nextObserverId = 0
private var latestState: NetworkState?

private init() {
startMonitoring()
}

func addObserver(_ observer: @escaping (NetworkEvent) -> Void) -> Int {
let id = nextObserverId
nextObserverId += 1
observers[id] = observer

if let latestState {
observer(.stateChanged(latestState, isInitial: true))
}

return id
}

func removeObserver(byId id: Int) {
observers.removeValue(forKey: id)
}

private func startMonitoring() {
monitor.pathUpdateHandler = { [weak self] path in
Task { @MainActor in
self?.handlePathUpdate(path)
}
}
monitor.start(queue: monitorQueue)
}

private func handlePathUpdate(_ path: NWPath) {
let newState = normalizedState(from: path)
let isInitial = latestState == nil

guard isInitial || latestState != newState else { return }

latestState = newState
notifyObservers(event: .stateChanged(newState, isInitial: isInitial))
}

private func normalizedState(from path: NWPath) -> NetworkState {
let status: NetworkState.Status

switch path.status {
case .satisfied:
status = path.usesInterfaceType(.wifi) ? .connected : .disconnected
case .requiresConnection:
status = .requiresConnection
case .unsatisfied:
status = .disconnected
@unknown default:
status = .disconnected
}

return NetworkState(
status: status,
isConstrained: path.isConstrained,
isExpensive: path.isExpensive
)
}

private func notifyObservers(event: NetworkEvent) {
observers.values.forEach { observer in
observer(event)
}
}
}
111 changes: 111 additions & 0 deletions boringNotch/models/NetworkStatusViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import Foundation
import SwiftUI

@MainActor
class NetworkStatusViewModel: ObservableObject {

private let coordinator = BoringViewCoordinator.shared

@Published private(set) var state = NetworkActivityManager.NetworkState(
status: .disconnected,
isConstrained: false,
isExpensive: false
)

private let networkManager = NetworkActivityManager.shared
private var networkManagerId: Int?

static let shared = NetworkStatusViewModel()

private init() {
setupMonitor()
}

var statusText: String {
switch state.status {
case .connected:
if state.isConstrained {
return "Limited Wi-Fi"
}
if state.isExpensive {
return "Hotspot Connected"
}
return "Wi-Fi Connected"
case .disconnected:
return "Wi-Fi Disconnected"
case .requiresConnection:
return "Wi-Fi Unavailable"
}
}

var symbolName: String {
switch state.status {
case .connected:
return "wifi"
case .disconnected, .requiresConnection:
return "wifi.slash"
}
}

var isConnected: Bool {
state.status == .connected
}

var preferredNotificationWidth: CGFloat {
textWidth + 76 + 16
}
Comment on lines +54 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the notch spacer in the network chin width

When the network activity is shown, ContentView uses preferredNotificationWidth as the full chin width, but BoringNetworkActivityView also renders a center spacer of vm.closedNotchSize.width + 10 between the text and icon. On the default non-notch width this makes the rendered activity roughly textWidth + 195 + 76, while the chin is only textWidth + 76 + 16, so with hideTitleBar enabled the lower chin area is substantially narrower than the visible activity and leaves the right side without the expected black fill/hit area. Include the center spacer in this width calculation or compute the width where closedNotchSize is available.

Useful? React with 👍 / 👎.


var textWidth: CGFloat {
switch state.status {
case .connected:
if state.isConstrained {
return 185
}
if state.isExpensive {
return 215
}
return 190
case .disconnected:
return 235
case .requiresConnection:
return 210
}
}

private func setupMonitor() {
networkManagerId = networkManager.addObserver { [weak self] event in
self?.handleNetworkEvent(event)
}
}

private func handleNetworkEvent(_ event: NetworkActivityManager.NetworkEvent) {
switch event {
case .stateChanged(let newState, let isInitial):
if isInitial {
state = newState
return
}

withAnimation {
state = newState
}

notifyImportantChangeStatus()
}
}

private func notifyImportantChangeStatus(delay: Double = 0.0) {
Task {
try? await Task.sleep(for: .seconds(delay))
coordinator.toggleExpandingView(status: true, type: .network)
}
}

deinit {
guard let networkManagerId else { return }
let manager = networkManager
Task { @MainActor in
manager.removeObserver(byId: networkManagerId)
}
}
}