aboutsummaryrefslogtreecommitdiff
path: root/Hotline/Utility
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-11-07 12:44:55 -0800
committerDustin Mierau <dustin@mierau.me>2025-11-07 12:44:55 -0800
commita55318fa8d643160900bec3e6b14e7404c63497a (patch)
treeab6a9eca9a368b35e1490becc70f94ebde6ac271 /Hotline/Utility
parent786a387d58fc66ae20716a9dee46b74dff8e6894 (diff)
Organize Library folder a bit. Update Tracker add/edit sheet design.
Diffstat (limited to 'Hotline/Utility')
-rw-r--r--Hotline/Utility/BookmarkDocument.swift32
-rw-r--r--Hotline/Utility/ColorArt.swift424
-rw-r--r--Hotline/Utility/DAKeychain.swift81
-rw-r--r--Hotline/Utility/FoundationExtensions.swift107
-rw-r--r--Hotline/Utility/NSWindowBridge.swift46
-rw-r--r--Hotline/Utility/ObservableScrollView.swift38
-rw-r--r--Hotline/Utility/RegularExpressions.swift235
-rw-r--r--Hotline/Utility/SoundEffects.swift50
-rw-r--r--Hotline/Utility/SwiftUIExtensions.swift8
-rw-r--r--Hotline/Utility/TextDocument.swift31
10 files changed, 0 insertions, 1052 deletions
diff --git a/Hotline/Utility/BookmarkDocument.swift b/Hotline/Utility/BookmarkDocument.swift
deleted file mode 100644
index 47fa442..0000000
--- a/Hotline/Utility/BookmarkDocument.swift
+++ /dev/null
@@ -1,32 +0,0 @@
-import SwiftUI
-import Foundation
-import UniformTypeIdentifiers
-
-struct BookmarkDocument: FileDocument {
- static var readableContentTypes: [UTType] { [.data, UTType(filenameExtension: "hlbm")!] }
- static var writableContentTypes: [UTType] { [.data, UTType(filenameExtension: "hlbm")!] }
-
- var bookmark: Bookmark
-
- init(bookmark: Bookmark) {
- self.bookmark = bookmark
- }
-
- init(configuration: ReadConfiguration) throws {
- guard configuration.file.isRegularFile,
- let data = configuration.file.regularFileContents,
- let fileName = configuration.file.preferredFilename,
- let bookmark = Bookmark(fileData: data, name: (fileName as NSString).deletingPathExtension)
- else {
- throw CocoaError(.fileReadCorruptFile)
- }
- self.bookmark = bookmark
- }
-
- func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
- let wrapper = FileWrapper(regularFileWithContents: self.bookmark.bookmarkFileData()!)
- wrapper.fileAttributes[FileAttributeKey.hfsCreatorCode.rawValue] = "HTLC".fourCharCode()
- wrapper.fileAttributes[FileAttributeKey.hfsTypeCode.rawValue] = "HTbm".fourCharCode()
- return wrapper
- }
-}
diff --git a/Hotline/Utility/ColorArt.swift b/Hotline/Utility/ColorArt.swift
deleted file mode 100644
index 93889a3..0000000
--- a/Hotline/Utility/ColorArt.swift
+++ /dev/null
@@ -1,424 +0,0 @@
-
-// Swift translation and modernization
-// by Dustin Mierau
-//
-// of:
-//
-// ColorArt.swift
-// SLColorArt by Panic Inc.
-//
-// 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
-
-// ColorArt.analyze(image: img) -> ColorArt?
-
-struct ColorArt: Equatable {
- let backgroundColor: NSColor
- let primaryColor: NSColor
- let secondaryColor: NSColor
- let detailColor: NSColor
-// let scaledImage: NSImage
-
- static func == (lhs: ColorArt, rhs: ColorArt) -> Bool {
- return lhs.backgroundColor == rhs.backgroundColor &&
- lhs.primaryColor == rhs.primaryColor &&
- lhs.secondaryColor == rhs.secondaryColor &&
- lhs.detailColor == rhs.detailColor
- }
-
- static func analyze(image: NSImage) -> ColorArt? {
- print("ColorArt.analyze: Starting, image size: \(image.size)")
- // Scale image to a reasonable size for analysis
- // This is important because:
- // 1. Makes analysis faster (fewer pixels)
- // 2. Normalizes weird image dimensions
- // 3. Ensures CGImage conversion succeeds
- print("ColorArt.analyze: Calling scaleImage...")
- let finalImage = Self.scaleImage(image, size: NSSize(width: 100, height: 100))
- print("ColorArt.analyze: scaleImage returned, scaled size: \(finalImage.size)")
-
- guard let colors = Self.analyzeImage(finalImage) else {
- print("ColorArt.analyze: failed with no colors")
- return nil
- }
-
- print("ColorArt.analyze: returning colors", colors)
-
- return ColorArt(backgroundColor: colors.background,
- primaryColor: colors.primary,
- secondaryColor: colors.secondary,
- detailColor: colors.detail)
- }
-
- // MARK: - Image Scaling
-
- private static func scaleImage(_ image: NSImage, size scaledSize: NSSize) -> NSImage {
- print("ColorArt.scaleImage: Entered, input: \(image.size), target: \(scaledSize)")
- // Get CGImage directly without using lockFocus
- print("ColorArt.scaleImage: Getting CGImage...")
- guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
- print("ColorArt.scaleImage: Failed to get CGImage, returning original")
- return image
- }
- print("ColorArt.scaleImage: Got CGImage")
-
- let imageSize = image.size
- let squareSize = min(imageSize.width, imageSize.height)
-
- // Use native square size if passed zero size
- let finalScaledSize = scaledSize == .zero ? NSSize(width: squareSize, height: squareSize) : scaledSize
-
- // Create bitmap context for drawing
- let width = Int(finalScaledSize.width)
- let height = Int(finalScaledSize.height)
- let colorSpace = CGColorSpaceCreateDeviceRGB()
- let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
-
- guard let context = CGContext(
- data: nil,
- width: width,
- height: height,
- bitsPerComponent: 8,
- bytesPerRow: width * 4,
- space: colorSpace,
- bitmapInfo: bitmapInfo.rawValue
- ) else {
- return image
- }
-
- // Draw the image scaled
- context.interpolationQuality = .high
- context.draw(cgImage, in: CGRect(x: 0, y: 0, width: finalScaledSize.width, height: finalScaledSize.height))
-
- // Create NSImage from context
- guard let scaledCGImage = context.makeImage() else {
- return image
- }
-
- let bitmapRep = NSBitmapImageRep(cgImage: scaledCGImage)
- let finalImage = NSImage(size: finalScaledSize)
- 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
- }
-
- // Convert all colors to calibrated RGB color space for consistency
- // This ensures all colors are in the same color space and prevents
- // any color space conversion issues when used in SwiftUI
- let rgbColorSpace = NSColorSpace.genericRGB
- let finalBackground = backgroundColor.usingColorSpace(rgbColorSpace) ?? backgroundColor
- let finalPrimary = primaryColor!.usingColorSpace(rgbColorSpace) ?? primaryColor!
- let finalSecondary = secondaryColor!.usingColorSpace(rgbColorSpace) ?? secondaryColor!
- let finalDetail = detailColor!.usingColorSpace(rgbColorSpace) ?? detailColor!
-
- return (finalBackground, finalPrimary, finalSecondary, finalDetail)
- }
-
- // MARK: - Edge Color Detection
-
- private static func findEdgeColor(_ image: NSImage, imageColors: inout NSCountedSet?) -> NSColor? {
- var bitmapRep: NSBitmapImageRep?
-
- // Try to get existing bitmap representation
- if let existingRep = image.representations.last as? NSBitmapImageRep {
- bitmapRep = existingRep
- } else {
- // Create bitmap rep from CGImage instead of using lockFocus
- guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
- return nil
- }
- bitmapRep = NSBitmapImageRep(cgImage: cgImage)
- }
-
- // Convert to RGB color space
- guard let bitmapRep = bitmapRep?.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/Utility/DAKeychain.swift b/Hotline/Utility/DAKeychain.swift
deleted file mode 100644
index 8031886..0000000
--- a/Hotline/Utility/DAKeychain.swift
+++ /dev/null
@@ -1,81 +0,0 @@
-//
-// DAKeychain.swift
-// DAKeychain
-//
-// Created by Dejan on 28/02/2017.
-// Copyright © 2017 Dejan. All rights reserved.
-//
-
-import Foundation
-
-open class DAKeychain {
-
- open var loggingEnabled = false
-
- private init() {}
- public static let shared = DAKeychain()
-
- open subscript(key: String) -> String? {
- get {
- return load(withKey: key)
- } set {
- DispatchQueue.global().sync(flags: .barrier) {
- self.save(newValue, forKey: key)
- }
- }
- }
-
- private func save(_ string: String?, forKey key: String) {
- let query = keychainQuery(withKey: key)
- let objectData: Data? = string?.data(using: .utf8, allowLossyConversion: false)
-
- if SecItemCopyMatching(query, nil) == noErr {
- if let dictData = objectData {
- let status = SecItemUpdate(query, NSDictionary(dictionary: [kSecValueData: dictData]))
- logPrint("Update status: ", status)
- } else {
- let status = SecItemDelete(query)
- logPrint("Delete status: ", status)
- }
- } else {
- if let dictData = objectData {
- query.setValue(dictData, forKey: kSecValueData as String)
- let status = SecItemAdd(query, nil)
- logPrint("Update status: ", status)
- }
- }
- }
-
- private func load(withKey key: String) -> String? {
- let query = keychainQuery(withKey: key)
- query.setValue(kCFBooleanTrue, forKey: kSecReturnData as String)
- query.setValue(kCFBooleanTrue, forKey: kSecReturnAttributes as String)
-
- var result: CFTypeRef?
- let status = SecItemCopyMatching(query, &result)
-
- guard
- let resultsDict = result as? NSDictionary,
- let resultsData = resultsDict.value(forKey: kSecValueData as String) as? Data,
- status == noErr
- else {
- logPrint("Load status: ", status)
- return nil
- }
- return String(data: resultsData, encoding: .utf8)
- }
-
- private func keychainQuery(withKey key: String) -> NSMutableDictionary {
- let result = NSMutableDictionary()
- result.setValue(kSecClassGenericPassword, forKey: kSecClass as String)
- result.setValue(key, forKey: kSecAttrService as String)
- result.setValue(kSecAttrAccessibleWhenUnlocked, forKey: kSecAttrAccessible as String)
- return result
- }
-
- private func logPrint(_ items: Any...) {
- if loggingEnabled {
- print(items)
- }
- }
-}
diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift
deleted file mode 100644
index 90a3032..0000000
--- a/Hotline/Utility/FoundationExtensions.swift
+++ /dev/null
@@ -1,107 +0,0 @@
-import Foundation
-import SwiftUI
-
-enum Endianness {
- case big
- case little
-}
-
-extension String {
-
- func markdownToAttributedString() -> AttributedString {
- let markdownText = self.convertingLinksToMarkdown()
- let attr = (try? AttributedString(markdown: markdownText, options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace))) ?? AttributedString(self)
-
- return attr
- }
-
- func convertToAttributedStringWithLinks() -> AttributedString {
- let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: self)
- let matches = self.ranges(of: RegularExpressions.relaxedLink)
- for match in matches {
- let matchString = String(self[match])
- if matchString.isEmailAddress() {
- attributedString.addAttribute(.link, value: "mailto:\(matchString)", range: NSRange(match, in: self))
- }
- else {
- attributedString.addAttribute(.link, value: matchString, range: NSRange(match, in: self))
- }
-// attributedString.addAttribute(.underlineStyle, value: 1, range: NSRange(match, in: self))
- }
- return AttributedString(attributedString)
- }
-
- func isEmailAddress() -> Bool {
- self.wholeMatch(of: RegularExpressions.emailAddress) != nil
- }
-
- func isWebURL() -> Bool {
- guard let url = URL(string: self) else {
- return false
- }
- switch url.scheme?.lowercased() {
- case "http", "https":
- return true
- default:
- return false
- }
- }
-
- func isImageURL() -> Bool {
- guard let url = URL(string: self) else {
- return false
- }
-
- switch url.pathExtension.lowercased() {
- case "jpg", "jpeg", "png", "gif":
- return true
- default:
- return false
- }
- }
-
- func convertingLinksToMarkdown() -> String {
-// var cp = String(self)
-
- self.replacing(RegularExpressions.relaxedLink) { match in
- let linkText = self[match.range]
-
- // Only add https:// if the link doesn't already have a scheme
- let hasScheme = (try? RegularExpressions.supportedLinkScheme.prefixMatch(in: linkText)) != nil
- let url = hasScheme ? String(linkText) : "https://\(linkText)"
-
- return "[\(linkText)](\(url))"
- }
-
-// cp.replace(RegularExpressions.relaxedLink) { match -> String in
-// let linkText = self[match.range]
-// var injectedScheme = "https://"
-// if let _ = try? RegularExpressions.supportedLinkScheme.prefixMatch(in: linkText) {
-// injectedScheme = ""
-// }
-//
-// return "[\(linkText)](\(injectedScheme)\(linkText))"
-// }
-// return cp
- }
-}
-
-
-
-extension Binding where Value: OptionSet, Value == Value.Element {
- func bindedValue(_ options: Value) -> Bool {
- return wrappedValue.contains(options)
- }
-
- func bind(_ options: Value) -> Binding<Bool> {
- return .init { () -> Bool in
- self.wrappedValue.contains(options)
- } set: { newValue in
- if newValue {
- self.wrappedValue.insert(options)
- } else {
- self.wrappedValue.remove(options)
- }
- }
- }
-}
diff --git a/Hotline/Utility/NSWindowBridge.swift b/Hotline/Utility/NSWindowBridge.swift
deleted file mode 100644
index 8f766d9..0000000
--- a/Hotline/Utility/NSWindowBridge.swift
+++ /dev/null
@@ -1,46 +0,0 @@
-import SwiftUI
-
-fileprivate class NSWindowAccessorView: NSView {
- let executeBlock: (_ window: NSWindow? ) -> ()
-
- init(_ inConfigFunction: @escaping (_ window: NSWindow? ) -> () ) {
- executeBlock = inConfigFunction
- super.init( frame: NSRect() )
- }
-
- required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
-
- public override func viewDidMoveToWindow() {
- super.viewDidMoveToWindow()
- executeBlock( self.window ) // We pass it through even if it is nil.
- }
-}
-
-public struct NSWindowAccessor: NSViewRepresentable {
- var configCode: (_ window: NSWindow? ) -> ()
-
- public init(_ configCode: @escaping (_: NSWindow?) -> Void) { self.configCode = configCode }
- public func makeNSView(context: Context) -> NSView { return NSWindowAccessorView( configCode ) }
- public func updateNSView(_ nsView: NSView, context: Context) {}
-}
-
-
-//import SwiftUI
-
- /// A helper view you can embed once per window to run a closure
-/// with the underlying NSWindow reference.
-struct WindowConfigurator: NSViewRepresentable {
- let configure: (NSWindow) -> Void
-
- func makeNSView(context: Context) -> NSView {
- let view = NSView()
- DispatchQueue.main.async {
- if let window = view.window {
- configure(window)
- }
- }
- return view
- }
-
- func updateNSView(_ nsView: NSView, context: Context) { }
-}
diff --git a/Hotline/Utility/ObservableScrollView.swift b/Hotline/Utility/ObservableScrollView.swift
deleted file mode 100644
index a6f46cc..0000000
--- a/Hotline/Utility/ObservableScrollView.swift
+++ /dev/null
@@ -1,38 +0,0 @@
-import SwiftUI
-
-struct ScrollViewOffsetPreferenceKey: PreferenceKey {
- typealias Value = CGFloat
- static var defaultValue = CGFloat.zero
- static func reduce(value: inout Value, nextValue: () -> Value) {
- value += nextValue()
- }
-}
-
-struct ObservableScrollView<Content>: View where Content : View {
- @Namespace var scrollSpace
- @Binding var scrollOffset: CGFloat
- let content: () -> Content
-
- init(scrollOffset: Binding<CGFloat>,
- @ViewBuilder content: @escaping () -> Content) {
- _scrollOffset = scrollOffset
- self.content = content
- }
-
- var body: some View {
- ScrollView {
- content()
- .background(GeometryReader { geo in
- let offset = -geo.frame(in: .named(scrollSpace)).minY
- Color.clear
- .preference(key: ScrollViewOffsetPreferenceKey.self,
- value: offset)
- })
-
- }
- .coordinateSpace(name: scrollSpace)
- .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in
- scrollOffset = value
- }
- }
-}
diff --git a/Hotline/Utility/RegularExpressions.swift b/Hotline/Utility/RegularExpressions.swift
deleted file mode 100644
index c1f2a18..0000000
--- a/Hotline/Utility/RegularExpressions.swift
+++ /dev/null
@@ -1,235 +0,0 @@
-import RegexBuilder
-
-struct RegularExpressions {
- static let messageBoardDivider = Regex {
- Capture {
- OneOrMore {
- CharacterClass(.newlineSequence)
- }
- ZeroOrMore {
- CharacterClass(.whitespace, .newlineSequence)
- }
- Repeat(2...) {
- CharacterClass(.anyOf("_-"))
- }
- ZeroOrMore {
- CharacterClass(.whitespace)
- }
- OneOrMore {
- CharacterClass(.newlineSequence)
- }
- }
- }
-
- static let supportedLinkScheme = Regex {
- Anchor.startOfLine
- ChoiceOf {
- "hotline"
- "http"
- "https"
- }
- "://"
- }.ignoresCase().anchorsMatchLineEndings()
-
- static let relaxedLink = Regex {
- ChoiceOf {
- Anchor.startOfLine
- Anchor.wordBoundary
- }
- Capture {
- // scheme (optional)
- Optionally {
- ChoiceOf {
- "hotline://"
- "http://"
- "https://"
- }
- }
- // domain name
- OneOrMore {
- CharacterClass(
- .anyOf(".-@"),
- ("a"..."z"),
- ("0"..."9")
- )
- }
- // top-level domain name
- "."
- ChoiceOf {
- "com"
- "net"
- "org"
- "edu"
- "gov"
- "mil"
- "aero"
- "asia"
- "biz"
- "cat"
- "coop"
- "info"
- "int"
- "jobs"
- "mobi"
- "museum"
- "name"
- "pizza"
- "post"
- "pro"
- "red"
- "tel"
- "today"
- "travel"
- "garden"
- "online"
- "ai"
- "be"
- "by"
- "ca"
- "co"
- "de"
- "er"
- "es"
- "fr"
- "gs"
- "ie"
- "im"
- "in"
- "io"
- "is"
- "it"
- "jp"
- "la"
- "ly"
- "ma"
- "md"
- "me"
- "my"
- "nl"
- "ps"
- "pt"
- "ja"
- "st"
- "to"
- "tv"
- "uk"
- "ws"
- }
- // Port
- Optionally {
- ":"
- OneOrMore {
- CharacterClass(.digit)
- }
- }
- // path
- ZeroOrMore {
- CharacterClass(
- .anyOf("#_-/.?=&%\\()[]"),
- ("a"..."z"),
- ("0"..."9")
- )
- }
- }
- ChoiceOf {
- Anchor.endOfLine
- Anchor.wordBoundary
- }
- }
- .anchorsMatchLineEndings()
- .ignoresCase()
-
- static let emailAddress = Regex {
- ChoiceOf {
- Anchor.startOfLine
- Anchor.wordBoundary
- }
- Capture {
- // username
- OneOrMore {
- CharacterClass(
- .anyOf(".-_"),
- ("a"..."z"),
- ("0"..."9")
- )
- }
- "@"
- // domain name
- OneOrMore {
- CharacterClass(
- .anyOf(".-"),
- ("a"..."z"),
- ("0"..."9")
- )
- }
- // top-level domain name
- "."
- ChoiceOf {
- "com"
- "net"
- "org"
- "edu"
- "gov"
- "mil"
- "aero"
- "asia"
- "biz"
- "cat"
- "coop"
- "info"
- "int"
- "jobs"
- "mobi"
- "museum"
- "name"
- "pizza"
- "post"
- "pro"
- "red"
- "tel"
- "today"
- "travel"
- "garden"
- "online"
- "ai"
- "be"
- "by"
- "ca"
- "co"
- "de"
- "er"
- "es"
- "fr"
- "gs"
- "ie"
- "im"
- "in"
- "io"
- "is"
- "it"
- "jp"
- "la"
- "ly"
- "ma"
- "md"
- "me"
- "my"
- "nl"
- "ps"
- "pt"
- "ja"
- "st"
- "to"
- "tv"
- "uk"
- "ws"
- }
- }
- ChoiceOf {
- Anchor.endOfLine
- Anchor.wordBoundary
- }
- }
- .anchorsMatchLineEndings()
- .ignoresCase()
-}
diff --git a/Hotline/Utility/SoundEffects.swift b/Hotline/Utility/SoundEffects.swift
deleted file mode 100644
index 93f8b9a..0000000
--- a/Hotline/Utility/SoundEffects.swift
+++ /dev/null
@@ -1,50 +0,0 @@
-import Foundation
-import AVFAudio
-
-enum SoundEffects: String {
- case loggedIn = "logged-in"
- case chatMessage = "chat-message"
- case transferComplete = "transfer-complete"
- case userLogin = "user-login"
- case userLogout = "user-logout"
- case newNews = "new-news"
- case serverMessage = "server-message"
- case error = "error"
-}
-
-@Observable
-class SoundEffectPlayer: NSObject, AVAudioPlayerDelegate {
- static let shared = SoundEffectPlayer()
-
- private var activeSounds: [AVAudioPlayer] = []
-
- func playSoundEffect(_ name: SoundEffects) {
- // Load a local sound file
- guard let soundFileURL = Bundle.main.url(
- forResource: name.rawValue,
- withExtension: "aiff"
- ) else {
- return
- }
-
- DispatchQueue.main.async { [weak self] in
- guard let self = self else {
- return
- }
-
- if let soundEffect = try? AVAudioPlayer(contentsOf: soundFileURL) {
- soundEffect.delegate = self
- soundEffect.volume = 0.75
- soundEffect.play()
-
- self.activeSounds.append(soundEffect)
- }
- }
- }
-
- func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
- if let i = self.activeSounds.firstIndex(of: player) {
- self.activeSounds.remove(at: i)
- }
- }
-}
diff --git a/Hotline/Utility/SwiftUIExtensions.swift b/Hotline/Utility/SwiftUIExtensions.swift
deleted file mode 100644
index d0dfff4..0000000
--- a/Hotline/Utility/SwiftUIExtensions.swift
+++ /dev/null
@@ -1,8 +0,0 @@
-import SwiftUI
-import Foundation
-
-extension Color {
- init(hex: Int, opacity: Double = 1.0) {
- self.init(red: Double((hex >> 16) & 0xFF) / 255.0, green: Double((hex >> 8) & 0xFF) / 255.0, blue: Double(hex & 0xFF) / 255.0, opacity: opacity)
- }
-}
diff --git a/Hotline/Utility/TextDocument.swift b/Hotline/Utility/TextDocument.swift
deleted file mode 100644
index 82727de..0000000
--- a/Hotline/Utility/TextDocument.swift
+++ /dev/null
@@ -1,31 +0,0 @@
-import Foundation
-import UniformTypeIdentifiers
-import SwiftUI
-
-struct TextFile: FileDocument {
- // tell the system we support only plain text
- static var readableContentTypes = [UTType.plainText, UTType.utf8PlainText]
-
- // by default our document is empty
- var text = ""
-
- // a simple initializer that creates new, empty documents
- init(initialText: String = "") {
- text = initialText
- }
-
- // this initializer loads data that has been saved previously
- init(configuration: ReadConfiguration) throws {
-
- if let data = configuration.file.regularFileContents {
- if let str = String(data: data, encoding: .utf8) {
- self.text = str
- }
- }
- }
-
- // this will be called when the system wants to write our data to disk
- func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
- return FileWrapper(regularFileWithContents: self.text.data(using: .utf8)!)
- }
-}