From 2cad27b5044769bb3bcff9742edc640e46594384 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Sat, 15 Feb 2025 01:02:25 +0100 Subject: [PATCH] Add upload, preview sheet --- CHDataManagement.xcodeproj/project.pbxproj | 28 ++++-- .../Extensions/String+Extensions.swift | 25 +++++- CHDataManagement/Main/MainView.swift | 51 +++++------ CHDataManagement/Main/TabSelection.swift | 1 - CHDataManagement/Model/Content.swift | 5 +- .../Model/Settings/GeneralSettings.swift | 27 +++++- .../Model/Settings/Settings.swift | 5 +- CHDataManagement/Push/RemotePush.swift | 85 +++++++++++++++++++ CHDataManagement/Push/UploadSheet.swift | 71 ++++++++++++++++ CHDataManagement/Server/WebContentView.swift | 20 ----- CHDataManagement/Server/WebDetailView.swift | 51 ----------- CHDataManagement/Server/WebServer.swift | 15 +++- .../Server/WebsitePreviewSheet.swift | 74 ++++++++++++++++ .../General/GeneralSettingsDetailView.swift | 15 ++++ 14 files changed, 358 insertions(+), 115 deletions(-) create mode 100644 CHDataManagement/Push/RemotePush.swift create mode 100644 CHDataManagement/Push/UploadSheet.swift delete mode 100644 CHDataManagement/Server/WebContentView.swift delete mode 100644 CHDataManagement/Server/WebDetailView.swift create mode 100644 CHDataManagement/Server/WebsitePreviewSheet.swift diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 0cb15c8..3d2f118 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -172,9 +172,10 @@ E2B482002D5D1136005C309D /* Vapor in Frameworks */ = {isa = PBXBuildFile; productRef = E2B481FF2D5D1136005C309D /* Vapor */; }; E2B482032D5D1331005C309D /* WebServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482022D5D132D005C309D /* WebServer.swift */; }; E2B482052D5E7D4A005C309D /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482042D5E7D4A005C309D /* WebView.swift */; }; - E2B482072D5E7DF4005C309D /* WebDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482062D5E7DF0005C309D /* WebDetailView.swift */; }; - E2B482092D5E7F4F005C309D /* WebContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482082D5E7F4C005C309D /* WebContentView.swift */; }; + E2B482092D5E7F4F005C309D /* WebsitePreviewSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482082D5E7F4C005C309D /* WebsitePreviewSheet.swift */; }; E2B4820D2D5E811E005C309D /* TryFilesMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B4820C2D5E811E005C309D /* TryFilesMiddleware.swift */; }; + E2B482102D5E9FF9005C309D /* RemotePush.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B4820F2D5E9FF5005C309D /* RemotePush.swift */; }; + E2B482122D600AE0005C309D /* UploadSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482112D600AD1005C309D /* UploadSheet.swift */; }; E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2B85F352C426BEE0047CD0C /* SFSafeSymbols */; }; E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3A2C428F0D0047CD0C /* Post.swift */; }; E2B85F3D2C4293F80047CD0C /* FeedPageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */; }; @@ -433,9 +434,10 @@ E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = ""; }; E2B482022D5D132D005C309D /* WebServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebServer.swift; sourceTree = ""; }; E2B482042D5E7D4A005C309D /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; - E2B482062D5E7DF0005C309D /* WebDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebDetailView.swift; sourceTree = ""; }; - E2B482082D5E7F4C005C309D /* WebContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebContentView.swift; sourceTree = ""; }; + E2B482082D5E7F4C005C309D /* WebsitePreviewSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsitePreviewSheet.swift; sourceTree = ""; }; E2B4820C2D5E811E005C309D /* TryFilesMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TryFilesMiddleware.swift; sourceTree = ""; }; + E2B4820F2D5E9FF5005C309D /* RemotePush.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePush.swift; sourceTree = ""; }; + E2B482112D600AD1005C309D /* UploadSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadSheet.swift; sourceTree = ""; }; E2B85F3A2C428F0D0047CD0C /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPageGenerator.swift; sourceTree = ""; }; E2B85F402C4294790047CD0C /* PageHead.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHead.swift; sourceTree = ""; }; @@ -853,14 +855,22 @@ isa = PBXGroup; children = ( E2B4820C2D5E811E005C309D /* TryFilesMiddleware.swift */, - E2B482082D5E7F4C005C309D /* WebContentView.swift */, - E2B482062D5E7DF0005C309D /* WebDetailView.swift */, + E2B482082D5E7F4C005C309D /* WebsitePreviewSheet.swift */, E2B482042D5E7D4A005C309D /* WebView.swift */, E2B482022D5D132D005C309D /* WebServer.swift */, ); path = Server; sourceTree = ""; }; + E2B4820E2D5E9FF0005C309D /* Push */ = { + isa = PBXGroup; + children = ( + E2B482112D600AD1005C309D /* UploadSheet.swift */, + E2B4820F2D5E9FF5005C309D /* RemotePush.swift */, + ); + path = Push; + sourceTree = ""; + }; E2B85F392C428F020047CD0C /* Model */ = { isa = PBXGroup; children = ( @@ -981,6 +991,7 @@ E2DD04722C276F31003BFF1F /* CHDataManagement */ = { isa = PBXGroup; children = ( + E2B4820E2D5E9FF0005C309D /* Push */, E2B482012D5D1325005C309D /* Server */, E29D31372D043EB80051B7F4 /* Main */, E25DA5782D01C56200AEF16D /* Generator */, @@ -1245,7 +1256,6 @@ E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */, E229904C2D10BE5D009F8D77 /* InitialSetupView.swift in Sources */, E218502B2CF790B30090B18B /* PostContentView.swift in Sources */, - E2B482072D5E7DF4005C309D /* WebDetailView.swift in Sources */, E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */, E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */, E2521E042D51796000C56662 /* StorageErrorView.swift in Sources */, @@ -1333,7 +1343,7 @@ E2B4820D2D5E811E005C309D /* TryFilesMiddleware.swift in Sources */, E20BCC9F2D53851400B8DBEB /* SelectableListItem.swift in Sources */, E20BCCAB2D53B86900B8DBEB /* GenerationResultsIssueView.swift in Sources */, - E2B482092D5E7F4F005C309D /* WebContentView.swift in Sources */, + E2B482092D5E7F4F005C309D /* WebsitePreviewSheet.swift in Sources */, E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */, E2FD1D462D46428100B48627 /* PageIconView.swift in Sources */, E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */, @@ -1428,6 +1438,7 @@ E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */, E2FE0F2C2D2B119A002963B7 /* ImageCommand.swift in Sources */, E2FE0F112D268E7E002963B7 /* MarkdownCodeProcessor.swift in Sources */, + E2B482102D5E9FF9005C309D /* RemotePush.swift in Sources */, E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */, E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */, E22990192D0E3546009F8D77 /* ItemReference.swift in Sources */, @@ -1442,6 +1453,7 @@ E2A37D0E2CE527070000979F /* Storage.swift in Sources */, E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */, E29D314D2D04FCBF0051B7F4 /* FileToAddView.swift in Sources */, + E2B482122D600AE0005C309D /* UploadSheet.swift in Sources */, E2FE0F6C2D2D335E002963B7 /* LocalizedPageSettingsView.swift in Sources */, E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */, E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */, diff --git a/CHDataManagement/Extensions/String+Extensions.swift b/CHDataManagement/Extensions/String+Extensions.swift index 294f10c..194e2b5 100644 --- a/CHDataManagement/Extensions/String+Extensions.swift +++ b/CHDataManagement/Extensions/String+Extensions.swift @@ -17,10 +17,29 @@ extension String { } var withLeadingSlashRemoved: String { - if !hasPrefix("/") { - return self + hasPrefix("/") ? String(dropFirst("/".count)) : self + } + + var withLeadingSlash: String { + hasPrefix("/") ? self : "/" + self + } + + var withTrailingSlashRemoved: String { + hasSuffix("/") ? String(dropLast("/".count)) : self + } + + var withTrailingSlash: String { + hasSuffix("/") ? self : self + "/" + } + + var withHttpPrefixRemoved: String { + if hasPrefix("https://") { + return String(dropFirst("https://".count)) } - return String(dropFirst("/".count)) + if hasPrefix("http://") { + return String(dropFirst("http://".count)) + } + return self } var removingSurroundingQuotes: String { diff --git a/CHDataManagement/Main/MainView.swift b/CHDataManagement/Main/MainView.swift index 598cf03..15b456f 100644 --- a/CHDataManagement/Main/MainView.swift +++ b/CHDataManagement/Main/MainView.swift @@ -39,6 +39,9 @@ struct MainView: App { @StateObject private var content: Content = .init() + @StateObject + private var upload: RemotePush = .init() + @State private var language: ContentLanguage = .english @@ -60,6 +63,12 @@ struct MainView: App { @State private var showGenerationSheet = false + @State + private var showPreviewSheet = false + + @State + private var showUploadSheet = false + @ViewBuilder var sidebar: some View { switch selection.tab { @@ -67,7 +76,6 @@ struct MainView: App { case .pages: PageListView() case .tags: TagListView() case .files: FileListView(selectedFile: $selection.file) - case .browser: EmptyView() } } @@ -82,9 +90,6 @@ struct MainView: App { SelectedContentView(selected: $selection.tag) case .files: SelectedContentView(selected: $selection.file) - case .browser: - WebContentView() - .environmentObject(server) } } @@ -99,16 +104,13 @@ struct MainView: App { SelectedDetailView(selected: $selection.tag) case .files: SelectedDetailView(selected: $selection.file) - case .browser: - WebDetailView() - .environmentObject(server) } } @ViewBuilder var addItemSheet: some View { switch selection.tab { - case .posts, .browser: + case .posts: AddPostView(selected: $selection.post) case .pages: AddPageView(selected: $selection.page) @@ -141,7 +143,6 @@ struct MainView: App { Text("Pages").tag(MainViewTab.pages) Text("Tags").tag(MainViewTab.tags) Text("Files").tag(MainViewTab.files) - Text("Preview").tag(MainViewTab.browser) }.pickerStyle(.segmented) }.frame(minWidth: 400) } @@ -179,8 +180,13 @@ struct MainView: App { } } ToolbarItem { - Button(action: toggleWebServer) { - Image(systemSymbol: server.isRunning ? .eye : .eyeSlash) + Button(action: { showPreviewSheet = true }) { + Image(systemSymbol: .eye) + } + } + ToolbarItem { + Button(action: { showUploadSheet = true }) { + Image(systemSymbol: .squareAndArrowUp) } } ToolbarItem { @@ -220,6 +226,16 @@ struct MainView: App { GenerationContentView() .environmentObject(content) } + .sheet(isPresented: $showPreviewSheet) { + WebsitePreviewSheet() + .environmentObject(content) + .environmentObject(server) + } + .sheet(isPresented: $showUploadSheet) { + UploadSheet() + .environmentObject(content) + .environmentObject(upload) + } } } @@ -267,18 +283,5 @@ struct MainView: App { showInitialSetupSheet = true } } - - private func toggleWebServer() { - guard !server.isRunning else { - server.stopServer() - return - } - guard let folder = content.storage.outputScope?.url.path() else { - print("No output folder to start server") - return - } - - server.startServer(in: folder) - } } diff --git a/CHDataManagement/Main/TabSelection.swift b/CHDataManagement/Main/TabSelection.swift index d7a5482..7070584 100644 --- a/CHDataManagement/Main/TabSelection.swift +++ b/CHDataManagement/Main/TabSelection.swift @@ -6,6 +6,5 @@ enum MainViewTab { case pages case tags case files - case browser } diff --git a/CHDataManagement/Model/Content.swift b/CHDataManagement/Model/Content.swift index fbd805a..d900974 100644 --- a/CHDataManagement/Model/Content.swift +++ b/CHDataManagement/Model/Content.swift @@ -63,7 +63,10 @@ final class Content: ObservableObject { storage: storage, settings: settings) storage.errorNotification = { [weak self] error in - self?.storageErrors.append(error) + guard let self else { return } + DispatchQueue.main.async { + self.storageErrors.append(error) + } } settings.content = self } diff --git a/CHDataManagement/Model/Settings/GeneralSettings.swift b/CHDataManagement/Model/Settings/GeneralSettings.swift index b99c75b..89807b1 100644 --- a/CHDataManagement/Model/Settings/GeneralSettings.swift +++ b/CHDataManagement/Model/Settings/GeneralSettings.swift @@ -11,10 +11,22 @@ final class GeneralSettings: ObservableObject { @Published var linkPreviewImageHeight: Int - init(url: String, linkPreviewImageWidth: Int, linkPreviewImageHeight: Int) { + @Published + var remoteUserForUpload: String + + @Published + var remotePortForUpload: Int + + @Published + var remotePathForUpload: String + + init(url: String, linkPreviewImageWidth: Int, linkPreviewImageHeight: Int, remoteUserForUpload: String, remotePortForUpload: Int, remotePathForUpload: String) { self.url = url self.linkPreviewImageWidth = linkPreviewImageWidth self.linkPreviewImageHeight = linkPreviewImageHeight + self.remoteUserForUpload = remoteUserForUpload + self.remotePortForUpload = remotePortForUpload + self.remotePathForUpload = remotePathForUpload } } @@ -24,19 +36,28 @@ extension GeneralSettings { self.init( url: data.url, linkPreviewImageWidth: data.linkPreviewImageWidth, - linkPreviewImageHeight: data.linkPreviewImageHeight) + linkPreviewImageHeight: data.linkPreviewImageHeight, + remoteUserForUpload: data.remoteUserForUpload, + remotePortForUpload: data.remotePortForUpload, + remotePathForUpload: data.remotePathForUpload) } var data: Data { .init( url: url, linkPreviewImageWidth: linkPreviewImageWidth, - linkPreviewImageHeight: linkPreviewImageHeight) + linkPreviewImageHeight: linkPreviewImageHeight, + remoteUserForUpload: remoteUserForUpload, + remotePortForUpload: remotePortForUpload, + remotePathForUpload: remotePathForUpload) } struct Data: Codable, Equatable { let url: String let linkPreviewImageWidth: Int let linkPreviewImageHeight: Int + let remoteUserForUpload: String + let remotePortForUpload: Int + let remotePathForUpload: String } } diff --git a/CHDataManagement/Model/Settings/Settings.swift b/CHDataManagement/Model/Settings/Settings.swift index b18d498..ba1baf1 100644 --- a/CHDataManagement/Model/Settings/Settings.swift +++ b/CHDataManagement/Model/Settings/Settings.swift @@ -108,7 +108,10 @@ extension GeneralSettings { static let `default`: GeneralSettings = .init( url: "https://example.com", linkPreviewImageWidth: 1200, - linkPreviewImageHeight: 630) + linkPreviewImageHeight: 630, + remoteUserForUpload: "user", + remotePortForUpload: 22, + remotePathForUpload: "/home/user/web") } extension AudioPlayerSettings { diff --git a/CHDataManagement/Push/RemotePush.swift b/CHDataManagement/Push/RemotePush.swift new file mode 100644 index 0000000..fc6ac22 --- /dev/null +++ b/CHDataManagement/Push/RemotePush.swift @@ -0,0 +1,85 @@ +import Foundation + +final class RemotePush: ObservableObject { + + @Published + var isTransmittingToRemote = false + + @Published + var lastPushWasSuccessful = true + + func transmitToRemote(settings: GeneralSettings, outputFolder: String, outputHandler: @escaping (String) -> Void) { + guard !isTransmittingToRemote else { return } + DispatchQueue.main.async { + self.isTransmittingToRemote = true + } + DispatchQueue.global().async { + let success = self.transmit( + outputFolder: outputFolder, + remoteUser: settings.remoteUserForUpload, + remotePort: settings.remotePortForUpload, + remoteDomain: settings.url, + remotePath: settings.remotePathForUpload, + excludedItems: [".git"], + outputHandler: outputHandler) + DispatchQueue.main.async { + self.isTransmittingToRemote = false + self.lastPushWasSuccessful = success + } + } + } + + private func transmit( + outputFolder: String, + remoteUser: String, + remotePort: Int, + remoteDomain: String, + remotePath: String, + excludedItems: [String], + outputHandler: @escaping (String) -> Void + ) -> Bool { + let remoteDomain = remoteDomain.withHttpPrefixRemoved + let remotePath = remotePath.withLeadingSlash.withTrailingSlash + let outputFolder = outputFolder.withLeadingSlash.withTrailingSlash + + let process = Process() + process.executableURL = URL(fileURLWithPath: "/bin/bash") + let arguments = [ + "/opt/homebrew/bin/rsync", + "-hrutv", + "--info=progress2"] + + excludedItems.reduce(into: []) { $0 += ["--exclude", $1] } + + [ + "-e", "\"/opt/homebrew/bin/ssh -p \(remotePort)\"", + outputFolder, + "\(remoteUser)@\(remoteDomain):\(remotePath)" + ] + + let argument = arguments.joined(separator: " ") + + process.arguments = ["-c", argument] + + print(argument) + + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe + + let fileHandle = pipe.fileHandleForReading + + // Use a DispatchQueue to read output asynchronously + fileHandle.readabilityHandler = { fileHandle in + if let output = String(data: fileHandle.availableData, encoding: .utf8), !output.isEmpty { + outputHandler(output) + } + } + + process.launch() + process.waitUntilExit() + + if process.terminationStatus == 0 { + return true + } + return false + } +} diff --git a/CHDataManagement/Push/UploadSheet.swift b/CHDataManagement/Push/UploadSheet.swift new file mode 100644 index 0000000..a7ee2e7 --- /dev/null +++ b/CHDataManagement/Push/UploadSheet.swift @@ -0,0 +1,71 @@ +import SwiftUI +import SFSafeSymbols + +struct UploadSheet: View { + + @EnvironmentObject + private var content: Content + + @EnvironmentObject + private var upload: RemotePush + + @Environment(\.dismiss) + private var dismiss + + @State + private var output = "" + + private var uploadSymbol: SFSymbol { + if upload.isTransmittingToRemote { + return .squareAndArrowUpBadgeClock + } + if !upload.lastPushWasSuccessful { + return .squareAndArrowUpTrianglebadgeExclamationmark + } + return .squareAndArrowUp + } + + var header: String { + let user = content.settings.general.remoteUserForUpload + let port = content.settings.general.remotePortForUpload + let url = content.settings.general.remotePathForUpload.withHttpPrefixRemoved + let path = content.settings.general.remotePathForUpload + return "\(user)@\(url):\(port)/\(path)" + } + + var body: some View { + VStack { + HStack { + Button("Upload", action: startUpload) + .disabled(upload.isTransmittingToRemote) + Text(header) + Spacer() + Button("Close", action: { dismiss() }) + } + ScrollView { + Text(output) + .font(.body.monospaced()) + .foregroundStyle(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding() + .frame(minWidth: 500, idealWidth: 600, idealHeight: 500) + } + + private func startUpload() { + guard let folder = content.storage.outputScope?.url.path() else { + print("No output folder to start upload") + return + } + output = "" + + upload.transmitToRemote( + settings: content.settings.general, + outputFolder: folder) { newContent in + DispatchQueue.main.async { + self.output += newContent + } + } + } +} diff --git a/CHDataManagement/Server/WebContentView.swift b/CHDataManagement/Server/WebContentView.swift deleted file mode 100644 index 3707e63..0000000 --- a/CHDataManagement/Server/WebContentView.swift +++ /dev/null @@ -1,20 +0,0 @@ -import SwiftUI - -struct WebContentView: View { - - @EnvironmentObject - private var server: WebServer - - var body: some View { - if server.isRunning { - WebView(viewModel: server) - } else { - VStack { - Text("Webserver disabled") - .font(.title) - Text("Enable it to check out the generated content") - } - .foregroundStyle(.secondary) - } - } -} diff --git a/CHDataManagement/Server/WebDetailView.swift b/CHDataManagement/Server/WebDetailView.swift deleted file mode 100644 index 8b41ecf..0000000 --- a/CHDataManagement/Server/WebDetailView.swift +++ /dev/null @@ -1,51 +0,0 @@ -import SwiftUI -import SFSafeSymbols - -struct WebDetailView: View { - - @EnvironmentObject - private var content: Content - - @EnvironmentObject - private var server: WebServer - - var text: String { - server.isRunning ? "Stop" : "Start" - } - - var body: some View { - VStack { - TextField("", text: $server.currentUrl) - .disabled(true) - .textFieldStyle(.roundedBorder) - HStack { - Button(text, action: toggleWebServer) - .disabled(!server.isRunning && content.storage.outputScope == nil) - Button(action: { server.reloadPage() }) { - Label("Reload", systemSymbol: .arrowClockwise) - } - .disabled(!server.isRunning) - Button(action: { server.loadHomeUrl() }) { - Label("Home", systemSymbol: .house) - } - .disabled(!server.isRunning) - Spacer() - } - Spacer() - } - .padding() - } - - private func toggleWebServer() { - guard !server.isRunning else { - server.stopServer() - return - } - guard let folder = content.storage.outputScope?.url.path() else { - print("No output folder to start server") - return - } - - server.startServer(in: folder) - } -} diff --git a/CHDataManagement/Server/WebServer.swift b/CHDataManagement/Server/WebServer.swift index 9e056f2..4f54a90 100644 --- a/CHDataManagement/Server/WebServer.swift +++ b/CHDataManagement/Server/WebServer.swift @@ -10,6 +10,9 @@ final class WebServer: NSObject, ObservableObject, WKNavigationDelegate { @Published var isRunning = false + @Published + var isStarting = false + @Published var port: Int @@ -19,6 +22,10 @@ final class WebServer: NSObject, ObservableObject, WKNavigationDelegate { @Published var currentUrl: String = "" + var isNotReady: Bool { + isStarting || !isRunning + } + init(port: Int) { self.port = port super.init() @@ -40,6 +47,9 @@ final class WebServer: NSObject, ObservableObject, WKNavigationDelegate { print("WebServer: Already running") return } + guard !isStarting else { return } + self.isStarting = true + Task { var vaporArgs = CommandLine.arguments let allowedCommands = ["serve", "routes"] @@ -52,11 +62,10 @@ final class WebServer: NSObject, ObservableObject, WKNavigationDelegate { app.middleware.use(TryFilesMiddleware(publicDirectory: directory)) app.http.server.configuration.port = 8000 - DispatchQueue.main.async { - self.isRunning = true - } DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { self.loadHomeUrl() + self.isStarting = false + self.isRunning = true } print("WebServer: Starting") try await app.execute() diff --git a/CHDataManagement/Server/WebsitePreviewSheet.swift b/CHDataManagement/Server/WebsitePreviewSheet.swift new file mode 100644 index 0000000..a756219 --- /dev/null +++ b/CHDataManagement/Server/WebsitePreviewSheet.swift @@ -0,0 +1,74 @@ +import SwiftUI + +struct WebsitePreviewSheet: View { + + @EnvironmentObject + private var content: Content + + @EnvironmentObject + private var server: WebServer + + @Environment(\.dismiss) + private var dismiss + + var body: some View { + VStack { + HStack { + Button(action: { server.loadHomeUrl() }) { + Image(systemSymbol: .house) + } + .disabled(server.isNotReady) + Button(action: { server.reloadPage() }) { + Image(systemSymbol: .arrowClockwise) + } + .disabled(server.isNotReady) + TextField("", text: $server.currentUrl) + .disabled(true) + .textFieldStyle(.roundedBorder) + Spacer() + Button("Close", action: dismissSheet) + } + .padding() + if server.isRunning { + WebView(viewModel: server) + } else if server.isStarting { + Spacer() + ProgressView() + Text("Loading preview...") + .font(.title) + .foregroundStyle(.secondary) + Spacer() + } else { + Spacer() + Text("Webserver disabled") + .font(.title) + .foregroundStyle(.secondary) + Text("Enable it to check out the generated content") + .foregroundStyle(.secondary) + Spacer() + } + } + .frame(minWidth: 500, idealWidth: 600, idealHeight: 600) + .onAppear(perform: startWebServer) + } + + private func dismissSheet() { + if server.isRunning { + server.stopServer() + } + dismiss() + } + + private func startWebServer() { + guard !server.isRunning else { + server.stopServer() + return + } + guard let folder = content.storage.outputScope?.url.path() else { + print("No output folder to start server") + return + } + + server.startServer(in: folder) + } +} diff --git a/CHDataManagement/Views/Settings/General/GeneralSettingsDetailView.swift b/CHDataManagement/Views/Settings/General/GeneralSettingsDetailView.swift index 42d7da4..f62bb25 100644 --- a/CHDataManagement/Views/Settings/General/GeneralSettingsDetailView.swift +++ b/CHDataManagement/Views/Settings/General/GeneralSettingsDetailView.swift @@ -22,6 +22,21 @@ struct GeneralSettingsDetailView: View { title: "Link Preview Image Height", value: $generalSettings.linkPreviewImageHeight, footer: "The maximum height of a link preview image") + + StringPropertyView( + title: "Upload User", + text: $generalSettings.remoteUserForUpload, + footer: "The user on the server to connect via ssh for upload") + + IntegerPropertyView( + title: "Upload Port", + value: $generalSettings.remotePortForUpload, + footer: "The port on the server to rsync the generated website") + + StringPropertyView( + title: "Upload Folder", + text: $generalSettings.remotePathForUpload, + footer: "The path to the folder on the server where the files should be uploaded to") } .padding() }