347 lines
11 KiB
Swift
347 lines
11 KiB
Swift
import SwiftUI
|
|
|
|
|
|
|
|
private struct PageIssue {
|
|
|
|
let id: Int
|
|
|
|
let page: Page
|
|
|
|
let language: ContentLanguage
|
|
|
|
let message: PageContentAnomaly
|
|
|
|
init(page: Page, language: ContentLanguage, message: PageContentAnomaly) {
|
|
self.id = .random()
|
|
self.page = page
|
|
self.language = language
|
|
self.message = message
|
|
}
|
|
|
|
var title: String {
|
|
page.localized(in: language).title
|
|
}
|
|
}
|
|
|
|
extension PageIssue: Identifiable {
|
|
|
|
}
|
|
|
|
private struct FixSheet: View {
|
|
|
|
@Binding
|
|
var isPresented: Bool
|
|
|
|
@Binding
|
|
var message: String
|
|
|
|
@Binding
|
|
var infoItems: [String]
|
|
|
|
let action: () -> Void
|
|
|
|
init(isPresented: Binding<Bool>, message: Binding<String>, infoItems: Binding<[String]>, action: @escaping () -> Void) {
|
|
self._isPresented = isPresented
|
|
self._message = message
|
|
self._infoItems = infoItems
|
|
self.action = action
|
|
}
|
|
|
|
var body: some View {
|
|
VStack {
|
|
Text("Fix issue")
|
|
.font(.headline)
|
|
Text(message)
|
|
.font(.body)
|
|
List {
|
|
ForEach(infoItems, id: \.self) { item in
|
|
Text(item)
|
|
}
|
|
}
|
|
HStack {
|
|
Button("Fix", action: {
|
|
isPresented = false
|
|
action()
|
|
})
|
|
Button("Cancel", action: { isPresented = false })
|
|
}
|
|
}
|
|
.frame(minHeight: 200)
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
private struct ErrorSheet: View {
|
|
@Binding
|
|
var isPresented: Bool
|
|
|
|
@Binding
|
|
var message: String
|
|
|
|
|
|
var body: some View {
|
|
VStack {
|
|
Text("Error")
|
|
.font(.headline)
|
|
Text(message)
|
|
Button("Dismiss", action: { isPresented = false })
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PageSettingsContentView: View {
|
|
|
|
@EnvironmentObject
|
|
private var content: Content
|
|
|
|
@State
|
|
private var isCheckingPages: Bool = false
|
|
|
|
|
|
@State
|
|
private var issues: [PageIssue] = []
|
|
|
|
@State
|
|
private var message: String = "No fix available"
|
|
|
|
@State
|
|
private var infoItems: [String] = ["No items set"]
|
|
|
|
@State
|
|
private var fixAction: () -> () = {
|
|
print("No fix action defined")
|
|
}
|
|
|
|
@State
|
|
private var showFixActionSheet: Bool = false
|
|
|
|
@State
|
|
private var errorMessage: String = ""
|
|
|
|
@State
|
|
private var showErrorAlert: Bool = false
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading) {
|
|
HStack {
|
|
Button("Check pages", action: checkAllPagesForErrors)
|
|
.disabled(isCheckingPages)
|
|
Button("Fix all", action: applyAllEasyFixes)
|
|
if isCheckingPages {
|
|
ProgressView()
|
|
.progressViewStyle(.circular)
|
|
.frame(height: 20)
|
|
}
|
|
}
|
|
Text("\(issues.count) Issues")
|
|
.font(.headline)
|
|
List(issues) { issue in
|
|
HStack {
|
|
Button("Attempt Fix", action: { attemptFix(issue: issue) })
|
|
VStack(alignment: .leading) {
|
|
Text(issue.message.description)
|
|
Text("\(issue.title) (\(issue.language.rawValue.uppercased()))")
|
|
.font(.caption)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
.sheet(isPresented: $showFixActionSheet) {
|
|
FixSheet(isPresented: $showFixActionSheet,
|
|
message: $message,
|
|
infoItems: $infoItems) {
|
|
fixAction()
|
|
resetFixSheet()
|
|
}
|
|
}
|
|
.sheet(isPresented: $showErrorAlert) {
|
|
ErrorSheet(isPresented: $showErrorAlert, message: $errorMessage)
|
|
}
|
|
}
|
|
|
|
private func checkAllPagesForErrors() {
|
|
guard !isCheckingPages else {
|
|
return
|
|
}
|
|
isCheckingPages = true
|
|
issues = []
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
for language in ContentLanguage.allCases {
|
|
let parser = PageContentParser(
|
|
content: content,
|
|
language: language)
|
|
for page in content.pages {
|
|
analyze(page: page, parser: parser)
|
|
}
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self.isCheckingPages = false
|
|
}
|
|
}
|
|
}
|
|
|
|
private func analyze(page: Page, parser: PageContentParser) {
|
|
parser.reset()
|
|
do {
|
|
let rawPageContent = try content.storage.pageContent(for: page.id, language: parser.language)
|
|
_ = parser.generatePage(from: rawPageContent)
|
|
let results = parser.results.convertedWarnings.map {
|
|
PageIssue(page: page, language: parser.language, message: $0)
|
|
}
|
|
DispatchQueue.main.async {
|
|
issues = results + issues
|
|
}
|
|
} catch {
|
|
let message = PageContentAnomaly.failedToLoadContent(error)
|
|
let error = PageIssue(page: page, language: parser.language, message: message)
|
|
DispatchQueue.main.async {
|
|
issues.insert(error, at: 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func applyAllEasyFixes() {
|
|
issues.forEach { issue in
|
|
switch issue.message {
|
|
case .missingFile(let file):
|
|
fix(missingFile: file, in: issue.page, language: issue.language, ask: false)
|
|
case .unknownCommand(let string):
|
|
fixUnknownCommand(string, in: issue.page, language: issue.language)
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
private func attemptFix(issue: PageIssue) {
|
|
switch issue.message {
|
|
case .failedToLoadContent:
|
|
show(error: "No fix available for read errors")
|
|
case .missingFile(let string):
|
|
fix(missingFile: string, in: issue.page, language: issue.language)
|
|
case .missingPage(let string):
|
|
show(error: "No fix available for missing page \(string)")
|
|
case .unknownCommand(let string):
|
|
fixUnknownCommand(string, in: issue.page, language: issue.language)
|
|
case .invalidCommandArguments(let command, let arguments):
|
|
show(error: "No fix available for invalid arguments to command \(command) (\(arguments))")
|
|
case .missingTag(let string):
|
|
show(error: "No fix available for missing tag \(string)")
|
|
}
|
|
}
|
|
|
|
private func fix(missingFile: String, in page: Page, language: ContentLanguage, ask: Bool = true) {
|
|
print("Fixing missing file \(missingFile)")
|
|
let fileId = page.id + "-" + missingFile
|
|
if let file = content.file(id: fileId) {
|
|
replace(missingFile, with: file.id, in: page, language: language)
|
|
// Remove all errors of the page, and generate them new
|
|
recalculate(page: page, language: language)
|
|
return
|
|
}
|
|
guard ask else {
|
|
return
|
|
}
|
|
let partialMatches = content.files.filter { $0.id.contains(missingFile) }
|
|
guard partialMatches.count == 1 else {
|
|
show(error: "Found \(partialMatches.count) matches for file \(missingFile): \(partialMatches.map { $0.id })")
|
|
return
|
|
}
|
|
let file = partialMatches[0]
|
|
|
|
// Ask to fix partially matching file
|
|
let occurences = findOccurences(of: missingFile, in: page, language: language)
|
|
message = "Found file '\(file.id)' to match \(missingFile) on page '\(page.localized(in: language).title)'. Do you want to replace it?"
|
|
infoItems = occurences
|
|
fixAction = {
|
|
replace(missingFile, with: file.id, in: page, language: language)
|
|
// Remove all errors of the page, and generate them new
|
|
recalculate(page: page, language: language)
|
|
}
|
|
DispatchQueue.main.async {
|
|
showFixActionSheet = true
|
|
}
|
|
}
|
|
|
|
private func recalculate(page: Page, language: ContentLanguage) {
|
|
let remaining = issues.filter {
|
|
$0.language != language || $0.page.id != page.id
|
|
}
|
|
DispatchQueue.main.async {
|
|
self.issues = remaining
|
|
self.isCheckingPages = true
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
let parser = PageContentParser(content: content, language: language)
|
|
self.analyze(page: page, parser: parser)
|
|
self.isCheckingPages = false
|
|
}
|
|
}
|
|
}
|
|
|
|
private func resetFixSheet() {
|
|
DispatchQueue.main.async {
|
|
self.message = "No fix available"
|
|
self.fixAction = { print("No fix action defined") }
|
|
self.infoItems = ["No items set"]
|
|
}
|
|
}
|
|
|
|
private func show(error: String) {
|
|
DispatchQueue.main.async {
|
|
errorMessage = error
|
|
showErrorAlert = true
|
|
}
|
|
}
|
|
|
|
private func findMatchingFile(with missingFile: String, in page: Page) -> FileResource? {
|
|
let fileId = page.id + "-" + missingFile
|
|
if let file = content.file(id: fileId) {
|
|
return file
|
|
}
|
|
let partialMatches = content.files.filter { $0.id.contains(missingFile) }
|
|
if partialMatches.count == 1 {
|
|
return partialMatches[0]
|
|
}
|
|
show(error: "Found \(partialMatches.count) matches for file \(missingFile): \(partialMatches.map { $0.id })")
|
|
return nil
|
|
}
|
|
|
|
private func findOccurences(of searchString: String, in page: Page, language: ContentLanguage) -> [String] {
|
|
let parts: [String]
|
|
do {
|
|
parts = try content.storage.pageContent(for: page.id, language: language)
|
|
.components(separatedBy: searchString)
|
|
} catch {
|
|
show(error: "Failed to get page content to find occurences: \(error.localizedDescription)")
|
|
return []
|
|
}
|
|
|
|
var occurrences: [String] = []
|
|
for index in parts.indices.dropLast() {
|
|
let start = parts[index].suffix(10)
|
|
let end = parts[index+1].prefix(10)
|
|
let full = "...\(start)\(searchString)\(end)...".replacingOccurrences(of: "\n", with: "\\n")
|
|
occurrences.append(full)
|
|
}
|
|
return occurrences
|
|
}
|
|
|
|
private func replace(_ oldString: String, with newString: String, in page: Page, language: ContentLanguage) {
|
|
do {
|
|
let pageContent = try content.storage.pageContent(for: page.id, language: language)
|
|
.replacingOccurrences(of: oldString, with: newString)
|
|
try content.storage.save(pageContent: pageContent, for: page.id, language: language)
|
|
print("Replaced \(oldString) with \(newString) in page \(page.id) (\(language))")
|
|
} catch {
|
|
print("Failed to replace in page \(page.id) (\(language)): \(error)")
|
|
}
|
|
}
|
|
|
|
private func fixUnknownCommand(_ string: String, in page: Page, language: ContentLanguage) {
|
|
show(error: "No fix available for command '\(string)'")
|
|
}
|
|
}
|