Compare commits
2 Commits
58023c4dbc
...
c82f20bb60
| Author | SHA1 | Date | |
|---|---|---|---|
|
c82f20bb60
|
|||
|
458b3bd644
|
@@ -9,6 +9,7 @@ let package = Package(
|
|||||||
.library(name: "ApiController", targets: ["ApiController"]),
|
.library(name: "ApiController", targets: ["ApiController"]),
|
||||||
.library(name: "AuthClient", targets: ["AuthClient"]),
|
.library(name: "AuthClient", targets: ["AuthClient"]),
|
||||||
.library(name: "DatabaseClient", targets: ["DatabaseClient"]),
|
.library(name: "DatabaseClient", targets: ["DatabaseClient"]),
|
||||||
|
.library(name: "EnvClient", targets: ["EnvClient"]),
|
||||||
.library(name: "FileClient", targets: ["FileClient"]),
|
.library(name: "FileClient", targets: ["FileClient"]),
|
||||||
.library(name: "HTMLSnapshotTesting", targets: ["HTMLSnapshotTesting"]),
|
.library(name: "HTMLSnapshotTesting", targets: ["HTMLSnapshotTesting"]),
|
||||||
.library(name: "PdfClient", targets: ["PdfClient"]),
|
.library(name: "PdfClient", targets: ["PdfClient"]),
|
||||||
@@ -79,6 +80,14 @@ let package = Package(
|
|||||||
.product(name: "Vapor", package: "vapor"),
|
.product(name: "Vapor", package: "vapor"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
||||||
|
.target(
|
||||||
|
name: "EnvClient",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||||
|
]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "FileClient",
|
name: "FileClient",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
@@ -96,6 +105,8 @@ let package = Package(
|
|||||||
.target(
|
.target(
|
||||||
name: "PdfClient",
|
name: "PdfClient",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
.target(name: "EnvClient"),
|
||||||
|
.target(name: "FileClient"),
|
||||||
.target(name: "ManualDCore"),
|
.target(name: "ManualDCore"),
|
||||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||||
@@ -108,6 +119,7 @@ let package = Package(
|
|||||||
.target(name: "DatabaseClient"),
|
.target(name: "DatabaseClient"),
|
||||||
.target(name: "ManualDClient"),
|
.target(name: "ManualDClient"),
|
||||||
.target(name: "PdfClient"),
|
.target(name: "PdfClient"),
|
||||||
|
.product(name: "Vapor", package: "vapor"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
|
|||||||
12
Public/css/htmx.css
Normal file
12
Public/css/htmx.css
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.htmx-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htmx-request .htmx-indicator {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htmx-request.htmx-indicator {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,4 +2,3 @@
|
|||||||
@plugin "daisyui" {
|
@plugin "daisyui" {
|
||||||
themes: all;
|
themes: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8548,10 +8548,6 @@
|
|||||||
.italic {
|
.italic {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
.lining-nums {
|
|
||||||
--tw-numeric-figure: lining-nums;
|
|
||||||
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
|
||||||
}
|
|
||||||
.tabular-nums {
|
.tabular-nums {
|
||||||
--tw-numeric-spacing: tabular-nums;
|
--tw-numeric-spacing: tabular-nums;
|
||||||
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
||||||
|
|||||||
63
Public/js/htmx-download.js
Normal file
63
Public/js/htmx-download.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Copied from: https://github.com/dakixr/htmx-download/blob/main/htmx-download.js
|
||||||
|
htmx.defineExtension('htmx-download', {
|
||||||
|
onEvent: function(name, evt) {
|
||||||
|
|
||||||
|
if (name === 'htmx:beforeRequest') {
|
||||||
|
// Set the responseType to 'arraybuffer' to handle binary data
|
||||||
|
evt.detail.xhr.responseType = 'arraybuffer';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'htmx:beforeSwap') {
|
||||||
|
const xhr = evt.detail.xhr;
|
||||||
|
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
// Parse headers
|
||||||
|
const headers = {};
|
||||||
|
const headerStr = xhr.getAllResponseHeaders();
|
||||||
|
const headerArr = headerStr.trim().split(/[\r\n]+/);
|
||||||
|
headerArr.forEach((line) => {
|
||||||
|
const parts = line.split(": ");
|
||||||
|
const header = parts.shift().toLowerCase();
|
||||||
|
const value = parts.join(": ");
|
||||||
|
headers[header] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract filename
|
||||||
|
let filename = 'downloaded_file.xlsx';
|
||||||
|
if (headers['content-disposition']) {
|
||||||
|
const filenameMatch = headers['content-disposition'].match(/filename\*?=(?:UTF-8'')?"?([^;\n"]+)/i);
|
||||||
|
if (filenameMatch && filenameMatch[1]) {
|
||||||
|
filename = decodeURIComponent(filenameMatch[1].replace(/['"]/g, ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine MIME type
|
||||||
|
const mimetype = headers['content-type'] || 'application/octet-stream';
|
||||||
|
|
||||||
|
// Create Blob
|
||||||
|
const blob = new Blob([xhr.response], { type: mimetype });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Trigger download
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.style.display = "none";
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
link.remove();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.warn(`[htmx-download] Unexpected response status: ${xhr.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent htmx from swapping content
|
||||||
|
evt.detail.shouldSwap = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -117,37 +117,38 @@ extension DuctSizes: Content {}
|
|||||||
// FIX: Move
|
// FIX: Move
|
||||||
func handlePdf(_ projectID: Project.ID, on request: Request) async throws -> Response {
|
func handlePdf(_ projectID: Project.ID, on request: Request) async throws -> Response {
|
||||||
@Dependency(\.projectClient) var projectClient
|
@Dependency(\.projectClient) var projectClient
|
||||||
|
return try await projectClient.generatePdf(projectID, request.fileio)
|
||||||
|
|
||||||
let html = try await projectClient.toHTML(projectID)
|
// let html = try await projectClient.toHTML(projectID)
|
||||||
let url = "/tmp/\(projectID)"
|
// let url = "/tmp/\(projectID)"
|
||||||
try await request.fileio.writeFile(.init(string: html.render()), at: "\(url).html")
|
// try await request.fileio.writeFile(.init(string: html.render()), at: "\(url).html")
|
||||||
|
//
|
||||||
let process = Process()
|
// let process = Process()
|
||||||
let standardInput = Pipe()
|
// let standardInput = Pipe()
|
||||||
let standardOutput = Pipe()
|
// let standardOutput = Pipe()
|
||||||
process.standardInput = standardInput
|
// process.standardInput = standardInput
|
||||||
process.standardOutput = standardOutput
|
// process.standardOutput = standardOutput
|
||||||
process.executableURL = URL(fileURLWithPath: "/bin/pandoc")
|
// process.executableURL = URL(fileURLWithPath: "/bin/pandoc")
|
||||||
process.arguments = [
|
// process.arguments = [
|
||||||
"\(url).html",
|
// "\(url).html",
|
||||||
"--pdf-engine=weasyprint",
|
// "--pdf-engine=weasyprint",
|
||||||
"--from=html",
|
// "--from=html",
|
||||||
"--css=Public/css/pdf.css",
|
// "--css=Public/css/pdf.css",
|
||||||
"-o", "\(url).pdf",
|
// "-o", "\(url).pdf",
|
||||||
]
|
// ]
|
||||||
try process.run()
|
// try process.run()
|
||||||
process.waitUntilExit()
|
// process.waitUntilExit()
|
||||||
|
//
|
||||||
let response = try await request.fileio.asyncStreamFile(at: "\(url).pdf", mediaType: .pdf) { _ in
|
// let response = try await request.fileio.asyncStreamFile(at: "\(url).pdf", mediaType: .pdf) { _ in
|
||||||
// Remove files here.
|
// // Remove files here.
|
||||||
try FileManager.default.removeItem(atPath: "\(url).pdf")
|
// try FileManager.default.removeItem(atPath: "\(url).pdf")
|
||||||
try FileManager.default.removeItem(atPath: "\(url).html")
|
// try FileManager.default.removeItem(atPath: "\(url).html")
|
||||||
}
|
// }
|
||||||
response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream")
|
// response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream")
|
||||||
response.headers.replaceOrAdd(
|
// response.headers.replaceOrAdd(
|
||||||
name: .contentDisposition, value: "attachment; filename=Duct-Calc.pdf"
|
// name: .contentDisposition, value: "attachment; filename=Duct-Calc.pdf"
|
||||||
)
|
// )
|
||||||
return response
|
// return response
|
||||||
}
|
}
|
||||||
|
|
||||||
@Sendable
|
@Sendable
|
||||||
|
|||||||
74
Sources/EnvClient/Interface.swift
Normal file
74
Sources/EnvClient/Interface.swift
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension DependencyValues {
|
||||||
|
|
||||||
|
/// Holds values defined in the process environment that are needed.
|
||||||
|
///
|
||||||
|
/// These are generally loaded from a `.env` file, but also have default values,
|
||||||
|
/// if not found.
|
||||||
|
public var env: @Sendable () throws -> EnvVars {
|
||||||
|
get { self[EnvClient.self].env }
|
||||||
|
set { self[EnvClient.self].env = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DependencyClient
|
||||||
|
struct EnvClient: Sendable {
|
||||||
|
public var env: @Sendable () throws -> EnvVars
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds values defined in the process environment that are needed.
|
||||||
|
///
|
||||||
|
/// These are generally loaded from a `.env` file, but also have default values,
|
||||||
|
/// if not found.
|
||||||
|
public struct EnvVars: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
/// The path to the pandoc executable on the system, used to generate pdf's.
|
||||||
|
public let pandocPath: String
|
||||||
|
|
||||||
|
/// The pdf engine to use with pandoc when creating pdf's.
|
||||||
|
public let pdfEngine: String
|
||||||
|
|
||||||
|
public init(
|
||||||
|
pandocPath: String = "/bin/pandoc",
|
||||||
|
pdfEngine: String = "weasyprint"
|
||||||
|
) {
|
||||||
|
self.pandocPath = pandocPath
|
||||||
|
self.pdfEngine = pdfEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case pandocPath = "PANDOC_PATH"
|
||||||
|
case pdfEngine = "PDF_ENGINE"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvClient: DependencyKey {
|
||||||
|
static let testValue = Self()
|
||||||
|
|
||||||
|
static let liveValue = Self(env: {
|
||||||
|
// Convert default values into a dictionary.
|
||||||
|
let defaults =
|
||||||
|
(try? encoder.encode(EnvVars()))
|
||||||
|
.flatMap { try? decoder.decode([String: String].self, from: $0) }
|
||||||
|
?? [:]
|
||||||
|
|
||||||
|
// Merge the default values with values found in process environment.
|
||||||
|
let assigned = defaults.merging(ProcessInfo.processInfo.environment, uniquingKeysWith: { $1 })
|
||||||
|
|
||||||
|
return (try? JSONSerialization.data(withJSONObject: assigned))
|
||||||
|
.flatMap { try? decoder.decode(EnvVars.self, from: $0) }
|
||||||
|
?? .init()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private let encoder: JSONEncoder = {
|
||||||
|
JSONEncoder()
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let decoder: JSONDecoder = {
|
||||||
|
JSONDecoder()
|
||||||
|
}()
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import Dependencies
|
import Dependencies
|
||||||
import DependenciesMacros
|
import DependenciesMacros
|
||||||
import Elementary
|
import Elementary
|
||||||
|
import EnvClient
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
import ManualDCore
|
import ManualDCore
|
||||||
|
|
||||||
extension DependencyValues {
|
extension DependencyValues {
|
||||||
@@ -14,7 +17,13 @@ extension DependencyValues {
|
|||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct PdfClient: Sendable {
|
public struct PdfClient: Sendable {
|
||||||
public var html: @Sendable (Request) async throws -> (any HTML & Sendable)
|
public var html: @Sendable (Request) async throws -> (any HTML & Sendable)
|
||||||
public var markdown: @Sendable (Request) async throws -> String
|
public var generatePdf: @Sendable (Project.ID, any HTML & Sendable) async throws -> Response
|
||||||
|
|
||||||
|
public func generatePdf(request: Request) async throws -> Response {
|
||||||
|
let html = try await self.html(request)
|
||||||
|
return try await self.generatePdf(request.project.id, html)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PdfClient: DependencyKey {
|
extension PdfClient: DependencyKey {
|
||||||
@@ -24,8 +33,32 @@ extension PdfClient: DependencyKey {
|
|||||||
html: { request in
|
html: { request in
|
||||||
request.toHTML()
|
request.toHTML()
|
||||||
},
|
},
|
||||||
markdown: { request in
|
generatePdf: { projectID, html in
|
||||||
request.toMarkdown()
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
@Dependency(\.env) var env
|
||||||
|
|
||||||
|
let envVars = try env()
|
||||||
|
let baseUrl = "/tmp/\(projectID)"
|
||||||
|
try await fileClient.writeFile(html.render(), "\(baseUrl).html")
|
||||||
|
|
||||||
|
let process = Process()
|
||||||
|
let standardInput = Pipe()
|
||||||
|
let standardOutput = Pipe()
|
||||||
|
process.standardInput = standardInput
|
||||||
|
process.standardOutput = standardOutput
|
||||||
|
process.executableURL = URL(fileURLWithPath: envVars.pandocPath)
|
||||||
|
process.arguments = [
|
||||||
|
"\(baseUrl).html",
|
||||||
|
"--pdf-engine=\(envVars.pdfEngine)",
|
||||||
|
"--from=html",
|
||||||
|
"--css=Public/css/pdf.css",
|
||||||
|
"--output=\(baseUrl).pdf",
|
||||||
|
]
|
||||||
|
try process.run()
|
||||||
|
process.waitUntilExit()
|
||||||
|
|
||||||
|
return .init(htmlPath: "\(baseUrl).html", pdfPath: "\(baseUrl).pdf")
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -44,6 +77,10 @@ extension PdfClient {
|
|||||||
public let frictionRate: FrictionRate
|
public let frictionRate: FrictionRate
|
||||||
public let projectSHR: Double
|
public let projectSHR: Double
|
||||||
|
|
||||||
|
var totalEquivalentLength: Double {
|
||||||
|
maxReturnTEL.totalEquivalentLength + maxSupplyTEL.totalEquivalentLength
|
||||||
|
}
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
project: Project,
|
project: Project,
|
||||||
rooms: [Room],
|
rooms: [Room],
|
||||||
@@ -66,6 +103,17 @@ extension PdfClient {
|
|||||||
self.projectSHR = projectSHR
|
self.projectSHR = projectSHR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct Response: Equatable, Sendable {
|
||||||
|
|
||||||
|
public let htmlPath: String
|
||||||
|
public let pdfPath: String
|
||||||
|
|
||||||
|
public init(htmlPath: String, pdfPath: String) {
|
||||||
|
self.htmlPath = htmlPath
|
||||||
|
self.pdfPath = pdfPath
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ struct PdfDocument: HTMLDocument {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div(.class("section")) {
|
div(.class("section")) {
|
||||||
h2 { "Duct Sizes" }
|
h2 { "Duct Sizes" }
|
||||||
DuctSizesTable(rooms: request.ductSizes.rooms)
|
DuctSizesTable(rooms: request.ductSizes.rooms)
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import ManualDCore
|
|
||||||
|
|
||||||
extension PdfClient.Request {
|
|
||||||
|
|
||||||
func toMarkdown() -> String {
|
|
||||||
var retval = """
|
|
||||||
# Duct Calc
|
|
||||||
|
|
||||||
**Name:** \(project.name)
|
|
||||||
**Address:** \(project.streetAddress)
|
|
||||||
\(project.city), \(project.state) \(project.zipCode)
|
|
||||||
|
|
||||||
## Equipment
|
|
||||||
|
|
||||||
| | Value |
|
|
||||||
|:----------------|:--------------------------------|
|
|
||||||
| Static Pressure | \(equipmentInfo.staticPressure.string()) |
|
|
||||||
| Heating CFM | \(equipmentInfo.heatingCFM.string()) |
|
|
||||||
| Cooling CFM | \(equipmentInfo.coolingCFM.string()) |
|
|
||||||
|
|
||||||
## Friction Rate
|
|
||||||
|
|
||||||
| Component Loss | Value |
|
|
||||||
|:----------------|:--------------------------------|
|
|
||||||
|
|
||||||
"""
|
|
||||||
for row in componentLosses {
|
|
||||||
retval += "\(componentLossRow(row))\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
retval += """
|
|
||||||
|
|
||||||
|
|
||||||
| Results | Value |
|
|
||||||
|:-----------------|:---------------------------------|
|
|
||||||
| Available Static Pressure | \(frictionRate.availableStaticPressure.string()) |
|
|
||||||
| Total Equivalent Length | \(totalEquivalentLength.string()) |
|
|
||||||
| Friction Rate Design Value | \(frictionRate.value.string()) |
|
|
||||||
|
|
||||||
## Duct Sizes
|
|
||||||
|
|
||||||
| Register | Dsn CFM | Round Size | Velocity | Final Size | Flex Size | Height | Width |
|
|
||||||
|:---------|:--------|:----------------|:---------|:-----------|:----------|:-------|:------|
|
|
||||||
|
|
||||||
"""
|
|
||||||
for row in ductSizes.rooms {
|
|
||||||
retval += "\(registerRow(row))\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
retval += """
|
|
||||||
|
|
||||||
## Trunk Sizes
|
|
||||||
|
|
||||||
### Supply Trunks
|
|
||||||
|
|
||||||
| Name | Associated Supplies | Dsn CFM | Velocity | Final Size | Flex Size | Height | Width |
|
|
||||||
|:---------|:--------------------|:--------|:---------|:-----------|:----------|:-------|:------|
|
|
||||||
|
|
||||||
"""
|
|
||||||
for row in ductSizes.trunks.filter({ $0.type == .supply }) {
|
|
||||||
retval += "\(trunkRow(row))\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
retval += """
|
|
||||||
|
|
||||||
### Return Trunks / Run Outs
|
|
||||||
|
|
||||||
| Name | Associated Supplies | Dsn CFM | Velocity | Final Size | Flex Size | Height | Width |
|
|
||||||
|:---------|:--------------------|:--------|:---------|:-----------|:----------|:-------|:------|
|
|
||||||
|
|
||||||
"""
|
|
||||||
for row in ductSizes.trunks.filter({ $0.type == .return }) {
|
|
||||||
retval += "\(trunkRow(row))\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
return retval
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerRow(_ row: DuctSizes.RoomContainer) -> String {
|
|
||||||
return """
|
|
||||||
| \(row.roomName) | \(row.designCFM.value.string(digits: 0)) | \(row.roundSize.string()) | \(row.velocity.string()) | \(row.finalSize.string()) | \(row.flexSize.string()) | \(row.height?.string() ?? "") | \(row.width?.string() ?? "") |
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
|
|
||||||
func trunkRow(_ row: DuctSizes.TrunkContainer) -> String {
|
|
||||||
return """
|
|
||||||
| \(row.name ?? "") | \(associatedSupplyString(row)) | \(row.designCFM.value.string(digits: 0)) | \(row.roundSize.string()) | \(row.velocity.string()) | \(row.finalSize.string()) | \(row.flexSize.string()) | \(row.ductSize.height?.string() ?? "") | \(row.width?.string() ?? "") |
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
|
|
||||||
func componentLossRow(_ row: ComponentPressureLoss) -> String {
|
|
||||||
return """
|
|
||||||
| \(row.name) | \(row.value.string()) |
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalEquivalentLength: Double {
|
|
||||||
maxSupplyTEL.totalEquivalentLength + maxReturnTEL.totalEquivalentLength
|
|
||||||
}
|
|
||||||
|
|
||||||
func associatedSupplyString(_ row: DuctSizes.TrunkContainer) -> String {
|
|
||||||
row.associatedSupplyString(rooms: ductSizes.rooms)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DuctSizes.TrunkContainer {
|
|
||||||
|
|
||||||
func associatedSupplyString(rooms: [DuctSizes.RoomContainer]) -> String {
|
|
||||||
self.registerIDS(rooms: rooms)
|
|
||||||
.joined(separator: ", ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ import DependenciesMacros
|
|||||||
import Elementary
|
import Elementary
|
||||||
import ManualDClient
|
import ManualDClient
|
||||||
import ManualDCore
|
import ManualDCore
|
||||||
|
import Vapor
|
||||||
|
|
||||||
extension DependencyValues {
|
extension DependencyValues {
|
||||||
public var projectClient: ProjectClient {
|
public var projectClient: ProjectClient {
|
||||||
@@ -29,8 +30,10 @@ public struct ProjectClient: Sendable {
|
|||||||
public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse
|
public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse
|
||||||
|
|
||||||
// FIX: Name to something to do with generating a pdf, just experimenting now.
|
// FIX: Name to something to do with generating a pdf, just experimenting now.
|
||||||
public var toMarkdown: @Sendable (Project.ID) async throws -> String
|
// public var toMarkdown: @Sendable (Project.ID) async throws -> String
|
||||||
public var toHTML: @Sendable (Project.ID) async throws -> (any HTML & Sendable)
|
// public var toHTML: @Sendable (Project.ID) async throws -> (any HTML & Sendable)
|
||||||
|
|
||||||
|
public var generatePdf: @Sendable (Project.ID, FileIO) async throws -> Response
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProjectClient: TestDependencyKey {
|
extension ProjectClient: TestDependencyKey {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import DatabaseClient
|
import DatabaseClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
import Logging
|
import Logging
|
||||||
import ManualDClient
|
import ManualDClient
|
||||||
import ManualDCore
|
import ManualDCore
|
||||||
@@ -11,6 +12,7 @@ extension ProjectClient: DependencyKey {
|
|||||||
@Dependency(\.database) var database
|
@Dependency(\.database) var database
|
||||||
@Dependency(\.manualD) var manualD
|
@Dependency(\.manualD) var manualD
|
||||||
@Dependency(\.pdfClient) var pdfClient
|
@Dependency(\.pdfClient) var pdfClient
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
|
||||||
return .init(
|
return .init(
|
||||||
calculateDuctSizes: { projectID in
|
calculateDuctSizes: { projectID in
|
||||||
@@ -35,11 +37,21 @@ extension ProjectClient: DependencyKey {
|
|||||||
frictionRate: { projectID in
|
frictionRate: { projectID in
|
||||||
try await manualD.frictionRate(projectID: projectID)
|
try await manualD.frictionRate(projectID: projectID)
|
||||||
},
|
},
|
||||||
toMarkdown: { projectID in
|
generatePdf: { projectID, fileIO in
|
||||||
try await pdfClient.markdown(database.makePdfRequest(projectID))
|
let pdfResponse = try await pdfClient.generatePdf(
|
||||||
},
|
request: database.makePdfRequest(projectID))
|
||||||
toHTML: { projectID in
|
|
||||||
try await pdfClient.html(database.makePdfRequest(projectID))
|
let response = try await fileIO.asyncStreamFile(at: pdfResponse.pdfPath) { _ in
|
||||||
|
try await fileClient.removeFile(pdfResponse.htmlPath)
|
||||||
|
try await fileClient.removeFile(pdfResponse.pdfPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream")
|
||||||
|
response.headers.replaceOrAdd(
|
||||||
|
name: .contentDisposition, value: "attachment; filename=Duct-Calc.pdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -48,31 +60,6 @@ extension ProjectClient: DependencyKey {
|
|||||||
|
|
||||||
extension DatabaseClient {
|
extension DatabaseClient {
|
||||||
|
|
||||||
// fileprivate func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
|
|
||||||
// @Dependency(\.manualD) var manualD
|
|
||||||
//
|
|
||||||
// guard let project = try await projects.get(projectID) else {
|
|
||||||
// throw ProjectClientError("Project not found. id: \(projectID)")
|
|
||||||
// }
|
|
||||||
// let frictionRateResponse = try await manualD.frictionRate(projectID: projectID)
|
|
||||||
// guard let frictionRate = frictionRateResponse.frictionRate else {
|
|
||||||
// throw ProjectClientError("Friction rate not found. id: \(projectID)")
|
|
||||||
// }
|
|
||||||
// let (ductSizes, sharedInfo, rooms) = try await calculateDuctSizes(projectID: projectID)
|
|
||||||
//
|
|
||||||
// return .init(
|
|
||||||
// project: project,
|
|
||||||
// rooms: rooms,
|
|
||||||
// componentLosses: frictionRateResponse.componentLosses,
|
|
||||||
// ductSizes: ductSizes,
|
|
||||||
// equipmentInfo: sharedInfo.equipmentInfo,
|
|
||||||
// maxSupplyTEL: sharedInfo.maxSupplyLength,
|
|
||||||
// maxReturnTEL: sharedInfo.maxReturnLenght,
|
|
||||||
// frictionRate: frictionRate,
|
|
||||||
// projectSHR: sharedInfo.projectSHR
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
fileprivate func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
|
fileprivate func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
|
||||||
@Dependency(\.manualD) var manualD
|
@Dependency(\.manualD) var manualD
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,6 @@ extension HTMLAttribute.hx {
|
|||||||
extension HTMLAttribute.hx {
|
extension HTMLAttribute.hx {
|
||||||
@Sendable
|
@Sendable
|
||||||
public static func indicator() -> HTMLAttribute {
|
public static func indicator() -> HTMLAttribute {
|
||||||
indicator(".hx-indicator")
|
indicator(".htmx-indicator")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,9 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
case .pdf:
|
case .pdf:
|
||||||
// FIX: This should return a pdf to download or be wrapped in a
|
// FIX: This should return a pdf to download or be wrapped in a
|
||||||
// result view.
|
// result view.
|
||||||
return try! await projectClient.toHTML(projectID)
|
// return try! await projectClient.toHTML(projectID)
|
||||||
|
// This get's handled elsewhere because it returns a response, not a view.
|
||||||
|
fatalError()
|
||||||
case .rooms(let route):
|
case .rooms(let route):
|
||||||
return await route.renderView(on: request, projectID: projectID)
|
return await route.renderView(on: request, projectID: projectID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,20 @@ struct DuctSizingView: HTML, Sendable {
|
|||||||
.attributes(.class("text-error font-bold italic mt-4"))
|
.attributes(.class("text-error font-bold italic mt-4"))
|
||||||
}
|
}
|
||||||
|
|
||||||
a(
|
div {
|
||||||
.class("btn btn-primary"),
|
button(
|
||||||
.href(route: .project(.detail(projectID, .pdf)))
|
.class("btn btn-primary"),
|
||||||
) {
|
.hx.get(route: .project(.detail(projectID, .pdf))),
|
||||||
"PDF"
|
.hx.ext("htmx-download"),
|
||||||
|
.hx.swap(.none),
|
||||||
|
.hx.indicator()
|
||||||
|
) {
|
||||||
|
span { "PDF" }
|
||||||
|
Indicator()
|
||||||
|
}
|
||||||
|
// div {
|
||||||
|
// Indicator()
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable
|
|||||||
meta(.content("1024"), .name("og:image:height"))
|
meta(.content("1024"), .name("og:image:height"))
|
||||||
meta(.content(keywords), .name(.keywords))
|
meta(.content(keywords), .name(.keywords))
|
||||||
script(.src("https://unpkg.com/htmx.org@2.0.8")) {}
|
script(.src("https://unpkg.com/htmx.org@2.0.8")) {}
|
||||||
|
script(.src("/js/htmx-download.js")) {}
|
||||||
script(.src("/js/main.js")) {}
|
script(.src("/js/main.js")) {}
|
||||||
link(.rel(.stylesheet), .href("/css/output.css"))
|
link(.rel(.stylesheet), .href("/css/output.css"))
|
||||||
|
link(.rel(.stylesheet), .href("/css/htmx.css"))
|
||||||
link(
|
link(
|
||||||
.rel(.icon),
|
.rel(.icon),
|
||||||
.href("/images/favicon.ico"),
|
.href("/images/favicon.ico"),
|
||||||
|
|||||||
Reference in New Issue
Block a user