diff options
| author | Dustin Mierau <dustin@mierau.me> | 2025-10-28 09:33:19 -0700 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2025-10-28 09:33:19 -0700 |
| commit | d23d0ad9405c1622569415e6ce16d3768af2936a (patch) | |
| tree | 13b1f3ba43367d9b53383c1d5e90eb8959d158e1 | |
| parent | d8a10c38a3c26a187d8f310eba56d4e9ca8c5b27 (diff) | |
New default banner artwork. Theme toolbar to server's banner (if any).
| -rw-r--r-- | Hotline.xcodeproj/project.pbxproj | 4 | ||||
| -rw-r--r-- | Hotline/Assets.xcassets/Default Banner.imageset/Contents.json | 6 | ||||
| -rw-r--r-- | Hotline/Assets.xcassets/Default Banner.imageset/Default Banner.png | bin | 0 -> 54979 bytes | |||
| -rw-r--r-- | Hotline/Assets.xcassets/Default Banner.imageset/Default Banner@2x.png | bin | 0 -> 218553 bytes | |||
| -rw-r--r-- | Hotline/Assets.xcassets/Default Banner.imageset/Default Banner@3x.png | bin | 0 -> 474621 bytes | |||
| -rw-r--r-- | Hotline/Assets.xcassets/Default Banner.imageset/Frame 2.png | bin | 59733 -> 0 bytes | |||
| -rw-r--r-- | Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@2x.png | bin | 207508 -> 0 bytes | |||
| -rw-r--r-- | Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@3x.png | bin | 386312 -> 0 bytes | |||
| -rw-r--r-- | Hotline/Utility/ColorArt.swift | 376 | ||||
| -rw-r--r-- | Hotline/macOS/Chat/ChatView.swift | 3 | ||||
| -rw-r--r-- | Hotline/macOS/HotlinePanelView.swift | 25 |
11 files changed, 402 insertions, 12 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj index a173d55..5c0b943 100644 --- a/Hotline.xcodeproj/project.pbxproj +++ b/Hotline.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ DA5268A02EB073BC00DCB941 /* SoundSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA52689F2EB073BC00DCB941 /* SoundSettingsView.swift */; platformFilters = (macos, ); }; DA5268A32EB0741B00DCB941 /* FolderItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268A22EB0741B00DCB941 /* FolderItemView.swift */; platformFilters = (macos, ); }; DA5268A52EB0743000DCB941 /* FileItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268A42EB0743000DCB941 /* FileItemView.swift */; platformFilters = (macos, ); }; + DA5268AB2EB11EA300DCB941 /* ColorArt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268AA2EB11EA300DCB941 /* ColorArt.swift */; }; DA55AC732BE42AF000034857 /* AsyncLinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA55AC722BE42AF000034857 /* AsyncLinkPreview.swift */; platformFilters = (macos, ); }; DA55AC752BE4888300034857 /* InstantMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA55AC742BE4888300034857 /* InstantMessage.swift */; }; DA55AC772BE589F700034857 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA55AC762BE589F700034857 /* AboutView.swift */; platformFilters = (macos, ); }; @@ -128,6 +129,7 @@ DA52689F2EB073BC00DCB941 /* SoundSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundSettingsView.swift; sourceTree = "<group>"; }; DA5268A22EB0741B00DCB941 /* FolderItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderItemView.swift; sourceTree = "<group>"; }; DA5268A42EB0743000DCB941 /* FileItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileItemView.swift; sourceTree = "<group>"; }; + DA5268AA2EB11EA300DCB941 /* ColorArt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorArt.swift; sourceTree = "<group>"; }; DA55AC722BE42AF000034857 /* AsyncLinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncLinkPreview.swift; sourceTree = "<group>"; }; DA55AC742BE4888300034857 /* InstantMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantMessage.swift; sourceTree = "<group>"; }; DA55AC762BE589F700034857 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; }; @@ -397,6 +399,7 @@ isa = PBXGroup; children = ( DABFCC282B1530DC009F40D2 /* FoundationExtensions.swift */, + DA5268AA2EB11EA300DCB941 /* ColorArt.swift */, DAC002182B21630900A6C290 /* SwiftUIExtensions.swift */, DA7725402B21435B006C5ABB /* ObservableScrollView.swift */, DAE735062B3251B3000C56F6 /* SoundEffects.swift */, @@ -554,6 +557,7 @@ DA65499C2BEC3FBD00EDB697 /* ServerAgreementView.swift in Sources */, DAB4D8802B4C8E9A0048A05C /* URLAdditions.swift in Sources */, DA2863DD2B3E8B7000A7D050 /* FilePreview.swift in Sources */, + DA5268AB2EB11EA300DCB941 /* ColorArt.swift in Sources */, DA2863D82B37AD1C00A7D050 /* SettingsView.swift in Sources */, DA7725412B21435B006C5ABB /* ObservableScrollView.swift in Sources */, DAE734FF2B2E6750000C56F6 /* ChatView.swift in Sources */, diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Contents.json b/Hotline/Assets.xcassets/Default Banner.imageset/Contents.json index 1abbabf..ba0af63 100644 --- a/Hotline/Assets.xcassets/Default Banner.imageset/Contents.json +++ b/Hotline/Assets.xcassets/Default Banner.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "Frame 2.png", + "filename" : "Default Banner.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "Frame 2@2x.png", + "filename" : "Default Banner@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "Frame 2@3x.png", + "filename" : "Default Banner@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Default Banner.png b/Hotline/Assets.xcassets/Default Banner.imageset/Default Banner.png Binary files differnew file mode 100644 index 0000000..d960610 --- /dev/null +++ b/Hotline/Assets.xcassets/Default Banner.imageset/Default Banner.png diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Default Banner@2x.png b/Hotline/Assets.xcassets/Default Banner.imageset/Default Banner@2x.png Binary files differnew file mode 100644 index 0000000..4ce4eab --- /dev/null +++ b/Hotline/Assets.xcassets/Default Banner.imageset/Default Banner@2x.png diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Default Banner@3x.png b/Hotline/Assets.xcassets/Default Banner.imageset/Default Banner@3x.png Binary files differnew file mode 100644 index 0000000..13c2236 --- /dev/null +++ b/Hotline/Assets.xcassets/Default Banner.imageset/Default Banner@3x.png diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2.png b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2.png Binary files differdeleted file mode 100644 index fae0905..0000000 --- a/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2.png +++ /dev/null diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@2x.png b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@2x.png Binary files differdeleted file mode 100644 index 163e634..0000000 --- a/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@2x.png +++ /dev/null diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@3x.png b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@3x.png Binary files differdeleted file mode 100644 index 3f2ef8f..0000000 --- a/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@3x.png +++ /dev/null diff --git a/Hotline/Utility/ColorArt.swift b/Hotline/Utility/ColorArt.swift new file mode 100644 index 0000000..abbe04b --- /dev/null +++ b/Hotline/Utility/ColorArt.swift @@ -0,0 +1,376 @@ + +// ColorArt.swift +// SLColorArt by Panic Inc. +// Swift translation by Dustin Mierau +// +// Copyright (C) 2012 Panic Inc. Code by Wade Cosgrove. All rights reserved. +// +// Redistribution and use, with or without modification, are permitted +// provided that the following conditions are met: +// +// - Redistributions must reproduce the above copyright notice, this list of +// conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// - Neither the name of Panic Inc nor the names of its contributors may be used +// to endorse or promote works derived from this software without specific prior +// written permission from Panic Inc. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL PANIC INC BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +import AppKit +import SwiftUI + +fileprivate let kColorThresholdMinimumPercentage: CGFloat = 0.001 + +struct ColorArt { + let backgroundColor: NSColor + let primaryColor: NSColor + let secondaryColor: NSColor + let detailColor: NSColor + let scaledImage: NSImage + + init?(image: NSImage, scaledSize: NSSize = .zero) { + let finalImage = Self.scaleImage(image, size: scaledSize) + self.scaledImage = finalImage + + guard let colors = Self.analyzeImage(finalImage) else { + return nil + } + + self.backgroundColor = colors.background + self.primaryColor = colors.primary + self.secondaryColor = colors.secondary + self.detailColor = colors.detail + } + + // MARK: - Image Scaling + + private static func scaleImage(_ image: NSImage, size scaledSize: NSSize) -> NSImage { + let imageSize = image.size + let squareImage = NSImage(size: NSSize(width: imageSize.width, height: imageSize.width)) + var drawRect: NSRect + + // Make the image square + if imageSize.height > imageSize.width { + drawRect = NSRect(x: 0, y: imageSize.height - imageSize.width, width: imageSize.width, height: imageSize.width) + } else { + drawRect = NSRect(x: 0, y: 0, width: imageSize.height, height: imageSize.height) + } + + // Use native square size if passed zero size + let finalScaledSize = scaledSize == .zero ? drawRect.size : scaledSize + let scaledImage = NSImage(size: finalScaledSize) + + squareImage.lockFocus() + image.draw(in: NSRect(x: 0, y: 0, width: imageSize.width, height: imageSize.width), from: drawRect, operation: .sourceOver, fraction: 1.0) + squareImage.unlockFocus() + + // Scale the image to the desired size + scaledImage.lockFocus() + squareImage.draw(in: NSRect(x: 0, y: 0, width: finalScaledSize.width, height: finalScaledSize.height), from: .zero, operation: .sourceOver, fraction: 1.0) + scaledImage.unlockFocus() + + // Convert back to readable bitmap data + guard let cgImage = scaledImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else { + return scaledImage + } + + let bitmapRep = NSBitmapImageRep(cgImage: cgImage) + let finalImage = NSImage(size: scaledImage.size) + finalImage.addRepresentation(bitmapRep) + + return finalImage + } + + // MARK: - Image Analysis + + private static func analyzeImage(_ image: NSImage) -> (background: NSColor, primary: NSColor, secondary: NSColor, detail: NSColor)? { + var imageColors: NSCountedSet? + guard let backgroundColor = self.findEdgeColor(image, imageColors: &imageColors), + let colors = imageColors + else { + return nil + } + + let darkBackground = backgroundColor.isDarkColor + var primaryColor: NSColor? + var secondaryColor: NSColor? + var detailColor: NSColor? + + self.findTextColors(colors, primaryColor: &primaryColor, secondaryColor: &secondaryColor, detailColor: &detailColor, backgroundColor: backgroundColor) + + // Fallback to black or white if colors not found + if primaryColor == nil { + primaryColor = darkBackground ? .white : .black + } + + if secondaryColor == nil { + secondaryColor = darkBackground ? .white : .black + } + + if detailColor == nil { + detailColor = darkBackground ? .white : .black + } + + return (backgroundColor, primaryColor!, secondaryColor!, detailColor!) + } + + // MARK: - Edge Color Detection + + private static func findEdgeColor(_ image: NSImage, imageColors: inout NSCountedSet?) -> NSColor? { + guard var imageRep = image.representations.last else { + return nil + } + + if !(imageRep is NSBitmapImageRep) { + image.lockFocus() + imageRep = NSBitmapImageRep(focusedViewRect: NSRect(x: 0, y: 0, width: image.size.width, height: image.size.height))! + image.unlockFocus() + } + + // Convert to RGB color space + guard let bitmapRep = (imageRep as? NSBitmapImageRep)?.converting(to: .genericRGB, renderingIntent: .default) else { + return nil + } + + let pixelsWide = bitmapRep.pixelsWide + let pixelsHigh = bitmapRep.pixelsHigh + + let colors = NSCountedSet(capacity: pixelsWide * pixelsHigh) + let leftEdgeColors = NSCountedSet(capacity: pixelsHigh) + var searchColumnX = 0 + + for x in 0..<pixelsWide { + for y in 0..<pixelsHigh { + guard let color = bitmapRep.colorAt(x: x, y: y) else { continue } + + if x == searchColumnX { + // Make sure it's a meaningful color + if color.alphaComponent > 0.5 { + leftEdgeColors.add(color) + } + } + + if color.alphaComponent > CGFloat.ulpOfOne { + colors.add(color) + } + } + + // Background is clear, keep looking in next column for background color + if leftEdgeColors.count == 0 { + searchColumnX += 1 + } + } + + imageColors = colors + + var sortedColors: [CountedColor] = [] + + for color in leftEdgeColors { + guard let nsColor = color as? NSColor else { continue } + let colorCount = leftEdgeColors.count(for: nsColor) + + let randomColorsThreshold = Int(CGFloat(pixelsHigh) * kColorThresholdMinimumPercentage) + + if colorCount <= randomColorsThreshold { + continue + } + + sortedColors.append(CountedColor(color: nsColor, count: colorCount)) + } + + sortedColors.sort { $0.count > $1.count } + + guard var proposedEdgeColor = sortedColors.first else { + return nil + } + + // Want to choose color over black/white so we keep looking + if proposedEdgeColor.color.isBlackOrWhite { + for i in 1..<sortedColors.count { + let nextProposedColor = sortedColors[i] + + // Make sure the second choice color is 30% as common as the first choice + if Double(nextProposedColor.count) / Double(proposedEdgeColor.count) > 0.3 { + if !nextProposedColor.color.isBlackOrWhite { + proposedEdgeColor = nextProposedColor + break + } + } else { + // Reached color threshold less than 30% of the original proposed edge color + break + } + } + } + + return proposedEdgeColor.color + } + + // MARK: - Text Color Detection + + private static func findTextColors(_ colors: NSCountedSet, primaryColor: inout NSColor?, secondaryColor: inout NSColor?, detailColor: inout NSColor?, backgroundColor: NSColor) { + var sortedColors: [CountedColor] = [] + let findDarkTextColor = !backgroundColor.isDarkColor + + for color in colors { + guard let nsColor = color as? NSColor else { continue } + let adjustedColor = nsColor.withMinimumSaturation(0.15) + + if adjustedColor.isDarkColor == findDarkTextColor { + let colorCount = colors.count(for: nsColor) + sortedColors.append(CountedColor(color: adjustedColor, count: colorCount)) + } + } + + sortedColors.sort { $0.count > $1.count } + + for container in sortedColors { + let curColor = container.color + + if primaryColor == nil { + if curColor.isContrasting(to: backgroundColor) { + primaryColor = curColor + } + } else if secondaryColor == nil { + if let primary = primaryColor, + primary.isDistinct(from: curColor) && curColor.isContrasting(to: backgroundColor) { + secondaryColor = curColor + } + } else if detailColor == nil { + if let primary = primaryColor, + let secondary = secondaryColor, + secondary.isDistinct(from: curColor) && + primary.isDistinct(from: curColor) && + curColor.isContrasting(to: backgroundColor) { + detailColor = curColor + break + } + } + } + } +} + +// MARK: - Helper Classes + +fileprivate struct CountedColor { + let color: NSColor + let count: Int +} + +// MARK: - NSColor Extensions + +extension NSColor { + var isDarkColor: Bool { + guard let convertedColor = usingColorSpace(.genericRGB) else { + return false + } + + var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 + convertedColor.getRed(&r, green: &g, blue: &b, alpha: &a) + + let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b + + return lum < 0.5 + } + + func isDistinct(from compareColor: NSColor) -> Bool { + guard let convertedColor = usingColorSpace(.genericRGB), + let convertedCompareColor = compareColor.usingColorSpace(.genericRGB) + else { + return false + } + + var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 + var r1: CGFloat = 0, g1: CGFloat = 0, b1: CGFloat = 0, a1: CGFloat = 0 + + convertedColor.getRed(&r, green: &g, blue: &b, alpha: &a) + convertedCompareColor.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) + + let threshold: CGFloat = 0.25 + + if abs(r - r1) > threshold || abs(g - g1) > threshold || abs(b - b1) > threshold || abs(a - a1) > threshold { + // Check for grays, prevent multiple gray colors + if abs(r - g) < 0.03 && abs(r - b) < 0.03 { + if abs(r1 - g1) < 0.03 && abs(r1 - b1) < 0.03 { + return false + } + } + + return true + } + + return false + } + + func withMinimumSaturation(_ minSaturation: CGFloat) -> NSColor { + guard let tempColor = usingColorSpace(.genericRGB) else { + return self + } + + var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0 + tempColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + + if saturation < minSaturation { + return NSColor(calibratedHue: hue, saturation: minSaturation, brightness: brightness, alpha: alpha) + } + + return self + } + + var isBlackOrWhite: Bool { + guard let tempColor = usingColorSpace(.genericRGB) else { + return false + } + + var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 + tempColor.getRed(&r, green: &g, blue: &b, alpha: &a) + + // White + if r > 0.91 && g > 0.91 && b > 0.91 { + return true + } + + // Black + if r < 0.09 && g < 0.09 && b < 0.09 { + return true + } + + return false + } + + func isContrasting(to color: NSColor) -> Bool { + guard let backgroundColor = usingColorSpace(.genericRGB), + let foregroundColor = color.usingColorSpace(.genericRGB) + else { + return true + } + + var br: CGFloat = 0, bg: CGFloat = 0, bb: CGFloat = 0, ba: CGFloat = 0 + var fr: CGFloat = 0, fg: CGFloat = 0, fb: CGFloat = 0, fa: CGFloat = 0 + + backgroundColor.getRed(&br, green: &bg, blue: &bb, alpha: &ba) + foregroundColor.getRed(&fr, green: &fg, blue: &fb, alpha: &fa) + + let bLum = 0.2126 * br + 0.7152 * bg + 0.0722 * bb + let fLum = 0.2126 * fr + 0.7152 * fg + 0.0722 * fb + + let contrast: CGFloat + if bLum > fLum { + contrast = (bLum + 0.05) / (fLum + 0.05) + } else { + contrast = (fLum + 0.05) / (bLum + 0.05) + } + + return contrast > 1.6 + } +} diff --git a/Hotline/macOS/Chat/ChatView.swift b/Hotline/macOS/Chat/ChatView.swift index 65087c6..0795856 100644 --- a/Hotline/macOS/Chat/ChatView.swift +++ b/Hotline/macOS/Chat/ChatView.swift @@ -192,8 +192,9 @@ struct ChatView: View { model.markPublicChatAsRead() } .onAppear { - reader.scrollTo(bottomID, anchor: .bottom) self.focusedField = .chatInput + model.markPublicChatAsRead() + reader.scrollTo(bottomID, anchor: .bottom) } .onChange(of: self.model.bannerImage) { reader.scrollTo(bottomID, anchor: .bottom) diff --git a/Hotline/macOS/HotlinePanelView.swift b/Hotline/macOS/HotlinePanelView.swift index fd43c15..4ae4924 100644 --- a/Hotline/macOS/HotlinePanelView.swift +++ b/Hotline/macOS/HotlinePanelView.swift @@ -3,7 +3,14 @@ import SwiftUI struct HotlinePanelView: View { @Environment(\.openWindow) var openWindow @Environment(\.colorScheme) var colorScheme - + + private var colorArt: ColorArt? { + if let banner = AppState.shared.activeServerBanner { + return ColorArt(image: banner, scaledSize: NSSize(width: 100, height: 100)) + } + return nil + } + var body: some View { VStack(spacing: 0) { Image(nsImage: AppState.shared.activeServerBanner ?? NSImage(named: "Default Banner")!) @@ -34,7 +41,7 @@ struct HotlinePanelView: View { .buttonStyle(.plain) .frame(width: 20, height: 20) .help("Hotline Servers") - + Button { AppState.shared.activeServerState?.selection = .chat } @@ -47,7 +54,7 @@ struct HotlinePanelView: View { .frame(width: 20, height: 20) .disabled(AppState.shared.activeServerState == nil) .help("Public Chat") - + Button { AppState.shared.activeServerState?.selection = .board } @@ -60,7 +67,7 @@ struct HotlinePanelView: View { .frame(width: 20, height: 20) .disabled(AppState.shared.activeServerState == nil) .help("Message Board") - + Button { AppState.shared.activeServerState?.selection = .news } @@ -73,7 +80,7 @@ struct HotlinePanelView: View { .frame(width: 20, height: 20) .disabled(AppState.shared.activeServerState == nil || (AppState.shared.activeHotline?.serverVersion ?? 0) < 151) .help("News") - + Button { AppState.shared.activeServerState?.selection = .files } @@ -86,9 +93,9 @@ struct HotlinePanelView: View { .frame(width: 20, height: 20) .disabled(AppState.shared.activeServerState == nil) .help("Files") - + Spacer() - + if AppState.shared.activeHotline?.access?.contains(.canOpenUsers) == true { Button { AppState.shared.activeServerState?.selection = .accounts @@ -103,7 +110,7 @@ struct HotlinePanelView: View { .disabled(AppState.shared.activeServerState == nil) .help("Accounts") } - + SettingsLink(label: { Image("Section Settings") .resizable() @@ -116,6 +123,8 @@ struct HotlinePanelView: View { .padding(.top, 12) .padding(.bottom, 12) .padding([.leading, .trailing], 12) + .background(colorArt.map { Color(nsColor: $0.backgroundColor) } ?? Color(nsColor: .controlBackgroundColor)) + .foregroundStyle(colorArt.map { Color(nsColor: $0.primaryColor) } ?? Color.primary) // .background(Color.red.opacity(0.5).blendMode(.multiply)) // GroupBox { |