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 */; };
|
E25DA56D2D00EBCF00AEF16D /* NavigationBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */; };
|
||||||
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */; };
|
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */; };
|
||||||
E25DA5712D01015400AEF16D /* GenerationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5702D01015400AEF16D /* GenerationSettingsView.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 */; };
|
E2A21C012CB16A820060935B /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C002CB16A820060935B /* PostView.swift */; };
|
||||||
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C022CB16C220060935B /* Environment+Language.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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
E2A21C042CB176670060935B /* LocalizedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedText.swift; sourceTree = "<group>"; };
|
||||||
@ -336,6 +342,8 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E2A21C532CBBF87A0060935B /* FilesView.swift */,
|
E2A21C532CBBF87A0060935B /* FilesView.swift */,
|
||||||
|
E25DA5722D018AA100AEF16D /* FileContentView.swift */,
|
||||||
|
E25DA5742D018B6100AEF16D /* FileDetailView.swift */,
|
||||||
);
|
);
|
||||||
path = Files;
|
path = Files;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -496,6 +504,7 @@
|
|||||||
E2DD047C2C276F32003BFF1F /* Preview Content */ = {
|
E2DD047C2C276F32003BFF1F /* Preview Content */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E25DA5762D018B9500AEF16D /* File+Mock.swift */,
|
||||||
E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */,
|
E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */,
|
||||||
E218500A2CEE02FA0090B18B /* Content+Mock.swift */,
|
E218500A2CEE02FA0090B18B /* Content+Mock.swift */,
|
||||||
E2A37D1A2CEA45530000979F /* Tag+Mock.swift */,
|
E2A37D1A2CEA45530000979F /* Tag+Mock.swift */,
|
||||||
@ -589,6 +598,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */,
|
||||||
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */,
|
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */,
|
||||||
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */,
|
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */,
|
||||||
E2A21C162CB1A3C90060935B /* PostImageGalleryView.swift in Sources */,
|
E2A21C162CB1A3C90060935B /* PostImageGalleryView.swift in Sources */,
|
||||||
@ -644,12 +654,14 @@
|
|||||||
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 */,
|
||||||
|
E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */,
|
||||||
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */,
|
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */,
|
||||||
E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */,
|
E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */,
|
||||||
E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */,
|
E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */,
|
||||||
E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */,
|
E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */,
|
||||||
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
|
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
|
||||||
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */,
|
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */,
|
||||||
|
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */,
|
||||||
E25DA5432D0094A900AEF16D /* SettingsSidebar.swift in Sources */,
|
E25DA5432D0094A900AEF16D /* SettingsSidebar.swift in Sources */,
|
||||||
E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */,
|
E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */,
|
||||||
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
|
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
|
||||||
|
@ -14,3 +14,22 @@ final class FileResource: ObservableObject {
|
|||||||
self.description = description
|
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"
|
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.
|
A class that handles the storage of the website data.
|
||||||
|
|
||||||
@ -218,6 +242,9 @@ final class Storage {
|
|||||||
filesFolder.appending(path: file, directoryHint: .notDirectory)
|
filesFolder.appending(path: file, directoryHint: .notDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Copy an external file to the content folder
|
||||||
|
*/
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func copyFile(at url: URL, fileId: String) -> Bool {
|
func copyFile(at url: URL, fileId: String) -> Bool {
|
||||||
let contentUrl = fileUrl(file: fileId)
|
let contentUrl = fileUrl(file: fileId)
|
||||||
@ -234,6 +261,15 @@ final class Storage {
|
|||||||
try deleteFiles(in: filesFolder, notIn: Set(fileSet))
|
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
|
// MARK: Website data
|
||||||
|
|
||||||
private var settingsDataUrl: URL {
|
private var settingsDataUrl: URL {
|
||||||
@ -284,18 +320,25 @@ final class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func write(in scope: SecurityScopeBookmark, operation: (URL) -> Bool) -> Bool {
|
func write(in scope: SecurityScopeBookmark, operation: (URL) -> Bool) -> Bool {
|
||||||
guard let bookmarkData = UserDefaults.standard.data(forKey: scope.rawValue) else {
|
do {
|
||||||
print("No bookmark data to access folder")
|
return try operate(in: scope, operation: operation)
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
return false
|
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
|
var isStale = false
|
||||||
let folderURL: URL
|
let folderUrl: URL
|
||||||
do {
|
do {
|
||||||
// Resolve the bookmark to get the folder URL
|
// 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 {
|
} catch {
|
||||||
print("Failed to resolve bookmark: \(error)")
|
throw StorageAccessError.bookmarkDataCorrupted(error)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if isStale {
|
if isStale {
|
||||||
@ -303,14 +346,11 @@ final class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start accessing the security-scoped resource
|
// Start accessing the security-scoped resource
|
||||||
if folderURL.startAccessingSecurityScopedResource() {
|
guard folderUrl.startAccessingSecurityScopedResource() else {
|
||||||
let result = operation(folderURL)
|
throw StorageAccessError.folderAccessFailed(folderUrl)
|
||||||
folderURL.stopAccessingSecurityScopedResource()
|
|
||||||
return result
|
|
||||||
} else {
|
|
||||||
print("Failed to access folder: \(folderURL.path)")
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
defer { folderUrl.stopAccessingSecurityScopedResource() }
|
||||||
|
return try operation(folderUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Writing files
|
// 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 {
|
struct FilesView: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
var content: Content
|
private var content: Content
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var selected: FileResource? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
NavigationSplitView {
|
||||||
VStack {
|
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 {
|
#Preview {
|
||||||
FilesView()
|
FilesView()
|
||||||
|
.environmentObject(Content.mock)
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ struct PageListView: View {
|
|||||||
} content: {
|
} content: {
|
||||||
if let selected {
|
if let selected {
|
||||||
PageDetailView(page: selected)
|
PageDetailView(page: selected)
|
||||||
|
.id(selected.id)
|
||||||
.layoutPriority(1)
|
.layoutPriority(1)
|
||||||
} else {
|
} else {
|
||||||
// Fallback if no item is selected
|
// Fallback if no item is selected
|
||||||
|
Loading…
x
Reference in New Issue
Block a user