Improve settings, sidebars

This commit is contained in:
Christoph Hagen
2024-12-04 22:54:05 +01:00
parent b3cc4a57db
commit c3309197c0
36 changed files with 968 additions and 426 deletions

View 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()
}

View 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()
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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()
}

View 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()
}

View 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()
}

View 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 }
}

View 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")
}
}

View File

@ -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)
}