Improve asset handling
This commit is contained in:
parent
31d1ecb8bd
commit
b22b76fd32
CHDataManagement.xcodeproj
CHDataManagement
Generator
FeedPageGenerator.swiftHeaderElement.swiftKnownHeaderElement.swiftLocalizedWebsiteGenerator.swiftPageGenerationResults.swiftPageGenerator.swift
Main
Model
Page Elements/ContentElements/AudioPlayer
Pages
Views
@ -127,7 +127,7 @@
|
||||
E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317C2D086AAE0051B7F4 /* Int+Random.swift */; };
|
||||
E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */; };
|
||||
E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */; };
|
||||
E29D31852D0AE8EE0051B7F4 /* RequiredHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */; };
|
||||
E29D31852D0AE8EE0051B7F4 /* KnownHeaderElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31842D0AE8EE0051B7F4 /* KnownHeaderElement.swift */; };
|
||||
E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */; };
|
||||
E29D318B2D0B07EE0051B7F4 /* ContentBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318A2D0B07E60051B7F4 /* ContentBox.swift */; };
|
||||
E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */; };
|
||||
@ -311,7 +311,7 @@
|
||||
E29D317C2D086AAE0051B7F4 /* Int+Random.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Random.swift"; sourceTree = "<group>"; };
|
||||
E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsIcons.swift; sourceTree = "<group>"; };
|
||||
E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedPageLink.swift; sourceTree = "<group>"; };
|
||||
E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredHeaders.swift; sourceTree = "<group>"; };
|
||||
E29D31842D0AE8EE0051B7F4 /* KnownHeaderElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnownHeaderElement.swift; sourceTree = "<group>"; };
|
||||
E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelViewer.swift; sourceTree = "<group>"; };
|
||||
E29D318A2D0B07E60051B7F4 /* ContentBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBox.swift; sourceTree = "<group>"; };
|
||||
E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsContentView.swift; sourceTree = "<group>"; };
|
||||
@ -452,7 +452,7 @@
|
||||
E22990232D0EDBD0009F8D77 /* HeaderElement.swift */,
|
||||
E29D31B62D0DAC030051B7F4 /* Page Content */,
|
||||
E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */,
|
||||
E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */,
|
||||
E29D31842D0AE8EE0051B7F4 /* KnownHeaderElement.swift */,
|
||||
E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */,
|
||||
E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */,
|
||||
E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */,
|
||||
@ -921,7 +921,7 @@
|
||||
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */,
|
||||
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */,
|
||||
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */,
|
||||
E29D31852D0AE8EE0051B7F4 /* RequiredHeaders.swift in Sources */,
|
||||
E29D31852D0AE8EE0051B7F4 /* KnownHeaderElement.swift in Sources */,
|
||||
E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */,
|
||||
E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */,
|
||||
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
|
||||
|
@ -8,27 +8,12 @@ final class FeedPageGenerator {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var swiperIncludes: [HeaderElement] {
|
||||
var result = [HeaderElement]()
|
||||
private func includeSwiper(in headers: inout Set<HeaderElement>) {
|
||||
if let swiperCss = content.settings.posts.swiperCssFile {
|
||||
result.append(.css(swiperCss))
|
||||
} else {
|
||||
#warning("Add warning message")
|
||||
headers.insert(.css(file: swiperCss, order: HeaderElement.swiperCssFileOrder))
|
||||
}
|
||||
if let swiperJs = content.settings.posts.swiperJsFile {
|
||||
result.append(.js(file: swiperJs, defer: true))
|
||||
} else {
|
||||
#warning("Add warning message")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var defaultHeaders: [HeaderElement] {
|
||||
if let header = content.settings.posts.defaultCssFile {
|
||||
return [.css(header)]
|
||||
} else {
|
||||
#warning("Add warning message")
|
||||
return []
|
||||
headers.insert(.js(file: swiperJs, defer: true))
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,10 +24,11 @@ final class FeedPageGenerator {
|
||||
showTitle: Bool,
|
||||
pageNumber: Int,
|
||||
totalPages: Int) -> String {
|
||||
var headers = defaultHeaders
|
||||
var headers = content.defaultPageHeaders
|
||||
var footer = ""
|
||||
if posts.contains(where: { $0.images.count > 1 }) {
|
||||
headers += swiperIncludes
|
||||
// Sort swiper style sheet before default style sheet
|
||||
includeSwiper(in: &headers)
|
||||
footer = swiperInitScript(posts: posts)
|
||||
}
|
||||
|
||||
@ -68,15 +54,14 @@ final class FeedPageGenerator {
|
||||
}
|
||||
|
||||
func swiperInitScript(posts: [FeedEntryData]) -> String {
|
||||
var result = "<script>"
|
||||
var result = "<script> window.onload = () => { "
|
||||
for post in posts {
|
||||
guard post.images.count > 1 else {
|
||||
continue
|
||||
}
|
||||
result += ImageGallery.swiperInit(id: post.entryId)
|
||||
}
|
||||
result += "</script>"
|
||||
result += "}; </script>"
|
||||
return result
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +1,135 @@
|
||||
|
||||
#warning("Add remaining header elements")
|
||||
// <meta name="msapplication-TileColor" content="#da532c">
|
||||
// <meta name="theme-color" content="#ffffff">
|
||||
// <meta name="msapplication-config" content="/assets/icons/browserconfig.xml?v=1">
|
||||
|
||||
// TODO: Move to settings?
|
||||
extension HeaderElement {
|
||||
|
||||
static let swiperCssFileOrder = 41
|
||||
|
||||
static let defaultCssFileOrder = 42
|
||||
|
||||
static let audioPlayerCssOrder = 43
|
||||
}
|
||||
|
||||
enum HeaderElement {
|
||||
case css(FileResource)
|
||||
|
||||
/// Order: 10-19
|
||||
case icon(file: FileResource, size: Int, rel: String)
|
||||
|
||||
/// Order: From 40-99, lower numbers appear first
|
||||
case css(file: FileResource, order: Int)
|
||||
|
||||
/// Order: 20-29
|
||||
case js(file: FileResource, defer: Bool)
|
||||
|
||||
/// Order: 30-39
|
||||
case jsModule(FileResource)
|
||||
|
||||
case author(String)
|
||||
|
||||
case title(String)
|
||||
|
||||
case description(String)
|
||||
|
||||
case charset
|
||||
|
||||
case viewport
|
||||
|
||||
case robots
|
||||
|
||||
var order: Int {
|
||||
switch self {
|
||||
case .charset:
|
||||
return 1
|
||||
case .robots:
|
||||
return 2
|
||||
case .viewport:
|
||||
return 3
|
||||
case .icon:
|
||||
return 10
|
||||
case .css(_, let order):
|
||||
return order
|
||||
case .js:
|
||||
return 20
|
||||
case .jsModule:
|
||||
return 30
|
||||
case .author:
|
||||
return 100
|
||||
case .title:
|
||||
return 101
|
||||
case .description:
|
||||
return 102
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension HeaderElement: Hashable {
|
||||
|
||||
}
|
||||
|
||||
extension HeaderElement: Comparable {
|
||||
|
||||
static func < (lhs: HeaderElement, rhs: HeaderElement) -> Bool {
|
||||
lhs.order < rhs.order
|
||||
}
|
||||
}
|
||||
|
||||
extension HeaderElement {
|
||||
|
||||
var content: String {
|
||||
switch self {
|
||||
case .css(let file):
|
||||
case .icon(let file, let size, let rel):
|
||||
return "<link rel='\(rel)' sizes='\(size)x\(size)' href='\(file.assetUrl)'>"
|
||||
case .css(let file, _):
|
||||
return "<link rel='stylesheet' href='\(file.assetUrl)' />"
|
||||
case .js(let file, let deferred):
|
||||
let deferText = deferred ? " defer" : ""
|
||||
return "<script src='\(file.assetUrl)'\(deferText)></script>"
|
||||
case .jsModule(let file):
|
||||
return "<script type='module' src='\(file.assetUrl)'></script>"
|
||||
case .author(let author):
|
||||
return "<meta name='author' content='\(author)'>"
|
||||
case .title(let title):
|
||||
return "<title>\(title)</title>"
|
||||
case .description(let description):
|
||||
return "<meta name='description' content='\(description)'>"
|
||||
return "<meta name='description' content=\"\(description)\">"
|
||||
case .charset:
|
||||
return "<meta charset='utf-8' />"
|
||||
case .viewport:
|
||||
return "<meta name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1' />"
|
||||
case .robots:
|
||||
return "<meta name='robots' content='noindex'>"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension HeaderElement: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .icon(let file, _, _):
|
||||
return file.description
|
||||
case .css(let file, _):
|
||||
return file.description
|
||||
case .js(let file, let deferred):
|
||||
return file.description + (deferred ? " (deferred)" : "")
|
||||
case .jsModule(let file):
|
||||
return file.description
|
||||
case .author:
|
||||
return "author"
|
||||
case .title:
|
||||
return "title"
|
||||
case .description:
|
||||
return "description"
|
||||
case .charset:
|
||||
return "charset"
|
||||
case .viewport:
|
||||
return "viewport"
|
||||
case .robots:
|
||||
return "robots"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
enum HeaderFile: Int {
|
||||
enum KnownHeaderElement: Int {
|
||||
|
||||
case codeHightlighting = 4
|
||||
|
||||
@ -23,7 +23,7 @@ enum HeaderFile: Int {
|
||||
}
|
||||
case .audioPlayerCss:
|
||||
if let file = content.settings.pages.audioPlayerCssFile {
|
||||
return .css(file)
|
||||
return .css(file: file, order: HeaderElement.audioPlayerCssOrder)
|
||||
}
|
||||
case .audioPlayerJs:
|
||||
if let file = content.settings.pages.audioPlayerJsFile {
|
||||
@ -34,13 +34,27 @@ enum HeaderFile: Int {
|
||||
}
|
||||
}
|
||||
|
||||
extension HeaderFile: Comparable {
|
||||
extension KnownHeaderElement: Comparable {
|
||||
|
||||
static func < (lhs: HeaderFile, rhs: HeaderFile) -> Bool {
|
||||
static func < (lhs: KnownHeaderElement, rhs: KnownHeaderElement) -> Bool {
|
||||
lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
typealias RequiredHeaders = Set<HeaderFile>
|
||||
extension KnownHeaderElement: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .codeHightlighting:
|
||||
return "code-highlighting"
|
||||
case .modelViewer:
|
||||
return "model-viewer"
|
||||
case .audioPlayerCss:
|
||||
return "audio-player-css"
|
||||
case .audioPlayerJs:
|
||||
return "audio-player-js"
|
||||
}
|
||||
}
|
||||
}
|
@ -2,10 +2,23 @@ import Foundation
|
||||
|
||||
final class LocalizedWebsiteGenerator {
|
||||
|
||||
private let content: Content
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
private let imageGenerator: ImageGenerator
|
||||
|
||||
private let localizedPostSettings: LocalizedPostSettings
|
||||
|
||||
init(content: Content, language: ContentLanguage) {
|
||||
self.language = language
|
||||
self.content = content
|
||||
self.localizedPostSettings = content.settings.localized(in: language)
|
||||
self.imageGenerator = ImageGenerator(
|
||||
storage: content.storage,
|
||||
relativeImageOutputPath: content.settings.paths.imagesOutputFolderPath)
|
||||
}
|
||||
|
||||
private var outputDirectory: URL {
|
||||
content.settings.outputDirectory
|
||||
}
|
||||
@ -18,19 +31,6 @@ final class LocalizedWebsiteGenerator {
|
||||
CGFloat(content.settings.posts.contentWidth)
|
||||
}
|
||||
|
||||
private let content: Content
|
||||
|
||||
private let imageGenerator: ImageGenerator
|
||||
|
||||
init(content: Content, language: ContentLanguage) {
|
||||
self.language = language
|
||||
self.content = content
|
||||
self.localizedPostSettings = content.settings.localized(in: language)
|
||||
self.imageGenerator = ImageGenerator(
|
||||
storage: content.storage,
|
||||
relativeImageOutputPath: content.settings.paths.imagesOutputFolderPath)
|
||||
}
|
||||
|
||||
func generateWebsite(callback: (String) -> Void) -> Bool {
|
||||
guard imageGenerator.prepareForGeneration() else {
|
||||
return false
|
||||
|
@ -45,7 +45,7 @@ final class PageGenerationResults: ObservableObject {
|
||||
var invalidCommands: [(command: ShorthandMarkdownKey?, markdown: String)] = []
|
||||
|
||||
@Published
|
||||
var requiredHeaders: RequiredHeaders = []
|
||||
var requiredHeaders: Set<KnownHeaderElement> = []
|
||||
|
||||
@Published
|
||||
var requiredFooters: Set<String> = []
|
||||
|
@ -9,14 +9,15 @@ final class PageGenerator {
|
||||
self.imageGenerator = imageGenerator
|
||||
}
|
||||
|
||||
func makeHeaders(requiredItems: [HeaderFile]) -> [HeaderElement] {
|
||||
var result = [HeaderElement]()
|
||||
private func makeHeaders(requiredItems: Set<KnownHeaderElement>) -> Set<HeaderElement> {
|
||||
var result = content.defaultPageHeaders
|
||||
for item in requiredItems {
|
||||
guard let header = item.header(content: content) else {
|
||||
print("Missing header \(item)")
|
||||
#warning("Add warning on missing file assignment")
|
||||
continue
|
||||
}
|
||||
result.append(header)
|
||||
result.insert(header)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -39,7 +40,8 @@ final class PageGenerator {
|
||||
url: content.absoluteUrlToTag(tag, language: language))
|
||||
}
|
||||
|
||||
let headers = makeHeaders(requiredItems: contentGenerator.results.requiredHeaders.sorted())
|
||||
let headers = makeHeaders(requiredItems: contentGenerator.results.requiredHeaders)
|
||||
print("Headers for page: \(headers)")
|
||||
|
||||
let fullPage = ContentPage(
|
||||
language: language,
|
||||
|
@ -16,6 +16,7 @@ import SFSafeSymbols
|
||||
#warning("Generate pages for posts")
|
||||
#warning("Clean up mock content")
|
||||
#warning("Show posts linking to a page")
|
||||
#warning("Add author to settings and page headers")
|
||||
|
||||
@main
|
||||
struct MainView: App {
|
||||
|
@ -1,5 +1,54 @@
|
||||
import Foundation
|
||||
|
||||
extension Content {
|
||||
|
||||
func generatePage(_ page: Page) -> Bool {
|
||||
guard startGenerating() else { return false }
|
||||
defer { endGenerating() }
|
||||
|
||||
for language in ContentLanguage.allCases {
|
||||
guard generateInternal(page, in: language) else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func generatePage(_ page: Page, in language: ContentLanguage) -> Bool {
|
||||
guard startGenerating() else { return false }
|
||||
defer { endGenerating() }
|
||||
return generateInternal(page, in: language)
|
||||
}
|
||||
|
||||
private func startGenerating() -> Bool {
|
||||
guard !isGeneratingWebsite else {
|
||||
return false
|
||||
}
|
||||
// TODO: Fix bug where multiple generating operations can be started
|
||||
// due to dispatch of locking property on main queue
|
||||
DispatchQueue.main.async {
|
||||
self.isGeneratingWebsite = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func endGenerating() {
|
||||
DispatchQueue.main.async {
|
||||
self.isGeneratingWebsite = false
|
||||
}
|
||||
}
|
||||
|
||||
private func generateInternal(_ page: Page, in language: ContentLanguage) -> Bool {
|
||||
let generator = LocalizedWebsiteGenerator(
|
||||
content: self,
|
||||
language: language)
|
||||
if !generator.generate(page: page) {
|
||||
print("Generation failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func makeCleanAbsolutePath(_ path: String) -> String {
|
||||
("/" + path).replacingOccurrences(of: "//", with: "/")
|
||||
}
|
||||
@ -36,10 +85,20 @@ extension Content {
|
||||
tags.first { $0.id == tagId }
|
||||
}
|
||||
|
||||
// MARK: Generation input
|
||||
|
||||
func navigationBar(in language: ContentLanguage) -> [NavigationBar.Link] {
|
||||
settings.navigationItems.map {
|
||||
.init(text: $0.title(in: language),
|
||||
url: $0.absoluteUrl(in: language))
|
||||
}
|
||||
}
|
||||
|
||||
var defaultPageHeaders: Set<HeaderElement> {
|
||||
var result: Set<HeaderElement> = [.charset, .viewport]
|
||||
if let defaultCss = settings.posts.defaultCssFile {
|
||||
result.insert(.css(file: defaultCss, order: HeaderElement.defaultCssFileOrder))
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,9 @@ final class Content: ObservableObject {
|
||||
@Published
|
||||
var results: [ItemId : PageGenerationResults] = [:]
|
||||
|
||||
@Published
|
||||
var isGeneratingWebsite = false
|
||||
|
||||
@AppStorage("contentPath")
|
||||
private var storedContentPath: String = ""
|
||||
|
||||
|
@ -137,3 +137,10 @@ final class FileResource: Item {
|
||||
extension FileResource: LocalizedItem {
|
||||
|
||||
}
|
||||
|
||||
extension FileResource: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ final class PageSettings: ObservableObject {
|
||||
defaultCssFile: defaultCssFile?.id,
|
||||
codeHighlightingJsFile: codeHighlightingJsFile?.id,
|
||||
audioPlayerJsFile: audioPlayerJsFile?.id,
|
||||
audioPlayerCssFile: audioPlayerJsFile?.id,
|
||||
audioPlayerCssFile: audioPlayerCssFile?.id,
|
||||
modelViewerJsFile: modelViewerJsFile?.id)
|
||||
}
|
||||
}
|
||||
|
@ -18,11 +18,11 @@ struct AudioPlayerScript: HtmlProducer {
|
||||
}
|
||||
|
||||
func populate(_ result: inout String) {
|
||||
result += "<script>\n"
|
||||
result += "<script>window.onload = () => { "
|
||||
result += "Amplitude.init({ songs: "
|
||||
let songData = try! JSONEncoder().encode(items)
|
||||
result += String(data: songData, encoding: .utf8)!
|
||||
result += "});"
|
||||
result += "}); }; " // Close Amplitude.init and window.onload
|
||||
result += "function playEntry(index) { Amplitude.playSongAtIndex(index) };"
|
||||
result += animatePlaylist
|
||||
result += "</script>"
|
||||
|
@ -24,7 +24,7 @@ struct ContentPage: HtmlProducer {
|
||||
|
||||
private let icons: Set<PageIcon>
|
||||
|
||||
init(language: ContentLanguage, dateString: String, title: String, tags: [FeedEntryData.Tag], linkTitle: String, description: String, navigationBarLinks: [NavigationBar.Link], pageContent: String, headers: [HeaderElement], footers: [String], icons: Set<PageIcon>) {
|
||||
init(language: ContentLanguage, dateString: String, title: String, tags: [FeedEntryData.Tag], linkTitle: String, description: String, navigationBarLinks: [NavigationBar.Link], pageContent: String, headers: Set<HeaderElement>, footers: [String], icons: Set<PageIcon>) {
|
||||
self.language = language
|
||||
self.dateString = dateString
|
||||
self.title = title
|
||||
@ -33,7 +33,7 @@ struct ContentPage: HtmlProducer {
|
||||
self.description = description
|
||||
self.navigationBarLinks = navigationBarLinks
|
||||
self.pageContent = pageContent
|
||||
self.headers = headers
|
||||
self.headers = headers.union([.title(title), .description(description)]).sorted()
|
||||
self.footers = footers.joined()
|
||||
self.icons = icons
|
||||
}
|
||||
@ -41,7 +41,7 @@ struct ContentPage: HtmlProducer {
|
||||
func populate(_ result: inout String) {
|
||||
// TODO: Add headers and footers from page content
|
||||
result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">"
|
||||
result += PageHead(items: [.charset, .viewport] + headers).content
|
||||
result += PageHead(items: headers).content
|
||||
result += "<body>"
|
||||
result += NavigationBar(links: navigationBarLinks).content
|
||||
|
||||
|
@ -16,19 +16,21 @@ struct GenericPage {
|
||||
|
||||
let insertedContent: (inout String) -> Void
|
||||
|
||||
init(language: ContentLanguage, title: String, description: String, links: [NavigationBar.Link], headers: [HeaderElement], additionalFooter: String, insertedContent: @escaping (inout String) -> Void) {
|
||||
init(language: ContentLanguage, title: String, description: String, links: [NavigationBar.Link], headers: Set<HeaderElement>, additionalFooter: String, insertedContent: @escaping (inout String) -> Void) {
|
||||
self.language = language
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.links = links
|
||||
self.headers = headers
|
||||
self.headers = headers.union([.title(title), .description(description)]).sorted()
|
||||
self.additionalFooter = additionalFooter
|
||||
self.insertedContent = insertedContent
|
||||
}
|
||||
|
||||
var content: String {
|
||||
#warning("Consolidate this code with ContentPage")
|
||||
var result = ""
|
||||
result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">"
|
||||
result += PageHead(items: [.charset, .viewport] + headers).content
|
||||
result += PageHead(items: headers).content
|
||||
result += "<body>"
|
||||
result += NavigationBar(links: links).content
|
||||
result += "<div class=\"content\"><div style=\"height: 70px;\"></div>"
|
||||
|
@ -9,6 +9,15 @@ struct FilePropertyView: View {
|
||||
@Binding
|
||||
var selectedFile: FileResource?
|
||||
|
||||
let allowedType: FileFilterType?
|
||||
|
||||
init(title: LocalizedStringKey, footer: LocalizedStringKey, selectedFile: Binding<FileResource?>, allowedType: FileFilterType? = nil) {
|
||||
self.title = title
|
||||
self.footer = footer
|
||||
self._selectedFile = selectedFile
|
||||
self.allowedType = allowedType
|
||||
}
|
||||
|
||||
@State
|
||||
private var showFileSelectionSheet = false
|
||||
|
||||
@ -23,7 +32,7 @@ struct FilePropertyView: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showFileSelectionSheet) {
|
||||
FileSelectionView(selectedFile: $selectedFile)
|
||||
FileSelectionView(selectedFile: $selectedFile, allowedType: allowedType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,9 +12,6 @@ struct PageDetailView: View {
|
||||
@ObservedObject
|
||||
private var page: Page
|
||||
|
||||
@State
|
||||
private var isGeneratingWebsite = false
|
||||
|
||||
@State
|
||||
private var didGenerateWebsite: Bool?
|
||||
|
||||
@ -28,11 +25,11 @@ struct PageDetailView: View {
|
||||
DetailTitle(
|
||||
title: "Page",
|
||||
text: "A page contains longer content")
|
||||
HStack {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Button(action: generate) {
|
||||
Text("Generate")
|
||||
}
|
||||
.disabled(isGeneratingWebsite)
|
||||
.disabled(content.isGeneratingWebsite)
|
||||
if let didGenerateWebsite {
|
||||
if didGenerateWebsite {
|
||||
Image(systemSymbol: .checkmarkCircleFill)
|
||||
@ -93,20 +90,9 @@ struct PageDetailView: View {
|
||||
print("Missing output folder")
|
||||
return
|
||||
}
|
||||
isGeneratingWebsite = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
var success = true
|
||||
for language in ContentLanguage.allCases {
|
||||
let generator = LocalizedWebsiteGenerator(
|
||||
content: content,
|
||||
language: language)
|
||||
if !generator.generate(page: page) {
|
||||
print("Generation failed")
|
||||
success = false
|
||||
}
|
||||
}
|
||||
let success = content.generatePage(page)
|
||||
DispatchQueue.main.async {
|
||||
isGeneratingWebsite = false
|
||||
didGenerateWebsite = success
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ struct NavigationBarSettingsView: View {
|
||||
Text("Select the tags to show in the navigation bar. The number should be even.")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.sheet(isPresented: $showItemPicker) {
|
||||
ItemSelectionView(
|
||||
|
@ -33,28 +33,34 @@ struct PageSettingsDetailView: View {
|
||||
FilePropertyView(
|
||||
title: "Default CSS File",
|
||||
footer: "The CSS file containing the styling of all pages",
|
||||
selectedFile: $content.settings.pages.defaultCssFile)
|
||||
selectedFile: $content.settings.pages.defaultCssFile,
|
||||
allowedType: .text)
|
||||
|
||||
FilePropertyView(
|
||||
title: "Code Highlighting File",
|
||||
footer: "The JavaScript file to provide syntax highlighting of code blocks",
|
||||
selectedFile: $content.settings.pages.codeHighlightingJsFile)
|
||||
selectedFile: $content.settings.pages.codeHighlightingJsFile,
|
||||
allowedType: .text)
|
||||
|
||||
FilePropertyView(
|
||||
title: "Audio Player CSS File",
|
||||
footer: "The CSS file to provide the style for the audio player",
|
||||
selectedFile: $content.settings.pages.audioPlayerCssFile)
|
||||
selectedFile: $content.settings.pages.audioPlayerCssFile,
|
||||
allowedType: .text)
|
||||
|
||||
FilePropertyView(
|
||||
title: "Audio Player JavaScript File",
|
||||
footer: "The CSS file to provide the functionality for the audio player",
|
||||
selectedFile: $content.settings.pages.audioPlayerJsFile)
|
||||
selectedFile: $content.settings.pages.audioPlayerJsFile,
|
||||
allowedType: .text)
|
||||
|
||||
FilePropertyView(
|
||||
title: "3D Model Viewer File",
|
||||
footer: "The JavaScript file to provide the functionality for the 3D model viewer",
|
||||
selectedFile: $content.settings.pages.modelViewerJsFile)
|
||||
selectedFile: $content.settings.pages.modelViewerJsFile,
|
||||
allowedType: .text)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ struct PostFeedSettingsView: View {
|
||||
LocalizedPostFeedSettingsView(
|
||||
settings: content.settings.localized(in: language))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,5 +70,6 @@ private struct TagOverviewDetails: View {
|
||||
text: $page.linkPreviewDescription,
|
||||
footer: "The description to show in previews of the page")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user