Add browser

This commit is contained in:
Christoph Hagen
2025-02-13 21:24:39 +01:00
parent e05e63d6a7
commit 8f426bd719
8 changed files with 510 additions and 1 deletions

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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"
}
}
}

View File

@ -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) {}
}