consolidate tags, hide date
This commit is contained in:
parent
93e642c3c9
commit
5ac5a7b000
@ -213,6 +213,7 @@
|
|||||||
E2FE0F1E2D281AE1002963B7 /* TagOverviewGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */; };
|
E2FE0F1E2D281AE1002963B7 /* TagOverviewGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */; };
|
||||||
E2FE0F202D29A70E002963B7 /* Array+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */; };
|
E2FE0F202D29A70E002963B7 /* Array+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */; };
|
||||||
E2FE0F222D2A84A0002963B7 /* VideoCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */; };
|
E2FE0F222D2A84A0002963B7 /* VideoCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */; };
|
||||||
|
E2FE0F242D2A8C21002963B7 /* TagDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@ -417,6 +418,7 @@
|
|||||||
E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewGenerator.swift; sourceTree = "<group>"; };
|
E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewGenerator.swift; sourceTree = "<group>"; };
|
||||||
E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Remove.swift"; sourceTree = "<group>"; };
|
E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Remove.swift"; sourceTree = "<group>"; };
|
||||||
E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCommandProcessor.swift; sourceTree = "<group>"; };
|
E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCommandProcessor.swift; sourceTree = "<group>"; };
|
||||||
|
E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDisplayView.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -638,6 +640,7 @@
|
|||||||
E2A21C372CB9A4F10060935B /* Generic */ = {
|
E2A21C372CB9A4F10060935B /* Generic */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */,
|
||||||
E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */,
|
E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */,
|
||||||
E22990312D0F7678009F8D77 /* DatePropertyView.swift */,
|
E22990312D0F7678009F8D77 /* DatePropertyView.swift */,
|
||||||
E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */,
|
E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */,
|
||||||
@ -998,6 +1001,7 @@
|
|||||||
E229904E2D13535C009F8D77 /* SecurityBookmark.swift in Sources */,
|
E229904E2D13535C009F8D77 /* SecurityBookmark.swift in Sources */,
|
||||||
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
|
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
|
||||||
E29D313D2D047C1B0051B7F4 /* LocalizedPageContentView.swift in Sources */,
|
E29D313D2D047C1B0051B7F4 /* LocalizedPageContentView.swift in Sources */,
|
||||||
|
E2FE0F242D2A8C21002963B7 /* TagDisplayView.swift in Sources */,
|
||||||
E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */,
|
E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */,
|
||||||
E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */,
|
E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */,
|
||||||
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
|
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
|
||||||
|
@ -58,7 +58,7 @@ final class FeedPageGenerator {
|
|||||||
header: pageHeader,
|
header: pageHeader,
|
||||||
additionalFooter: footer) { content in
|
additionalFooter: footer) { content in
|
||||||
if showTitle {
|
if showTitle {
|
||||||
content += "<h1>\(title)</h1>"
|
content += "<h1 class='separated-headline'>\(title)</h1>"
|
||||||
}
|
}
|
||||||
for post in posts {
|
for post in posts {
|
||||||
content += FeedEntry(data: post).content
|
content += FeedEntry(data: post).content
|
||||||
|
@ -80,7 +80,9 @@ final class PageGenerator {
|
|||||||
additionalFooter: results.requiredFooters.sorted().joined()) { content in
|
additionalFooter: results.requiredFooters.sorted().joined()) { content in
|
||||||
content += "<article>"
|
content += "<article>"
|
||||||
if !localized.hideTitle {
|
if !localized.hideTitle {
|
||||||
|
if !page.hideDate {
|
||||||
content += "<h3>\(page.dateText(in: language))</h3>"
|
content += "<h3>\(page.dateText(in: language))</h3>"
|
||||||
|
}
|
||||||
content += "<h1>\(localized.title)</h1>"
|
content += "<h1>\(localized.title)</h1>"
|
||||||
content += TagList(tags: tags).content
|
content += TagList(tags: tags).content
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ final class TagOverviewGenerator {
|
|||||||
let page = GenericPage(
|
let page = GenericPage(
|
||||||
header: pageHeader,
|
header: pageHeader,
|
||||||
additionalFooter: "") { content in
|
additionalFooter: "") { content in
|
||||||
content += "<h1>\(header.title)</h1>"
|
content += "<h1 class='separated-headline'>\(header.title)</h1>"
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
let description = tag.localized.description ?? ""
|
let description = tag.localized.description ?? ""
|
||||||
let image = self.makePageImage(item: tag.localized)
|
let image = self.makePageImage(item: tag.localized)
|
||||||
|
@ -99,6 +99,28 @@ final class PageGenerationResults: ObservableObject {
|
|||||||
pageIsEmpty = false
|
pageIsEmpty = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reset() {
|
||||||
|
inaccessibleFiles = []
|
||||||
|
unparsableFiles = [:]
|
||||||
|
missingFiles = [:]
|
||||||
|
missingLinkedFiles = [:]
|
||||||
|
missingLinkedTags = [:]
|
||||||
|
missingLinkedPages = [:]
|
||||||
|
requiredHeaders = []
|
||||||
|
requiredFooters = []
|
||||||
|
requiredIcons = []
|
||||||
|
linkedPages = []
|
||||||
|
linkedTags = []
|
||||||
|
externalLinks = []
|
||||||
|
usedFiles = []
|
||||||
|
requiredFiles = []
|
||||||
|
imagesToGenerate = []
|
||||||
|
invalidCommands = []
|
||||||
|
warnings = []
|
||||||
|
unsavedOutputFiles = [:]
|
||||||
|
pageIsEmpty = false
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Adding entries
|
// MARK: Adding entries
|
||||||
|
|
||||||
func inaccessibleContent(file: FileResource) {
|
func inaccessibleContent(file: FileResource) {
|
||||||
|
@ -38,8 +38,7 @@ final class PostListPageGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>) {
|
private func makePostData(post: Post) -> FeedEntryData {
|
||||||
let posts: [FeedEntryData] = posts.map { post in
|
|
||||||
let localized: LocalizedPost = post.localized(in: language)
|
let localized: LocalizedPost = post.localized(in: language)
|
||||||
|
|
||||||
#warning("Add post link text to settings or to each post")
|
#warning("Add post link text to settings or to each post")
|
||||||
@ -49,7 +48,8 @@ final class PostListPageGenerator {
|
|||||||
text: language == .english ? "View" : "Anzeigen")
|
text: language == .english ? "View" : "Anzeigen")
|
||||||
}
|
}
|
||||||
|
|
||||||
let tags: [FeedEntryData.Tag] = post.tags.filter { $0.isVisible }.map { tag in
|
// Use the tags of the page if one is linked
|
||||||
|
let tags: [FeedEntryData.Tag] = post.usedTags().filter { $0.isVisible }.map { tag in
|
||||||
.init(name: tag.localized(in: language).name,
|
.init(name: tag.localized(in: language).name,
|
||||||
url: tag.absoluteUrl(in: language))
|
url: tag.absoluteUrl(in: language))
|
||||||
}
|
}
|
||||||
@ -65,10 +65,13 @@ final class PostListPageGenerator {
|
|||||||
textAboveTitle: post.dateText(in: language),
|
textAboveTitle: post.dateText(in: language),
|
||||||
link: linkUrl,
|
link: linkUrl,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
text: localized.text.components(separatedBy: "\n"),
|
text: localized.text.components(separatedBy: "\n\n"),
|
||||||
images: images)
|
images: images)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>) {
|
||||||
|
let posts: [FeedEntryData] = posts.map(makePostData)
|
||||||
|
|
||||||
let feedPageGenerator = FeedPageGenerator(content: source.content, results: source.results)
|
let feedPageGenerator = FeedPageGenerator(content: source.content, results: source.results)
|
||||||
|
|
||||||
let languageButtonUrl = "/" + pageUrl(in: language.next, pageNumber: pageIndex)
|
let languageButtonUrl = "/" + pageUrl(in: language.next, pageNumber: pageIndex)
|
||||||
|
@ -1,29 +1,40 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SFSafeSymbols
|
import SFSafeSymbols
|
||||||
|
|
||||||
#warning("Fix podcast")
|
/**
|
||||||
#warning("Fix endeavor basics (image compare)")
|
**Content**
|
||||||
#warning("Fix cap mosaic GIF")
|
- Podcast: Fix audio player, preview image
|
||||||
|
- Endeavor Basics: -> image compare command
|
||||||
|
- Article Cap Mosaic: -> GIF feature
|
||||||
|
- iPhone Backgrounds: Add page, html
|
||||||
|
|
||||||
#warning("Add custom url string to external files (optional)")
|
**UI**
|
||||||
#warning("Show all warnings on page content")
|
- Image search: Add view to see all images and filter
|
||||||
#warning("Button to delete file, show in finder, replace, mark changed (-> images)")
|
- Pages: Show linking posts
|
||||||
#warning("Transfer images of posts to other language")
|
- Page Content: Show all results of `PageGenerationResults`
|
||||||
#warning("Show tag selection view for pages")
|
- Files: Show usages of file
|
||||||
#warning("Button to replace files")
|
- Images: Show list of generated versions
|
||||||
#warning("Replace links to files inside pages when id changes")
|
|
||||||
#warning("Calculate file sizes")
|
**Features**
|
||||||
#warning("Specify image aspect ratio to prevent page jumps")
|
- GIF Support (No image set, don't rescale)
|
||||||
#warning("Add version and source url properties to file resources")
|
- Image compare command ``
|
||||||
#warning("Show errors during loading of content")
|
- Files: Optional Property `customFilePath` for external files to place them in another location
|
||||||
#warning("Generate pages for posts")
|
- Files: Property `version` and `sourceUrl` to track asset files
|
||||||
#warning("Clean up mock content")
|
- Posts: Generate separate pages for posts to link to
|
||||||
#warning("Show posts linking to a page")
|
- Settings: Introduce `Authors` (`name`, `image`, `description`)
|
||||||
#warning("Add author to settings and page headers")
|
- Page: Property `author`
|
||||||
#warning("Check for files in output folder not generated by app")
|
- Video: Specify versions
|
||||||
#warning("Fix GIFs: Don't rescale, don't use image set")
|
|
||||||
#warning("Add view to browse images")
|
**Generation**
|
||||||
#warning("Show warnings for empty item properties")
|
- ImageSet: Specify image aspect ratio (width, height) to prevent page jumps
|
||||||
|
- 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
|
||||||
|
*/
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct MainView: App {
|
struct MainView: App {
|
||||||
|
@ -34,8 +34,10 @@ extension Content {
|
|||||||
func check(content: String, of page: Page, for language: ContentLanguage, onComplete: @escaping (PageGenerationResults) -> Void) {
|
func check(content: String, of page: Page, for language: ContentLanguage, onComplete: @escaping (PageGenerationResults) -> Void) {
|
||||||
performGenerationIfIdle {
|
performGenerationIfIdle {
|
||||||
let results = self.results.makeResults(for: page, in: language)
|
let results = self.results.makeResults(for: page, in: language)
|
||||||
|
results.reset()
|
||||||
let generator = PageContentParser(content: page.content, language: language, results: results)
|
let generator = PageContentParser(content: page.content, language: language, results: results)
|
||||||
_ = generator.generatePage(from: content)
|
_ = generator.generatePage(from: content)
|
||||||
|
self.results.recalculate()
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
onComplete(results)
|
onComplete(results)
|
||||||
}
|
}
|
||||||
@ -227,7 +229,7 @@ extension Content {
|
|||||||
for language in ContentLanguage.allCases {
|
for language in ContentLanguage.allCases {
|
||||||
let results = results.makeResults(for: tag, in: language)
|
let results = results.makeResults(for: tag, in: language)
|
||||||
|
|
||||||
let posts = posts.filter { $0.tags.contains(tag) }
|
let posts = posts.filter { $0.contains(tag) }
|
||||||
guard posts.count > 0 else { continue }
|
guard posts.count > 0 else { continue }
|
||||||
|
|
||||||
let source = TagPageGeneratorSource(
|
let source = TagPageGeneratorSource(
|
||||||
|
@ -180,6 +180,7 @@ extension Content {
|
|||||||
externalLink: page.externalLink,
|
externalLink: page.externalLink,
|
||||||
isDraft: page.isDraft,
|
isDraft: page.isDraft,
|
||||||
createdDate: page.createdDate,
|
createdDate: page.createdDate,
|
||||||
|
hideDate: page.hideDate ?? false,
|
||||||
startDate: page.startDate,
|
startDate: page.startDate,
|
||||||
endDate: page.endDate,
|
endDate: page.endDate,
|
||||||
german: convert(page.german, images: files),
|
german: convert(page.german, images: files),
|
||||||
|
@ -59,6 +59,7 @@ private extension Page {
|
|||||||
.init(isDraft: isDraft,
|
.init(isDraft: isDraft,
|
||||||
externalLink: externalLink,
|
externalLink: externalLink,
|
||||||
tags: tags.map { $0.id },
|
tags: tags.map { $0.id },
|
||||||
|
hideDate: hideDate ? true : nil,
|
||||||
createdDate: createdDate,
|
createdDate: createdDate,
|
||||||
startDate: startDate,
|
startDate: startDate,
|
||||||
endDate: hasEndDate ? endDate : nil,
|
endDate: hasEndDate ? endDate : nil,
|
||||||
|
@ -49,6 +49,11 @@ final class LocalizedTag: ObservableObject {
|
|||||||
content.isValidIdForTagOrPageOrPost(urlComponent) &&
|
content.isValidIdForTagOrPageOrPost(urlComponent) &&
|
||||||
!content.containsTag(withUrlComponent: urlComponent)
|
!content.containsTag(withUrlComponent: urlComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The title to display when considering multiple items of this tag
|
||||||
|
var title: String {
|
||||||
|
linkPreviewTitle ?? name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalizedTag: LinkPreviewItem {
|
extension LocalizedTag: LinkPreviewItem {
|
||||||
|
@ -17,6 +17,9 @@ final class Page: Item {
|
|||||||
@Published
|
@Published
|
||||||
var createdDate: Date
|
var createdDate: Date
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var hideDate: Bool
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var startDate: Date
|
var startDate: Date
|
||||||
|
|
||||||
@ -46,6 +49,7 @@ final class Page: Item {
|
|||||||
externalLink: String?,
|
externalLink: String?,
|
||||||
isDraft: Bool,
|
isDraft: Bool,
|
||||||
createdDate: Date,
|
createdDate: Date,
|
||||||
|
hideDate: Bool,
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date?,
|
endDate: Date?,
|
||||||
german: LocalizedPage,
|
german: LocalizedPage,
|
||||||
@ -55,6 +59,7 @@ final class Page: Item {
|
|||||||
self.externalLink = externalLink
|
self.externalLink = externalLink
|
||||||
self.isDraft = isDraft
|
self.isDraft = isDraft
|
||||||
self.createdDate = createdDate
|
self.createdDate = createdDate
|
||||||
|
self.hideDate = hideDate
|
||||||
self.startDate = startDate
|
self.startDate = startDate
|
||||||
self.hasEndDate = endDate != nil
|
self.hasEndDate = endDate != nil
|
||||||
self.endDate = endDate ?? startDate
|
self.endDate = endDate ?? startDate
|
||||||
@ -86,6 +91,34 @@ final class Page: Item {
|
|||||||
externalLink != nil
|
externalLink != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Tags
|
||||||
|
|
||||||
|
/**
|
||||||
|
Check if a tag is associated with this page
|
||||||
|
*/
|
||||||
|
func contains(_ tag: Tag) -> Bool {
|
||||||
|
tags.contains(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggle(_ tag: Tag) {
|
||||||
|
guard let index = tags.firstIndex(of: tag) else {
|
||||||
|
tags.append(tag)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tags.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(_ tag: Tag) {
|
||||||
|
tags.remove(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func associate(_ tag: Tag) {
|
||||||
|
guard !tags.contains(tag) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tags.append(tag)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Paths
|
// MARK: Paths
|
||||||
|
|
||||||
override func absoluteUrl(in language: ContentLanguage) -> String {
|
override func absoluteUrl(in language: ContentLanguage) -> String {
|
||||||
|
@ -17,6 +17,12 @@ final class Post: Item {
|
|||||||
@Published
|
@Published
|
||||||
var endDate: Date
|
var endDate: Date
|
||||||
|
|
||||||
|
/**
|
||||||
|
The tags associated with the post
|
||||||
|
|
||||||
|
This list is only used if ``linkedPage`` is `nil`,
|
||||||
|
otherwise the tag list of the linked page is used.
|
||||||
|
*/
|
||||||
@Published
|
@Published
|
||||||
var tags: [Tag]
|
var tags: [Tag]
|
||||||
|
|
||||||
@ -52,6 +58,55 @@ final class Post: Item {
|
|||||||
super.init(content: content, id: id)
|
super.init(content: content, id: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Tags
|
||||||
|
|
||||||
|
func usedTags() -> [Tag] {
|
||||||
|
linkedPage?.tags ?? tags
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Check if a tag is associated with this post.
|
||||||
|
|
||||||
|
If the post is linked to a page, then the tags of the page are checked.
|
||||||
|
*/
|
||||||
|
func contains(_ tag: Tag) -> Bool {
|
||||||
|
guard let linkedPage else {
|
||||||
|
return tags.contains(tag)
|
||||||
|
}
|
||||||
|
return linkedPage.tags.contains(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggle(_ tag: Tag) {
|
||||||
|
if let linkedPage {
|
||||||
|
linkedPage.toggle(tag)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let index = tags.firstIndex(of: tag) else {
|
||||||
|
tags.append(tag)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tags.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(_ tag: Tag) {
|
||||||
|
if let linkedPage {
|
||||||
|
linkedPage.remove(tag)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tags.remove(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func associate(_ tag: Tag) {
|
||||||
|
if let linkedPage {
|
||||||
|
linkedPage.associate(tag)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard !tags.contains(tag) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tags.append(tag)
|
||||||
|
}
|
||||||
|
|
||||||
func localized(in language: ContentLanguage) -> LocalizedPost {
|
func localized(in language: ContentLanguage) -> LocalizedPost {
|
||||||
switch language {
|
switch language {
|
||||||
case .english: return english
|
case .english: return english
|
||||||
|
@ -56,7 +56,7 @@ final class Tag: Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func title(in language: ContentLanguage) -> String {
|
override func title(in language: ContentLanguage) -> String {
|
||||||
localized(in: language).name
|
localized(in: language).title
|
||||||
}
|
}
|
||||||
|
|
||||||
override var itemType: ItemType {
|
override var itemType: ItemType {
|
||||||
|
@ -9,6 +9,7 @@ extension Page {
|
|||||||
externalLink: nil,
|
externalLink: nil,
|
||||||
isDraft: true,
|
isDraft: true,
|
||||||
createdDate: Date(),
|
createdDate: Date(),
|
||||||
|
hideDate: false,
|
||||||
startDate: Date().addingTimeInterval(-86400),
|
startDate: Date().addingTimeInterval(-86400),
|
||||||
endDate: nil,
|
endDate: nil,
|
||||||
german: .german,
|
german: .german,
|
||||||
|
@ -8,6 +8,8 @@ struct PageFile {
|
|||||||
|
|
||||||
let tags: [String]
|
let tags: [String]
|
||||||
|
|
||||||
|
let hideDate: Bool?
|
||||||
|
|
||||||
let createdDate: Date
|
let createdDate: Date
|
||||||
|
|
||||||
let startDate: Date
|
let startDate: Date
|
||||||
|
42
CHDataManagement/Views/Generic/TagDisplayView.swift
Normal file
42
CHDataManagement/Views/Generic/TagDisplayView.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TagDisplayView: View {
|
||||||
|
|
||||||
|
@Environment(\.language)
|
||||||
|
private var language
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var content: Content
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var tags: [Tag]
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showTagPicker = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
FlowHStack {
|
||||||
|
ForEach(tags, id: \.id) { tag in
|
||||||
|
TagView(text: tag.localized(in: language).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)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showTagPicker) {
|
||||||
|
TagSelectionView(
|
||||||
|
presented: $showTagPicker,
|
||||||
|
selected: $tags,
|
||||||
|
tags: $content.tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -72,6 +72,7 @@ struct AddPageView: View {
|
|||||||
externalLink: nil,
|
externalLink: nil,
|
||||||
isDraft: true,
|
isDraft: true,
|
||||||
createdDate: .now,
|
createdDate: .now,
|
||||||
|
hideDate: false,
|
||||||
startDate: .now,
|
startDate: .now,
|
||||||
endDate: nil,
|
endDate: nil,
|
||||||
german: .init(content: content,
|
german: .init(content: content,
|
||||||
|
@ -24,9 +24,6 @@ struct PageContentView: View {
|
|||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var content: Content
|
private var content: Content
|
||||||
|
|
||||||
@State
|
|
||||||
private var showTagPicker = false
|
|
||||||
|
|
||||||
init(page: Page) {
|
init(page: Page) {
|
||||||
self.page = page
|
self.page = page
|
||||||
}
|
}
|
||||||
@ -44,38 +41,15 @@ struct PageContentView: View {
|
|||||||
}.padding()
|
}.padding()
|
||||||
} else {
|
} else {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
FlowHStack {
|
TagDisplayView(tags: $page.tags)
|
||||||
ForEach(page.tags, id: \.id) { tag in
|
|
||||||
TagView(text: tag.localized(in: language).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)
|
|
||||||
}
|
|
||||||
LocalizedPageContentView(pageId: page.id, page: page.localized(in: language), language: language)
|
LocalizedPageContentView(pageId: page.id, page: page.localized(in: language), language: language)
|
||||||
.id(page.id + language.rawValue)
|
.id(page.id + language.rawValue)
|
||||||
}.padding()
|
}
|
||||||
.sheet(isPresented: $showTagPicker) {
|
.padding()
|
||||||
TagSelectionView(
|
|
||||||
presented: $showTagPicker,
|
|
||||||
selected: $page.tags,
|
|
||||||
tags: $content.tags)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PageContentView: MainContentView {
|
extension PageContentView: MainContentView {
|
||||||
|
|
||||||
init(item: Page) {
|
init(item: Page) {
|
||||||
|
@ -67,6 +67,18 @@ struct PageDetailView: View {
|
|||||||
value: $page.isDraft,
|
value: $page.isDraft,
|
||||||
footer: "Indicate a page as a draft to hide it from the website")
|
footer: "Indicate a page as a draft to hide it from the website")
|
||||||
.disabled(page.isExternalUrl)
|
.disabled(page.isExternalUrl)
|
||||||
|
.onChange(of: page.externalLink) { _, newValue in
|
||||||
|
// Ensure that external pages are not drafts
|
||||||
|
if newValue != nil && page.isDraft {
|
||||||
|
page.isDraft = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BoolPropertyView(
|
||||||
|
title: "Hide date",
|
||||||
|
value: $page.hideDate,
|
||||||
|
footer: "Do not show the date string on the page")
|
||||||
|
.disabled(page.isExternalUrl)
|
||||||
|
|
||||||
DatePropertyView(
|
DatePropertyView(
|
||||||
title: "Start date",
|
title: "Start date",
|
||||||
|
@ -68,14 +68,21 @@ private struct LocalizedContentEditor: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct LinkedPageTagView: View {
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var page: Page
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TagDisplayView(tags: $page.tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct LocalizedPostContentView: View {
|
struct LocalizedPostContentView: View {
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var post: Post
|
var post: Post
|
||||||
|
|
||||||
@State
|
|
||||||
private var showTagPicker = false
|
|
||||||
|
|
||||||
@Environment(\.language)
|
@Environment(\.language)
|
||||||
private var language
|
private var language
|
||||||
|
|
||||||
@ -95,32 +102,14 @@ struct LocalizedPostContentView: View {
|
|||||||
}
|
}
|
||||||
PostImagesView(post: post.localized(in: language))
|
PostImagesView(post: post.localized(in: language))
|
||||||
LocalizedTitle(post: post.localized(in: language))
|
LocalizedTitle(post: post.localized(in: language))
|
||||||
FlowHStack {
|
if let page = post.linkedPage {
|
||||||
ForEach(post.tags, id: \.id) { tag in
|
LinkedPageTagView(page: page)
|
||||||
TagView(text: tag.localized(in: language).name)
|
} else {
|
||||||
.foregroundStyle(.white)
|
TagDisplayView(tags: $post.tags)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
LocalizedContentEditor(post: post.localized(in: language))
|
LocalizedContentEditor(post: post.localized(in: language))
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.sheet(isPresented: $showTagPicker) {
|
|
||||||
TagSelectionView(
|
|
||||||
presented: $showTagPicker,
|
|
||||||
selected: $post.tags,
|
|
||||||
tags: $content.tags)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func copyImagesFromOtherLanguage() {
|
private func copyImagesFromOtherLanguage() {
|
||||||
|
@ -76,6 +76,16 @@ struct PostDetailView: View {
|
|||||||
title: "Linked page",
|
title: "Linked page",
|
||||||
selectedPage: $post.linkedPage,
|
selectedPage: $post.linkedPage,
|
||||||
footer: "The page to open when clicking on the post")
|
footer: "The page to open when clicking on the post")
|
||||||
|
.onChange(of: post.linkedPage) { oldValue, newValue in
|
||||||
|
if newValue != nil {
|
||||||
|
post.tags = []
|
||||||
|
} else {
|
||||||
|
// Link removed, so copy tags from previous link
|
||||||
|
if let oldValue {
|
||||||
|
post.tags = oldValue.tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LocalizedPostDetailView(post: post.localized(in: language))
|
LocalizedPostDetailView(post: post.localized(in: language))
|
||||||
}
|
}
|
||||||
|
@ -223,6 +223,7 @@ struct PageIssueView: View {
|
|||||||
externalLink: nil,
|
externalLink: nil,
|
||||||
isDraft: true,
|
isDraft: true,
|
||||||
createdDate: .now,
|
createdDate: .now,
|
||||||
|
hideDate: false,
|
||||||
startDate: .now,
|
startDate: .now,
|
||||||
endDate: nil,
|
endDate: nil,
|
||||||
german: .init(content: content,
|
german: .init(content: content,
|
||||||
|
@ -14,24 +14,16 @@ private struct PageSelectionView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
let isSelected = page.tags.contains(tag)
|
let isSelected = page.contains(tag)
|
||||||
Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle)
|
Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle)
|
||||||
.foregroundStyle(Color.blue)
|
.foregroundStyle(Color.blue)
|
||||||
Text(page.localized(in: language).title)
|
Text(page.localized(in: language).title)
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
toggleTagAssignment()
|
page.toggle(tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func toggleTagAssignment() {
|
|
||||||
guard let index = page.tags.firstIndex(of: tag) else {
|
|
||||||
page.tags.append(tag)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
page.tags.remove(at: index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PageTagAssignmentView: View {
|
struct PageTagAssignmentView: View {
|
||||||
|
@ -14,24 +14,16 @@ private struct PostSelectionView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
let isSelected = post.tags.contains(tag)
|
let isSelected = post.contains(tag)
|
||||||
Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle)
|
Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle)
|
||||||
.foregroundStyle(Color.blue)
|
.foregroundStyle(Color.blue)
|
||||||
Text(post.localized(in: language).title)
|
Text(post.localized(in: language).title)
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
toggleTagAssignment()
|
post.toggle(tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func toggleTagAssignment() {
|
|
||||||
guard let index = post.tags.firstIndex(of: tag) else {
|
|
||||||
post.tags.append(tag)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
post.tags.remove(at: index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PostTagAssignmentView: View {
|
struct PostTagAssignmentView: View {
|
||||||
|
@ -22,7 +22,7 @@ struct TagContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var selectedPosts: [Post] {
|
var selectedPosts: [Post] {
|
||||||
content.posts.filter { $0.tags.contains(tag) }
|
content.posts.filter { $0.contains(tag) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user