diff --git a/Harbour.xcodeproj/project.pbxproj b/Harbour.xcodeproj/project.pbxproj index c3c251ea..a4edd90b 100644 --- a/Harbour.xcodeproj/project.pbxproj +++ b/Harbour.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ E70A5CEF27220C4300FF6672 /* EnvironmentValues+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70A5CEE27220C4300FF6672 /* EnvironmentValues+.swift */; }; E70A5CF127220D2900FF6672 /* ContainersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70A5CF027220D2900FF6672 /* ContainersView.swift */; }; - E70A5CF8272210DA00FF6672 /* ContainerCellBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70A5CF7272210DA00FF6672 /* ContainerCellBackground.swift */; }; E720D0202720B838004A21FD /* DisclosureSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E720D01F2720B838004A21FD /* DisclosureSection.swift */; }; E72112EA267F496B00D6004D /* LabeledSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E72112E9267F496B00D6004D /* LabeledSection.swift */; }; E7272E8A26736CCF00228494 /* PortainerKit+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7272E8926736CCF00228494 /* PortainerKit+.swift */; }; @@ -68,7 +67,7 @@ E788E1F627957DC300F24CAB /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7BD390F2792238100D3A7E6 /* SwiftUI.framework */; platformFilter = maccatalyst; }; E788E1F927957DC300F24CAB /* Widgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = E788E1F827957DC300F24CAB /* Widgets.swift */; }; E788E1FC27957DC500F24CAB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E788E1FB27957DC500F24CAB /* Assets.xcassets */; }; - E788E20227957DC500F24CAB /* HarbourWidgets.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E788E1F427957DC300F24CAB /* HarbourWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + E788E20227957DC500F24CAB /* HarbourWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E788E1F427957DC300F24CAB /* HarbourWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; E788E20727957E1300F24CAB /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = E79F4CAD27935DEF007FBAF6 /* Intents.intentdefinition */; }; E788E20A27957E8A00F24CAB /* ContainerStatusWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = E788E20927957E8A00F24CAB /* ContainerStatusWidget.swift */; }; E788E20C27957EBD00F24CAB /* ContainerStatusWidget+WidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E788E20B27957EBD00F24CAB /* ContainerStatusWidget+WidgetView.swift */; }; @@ -87,11 +86,8 @@ E7992A4426BF3BA3007335CA /* ToolbarTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7992A4326BF3BA3007335CA /* ToolbarTitle.swift */; }; E79F4CA127935DD1007FBAF6 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E79F4CA027935DD1007FBAF6 /* Intents.framework */; platformFilter = maccatalyst; }; E79F4CA427935DD1007FBAF6 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79F4CA327935DD1007FBAF6 /* IntentHandler.swift */; }; - E79F4CA827935DD1007FBAF6 /* HarbourIntents.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E79F4C9F27935DD1007FBAF6 /* HarbourIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + E79F4CA827935DD1007FBAF6 /* HarbourIntents.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E79F4C9F27935DD1007FBAF6 /* HarbourIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; E79F4CAE27935DEF007FBAF6 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = E79F4CAD27935DEF007FBAF6 /* Intents.intentdefinition */; }; - E79F6643279D74F500D15616 /* Error+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79F6642279D74F500D15616 /* Error+.swift */; }; - E79F6644279D74FC00D15616 /* Error+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79F6642279D74F500D15616 /* Error+.swift */; }; - E79F6645279D74FD00D15616 /* Error+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79F6642279D74F500D15616 /* Error+.swift */; }; E7ABFB952722BD2500054FB7 /* SceneState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7ABFB942722BD2500054FB7 /* SceneState.swift */; }; E7ABFD2B277CA6D300D475C0 /* UIApplication+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7ABFD2A277CA6D300D475C0 /* UIApplication+.swift */; }; E7B10B7026CD80C3005B82BC /* SettingsView+Components.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B10B6F26CD80C3005B82BC /* SettingsView+Components.swift */; }; @@ -120,6 +116,9 @@ E7D1BD6E2795B13700A4288C /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7BF9E8E2672F4C700AAB6A1 /* Constants.swift */; }; E7DC3BA82708FB8A00F32F8B /* TransparentButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7DC3BA72708FB8A00F32F8B /* TransparentButtonStyle.swift */; }; E7DD4CC6267650CB002709F0 /* ContainerConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7DD4CC5267650CB002709F0 /* ContainerConsoleView.swift */; }; + E7DF234B28256628000E2B76 /* GenericError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7DF234A28256628000E2B76 /* GenericError.swift */; }; + E7DF234C28256628000E2B76 /* GenericError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7DF234A28256628000E2B76 /* GenericError.swift */; }; + E7DF234D28256628000E2B76 /* GenericError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7DF234A28256628000E2B76 /* GenericError.swift */; }; E7EAE5CC270B6234008CFD20 /* CustomSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7EAE5CB270B6234008CFD20 /* CustomSection.swift */; }; E7FE3105278DF17500793471 /* SelectedRoundedRectangle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7FE3104278DF17500793471 /* SelectedRoundedRectangle.swift */; }; /* End PBXBuildFile section */ @@ -152,16 +151,16 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - E7BD391E2792238400D3A7E6 /* Embed App Extensions */ = { + E7BD391E2792238400D3A7E6 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( - E788E20227957DC500F24CAB /* HarbourWidgets.appex in Embed App Extensions */, - E79F4CA827935DD1007FBAF6 /* HarbourIntents.appex in Embed App Extensions */, + E788E20227957DC500F24CAB /* HarbourWidgets.appex in Embed Foundation Extensions */, + E79F4CA827935DD1007FBAF6 /* HarbourIntents.appex in Embed Foundation Extensions */, ); - name = "Embed App Extensions"; + name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ @@ -169,7 +168,6 @@ /* Begin PBXFileReference section */ E70A5CEE27220C4300FF6672 /* EnvironmentValues+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+.swift"; sourceTree = ""; }; E70A5CF027220D2900FF6672 /* ContainersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainersView.swift; sourceTree = ""; }; - E70A5CF7272210DA00FF6672 /* ContainerCellBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerCellBackground.swift; sourceTree = ""; }; E720D01F2720B838004A21FD /* DisclosureSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisclosureSection.swift; sourceTree = ""; }; E72112E9267F496B00D6004D /* LabeledSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabeledSection.swift; sourceTree = ""; }; E7272E8926736CCF00228494 /* PortainerKit+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PortainerKit+.swift"; sourceTree = ""; }; @@ -222,7 +220,6 @@ E79F4CA527935DD1007FBAF6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E79F4CA927935DD1007FBAF6 /* Intents.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Intents.entitlements; sourceTree = ""; }; E79F4CAD27935DEF007FBAF6 /* Intents.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = Intents.intentdefinition; sourceTree = ""; }; - E79F6642279D74F500D15616 /* Error+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+.swift"; sourceTree = ""; }; E7ABFB942722BD2500054FB7 /* SceneState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneState.swift; sourceTree = ""; }; E7ABFD2A277CA6D300D475C0 /* UIApplication+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+.swift"; sourceTree = ""; }; E7B10B6F26CD80C3005B82BC /* SettingsView+Components.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsView+Components.swift"; sourceTree = ""; }; @@ -248,6 +245,7 @@ E7CECDD027F34CB6008AA6AB /* UserActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivity.swift; sourceTree = ""; }; E7DC3BA72708FB8A00F32F8B /* TransparentButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentButtonStyle.swift; sourceTree = ""; }; E7DD4CC5267650CB002709F0 /* ContainerConsoleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerConsoleView.swift; sourceTree = ""; }; + E7DF234A28256628000E2B76 /* GenericError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericError.swift; sourceTree = ""; }; E7EAE5CB270B6234008CFD20 /* CustomSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSection.swift; sourceTree = ""; }; E7FE3104278DF17500793471 /* SelectedRoundedRectangle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedRoundedRectangle.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -443,6 +441,7 @@ isa = PBXGroup; children = ( E7BF9E8E2672F4C700AAB6A1 /* Constants.swift */, + E7DF234A28256628000E2B76 /* GenericError.swift */, E788E21D2795A74C00F24CAB /* HarbourURLScheme.swift */, E7CECDD027F34CB6008AA6AB /* UserActivity.swift */, E74C8FEC27B29E4000CC1DF3 /* CoreData */, @@ -458,7 +457,6 @@ children = ( E751F062270B37DA00980DCA /* Array+.swift */, E75A51E12673A1C100857D2B /* Bundle+.swift */, - E79F6642279D74F500D15616 /* Error+.swift */, E7BD393C27922DB800D3A7E6 /* Portainer+.swift */, E7272E8926736CCF00228494 /* PortainerKit+.swift */, E7339C572674262500A55B5C /* String+.swift */, @@ -569,7 +567,6 @@ E7EAE5CB270B6234008CFD20 /* CustomSection.swift */, E75A51E32673A4F000857D2B /* NavigationLinkLabel.swift */, E7339C552674236700A55B5C /* ContainerContextMenu.swift */, - E70A5CF7272210DA00FF6672 /* ContainerCellBackground.swift */, E7992A4326BF3BA3007335CA /* ToolbarTitle.swift */, E720D01F2720B838004A21FD /* DisclosureSection.swift */, ); @@ -640,7 +637,7 @@ E7BF9E542672B5B100AAB6A1 /* Frameworks */, E7BF9E552672B5B100AAB6A1 /* Resources */, E75A51D3267380AE00857D2B /* Embed Frameworks */, - E7BD391E2792238400D3A7E6 /* Embed App Extensions */, + E7BD391E2792238400D3A7E6 /* Embed Foundation Extensions */, ); buildRules = ( ); @@ -666,7 +663,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1320; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1400; TargetAttributes = { E788E1F327957DC300F24CAB = { CreatedOnToolsVersion = 13.2.1; @@ -801,6 +798,7 @@ E7BF1FD027B29F95009AB829 /* Persistence.swift in Sources */, E788E20C27957EBD00F24CAB /* ContainerStatusWidget+WidgetView.swift in Sources */, E7D1BD6E2795B13700A4288C /* Constants.swift in Sources */, + E7DF234D28256628000E2B76 /* GenericError.swift in Sources */, E74AA0D327B1C0FC00B58F17 /* Localization.swift in Sources */, E788E20727957E1300F24CAB /* Intents.intentdefinition in Sources */, E74C8FF127B29E6A00CC1DF3 /* Harbour.xcdatamodeld in Sources */, @@ -811,7 +809,6 @@ E77610CD279DEAA60034BFFB /* Portainer+.swift in Sources */, E788E20D27957FA300F24CAB /* View+Shared.swift in Sources */, E74598FC27AE857600688066 /* Portainer.swift in Sources */, - E79F6645279D74FD00D15616 /* Error+.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -826,6 +823,7 @@ E7BF1FCF27B29F95009AB829 /* Persistence.swift in Sources */, E7D1BD6D2795B13600A4288C /* Constants.swift in Sources */, E77717C227935F8E00DED48D /* String+.swift in Sources */, + E7DF234C28256628000E2B76 /* GenericError.swift in Sources */, E77610CB279DEAA60034BFFB /* Portainer+.swift in Sources */, E77717C027935F8E00DED48D /* Array+.swift in Sources */, E74598FB27AE857600688066 /* Portainer.swift in Sources */, @@ -834,7 +832,6 @@ E788E2202795A75000F24CAB /* HarbourURLScheme.swift in Sources */, E74C8FF027B29E6A00CC1DF3 /* Harbour.xcdatamodeld in Sources */, E74AA0D227B1C0FB00B58F17 /* Localization.swift in Sources */, - E79F6644279D74FC00D15616 /* Error+.swift in Sources */, E77717CD2793794A00DED48D /* ExecuteActionIntentHandler.swift in Sources */, E77717BD27935F4E00DED48D /* ContainerStatusIntentHandler.swift in Sources */, ); @@ -884,6 +881,7 @@ E77C5FB3267E9B99000B4994 /* DebugView.swift in Sources */, E70A5CF127220D2900FF6672 /* ContainersView.swift in Sources */, E77C5FB5267E9D1A000B4994 /* SetupView.swift in Sources */, + E7DF234B28256628000E2B76 /* GenericError.swift in Sources */, E7B10B7426CD82A2005B82BC /* SettingsView+PortainerSection.swift in Sources */, E751F05B270B315200980DCA /* ContainersListView+ContainerCell.swift in Sources */, E7B10B7626CD82D5005B82BC /* SettingsView+InterfaceSection.swift in Sources */, @@ -895,13 +893,11 @@ E75A51EC2673A7F300857D2B /* ContainerConfigDetailsView.swift in Sources */, E75A51EE2673B17B00857D2B /* Labeled.swift in Sources */, E75A51E82673A78300857D2B /* ContainerNetworkDetailsView.swift in Sources */, - E70A5CF8272210DA00FF6672 /* ContainerCellBackground.swift in Sources */, E75A51D8267384E100857D2B /* AppState.swift in Sources */, E7CECDD127F34CB6008AA6AB /* UserActivity.swift in Sources */, E7BF9E5E2672B5B100AAB6A1 /* HarbourApp.swift in Sources */, E72112EA267F496B00D6004D /* LabeledSection.swift in Sources */, E7BF9E8F2672F4C700AAB6A1 /* Constants.swift in Sources */, - E79F6643279D74F500D15616 /* Error+.swift in Sources */, E751F05D270B315B00980DCA /* ContainersGridView.swift in Sources */, E75A51E026739ED100857D2B /* Preferences.swift in Sources */, E75A51DE26738A2000857D2B /* ContainerDetailView.swift in Sources */, @@ -943,7 +939,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = Widgets/WidgetsExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_TEAM = WPN9Y7CDCT; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Widgets/Info.plist; @@ -976,7 +972,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = Widgets/WidgetsExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_TEAM = WPN9Y7CDCT; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Widgets/Info.plist; @@ -1008,7 +1004,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = Intents/Intents.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_TEAM = WPN9Y7CDCT; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Intents/Info.plist; @@ -1041,7 +1037,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = Intents/Intents.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_TEAM = WPN9Y7CDCT; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Intents/Info.plist; @@ -1104,6 +1100,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -1164,6 +1161,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -1191,7 +1189,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = Harbour/Harbour.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_TEAM = WPN9Y7CDCT; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; @@ -1233,7 +1231,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = Harbour/Harbour.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_TEAM = WPN9Y7CDCT; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; diff --git a/Harbour.xcodeproj/xcshareddata/xcschemes/Harbour.xcscheme b/Harbour.xcodeproj/xcshareddata/xcschemes/Harbour.xcscheme index 6df1d0c1..be2eca4e 100644 --- a/Harbour.xcodeproj/xcshareddata/xcschemes/Harbour.xcscheme +++ b/Harbour.xcodeproj/xcshareddata/xcschemes/Harbour.xcscheme @@ -1,6 +1,6 @@ Bool { - lhs.container == rhs.container + lhs.container.id == rhs.container.id } } fileprivate extension ContainerDetailView { struct GeneralSection: View { + let container: PortainerKit.Container + let details: PortainerKit.ContainerDetails + + var body: some View { + LabeledSection(label: "ID", content: details.id, monospace: true, hideIfEmpty: false) + LabeledSection(label: "Created", content: details.created.formatted(), hideIfEmpty: false) + LabeledSection(label: "PID", content: "\(details.state.pid)", monospace: true, hideIfEmpty: false) + LabeledSection(label: "Status", content: container.status ?? details.state.status.rawValue, monospace: true, hideIfEmpty: false) + LabeledSection(label: "Error", content: details.state.error, monospace: true, hideIfEmpty: false) + LabeledSection(label: "Started at", content: details.state.startedAt?.formatted(), hideIfEmpty: false) + LabeledSection(label: "Finished at", content: details.state.finishedAt?.formatted(), hideIfEmpty: false) + } + } + + struct LogsSection: View { + let logs: String + let tailCount: Int + + var body: some View { + CustomSection(label: "Logs (last \(tailCount) lines)") { + Text(logs.isReallyEmpty ? "empty" : logs) + .font(.system(.footnote, design: .monospaced)) + .foregroundStyle(logs.isReallyEmpty ? .secondary : .primary) + .lineLimit(nil) + .contentShape(Rectangle()) + .frame(maxWidth: .infinity, alignment: .topLeading) + .textSelection(.enabled) + } + .frame(maxWidth: .infinity) + } + } + + struct DetailsSection: View { let details: PortainerKit.ContainerDetails var body: some View { diff --git a/Harbour/Views/Containers List/ContainersView.swift b/Harbour/Views/Containers List/ContainersView.swift index 0ae5f18e..677d090b 100644 --- a/Harbour/Views/Containers List/ContainersView.swift +++ b/Harbour/Views/Containers List/ContainersView.swift @@ -17,10 +17,10 @@ struct ContainersView: View { var body: some View { if useContainerGridView { ContainersGridView(containers: containers) -// .equatable() + .equatable() } else { ContainersListView(containers: containers) -// .equatable() + .equatable() } } } diff --git a/Harbour/Views/Containers List/Grid/ContainersGridView+ContainerCell.swift b/Harbour/Views/Containers List/Grid/ContainersGridView+ContainerCell.swift index ffc3e545..73882017 100644 --- a/Harbour/Views/Containers List/Grid/ContainersGridView+ContainerCell.swift +++ b/Harbour/Views/Containers List/Grid/ContainersGridView+ContainerCell.swift @@ -56,7 +56,7 @@ extension ContainersGridView { } .padding(.medium) .aspectRatio(1, contentMode: .fill) - .background(ContainerCellBackground(state: container.state)) + .background(Color(uiColor: .systemBackground)) .containerShape(Self.backgroundShape) .animation(.easeInOut, value: container.status) .animation(.easeInOut, value: container.displayName) diff --git a/Harbour/Views/Containers List/List/ContainersListView+ContainerCell.swift b/Harbour/Views/Containers List/List/ContainersListView+ContainerCell.swift index 58fc46ab..de8077e6 100644 --- a/Harbour/Views/Containers List/List/ContainersListView+ContainerCell.swift +++ b/Harbour/Views/Containers List/List/ContainersListView+ContainerCell.swift @@ -61,7 +61,7 @@ extension ContainersListView { .animation(.easeInOut, value: container.state.color) } .padding() - .background(ContainerCellBackground(state: container.state)) + .background(Color(uiColor: .systemBackground)) .containerShape(Self.backgroundShape) .animation(.easeInOut, value: container.state) .animation(.easeInOut, value: container.status) diff --git a/Harbour/Views/ContentView.swift b/Harbour/Views/ContentView.swift index 6c83369b..46654784 100644 --- a/Harbour/Views/ContentView.swift +++ b/Harbour/Views/ContentView.swift @@ -64,6 +64,10 @@ struct ContentView: View { } .disabled(!portainer.isSetup) } + + var background: some View { + Color(uiColor: .systemGroupedBackground) + } @ViewBuilder var content: some View { @@ -86,6 +90,8 @@ struct ContentView: View { var body: some View { NavigationView { content + .maxSize() + .background(background.ignoresSafeArea()) .transition(.opacity) .navigationTitle("Harbour") .navigationBarTitleDisplayMode(.inline) diff --git a/Harbour/Views/DebugView.swift b/Harbour/Views/DebugView.swift index 72afdb80..6c9725a5 100644 --- a/Harbour/Views/DebugView.swift +++ b/Harbour/Views/DebugView.swift @@ -12,6 +12,7 @@ import OSLog import Indicators import BackgroundTasks import WidgetKit +import PortainerKit struct DebugView: View { @EnvironmentObject var sceneState: SceneState @@ -33,6 +34,10 @@ struct DebugView: View { Preferences.shared.selectedEndpointID = nil Portainer.shared.cleanup() } + + Button("\(Preferences.shared.enableDebugLogging ? "Disable" : "Enable") debug logging") { + Preferences.shared.enableDebugLogging.toggle() + } } #if DEBUG @@ -65,7 +70,7 @@ struct DebugView: View { Button("Reset all") { UIDevice.generateHaptic(.heavy) - Preferences.Key.allCases.forEach { Preferences.ud.removeObject(forKey: $0.rawValue) } + Preferences.Keys.allCases.forEach { Preferences.ud.removeObject(forKey: $0.rawValue) } exit(0) } .foregroundStyle(.red) @@ -161,7 +166,7 @@ extension DebugView { .compactMap { $0 as? OSLogEntryLog } .map { LogEntry(message: $0.composedMessage, level: $0.level, date: $0.date, category: $0.category) } } catch { - logs = [LogEntry(message: error.readableDescription, level: nil, date: nil, category: nil)] + logs = [LogEntry(message: error.localizedDescription, level: nil, date: nil, category: nil)] } } } diff --git a/Harbour/Views/LoginView.swift b/Harbour/Views/LoginView.swift index 76e6deb0..e6cf67c3 100644 --- a/Harbour/Views/LoginView.swift +++ b/Harbour/Views/LoginView.swift @@ -9,7 +9,7 @@ import SwiftUI import PortainerKit struct LoginView: View { - @Environment(\.presentationMode) var presentationMode + @Environment(\.dismiss) var dismiss @EnvironmentObject var sceneState: SceneState @EnvironmentObject var portainer: Portainer @@ -111,34 +111,24 @@ struct LoginView: View { } @Sendable - func login() { - let url: URL? = { - guard var components = URLComponents(string: self.url) else { return nil } - components.path = components.path.split(separator: "/").joined(separator: "/") // #HB-8 - return components.url - }() - - guard let url = url else { - UIDevice.generateHaptic(.error) - buttonLabel = "Invalid URL" - buttonColor = .red - return - } - + private func login() { loginTask?.cancel() loginTask = Task { do { + guard let url = URL(string: url) else { + throw GenericError.invalidURL + } + try await portainer.setup(url: url, token: token) UIDevice.generateHaptic(.success) buttonColor = .green buttonLabel = "Success!" - presentationMode.wrappedValue.dismiss() + dismiss() Task { do { - try await portainer.getEndpoints() if portainer.selectedEndpointID != nil { try await portainer.getContainers() } @@ -153,7 +143,7 @@ struct LoginView: View { loginTask?.cancel() buttonColor = .red - buttonLabel = error.readableDescription + buttonLabel = error.localizedDescription errorTimer?.invalidate() errorTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in diff --git a/Harbour/Views/Settings/SettingsView+PortainerSection.swift b/Harbour/Views/Settings/SettingsView+PortainerSection.swift index 8a0b1ec6..8fafb24d 100644 --- a/Harbour/Views/Settings/SettingsView+PortainerSection.swift +++ b/Harbour/Views/Settings/SettingsView+PortainerSection.swift @@ -59,7 +59,7 @@ extension SettingsView { Button(role: .destructive, action: { UIDevice.generateHaptic(.heavy) do { - try portainer.logout(from: server) + try portainer.deleteServer(url: server) } catch { sceneState.handle(error) } diff --git a/Harbour/Views/Settings/SettingsView.swift b/Harbour/Views/Settings/SettingsView.swift index aac754a7..6db01f4e 100644 --- a/Harbour/Views/Settings/SettingsView.swift +++ b/Harbour/Views/Settings/SettingsView.swift @@ -11,7 +11,7 @@ struct SettingsView: View { @EnvironmentObject var portainer: Portainer @EnvironmentObject var preferences: Preferences @Environment(\.presentationMode) var presentationMode - + let listRowInsets = EdgeInsets(top: 10, leading: 15, bottom: 10, trailing: 15) var body: some View { diff --git a/Intents/Handlers/ContainerStatusIntentHandler.swift b/Intents/Handlers/ContainerStatusIntentHandler.swift index 7efc37bf..f5d10e57 100644 --- a/Intents/Handlers/ContainerStatusIntentHandler.swift +++ b/Intents/Handlers/ContainerStatusIntentHandler.swift @@ -67,7 +67,7 @@ final class ContainerStatusIntentHandler: NSObject, ContainerStatusIntentHandlin return .failure(error: "Container not found") } } catch { - return .failure(error: error.readableDescription) + return .failure(error: error.localizedDescription) } } } diff --git a/Intents/Handlers/ExecuteActionIntentHandler.swift b/Intents/Handlers/ExecuteActionIntentHandler.swift index 5b2a532a..e2ec2f45 100644 --- a/Intents/Handlers/ExecuteActionIntentHandler.swift +++ b/Intents/Handlers/ExecuteActionIntentHandler.swift @@ -63,7 +63,7 @@ final class ExecuteActionIntentHandler: NSObject, ExecuteActionIntentHandling { try await portainer.execute(action, on: containerID) return .success(newStatus: action.expectedState.asContainerStatus) } catch { - return .failure(error: error.readableDescription) + return .failure(error: error.localizedDescription) } } } diff --git a/Modules/Indicators/.swiftpm/xcode/xcshareddata/xcschemes/Indicators.xcscheme b/Modules/Indicators/.swiftpm/xcode/xcshareddata/xcschemes/Indicators.xcscheme index 5f89f3e2..1e726f8d 100644 --- a/Modules/Indicators/.swiftpm/xcode/xcshareddata/xcschemes/Indicators.xcscheme +++ b/Modules/Indicators/.swiftpm/xcode/xcshareddata/xcschemes/Indicators.xcscheme @@ -1,6 +1,6 @@ + + internal static let tokenItemDescription = "Harbour - Token" let service: String let accessGroup: String? let synchronizable: Bool = true private let baseQuery: QueryDictionary - private let textEncoding: String.Encoding = .utf8 + + private var tokenQuery: QueryDictionary { + var query = baseQuery + query[kSecClass] = kSecClassInternetPassword + return query + } // MARK: - init @@ -31,7 +38,8 @@ public final class Keychain { self.baseQuery = [ kSecAttrAccessGroup: accessGroup, - kSecAttrSynchronizable: synchronizable + kSecAttrSynchronizable: synchronizable, + kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock ] } @@ -43,7 +51,8 @@ public final class Keychain { /// - token: Account token /// - comment: Item comment public func saveToken(server: URL, token: String, comment: String? = nil) throws { - let query = tokenQuery(for: server) + var query = tokenQuery + query[kSecAttrServer] = server.host // query[kSecAttrAccount] = server.absoluteString guard let tokenData = token.data(using: self.textEncoding) else { @@ -52,8 +61,9 @@ public final class Keychain { let attributes: QueryDictionary = [ kSecValueData: tokenData, kSecAttrComment: comment as Any, + kSecAttrPath: server.path, kSecAttrLabel: server.absoluteString, - kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock + kSecAttrDescription: Self.tokenItemDescription ] try addOrUpdate(query: query, attributes: attributes) } @@ -62,7 +72,8 @@ public final class Keychain { /// - Parameter server: Service URL /// - Returns: Token public func getToken(server: URL) throws -> String { - var query = tokenQuery(for: server) + var query = tokenQuery + query[kSecAttrServer] = server.host query[kSecReturnData] = true var item: CFTypeRef? @@ -82,7 +93,8 @@ public final class Keychain { /// Deletes token for supplied URL /// - Parameter server: Service URL public func removeToken(server: URL) throws { - let query = tokenQuery(for: server) + var query = tokenQuery + query[kSecAttrServer] = server.host let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw SecError(status) } } @@ -116,20 +128,6 @@ public final class Keychain { // MARK: - Helpers - internal static let tokenItemDescription = "Harbour - Token" - - /// Creates token query for supplied URL - /// - Parameter server: Service URL - /// - Returns: SecItem dictionary - private func tokenQuery(for server: URL) -> QueryDictionary { - var query = baseQuery - query[kSecClass] = kSecClassInternetPassword - query[kSecAttrDescription] = Self.tokenItemDescription - query[kSecAttrServer] = server.absoluteString - - return query - } - /// Adds or updates item with supplied query and attributes, /// - Parameters: /// - query: Item query diff --git a/Modules/PortainerKit/.swiftpm/xcode/xcshareddata/xcschemes/PortainerKit.xcscheme b/Modules/PortainerKit/.swiftpm/xcode/xcshareddata/xcschemes/PortainerKit.xcscheme index b0730be2..5b5a3211 100644 --- a/Modules/PortainerKit/.swiftpm/xcode/xcshareddata/xcschemes/PortainerKit.xcscheme +++ b/Modules/PortainerKit/.swiftpm/xcode/xcshareddata/xcschemes/PortainerKit.xcscheme @@ -1,6 +1,6 @@ , Error> + + public static let userDefaultsLoggingKey = "EnableDebugLogging" // MARK: Public properties @@ -19,13 +25,15 @@ public final class PortainerKit { /// Used `URLSession` public let session: URLSession - - // MARK: Public variables - + /// Authorization token public var token: String? + + // MARK: Private properties + + private let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "xyz.shameful.PortainerKit", category: "PortainerKit") - // MARK: - init + // MARK: init /// Initializes PortainerKit with endpoint URL and optional authorization token. /// - Parameters: @@ -45,32 +53,6 @@ public final class PortainerKit { // MARK: - Public functions - /// Logs in to Portainer. - /// - Parameters: - /// - username: Username - /// - password: Password - /// - Returns: JWT token - public func login(username: String, password: String) async throws -> String { - var request = try request(for: .login) - request.httpMethod = "POST" - - let body = [ - "Username": username, - "Password": password - ] - request.httpBody = try JSONEncoder().encode(body) - - let (data, _) = try await session.data(for: request) - let decoded = try JSONDecoder().decode([String: String].self, from: data) - - if let jwt: String = decoded["jwt"] { - token = jwt - return jwt - } else { - throw APIError.fromMessage(decoded[APIError.errorMessageKey]) - } - } - /// Fetches available endpoints. /// - Returns: `[Endpoint]` public func fetchEndpoints() async throws -> [Endpoint] { @@ -253,6 +235,16 @@ public final class PortainerKit { /// - Returns: Output private func fetch(request: URLRequest, decoder: JSONDecoder = JSONDecoder()) async throws -> Output { let response = try await session.data(for: request) + + if UserDefaults.standard.bool(forKey: Self.userDefaultsLoggingKey) { + logger.warning("Logging is enabled! All of the data for this request will be logged to the console. To disable it, set \(Self.userDefaultsLoggingKey) to false.") + let obj: [String: Any] = [ + "data": response.0.base64EncodedString() + ] + let json = try? JSONSerialization.data(withJSONObject: obj) + let str = json?.base64EncodedString() ?? "" + logger.debug("\(request.url?.absoluteString ?? ""): \(str)") + } do { let decoded = try decoder.decode(Output.self, from: response.0) diff --git a/Modules/PortainerKit/Sources/PortainerKit/Types/Common.swift b/Modules/PortainerKit/Sources/PortainerKit/Types/Common.swift index 2249051a..ad277a64 100644 --- a/Modules/PortainerKit/Sources/PortainerKit/Types/Common.swift +++ b/Modules/PortainerKit/Sources/PortainerKit/Types/Common.swift @@ -35,9 +35,10 @@ public extension PortainerKit { enum EndpointType: Int, Decodable, Sendable, Hashable { case docker = 1 - case agent - case azure - case edgeAgent + case agent = 2 + case azure = 3 + case edgeAgent = 4 + case edgeAgentK8s = 7 } enum ExecuteAction: String, Sendable, Hashable { @@ -171,7 +172,7 @@ public extension PortainerKit { public let shell: [String]? } - struct ContainerState: Decodable, Sendable { + struct ContainerState: Decodable { enum CodingKeys: String, CodingKey { case status = "Status" case running = "Running" diff --git a/README.md b/README.md index f10a789e..4913bff2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ Harbour App icon + # Harbour Docker/Portainer management app for iOS & iPadOS built with SwiftUI. diff --git a/Shared/Controllers/Portainer.swift b/Shared/Controllers/Portainer.swift index 7c498e4d..5146c643 100644 --- a/Shared/Controllers/Portainer.swift +++ b/Shared/Controllers/Portainer.swift @@ -14,6 +14,7 @@ import Keychain import PortainerKit @MainActor +/// Main Portainer-related data store final class Portainer: ObservableObject { public typealias ContainerInspection = (general: PortainerKit.Container?, details: PortainerKit.ContainerDetails) @@ -83,9 +84,10 @@ final class Portainer: ObservableObject { /// - Parameters: /// - url: Server URL /// - token: Access token - @Sendable @MainActor public func setup(url: URL? = Preferences.shared.selectedServer, token: String? = nil) async throws { + @Sendable @MainActor + public func setup(url: URL? = Preferences.shared.selectedServer, token: String? = nil) async throws { do { - guard let url = url else { throw PortainerError.noServerURL } + guard let url = url else { throw PortainerError.noServerURL(nil) } guard url != serverURL else { return } guard let token = try? (token ?? keychain.getToken(server: url)) else { throw PortainerError.noToken } @@ -95,12 +97,16 @@ final class Portainer: ObservableObject { defer { isSettingUp = false } let api = PortainerKit(url: url, token: token) + let endpoints = try await api.fetchEndpoints() - try keychain.saveToken(server: url, token: token, comment: Localization.Keychain.tokenComment(Bundle.main.mainBundleIdentifier)) self.api = api + self.endpoints = endpoints isSetup = true + try? keychain.saveToken(server: url, token: token, comment: Localization.Keychain.tokenComment(Bundle.main.mainBundleIdentifier)) Preferences.shared.selectedServer = url + + servers = (try? keychain.getURLs()) ?? [] } catch { handle(error) throw error @@ -109,8 +115,9 @@ final class Portainer: ObservableObject { /// Removes credentials for supplied server URL /// - Parameter url: URL to remove credentials for - @Sendable @MainActor public func logout(from url: URL) throws { - logger.info("Logging out from \"\(url.absoluteString, privacy: .sensitive(mask: .hash))\" ") + @Sendable @MainActor + public func deleteServer(url: URL) throws { + logger.info("Deleting server with URL: \"\(url.absoluteString, privacy: .sensitive(mask: .hash))\" ") try keychain.removeToken(server: url) servers.remove(url) @@ -122,10 +129,13 @@ final class Portainer: ObservableObject { if serverURL == url { cleanup() } + + servers = (try? keychain.getURLs()) ?? [] } /// Cleans up local data (used after logging out) - @Sendable @MainActor public func cleanup() { + @Sendable @MainActor + public func cleanup() { logger.info("Cleaning up!") api = nil @@ -144,7 +154,8 @@ final class Portainer: ObservableObject { /// Sets selectedEndpointID and fetches containers /// - Parameter endpointID: Endpoint ID - @Sendable @MainActor public func setSelectedEndpoint(_ endpointID: PortainerKit.Endpoint.ID?) async throws { + @Sendable @MainActor + public func setSelectedEndpoint(_ endpointID: PortainerKit.Endpoint.ID?) async throws { logger.info("Selected endpoint with ID \(endpointID?.description ?? "")") selectedEndpointID = endpointID @@ -163,8 +174,8 @@ final class Portainer: ObservableObject { /// Fetches available endpoints. /// - Returns: `[PortainerKit.Endpoint]` - @discardableResult - @Sendable @MainActor public func getEndpoints() async throws -> [PortainerKit.Endpoint] { + @discardableResult @Sendable @MainActor + public func getEndpoints() async throws -> [PortainerKit.Endpoint] { do { guard let api = api else { throw PortainerError.noAPI } @@ -191,8 +202,8 @@ final class Portainer: ObservableObject { /// - endpointID: Endpoint ID to search /// - containerID: Search for container with this ID /// - Returns: `[PortainerKit.Container]` - @discardableResult - @Sendable @MainActor public func getContainers(endpointID: Int? = nil, containerID: PortainerKit.Container.ID? = nil) async throws -> [PortainerKit.Container] { + @discardableResult @Sendable @MainActor + public func getContainers(endpointID: Int? = nil, containerID: PortainerKit.Container.ID? = nil) async throws -> [PortainerKit.Container] { do { guard let api = api else { throw PortainerError.noAPI } guard let endpointID = endpointID ?? self.selectedEndpointID else { throw PortainerError.noEndpoint } @@ -218,9 +229,9 @@ final class Portainer: ObservableObject { isLoggedIn = true self.containers = containers - #if IOS +// #if IOS // Task { try? storeContainers(containers: containers) } - #endif +// #endif return containers } catch { @@ -233,7 +244,8 @@ final class Portainer: ObservableObject { /// - Parameter container: Container to be inspected /// - Parameter endpointID: Endpoint ID to inspect /// - Returns: `ContainerInspection` - @Sendable @MainActor public func inspectContainer(_ container: PortainerKit.Container, endpointID: Int? = nil) async throws -> ContainerInspection { + @Sendable @MainActor + public func inspectContainer(_ container: PortainerKit.Container, endpointID: Int? = nil) async throws -> ContainerInspection { do { guard let api = api else { throw PortainerError.noAPI } guard let endpointID = endpointID ?? self.selectedEndpointID else { throw PortainerError.noEndpoint } @@ -262,7 +274,8 @@ final class Portainer: ObservableObject { /// - Parameters: /// - action: Action to be executed /// - container: Container, where the action will be executed - @Sendable public func execute(_ action: PortainerKit.ExecuteAction, on containerID: PortainerKit.Container.ID, endpointID: Int? = nil) async throws { + @Sendable + public func execute(_ action: PortainerKit.ExecuteAction, on containerID: PortainerKit.Container.ID, endpointID: Int? = nil) async throws { do { guard let api = api else { throw PortainerError.noAPI } guard let endpointID = endpointID ?? self.selectedEndpointID else { throw PortainerError.noEndpoint } @@ -284,7 +297,8 @@ final class Portainer: ObservableObject { /// - tail: Number of lines /// - displayTimestamps: Display timestamps? /// - Returns: `String` logs - @Sendable public func getLogs(from containerID: PortainerKit.Container.ID, endpointID: Int? = nil, since: TimeInterval = 0, tail: Int = 100, displayTimestamps: Bool = false) async throws -> String { + @Sendable + public func getLogs(from containerID: PortainerKit.Container.ID, endpointID: Int? = nil, since: TimeInterval = 0, tail: Int = 100, displayTimestamps: Bool = false) async throws -> String { do { guard let api = api else { throw PortainerError.noAPI } guard let endpointID = endpointID ?? self.selectedEndpointID else { throw PortainerError.noEndpoint } @@ -357,7 +371,8 @@ final class Portainer: ObservableObject { } /// Loads stored containers - @MainActor private func loadStoredContainers() throws { + @MainActor + private func loadStoredContainers() throws { logger.info("(Persistence) Fetching stored containers...") let context = Persistence.shared.backgroundContext @@ -386,26 +401,31 @@ final class Portainer: ObservableObject { private func handle(_ error: Error, _function: StaticString = #function, _fileID: StaticString = #fileID, _line: Int = #line) { logger.error("\(String(describing: error)) (\(_function) [\(_fileID):\(_line)])") - // PortainerKit - if let error = error as? PortainerKit.APIError { - switch error { - case .invalidToken: - cleanup() - default: - break + switch error { + case let error as Portainer.PortainerError: do { + switch error { + case .noServerURL(let url): + if let url = url { + try? keychain.removeToken(server: url) + } + if Preferences.shared.selectedServer == url { + Preferences.shared.selectedServer = nil + } + break + default: + break + } } - } else if let error = error as? URLError { - switch error.code { - case .cannotConnectToHost, .cannotFindHost, .dnsLookupFailed, .networkConnectionLost, .notConnectedToInternet, .timedOut: - endpoints = [] - containers = [] - isLoggedIn = false - #if IOS - attachedContainer = nil - #endif - default: - break + case let error as PortainerKit.APIError: do { + switch error { + case .invalidToken: + cleanup() + default: + break + } } + default: + break } } } diff --git a/Shared/Controllers/Preferences.swift b/Shared/Controllers/Preferences.swift index 647eb7cd..abec4085 100644 --- a/Shared/Controllers/Preferences.swift +++ b/Shared/Controllers/Preferences.swift @@ -5,39 +5,35 @@ // Created by royal on 11/06/2021. // -import Foundation import SwiftUI final class Preferences: ObservableObject { public static let shared: Preferences = Preferences() - @AppStorage(Key.finishedSetup.rawValue, store: .standard) public var finishedSetup: Bool = false - - @AppStorage(Key.selectedServer.rawValue, store: .group) public var selectedServer: URL? - @AppStorage(Key.selectedEndpointID.rawValue, store: .group) public var selectedEndpointID: Int? + @AppStorage(Keys.finishedSetup.rawValue, store: .standard) public var finishedSetup: Bool = false + @AppStorage(Keys.autoRefreshInterval.rawValue, store: .standard) public var autoRefreshInterval: Double = 0 + @AppStorage(Keys.enableHaptics.rawValue, store: .standard) public var enableHaptics: Bool = true + @AppStorage(Keys.persistAttachedContainer.rawValue, store: .standard) public var persistAttachedContainer: Bool = true + @AppStorage(Keys.enableDebugLogging.rawValue, store: .standard) public var enableDebugLogging: Bool = false + @AppStorage(Keys.clUseGridView.rawValue, store: .standard) public var clUseGridView: Bool = false + @AppStorage(Keys.clUseColumns.rawValue, store: .standard) public var clUseColumns: Bool = true + + @AppStorage(Keys.selectedServer.rawValue, store: .group) public var selectedServer: URL? + @AppStorage(Keys.selectedEndpointID.rawValue, store: .group) public var selectedEndpointID: Int? + @AppStorage(Keys.enableBackgroundRefresh.rawValue, store: .group) public var enableBackgroundRefresh: Bool = false + @AppStorage(Keys.clUseColoredContainerCells.rawValue, store: .group) public var clUseColoredContainerCells: Bool = false - @AppStorage(Key.enableBackgroundRefresh.rawValue, store: .group) public var enableBackgroundRefresh: Bool = false - @AppStorage(Key.autoRefreshInterval.rawValue, store: .standard) public var autoRefreshInterval: Double = 0 - - @AppStorage(Key.enableHaptics.rawValue, store: .standard) public var enableHaptics: Bool = true - - @AppStorage(Key.clUseGridView.rawValue, store: .standard) public var clUseGridView: Bool = false - @AppStorage(Key.clUseColumns.rawValue, store: .standard) public var clUseColumns: Bool = true - @AppStorage(Key.clUseColoredContainerCells.rawValue, store: .group) public var clUseColoredContainerCells: Bool = false - - @AppStorage(Key.persistAttachedContainer.rawValue, store: .standard) public var persistAttachedContainer: Bool = true - #if DEBUG var lastBackgroundTaskDate: Date? { get { - let time = Self.ud.double(forKey: Key.lastBackgroundTaskDate.rawValue) + let time = Self.ud.double(forKey: Keys.lastBackgroundTaskDate.rawValue) if time > 0 { return Date(timeIntervalSinceReferenceDate: time) } else { return nil } } - set { Self.ud.set(newValue?.timeIntervalSinceReferenceDate, forKey: Key.lastBackgroundTaskDate.rawValue) } + set { Self.ud.set(newValue?.timeIntervalSinceReferenceDate, forKey: Keys.lastBackgroundTaskDate.rawValue) } } #endif @@ -47,7 +43,7 @@ final class Preferences: ObservableObject { } extension Preferences { - enum Key: String, CaseIterable { + enum Keys: String, CaseIterable { case finishedSetup = "FinishedSetup" case selectedServer = "SelectedServer" @@ -67,9 +63,11 @@ extension Preferences { #if DEBUG case lastBackgroundTaskDate = "LastBackgroundTaskDate" #endif + + case enableDebugLogging = "EnableDebugLogging" } } extension UserDefaults { - static var group: UserDefaults = UserDefaults(suiteName: "group.\(Bundle.main.mainBundleIdentifier)")! + static let group = UserDefaults(suiteName: "group.\(Bundle.main.mainBundleIdentifier)")! } diff --git a/Shared/Extensions+Modifiers/Error+.swift b/Shared/Extensions+Modifiers/Error+.swift deleted file mode 100644 index 98d6e8f7..00000000 --- a/Shared/Extensions+Modifiers/Error+.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Error+.swift -// Harbour -// -// Created by royal on 23/01/2022. -// - -import Foundation - -extension Error { - var readableDescription: String { - (self as? LocalizedError)?.errorDescription ?? self.localizedDescription - } -} diff --git a/Shared/Extensions+Modifiers/Portainer+.swift b/Shared/Extensions+Modifiers/Portainer+.swift index 8ccceb7d..cbf5fe3b 100644 --- a/Shared/Extensions+Modifiers/Portainer+.swift +++ b/Shared/Extensions+Modifiers/Portainer+.swift @@ -10,7 +10,7 @@ import PortainerKit extension Portainer { enum PortainerError: LocalizedError { - case noServerURL + case noServerURL(URL?) case noAPI case noEndpoint case noToken diff --git a/Shared/Extensions+Modifiers/View+Shared.swift b/Shared/Extensions+Modifiers/View+Shared.swift index 62977c9c..7db72f05 100644 --- a/Shared/Extensions+Modifiers/View+Shared.swift +++ b/Shared/Extensions+Modifiers/View+Shared.swift @@ -17,6 +17,10 @@ extension View { func padding(_ size: PaddingSize) -> some View { padding(size.rawValue) } + + func maxSize(width: Bool = true, height: Bool = true, alignment: Alignment = .center) -> some View { + frame(maxWidth: width ? .infinity : nil, maxHeight: height ? .infinity : nil, alignment: alignment) + } } // MARK: PaddingSize diff --git a/Shared/GenericError.swift b/Shared/GenericError.swift new file mode 100644 index 00000000..d19fd059 --- /dev/null +++ b/Shared/GenericError.swift @@ -0,0 +1,19 @@ +// +// GenericError.swift +// Harbour +// +// Created by royal on 06/05/2022. +// + +import Foundation + +enum GenericError: LocalizedError { + case invalidURL + + var localizedDescription: String? { + switch self { + case .invalidURL: + return Localization.Error.invalidURL + } + } +} diff --git a/Widgets/ContainerStatus/ContainerStatusWidget+WidgetView.swift b/Widgets/ContainerStatus/ContainerStatusWidget+WidgetView.swift index 692e61bf..4528e7c0 100644 --- a/Widgets/ContainerStatus/ContainerStatusWidget+WidgetView.swift +++ b/Widgets/ContainerStatus/ContainerStatusWidget+WidgetView.swift @@ -16,13 +16,13 @@ extension ContainerStatusWidget { @ViewBuilder var errorOverlay: some View { if let error = entry.error { - Text("Error: \(error.readableDescription)") + Text("Error: \(error.localizedDescription)") .font(.system(.footnote, design: .monospaced)) .lineLimit(nil) .multilineTextAlignment(.center) .minimumScaleFactor(0.7) .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .maxSize() .background(.ultraThinMaterial) } } diff --git a/Widgets/ContainerStatus/ContainerStatusWidget.swift b/Widgets/ContainerStatus/ContainerStatusWidget.swift index 3f1cd5e2..af2890fc 100644 --- a/Widgets/ContainerStatus/ContainerStatusWidget.swift +++ b/Widgets/ContainerStatus/ContainerStatusWidget.swift @@ -49,7 +49,7 @@ struct ContainerStatusWidget { let entry = Entry(date: now, configuration: configuration, container: container) completion(entry) } catch { - Widgets.logger.error("\(#fileID, privacy: .public):\(#line, privacy: .public) \(#function, privacy: .public) (\(configuration.container?.identifier ?? "", privacy: .sensitive(mask: .hash))) Error! \(error.readableDescription, privacy: .public)") + Widgets.logger.error("\(#fileID, privacy: .public):\(#line, privacy: .public) \(#function, privacy: .public) (\(configuration.container?.identifier ?? "", privacy: .sensitive(mask: .hash))) Error! \(error.localizedDescription, privacy: .public)") let entry = Entry(date: now, configuration: configuration, container: nil, error: error) completion(entry) @@ -80,7 +80,7 @@ struct ContainerStatusWidget { let timeline = Timeline(entries: [entry], policy: .atEnd) completion(timeline) } catch { - Widgets.logger.error("\(#fileID, privacy: .public):\(#line, privacy: .public) \(#function, privacy: .public) (\(configuration.container?.identifier ?? "", privacy: .sensitive(mask: .hash))) Error! \(error.readableDescription, privacy: .public)") + Widgets.logger.error("\(#fileID, privacy: .public):\(#line, privacy: .public) \(#function, privacy: .public) (\(configuration.container?.identifier ?? "", privacy: .sensitive(mask: .hash))) Error! \(error.localizedDescription, privacy: .public)") let entry = Entry(date: now, configuration: configuration, container: nil, error: error) let timeline = Timeline(entries: [entry], policy: .atEnd)