diff options
| author | Dustin Mierau <dustin@mierau.me> | 2025-11-07 12:44:55 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2025-11-07 12:44:55 -0800 |
| commit | a55318fa8d643160900bec3e6b14e7404c63497a (patch) | |
| tree | ab6a9eca9a368b35e1490becc70f94ebde6ac271 /Hotline/Utility | |
| parent | 786a387d58fc66ae20716a9dee46b74dff8e6894 (diff) | |
Organize Library folder a bit. Update Tracker add/edit sheet design.
Diffstat (limited to 'Hotline/Utility')
| -rw-r--r-- | Hotline/Utility/BookmarkDocument.swift | 32 | ||||
| -rw-r--r-- | Hotline/Utility/ColorArt.swift | 424 | ||||
| -rw-r--r-- | Hotline/Utility/DAKeychain.swift | 81 | ||||
| -rw-r--r-- | Hotline/Utility/FoundationExtensions.swift | 107 | ||||
| -rw-r--r-- | Hotline/Utility/NSWindowBridge.swift | 46 | ||||
| -rw-r--r-- | Hotline/Utility/ObservableScrollView.swift | 38 | ||||
| -rw-r--r-- | Hotline/Utility/RegularExpressions.swift | 235 | ||||
| -rw-r--r-- | Hotline/Utility/SoundEffects.swift | 50 | ||||
| -rw-r--r-- | Hotline/Utility/SwiftUIExtensions.swift | 8 | ||||
| -rw-r--r-- | Hotline/Utility/TextDocument.swift | 31 |
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)!) - } -} |