aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-10-27 11:03:49 -0700
committerDustin Mierau <dustin@mierau.me>2025-10-27 11:03:49 -0700
commit5924204ab2956d3947ca19c07585b81173226a4f (patch)
tree68e2aa644f54bd5acfc02894e1f7f44f7da184e7
parent2b278f662326b44d86d84ae3e11ff0b770757f45 (diff)
Add spinning globe to TrackerView. Continue cleanup of NetSocketNew.
-rw-r--r--Hotline.xcodeproj/project.pbxproj4
-rw-r--r--Hotline/Library/NetSocketNew.swift4
-rw-r--r--Hotline/Library/SpinningGlobeView.swift90
-rw-r--r--Hotline/macOS/TrackerView.swift21
4 files changed, 106 insertions, 13 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj
index 2aafcfe..6368fe0 100644
--- a/Hotline.xcodeproj/project.pbxproj
+++ b/Hotline.xcodeproj/project.pbxproj
@@ -76,6 +76,7 @@
DAC3D9832BC33FD000A727C9 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC3D9822BC33FD000A727C9 /* AppState.swift */; };
DAC6B2E02EAC6236004E2CBA /* ChatStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC6B2DE2EAC6236004E2CBA /* ChatStore.swift */; };
DAC6B2E22EAEE9FE004E2CBA /* NetSocketNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC6B2E12EAEE9EF004E2CBA /* NetSocketNew.swift */; };
+ DAC6B2E42EAFE92F004E2CBA /* SpinningGlobeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC6B2E32EAFE92F004E2CBA /* SpinningGlobeView.swift */; };
DAC87F072C5010E80060FADF /* HotlineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC87F062C5010E80060FADF /* HotlineExtensions.swift */; };
DACCE5E12EABE4B4008CDD92 /* AppUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DACCE5E02EABE4B4008CDD92 /* AppUpdate.swift */; platformFilters = (macos, ); };
DACCE5E32EABE86A008CDD92 /* AppUpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DACCE5E22EABE86A008CDD92 /* AppUpdateView.swift */; platformFilters = (macos, ); };
@@ -165,6 +166,7 @@
DAC3D9822BC33FD000A727C9 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
DAC6B2DE2EAC6236004E2CBA /* ChatStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStore.swift; sourceTree = "<group>"; };
DAC6B2E12EAEE9EF004E2CBA /* NetSocketNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetSocketNew.swift; sourceTree = "<group>"; };
+ DAC6B2E32EAFE92F004E2CBA /* SpinningGlobeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinningGlobeView.swift; sourceTree = "<group>"; };
DAC87F062C5010E80060FADF /* HotlineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineExtensions.swift; sourceTree = "<group>"; };
DACCE5E02EABE4B4008CDD92 /* AppUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdate.swift; sourceTree = "<group>"; };
DACCE5E22EABE86A008CDD92 /* AppUpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateView.swift; sourceTree = "<group>"; };
@@ -278,6 +280,7 @@
isa = PBXGroup;
children = (
DAC6B2E12EAEE9EF004E2CBA /* NetSocketNew.swift */,
+ DAC6B2E32EAFE92F004E2CBA /* SpinningGlobeView.swift */,
DAB4D87A2B4B78310048A05C /* FileImageView.swift */,
DAB4D8812B4C8FED0048A05C /* FileIconView.swift */,
DAB4D8832B4CABEF0048A05C /* DataAdditions.swift */,
@@ -491,6 +494,7 @@
DA7725412B21435B006C5ABB /* ObservableScrollView.swift in Sources */,
DAE734FF2B2E6750000C56F6 /* ChatView.swift in Sources */,
11F8288B2BF9428100216BA0 /* AccountManagerView.swift in Sources */,
+ DAC6B2E42EAFE92F004E2CBA /* SpinningGlobeView.swift in Sources */,
DA4930BD2B4F8DD700822D0B /* NetSocket.swift in Sources */,
DAC002192B21630900A6C290 /* SwiftUIExtensions.swift in Sources */,
DADDB28F2B238D850024040D /* Hotline.swift in Sources */,
diff --git a/Hotline/Library/NetSocketNew.swift b/Hotline/Library/NetSocketNew.swift
index d4507b8..8873ee6 100644
--- a/Hotline/Library/NetSocketNew.swift
+++ b/Hotline/Library/NetSocketNew.swift
@@ -1108,7 +1108,7 @@ fileprivate extension NetSocketNew {
let values = try url.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey])
guard values.isRegularFile == true else {
throw NetSocketError.failed(underlying: NSError(
- domain: "CleanSockets", code: 1001,
+ domain: "NetSocket", code: 1001,
userInfo: [NSLocalizedDescriptionKey: "Not a regular file: \(url.path)"]
))
}
@@ -1116,7 +1116,7 @@ fileprivate extension NetSocketNew {
let attrs = try FileManager.default.attributesOfItem(atPath: url.path)
if let n = attrs[.size] as? NSNumber { return n.int64Value }
throw NetSocketError.failed(underlying: NSError(
- domain: "CleanSockets", code: 1002,
+ domain: "NetSocket", code: 1002,
userInfo: [NSLocalizedDescriptionKey: "Unable to determine file size for \(url.lastPathComponent)"]
))
}
diff --git a/Hotline/Library/SpinningGlobeView.swift b/Hotline/Library/SpinningGlobeView.swift
new file mode 100644
index 0000000..bccb969
--- /dev/null
+++ b/Hotline/Library/SpinningGlobeView.swift
@@ -0,0 +1,90 @@
+import SwiftUI
+
+/// An animated globe icon that cycles through different world regions
+///
+/// Displays a spinning globe effect by cycling through SF Symbol images showing
+/// different parts of the world. Useful for indicating network activity or global content.
+///
+/// Example:
+/// ```swift
+/// SpinningGlobeView()
+/// .frame(width: 16, height: 16)
+///
+/// SpinningGlobeView(frameDelay: 0.5)
+/// .frame(width: 24, height: 24)
+/// ```
+struct SpinningGlobeView: View {
+ /// Delay between frames in seconds (default: 0.3)
+ let frameDelay: TimeInterval
+
+ /// SF Symbol names for each frame of the globe animation
+ private let globeFrames = [
+ "globe.americas.fill",
+ "globe.europe.africa.fill",
+ "globe.central.south.asia.fill",
+ "globe.asia.australia.fill"
+ ]
+
+ @State private var currentFrameIndex = 0
+ @State private var animationTask: Task<Void, Never>?
+
+ init(frameDelay: TimeInterval = 0.25) {
+ self.frameDelay = frameDelay
+ }
+
+ var body: some View {
+ Image(systemName: globeFrames[currentFrameIndex])
+ .onAppear {
+ startAnimation()
+ }
+ .onDisappear {
+ stopAnimation()
+ }
+ }
+
+ private func startAnimation() {
+ animationTask = Task {
+ while !Task.isCancelled {
+ try? await Task.sleep(nanoseconds: UInt64(frameDelay * 1_000_000_000))
+
+ guard !Task.isCancelled else { break }
+
+ currentFrameIndex = (currentFrameIndex + 1) % globeFrames.count
+ }
+ }
+ }
+
+ private func stopAnimation() {
+ animationTask?.cancel()
+ animationTask = nil
+ }
+}
+
+#Preview {
+ VStack(spacing: 20) {
+ HStack(spacing: 20) {
+ SpinningGlobeView()
+ .frame(width: 12, height: 12)
+
+ SpinningGlobeView()
+ .frame(width: 16, height: 16)
+
+ SpinningGlobeView()
+ .frame(width: 24, height: 24)
+ }
+
+ Text("Different frame delays:")
+
+ HStack(spacing: 20) {
+ SpinningGlobeView(frameDelay: 0.1)
+ .frame(width: 16, height: 16)
+
+ SpinningGlobeView(frameDelay: 0.3)
+ .frame(width: 16, height: 16)
+
+ SpinningGlobeView(frameDelay: 0.6)
+ .frame(width: 16, height: 16)
+ }
+ }
+ .padding()
+}
diff --git a/Hotline/macOS/TrackerView.swift b/Hotline/macOS/TrackerView.swift
index ae653b8..251248f 100644
--- a/Hotline/macOS/TrackerView.swift
+++ b/Hotline/macOS/TrackerView.swift
@@ -755,19 +755,18 @@ struct TrackerItemView: View {
if isExpanded && count > 0 {
HStack(spacing: 4) {
Text(String(count))
-
- Image(systemName: "globe.americas.fill")
- .resizable()
- .scaledToFit()
+
+ SpinningGlobeView()
+ .fontWeight(.semibold)
.frame(width: 12, height: 12)
- .opacity(0.5)
+// .opacity(0.5)
}
-
- .padding(.horizontal, 8)
- .padding(.vertical, 2)
- .foregroundStyle(.secondary)
- .background(.quinary)
- .clipShape(.capsule)
+
+ .padding(.horizontal, 8)
+ .padding(.vertical, 2)
+ .foregroundStyle(.secondary)
+ .background(.quinary)
+ .clipShape(.capsule)
}
case .server:
Image(systemName: "bookmark.fill")