Improve settings, sidebars
This commit is contained in:
91
CHDataManagement/Views/Settings/FolderSettingsView.swift
Normal file
91
CHDataManagement/Views/Settings/FolderSettingsView.swift
Normal file
@ -0,0 +1,91 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FolderSettingsView: View {
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@AppStorage("contentPath")
|
||||
private var contentPath: String = ""
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@State
|
||||
private var folderSelection: SecurityScopeBookmark = .contentPath
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Folder Settings")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Select the folders for the app to work.")
|
||||
.padding(.bottom, 30)
|
||||
Text("Content Folder")
|
||||
.font(.headline)
|
||||
.padding(.bottom, 1)
|
||||
Text(contentPath)
|
||||
Button(action: selectContentFolder) {
|
||||
Text("Select folder")
|
||||
}
|
||||
.padding(.bottom)
|
||||
Text("Output Folder")
|
||||
.font(.headline)
|
||||
.padding(.bottom, 1)
|
||||
Text(content.settings.outputDirectoryPath)
|
||||
Button(action: selectOutputFolder) {
|
||||
Text("Select folder")
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Folder selection
|
||||
|
||||
private func selectContentFolder() {
|
||||
folderSelection = .contentPath
|
||||
guard let url = savePanelUsingOpenPanel() else {
|
||||
return
|
||||
}
|
||||
self.contentPath = url.path()
|
||||
}
|
||||
|
||||
private func selectOutputFolder() {
|
||||
folderSelection = .outputPath
|
||||
guard let url = savePanelUsingOpenPanel() else {
|
||||
return
|
||||
}
|
||||
content.settings.outputDirectoryPath = url.path()
|
||||
}
|
||||
|
||||
private func savePanelUsingOpenPanel() -> URL? {
|
||||
let panel = NSOpenPanel()
|
||||
// Sets up so user can only select a single directory
|
||||
panel.canChooseFiles = false
|
||||
panel.canChooseDirectories = true
|
||||
panel.allowsMultipleSelection = false
|
||||
panel.showsHiddenFiles = false
|
||||
panel.title = "Select Save Directory"
|
||||
panel.prompt = "Select Save Directory"
|
||||
|
||||
let response = panel.runModal()
|
||||
guard response == .OK else {
|
||||
|
||||
return nil
|
||||
}
|
||||
guard let url = panel.url else {
|
||||
return nil
|
||||
}
|
||||
content.storage.save(folderUrl: url, in: folderSelection)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
FolderSettingsView()
|
||||
.environmentObject(Content.mock)
|
||||
.padding()
|
||||
}
|
76
CHDataManagement/Views/Settings/GenerationSettingsView.swift
Normal file
76
CHDataManagement/Views/Settings/GenerationSettingsView.swift
Normal file
@ -0,0 +1,76 @@
|
||||
import SwiftUI
|
||||
|
||||
struct GenerationSettingsView: View {
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@State
|
||||
private var isGeneratingWebsite = false
|
||||
|
||||
@State
|
||||
private var generatorText: String = ""
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Website Generation")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Regenerate the website and monitor the output")
|
||||
.padding(.bottom, 30)
|
||||
|
||||
HStack {
|
||||
Button(action: generateFeed) {
|
||||
Text("Generate")
|
||||
}
|
||||
.disabled(isGeneratingWebsite)
|
||||
if isGeneratingWebsite {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.frame(height: 25)
|
||||
}
|
||||
Text(generatorText)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func generateFeed() {
|
||||
guard content.settings.outputDirectoryPath != "" else {
|
||||
print("Invalid output path")
|
||||
return
|
||||
}
|
||||
let url = URL(fileURLWithPath: content.settings.outputDirectoryPath)
|
||||
|
||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||
print("Missing output folder")
|
||||
return
|
||||
}
|
||||
isGeneratingWebsite = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let generator = WebsiteGenerator(
|
||||
content: content,
|
||||
language: language)
|
||||
_ = generator.generateWebsite { text in
|
||||
DispatchQueue.main.async {
|
||||
self.generatorText = text
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
isGeneratingWebsite = false
|
||||
self.generatorText = "Generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
GenerationSettingsView()
|
||||
.environmentObject(Content.mock)
|
||||
.padding()
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import SwiftUI
|
||||
|
||||
struct LocalizedPostFeedSettingsView: View {
|
||||
|
||||
@ObservedObject
|
||||
var settings: LocalizedPostSettings
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Title")
|
||||
.font(.headline)
|
||||
TextField("", text: $settings.title)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 400)
|
||||
Text("The title of all post feed pages.")
|
||||
.padding(.bottom)
|
||||
Text("URL prefix")
|
||||
.font(.headline)
|
||||
TextField("", text: $settings.feedUrlPrefix)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 400)
|
||||
Text("The prefix to generate the urls for all post feed pages.")
|
||||
.padding(.bottom)
|
||||
Text("Description")
|
||||
.font(.headline)
|
||||
TextEditor(text: $settings.description)
|
||||
.font(.body)
|
||||
.lineLimit(5, reservesSpace: true)
|
||||
.frame(maxWidth: 400, minHeight: 50, maxHeight: 500)
|
||||
.textEditorStyle(.plain)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.leading, 3)
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
Text("The description of all post feed pages.")
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LocalizedPostFeedSettingsView(settings: .english)
|
||||
.padding()
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct LocalizedSettingsView: View {
|
||||
|
||||
@ObservedObject
|
||||
var settings: LocalizedWebsiteData
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Title")
|
||||
.font(.headline)
|
||||
TextField("", text: $settings.title)
|
||||
Text("Description")
|
||||
.font(.headline)
|
||||
TextField("", text: $settings.description)
|
||||
Text("Icon description")
|
||||
.font(.headline)
|
||||
TextField("", text: $settings.iconDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LocalizedSettingsView(settings: .english)
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import SwiftUI
|
||||
|
||||
private struct IconDescriptionView: View {
|
||||
|
||||
@ObservedObject
|
||||
var settings: LocalizedSettings
|
||||
|
||||
var body: some View {
|
||||
TextField("", text: $settings.navigationBarIconDescription)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 300)
|
||||
}
|
||||
}
|
||||
|
||||
struct NavigationBarSettingsView: View {
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@State
|
||||
private var showTagPicker = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Notification Bar Settings")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Customize the navigation bar for all pages at the top of the website")
|
||||
.padding(.bottom, 30)
|
||||
Text("Icon Path")
|
||||
.font(.headline)
|
||||
TextField("", text: $content.settings.navigationBar.iconPath)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 300)
|
||||
Text("Specify the path to the icon file with regard to the final website folder.")
|
||||
.padding(.bottom, 30)
|
||||
Text("Icon Description")
|
||||
.font(.headline)
|
||||
IconDescriptionView(settings: content.settings.localized(in: language))
|
||||
Text("Provide a description of the icon for screen readers.")
|
||||
.padding(.bottom, 30)
|
||||
Text("Visible Tags")
|
||||
.font(.headline)
|
||||
FlowHStack {
|
||||
ForEach(content.settings.navigationBar.tags, id: \.id) { tag in
|
||||
TagView(tag: .init(
|
||||
en: tag.english.name,
|
||||
de: tag.german.name)
|
||||
)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
Button(action: { showTagPicker = true }) {
|
||||
Image(systemSymbol: .squareAndPencilCircleFill)
|
||||
.resizable()
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.frame(height: 22)
|
||||
.foregroundColor(Color.gray)
|
||||
.background(Circle()
|
||||
.fill(Color.white)
|
||||
.padding(1))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
Text("Select the tags to show in the navigation bar. The number should be even.")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showTagPicker) {
|
||||
TagSelectionView(
|
||||
presented: $showTagPicker,
|
||||
selected: $content.settings.navigationBar.tags,
|
||||
tags: $content.tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationBarSettingsView()
|
||||
.environmentObject(Content.mock)
|
||||
.padding()
|
||||
}
|
30
CHDataManagement/Views/Settings/PostFeedSettingsView.swift
Normal file
30
CHDataManagement/Views/Settings/PostFeedSettingsView.swift
Normal file
@ -0,0 +1,30 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PostFeedSettingsView: View {
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Post Feed Settings")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Change the way the posts are displayed")
|
||||
.padding(.bottom, 30)
|
||||
LocalizedPostFeedSettingsView(settings: content.settings.localized(in: language).posts)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
PostFeedSettingsView()
|
||||
.environmentObject(Content.mock)
|
||||
.padding()
|
||||
}
|
81
CHDataManagement/Views/Settings/SectionedSettingsView.swift
Normal file
81
CHDataManagement/Views/Settings/SectionedSettingsView.swift
Normal file
@ -0,0 +1,81 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SectionedSettingsView: View {
|
||||
|
||||
@State
|
||||
private var selectedSection: SettingsSection? = .generation
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
SettingsSidebar(selectedSection: $selectedSection)
|
||||
.frame(minWidth: 200, idealWidth: 200, maxWidth: 200)
|
||||
} detail: {
|
||||
DetailView(section: selectedSection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct DetailView: View {
|
||||
|
||||
let section: SettingsSection?
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch section {
|
||||
case .generation:
|
||||
GenerationSettingsView()
|
||||
case .folders:
|
||||
FolderSettingsView()
|
||||
case .navigationBar:
|
||||
NavigationBarSettingsView()
|
||||
case .postFeed:
|
||||
PostFeedSettingsView()
|
||||
case .none:
|
||||
Text("Select a setting from the sidebar")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.padding()
|
||||
.navigationTitle(section?.rawValue ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct AppearanceView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Appearance Settings")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Customize the look and feel of the app.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationsView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Notifications Settings")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Manage your notification preferences.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PrivacyView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Privacy Settings")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Configure your privacy and security settings.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SectionedSettingsView()
|
||||
}
|
34
CHDataManagement/Views/Settings/SettingsSection.swift
Normal file
34
CHDataManagement/Views/Settings/SettingsSection.swift
Normal file
@ -0,0 +1,34 @@
|
||||
import SFSafeSymbols
|
||||
|
||||
enum SettingsSection: String {
|
||||
|
||||
case generation = "Generation"
|
||||
|
||||
case folders = "Folders"
|
||||
|
||||
case navigationBar = "Navigation Bar"
|
||||
|
||||
case postFeed = "Post Feed"
|
||||
|
||||
}
|
||||
|
||||
extension SettingsSection {
|
||||
|
||||
var icon: SFSymbol {
|
||||
switch self {
|
||||
case .generation: return .arrowTriangle2Circlepath
|
||||
case .folders: return .folder
|
||||
case .navigationBar: return .menubarRectangle
|
||||
case .postFeed: return .rectangleGrid1x2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsSection: CaseIterable {
|
||||
|
||||
}
|
||||
|
||||
extension SettingsSection: Identifiable {
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
15
CHDataManagement/Views/Settings/SettingsSidebar.swift
Normal file
15
CHDataManagement/Views/Settings/SettingsSidebar.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
struct SettingsSidebar: View {
|
||||
|
||||
@Binding var selectedSection: SettingsSection?
|
||||
|
||||
var body: some View {
|
||||
List(SettingsSection.allCases, selection: $selectedSection) { item in
|
||||
Label(item.rawValue, systemSymbol: item.icon)
|
||||
.tag(item)
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
@Environment(\.language)
|
||||
var language
|
||||
|
||||
@AppStorage("contentPath")
|
||||
var contentPath: String = ""
|
||||
|
||||
@AppStorage("outputPath")
|
||||
var outputPath: String = ""
|
||||
|
||||
@EnvironmentObject
|
||||
var content: Content
|
||||
|
||||
@State
|
||||
private var folderSelection: SecurityScopeBookmark = .contentPath
|
||||
|
||||
@State
|
||||
private var showTagPicker = false
|
||||
|
||||
@State
|
||||
private var isGeneratingWebsite = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Content Folder")
|
||||
.font(.headline)
|
||||
TextField("Content Folder", text: $contentPath)
|
||||
Button(action: selectContentFolder) {
|
||||
Text("Select folder")
|
||||
}
|
||||
Text("Output Folder")
|
||||
.font(.headline)
|
||||
TextField("Output Folder", text: $outputPath)
|
||||
Button(action: selectOutputFolder) {
|
||||
Text("Select folder")
|
||||
}
|
||||
Text("Navigation Bar Items")
|
||||
.font(.headline)
|
||||
FlowHStack {
|
||||
ForEach(content.websiteData.navigationTags, id: \.id) { tag in
|
||||
TagView(tag: .init(
|
||||
en: tag.english.name,
|
||||
de: tag.german.name)
|
||||
)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
Button(action: { showTagPicker = true }) {
|
||||
Image(systemSymbol: .squareAndPencilCircleFill)
|
||||
.resizable()
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.frame(height: 22)
|
||||
.foregroundColor(Color.gray)
|
||||
.background(Circle()
|
||||
.fill(Color.white)
|
||||
.padding(1))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
LocalizedSettingsView(settings: content.websiteData.localized(in: language))
|
||||
Text("Feed")
|
||||
.font(.headline)
|
||||
HStack {
|
||||
Button(action: generateFeed) {
|
||||
Text("Generate")
|
||||
}
|
||||
.disabled(isGeneratingWebsite)
|
||||
if isGeneratingWebsite {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.frame(height: 25)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.sheet(isPresented: $showTagPicker) {
|
||||
TagSelectionView(
|
||||
presented: $showTagPicker,
|
||||
selected: $content.websiteData.navigationTags,
|
||||
tags: $content.tags)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Folder selection
|
||||
|
||||
private func selectContentFolder() {
|
||||
folderSelection = .contentPath
|
||||
guard let url = savePanelUsingOpenPanel() else {
|
||||
return
|
||||
}
|
||||
self.contentPath = url.path()
|
||||
}
|
||||
|
||||
private func selectOutputFolder() {
|
||||
folderSelection = .outputPath
|
||||
guard let url = savePanelUsingOpenPanel() else {
|
||||
return
|
||||
}
|
||||
self.outputPath = url.path()
|
||||
}
|
||||
|
||||
// MARK: Feed
|
||||
|
||||
private func generateFeed() {
|
||||
guard outputPath != "" else {
|
||||
print("Invalid output path")
|
||||
return
|
||||
}
|
||||
let url = URL(fileURLWithPath: outputPath)
|
||||
|
||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||
print("Missing output folder")
|
||||
return
|
||||
}
|
||||
isGeneratingWebsite = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let generator = WebsiteGenerator(
|
||||
content: content,
|
||||
configuration: configuration)
|
||||
_ = generator.generateWebsite()
|
||||
DispatchQueue.main.async {
|
||||
isGeneratingWebsite = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var configuration: WebsiteGeneratorConfiguration {
|
||||
return .init(
|
||||
language: language,
|
||||
outputDirectory: URL(filePath: outputPath, directoryHint: .isDirectory),
|
||||
postsPerPage: 20,
|
||||
postFeedTitle: "Posts",
|
||||
postFeedDescription: "The most recent posts on christophhagen.de",
|
||||
postFeedUrlPrefix: "feed",
|
||||
navigationIconPath: "/assets/icons/ch.svg",
|
||||
mainContentMaximumWidth: 600)
|
||||
}
|
||||
|
||||
func savePanelUsingOpenPanel() -> URL? {
|
||||
let panel = NSOpenPanel()
|
||||
// Sets up so user can only select a single directory
|
||||
panel.canChooseFiles = false
|
||||
panel.canChooseDirectories = true
|
||||
panel.allowsMultipleSelection = false
|
||||
panel.showsHiddenFiles = false
|
||||
panel.title = "Select Save Directory"
|
||||
panel.prompt = "Select Save Directory"
|
||||
|
||||
let response = panel.runModal()
|
||||
guard response == .OK else {
|
||||
|
||||
return nil
|
||||
}
|
||||
guard let url = panel.url else {
|
||||
return nil
|
||||
}
|
||||
content.storage.save(folderUrl: url, in: folderSelection)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
.environmentObject(Content.mock)
|
||||
}
|
Reference in New Issue
Block a user