265 lines
8.5 KiB
Swift
265 lines
8.5 KiB
Swift
import SwiftUI
|
|
import SFSafeSymbols
|
|
|
|
/**
|
|
**Content**
|
|
- iPhone Backgrounds: Add page, html
|
|
- CV: Update PDF
|
|
|
|
**UI**
|
|
- Image search: Add view to see all images and filter
|
|
- Page Content: Show all results of `PageGenerationResults`
|
|
- Files: Show usages of file
|
|
- Buttons to insert special commands (images, page links, ...)
|
|
- Post: Create page from information of post
|
|
|
|
**Features**
|
|
- Posts: Generate separate pages for posts to link to
|
|
- Settings: Introduce `Authors` (`name`, `image`, `description`)
|
|
- Page: Property `author`
|
|
- Blocks: Convert more commands to blocks
|
|
- Graphs, Map, GPX for hikes
|
|
|
|
**Generation**
|
|
- Consistency: Check output folder for unused files
|
|
- Empty properties: Show warnings for empty link previews, etc.
|
|
|
|
**Fixes**
|
|
- Files: Id change: Check all page contents for links to the renamed file and replace occurences
|
|
- Database: Show errors during loading
|
|
- Mock content: Clean and improve
|
|
- Investigate issue with spaces in content file names
|
|
- Check assignment of blog posts to tags
|
|
*/
|
|
|
|
@main
|
|
struct MainView: App {
|
|
|
|
private let sidebarWidth: CGFloat = 250
|
|
|
|
private let detailWidth: CGFloat = 300
|
|
|
|
@StateObject
|
|
private var content: Content = .init()
|
|
|
|
@State
|
|
private var language: ContentLanguage = .english
|
|
|
|
@ObservedObject
|
|
private var selection: SelectedContent = .init()
|
|
|
|
@State
|
|
private var showAddSheet = false
|
|
|
|
@State
|
|
private var showInitialSetupSheet = false
|
|
|
|
@State
|
|
private var showLoadErrorSheet = false
|
|
|
|
@State
|
|
private var loadErrors: [String] = []
|
|
|
|
@ViewBuilder
|
|
var sidebar: some View {
|
|
switch selection.tab {
|
|
case .posts:
|
|
PostListView(selectedPost: $selection.post)
|
|
case .pages:
|
|
PageListView(selectedPage: $selection.page)
|
|
case .tags:
|
|
TagListView(selectedTag: $selection.tag)
|
|
case .files:
|
|
FileListView(selectedFile: $selection.file)
|
|
case .generation:
|
|
SettingsListView(selectedSection: $selection.section)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
var viewContent: some View {
|
|
switch selection.tab {
|
|
case .posts:
|
|
SelectedContentView<PostContentView>(selected: $selection.post)
|
|
case .pages:
|
|
SelectedContentView<PageContentView>(selected: $selection.page)
|
|
case .tags:
|
|
SelectedContentView<TagContentView>(selected: $selection.tag)
|
|
case .files:
|
|
SelectedContentView<FileContentView>(selected: $selection.file)
|
|
case .generation:
|
|
GenerationContentView(selected: $selection.section)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
var detail: some View {
|
|
switch selection.tab {
|
|
case .posts:
|
|
SelectedDetailView<PostDetailView>(selected: $selection.post)
|
|
case .pages:
|
|
SelectedDetailView<PageDetailView>(selected: $selection.page)
|
|
case .tags:
|
|
SelectedDetailView<TagDetailView>(selected: $selection.tag)
|
|
case .files:
|
|
SelectedDetailView<FileDetailView>(selected: $selection.file)
|
|
case .generation:
|
|
GenerationDetailView(section: selection.section)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
var addItemSheet: some View {
|
|
switch selection.tab {
|
|
case .posts:
|
|
AddPostView(selected: $selection.post)
|
|
case .pages:
|
|
AddPageView(selected: $selection.page)
|
|
case .tags:
|
|
AddTagView(selected: $selection.tag)
|
|
case .files:
|
|
AddFileView(selectedFile: $selection.file)
|
|
case .generation:
|
|
Text("Not implemented")
|
|
}
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
NavigationSplitView {
|
|
sidebar
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigation) {
|
|
Picker("", selection: $selection.tab) {
|
|
Text("Posts").tag(MainViewTab.posts)
|
|
Text("Pages").tag(MainViewTab.pages)
|
|
Text("Tags").tag(MainViewTab.tags)
|
|
Text("Files").tag(MainViewTab.files)
|
|
Text("Generation").tag(MainViewTab.generation)
|
|
}.pickerStyle(.segmented)
|
|
}
|
|
}
|
|
.navigationSplitViewColumnWidth(min: sidebarWidth, ideal: sidebarWidth, max: sidebarWidth)
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
Button(action: { showAddSheet = true }) {
|
|
Label("Add", systemSymbol: .plus)
|
|
}
|
|
.disabled(!selection.tab.canAddItems)
|
|
}
|
|
}
|
|
} content: {
|
|
viewContent
|
|
} detail: {
|
|
detail
|
|
.navigationSplitViewColumnWidth(min: detailWidth, ideal: detailWidth, max: detailWidth)
|
|
}
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
Picker("", selection: $language) {
|
|
Text("English")
|
|
.tag(ContentLanguage.english)
|
|
Text("German")
|
|
.tag(ContentLanguage.german)
|
|
}.pickerStyle(.segmented)
|
|
}
|
|
ToolbarItem(placement: .primaryAction) {
|
|
if content.storage.contentScope != nil {
|
|
Button(action: save) {
|
|
Text("Save")
|
|
}
|
|
} else {
|
|
Button(action: showInitialSheet) {
|
|
Text("Setup")
|
|
}
|
|
.background(RoundedRectangle(cornerRadius: 8).fill(Color.red))
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("")
|
|
.environment(\.language, language)
|
|
.environmentObject(content)
|
|
.environmentObject(selection)
|
|
.onAppear(perform: loadContent)
|
|
.onReceive(Timer.publish(every: 60.0, on: .main, in: .common).autoconnect()) { _ in
|
|
save()
|
|
}
|
|
.sheet(isPresented: $showAddSheet) {
|
|
addItemSheet
|
|
.environment(\.language, language)
|
|
.environmentObject(content)
|
|
.environmentObject(selection)
|
|
}
|
|
.sheet(isPresented: $showInitialSetupSheet) {
|
|
InitialSetupView()
|
|
.environment(\.language, language)
|
|
.environmentObject(content)
|
|
.environmentObject(selection)
|
|
}
|
|
.sheet(isPresented: $showLoadErrorSheet) {
|
|
VStack {
|
|
Text("Failed to load database")
|
|
.font(.headline)
|
|
List(loadErrors, id: \.self) { error in
|
|
HStack {
|
|
Text(error)
|
|
Spacer()
|
|
}
|
|
}
|
|
.frame(minHeight: 200)
|
|
Button("Dismiss", action: { showLoadErrorSheet = false })
|
|
.padding()
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func save() {
|
|
guard content.saveToDisk() else {
|
|
print("Failed to save content")
|
|
#warning("Show error message")
|
|
return
|
|
}
|
|
}
|
|
|
|
private func loadContent() {
|
|
guard content.storage.contentScope != nil else {
|
|
showInitialSheet()
|
|
return
|
|
}
|
|
content.loadFromDisk { errors in
|
|
prepareAfterLoad()
|
|
guard !errors.isEmpty else {
|
|
return
|
|
}
|
|
self.loadErrors = errors
|
|
self.showLoadErrorSheet = true
|
|
}
|
|
}
|
|
|
|
private func prepareAfterLoad() {
|
|
if selection.post == nil {
|
|
selection.post = content.posts.first
|
|
}
|
|
if selection.page == nil {
|
|
selection.page = content.pages.first
|
|
}
|
|
if selection.tag == nil {
|
|
selection.tag = content.tags.first
|
|
}
|
|
if selection.file == nil {
|
|
selection.file = content.files.first
|
|
}
|
|
}
|
|
|
|
private func showInitialSheet() {
|
|
DispatchQueue.main.async {
|
|
selection.section = .folders
|
|
selection.tab = .generation
|
|
showInitialSetupSheet = true
|
|
}
|
|
}
|
|
}
|
|
|