Colors, pages, post links

This commit is contained in:
Christoph Hagen
2024-11-20 13:53:44 +01:00
parent 943d8d962b
commit 8ae2a237cc
19 changed files with 466 additions and 46 deletions

View File

@@ -15,7 +15,7 @@ struct CHDataManagementApp: App {
""
}
@ObservedObject
@StateObject
var content: Content = .init()
@State
@@ -28,29 +28,26 @@ struct CHDataManagementApp: App {
WindowGroup {
TabView {
Tab("Posts", systemImage: SFSymbol.rectangleAndPencilAndEllipsis.rawValue) {
PostList(posts: $content.posts)
.environment(\.language, selectedLanguage)
.background(Color(r: 2, g: 15, b: 26))
PostList()
}
Tab("Pages", systemImage: SFSymbol.textBelowPhoto.rawValue) {
Text("TODO")
PageListView()
}
Tab("Tags", systemImage: SFSymbol.tag.rawValue) {
Text("TODO")
TagsListView()
}
Tab("Images", systemImage: SFSymbol.photo.rawValue) {
ImagesView()
.environmentObject(content)
}
Tab("Files", systemImage: SFSymbol.doc.rawValue) {
FilesView()
.environmentObject(content)
}
Tab("Settings", systemImage: SFSymbol.gear.rawValue) {
SettingsView()
.environmentObject(content)
}
}
.environment(\.language, selectedLanguage)
.environmentObject(content)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Picker("", selection: $selectedLanguage) {

View File

@@ -1,5 +1,6 @@
import Foundation
import SwiftUI
import Combine
final class Content: ObservableObject {
@@ -19,7 +20,64 @@ final class Content: ObservableObject {
var files: [FileResources] = []
@AppStorage("contentPath")
var contentPath: String = ""
private var storedContentPath: String = ""
@Published
var contentPath: String = "" {
didSet {
storedContentPath = contentPath
}
}
let storage: Storage
private var cancellables = Set<AnyCancellable>()
init(posts: [Post] = [],
pages: [Page] = [],
tags: [Tag] = [],
images: [ImageResource] = [],
files: [FileResources] = [],
storedContentPath: String) {
self.posts = posts
self.pages = pages
self.tags = tags
self.images = images
self.files = files
self.storedContentPath = storedContentPath
self.contentPath = storedContentPath
self.storage = Storage(baseFolder: URL(filePath: storedContentPath))
do {
try storage.createFolderStructure()
} catch {
print(error)
return
}
observeContentPath()
}
init() {
self.storage = Storage(baseFolder: URL(filePath: ""))
contentPath = storedContentPath
do {
try storage.createFolderStructure()
} catch {
print(error)
return
}
try? storage.update(baseFolder: URL(filePath: contentPath), moveContent: false)
observeContentPath()
}
private func observeContentPath() {
$contentPath.sink { newValue in
let url = URL(filePath: newValue)
try? self.storage.update(baseFolder: url, moveContent: true)
}
.store(in: &cancellables)
}
func generateFeed(for language: ContentLanguage, bookmarkKey: String) {
let posts = posts.map { $0.feedEntry(for: language) }
@@ -60,13 +118,6 @@ final class Content: ObservableObject {
}
func importOldContent() {
let storage = Storage(baseFolder: URL(filePath: "/Users/ch/Downloads/Content"))
do {
try storage.createFolderStructure()
} catch {
print(error)
return
}
let importer = Importer()
do {

View File

@@ -1,4 +1,5 @@
import Foundation
import SwiftUI
/**
A localized page contains the page content of a single language,
@@ -69,4 +70,16 @@ final class LocalizedPage: ObservableObject {
self.externalFiles = externalFiles
self.requiredFiles = requiredFiles
}
@MainActor
func editableTitle() -> Binding<String> {
Binding(
get: {
self.title
},
set: { newValue in
self.title = newValue
}
)
}
}

View File

@@ -41,6 +41,7 @@ final class LocalizedPost: ObservableObject {
)
}
@MainActor
func editableContent() -> Binding<String> {
Binding(
get: {

View File

@@ -59,7 +59,7 @@ final class Page: ObservableObject {
self.tags = tags
}
func metadata(for language: ContentLanguage) -> LocalizedPage? {
func localized(in language: ContentLanguage) -> LocalizedPage {
switch language {
case .german: return german
case .english: return english

View File

@@ -24,6 +24,13 @@ final class Tag: ObservableObject {
var url: String {
"/tags/\(linkName).html"
}
func localized(in language: ContentLanguage) -> LocalizedTag {
switch language {
case .english: return english
case .german: return german
}
}
}
extension Tag {

View File

@@ -0,0 +1,24 @@
import Foundation
extension FileManager {
var documentDirectory: URL {
try! url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil, create: true)
}
}
extension Content {
private static let dbPath = FileManager.default.documentDirectory.appendingPathComponent("db").path()
static let mock: Content = Content(
posts: [.empty, .mock, .fullMock],
pages: [.empty],
tags: [.hiking, .mountains, .nature, .sports],
images: [],
files: [],
storedContentPath: dbPath)
}

View File

@@ -122,6 +122,20 @@ final class Storage {
try loadAll(in: pagesFolder)
}
func pageContent(for pageId: String, language: ContentLanguage) -> String {
let contentUrl = pageContentUrl(pageId: pageId, language: language)
guard fm.fileExists(atPath: contentUrl.path()) else {
print("No file at \(contentUrl.path())")
return "New file"
}
do {
return try String(contentsOf: contentUrl, encoding: .utf8)
} catch {
print("Failed to load page content for \(pageId) (\(language)): \(error)")
return error.localizedDescription
}
}
// MARK: Posts
/// The folder path where the markdown files of the posts are stored (by their unique id/url component)

View File

@@ -0,0 +1,17 @@
import SwiftUI
enum ColorPalette {
static let tagBackground = Color(r: 9, g: 62, b: 103)
static let tagForeground = Color(r: 96, g: 186, b: 255)
static let listBackground = Color(r: 2, g: 15, b: 26)
static let postBackground = Color(r: 4, g: 31, b: 52)
static let postText = Color(r: 221, g: 221, b: 221)
static let postDate = tagForeground
}

View File

@@ -0,0 +1,28 @@
import SwiftUI
// A reusable component to handle optional strings with a TextField
struct OptionalTextField: View {
let titleKey: LocalizedStringKey
// The optional text that will be passed in and out of the component
@Binding var text: String?
init(_ titleKey: LocalizedStringKey, text: Binding<String?>) {
self.titleKey = titleKey
self._text = text
}
var body: some View {
TextField(titleKey, text: Binding(
get: {
// Convert `nil` to an empty string for display
text ?? ""
},
set: { newValue in
// Convert an empty string to `nil`
text = newValue.isEmpty ? nil : newValue
}
))
}
}

View File

@@ -1,17 +1,50 @@
import SwiftUI
import HighlightedTextEditor
struct PageDetailView: View {
@ObservedObject var page: Page
@ObservedObject
var page: Page
@Binding
var language: ContentLanguage
@Environment(\.language)
private var language
@EnvironmentObject
private var content: Content
@State
private var pageContent: String = ""
var body: some View {
Text(page.metadata(for: language)?.title ?? "No headline")
VStack(alignment: .leading) {
TextField("", text: page.localized(in: language).editableTitle())
.font(.title)
HStack(alignment: .firstTextBaseline) {
Button(action: loadContent) {
Text("Load")
}
Button(action: saveContent) {
Text("Save")
}
Spacer()
}
HighlightedTextEditor(
text: $pageContent,
highlightRules: .markdown)
}
.padding()
}
private func loadContent() {
pageContent = content.storage.pageContent(for: page.id, language: language)
}
private func saveContent() {
content.storage.save(pageContent: pageContent, for: page.id, language: language)
}
}
#Preview {
PageDetailView(page: .empty, language: .constant(.english))
PageDetailView(page: .empty)
}

View File

@@ -0,0 +1,38 @@
import SwiftUI
struct PageListView: View {
@Environment(\.language)
var language
@EnvironmentObject
var content: Content
@State
var selectedPage: Page?
var body: some View {
NavigationSplitView {
List(content.pages, selection: $selectedPage) { page in
Text(page.localized(in: language).title)
.tag(page)
}
} detail: {
// Detail view when an item is selected
if let selectedPage {
PageDetailView(page: selectedPage)
} else {
// Fallback if no item is selected
Text("Select a page to show the content.")
.font(.largeTitle)
.foregroundColor(.secondary)
}
}
}
}
#Preview {
PageListView()
.environmentObject(Content())
}

View File

@@ -0,0 +1,61 @@
import SwiftUI
struct PagePickerView: View {
@Binding var showPagePicker: Bool
@Binding var selectedPage: Page?
@EnvironmentObject
private var content: Content
@Environment(\.language)
private var language
@State
private var newSelection: Page?
init(showPagePicker: Binding<Bool>, selectedPage: Binding<Page?>) {
self._showPagePicker = showPagePicker
self._selectedPage = selectedPage
self.newSelection = selectedPage.wrappedValue
// TODO: Fix assignment not working
}
var body: some View {
VStack {
Text("Select a page to link to")
List(content.pages, selection: $newSelection) { page in
let loc = page.localized(in: language)
Text("\(loc.title) (\(page.id))")
.tag(page)
}
.frame(minHeight: 300)
HStack {
Button("Use selection") {
DispatchQueue.main.async {
self.selectedPage = self.newSelection
}
showPagePicker = false
}
Button("Remove page", role: .destructive) {
DispatchQueue.main.async {
self.selectedPage = nil
}
showPagePicker = false
}
Button("Cancel", role: .cancel) {
showPagePicker = false
}
}
}
.navigationTitle("Pick a page")
.padding()
}
}
#Preview {
PagePickerView(showPagePicker: .constant(true),
selectedPage: .constant(nil))
.environmentObject(Content.mock)
}

View File

@@ -13,20 +13,18 @@ private struct CenteredPost<Content>: View where Content: View {
HorizontalCenter {
content
}
.listRowBackground(PostList.background)
.listRowBackground(ColorPalette.listBackground)
}
}
struct PostList: View {
static let background = Color(r: 2, g: 15, b: 26)
@Binding
var posts: [Post]
@EnvironmentObject
private var content: Content
var body: some View {
List {
if posts.isEmpty {
if content.posts.isEmpty {
CenteredPost {
Text("No posts yet.")
.padding()
@@ -40,7 +38,7 @@ struct PostList: View {
.padding()
.listRowSeparator(.hidden)
}
ForEach(posts) { post in
ForEach(content.posts) { post in
CenteredPost {
PostView(post: post)
.frame(maxWidth: 600)
@@ -50,7 +48,7 @@ struct PostList: View {
}
}
.listStyle(.plain)
.background(PostList.background)
.background(ColorPalette.listBackground)
.scrollContentBackground(.hidden)
}
@@ -64,10 +62,11 @@ struct PostList: View {
tags: [],
german: .init(title: "Titel", content: "Text"),
english: .init(title: "Title", content: "Text"))
posts.insert(post, at: 0)
content.posts.insert(post, at: 0)
}
}
#Preview {
PostList(posts: .constant([.mock, .fullMock]))
PostList()
.environmentObject(Content())
}

View File

@@ -11,6 +11,16 @@ struct PostView: View {
@State
private var showDatePicker = false
@State
private var showPagePicker = false
private var linkedPageText: String {
if let page = post.linkedPage {
return page.localized(in: language).title
}
return "Add linked page"
}
var body: some View {
VStack(alignment: .center) {
if !post.localized(in: language).images.isEmpty {
@@ -25,7 +35,7 @@ struct PostView: View {
Spacer()
Toggle("Draft", isOn: $post.isDraft)
}
.foregroundStyle(Color(r: 96, g: 186, b: 255))
.foregroundStyle(ColorPalette.postDate)
TextField("", text: post.localized(in: language).editableTitle())
.font(.system(size: 24, weight: .bold))
.foregroundStyle(Color.white)
@@ -46,7 +56,7 @@ struct PostView: View {
.resizable()
.aspectRatio(1, contentMode: .fit)
.frame(height: 18)
.foregroundColor(TagView.foreground)
.foregroundColor(ColorPalette.tagForeground)
.opacity(0.7)
.padding(.top, 3)
}
@@ -54,17 +64,31 @@ struct PostView: View {
}
TextEditor(text: post.localized(in: language).editableContent())
.font(.body)
.foregroundStyle(Color(r: 221, g: 221, b: 221))
.foregroundStyle(ColorPalette.postText)
.textEditorStyle(.plain)
.padding(.leading, -5)
.scrollDisabled(true)
HorizontalCenter {
Button(action: { showPagePicker = true }) {
Text(linkedPageText)
}
.buttonStyle(.plain)
.foregroundStyle(ColorPalette.postDate)
}
}
.padding()
}
.background(Color(r: 4, g: 31, b: 52))
.background(ColorPalette.postBackground)
.cornerRadius(8)
.sheet(isPresented: $showDatePicker) {
DatePickerView(post: post, showDatePicker: $showDatePicker)
DatePickerView(
post: post,
showDatePicker: $showDatePicker)
}
.sheet(isPresented: $showPagePicker) {
PagePickerView(
showPagePicker: $showPagePicker,
selectedPage: $post.linkedPage)
}
}
@@ -81,11 +105,11 @@ struct PostView: View {
List {
PostView(post: .fullMock)
.listRowSeparator(.hidden)
.listRowBackground(Color(r: 2, g: 15, b: 26))
.listRowBackground(ColorPalette.listBackground)
.environment(\.language, ContentLanguage.german)
PostView(post: .mock)
.listRowSeparator(.hidden)
.listRowBackground(Color(r: 2, g: 15, b: 26))
.listRowBackground(ColorPalette.listBackground)
}
.listStyle(.plain)
}

View File

@@ -4,10 +4,6 @@ import SFSafeSymbols
struct TagView: View {
static let background = Color(r: 9, g: 62, b: 103)
static let foreground = Color(r: 96, g: 186, b: 255)
@Environment(\.language)
var language: ContentLanguage
@@ -37,11 +33,11 @@ struct TagView: View {
.opacity(0.7)
.padding(.leading, -5)
}
.foregroundColor(TagView.foreground)
.foregroundColor(ColorPalette.tagForeground)
.font(.caption2)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(TagView.background)
.background(ColorPalette.tagBackground)
.cornerRadius(8)
}
}

View File

@@ -0,0 +1,53 @@
import SwiftUI
struct TagDetailView: View {
@ObservedObject
var tag: LocalizedTag
@EnvironmentObject
private var content: Content
var body: some View {
VStack(alignment: .leading) {
Text("Name")
.font(.callout)
.foregroundStyle(.secondary)
TextField("", text: $tag.name)
Text("URL String")
.font(.callout)
.foregroundStyle(.secondary)
TextField("", text: $tag.urlComponent)
Text("Original url")
.font(.callout)
.foregroundStyle(.secondary)
Text(tag.originalUrl ?? "-")
.padding(.top, 1)
.padding(.bottom)
Text("Subtitle")
.font(.callout)
.foregroundStyle(.secondary)
OptionalTextField("", text: $tag.subtitle)
Text("Description")
.font(.callout)
.foregroundStyle(.secondary)
OptionalTextField("", text: $tag.description)
Text("Thumbnail")
.font(.callout)
.foregroundStyle(.secondary)
Text(tag.thumbnail ?? "-")
.padding(.top, 1)
.padding(.bottom)
}
.padding()
}
}
#Preview {
TagDetailView(tag: Tag.mock.english)
}

View File

@@ -0,0 +1,36 @@
import SwiftUI
struct TagsListView: View {
@Environment(\.language)
var language
@EnvironmentObject
var content: Content
@State
var selectedTag: Tag?
var body: some View {
NavigationSplitView {
List(content.tags, selection: $selectedTag) { tag in
Text(tag.localized(in: language).name)
.tag(tag)
}
} detail: {
if let selectedTag {
TagDetailView(tag: selectedTag.localized(in: language))
} else {
Text("Select a tag to show the details")
.font(.largeTitle)
.foregroundColor(.secondary)
}
}
}
}
#Preview {
PageListView()
.environmentObject(Content())
}