Add “Add Folder/File” button

This commit is contained in:
1024jp 2024-05-16 23:20:53 +09:00
parent 383275a784
commit 5e845da3fb
5 changed files with 417 additions and 1 deletions

View File

@ -1,6 +1,82 @@
{
"sourceLanguage" : "en",
"strings" : {
"Add" : {
"localizations" : {
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Přidat"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hinzufügen"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Añadir"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ajouter"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Aggiungi"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "追加"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Voeg toe"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
"value" : "Adicionar"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ekle"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "添加"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "加入"
}
}
}
},
"Character" : {
"comment" : "section title in inspector\ntable column header",
"localizations" : {
@ -3368,6 +3444,160 @@
}
}
},
"New File" : {
"comment" : "menu item label",
"localizations" : {
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nový soubor"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Neue Datei"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "New File"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nuevo archivo"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nouveau fichier"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nuovo file"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "新規ファイル"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nieuw bestand"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
"value" : "Novo Arquivo"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Yeni Dosya"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "新建文件"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "新增檔案"
}
}
}
},
"New Folder" : {
"comment" : "menu item label",
"localizations" : {
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nová složka"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Neuer Ordner"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "New Folder"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nueva carpeta"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nouveau dossier"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nuova cartella"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "新規フォルダ"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nieuwe map"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nova Pasta"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Yeni Klasör"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "新建文件夹"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "新增檔案夾"
}
}
}
},
"Next Outline Item" : {
"comment" : "accessibility label for button",
"localizations" : {

View File

@ -5715,7 +5715,7 @@
}
},
"Untitled" : {
"comment" : "initial setting file name",
"comment" : "default file name for new creation\ninitial setting file name",
"localizations" : {
"cs" : {
"stringUnit" : {
@ -5791,6 +5791,83 @@
}
}
},
"untitled folder" : {
"comment" : "default folder name for new creation",
"localizations" : {
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bez názvu"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Neuer Ordner"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "untitled folder"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "carpeta sin título"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "dossier sans titre"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "cartella senza nome"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "名称未設定フォルダ"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "naamloze map"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
"value" : "pasta sem título"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "adsız klasör"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "未命名文件夹"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "未命名檔案夾"
}
}
}
},
"UpdatedByExternalProcessAlert.button.keep" : {
"comment" : "button label",
"extractionState" : "extracted_with_value",

View File

@ -252,6 +252,67 @@ import OSLog
}
/// Creates a empty file at the same level of the given fileURL.
///
/// - Parameter directoryURL: The URL of the directory where creates a new file.
/// - Returns: The URL of the created file.
@discardableResult func addFile(at directoryURL: URL) throws -> URL {
assert(directoryURL.hasDirectoryPath)
let name = String(localized: "Untitled", comment: "default file name for new creation")
let pathExtension = (try? SyntaxManager.shared.setting(name: UserDefaults.standard[.syntax]))?.extensions.first
let fileURL = directoryURL.appending(component: name).appendingPathExtension(pathExtension ?? "").appendingUniqueNumber()
var coordinationError: NSError?
var writingError: (any Error)?
let coordinator = NSFileCoordinator(filePresenter: self)
coordinator.coordinate(writingItemAt: fileURL, error: &coordinationError) { newURL in
do {
try Data().write(to: newURL, options: .withoutOverwriting)
} catch {
writingError = error
}
}
if let error = coordinationError ?? writingError {
throw error
}
return fileURL
}
/// Creates a folder at the same level of the given fileURL.
///
/// - Parameter directoryURL: The URL of the directory where creates a new folder.
/// - Returns: The URL of the created folder.
@discardableResult func addFolder(at directoryURL: URL) throws -> URL {
assert(directoryURL.hasDirectoryPath)
let name = String(localized: "untitled folder", comment: "default folder name for new creation")
let folderURL = directoryURL.appending(component: name).appendingUniqueNumber()
var coordinationError: NSError?
var writingError: (any Error)?
let coordinator = NSFileCoordinator(filePresenter: self)
coordinator.coordinate(writingItemAt: folderURL, error: &coordinationError) { newURL in
do {
try FileManager.default.createDirectory(at: newURL, withIntermediateDirectories: true)
} catch {
writingError = error
}
}
if let error = coordinationError ?? writingError {
throw error
}
return folderURL
}
/// Renames the file at the given `fileURL` with a new name.
///
/// - Parameters:

View File

@ -65,6 +65,36 @@ struct FileBrowserView: View {
let node = ids.first.flatMap { self.document.fileNode?.node(with: $0, keyPath: \.id) }
self.contextMenu(node: node)
}
HStack(spacing: 2) {
Menu(String(localized: "Add", table: "Document"), systemImage: "plus") {
if let fileURL = self.selectedNode?.directoryURL ?? self.document.fileURL {
Button(String(localized: "New File", table: "Document", comment: "menu item label")) {
do {
let url = try self.document.addFile(at: fileURL)
Task {
await self.document.openDocument(at: url)
}
} catch {
self.error = error
}
}
Button(String(localized: "New Folder", table: "Document", comment: "menu item label")) {
do {
try self.document.addFolder(at: fileURL)
} catch {
self.error = error
}
}
}
}
.menuIndicator(.hidden)
.labelStyle(.iconOnly)
Spacer()
}
.buttonStyle(.borderless)
.padding(6)
}
.onChange(of: self.selection) { (oldValue, _) in
guard let node = self.selectedNode, !node.isDirectory else { return }

View File

@ -74,6 +74,24 @@ extension URL {
}
/// Creates an URL with a unique filename at the same directory by appending a number before the path extension.
///
/// - Returns: A unique file URL, or `self` if it is already unique.
func appendingUniqueNumber() -> URL {
guard self.isReachable else { return self }
let pathExtension = self.pathExtension
let baseName = self.deletingPathExtension().lastPathComponent
let baseURL = self.deletingLastPathComponent()
return (2...).lazy
.map { "\(baseName) \($0)" }
.map { baseURL.appending(component: $0).appendingPathExtension(pathExtension) }
.first { !$0.isReachable }!
}
/// Checks the given URL is ancestor of the receiver.
///
/// - Parameter url: The child candidate URL.