Show file list and contents
This commit is contained in:
parent
c3309197c0
commit
f2d78aef93
@ -63,6 +63,9 @@
|
||||
E25DA56D2D00EBCF00AEF16D /* NavigationBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */; };
|
||||
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */; };
|
||||
E25DA5712D01015400AEF16D /* GenerationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5702D01015400AEF16D /* GenerationSettingsView.swift */; };
|
||||
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5722D018AA100AEF16D /* FileContentView.swift */; };
|
||||
E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5742D018B6100AEF16D /* FileDetailView.swift */; };
|
||||
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5762D018B9500AEF16D /* File+Mock.swift */; };
|
||||
E2A21C012CB16A820060935B /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C002CB16A820060935B /* PostView.swift */; };
|
||||
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C022CB16C220060935B /* Environment+Language.swift */; };
|
||||
E2A21C052CB1766C0060935B /* LocalizedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C042CB176670060935B /* LocalizedText.swift */; };
|
||||
@ -170,6 +173,9 @@
|
||||
E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettingsView.swift; sourceTree = "<group>"; };
|
||||
E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedSettingsView.swift; sourceTree = "<group>"; };
|
||||
E25DA5702D01015400AEF16D /* GenerationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationSettingsView.swift; sourceTree = "<group>"; };
|
||||
E25DA5722D018AA100AEF16D /* FileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileContentView.swift; sourceTree = "<group>"; };
|
||||
E25DA5742D018B6100AEF16D /* FileDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDetailView.swift; sourceTree = "<group>"; };
|
||||
E25DA5762D018B9500AEF16D /* File+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "File+Mock.swift"; sourceTree = "<group>"; };
|
||||
E2A21C002CB16A820060935B /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
|
||||
E2A21C022CB16C220060935B /* Environment+Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Language.swift"; sourceTree = "<group>"; };
|
||||
E2A21C042CB176670060935B /* LocalizedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedText.swift; sourceTree = "<group>"; };
|
||||
@ -336,6 +342,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2A21C532CBBF87A0060935B /* FilesView.swift */,
|
||||
E25DA5722D018AA100AEF16D /* FileContentView.swift */,
|
||||
E25DA5742D018B6100AEF16D /* FileDetailView.swift */,
|
||||
);
|
||||
path = Files;
|
||||
sourceTree = "<group>";
|
||||
@ -496,6 +504,7 @@
|
||||
E2DD047C2C276F32003BFF1F /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E25DA5762D018B9500AEF16D /* File+Mock.swift */,
|
||||
E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */,
|
||||
E218500A2CEE02FA0090B18B /* Content+Mock.swift */,
|
||||
E2A37D1A2CEA45530000979F /* Tag+Mock.swift */,
|
||||
@ -589,6 +598,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */,
|
||||
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */,
|
||||
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */,
|
||||
E2A21C162CB1A3C90060935B /* PostImageGalleryView.swift in Sources */,
|
||||
@ -644,12 +654,14 @@
|
||||
E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */,
|
||||
E2A21C202CB28ED20060935B /* MockImage.swift in Sources */,
|
||||
E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */,
|
||||
E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */,
|
||||
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */,
|
||||
E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */,
|
||||
E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */,
|
||||
E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */,
|
||||
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
|
||||
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */,
|
||||
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */,
|
||||
E25DA5432D0094A900AEF16D /* SettingsSidebar.swift in Sources */,
|
||||
E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */,
|
||||
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
|
||||
|
@ -14,3 +14,22 @@ final class FileResource: ObservableObject {
|
||||
self.description = description
|
||||
}
|
||||
}
|
||||
|
||||
extension FileResource: Identifiable {
|
||||
|
||||
var id: String { uniqueId }
|
||||
}
|
||||
|
||||
extension FileResource: Equatable {
|
||||
|
||||
static func == (lhs: FileResource, rhs: FileResource) -> Bool {
|
||||
lhs.uniqueId == rhs.uniqueId
|
||||
}
|
||||
}
|
||||
|
||||
extension FileResource: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(uniqueId)
|
||||
}
|
||||
}
|
||||
|
7
CHDataManagement/Preview Content/File+Mock.swift
Normal file
7
CHDataManagement/Preview Content/File+Mock.swift
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
extension FileResource {
|
||||
|
||||
static var mock: FileResource {
|
||||
.init(uniqueId: "my-file.txt", description: "Some text file")
|
||||
}
|
||||
}
|
@ -7,6 +7,30 @@ enum SecurityScopeBookmark: String {
|
||||
case contentPath = "contentPathBookmark"
|
||||
}
|
||||
|
||||
enum StorageAccessError: Error {
|
||||
|
||||
case noBookmarkData
|
||||
|
||||
case bookmarkDataCorrupted(Error)
|
||||
|
||||
case folderAccessFailed(URL)
|
||||
|
||||
}
|
||||
|
||||
extension StorageAccessError: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .noBookmarkData:
|
||||
return "No bookmark data to access resources in folder"
|
||||
case .bookmarkDataCorrupted(let error):
|
||||
return "Failed to resolve bookmark: \(error)"
|
||||
case .folderAccessFailed(let url):
|
||||
return "Failed to access folder: \(url.path())"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A class that handles the storage of the website data.
|
||||
|
||||
@ -218,6 +242,9 @@ final class Storage {
|
||||
filesFolder.appending(path: file, directoryHint: .notDirectory)
|
||||
}
|
||||
|
||||
/**
|
||||
Copy an external file to the content folder
|
||||
*/
|
||||
@discardableResult
|
||||
func copyFile(at url: URL, fileId: String) -> Bool {
|
||||
let contentUrl = fileUrl(file: fileId)
|
||||
@ -234,6 +261,15 @@ final class Storage {
|
||||
try deleteFiles(in: filesFolder, notIn: Set(fileSet))
|
||||
}
|
||||
|
||||
func fileContent(for file: String) throws -> String {
|
||||
try operate(in: .contentPath) { folder in
|
||||
let fileUrl = folder
|
||||
.appending(path: "files", directoryHint: .isDirectory)
|
||||
.appending(path: file, directoryHint: .notDirectory)
|
||||
return try String(contentsOf: fileUrl, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Website data
|
||||
|
||||
private var settingsDataUrl: URL {
|
||||
@ -284,18 +320,25 @@ final class Storage {
|
||||
}
|
||||
|
||||
func write(in scope: SecurityScopeBookmark, operation: (URL) -> Bool) -> Bool {
|
||||
guard let bookmarkData = UserDefaults.standard.data(forKey: scope.rawValue) else {
|
||||
print("No bookmark data to access folder")
|
||||
do {
|
||||
return try operate(in: scope, operation: operation)
|
||||
} catch {
|
||||
print(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func operate<T>(in scope: SecurityScopeBookmark, operation: (URL) throws -> T) throws -> T {
|
||||
guard let bookmarkData = UserDefaults.standard.data(forKey: scope.rawValue) else {
|
||||
throw StorageAccessError.noBookmarkData
|
||||
}
|
||||
var isStale = false
|
||||
let folderURL: URL
|
||||
let folderUrl: URL
|
||||
do {
|
||||
// Resolve the bookmark to get the folder URL
|
||||
folderURL = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
|
||||
folderUrl = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
|
||||
} catch {
|
||||
print("Failed to resolve bookmark: \(error)")
|
||||
return false
|
||||
throw StorageAccessError.bookmarkDataCorrupted(error)
|
||||
}
|
||||
|
||||
if isStale {
|
||||
@ -303,14 +346,11 @@ final class Storage {
|
||||
}
|
||||
|
||||
// Start accessing the security-scoped resource
|
||||
if folderURL.startAccessingSecurityScopedResource() {
|
||||
let result = operation(folderURL)
|
||||
folderURL.stopAccessingSecurityScopedResource()
|
||||
return result
|
||||
} else {
|
||||
print("Failed to access folder: \(folderURL.path)")
|
||||
return false
|
||||
guard folderUrl.startAccessingSecurityScopedResource() else {
|
||||
throw StorageAccessError.folderAccessFailed(folderUrl)
|
||||
}
|
||||
defer { folderUrl.stopAccessingSecurityScopedResource() }
|
||||
return try operation(folderUrl)
|
||||
}
|
||||
|
||||
// MARK: Writing files
|
||||
|
39
CHDataManagement/Views/Files/FileContentView.swift
Normal file
39
CHDataManagement/Views/Files/FileContentView.swift
Normal file
@ -0,0 +1,39 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FileContentView: View {
|
||||
|
||||
@ObservedObject
|
||||
var file: FileResource
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@State
|
||||
private var fileContent: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if fileContent != "" {
|
||||
TextEditor(text: $fileContent)
|
||||
.font(.body.monospaced())
|
||||
.textEditorStyle(.plain)
|
||||
} else {
|
||||
Text("The file is not a text file")
|
||||
.onAppear(perform: loadFileContent)
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
|
||||
private func loadFileContent() {
|
||||
do {
|
||||
fileContent = try content.storage.fileContent(for: file.uniqueId)
|
||||
} catch {
|
||||
print(error)
|
||||
fileContent = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
FileContentView(file: .mock)
|
||||
}
|
26
CHDataManagement/Views/Files/FileDetailView.swift
Normal file
26
CHDataManagement/Views/Files/FileDetailView.swift
Normal file
@ -0,0 +1,26 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FileDetailView: View {
|
||||
|
||||
@ObservedObject
|
||||
var file: FileResource
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("File Name")
|
||||
.font(.headline)
|
||||
TextField("", text: $file.uniqueId)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
.disabled(true)
|
||||
Text("Description")
|
||||
.font(.headline)
|
||||
TextField("", text: $file.description)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
FileDetailView(file: .mock)
|
||||
}
|
@ -3,18 +3,76 @@ import SwiftUI
|
||||
struct FilesView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
var content: Content
|
||||
private var content: Content
|
||||
|
||||
@State
|
||||
private var selected: FileResource? = nil
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack {
|
||||
|
||||
NavigationSplitView {
|
||||
List(content.files, selection: $selected) { file in
|
||||
Text(file.uniqueId)
|
||||
.tag(file)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(action: openFilePanel) {
|
||||
Label("Add file", systemSymbol: .plus)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationSplitViewColumnWidth(min: 250, ideal: 250, max: 250)
|
||||
} content: {
|
||||
if let selected {
|
||||
FileContentView(file: selected)
|
||||
.id(selected.uniqueId)
|
||||
} else {
|
||||
Text("Select a file")
|
||||
}
|
||||
} detail: {
|
||||
if let selected {
|
||||
FileDetailView(file: selected)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
|
||||
private func openFilePanel() {
|
||||
let panel = NSOpenPanel()
|
||||
// Sets up so user can only select a single directory
|
||||
panel.canChooseFiles = true
|
||||
panel.canChooseDirectories = false
|
||||
panel.allowsMultipleSelection = true
|
||||
panel.showsHiddenFiles = false
|
||||
panel.title = "Select files to add"
|
||||
panel.prompt = ""
|
||||
|
||||
let response = panel.runModal()
|
||||
guard response == .OK else {
|
||||
print("Failed to select files to import")
|
||||
return
|
||||
}
|
||||
|
||||
for url in panel.urls {
|
||||
let fileId = url.lastPathComponent
|
||||
guard !content.files.contains(where: { $0.uniqueId == fileId }) else {
|
||||
print("A file '\(fileId)' already exists")
|
||||
continue
|
||||
}
|
||||
let file = FileResource(uniqueId: fileId, description: "")
|
||||
guard content.storage.copyFile(at: url, fileId: fileId) else {
|
||||
print("Failed to import file '\(fileId)'")
|
||||
continue
|
||||
}
|
||||
content.files.insert(file, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
FilesView()
|
||||
.environmentObject(Content.mock)
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ struct PageListView: View {
|
||||
} content: {
|
||||
if let selected {
|
||||
PageDetailView(page: selected)
|
||||
.id(selected.id)
|
||||
.layoutPriority(1)
|
||||
} else {
|
||||
// Fallback if no item is selected
|
||||
|
Loading…
x
Reference in New Issue
Block a user