diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 4b729b1..0cb15c8 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -169,6 +169,12 @@ E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D2A2CED2CC30000979F /* TagDetailView.swift */; }; E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */; }; E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A9CB7D2C7BCF2A005C89CC /* Page.swift */; }; + 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 */; }; + E2B4820D2D5E811E005C309D /* TryFilesMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B4820C2D5E811E005C309D /* TryFilesMiddleware.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 */; }; @@ -425,6 +431,11 @@ E2A37D2A2CED2CC30000979F /* TagDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDetailView.swift; sourceTree = ""; }; E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextField.swift; sourceTree = ""; }; 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 = ""; }; + E2B4820C2D5E811E005C309D /* TryFilesMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TryFilesMiddleware.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 = ""; }; @@ -532,6 +543,7 @@ E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */, E25DA52C2CFFC3EC00AEF16D /* SDWebImageAVIFCoder in Frameworks */, E2FD1D522D4644B400B48627 /* SVGView in Frameworks */, + E2B482002D5D1136005C309D /* Vapor in Frameworks */, E25DA57D2D01C67900AEF16D /* Ink in Frameworks */, E25DA52F2CFFC91B00AEF16D /* SDWebImageWebPCoder in Frameworks */, E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */, @@ -837,6 +849,18 @@ path = Tags; sourceTree = ""; }; + E2B482012D5D1325005C309D /* Server */ = { + isa = PBXGroup; + children = ( + E2B4820C2D5E811E005C309D /* TryFilesMiddleware.swift */, + E2B482082D5E7F4C005C309D /* WebContentView.swift */, + E2B482062D5E7DF0005C309D /* WebDetailView.swift */, + E2B482042D5E7D4A005C309D /* WebView.swift */, + E2B482022D5D132D005C309D /* WebServer.swift */, + ); + path = Server; + sourceTree = ""; + }; E2B85F392C428F020047CD0C /* Model */ = { isa = PBXGroup; children = ( @@ -957,6 +981,7 @@ E2DD04722C276F31003BFF1F /* CHDataManagement */ = { isa = PBXGroup; children = ( + E2B482012D5D1325005C309D /* Server */, E29D31372D043EB80051B7F4 /* Main */, E25DA5782D01C56200AEF16D /* Generator */, E2A37D0F2CE5375E0000979F /* Storage */, @@ -1139,6 +1164,7 @@ E25DA57F2D01C6AC00AEF16D /* Splash */, E29D31A72D0CDC5D0051B7F4 /* SwiftSoup */, E2FD1D512D4644B400B48627 /* SVGView */, + E2B481FF2D5D1136005C309D /* Vapor */, ); productName = CHDataManagement; productReference = E2DD04702C276F31003BFF1F /* CHDataManagement.app */; @@ -1177,6 +1203,7 @@ E25DA57E2D01C6AC00AEF16D /* XCRemoteSwiftPackageReference "Splash" */, E29D31A62D0CDC5D0051B7F4 /* XCRemoteSwiftPackageReference "SwiftSoup" */, E2FD1D502D4644B400B48627 /* XCRemoteSwiftPackageReference "SVGView" */, + E2B481FE2D5D1136005C309D /* XCRemoteSwiftPackageReference "vapor" */, ); productRefGroup = E2DD04712C276F31003BFF1F /* Products */; projectDirPath = ""; @@ -1218,6 +1245,7 @@ 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 */, @@ -1253,6 +1281,7 @@ E22990422D107A95009F8D77 /* ImageVersion.swift in Sources */, E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */, E2FE0F282D2AFB11002963B7 /* ImageCompare.swift in Sources */, + E2B482052D5E7D4A005C309D /* WebView.swift in Sources */, E229904E2D13535C009F8D77 /* SecurityBookmark.swift in Sources */, E2FE0F6E2D2D3689002963B7 /* LocalizedAudioPlayerSettings.swift in Sources */, E2A21C082CB17B870060935B /* TagView.swift in Sources */, @@ -1301,8 +1330,10 @@ E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */, E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */, E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */, + E2B4820D2D5E811E005C309D /* TryFilesMiddleware.swift in Sources */, E20BCC9F2D53851400B8DBEB /* SelectableListItem.swift in Sources */, E20BCCAB2D53B86900B8DBEB /* GenerationResultsIssueView.swift in Sources */, + E2B482092D5E7F4F005C309D /* WebContentView.swift in Sources */, E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */, E2FD1D462D46428100B48627 /* PageIconView.swift in Sources */, E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */, @@ -1327,6 +1358,7 @@ E2FE0F622D2C0D8D002963B7 /* VersionedVideo.swift in Sources */, E2FE0EEE2D1C22F3002963B7 /* MarkdownLinkProcessor.swift in Sources */, E2FE0F602D2C0422002963B7 /* VideoBlock.swift in Sources */, + E2B482032D5D1331005C309D /* WebServer.swift in Sources */, E2FE0F022D266FCB002963B7 /* LocalizedNavigationSettings.swift in Sources */, E29D313F2D04822C0051B7F4 /* AddPostView.swift in Sources */, E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */, @@ -1725,6 +1757,14 @@ minimumVersion = 2.7.6; }; }; + E2B481FE2D5D1136005C309D /* XCRemoteSwiftPackageReference "vapor" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/vapor/vapor.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.113.2; + }; + }; E2B85F342C426BED0047CD0C /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols"; @@ -1774,6 +1814,11 @@ package = E29D31A62D0CDC5D0051B7F4 /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; + E2B481FF2D5D1136005C309D /* Vapor */ = { + isa = XCSwiftPackageProductDependency; + package = E2B481FE2D5D1136005C309D /* XCRemoteSwiftPackageReference "vapor" */; + productName = Vapor; + }; E2B85F352C426BEE0047CD0C /* SFSafeSymbols */ = { isa = XCSwiftPackageProductDependency; package = E2B85F342C426BED0047CD0C /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; diff --git a/CHDataManagement.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CHDataManagement.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b7a392d..9356e9a 100644 --- a/CHDataManagement.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CHDataManagement.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,33 @@ { - "originHash" : "83059c87de78e5571dba4ab957133083b5c56dff4c72bf5c872969be5ca53685", + "originHash" : "747e13d88856438f8013440b6d706faa50b8e06e8a370d5c6bbfaf192255f3ff", "pins" : [ + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "b645ad40822b5c59ac92b758c5c17af054b5b01f", + "version" : "1.25.1" + } + }, + { + "identity" : "async-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/async-kit.git", + "state" : { + "revision" : "e048c8ee94967e8d8a1c2ec0e1156d6f7fa34d31", + "version" : "1.20.0" + } + }, + { + "identity" : "console-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/console-kit.git", + "state" : { + "revision" : "742f624a998cba2a9e653d9b1e91ad3f3a5dff6b", + "version" : "4.15.2" + } + }, { "identity" : "highlightedtexteditor", "kind" : "remoteSourceControl", @@ -55,6 +82,24 @@ "version" : "1.3.2" } }, + { + "identity" : "multipart-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/multipart-kit.git", + "state" : { + "revision" : "3498e60218e6003894ff95192d756e238c01f44e", + "version" : "4.7.1" + } + }, + { + "identity" : "routing-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/routing-kit.git", + "state" : { + "revision" : "8c9a227476555c55837e569be71944e02a056b72", + "version" : "4.9.1" + } + }, { "identity" : "sdwebimage", "kind" : "remoteSourceControl", @@ -109,6 +154,159 @@ "version" : "1.0.6" } }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "ae33e5941bb88d88538d0a6b19ca0b01e6c76dcf", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "b828ba476ab068f0b00d6b41f92f364961b0f323", + "version" : "3.10.2" + } + }, + { + "identity" : "swift-distributed-tracing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-distributed-tracing.git", + "state" : { + "revision" : "a64a0abc2530f767af15dd88dda7f64d5f1ff9de", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "ef18d829e8b92d731ad27bb81583edd2094d1ce3", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91", + "version" : "1.6.2" + } + }, + { + "identity" : "swift-metrics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-metrics.git", + "state" : { + "revision" : "5e63558d12e0267782019f5dadfcae83a7d06e09", + "version" : "2.5.1" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "c51907a839e63ebf0ba2076bba73dd96436bd1b9", + "version" : "2.81.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "2e9746cfc57554f70b650b021b6ae4738abef3e6", + "version" : "1.24.1" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "170f4ca06b6a9c57b811293cebcb96e81b661310", + "version" : "1.35.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "0cc3528ff48129d64ab9cab0b1cd621634edfc6b", + "version" : "2.29.3" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "3c394067c08d1225ba8442e9cffb520ded417b64", + "version" : "1.23.1" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-service-context", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-service-context.git", + "state" : { + "revision" : "8946c930cae601452149e45d31d8ddfac973c3c7", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", + "version" : "1.4.0" + } + }, { "identity" : "swiftsoup", "kind" : "remoteSourceControl", @@ -117,6 +315,24 @@ "revision" : "0837db354faf9c9deb710dc597046edaadf5360f", "version" : "2.7.6" } + }, + { + "identity" : "vapor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/vapor.git", + "state" : { + "revision" : "a425e32f9b9d19c0ecab952cb4484c1c15e2536f", + "version" : "4.113.2" + } + }, + { + "identity" : "websocket-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/websocket-kit.git", + "state" : { + "revision" : "4232d34efa49f633ba61afde365d3896fc7f8740", + "version" : "2.15.0" + } } ], "version" : 3 diff --git a/CHDataManagement/Main/TabSelection.swift b/CHDataManagement/Main/TabSelection.swift index 7070584..d7a5482 100644 --- a/CHDataManagement/Main/TabSelection.swift +++ b/CHDataManagement/Main/TabSelection.swift @@ -6,5 +6,6 @@ enum MainViewTab { case pages case tags case files + case browser } diff --git a/CHDataManagement/Server/TryFilesMiddleware.swift b/CHDataManagement/Server/TryFilesMiddleware.swift new file mode 100644 index 0000000..1631b07 --- /dev/null +++ b/CHDataManagement/Server/TryFilesMiddleware.swift @@ -0,0 +1,68 @@ +import Vapor + +struct TryFilesMiddleware: AsyncMiddleware { + + let publicDirectory: String + + func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response { + let relativePath = request.url.path + let potentialPaths = [ + publicDirectory + relativePath, + publicDirectory + relativePath + ".html", + publicDirectory + relativePath + "/1.html" + ] + + for path in potentialPaths { + var isDirectory: ObjCBool = false + guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory), + !isDirectory.boolValue else { + continue + } + let url = URL(fileURLWithPath: path) + let data = try Data(contentsOf: url) + let response = Response(status: .ok, body: .init(data: data)) + response.headers.replaceOrAdd(name: .contentType, value: url.mimeType) + response.headers.replaceOrAdd(name: .contentLength, value: "\(data.count)") + return response + } + return .init(status: .notFound) + } +} + +private extension URL { + + var mimeType: String { + switch pathExtension.lowercased() { + case "html": return "text/html" + case "css": return "text/css" + case "js": return "application/javascript" + case "json": return "application/json" + case "xml": return "application/xml" + case "txt": return "text/plain" + + case "jpg", "jpeg": return "image/jpeg" + case "png": return "image/png" + case "gif": return "image/gif" + case "svg": return "image/svg+xml" + case "webp": return "image/webp" + case "ico": return "image/x-icon" + + case "pdf": return "application/pdf" + case "zip": return "application/zip" + case "tar": return "application/x-tar" + case "gz": return "application/gzip" + case "rar": return "application/vnd.rar" + + case "mp3": return "audio/mpeg" + case "wav": return "audio/wav" + case "ogg": return "audio/ogg" + + case "mp4": return "video/mp4" + case "mov": return "video/quicktime" + case "avi": return "video/x-msvideo" + case "webm": return "video/webm" + + default: return "application/octet-stream" // Default binary data type + } + } +} diff --git a/CHDataManagement/Server/WebContentView.swift b/CHDataManagement/Server/WebContentView.swift new file mode 100644 index 0000000..3707e63 --- /dev/null +++ b/CHDataManagement/Server/WebContentView.swift @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..8b41ecf --- /dev/null +++ b/CHDataManagement/Server/WebDetailView.swift @@ -0,0 +1,51 @@ +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 new file mode 100644 index 0000000..9e056f2 --- /dev/null +++ b/CHDataManagement/Server/WebServer.swift @@ -0,0 +1,93 @@ +import Vapor +import Foundation +import WebKit + +@MainActor +final class WebServer: NSObject, ObservableObject, WKNavigationDelegate { + + private var app: Application? + + @Published + var isRunning = false + + @Published + var port: Int + + @Published + var webView = WKWebView() + + @Published + var currentUrl: String = "" + + init(port: Int) { + self.port = port + super.init() + + webView.navigationDelegate = self + } + + func loadHomeUrl() { + let url = URL(string: "http://localhost:\(port)/feed")! + webView.load(URLRequest(url: url)) + } + + func reloadPage() { + webView.reload() + } + + func startServer(in directory: String) { + if let app, !app.didShutdown { + print("WebServer: Already running") + return + } + Task { + var vaporArgs = CommandLine.arguments + let allowedCommands = ["serve", "routes"] + vaporArgs = vaporArgs.filter { allowedCommands.contains($0) || $0 == CommandLine.arguments.first } + + let app = try await Application.make(.detect(arguments: vaporArgs)) + app.logger.logLevel = .warning + self.app = app + + 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() + } + print("WebServer: Starting") + try await app.execute() + try await app.asyncShutdown() + } + } + + func stopServer() { + guard let app else { + print("WebServer: Already stopped") + return + } + print("WebServer: Stopping") + Task { + do { + try await app.asyncShutdown() + } catch { + print("Failed to stop web server: \(error)") + } + DispatchQueue.main.async { + self.isRunning = false + } + } + } +} + +extension WebServer { + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + DispatchQueue.main.async { + self.currentUrl = webView.url?.absoluteString ?? "Unknown URL" + } + } +} diff --git a/CHDataManagement/Server/WebView.swift b/CHDataManagement/Server/WebView.swift new file mode 100644 index 0000000..4580f17 --- /dev/null +++ b/CHDataManagement/Server/WebView.swift @@ -0,0 +1,15 @@ +import SwiftUI +import WebKit + +// WebView Wrapper for SwiftUI +struct WebView: NSViewRepresentable { + + @ObservedObject + var viewModel: WebServer + + func makeNSView(context: Context) -> WKWebView { + return viewModel.webView + } + + func updateNSView(_ nsView: WKWebView, context: Context) {} +}