Skip to content

Commit

Permalink
πŸ”– v2.0.0 (4)
Browse files Browse the repository at this point in the history
- 🎨⚑️ Cleanups, background refresh + notifications
- 🎨 Refactors, ContainerConfigDetailsView
- 🎨 Refactors & cleanups
- 🎨✨ Optimizations, colored container cells
- ✨ Multi-window support
- 🎨 Better error handling, cleanups
- ✨🎨 Handoff, cleanups & optimizations
- ✨ "Use columns" setting
- πŸ› Fix build
- πŸ“ README
  • Loading branch information
rrroyal committed Nov 4, 2021
1 parent 016adcd commit 0894279
Show file tree
Hide file tree
Showing 58 changed files with 1,501 additions and 760 deletions.
72 changes: 57 additions & 15 deletions Harbour.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions Harbour/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// AppDelegate.swift
// Harbour
//
// Created by royal on 17/10/2021.
//

import Foundation
import UIKit
import BackgroundTasks

class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
#if DEBUG
let defaults: [String: Any] = [
"_UIConstraintBasedLayoutLogUnsatisfiable": false
]
UserDefaults.standard.register(defaults: defaults)
#endif

BGTaskScheduler.shared.register(forTaskWithIdentifier: AppState.BackgroundTask.refresh, using: nil) { task in
AppState.shared.scheduleBackgroundRefreshTask()
AppState.shared.handleBackgroundRefreshTask(task: task as! BGAppRefreshTask)
}

return true
}
}
100 changes: 0 additions & 100 deletions Harbour/Data/AppState.swift

This file was deleted.

83 changes: 83 additions & 0 deletions Harbour/Data/AppState/AppState+BackgroundTasks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// AppState+BackgroundTasks.swift
// Harbour
//
// Created by royal on 17/10/2021.
//

import Foundation
import BackgroundTasks
import UserNotifications
import WidgetKit
import PortainerKit

extension AppState {
enum BackgroundTask {
static var refresh = "\(Bundle.main.bundleIdentifier!).BackgroundRefresh"
}

public func scheduleBackgroundRefreshTask() {
let task = BackgroundTask.refresh

logger.info("(Background refresh) Scheduling background refresh task with identifier \"\(task)\"")

let request = BGAppRefreshTaskRequest(identifier: task)

do {
try BGTaskScheduler.shared.submit(request)
} catch {
logger.error("(Background refresh) Could not schedule app refresh: \(String(describing: error))")
}
}

public func cancelBackgroundRefreshTask() {
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundTask.refresh)
}

public func handleBackgroundRefreshTask(task: BGAppRefreshTask) {
logger.debug("(Background refresh) Handling background refresh for task \"\(task.identifier)\"")

WidgetCenter.shared.reloadAllTimelines()
#if DEBUG
Preferences.shared.lastBackgroundTaskDate = Date()
#endif

Task {
do {
let savedState = Portainer.shared.containers.reduce(into: [:]) { $0[$1.id] = $1.state?.rawValue }

let newContainers = try await Portainer.shared.getContainers()
let newState = newContainers.reduce(into: [:]) { $0[$1.id] = $1.state?.rawValue }
let differences = newState.filter { savedState[$0.key] != $0.value }

if differences.isEmpty {
logger.info("(Background refresh) differences.count (\(differences.count)) > 0!")

let notificationID = "ContainerStatusNotification-\(Date().timeIntervalSinceReferenceDate)"
let content = UNMutableNotificationContent()
content.relevanceScore = 1
content.interruptionLevel = .active
content.sound = .default

if differences.count == 1 {
let container = newContainers.first(where: { $0.id == differences.first?.key })
content.title = container?.displayName ?? container?.id ?? differences.first?.key ?? "Unknown container"
content.body = container?.status ?? differences.first?.value ?? "unknown"
} else {
let containers = newContainers.filter({ differences.keys.contains($0.id) }).map({ $0.displayName ?? $0.id })
content.title = "\(differences.count) containers changed!"
content.body = ListFormatter.localizedString(byJoining: containers)
}

let request = UNNotificationRequest(identifier: notificationID, content: content, trigger: nil)
try await UNUserNotificationCenter.current().add(request)
}

task.setTaskCompleted(success: true)
} catch {
logger.error("(Background refresh) Error handling background refresh: \(String(describing: error))")
task.setTaskCompleted(success: false)
}
}
}
}
14 changes: 14 additions & 0 deletions Harbour/Data/AppState/AppState+UserActivity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// AppState+UserActivity.swift
// Harbour
//
// Created by royal on 22/10/2021.
//

import Foundation

extension AppState {
enum UserActivity {
static let viewingContainer = "\(Bundle.main.bundleIdentifier!).ViewingContainer"
}
}
61 changes: 61 additions & 0 deletions Harbour/Data/AppState/AppState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// AppState.swift
// Harbour
//
// Created by royal on 11/06/2021.
//

import Foundation
import Combine
import UIKit.UIDevice
import os.log
import Indicators

class AppState: ObservableObject {
public static let shared: AppState = AppState()

@Published public var fetchingMainScreenData: Bool = false

internal let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppState")

internal var autoRefreshTimer: AnyCancellable? = nil

private init() {
if Preferences.shared.endpointURL != nil && Preferences.shared.autoRefreshInterval > 0 {
setupAutoRefreshTimer()
}
}

// MARK: - Auto refresh

public func setupAutoRefreshTimer(interval: Double = Preferences.shared.autoRefreshInterval) {
logger.debug("(Auto refresh) Interval: \(interval)")

autoRefreshTimer?.cancel()

guard interval > 0 else { return }

autoRefreshTimer = Timer.publish(every: interval, on: .current, in: .common)
.autoconnect()
.receive(on: DispatchQueue.main)
.sink { _ in
Task { [weak self] in
self?.fetchingMainScreenData = true

do {
try await Portainer.shared.getContainers()
} catch {
self?.handle(error)
}

self?.fetchingMainScreenData = false
}
}
}

// MARK: - Error handling

private func handle(_ error: Error, _fileID: StaticString = #fileID, _line: Int = #line) {

}
}
20 changes: 14 additions & 6 deletions Harbour/Data/Portainer/Portainer+AttachedContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,28 @@
//

import Combine
import SwiftUI
import Foundation
import os.log
import PortainerKit
import Indicators

extension Portainer {
class AttachedContainer: ObservableObject {
class AttachedContainer: ObservableObject {
public let container: PortainerKit.Container
public let messagePassthroughSubject: PortainerKit.WebSocketPassthroughSubject

@Published public private(set) var attributedString: AttributedString = ""
@Published public private(set) var buffer: AttributedString = ""

public private(set) var isConnected: Bool = true
public var errorHandler: SceneState.ErrorHandler?

private let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Portainer.AttachedContainer")
private var messageCancellable: AnyCancellable? = nil

public init(container: PortainerKit.Container, messagePassthroughSubject: PortainerKit.WebSocketPassthroughSubject) {
logger.info("Attached to container with ID \(container.id)")

self.container = container
self.messagePassthroughSubject = messagePassthroughSubject

Expand All @@ -40,14 +45,16 @@ extension Portainer {
}

private func passthroughSubjectCompletion(_ completion: Subscribers.Completion<Error>) {
isConnected = false

switch completion {
case .finished:
let string = "Session ended."
update(string)
case .failure(let error):
let string = "Session ended, reason: \(String(describing: error))"
update(string)
AppState.shared.handle(error)
errorHandler?(error, nil, #fileID, #line)
}
}

Expand All @@ -65,17 +72,18 @@ extension Portainer {
logger.debug("\(string)")
}
case .failure(let error):
isConnected = false
update(String(describing: error))

let indicator: Indicators.Indicator = .init(id: "ContainerWebSocketDisconnected-\(container.id)", icon: "bolt.fill", headline: Localization.WEBSOCKET_DISCONNECTED_TITLE.localizedString, subheadline: error.localizedDescription, dismissType: .after(5))
AppState.shared.handle(error, indicator: indicator)
let indicator: Indicators.Indicator = .init(id: "ContainerWebSocketDisconnected-\(container.id)", icon: "bolt.fill", headline: Localization.WEBSOCKET_DISCONNECTED_TITLE.localized, subheadline: error.localizedDescription, dismissType: .after(5))
errorHandler?(error, indicator, #fileID, #line)
}
}

private func update(_ string: String) {
let attributedString: AttributedString = AttributedString(string)
DispatchQueue.main.async { [weak self] in
self?.attributedString.append(attributedString)
self?.buffer.append(attributedString)
}
}
}
Expand Down

0 comments on commit 0894279

Please sign in to comment.