Colors, pages, post links
This commit is contained in:
parent
943d8d962b
commit
8ae2a237cc
@ -7,6 +7,9 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850082CEE01BF0090B18B /* PagePickerView.swift */; };
|
||||||
|
E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500A2CEE02FA0090B18B /* Content+Mock.swift */; };
|
||||||
|
E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500C2CEE07140090B18B /* ColorPalette.swift */; };
|
||||||
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
|
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
|
||||||
E24252032C5163CF0029FF16 /* Importer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252022C5163CF0029FF16 /* Importer.swift */; };
|
E24252032C5163CF0029FF16 /* Importer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252022C5163CF0029FF16 /* Importer.swift */; };
|
||||||
E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252052C51684E0029FF16 /* GenericMetadata.swift */; };
|
E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252052C51684E0029FF16 /* GenericMetadata.swift */; };
|
||||||
@ -47,6 +50,10 @@
|
|||||||
E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */; };
|
E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */; };
|
||||||
E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */; };
|
E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */; };
|
||||||
E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */; };
|
E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */; };
|
||||||
|
E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D242CEBD7A10000979F /* PageListView.swift */; };
|
||||||
|
E2A37D292CED2C6A0000979F /* TagsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D282CED2C6A0000979F /* TagsListView.swift */; };
|
||||||
|
E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D2A2CED2CC30000979F /* TagDetailView.swift */; };
|
||||||
|
E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */; };
|
||||||
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A9CB7D2C7BCF2A005C89CC /* Page.swift */; };
|
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A9CB7D2C7BCF2A005C89CC /* Page.swift */; };
|
||||||
E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2B85F352C426BEE0047CD0C /* SFSafeSymbols */; };
|
E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2B85F352C426BEE0047CD0C /* SFSafeSymbols */; };
|
||||||
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3A2C428F0D0047CD0C /* Post.swift */; };
|
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3A2C428F0D0047CD0C /* Post.swift */; };
|
||||||
@ -63,6 +70,9 @@
|
|||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
E21850082CEE01BF0090B18B /* PagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePickerView.swift; sourceTree = "<group>"; };
|
||||||
|
E218500A2CEE02FA0090B18B /* Content+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Mock.swift"; sourceTree = "<group>"; };
|
||||||
|
E218500C2CEE07140090B18B /* ColorPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPalette.swift; sourceTree = "<group>"; };
|
||||||
E24252022C5163CF0029FF16 /* Importer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Importer.swift; sourceTree = "<group>"; };
|
E24252022C5163CF0029FF16 /* Importer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Importer.swift; sourceTree = "<group>"; };
|
||||||
E24252052C51684E0029FF16 /* GenericMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericMetadata.swift; sourceTree = "<group>"; };
|
E24252052C51684E0029FF16 /* GenericMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericMetadata.swift; sourceTree = "<group>"; };
|
||||||
E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GenericMetadata+Localized.swift"; sourceTree = "<group>"; };
|
E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GenericMetadata+Localized.swift"; sourceTree = "<group>"; };
|
||||||
@ -102,6 +112,10 @@
|
|||||||
E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPost.swift; sourceTree = "<group>"; };
|
E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPost.swift; sourceTree = "<group>"; };
|
||||||
E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = "<group>"; };
|
E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Sorted.swift"; sourceTree = "<group>"; };
|
E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Sorted.swift"; sourceTree = "<group>"; };
|
||||||
|
E2A37D242CEBD7A10000979F /* PageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageListView.swift; sourceTree = "<group>"; };
|
||||||
|
E2A37D282CED2C6A0000979F /* TagsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsListView.swift; sourceTree = "<group>"; };
|
||||||
|
E2A37D2A2CED2CC30000979F /* TagDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDetailView.swift; sourceTree = "<group>"; };
|
||||||
|
E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextField.swift; sourceTree = "<group>"; };
|
||||||
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = "<group>"; };
|
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = "<group>"; };
|
||||||
E2B85F3A2C428F0D0047CD0C /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; };
|
E2B85F3A2C428F0D0047CD0C /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; };
|
||||||
E2B85F3C2C4293F80047CD0C /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
|
E2B85F3C2C4293F80047CD0C /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
|
||||||
@ -146,6 +160,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E2A21C312CB5BCAC0060935B /* PageDetailView.swift */,
|
E2A21C312CB5BCAC0060935B /* PageDetailView.swift */,
|
||||||
|
E2A37D242CEBD7A10000979F /* PageListView.swift */,
|
||||||
);
|
);
|
||||||
path = Pages;
|
path = Pages;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -161,6 +176,7 @@
|
|||||||
E2A21C372CB9A4F10060935B /* Generic */ = {
|
E2A21C372CB9A4F10060935B /* Generic */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */,
|
||||||
E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */,
|
E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */,
|
||||||
E2A21C0F2CB18B390060935B /* FlowHStack.swift */,
|
E2A21C0F2CB18B390060935B /* FlowHStack.swift */,
|
||||||
);
|
);
|
||||||
@ -199,6 +215,8 @@
|
|||||||
E2A9CB7F2C7E686C005C89CC /* Tags */ = {
|
E2A9CB7F2C7E686C005C89CC /* Tags */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2A37D282CED2C6A0000979F /* TagsListView.swift */,
|
||||||
|
E2A37D2A2CED2CC30000979F /* TagDetailView.swift */,
|
||||||
);
|
);
|
||||||
path = Tags;
|
path = Tags;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -244,6 +262,7 @@
|
|||||||
E2B85F462C42C7CA0047CD0C /* Views */ = {
|
E2B85F462C42C7CA0047CD0C /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E218500C2CEE07140090B18B /* ColorPalette.swift */,
|
||||||
E2A21C522CBBF86D0060935B /* Files */,
|
E2A21C522CBBF86D0060935B /* Files */,
|
||||||
E2A21C492CBB168F0060935B /* Images */,
|
E2A21C492CBB168F0060935B /* Images */,
|
||||||
E2A21C372CB9A4F10060935B /* Generic */,
|
E2A21C372CB9A4F10060935B /* Generic */,
|
||||||
@ -258,6 +277,7 @@
|
|||||||
E2B85F4B2C4B8B7F0047CD0C /* Posts */ = {
|
E2B85F4B2C4B8B7F0047CD0C /* Posts */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E21850082CEE01BF0090B18B /* PagePickerView.swift */,
|
||||||
E2A21C112CB18D520060935B /* DatePickerView.swift */,
|
E2A21C112CB18D520060935B /* DatePickerView.swift */,
|
||||||
E2A21C152CB1A3C60060935B /* PostImageGalleryView.swift */,
|
E2A21C152CB1A3C60060935B /* PostImageGalleryView.swift */,
|
||||||
E2A21C2B2CB2BB210060935B /* PostList.swift */,
|
E2A21C2B2CB2BB210060935B /* PostList.swift */,
|
||||||
@ -317,6 +337,7 @@
|
|||||||
E2DD047C2C276F32003BFF1F /* Preview Content */ = {
|
E2DD047C2C276F32003BFF1F /* Preview Content */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E218500A2CEE02FA0090B18B /* Content+Mock.swift */,
|
||||||
E2A37D1A2CEA45530000979F /* Tag+Mock.swift */,
|
E2A37D1A2CEA45530000979F /* Tag+Mock.swift */,
|
||||||
E2A21C1F2CB28ED20060935B /* MockImage.swift */,
|
E2A21C1F2CB28ED20060935B /* MockImage.swift */,
|
||||||
E2A21C292CB2AA4C0060935B /* Post+Mock.swift */,
|
E2A21C292CB2AA4C0060935B /* Post+Mock.swift */,
|
||||||
@ -416,11 +437,14 @@
|
|||||||
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */,
|
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */,
|
||||||
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */,
|
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */,
|
||||||
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
|
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
|
||||||
|
E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */,
|
||||||
|
E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */,
|
||||||
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
|
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
|
||||||
E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */,
|
E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */,
|
||||||
E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */,
|
E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */,
|
||||||
E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */,
|
E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */,
|
||||||
E2B85F3D2C4293F80047CD0C /* Feed.swift in Sources */,
|
E2B85F3D2C4293F80047CD0C /* Feed.swift in Sources */,
|
||||||
|
E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */,
|
||||||
E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */,
|
E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */,
|
||||||
E24252082C5168750029FF16 /* GenericMetadata+Localized.swift in Sources */,
|
E24252082C5168750029FF16 /* GenericMetadata+Localized.swift in Sources */,
|
||||||
E2581DED2C75202400F1F079 /* Tag.swift in Sources */,
|
E2581DED2C75202400F1F079 /* Tag.swift in Sources */,
|
||||||
@ -428,15 +452,19 @@
|
|||||||
E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */,
|
E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */,
|
||||||
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
|
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
|
||||||
E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */,
|
E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */,
|
||||||
|
E2A37D292CED2C6A0000979F /* TagsListView.swift in Sources */,
|
||||||
E24252032C5163CF0029FF16 /* Importer.swift in Sources */,
|
E24252032C5163CF0029FF16 /* Importer.swift in Sources */,
|
||||||
|
E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */,
|
||||||
E2A21C332CB5BCAC0060935B /* PageDetailView.swift in Sources */,
|
E2A21C332CB5BCAC0060935B /* PageDetailView.swift in Sources */,
|
||||||
E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */,
|
E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */,
|
||||||
E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */,
|
E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */,
|
||||||
E2A21C202CB28ED20060935B /* MockImage.swift in Sources */,
|
E2A21C202CB28ED20060935B /* MockImage.swift in Sources */,
|
||||||
E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */,
|
E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */,
|
||||||
|
E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */,
|
||||||
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
|
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
|
||||||
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
|
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
|
||||||
E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */,
|
E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */,
|
||||||
|
E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */,
|
||||||
E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */,
|
E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */,
|
||||||
E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */,
|
E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */,
|
||||||
E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */,
|
E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */,
|
||||||
|
@ -15,7 +15,7 @@ struct CHDataManagementApp: App {
|
|||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObservedObject
|
@StateObject
|
||||||
var content: Content = .init()
|
var content: Content = .init()
|
||||||
|
|
||||||
@State
|
@State
|
||||||
@ -28,29 +28,26 @@ struct CHDataManagementApp: App {
|
|||||||
WindowGroup {
|
WindowGroup {
|
||||||
TabView {
|
TabView {
|
||||||
Tab("Posts", systemImage: SFSymbol.rectangleAndPencilAndEllipsis.rawValue) {
|
Tab("Posts", systemImage: SFSymbol.rectangleAndPencilAndEllipsis.rawValue) {
|
||||||
PostList(posts: $content.posts)
|
PostList()
|
||||||
.environment(\.language, selectedLanguage)
|
|
||||||
.background(Color(r: 2, g: 15, b: 26))
|
|
||||||
}
|
}
|
||||||
Tab("Pages", systemImage: SFSymbol.textBelowPhoto.rawValue) {
|
Tab("Pages", systemImage: SFSymbol.textBelowPhoto.rawValue) {
|
||||||
Text("TODO")
|
PageListView()
|
||||||
}
|
}
|
||||||
Tab("Tags", systemImage: SFSymbol.tag.rawValue) {
|
Tab("Tags", systemImage: SFSymbol.tag.rawValue) {
|
||||||
Text("TODO")
|
TagsListView()
|
||||||
}
|
}
|
||||||
Tab("Images", systemImage: SFSymbol.photo.rawValue) {
|
Tab("Images", systemImage: SFSymbol.photo.rawValue) {
|
||||||
ImagesView()
|
ImagesView()
|
||||||
.environmentObject(content)
|
|
||||||
}
|
}
|
||||||
Tab("Files", systemImage: SFSymbol.doc.rawValue) {
|
Tab("Files", systemImage: SFSymbol.doc.rawValue) {
|
||||||
FilesView()
|
FilesView()
|
||||||
.environmentObject(content)
|
|
||||||
}
|
}
|
||||||
Tab("Settings", systemImage: SFSymbol.gear.rawValue) {
|
Tab("Settings", systemImage: SFSymbol.gear.rawValue) {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.environment(\.language, selectedLanguage)
|
||||||
.environmentObject(content)
|
.environmentObject(content)
|
||||||
}
|
|
||||||
}
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
Picker("", selection: $selectedLanguage) {
|
Picker("", selection: $selectedLanguage) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
final class Content: ObservableObject {
|
final class Content: ObservableObject {
|
||||||
|
|
||||||
@ -19,7 +20,64 @@ final class Content: ObservableObject {
|
|||||||
var files: [FileResources] = []
|
var files: [FileResources] = []
|
||||||
|
|
||||||
@AppStorage("contentPath")
|
@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) {
|
func generateFeed(for language: ContentLanguage, bookmarkKey: String) {
|
||||||
let posts = posts.map { $0.feedEntry(for: language) }
|
let posts = posts.map { $0.feedEntry(for: language) }
|
||||||
@ -60,13 +118,6 @@ final class Content: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func importOldContent() {
|
func importOldContent() {
|
||||||
let storage = Storage(baseFolder: URL(filePath: "/Users/ch/Downloads/Content"))
|
|
||||||
do {
|
|
||||||
try storage.createFolderStructure()
|
|
||||||
} catch {
|
|
||||||
print(error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let importer = Importer()
|
let importer = Importer()
|
||||||
do {
|
do {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
/**
|
/**
|
||||||
A localized page contains the page content of a single language,
|
A localized page contains the page content of a single language,
|
||||||
@ -69,4 +70,16 @@ final class LocalizedPage: ObservableObject {
|
|||||||
self.externalFiles = externalFiles
|
self.externalFiles = externalFiles
|
||||||
self.requiredFiles = requiredFiles
|
self.requiredFiles = requiredFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func editableTitle() -> Binding<String> {
|
||||||
|
Binding(
|
||||||
|
get: {
|
||||||
|
self.title
|
||||||
|
},
|
||||||
|
set: { newValue in
|
||||||
|
self.title = newValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ final class LocalizedPost: ObservableObject {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func editableContent() -> Binding<String> {
|
func editableContent() -> Binding<String> {
|
||||||
Binding(
|
Binding(
|
||||||
get: {
|
get: {
|
||||||
|
@ -59,7 +59,7 @@ final class Page: ObservableObject {
|
|||||||
self.tags = tags
|
self.tags = tags
|
||||||
}
|
}
|
||||||
|
|
||||||
func metadata(for language: ContentLanguage) -> LocalizedPage? {
|
func localized(in language: ContentLanguage) -> LocalizedPage {
|
||||||
switch language {
|
switch language {
|
||||||
case .german: return german
|
case .german: return german
|
||||||
case .english: return english
|
case .english: return english
|
||||||
|
@ -24,6 +24,13 @@ final class Tag: ObservableObject {
|
|||||||
var url: String {
|
var url: String {
|
||||||
"/tags/\(linkName).html"
|
"/tags/\(linkName).html"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func localized(in language: ContentLanguage) -> LocalizedTag {
|
||||||
|
switch language {
|
||||||
|
case .english: return english
|
||||||
|
case .german: return german
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Tag {
|
extension Tag {
|
||||||
|
24
CHDataManagement/Preview Content/Content+Mock.swift
Normal file
24
CHDataManagement/Preview Content/Content+Mock.swift
Normal 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)
|
||||||
|
}
|
@ -122,6 +122,20 @@ final class Storage {
|
|||||||
try loadAll(in: pagesFolder)
|
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
|
// MARK: Posts
|
||||||
|
|
||||||
/// The folder path where the markdown files of the posts are stored (by their unique id/url component)
|
/// The folder path where the markdown files of the posts are stored (by their unique id/url component)
|
||||||
|
17
CHDataManagement/Views/ColorPalette.swift
Normal file
17
CHDataManagement/Views/ColorPalette.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
28
CHDataManagement/Views/Generic/OptionalTextField.swift
Normal file
28
CHDataManagement/Views/Generic/OptionalTextField.swift
Normal 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
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,50 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import HighlightedTextEditor
|
||||||
|
|
||||||
struct PageDetailView: View {
|
struct PageDetailView: View {
|
||||||
|
|
||||||
@ObservedObject var page: Page
|
@ObservedObject
|
||||||
|
var page: Page
|
||||||
|
|
||||||
@Binding
|
@Environment(\.language)
|
||||||
var language: ContentLanguage
|
private var language
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var content: Content
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var pageContent: String = ""
|
||||||
|
|
||||||
var body: some View {
|
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 {
|
#Preview {
|
||||||
PageDetailView(page: .empty, language: .constant(.english))
|
PageDetailView(page: .empty)
|
||||||
}
|
}
|
||||||
|
38
CHDataManagement/Views/Pages/PageListView.swift
Normal file
38
CHDataManagement/Views/Pages/PageListView.swift
Normal 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())
|
||||||
|
}
|
61
CHDataManagement/Views/Posts/PagePickerView.swift
Normal file
61
CHDataManagement/Views/Posts/PagePickerView.swift
Normal 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)
|
||||||
|
}
|
@ -13,20 +13,18 @@ private struct CenteredPost<Content>: View where Content: View {
|
|||||||
HorizontalCenter {
|
HorizontalCenter {
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
.listRowBackground(PostList.background)
|
.listRowBackground(ColorPalette.listBackground)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PostList: View {
|
struct PostList: View {
|
||||||
|
|
||||||
static let background = Color(r: 2, g: 15, b: 26)
|
@EnvironmentObject
|
||||||
|
private var content: Content
|
||||||
@Binding
|
|
||||||
var posts: [Post]
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
if posts.isEmpty {
|
if content.posts.isEmpty {
|
||||||
CenteredPost {
|
CenteredPost {
|
||||||
Text("No posts yet.")
|
Text("No posts yet.")
|
||||||
.padding()
|
.padding()
|
||||||
@ -40,7 +38,7 @@ struct PostList: View {
|
|||||||
.padding()
|
.padding()
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
}
|
}
|
||||||
ForEach(posts) { post in
|
ForEach(content.posts) { post in
|
||||||
CenteredPost {
|
CenteredPost {
|
||||||
PostView(post: post)
|
PostView(post: post)
|
||||||
.frame(maxWidth: 600)
|
.frame(maxWidth: 600)
|
||||||
@ -50,7 +48,7 @@ struct PostList: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.background(PostList.background)
|
.background(ColorPalette.listBackground)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,10 +62,11 @@ struct PostList: View {
|
|||||||
tags: [],
|
tags: [],
|
||||||
german: .init(title: "Titel", content: "Text"),
|
german: .init(title: "Titel", content: "Text"),
|
||||||
english: .init(title: "Title", content: "Text"))
|
english: .init(title: "Title", content: "Text"))
|
||||||
posts.insert(post, at: 0)
|
content.posts.insert(post, at: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
PostList(posts: .constant([.mock, .fullMock]))
|
PostList()
|
||||||
|
.environmentObject(Content())
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,16 @@ struct PostView: View {
|
|||||||
@State
|
@State
|
||||||
private var showDatePicker = false
|
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 {
|
var body: some View {
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
if !post.localized(in: language).images.isEmpty {
|
if !post.localized(in: language).images.isEmpty {
|
||||||
@ -25,7 +35,7 @@ struct PostView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
Toggle("Draft", isOn: $post.isDraft)
|
Toggle("Draft", isOn: $post.isDraft)
|
||||||
}
|
}
|
||||||
.foregroundStyle(Color(r: 96, g: 186, b: 255))
|
.foregroundStyle(ColorPalette.postDate)
|
||||||
TextField("", text: post.localized(in: language).editableTitle())
|
TextField("", text: post.localized(in: language).editableTitle())
|
||||||
.font(.system(size: 24, weight: .bold))
|
.font(.system(size: 24, weight: .bold))
|
||||||
.foregroundStyle(Color.white)
|
.foregroundStyle(Color.white)
|
||||||
@ -46,7 +56,7 @@ struct PostView: View {
|
|||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(1, contentMode: .fit)
|
.aspectRatio(1, contentMode: .fit)
|
||||||
.frame(height: 18)
|
.frame(height: 18)
|
||||||
.foregroundColor(TagView.foreground)
|
.foregroundColor(ColorPalette.tagForeground)
|
||||||
.opacity(0.7)
|
.opacity(0.7)
|
||||||
.padding(.top, 3)
|
.padding(.top, 3)
|
||||||
}
|
}
|
||||||
@ -54,17 +64,31 @@ struct PostView: View {
|
|||||||
}
|
}
|
||||||
TextEditor(text: post.localized(in: language).editableContent())
|
TextEditor(text: post.localized(in: language).editableContent())
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.foregroundStyle(Color(r: 221, g: 221, b: 221))
|
.foregroundStyle(ColorPalette.postText)
|
||||||
.textEditorStyle(.plain)
|
.textEditorStyle(.plain)
|
||||||
.padding(.leading, -5)
|
.padding(.leading, -5)
|
||||||
.scrollDisabled(true)
|
.scrollDisabled(true)
|
||||||
|
HorizontalCenter {
|
||||||
|
Button(action: { showPagePicker = true }) {
|
||||||
|
Text(linkedPageText)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.foregroundStyle(ColorPalette.postDate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.background(Color(r: 4, g: 31, b: 52))
|
.background(ColorPalette.postBackground)
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
.sheet(isPresented: $showDatePicker) {
|
.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 {
|
List {
|
||||||
PostView(post: .fullMock)
|
PostView(post: .fullMock)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
.listRowBackground(Color(r: 2, g: 15, b: 26))
|
.listRowBackground(ColorPalette.listBackground)
|
||||||
.environment(\.language, ContentLanguage.german)
|
.environment(\.language, ContentLanguage.german)
|
||||||
PostView(post: .mock)
|
PostView(post: .mock)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
.listRowBackground(Color(r: 2, g: 15, b: 26))
|
.listRowBackground(ColorPalette.listBackground)
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,6 @@ import SFSafeSymbols
|
|||||||
|
|
||||||
struct TagView: View {
|
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)
|
@Environment(\.language)
|
||||||
var language: ContentLanguage
|
var language: ContentLanguage
|
||||||
|
|
||||||
@ -37,11 +33,11 @@ struct TagView: View {
|
|||||||
.opacity(0.7)
|
.opacity(0.7)
|
||||||
.padding(.leading, -5)
|
.padding(.leading, -5)
|
||||||
}
|
}
|
||||||
.foregroundColor(TagView.foreground)
|
.foregroundColor(ColorPalette.tagForeground)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.background(TagView.background)
|
.background(ColorPalette.tagBackground)
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
53
CHDataManagement/Views/Tags/TagDetailView.swift
Normal file
53
CHDataManagement/Views/Tags/TagDetailView.swift
Normal 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)
|
||||||
|
}
|
36
CHDataManagement/Views/Tags/TagsListView.swift
Normal file
36
CHDataManagement/Views/Tags/TagsListView.swift
Normal 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())
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user