mirror of
https://github.com/coteditor/CotEditor.git
synced 2024-09-11 19:27:30 +03:00
Add “Add Folder/File” button
This commit is contained in:
parent
383275a784
commit
5e845da3fb
@ -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" : {
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
|
@ -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 }
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user