Full generation, file type cleanup
This commit is contained in:
@ -24,7 +24,7 @@ struct FileContentView: View {
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
switch file.type {
|
||||
switch file.type.category {
|
||||
case .image:
|
||||
file.imageToDisplay
|
||||
.resizable()
|
||||
@ -39,7 +39,7 @@ struct FileContentView: View {
|
||||
.font(.title)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
case .text, .code:
|
||||
case .text, .code, .asset:
|
||||
TextFileContentView(file: file)
|
||||
.id(file.id)
|
||||
case .video:
|
||||
@ -52,7 +52,7 @@ struct FileContentView: View {
|
||||
.font(.title)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
case .other:
|
||||
case .resource:
|
||||
VStack {
|
||||
Image(systemSymbol: .docQuestionmark)
|
||||
.resizable()
|
||||
@ -62,6 +62,16 @@ struct FileContentView: View {
|
||||
.font(.title)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
case .audio:
|
||||
VStack {
|
||||
Image(systemSymbol: .waveformPath)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: iconSize)
|
||||
Text("No preview available")
|
||||
.font(.title)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}.padding()
|
||||
|
130
CHDataManagement/Views/Files/MultiFileSelectionView.swift
Normal file
130
CHDataManagement/Views/Files/MultiFileSelectionView.swift
Normal file
@ -0,0 +1,130 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MultiFileSelectionView: View {
|
||||
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@Binding
|
||||
private var selectedFiles: [FileResource]
|
||||
|
||||
let allowedType: FileFilterType?
|
||||
|
||||
let insertSorted: Bool
|
||||
|
||||
@State
|
||||
private var selectedFileType: FileFilterType
|
||||
|
||||
@State
|
||||
private var searchString = ""
|
||||
|
||||
@State
|
||||
private var newSelection: [FileResource]
|
||||
|
||||
init(selectedFiles: Binding<[FileResource]>, allowedType: FileFilterType? = nil, insertSorted: Bool = false) {
|
||||
self._selectedFiles = selectedFiles
|
||||
self.newSelection = selectedFiles.wrappedValue
|
||||
self.allowedType = allowedType
|
||||
self.selectedFileType = allowedType ?? .images
|
||||
self.insertSorted = insertSorted
|
||||
}
|
||||
|
||||
private var filesBySelectedType: [FileResource] {
|
||||
content.files.filter { selectedFileType.matches($0.type) }
|
||||
}
|
||||
|
||||
private var filteredFiles: [FileResource] {
|
||||
guard !searchString.isEmpty else {
|
||||
return filesBySelectedType
|
||||
}
|
||||
return filesBySelectedType.filter { $0.id.contains(searchString) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack {
|
||||
Text("Selected files")
|
||||
.font(.title)
|
||||
List {
|
||||
ForEach(newSelection) { file in
|
||||
HStack {
|
||||
Image(systemSymbol: .minusCircleFill)
|
||||
.foregroundStyle(.red)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { deselect(file: file) }
|
||||
Text(file.id)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.onMove(perform: moveSelectedFile)
|
||||
}
|
||||
HStack {
|
||||
Button("Cancel") {
|
||||
DispatchQueue.main.async {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Button("Save") {
|
||||
selectedFiles = newSelection
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
VStack {
|
||||
Picker("", selection: $selectedFileType) {
|
||||
ForEach(FileFilterType.allCases) { type in
|
||||
Text(type.text).tag(type)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.trailing, 7)
|
||||
.disabled(allowedType != nil)
|
||||
TextField("", text: $searchString, prompt: Text("Search"))
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.horizontal, 8)
|
||||
List(filteredFiles) { file in
|
||||
HStack {
|
||||
if newSelection.contains(file) {
|
||||
Image(systemSymbol: .checkmarkCircleFill)
|
||||
.foregroundStyle(.gray)
|
||||
} else {
|
||||
Image(systemSymbol: .plusCircleFill)
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
Text(file.id)
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { select(file: file) }
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 500, idealHeight: 600)
|
||||
.padding()
|
||||
}
|
||||
|
||||
private func deselect(file: FileResource) {
|
||||
guard let index = newSelection.firstIndex(of: file) else {
|
||||
return
|
||||
}
|
||||
newSelection.remove(at: index)
|
||||
}
|
||||
|
||||
private func select(file: FileResource) {
|
||||
guard !newSelection.contains(file) else {
|
||||
return
|
||||
}
|
||||
guard insertSorted else {
|
||||
newSelection.append(file)
|
||||
return
|
||||
}
|
||||
newSelection.insertSorted(file)
|
||||
}
|
||||
|
||||
private func moveSelectedFile(from source: IndexSet, to destination: Int) {
|
||||
newSelection.move(fromOffsets: source, toOffset: destination)
|
||||
}
|
||||
}
|
@ -30,7 +30,7 @@ struct IdPropertyView: View {
|
||||
}
|
||||
|
||||
private var isValid: Bool {
|
||||
validation(id)
|
||||
validation(newId)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
@ -80,7 +80,8 @@ struct AddPageView: View {
|
||||
english: .init(content: content,
|
||||
urlString: "page",
|
||||
title: "A Title"),
|
||||
tags: [])
|
||||
tags: [],
|
||||
requiredFiles: [])
|
||||
content.add(page)
|
||||
selectedPage = page
|
||||
dismissSheet()
|
||||
|
@ -4,6 +4,9 @@ import HighlightedTextEditor
|
||||
|
||||
struct LocalizedPageContentView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
var content: Content
|
||||
|
||||
let pageId: String
|
||||
|
||||
let language: ContentLanguage
|
||||
@ -11,9 +14,6 @@ struct LocalizedPageContentView: View {
|
||||
@ObservedObject
|
||||
var page: LocalizedPage
|
||||
|
||||
@State
|
||||
private var isGeneratingWebsite = false
|
||||
|
||||
@State
|
||||
private var pageContent: String = ""
|
||||
|
||||
@ -21,7 +21,7 @@ struct LocalizedPageContentView: View {
|
||||
private var pageContentUsedForGeneration: String = ""
|
||||
|
||||
@State
|
||||
private var generationResults = PageGenerationResults()
|
||||
private var generationResults: PageGenerationResults?
|
||||
|
||||
@State
|
||||
private var didChangeContent = false
|
||||
@ -47,10 +47,16 @@ struct LocalizedPageContentView: View {
|
||||
}
|
||||
Button(action: checkContent) {
|
||||
Text("Check")
|
||||
}.disabled(content.isGeneratingWebsite)
|
||||
if content.isGeneratingWebsite {
|
||||
ProgressView()
|
||||
.frame(height: 15)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
PageContentResultsView(results: generationResults)
|
||||
if let generationResults {
|
||||
PageContentResultsView(results: generationResults)
|
||||
}
|
||||
HighlightedTextEditor(
|
||||
text: $pageContent,
|
||||
highlightRules: .markdown)
|
||||
@ -65,9 +71,19 @@ struct LocalizedPageContentView: View {
|
||||
|
||||
private func loadContent() {
|
||||
let language = language
|
||||
guard page.content.storage.hasPageContent(for: pageId, language: language) else {
|
||||
pageContent = "New file"
|
||||
DispatchQueue.main.async {
|
||||
didChangeContent = false
|
||||
}
|
||||
return
|
||||
}
|
||||
guard let content = page.content.storage.pageContent(for: pageId, language: language) else {
|
||||
print("Failed to load page content")
|
||||
pageContent = "Failed to load"
|
||||
DispatchQueue.main.async {
|
||||
didChangeContent = false
|
||||
}
|
||||
return
|
||||
}
|
||||
guard content != "" else {
|
||||
@ -105,15 +121,14 @@ struct LocalizedPageContentView: View {
|
||||
guard content != pageContentUsedForGeneration else {
|
||||
return
|
||||
}
|
||||
isGeneratingWebsite = true
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let generator = PageContentParser(content: page.content, language: language)
|
||||
_ = generator.generatePage(from: content)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.generationResults = generator.results
|
||||
isGeneratingWebsite = false
|
||||
}
|
||||
guard let page = self.content.page(pageId) else {
|
||||
return
|
||||
}
|
||||
guard !self.content.isGeneratingWebsite else {
|
||||
return
|
||||
}
|
||||
self.content.check(content: content, of: page, for: language) {
|
||||
self.generationResults = $0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,17 +67,22 @@ struct PageContentResultsView: View {
|
||||
@ObservedObject
|
||||
var results: PageGenerationResults
|
||||
|
||||
#warning("Rework to only show a single popup will all files, and indicate missing ones")
|
||||
private var totalFileCount: Int {
|
||||
results.usedFiles.count + results.missingFiles.count + results.missingLinkedFiles.count
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
TextWithPopup(
|
||||
symbol: .photoOnRectangleAngled,
|
||||
text: "\(results.files.count + results.missingFiles.count) images and files",
|
||||
items: results.files.sorted().map { $0.id })
|
||||
text: "\(totalFileCount) images and files",
|
||||
items: results.usedFiles.sorted().map { $0.id })
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
TextWithPopup(
|
||||
symbol: .docBadgePlus,
|
||||
text: "\(results.linkedPages.count + results.missingPages.count) page links",
|
||||
text: "\(results.linkedPages.count + results.missingLinkedPages.count) page links",
|
||||
items: results.linkedPages.sorted().map { $0.localized(in: language).title })
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
@ -87,18 +92,18 @@ struct PageContentResultsView: View {
|
||||
items: results.externalLinks.sorted())
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if !results.missingPages.isEmpty {
|
||||
if !results.missingLinkedPages.isEmpty {
|
||||
TextWithPopup(
|
||||
symbol: .exclamationmarkTriangleFill,
|
||||
text: "\(results.missingPages.count) missing pages",
|
||||
items: results.missingPages.sorted())
|
||||
text: "\(results.missingLinkedPages.count) missing pages",
|
||||
items: results.missingLinkedPages.keys.sorted())
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
if !results.missingFiles.isEmpty {
|
||||
TextWithPopup(
|
||||
symbol: .exclamationmarkTriangleFill,
|
||||
text: "\(results.missingFiles.count) missing files",
|
||||
items: results.missingFiles.sorted())
|
||||
items: results.missingFiles.keys.sorted())
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
if !results.invalidCommands.isEmpty {
|
||||
@ -111,7 +116,3 @@ struct PageContentResultsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PageContentResultsView(results: .init())
|
||||
}
|
||||
|
@ -13,12 +13,21 @@ struct PageDetailView: View {
|
||||
private var page: Page
|
||||
|
||||
@State
|
||||
private var didGenerateWebsite: Bool?
|
||||
private var showFileSelectionSheet = false
|
||||
|
||||
init(page: Page) {
|
||||
self.page = page
|
||||
}
|
||||
|
||||
private var requiredFilesText: String {
|
||||
switch page.requiredFiles.count {
|
||||
case 0: return "No files"
|
||||
case 1: return "1 file"
|
||||
default: return "\(page.requiredFiles.count) files"
|
||||
}
|
||||
}
|
||||
|
||||
#warning("Show info on page generation")
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
@ -30,17 +39,17 @@ struct PageDetailView: View {
|
||||
Text("Generate")
|
||||
}
|
||||
.disabled(content.isGeneratingWebsite)
|
||||
switch didGenerateWebsite {
|
||||
case .none:
|
||||
Image(systemSymbol: .questionmarkCircleFill)
|
||||
.foregroundStyle(.gray)
|
||||
case .some(true):
|
||||
Image(systemSymbol: .checkmarkCircleFill)
|
||||
.foregroundStyle(.green)
|
||||
case .some(false):
|
||||
Image(systemSymbol: .xmarkCircleFill)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
// switch didGenerateWebsite {
|
||||
// case .none:
|
||||
// Image(systemSymbol: .questionmarkCircleFill)
|
||||
// .foregroundStyle(.gray)
|
||||
// case .some(true):
|
||||
// Image(systemSymbol: .checkmarkCircleFill)
|
||||
// .foregroundStyle(.green)
|
||||
// case .some(false):
|
||||
// Image(systemSymbol: .xmarkCircleFill)
|
||||
// .foregroundStyle(.red)
|
||||
// }
|
||||
}
|
||||
IdPropertyView(
|
||||
id: $page.id,
|
||||
@ -72,6 +81,24 @@ struct PageDetailView: View {
|
||||
footer: "The date when the page content ended")
|
||||
.disabled(page.isExternalUrl)
|
||||
|
||||
GenericPropertyView(
|
||||
title: "Required files",
|
||||
footer: "The additional files required by the page") {
|
||||
HStack {
|
||||
Image(systemSymbol: .squareAndPencilCircleFill)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 20)
|
||||
Text(requiredFilesText)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
showFileSelectionSheet = true
|
||||
}
|
||||
}
|
||||
|
||||
LocalizedPageDetailView(
|
||||
isExternalPage: page.isExternalUrl,
|
||||
page: page.localized(in: language))
|
||||
@ -79,14 +106,14 @@ struct PageDetailView: View {
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.sheet(isPresented: $showFileSelectionSheet) {
|
||||
MultiFileSelectionView(selectedFiles: $page.requiredFiles, insertSorted: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func generate() {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let success = content.generateFeed()
|
||||
DispatchQueue.main.async {
|
||||
didGenerateWebsite = success
|
||||
}
|
||||
content.generatePage(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,9 @@ struct PostImagesView: View {
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
MultiFileSelectionView(selectedFiles: $post.images, allowedType: .images)
|
||||
}
|
||||
}
|
||||
|
||||
private func shiftLeft(_ image: FileResource) {
|
||||
|
@ -32,7 +32,6 @@ struct GenerationContentView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var generationView: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Website Generation")
|
||||
.font(.largeTitle)
|
||||
@ -42,32 +41,75 @@ struct GenerationContentView: View {
|
||||
.padding(.bottom, 30)
|
||||
|
||||
HStack {
|
||||
Button(action: generateFeed) {
|
||||
Button(action: generateFullWebsite) {
|
||||
Text("Generate")
|
||||
}
|
||||
.disabled(isGeneratingWebsite)
|
||||
Text(generatorText)
|
||||
Spacer()
|
||||
if isGeneratingWebsite {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.frame(height: 25)
|
||||
}
|
||||
Button(action: updateGeneratedImages) {
|
||||
Text("Update images")
|
||||
}
|
||||
.disabled(isGeneratingWebsite)
|
||||
Text(content.generationStatus)
|
||||
.font(.subheadline)
|
||||
.padding()
|
||||
HStack(spacing: 8) {
|
||||
Text("\(content.results.imagesToGenerate.count) images")
|
||||
Text("\(content.results.externalLinks.count) external links")
|
||||
Text("\(content.results.resultCount) items processed")
|
||||
Text("\(content.results.requiredFiles.count) files")
|
||||
}
|
||||
List {
|
||||
Section("Inaccessible files") {
|
||||
ForEach(content.results.inaccessibleFiles.sorted()) { file in
|
||||
Text(file.id)
|
||||
}
|
||||
}
|
||||
Section("Unparsable files") {
|
||||
ForEach(content.results.unparsableFiles.sorted()) { file in
|
||||
Text(file.id)
|
||||
}
|
||||
}
|
||||
Section("Missing files") {
|
||||
ForEach(content.results.missingFiles.sorted(), id: \.self) { file in
|
||||
Text(file)
|
||||
}
|
||||
}
|
||||
Section("Missing tags") {
|
||||
ForEach(content.results.missingTags.sorted(), id: \.self) { tag in
|
||||
Text(tag)
|
||||
}
|
||||
}
|
||||
Section("Missing pages") {
|
||||
ForEach(content.results.missingPages.sorted(), id: \.self) { page in
|
||||
Text(page)
|
||||
}
|
||||
}
|
||||
Section("Invalid commands") {
|
||||
ForEach(content.results.invalidCommands.sorted(), id: \.self) { markdown in
|
||||
Text(markdown)
|
||||
}
|
||||
}
|
||||
Section("Warnings") {
|
||||
ForEach(content.results.warnings.sorted(), id: \.self) { warning in
|
||||
Text(warning)
|
||||
}
|
||||
}
|
||||
Section("Unsaved output files") {
|
||||
ForEach(content.results.unsavedOutputFiles.sorted(), id: \.self) { file in
|
||||
Text(file)
|
||||
}
|
||||
}
|
||||
Text(generatorText)
|
||||
Spacer()
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateGeneratedImages() {
|
||||
content.recalculateGeneratedImages()
|
||||
}
|
||||
|
||||
private func generateFeed() {
|
||||
private func generateFullWebsite() {
|
||||
DispatchQueue.main.async {
|
||||
_ = content.generateFeed()
|
||||
content.generateWebsiteInAllLanguages()
|
||||
}
|
||||
#warning("Update feed generation")
|
||||
/*
|
@ -5,9 +5,9 @@ struct PageIssue {
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
let message: PageContentAnomaly
|
||||
let message: GenerationAnomaly
|
||||
|
||||
init(page: Page, language: ContentLanguage, message: PageContentAnomaly) {
|
||||
init(page: Page, language: ContentLanguage, message: GenerationAnomaly) {
|
||||
self.page = page
|
||||
self.language = language
|
||||
self.message = message
|
||||
|
@ -50,24 +50,23 @@ final class PageIssueChecker: ObservableObject {
|
||||
}
|
||||
|
||||
private func analyze(page: Page, in language: ContentLanguage) {
|
||||
let parser = PageContentParser(content: page.content, language: language)
|
||||
let results = page.content.results.makeResults(for: page, in: language)
|
||||
let parser = PageContentParser(content: page.content, language: language, results: results)
|
||||
|
||||
let hasPreviousIssues = issues.contains { $0.page == page && $0.language == language }
|
||||
let pageIssues: [PageIssue]
|
||||
if let rawPageContent = page.content.storage.pageContent(for: page.id, language: language) {
|
||||
_ = parser.generatePage(from: rawPageContent)
|
||||
pageIssues = parser.results.issues.map {
|
||||
PageIssue(page: page, language: language, message: $0)
|
||||
}
|
||||
pageIssues = []
|
||||
} else {
|
||||
let message = PageContentAnomaly.failedToLoadContent
|
||||
let message = GenerationAnomaly.failedToLoadContent
|
||||
let error = PageIssue(page: page, language: language, message: message)
|
||||
pageIssues = [error]
|
||||
}
|
||||
guard hasPreviousIssues || !pageIssues.isEmpty else {
|
||||
return
|
||||
}
|
||||
update(issues: pageIssues, for: page, in: parser.language)
|
||||
update(issues: pageIssues, for: page, in: language)
|
||||
}
|
||||
|
||||
private func update(issues: [PageIssue], for page: Page, in language: ContentLanguage) {
|
||||
|
@ -231,7 +231,8 @@ struct PageIssueView: View {
|
||||
english: .init(content: content,
|
||||
urlString: pageId,
|
||||
title: pageId),
|
||||
tags: [])
|
||||
tags: [],
|
||||
requiredFiles: [])
|
||||
content.pages.insert(page, at: 0)
|
||||
|
||||
retryPageCheck()
|
||||
|
Reference in New Issue
Block a user