Load incomplete content, show errors
This commit is contained in:
parent
2b88584ba1
commit
d556a51228
@ -14,6 +14,8 @@
|
|||||||
E20BCC9F2D53851400B8DBEB /* SelectableListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCC9E2D53850A00B8DBEB /* SelectableListItem.swift */; };
|
E20BCC9F2D53851400B8DBEB /* SelectableListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCC9E2D53850A00B8DBEB /* SelectableListItem.swift */; };
|
||||||
E20BCCA32D5398AA00B8DBEB /* LocalizedAudioSettingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCCA22D5398AA00B8DBEB /* LocalizedAudioSettingsDetailView.swift */; };
|
E20BCCA32D5398AA00B8DBEB /* LocalizedAudioSettingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCCA22D5398AA00B8DBEB /* LocalizedAudioSettingsDetailView.swift */; };
|
||||||
E20BCCAB2D53B86900B8DBEB /* GenerationResultsIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCCAA2D53B85300B8DBEB /* GenerationResultsIssueView.swift */; };
|
E20BCCAB2D53B86900B8DBEB /* GenerationResultsIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCCAA2D53B85300B8DBEB /* GenerationResultsIssueView.swift */; };
|
||||||
|
E20BCCAD2D53F48100B8DBEB /* IssueStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCCAC2D53F48100B8DBEB /* IssueStatus.swift */; };
|
||||||
|
E20BCCAF2D53F4A500B8DBEB /* GenerationStringIssuesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCCAE2D53F4A500B8DBEB /* GenerationStringIssuesView.swift */; };
|
||||||
E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850082CEE01BF0090B18B /* PagePickerView.swift */; };
|
E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850082CEE01BF0090B18B /* PagePickerView.swift */; };
|
||||||
E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500A2CEE02FA0090B18B /* Content+Mock.swift */; };
|
E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500A2CEE02FA0090B18B /* Content+Mock.swift */; };
|
||||||
E21850172CEE55FC0090B18B /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850162CEE55FB0090B18B /* FileType.swift */; };
|
E21850172CEE55FC0090B18B /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850162CEE55FB0090B18B /* FileType.swift */; };
|
||||||
@ -274,6 +276,8 @@
|
|||||||
E20BCC9E2D53850A00B8DBEB /* SelectableListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableListItem.swift; sourceTree = "<group>"; };
|
E20BCC9E2D53850A00B8DBEB /* SelectableListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableListItem.swift; sourceTree = "<group>"; };
|
||||||
E20BCCA22D5398AA00B8DBEB /* LocalizedAudioSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedAudioSettingsDetailView.swift; sourceTree = "<group>"; };
|
E20BCCA22D5398AA00B8DBEB /* LocalizedAudioSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedAudioSettingsDetailView.swift; sourceTree = "<group>"; };
|
||||||
E20BCCAA2D53B85300B8DBEB /* GenerationResultsIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResultsIssueView.swift; sourceTree = "<group>"; };
|
E20BCCAA2D53B85300B8DBEB /* GenerationResultsIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResultsIssueView.swift; sourceTree = "<group>"; };
|
||||||
|
E20BCCAC2D53F48100B8DBEB /* IssueStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueStatus.swift; sourceTree = "<group>"; };
|
||||||
|
E20BCCAE2D53F4A500B8DBEB /* GenerationStringIssuesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationStringIssuesView.swift; sourceTree = "<group>"; };
|
||||||
E21850082CEE01BF0090B18B /* PagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePickerView.swift; sourceTree = "<group>"; };
|
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>"; };
|
E218500A2CEE02FA0090B18B /* Content+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Mock.swift"; sourceTree = "<group>"; };
|
||||||
E21850162CEE55FB0090B18B /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = "<group>"; };
|
E21850162CEE55FB0090B18B /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = "<group>"; };
|
||||||
@ -542,6 +546,8 @@
|
|||||||
E20BCCA02D53985500B8DBEB /* Generation */ = {
|
E20BCCA02D53985500B8DBEB /* Generation */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E20BCCAE2D53F4A500B8DBEB /* GenerationStringIssuesView.swift */,
|
||||||
|
E20BCCAC2D53F48100B8DBEB /* IssueStatus.swift */,
|
||||||
E20BCCAA2D53B85300B8DBEB /* GenerationResultsIssueView.swift */,
|
E20BCCAA2D53B85300B8DBEB /* GenerationResultsIssueView.swift */,
|
||||||
E25DA5702D01015400AEF16D /* GenerationContentView.swift */,
|
E25DA5702D01015400AEF16D /* GenerationContentView.swift */,
|
||||||
);
|
);
|
||||||
@ -879,9 +885,9 @@
|
|||||||
E2B85F462C42C7CA0047CD0C /* Views */ = {
|
E2B85F462C42C7CA0047CD0C /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E20BCCA02D53985500B8DBEB /* Generation */,
|
|
||||||
E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */,
|
E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */,
|
||||||
E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */,
|
E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */,
|
||||||
|
E20BCCA02D53985500B8DBEB /* Generation */,
|
||||||
E2A21C372CB9A4F10060935B /* Generic */,
|
E2A21C372CB9A4F10060935B /* Generic */,
|
||||||
E2B85F4B2C4B8B7F0047CD0C /* Posts */,
|
E2B85F4B2C4B8B7F0047CD0C /* Posts */,
|
||||||
E2A21C322CB5BCAC0060935B /* Pages */,
|
E2A21C322CB5BCAC0060935B /* Pages */,
|
||||||
@ -1291,6 +1297,7 @@
|
|||||||
E22990242D0EDBD0009F8D77 /* HeaderElement.swift in Sources */,
|
E22990242D0EDBD0009F8D77 /* HeaderElement.swift in Sources */,
|
||||||
E29D31BC2D0DB5120051B7F4 /* CommandProcessor.swift in Sources */,
|
E29D31BC2D0DB5120051B7F4 /* CommandProcessor.swift in Sources */,
|
||||||
E2FE0F662D2C3B3A002963B7 /* LabelsBlock.swift in Sources */,
|
E2FE0F662D2C3B3A002963B7 /* LabelsBlock.swift in Sources */,
|
||||||
|
E20BCCAF2D53F4A500B8DBEB /* GenerationStringIssuesView.swift in Sources */,
|
||||||
E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */,
|
E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */,
|
||||||
E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */,
|
E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */,
|
||||||
E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */,
|
E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */,
|
||||||
@ -1375,6 +1382,7 @@
|
|||||||
E2FD1D642D47EF4200B48627 /* DetailListItem.swift in Sources */,
|
E2FD1D642D47EF4200B48627 /* DetailListItem.swift in Sources */,
|
||||||
E2FE0F0B2D2689FF002963B7 /* FeedGeneratorSource.swift in Sources */,
|
E2FE0F0B2D2689FF002963B7 /* FeedGeneratorSource.swift in Sources */,
|
||||||
E2DD04742C276F31003BFF1F /* MainView.swift in Sources */,
|
E2DD04742C276F31003BFF1F /* MainView.swift in Sources */,
|
||||||
|
E20BCCAD2D53F48100B8DBEB /* IssueStatus.swift in Sources */,
|
||||||
E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */,
|
E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */,
|
||||||
E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */,
|
E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */,
|
||||||
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */,
|
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */,
|
||||||
|
@ -23,7 +23,6 @@ import SFSafeSymbols
|
|||||||
|
|
||||||
**Fixes**
|
**Fixes**
|
||||||
- Files: Id change: Check all page contents for links to the renamed file and replace occurences
|
- Files: Id change: Check all page contents for links to the renamed file and replace occurences
|
||||||
- Database: Show errors during loading
|
|
||||||
- Investigate issue with spaces in content file names
|
- Investigate issue with spaces in content file names
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -20,6 +20,11 @@ struct StorageErrorView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minHeight: 300)
|
.frame(minHeight: 300)
|
||||||
|
if content.saveState == .savingPausedDueToLoadErrors {
|
||||||
|
Button("Allow saving", action: { content.resumeSavingAfterLoadingErrors() })
|
||||||
|
.padding()
|
||||||
|
Text("Saving has been disabled to prevent data corruption due to loading errors. Enable saving to save the partially loaded data.")
|
||||||
|
}
|
||||||
Button("Dismiss", action: { isPresented = false })
|
Button("Dismiss", action: { isPresented = false })
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,13 @@ extension Content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveIfNeeded() {
|
func saveIfNeeded() {
|
||||||
guard saveState != .isSaved else {
|
switch saveState {
|
||||||
|
case .isSaved, .savingPausedDueToLoadErrors, .storageNotInitialized:
|
||||||
return
|
return
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if Date.now.timeIntervalSince(lastModification) < 5 {
|
if Date.now.timeIntervalSince(lastModification) < 5 {
|
||||||
// Additional modification made
|
// Additional modification made
|
||||||
// Wait for next scheduled invocation of saveIfNeeded()
|
// Wait for next scheduled invocation of saveIfNeeded()
|
||||||
@ -43,7 +47,6 @@ extension Content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func saveToDisk() -> Bool {
|
private func saveToDisk() -> Bool {
|
||||||
guard didLoadContent else { return false }
|
|
||||||
guard storage.contentScope != nil else {
|
guard storage.contentScope != nil else {
|
||||||
print("Storage not initialized, not saving content")
|
print("Storage not initialized, not saving content")
|
||||||
return false
|
return false
|
||||||
|
@ -4,9 +4,6 @@ import Combine
|
|||||||
|
|
||||||
final class Content: ObservableObject {
|
final class Content: ObservableObject {
|
||||||
|
|
||||||
@Published
|
|
||||||
var didLoadContent = false
|
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var storage: Storage
|
var storage: Storage
|
||||||
|
|
||||||
@ -132,31 +129,37 @@ final class Content: ObservableObject {
|
|||||||
|
|
||||||
func loadFromDisk(callback: @escaping () -> ()) {
|
func loadFromDisk(callback: @escaping () -> ()) {
|
||||||
DispatchQueue.global().async {
|
DispatchQueue.global().async {
|
||||||
let loader = ModelLoader(content: self, storage: self.storage)
|
self.loadInBackground(callback: callback)
|
||||||
let result = loader.load()
|
|
||||||
guard result.errors.isEmpty else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.didLoadContent = false
|
|
||||||
self.storageErrors.append(contentsOf: result.errors)
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.files = result.files
|
|
||||||
self.posts = result.posts
|
|
||||||
self.pages = result.pages
|
|
||||||
self.tags = result.tags
|
|
||||||
self.settings = result.settings
|
|
||||||
self.tagOverview = result.tagOverview
|
|
||||||
self.didLoadContent = true
|
|
||||||
callback()
|
|
||||||
self.generateMissingVideoThumbnails()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadInBackground(callback: @escaping () -> ()) {
|
||||||
|
let loader = ModelLoader(content: self, storage: self.storage)
|
||||||
|
let result = loader.load()
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.files = result.files
|
||||||
|
self.posts = result.posts
|
||||||
|
self.pages = result.pages
|
||||||
|
self.tags = result.tags
|
||||||
|
self.settings = result.settings
|
||||||
|
self.tagOverview = result.tagOverview
|
||||||
|
self.storageErrors.append(contentsOf: result.errors)
|
||||||
|
if !result.errors.isEmpty {
|
||||||
|
self.saveState = .savingPausedDueToLoadErrors
|
||||||
|
} else {
|
||||||
|
self.saveState = .isSaved
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
self.generateMissingVideoThumbnails()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resumeSavingAfterLoadingErrors() {
|
||||||
|
saveState = .needsSave
|
||||||
|
saveIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
func generateMissingVideoThumbnails() {
|
func generateMissingVideoThumbnails() {
|
||||||
Task {
|
Task {
|
||||||
for file in self.files {
|
for file in self.files {
|
||||||
|
@ -3,6 +3,7 @@ import SwiftUICore
|
|||||||
|
|
||||||
enum SaveState {
|
enum SaveState {
|
||||||
case storageNotInitialized
|
case storageNotInitialized
|
||||||
|
case savingPausedDueToLoadErrors
|
||||||
case isSaved
|
case isSaved
|
||||||
case needsSave
|
case needsSave
|
||||||
case failedToSave
|
case failedToSave
|
||||||
@ -11,6 +12,8 @@ enum SaveState {
|
|||||||
switch self {
|
switch self {
|
||||||
case .storageNotInitialized:
|
case .storageNotInitialized:
|
||||||
return .folderCircleFill
|
return .folderCircleFill
|
||||||
|
case .savingPausedDueToLoadErrors:
|
||||||
|
return .exclamationmarkCircleFill
|
||||||
case .isSaved:
|
case .isSaved:
|
||||||
return .checkmarkCircleFill
|
return .checkmarkCircleFill
|
||||||
case .needsSave:
|
case .needsSave:
|
||||||
@ -28,7 +31,7 @@ enum SaveState {
|
|||||||
return .green
|
return .green
|
||||||
case .needsSave:
|
case .needsSave:
|
||||||
return .yellow
|
return .yellow
|
||||||
case .failedToSave:
|
case .failedToSave, .savingPausedDueToLoadErrors:
|
||||||
return .red
|
return .red
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,91 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SFSafeSymbols
|
import SFSafeSymbols
|
||||||
|
|
||||||
enum IssueStatus {
|
|
||||||
case nominal
|
|
||||||
case warning
|
|
||||||
case error
|
|
||||||
|
|
||||||
var symbol: SFSymbol {
|
|
||||||
switch self {
|
|
||||||
case .nominal: .checkmarkCircleFill
|
|
||||||
case .warning, .error: .exclamationmarkTriangle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var color: Color {
|
|
||||||
switch self {
|
|
||||||
case .nominal: .green
|
|
||||||
case .warning: .yellow
|
|
||||||
case .error: .red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GenerationStringIssuesView<T>: View where T: Hashable {
|
|
||||||
|
|
||||||
let text: String
|
|
||||||
|
|
||||||
let statusWhenNonEmpty: IssueStatus
|
|
||||||
|
|
||||||
@Binding
|
|
||||||
var items: Set<T>
|
|
||||||
|
|
||||||
let map: (T) -> String
|
|
||||||
|
|
||||||
@State
|
|
||||||
private var showList = false
|
|
||||||
|
|
||||||
var status: IssueStatus {
|
|
||||||
items.isEmpty ? .nominal : statusWhenNonEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
init(text: String, statusWhenNonEmpty: IssueStatus = .error, items: Binding<Set<T>>, map: @escaping (T) -> String) {
|
|
||||||
self.text = text
|
|
||||||
self.statusWhenNonEmpty = statusWhenNonEmpty
|
|
||||||
self._items = items
|
|
||||||
self.map = map
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
Button(action: showListIfNonEmpty) {
|
|
||||||
Image(systemSymbol: status.symbol)
|
|
||||||
.foregroundStyle(status.color)
|
|
||||||
}.buttonStyle(.plain)
|
|
||||||
Text("\(items.count) \(text)")
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showList) {
|
|
||||||
VStack {
|
|
||||||
Text("\(items.count) \(text)")
|
|
||||||
.font(.title)
|
|
||||||
List(items.map(map).sorted(), id: \.self) { item in
|
|
||||||
Text(item)
|
|
||||||
}
|
|
||||||
.frame(minHeight: 400)
|
|
||||||
Button("Close") { showList = false }
|
|
||||||
}.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func showListIfNonEmpty() {
|
|
||||||
guard !items.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
showList = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension GenerationStringIssuesView where T == String {
|
|
||||||
|
|
||||||
init(text: String, statusWhenNonEmpty: IssueStatus = .error, items: Binding<Set<String>>) {
|
|
||||||
self.text = text
|
|
||||||
self.statusWhenNonEmpty = statusWhenNonEmpty
|
|
||||||
self._items = items
|
|
||||||
self.map = { $0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GenerationResultsIssueView: View {
|
struct GenerationResultsIssueView: View {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
@ -117,9 +32,9 @@ struct GenerationResultsIssueView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func showListIfNonEmpty() {
|
private func showListIfNonEmpty() {
|
||||||
// guard !items.isEmpty else {
|
guard !items().isEmpty else {
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
showList = true
|
showList = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct GenerationStringIssuesView<T>: View where T: Hashable {
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
|
||||||
|
let statusWhenNonEmpty: IssueStatus
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var items: Set<T>
|
||||||
|
|
||||||
|
let map: (T) -> String
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showList = false
|
||||||
|
|
||||||
|
var status: IssueStatus {
|
||||||
|
items.isEmpty ? .nominal : statusWhenNonEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
init(text: String, statusWhenNonEmpty: IssueStatus = .error, items: Binding<Set<T>>, map: @escaping (T) -> String) {
|
||||||
|
self.text = text
|
||||||
|
self.statusWhenNonEmpty = statusWhenNonEmpty
|
||||||
|
self._items = items
|
||||||
|
self.map = map
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Button(action: showListIfNonEmpty) {
|
||||||
|
Image(systemSymbol: status.symbol)
|
||||||
|
.foregroundStyle(status.color)
|
||||||
|
}.buttonStyle(.plain)
|
||||||
|
Text("\(items.count) \(text)")
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showList) {
|
||||||
|
VStack {
|
||||||
|
Text("\(items.count) \(text)")
|
||||||
|
.font(.title)
|
||||||
|
List(items.map(map).sorted(), id: \.self) { item in
|
||||||
|
Text(item)
|
||||||
|
}
|
||||||
|
.frame(minHeight: 400)
|
||||||
|
Button("Close") { showList = false }
|
||||||
|
}.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showListIfNonEmpty() {
|
||||||
|
guard !items.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showList = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GenerationStringIssuesView where T == String {
|
||||||
|
|
||||||
|
init(text: String, statusWhenNonEmpty: IssueStatus = .error, items: Binding<Set<String>>) {
|
||||||
|
self.text = text
|
||||||
|
self.statusWhenNonEmpty = statusWhenNonEmpty
|
||||||
|
self._items = items
|
||||||
|
self.map = { $0 }
|
||||||
|
}
|
||||||
|
}
|
23
CHDataManagement/Views/Generation/IssueStatus.swift
Normal file
23
CHDataManagement/Views/Generation/IssueStatus.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import SFSafeSymbols
|
||||||
|
import SwiftUICore
|
||||||
|
|
||||||
|
enum IssueStatus {
|
||||||
|
case nominal
|
||||||
|
case warning
|
||||||
|
case error
|
||||||
|
|
||||||
|
var symbol: SFSymbol {
|
||||||
|
switch self {
|
||||||
|
case .nominal: .checkmarkCircleFill
|
||||||
|
case .warning, .error: .exclamationmarkTriangle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var color: Color {
|
||||||
|
switch self {
|
||||||
|
case .nominal: .green
|
||||||
|
case .warning: .yellow
|
||||||
|
case .error: .red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user