aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hotline.xcodeproj/project.pbxproj50
-rw-r--r--Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved17
-rw-r--r--Hotline/Assets/Sounds/chat-message.aiff (renamed from Hotline/Sounds/chat-message.aiff)bin4554 -> 4554 bytes
-rw-r--r--Hotline/Assets/Sounds/error.aiff (renamed from Hotline/Sounds/error.aiff)bin4610 -> 4610 bytes
-rw-r--r--Hotline/Assets/Sounds/logged-in.aiff (renamed from Hotline/Sounds/logged-in.aiff)bin4166 -> 4166 bytes
-rw-r--r--Hotline/Assets/Sounds/new-news.aiff (renamed from Hotline/Sounds/new-news.aiff)bin17514 -> 17514 bytes
-rw-r--r--Hotline/Assets/Sounds/server-message.aiff (renamed from Hotline/Sounds/server-message.aiff)bin7914 -> 7914 bytes
-rw-r--r--Hotline/Assets/Sounds/transfer-complete.aiff (renamed from Hotline/Sounds/transfer-complete.aiff)bin21378 -> 21378 bytes
-rw-r--r--Hotline/Assets/Sounds/user-login.aiff (renamed from Hotline/Sounds/user-login.aiff)bin6186 -> 6186 bytes
-rw-r--r--Hotline/Assets/Sounds/user-logout.aiff (renamed from Hotline/Sounds/user-logout.aiff)bin7080 -> 7080 bytes
-rw-r--r--Hotline/Library/AsyncLinkPreview.swift (renamed from Hotline/Shared/AsyncLinkPreview.swift)0
-rw-r--r--Hotline/Library/DataAdditions.swift (renamed from Hotline/Shared/DataAdditions.swift)0
-rw-r--r--Hotline/Library/FileIconView.swift (renamed from Hotline/Shared/FileIconView.swift)0
-rw-r--r--Hotline/Library/FileImageView.swift (renamed from Hotline/Shared/FileImageView.swift)0
-rw-r--r--Hotline/Library/HotlinePanel.swift (renamed from Hotline/Shared/HotlinePanel.swift)0
-rw-r--r--Hotline/Library/NetSocket.swift (renamed from Hotline/Shared/NetSocket.swift)0
-rw-r--r--Hotline/Library/TextView.swift (renamed from Hotline/Shared/TextView.swift)0
-rw-r--r--Hotline/Library/URLAdditions.swift (renamed from Hotline/Shared/URLAdditions.swift)0
-rw-r--r--Hotline/Library/VisualEffectView.swift (renamed from Hotline/Shared/VisualEffectView.swift)0
-rw-r--r--Hotline/MacApp.swift (renamed from Hotline/Application-macOS.swift)26
-rw-r--r--Hotline/Models/Bookmark.swift100
-rw-r--r--Hotline/State/AppState.swift (renamed from Hotline/Models/ApplicationState.swift)12
-rw-r--r--Hotline/State/Preferences.swift (renamed from Hotline/Models/Preferences.swift)10
-rw-r--r--Hotline/iOSApp.swift (renamed from Hotline/Sounds/Application-iOS.swift)0
-rw-r--r--Hotline/macOS/HotlinePanelView.swift24
-rw-r--r--Hotline/macOS/TrackerView.swift503
26 files changed, 475 insertions, 267 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj
index cf71fdd..a188bfd 100644
--- a/Hotline.xcodeproj/project.pbxproj
+++ b/Hotline.xcodeproj/project.pbxproj
@@ -24,6 +24,7 @@
DA32CD4F2B2931CC0053B98B /* NewsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32CD4E2B2931CC0053B98B /* NewsInfo.swift */; };
DA43205E2B1D615600FC8843 /* ServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA43205D2B1D615600FC8843 /* ServerView.swift */; platformFilter = ios; };
DA4930BD2B4F8DD700822D0B /* NetSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4930BC2B4F8DD700822D0B /* NetSocket.swift */; };
+ DA4B8F3A2EA6FB3C00CBFD53 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4B8F392EA6FB3C00CBFD53 /* iOSApp.swift */; platformFilter = ios; };
DA4F2BF82B16A17200D8ADDC /* HotlineProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4F2BF72B16A17200D8ADDC /* HotlineProtocol.swift */; };
DA4F2C012B1A558E00D8ADDC /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4F2C002B1A558E00D8ADDC /* ChatView.swift */; platformFilter = ios; };
DA55AC732BE42AF000034857 /* AsyncLinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA55AC722BE42AF000034857 /* AsyncLinkPreview.swift */; platformFilters = (macos, ); };
@@ -47,7 +48,7 @@
DA872B132BDDBF78008B1012 /* HotlinePanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA872B122BDDBF78008B1012 /* HotlinePanel.swift */; platformFilters = (macos, ); };
DA872B152BDDEE1A008B1012 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA872B142BDDEE1A008B1012 /* VisualEffectView.swift */; platformFilters = (macos, ); };
DA99218E2C51AA5D0058FA6C /* HotlineDataBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA99218D2C51AA5D0058FA6C /* HotlineDataBuilder.swift */; };
- DA9CAFBB2B126D5700CDA197 /* Application-macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9CAFBA2B126D5700CDA197 /* Application-macOS.swift */; platformFilters = (macos, ); };
+ DA9CAFBB2B126D5700CDA197 /* MacApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9CAFBA2B126D5700CDA197 /* MacApp.swift */; platformFilters = (macos, ); };
DA9CAFBD2B126D5700CDA197 /* TrackerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9CAFBC2B126D5700CDA197 /* TrackerView.swift */; platformFilter = ios; };
DA9CAFC12B126D5800CDA197 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DA9CAFC02B126D5800CDA197 /* Assets.xcassets */; };
DA9CAFC42B126D5800CDA197 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DA9CAFC32B126D5800CDA197 /* Preview Assets.xcassets */; };
@@ -72,8 +73,7 @@
DABE8C042B57940B00884D28 /* DAKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABE8C032B57940A00884D28 /* DAKeychain.swift */; };
DABFCC292B1530DC009F40D2 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABFCC282B1530DC009F40D2 /* FoundationExtensions.swift */; };
DAC002192B21630900A6C290 /* SwiftUIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC002182B21630900A6C290 /* SwiftUIExtensions.swift */; };
- DAC3D97E2BC0F1ED00A727C9 /* Application-iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC3D97D2BC0F1ED00A727C9 /* Application-iOS.swift */; platformFilter = ios; };
- DAC3D9832BC33FD000A727C9 /* ApplicationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC3D9822BC33FD000A727C9 /* ApplicationState.swift */; };
+ DAC3D9832BC33FD000A727C9 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC3D9822BC33FD000A727C9 /* AppState.swift */; };
DAC87F072C5010E80060FADF /* HotlineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC87F062C5010E80060FADF /* HotlineExtensions.swift */; };
DADDB28B2B22B31F0024040D /* Tracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADDB28A2B22B31F0024040D /* Tracker.swift */; };
DADDB28D2B22B5920024040D /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADDB28C2B22B5920024040D /* Server.swift */; };
@@ -109,6 +109,7 @@
DA32CD4E2B2931CC0053B98B /* NewsInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsInfo.swift; sourceTree = "<group>"; };
DA43205D2B1D615600FC8843 /* ServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerView.swift; sourceTree = "<group>"; };
DA4930BC2B4F8DD700822D0B /* NetSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetSocket.swift; sourceTree = "<group>"; };
+ DA4B8F392EA6FB3C00CBFD53 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
DA4F2BF72B16A17200D8ADDC /* HotlineProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineProtocol.swift; sourceTree = "<group>"; };
DA4F2C002B1A558E00D8ADDC /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
DA55AC722BE42AF000034857 /* AsyncLinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncLinkPreview.swift; sourceTree = "<group>"; };
@@ -132,7 +133,7 @@
DA872B142BDDEE1A008B1012 /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = "<group>"; };
DA99218D2C51AA5D0058FA6C /* HotlineDataBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineDataBuilder.swift; sourceTree = "<group>"; };
DA9CAFB72B126D5700CDA197 /* Hotline.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Hotline.app; sourceTree = BUILT_PRODUCTS_DIR; };
- DA9CAFBA2B126D5700CDA197 /* Application-macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Application-macOS.swift"; sourceTree = "<group>"; };
+ DA9CAFBA2B126D5700CDA197 /* MacApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacApp.swift; sourceTree = "<group>"; };
DA9CAFBC2B126D5700CDA197 /* TrackerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackerView.swift; sourceTree = "<group>"; };
DA9CAFC02B126D5800CDA197 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
DA9CAFC32B126D5800CDA197 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
@@ -156,8 +157,7 @@
DABE8C032B57940A00884D28 /* DAKeychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DAKeychain.swift; sourceTree = "<group>"; };
DABFCC282B1530DC009F40D2 /* FoundationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoundationExtensions.swift; sourceTree = "<group>"; };
DAC002182B21630900A6C290 /* SwiftUIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExtensions.swift; sourceTree = "<group>"; };
- DAC3D97D2BC0F1ED00A727C9 /* Application-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Application-iOS.swift"; path = "Sounds/Application-iOS.swift"; sourceTree = "<group>"; };
- DAC3D9822BC33FD000A727C9 /* ApplicationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationState.swift; sourceTree = "<group>"; };
+ DAC3D9822BC33FD000A727C9 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
DAC87F062C5010E80060FADF /* HotlineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineExtensions.swift; sourceTree = "<group>"; };
DADDB28A2B22B31F0024040D /* Tracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tracker.swift; sourceTree = "<group>"; };
DADDB28C2B22B5920024040D /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
@@ -186,6 +186,23 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ DA4B8F3B2EA6FB5F00CBFD53 /* State */ = {
+ isa = PBXGroup;
+ children = (
+ DAC3D9822BC33FD000A727C9 /* AppState.swift */,
+ DA2863D92B37BF6E00A7D050 /* Preferences.swift */,
+ );
+ path = State;
+ sourceTree = "<group>";
+ };
+ DA4B8F3C2EA6FDCE00CBFD53 /* Assets */ = {
+ isa = PBXGroup;
+ children = (
+ DABE8BF22B55DBFC00884D28 /* Sounds */,
+ );
+ path = Assets;
+ sourceTree = "<group>";
+ };
DA4F2BF92B1A516F00D8ADDC /* iOS */ = {
isa = PBXGroup;
children = (
@@ -219,15 +236,16 @@
DA9CAFB92B126D5700CDA197 /* Hotline */ = {
isa = PBXGroup;
children = (
- DA9CAFBA2B126D5700CDA197 /* Application-macOS.swift */,
- DAC3D97D2BC0F1ED00A727C9 /* Application-iOS.swift */,
- DABE8BF22B55DBFC00884D28 /* Sounds */,
+ DA9CAFBA2B126D5700CDA197 /* MacApp.swift */,
+ DA4B8F392EA6FB3C00CBFD53 /* iOSApp.swift */,
DA4F2BF92B1A516F00D8ADDC /* iOS */,
DAE734F72B2E4126000C56F6 /* macOS */,
+ DA4B8F3B2EA6FB5F00CBFD53 /* State */,
DADDB2892B22B2C60024040D /* Models */,
DABFCC262B1530AE009F40D2 /* Hotline */,
- DAB4D87C2B4B783A0048A05C /* Shared */,
+ DAB4D87C2B4B783A0048A05C /* Library */,
DABFCC272B1530BE009F40D2 /* Utility */,
+ DA4B8F3C2EA6FDCE00CBFD53 /* Assets */,
DA9CAFC02B126D5800CDA197 /* Assets.xcassets */,
DA32CD472B28EE640053B98B /* Info.plist */,
DA2863DB2B37DBE000A7D050 /* Hotline.entitlements */,
@@ -244,7 +262,7 @@
path = "Preview Content";
sourceTree = "<group>";
};
- DAB4D87C2B4B783A0048A05C /* Shared */ = {
+ DAB4D87C2B4B783A0048A05C /* Library */ = {
isa = PBXGroup;
children = (
DAB4D87A2B4B78310048A05C /* FileImageView.swift */,
@@ -257,7 +275,7 @@
DA55AC722BE42AF000034857 /* AsyncLinkPreview.swift */,
DA6980782BF80525003E434B /* TextView.swift */,
);
- path = Shared;
+ path = Library;
sourceTree = "<group>";
};
DABE8BF22B55DBFC00884D28 /* Sounds */ = {
@@ -309,8 +327,6 @@
children = (
DADDB28E2B238D850024040D /* Hotline.swift */,
DADDB28A2B22B31F0024040D /* Tracker.swift */,
- DAC3D9822BC33FD000A727C9 /* ApplicationState.swift */,
- DA2863D92B37BF6E00A7D050 /* Preferences.swift */,
DADDB28C2B22B5920024040D /* Server.swift */,
DA32CD482B2931640053B98B /* User.swift */,
11A726072BE0672A000C1DA7 /* FileDetails.swift */,
@@ -443,12 +459,11 @@
DA6300972B24036B0034CBFD /* HotlineClient.swift in Sources */,
DA43205E2B1D615600FC8843 /* ServerView.swift in Sources */,
DA0D69912B1E894800C71DF5 /* FilesView.swift in Sources */,
- DAC3D9832BC33FD000A727C9 /* ApplicationState.swift in Sources */,
+ DAC3D9832BC33FD000A727C9 /* AppState.swift in Sources */,
DA65499C2BEC3FBD00EDB697 /* ServerAgreementView.swift in Sources */,
DAB4D8802B4C8E9A0048A05C /* URLAdditions.swift in Sources */,
DA2863DD2B3E8B7000A7D050 /* FilePreview.swift in Sources */,
DA2863D82B37AD1C00A7D050 /* SettingsView.swift in Sources */,
- DAC3D97E2BC0F1ED00A727C9 /* Application-iOS.swift in Sources */,
DA7725412B21435B006C5ABB /* ObservableScrollView.swift in Sources */,
DAE734FF2B2E6750000C56F6 /* ChatView.swift in Sources */,
11F8288B2BF9428100216BA0 /* AccountManagerView.swift in Sources */,
@@ -473,7 +488,7 @@
DA9CAFCB2B126E3300CDA197 /* HotlineTrackerClient.swift in Sources */,
DA2863DA2B37BF6E00A7D050 /* Preferences.swift in Sources */,
DA32CD4F2B2931CC0053B98B /* NewsInfo.swift in Sources */,
- DA9CAFBB2B126D5700CDA197 /* Application-macOS.swift in Sources */,
+ DA9CAFBB2B126D5700CDA197 /* MacApp.swift in Sources */,
DAAEE66B2B3FBC2100A5BA07 /* TransferInfo.swift in Sources */,
DA0D698F2B1E841600C71DF5 /* MessageBoardView.swift in Sources */,
DAE735072B3251B3000C56F6 /* SoundEffects.swift in Sources */,
@@ -488,6 +503,7 @@
DA77253F2B21176D006C5ABB /* NewsView.swift in Sources */,
DA99218E2C51AA5D0058FA6C /* HotlineDataBuilder.swift in Sources */,
DA872B132BDDBF78008B1012 /* HotlinePanel.swift in Sources */,
+ DA4B8F3A2EA6FB3C00CBFD53 /* iOSApp.swift in Sources */,
DA4F2BF82B16A17200D8ADDC /* HotlineProtocol.swift in Sources */,
DA5753682B33E88A00FAC277 /* HotlineTransferClient.swift in Sources */,
DAB4D8822B4C8FED0048A05C /* FileIconView.swift in Sources */,
diff --git a/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 13c2046..28a7003 100644
--- a/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/NetworkImage",
"state" : {
- "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1",
- "version" : "6.0.0"
+ "revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
+ "version" : "6.0.1"
}
},
{
@@ -20,12 +20,21 @@
}
},
{
+ "identity" : "swift-cmark",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swiftlang/swift-cmark",
+ "state" : {
+ "revision" : "b97d09472e847a416629f026eceae0e2afcfad65",
+ "version" : "0.7.0"
+ }
+ },
+ {
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
"state" : {
- "revision" : "9a8119b37e09a770367eeb26e05267c75d854053",
- "version" : "2.3.1"
+ "revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
+ "version" : "2.4.1"
}
}
],
diff --git a/Hotline/Sounds/chat-message.aiff b/Hotline/Assets/Sounds/chat-message.aiff
index 73ea391..73ea391 100644
--- a/Hotline/Sounds/chat-message.aiff
+++ b/Hotline/Assets/Sounds/chat-message.aiff
Binary files differ
diff --git a/Hotline/Sounds/error.aiff b/Hotline/Assets/Sounds/error.aiff
index 8a9f7f6..8a9f7f6 100644
--- a/Hotline/Sounds/error.aiff
+++ b/Hotline/Assets/Sounds/error.aiff
Binary files differ
diff --git a/Hotline/Sounds/logged-in.aiff b/Hotline/Assets/Sounds/logged-in.aiff
index 5f59de0..5f59de0 100644
--- a/Hotline/Sounds/logged-in.aiff
+++ b/Hotline/Assets/Sounds/logged-in.aiff
Binary files differ
diff --git a/Hotline/Sounds/new-news.aiff b/Hotline/Assets/Sounds/new-news.aiff
index 5c56582..5c56582 100644
--- a/Hotline/Sounds/new-news.aiff
+++ b/Hotline/Assets/Sounds/new-news.aiff
Binary files differ
diff --git a/Hotline/Sounds/server-message.aiff b/Hotline/Assets/Sounds/server-message.aiff
index 152bebe..152bebe 100644
--- a/Hotline/Sounds/server-message.aiff
+++ b/Hotline/Assets/Sounds/server-message.aiff
Binary files differ
diff --git a/Hotline/Sounds/transfer-complete.aiff b/Hotline/Assets/Sounds/transfer-complete.aiff
index b22e0ae..b22e0ae 100644
--- a/Hotline/Sounds/transfer-complete.aiff
+++ b/Hotline/Assets/Sounds/transfer-complete.aiff
Binary files differ
diff --git a/Hotline/Sounds/user-login.aiff b/Hotline/Assets/Sounds/user-login.aiff
index 0dcfb6f..0dcfb6f 100644
--- a/Hotline/Sounds/user-login.aiff
+++ b/Hotline/Assets/Sounds/user-login.aiff
Binary files differ
diff --git a/Hotline/Sounds/user-logout.aiff b/Hotline/Assets/Sounds/user-logout.aiff
index 8999edc..8999edc 100644
--- a/Hotline/Sounds/user-logout.aiff
+++ b/Hotline/Assets/Sounds/user-logout.aiff
Binary files differ
diff --git a/Hotline/Shared/AsyncLinkPreview.swift b/Hotline/Library/AsyncLinkPreview.swift
index 89b186f..89b186f 100644
--- a/Hotline/Shared/AsyncLinkPreview.swift
+++ b/Hotline/Library/AsyncLinkPreview.swift
diff --git a/Hotline/Shared/DataAdditions.swift b/Hotline/Library/DataAdditions.swift
index 0e9dd96..0e9dd96 100644
--- a/Hotline/Shared/DataAdditions.swift
+++ b/Hotline/Library/DataAdditions.swift
diff --git a/Hotline/Shared/FileIconView.swift b/Hotline/Library/FileIconView.swift
index d0949fa..d0949fa 100644
--- a/Hotline/Shared/FileIconView.swift
+++ b/Hotline/Library/FileIconView.swift
diff --git a/Hotline/Shared/FileImageView.swift b/Hotline/Library/FileImageView.swift
index 0cc50f1..0cc50f1 100644
--- a/Hotline/Shared/FileImageView.swift
+++ b/Hotline/Library/FileImageView.swift
diff --git a/Hotline/Shared/HotlinePanel.swift b/Hotline/Library/HotlinePanel.swift
index 191e89a..191e89a 100644
--- a/Hotline/Shared/HotlinePanel.swift
+++ b/Hotline/Library/HotlinePanel.swift
diff --git a/Hotline/Shared/NetSocket.swift b/Hotline/Library/NetSocket.swift
index a7d44ce..a7d44ce 100644
--- a/Hotline/Shared/NetSocket.swift
+++ b/Hotline/Library/NetSocket.swift
diff --git a/Hotline/Shared/TextView.swift b/Hotline/Library/TextView.swift
index 47746e2..47746e2 100644
--- a/Hotline/Shared/TextView.swift
+++ b/Hotline/Library/TextView.swift
diff --git a/Hotline/Shared/URLAdditions.swift b/Hotline/Library/URLAdditions.swift
index 1f25541..1f25541 100644
--- a/Hotline/Shared/URLAdditions.swift
+++ b/Hotline/Library/URLAdditions.swift
diff --git a/Hotline/Shared/VisualEffectView.swift b/Hotline/Library/VisualEffectView.swift
index e595c55..e595c55 100644
--- a/Hotline/Shared/VisualEffectView.swift
+++ b/Hotline/Library/VisualEffectView.swift
diff --git a/Hotline/Application-macOS.swift b/Hotline/MacApp.swift
index fe3a828..a4ff3cb 100644
--- a/Hotline/Application-macOS.swift
+++ b/Hotline/MacApp.swift
@@ -29,13 +29,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// We mark CloudKit has available now since we're not waiting on
// a server sync or anything.
- ApplicationState.shared.cloudKitReady = true
+ AppState.shared.cloudKitReady = true
default:
print("iCloud Available")
self.cloudKitObserverToken = NotificationCenter.default.addObserver(forName: NSPersistentCloudKitContainer.eventChangedNotification, object: nil, queue: OperationQueue.main) { [weak self] note in
print("iCloud Changed!")
- ApplicationState.shared.cloudKitReady = true
+ AppState.shared.cloudKitReady = true
guard let token = self?.cloudKitObserverToken else { return }
NotificationCenter.default.removeObserver(token)
@@ -48,15 +48,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
//
// // We mark CloudKit has available now since we're not waiting on
// // a server sync or anything.
-// ApplicationState.shared.cloudKitReady = true
+// AppState.shared.cloudKitReady = true
// }
// else {
// print("iCloud Available")
//
// self.cloudKitObserverToken = NotificationCenter.default.addObserver(forName: NSPersistentCloudKitContainer.eventChangedNotification, object: nil, queue: OperationQueue.main) { [weak self] note in
// print("iCloud Changed!")
-// ApplicationState.shared.cloudKitReady = true
-//
+// AppState.shared.cloudKitReady = true
+//
// guard let token = self?.cloudKitObserverToken else { return }
// NotificationCenter.default.removeObserver(token)
// }
@@ -77,7 +77,7 @@ struct Application: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@State private var hotlinePanel: HotlinePanel? = nil
- @State private var selection: Bookmark? = nil
+ @State private var selection: TrackerSelection? = nil
@FocusedValue(\.activeHotlineModel) private var activeHotline: Hotline?
@FocusedValue(\.activeServerState) private var activeServerState: ServerState?
@@ -142,25 +142,25 @@ struct Application: App {
.defaultSize(width: 750, height: 700)
.defaultPosition(.center)
.onChange(of: activeServerState) {
- ApplicationState.shared.activeServerState = activeServerState
+ AppState.shared.activeServerState = activeServerState
}
.onChange(of: activeHotline) {
- ApplicationState.shared.activeHotline = activeHotline
+ AppState.shared.activeHotline = activeHotline
}
.onChange(of: activeHotline?.serverTitle) {
if let hotline = activeHotline {
- ApplicationState.shared.activeServerName = hotline.serverTitle
+ AppState.shared.activeServerName = hotline.serverTitle
}
}
.onChange(of: activeHotline?.bannerImage) {
withAnimation {
- ApplicationState.shared.activeServerBanner = activeHotline?.bannerImage
+ AppState.shared.activeServerBanner = activeHotline?.bannerImage
}
}
.onChange(of: activeHotline) {
- ApplicationState.shared.activeHotline = activeHotline
+ AppState.shared.activeHotline = activeHotline
if let hotline = activeHotline {
- ApplicationState.shared.activeServerName = hotline.serverTitle
+ AppState.shared.activeServerName = hotline.serverTitle
}
}
.commands {
@@ -269,7 +269,7 @@ struct Application: App {
.defaultPosition(.center)
}
- func connect(to item: Bookmark) {
+ func connect(to item: TrackerSelection) {
if let server = item.server {
openWindow(id: "server", value: server)
}
diff --git a/Hotline/Models/Bookmark.swift b/Hotline/Models/Bookmark.swift
index 733a6eb..394f9f6 100644
--- a/Hotline/Models/Bookmark.swift
+++ b/Hotline/Models/Bookmark.swift
@@ -4,7 +4,27 @@ import SwiftData
enum BookmarkType: String, Codable {
case tracker = "tracker"
case server = "server"
- case temporary = "temporary"
+}
+
+struct BookmarkServer: Hashable, Identifiable {
+ let id = UUID()
+ let name: String?
+ let address: String
+ let port: Int
+ let description: String?
+ let users: Int
+
+ var server: Server {
+ Server(name: self.name, description: self.description, address: self.address, port: self.port, users: self.users)
+ }
+
+ init(server: Server) {
+ self.name = server.name
+ self.address = server.address
+ self.port = server.port
+ self.description = server.description
+ self.users = server.users
+ }
}
@Model
@@ -18,29 +38,10 @@ final class Bookmark {
@Attribute(.allowsCloudEncryption)
var login: String?
-
+
@Attribute(.allowsCloudEncryption)
var password: String?
-
- @Attribute(.ephemeral)
- var expanded: Bool = false
-
- @Attribute(.ephemeral)
- var loading: Bool = false
-
- @Attribute(.ephemeral)
- var serverDescription: String? = nil
-
- @Attribute(.ephemeral)
- var serverUserCount: Int? = nil
-
- @Transient
- var servers: [Bookmark] = []
-
- func hash(into hasher: inout Hasher) {
-
- }
-
+
@Transient
var displayAddress: String {
switch self.type {
@@ -51,8 +52,8 @@ final class Bookmark {
else {
return "\(self.address):\(String(self.port))"
}
-
- case .server, .temporary:
+
+ case .server:
if self.port == HotlinePorts.DefaultServerPort {
return self.address
}
@@ -67,9 +68,9 @@ final class Bookmark {
switch self.type {
case .tracker:
return nil
-
- case .server, .temporary:
- return Server(name: self.name, description: self.serverDescription, address: self.address, port: self.port, login: self.login, password: self.password)
+
+ case .server:
+ return Server(name: self.name, description: nil, address: self.address, port: self.port, login: self.login, password: self.password)
}
}
@@ -84,22 +85,11 @@ final class Bookmark {
self.name = name
self.address = address
self.port = port
-
+
self.login = login
self.password = password
}
-
- init(temporaryServer server: Server) {
- self.type = .temporary
-
- self.name = server.name ?? server.address
- self.address = server.address
- self.port = server.port
-
- self.serverDescription = server.description
- self.serverUserCount = server.users
- }
-
+
init?(fileData: Data, name: String? = nil) {
guard fileData.count <= 2000 else {
return nil
@@ -248,11 +238,6 @@ final class Bookmark {
}
static func add(_ bookmark: Bookmark, context: ModelContext) {
- guard bookmark.type != .temporary else {
- print("Bookmark: Attempting to add temporary bookmark to store. Aborting.")
- return
- }
-
let existingBookmarks = Bookmark.fetchAll(context: context)
// Reindex bookmarks before insert.
@@ -348,32 +333,23 @@ final class Bookmark {
}
}
- func fetchServers() async {
+ func fetchServers() async -> [BookmarkServer] {
guard self.type == .tracker else {
- // self.loading = false
- return
- }
-
- DispatchQueue.main.sync {
- self.loading = true
+ return []
}
-
- var fetchedBookmarks: [Bookmark] = []
-
+
+ var fetchedBookmarks: [BookmarkServer] = []
+
let client = HotlineTrackerClient()
if let fetchedServers: [HotlineServer] = try? await client.fetchServers(address: self.address, port: self.port) {
for fetchedServer in fetchedServers {
if let serverName = fetchedServer.name {
let server = Server(name: serverName, description: fetchedServer.description, address: fetchedServer.address, port: Int(fetchedServer.port), users: Int(fetchedServer.users))
- fetchedBookmarks.append(Bookmark(temporaryServer: server))
+ fetchedBookmarks.append(BookmarkServer(server: server))
}
}
}
-
- let newServers = fetchedBookmarks
- DispatchQueue.main.sync {
- self.servers = newServers
- self.loading = false
- }
+
+ return fetchedBookmarks
}
}
diff --git a/Hotline/Models/ApplicationState.swift b/Hotline/State/AppState.swift
index 6e077c5..11bae74 100644
--- a/Hotline/Models/ApplicationState.swift
+++ b/Hotline/State/AppState.swift
@@ -1,8 +1,16 @@
import SwiftUI
+extension EnvironmentValues {
+ @Entry var appState: AppState = AppState.shared
+}
+
@Observable
-final class ApplicationState {
- static let shared = ApplicationState()
+final class AppState {
+ static let shared = AppState()
+
+ private init() {
+
+ }
var activeHotline: Hotline? = nil
var activeServerState: ServerState? = nil
diff --git a/Hotline/Models/Preferences.swift b/Hotline/State/Preferences.swift
index 207ce91..fef2b9a 100644
--- a/Hotline/Models/Preferences.swift
+++ b/Hotline/State/Preferences.swift
@@ -1,5 +1,9 @@
import SwiftUI
+extension EnvironmentValues {
+ @Entry var preferences: Prefs = Prefs.shared
+}
+
enum PrefsKeys: String {
case username = "username"
case userIconID = "user icon id"
@@ -22,7 +26,9 @@ enum PrefsKeys: String {
@Observable
class Prefs {
- init() {
+ static let shared = Prefs()
+
+ private init() {
UserDefaults.standard.register(defaults:[
PrefsKeys.username.rawValue: "guest",
PrefsKeys.userIconID.rawValue: 191,
@@ -61,8 +67,6 @@ class Prefs {
self.showBannerToolbar = UserDefaults.standard.bool(forKey: PrefsKeys.showBannerToolbar.rawValue)
self.showJoinLeaveMessages = UserDefaults.standard.bool(forKey: PrefsKeys.showJoinLeaveMessages.rawValue)
}
-
- public static let shared = Prefs()
var username: String {
didSet { UserDefaults.standard.set(self.username, forKey: PrefsKeys.username.rawValue) }
diff --git a/Hotline/Sounds/Application-iOS.swift b/Hotline/iOSApp.swift
index 2fb6ad2..2fb6ad2 100644
--- a/Hotline/Sounds/Application-iOS.swift
+++ b/Hotline/iOSApp.swift
diff --git a/Hotline/macOS/HotlinePanelView.swift b/Hotline/macOS/HotlinePanelView.swift
index dc58698..fd43c15 100644
--- a/Hotline/macOS/HotlinePanelView.swift
+++ b/Hotline/macOS/HotlinePanelView.swift
@@ -6,7 +6,7 @@ struct HotlinePanelView: View {
var body: some View {
VStack(spacing: 0) {
- Image(nsImage: ApplicationState.shared.activeServerBanner ?? NSImage(named: "Default Banner")!)
+ Image(nsImage: AppState.shared.activeServerBanner ?? NSImage(named: "Default Banner")!)
.interpolation(.high)
.resizable()
.scaledToFill()
@@ -36,7 +36,7 @@ struct HotlinePanelView: View {
.help("Hotline Servers")
Button {
- ApplicationState.shared.activeServerState?.selection = .chat
+ AppState.shared.activeServerState?.selection = .chat
}
label: {
Image("Section Chat")
@@ -45,11 +45,11 @@ struct HotlinePanelView: View {
}
.buttonStyle(.plain)
.frame(width: 20, height: 20)
- .disabled(ApplicationState.shared.activeServerState == nil)
+ .disabled(AppState.shared.activeServerState == nil)
.help("Public Chat")
Button {
- ApplicationState.shared.activeServerState?.selection = .board
+ AppState.shared.activeServerState?.selection = .board
}
label: {
Image("Section Board")
@@ -58,11 +58,11 @@ struct HotlinePanelView: View {
}
.buttonStyle(.plain)
.frame(width: 20, height: 20)
- .disabled(ApplicationState.shared.activeServerState == nil)
+ .disabled(AppState.shared.activeServerState == nil)
.help("Message Board")
Button {
- ApplicationState.shared.activeServerState?.selection = .news
+ AppState.shared.activeServerState?.selection = .news
}
label: {
Image("Section News")
@@ -71,11 +71,11 @@ struct HotlinePanelView: View {
}
.buttonStyle(.plain)
.frame(width: 20, height: 20)
- .disabled(ApplicationState.shared.activeServerState == nil || (ApplicationState.shared.activeHotline?.serverVersion ?? 0) < 151)
+ .disabled(AppState.shared.activeServerState == nil || (AppState.shared.activeHotline?.serverVersion ?? 0) < 151)
.help("News")
Button {
- ApplicationState.shared.activeServerState?.selection = .files
+ AppState.shared.activeServerState?.selection = .files
}
label: {
Image("Section Files")
@@ -84,14 +84,14 @@ struct HotlinePanelView: View {
}
.buttonStyle(.plain)
.frame(width: 20, height: 20)
- .disabled(ApplicationState.shared.activeServerState == nil)
+ .disabled(AppState.shared.activeServerState == nil)
.help("Files")
Spacer()
- if ApplicationState.shared.activeHotline?.access?.contains(.canOpenUsers) == true {
+ if AppState.shared.activeHotline?.access?.contains(.canOpenUsers) == true {
Button {
- ApplicationState.shared.activeServerState?.selection = .accounts
+ AppState.shared.activeServerState?.selection = .accounts
}
label: {
Image("Section Users")
@@ -100,7 +100,7 @@ struct HotlinePanelView: View {
}
.buttonStyle(.plain)
.frame(width: 20, height: 20)
- .disabled(ApplicationState.shared.activeServerState == nil)
+ .disabled(AppState.shared.activeServerState == nil)
.help("Accounts")
}
diff --git a/Hotline/macOS/TrackerView.swift b/Hotline/macOS/TrackerView.swift
index 0fac8b0..2fbce85 100644
--- a/Hotline/macOS/TrackerView.swift
+++ b/Hotline/macOS/TrackerView.swift
@@ -3,6 +3,18 @@ import SwiftData
import Foundation
import UniformTypeIdentifiers
+enum TrackerSelection: Hashable {
+ case bookmark(Bookmark)
+ case bookmarkServer(BookmarkServer)
+
+ var server: Server? {
+ switch self {
+ case .bookmark(let b): return b.server
+ case .bookmarkServer(let t): return t.server
+ }
+ }
+}
+
struct TrackerView: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.openWindow) private var openWindow
@@ -16,22 +28,78 @@ struct TrackerView: View {
@State private var fileDropActive = false
@State private var bookmarkExportActive = false
@State private var bookmarkExport: BookmarkDocument? = nil
-
+ @State private var expandedTrackers: Set<Bookmark> = []
+ @State private var trackerServers: [Bookmark: [BookmarkServer]] = [:]
+ @State private var loadingTrackers: Set<Bookmark> = []
+ @State private var searchText: String = ""
+
@Query(sort: \Bookmark.order) private var bookmarks: [Bookmark]
- @Binding var selection: Bookmark?
+ @Binding var selection: TrackerSelection?
+
+ private var filteredBookmarks: [Bookmark] {
+ guard !self.searchText.isEmpty else {
+ return self.bookmarks
+ }
+
+ let searchWords = self.searchText.lowercased().split(separator: " ").map(String.init)
+
+ return self.bookmarks.filter { bookmark in
+ // Always show tracker bookmarks (filter only their servers)
+ if bookmark.type == .tracker {
+ return true
+ }
+
+ // Filter server bookmarks by search text
+ return self.bookmarkMatchesSearch(bookmark, searchWords: searchWords)
+ }
+ }
+
+ private func bookmarkMatchesSearch(_ bookmark: Bookmark, searchWords: [String]) -> Bool {
+ let searchableText = "\(bookmark.name) \(bookmark.address)".lowercased()
+
+ // All search words must match
+ return searchWords.allSatisfy { word in
+ searchableText.contains(word)
+ }
+ }
+
+ private func filteredServers(for bookmark: Bookmark) -> [BookmarkServer] {
+ let servers = self.trackerServers[bookmark] ?? []
+
+ guard !self.searchText.isEmpty else {
+ return servers
+ }
+
+ let searchWords = self.searchText.lowercased().split(separator: " ").map(String.init)
+
+ return servers.filter { server in
+ let searchableText = "\(server.name ?? "") \(server.address) \(server.description ?? "")".lowercased()
+
+ // All search words must match
+ return searchWords.allSatisfy { word in
+ searchableText.contains(word)
+ }
+ }
+ }
var body: some View {
List(selection: $selection) {
- ForEach(bookmarks, id: \.self) { bookmark in
- TrackerItemView(bookmark: bookmark)
- .tag(bookmark)
-
- if bookmark.type == .tracker && bookmark.expanded {
- ForEach(bookmark.servers, id: \.self) { trackedServer in
- TrackerItemView(bookmark: trackedServer)
+ ForEach(filteredBookmarks, id: \.self) { bookmark in
+ TrackerItemView(
+ bookmark: bookmark,
+ isExpanded: self.expandedTrackers.contains(bookmark),
+ isLoading: self.loadingTrackers.contains(bookmark)
+ ) {
+ self.toggleExpanded(for: bookmark)
+ }
+ .tag(TrackerSelection.bookmark(bookmark))
+
+ if bookmark.type == .tracker && self.expandedTrackers.contains(bookmark) {
+ ForEach(self.filteredServers(for: bookmark), id: \.self) { trackedServer in
+ TrackerBookmarkServerView(server: trackedServer)
.moveDisabled(true)
.deleteDisabled(true)
- .tag(trackedServer)
+ .tag(TrackerSelection.bookmarkServer(trackedServer))
}
}
}
@@ -43,15 +111,22 @@ struct TrackerView: View {
}
}
.onDeleteCommand {
- if let bookmark = selection,
- bookmark.type != .temporary {
+ switch self.selection {
+ case .bookmark(let bookmark):
Bookmark.delete(bookmark, context: modelContext)
+ default:
+ break
}
+
+// if let bookmark = selection,
+// bookmark.type != .temporary {
+// Bookmark.delete(bookmark, context: modelContext)
+// }
}
.environment(\.defaultMinListRowHeight, 34)
.listStyle(.inset)
.alternatingRowBackgrounds(.enabled)
- .onChange(of: ApplicationState.shared.cloudKitReady) {
+ .onChange(of: AppState.shared.cloudKitReady) {
if attemptedPrepopulate {
print("Tracker: Already attempted to prepopulate bookmarks")
return
@@ -67,53 +142,13 @@ struct TrackerView: View {
.onAppear {
// Bookmark.deleteAll(context: modelContext)
}
- .contextMenu(forSelectionType: Bookmark.self) { items in
+ .contextMenu(forSelectionType: TrackerSelection.self) { items in
if let item = items.first {
- if item.type == .temporary {
- Button {
- let newBookmark = Bookmark(type: .server, name: item.name, address: item.address, port: item.port, login: item.login, password: item.password)
- Bookmark.add(newBookmark, context: modelContext)
- } label: {
- Label("Bookmark", systemImage: "bookmark")
- }
-
- Divider()
- }
-
- Button {
- NSPasteboard.general.clearContents()
- NSPasteboard.general.setString(item.displayAddress, forType: .string)
- } label: {
- Label("Copy Address", systemImage: "doc.on.doc")
- }
-
- if item.type == .tracker || item.type == .server {
- Divider()
-
- if item.type == .tracker {
- Button {
- trackerSheetBookmark = item
- } label: {
- Label("Edit Tracker...", systemImage: "pencil")
- }
- }
-
- if item.type == .server {
- Button {
- bookmarkExport = BookmarkDocument(bookmark: item)
- bookmarkExportActive = true
- } label: {
- Label("Export Bookmark...", systemImage: "bookmark.square")
- }
- }
-
- Divider()
-
- Button {
- Bookmark.delete(item, context: modelContext)
- } label: {
- Label(item.type == .tracker ? "Delete Tracker" : "Delete Bookmark", systemImage: "trash")
- }
+ switch item {
+ case .bookmark(let bookmark):
+ self.bookmarkContextMenu(bookmark)
+ case .bookmarkServer(let server):
+ self.bookmarkServerContextMenu(server)
}
}
} primaryAction: { items in
@@ -121,17 +156,37 @@ struct TrackerView: View {
return
}
- if clickedItem.type == .tracker {
- if NSEvent.modifierFlags.contains(.option) {
- trackerSheetBookmark = clickedItem
+ switch clickedItem {
+ case .bookmark(let bookmark):
+ if bookmark.type == .server {
+ if let s = bookmark.server {
+ openWindow(id: "server", value: s)
+ }
}
- else {
- clickedItem.expanded.toggle()
+ else if bookmark.type == .tracker {
+ if NSEvent.modifierFlags.contains(.option) {
+ trackerSheetBookmark = bookmark
+ }
+ else {
+ self.toggleExpanded(for: bookmark)
+ }
}
+
+ case .bookmarkServer(let bookmarkServer):
+ openWindow(id: "server", value: bookmarkServer.server)
}
- else if let server = clickedItem.server {
- openWindow(id: "server", value: server)
- }
+
+// if clickedItem.type == .tracker {
+// if NSEvent.modifierFlags.contains(.option) {
+// trackerSheetBookmark = clickedItem
+// }
+// else {
+// clickedItem.expanded.toggle()
+// }
+// }
+// else if let server = clickedItem.server {
+// openWindow(id: "server", value: server)
+// }
}
.fileExporter(isPresented: $bookmarkExportActive, document: bookmarkExport, contentTypes: [.data], defaultFilename: "\(bookmarkExport?.bookmark.name ?? "Hotline Bookmark").hlbm", onCompletion: { result in
switch result {
@@ -145,21 +200,41 @@ struct TrackerView: View {
bookmarkExportActive = false
}, onCancellation: {})
.onKeyPress(.rightArrow) {
- if
- let bookmark = selection,
- bookmark.type == .tracker {
- bookmark.expanded = true
- return .handled
+ switch self.selection {
+ case .bookmark(let bookmark):
+ if bookmark.type == .tracker {
+ self.expandedTrackers.insert(bookmark)
+ return .handled
+ }
+ default:
+ break
}
+
+// if
+// let bookmark = selection,
+// bookmark.type == .tracker {
+// bookmark.expanded = true
+// return .handled
+// }
return .ignored
}
.onKeyPress(.leftArrow) {
- if
- let bookmark = selection,
- bookmark.type == .tracker {
- bookmark.expanded = false
- return .handled
+ switch self.selection {
+ case .bookmark(let bookmark):
+ if bookmark.type == .tracker {
+ self.expandedTrackers.remove(bookmark)
+ return .handled
+ }
+ default:
+ break
}
+
+// if
+// let bookmark = selection,
+// bookmark.type == .tracker {
+// bookmark.expanded = false
+// return .handled
+// }
return .ignored
}
.onDrop(of: [UTType.fileURL], isTargeted: $fileDropActive) { providers, dropPoint in
@@ -198,20 +273,26 @@ struct TrackerView: View {
.navigationTitle("Servers")
.toolbar {
ToolbarItem(placement: .navigation) {
- Image("Hotline")
- .resizable()
- .renderingMode(.template)
- .scaledToFit()
- .foregroundColor(Color(hex: 0xE10000))
- .frame(width: 9)
- .opacity(controlActiveState == .inactive ? 0.5 : 1.0)
+ let image = Image("Hotline")
+ .resizable()
+ .renderingMode(.template)
+ .scaledToFit()
+ .foregroundColor(Color(hex: 0xE10000))
+ .frame(width: 9)
+ .opacity(controlActiveState == .inactive ? 0.5 : 1.0)
+
+// if #available(macOS 26, *) {
+// image.sharedBackgroundVisibility(.hidden)
+// } else {
+ image
+// }
}
ToolbarItem(placement: .primaryAction) {
Button {
- refreshing = true
- refresh()
- refreshing = false
+ self.refreshing = true
+ self.refresh()
+ self.refreshing = false
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
}
@@ -242,38 +323,142 @@ struct TrackerView: View {
openWindow(id: "server", value: s)
}
})
+ .searchable(text: $searchText, placement: .automatic, prompt: "Search")
+ }
+
+ @ViewBuilder
+ func bookmarkServerContextMenu(_ server: BookmarkServer) -> some View {
+ Button {
+ let newBookmark = Bookmark(type: .server, name: server.name ?? server.address, address: server.address, port: server.port, login: nil, password: nil)
+ Bookmark.add(newBookmark, context: modelContext)
+ } label: {
+ Label("Bookmark", systemImage: "bookmark")
+ }
+
+ Divider()
+
+ Button {
+ NSPasteboard.general.clearContents()
+ let displayAddress = server.port == HotlinePorts.DefaultServerPort ?
+ server.address : "\(server.address):\(server.port)"
+ NSPasteboard.general.setString(displayAddress, forType: .string)
+ } label: {
+ Label("Copy Address", systemImage: "doc.on.doc")
+ }
+ }
+
+ @ViewBuilder
+ func bookmarkContextMenu(_ bookmark: Bookmark) -> some View {
+ Button {
+ NSPasteboard.general.clearContents()
+ NSPasteboard.general.setString(bookmark.displayAddress, forType: .string)
+ } label: {
+ Label("Copy Address", systemImage: "doc.on.doc")
+ }
+
+ Divider()
+
+ if bookmark.type == .tracker {
+ Button {
+ trackerSheetBookmark = bookmark
+ } label: {
+ Label("Edit Tracker...", systemImage: "pencil")
+ }
+ }
+
+ if bookmark.type == .server {
+ Button {
+ bookmarkExport = BookmarkDocument(bookmark: bookmark)
+ bookmarkExportActive = true
+ } label: {
+ Label("Export Bookmark...", systemImage: "bookmark.square")
+ }
+ }
+
+ Divider()
+
+ Button {
+ Bookmark.delete(bookmark, context: modelContext)
+ } label: {
+ Label(bookmark.type == .tracker ? "Delete Tracker" : "Delete Bookmark", systemImage: "trash")
+ }
}
+
func refresh() {
// When a tracker is selected, refresh only that tracker.
- if
- let selectedBookmark = selection,
- selectedBookmark.type == .tracker {
- if !selectedBookmark.expanded {
- selectedBookmark.expanded = true
- }
- else {
- Task {
- await selectedBookmark.fetchServers()
+ if let trackerSelection = self.selection {
+ switch trackerSelection {
+ case .bookmark(let bookmark):
+ if bookmark.type == .tracker {
+ if self.expandedTrackers.contains(bookmark) {
+ // Already expanded, just refresh the servers
+ Task {
+ await self.fetchServers(for: bookmark)
+ }
+ } else {
+ // Not expanded, expand it (which also fetches)
+ self.setExpanded(true, for: bookmark)
+ }
}
+ return
+ default:
+ break
}
- return
}
-
+
// Otherwise refresh/expand all trackers.
for bookmark in self.bookmarks {
if bookmark.type == .tracker {
- if !bookmark.expanded {
- bookmark.expanded = true
- }
- else {
+ if self.expandedTrackers.contains(bookmark) {
+ // Already expanded, just refresh the servers
Task {
- await bookmark.fetchServers()
+ await self.fetchServers(for: bookmark)
}
+ } else {
+ // Not expanded, expand it (which also fetches)
+ self.setExpanded(true, for: bookmark)
}
}
}
}
+
+ func toggleExpanded(for bookmark: Bookmark) {
+ guard bookmark.type == .tracker else { return }
+
+ if self.expandedTrackers.contains(bookmark) {
+ self.expandedTrackers.remove(bookmark)
+ self.trackerServers[bookmark] = nil
+ } else {
+ self.expandedTrackers.insert(bookmark)
+ Task {
+ await self.fetchServers(for: bookmark)
+ }
+ }
+ }
+
+ func setExpanded(_ expanded: Bool, for bookmark: Bookmark) {
+ guard bookmark.type == .tracker else { return }
+
+ if expanded && !self.expandedTrackers.contains(bookmark) {
+ self.expandedTrackers.insert(bookmark)
+ Task {
+ await self.fetchServers(for: bookmark)
+ }
+ } else if !expanded && self.expandedTrackers.contains(bookmark) {
+ self.expandedTrackers.remove(bookmark)
+ self.trackerServers[bookmark] = nil
+ }
+ }
+
+ private func fetchServers(for bookmark: Bookmark) async {
+ self.loadingTrackers.insert(bookmark)
+ let servers = await bookmark.fetchServers()
+ await MainActor.run {
+ self.trackerServers[bookmark] = servers
+ self.loadingTrackers.remove(bookmark)
+ }
+ }
}
struct TrackerBookmarkSheet: View {
@@ -359,18 +544,60 @@ struct TrackerBookmarkSheet: View {
}
}
+struct TrackerBookmarkServerView: View {
+ let server: BookmarkServer
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 6) {
+ Spacer()
+ .frame(width: 14 + 8 + 16)
+ Image("Server")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 16, height: 16, alignment: .center)
+ Text(self.server.name ?? "Server").lineLimit(1).truncationMode(.tail)
+ if let serverDescription = self.server.description {
+ Text(serverDescription)
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ }
+ Spacer(minLength: 0)
+ if self.server.users > 0 {
+ Text(String(self.server.users))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+
+ Circle()
+ .fill(.fileComplete)
+ .frame(width: 7, height: 7)
+ .keyframeAnimator(initialValue: 1.0, repeating: true) { content, opacity in
+ content.opacity(opacity)
+ } keyframes: { _ in
+ CubicKeyframe(1.0, duration: 2.0) // Stay visible for 1 second
+ CubicKeyframe(0.75, duration: 0.5) // Fade out quickly
+ CubicKeyframe(1.0, duration: 0.5) // Fade in quickly
+ }
+ .padding(.trailing, 6)
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+}
+
struct TrackerItemView: View {
let bookmark: Bookmark
-
- @State private var onlineAnimationMaxState: Bool = true
-
+ let isExpanded: Bool
+ let isLoading: Bool
+ let onToggleExpanded: () -> Void
+
var body: some View {
HStack(alignment: .center, spacing: 6) {
if bookmark.type == .tracker {
Button {
- bookmark.expanded.toggle()
+ self.onToggleExpanded()
} label: {
- Text(Image(systemName: bookmark.expanded ? "chevron.down" : "chevron.right"))
+ Text(Image(systemName: self.isExpanded ? "chevron.down" : "chevron.right"))
.bold()
.font(.system(size: 10))
.opacity(0.5)
@@ -381,7 +608,7 @@ struct TrackerItemView: View {
.padding(.leading, 4)
.padding(.trailing, 2)
}
-
+
switch bookmark.type {
case .tracker:
Image("Tracker")
@@ -389,7 +616,7 @@ struct TrackerItemView: View {
.scaledToFit()
.frame(width: 16, height: 16, alignment: .center)
Text(bookmark.name).bold().lineLimit(1).truncationMode(.tail)
- if bookmark.loading {
+ if isLoading {
ProgressView()
.padding([.leading, .trailing], 2)
.controlSize(.small)
@@ -410,58 +637,26 @@ struct TrackerItemView: View {
.frame(width: 16, height: 16, alignment: .center)
Text(bookmark.name).lineLimit(1).truncationMode(.tail)
Spacer(minLength: 0)
- case .temporary:
- Spacer()
- .frame(width: 14 + 8 + 16)
- Image("Server")
- .resizable()
- .scaledToFit()
- .frame(width: 16, height: 16, alignment: .center)
- Text(bookmark.name).lineLimit(1).truncationMode(.tail)
- if let serverDescription = bookmark.serverDescription {
- Text(serverDescription)
- .foregroundStyle(.secondary)
- .lineLimit(1)
- .truncationMode(.tail)
- }
- Spacer(minLength: 0)
- if let serverUserCount = bookmark.serverUserCount,
- serverUserCount > 0 {
- Text(String(serverUserCount))
- .foregroundStyle(.secondary)
- .lineLimit(1)
-
- Circle()
- .fill(.fileComplete)
- .frame(width: 7, height: 7)
- .opacity(onlineAnimationMaxState ? 1.0 : 0.2)
- .onAppear {
- withAnimation(.easeInOut(duration: 1.0).repeatForever()) {
- onlineAnimationMaxState.toggle()
- }
- }
- .padding(.trailing, 6)
- }
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
- .onChange(of: bookmark.expanded) {
- guard bookmark.type == .tracker else {
- return
- }
-
- if bookmark.expanded {
- Task {
- await bookmark.fetchServers()
- }
- }
- }
+// .onChange(of: self.isExpanded) {
+// guard bookmark.type == .tracker else {
+// return
+// }
+//
+// if self.isExpanded {
+// Task {
+// await bookmark.fetchServers()
+// }
+// }
+// }
}
}
#if DEBUG
private struct TrackerViewPreview: View {
- @State var selection: Bookmark? = nil
+ @State var selection: TrackerSelection? = nil
var body: some View {
TrackerView(selection: $selection)