diff --git a/Extensions/Notification+Names.swift b/Extensions/Notification+Names.swift
new file mode 100644
index 00000000..c6811c65
--- /dev/null
+++ b/Extensions/Notification+Names.swift
@@ -0,0 +1,27 @@
+//
+// Notification+Names.swift
+// Nextcloud
+//
+// Created by oli-ver on 16/08/2025.
+// Copyright © 2025 oli-ver. All rights reserved.
+//
+// Author oli-ver
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+//
+import Foundation
+
+extension Notification.Name {
+ static let attachmentsPrefetched = Foundation.Notification.Name("com.nextcloud.notes.ios.attachmentsPrefetched")
+}
diff --git a/Networking/NoteSessionManager.swift b/Networking/NoteSessionManager.swift
index afba2ab0..cdb03c59 100644
--- a/Networking/NoteSessionManager.swift
+++ b/Networking/NoteSessionManager.swift
@@ -498,14 +498,56 @@ class NoteSessionManager {
}
let router = Router.getNote(id: Int(note.id), exclude: "", etag: note.etag)
let validStatusCode = KeychainHelper.notesApiVersion == Router.defaultApiVersion ? 200..<300 : 200..<201
+ let attachmentHelper = AttachmentHelper()
+
session
.request(router)
.validate(statusCode: validStatusCode)
.validate(contentType: [Router.applicationJson])
- .responseDecodable(of: NoteStruct.self) { response in
+ .responseDecodable(of: NoteStruct.self, decoder: JSONDecoder()) { (response: AFDataResponse) in
switch response.result {
case let .success(note):
CDNote.update(notes: [note])
+ self.logger.debug("Checking server API version \(KeychainHelper.notesApiVersion, privacy: .public) for compatibility for attachment download")
+ guard
+ KeychainHelper.notesApiVersionisAtLeast("1.4")
+ else {
+ self.logger.warning("Server with API version \(KeychainHelper.notesApiVersion, privacy: .public) does not support attachment download (API version >= 1.4), not parsing and downloading attachments")
+ return
+ }
+
+ self.logger.debug("Searching for attachments")
+ let paths: [String] = attachmentHelper.extractRelativeAttachmentPaths(from: note.content, removeUrlEncoding: true)
+ self.logger.debug("Found the paths: \(paths, privacy: .public)")
+
+ guard !paths.isEmpty else {
+ self.logger.notice("Searching for attachments completed, no paths found in note")
+ completion?()
+ return
+ }
+
+ let group = DispatchGroup()
+ for path in paths {
+ group.enter()
+ Task { [weak self] in
+ guard let self else { return }
+ do {
+ let data = try await self.getAttachment(noteId: Int(note.id), path: path)
+ self.logger.debug("Attachment for note ID \(note.id, privacy: .public) and path \(path, privacy: .public) downloaded successfully")
+ try AttachmentStore.shared.store(data: data, noteId: Int(note.id), path: path)
+ self.logger.notice("Attachment for note ID \(note.id, privacy: .public) and path \(path, privacy: .public) stored successfully")
+ } catch {
+ self.logger.error("Attachment download for note ID \(note.id, privacy: .public) and path \(path, privacy: .public) failed: \(error.localizedDescription, privacy: .public)")
+ }
+ }
+ group.leave()
+ }
+
+ group.notify(queue: .main) {
+ completion?()
+ }
+ return
+
case let .failure(error):
if let urlResponse = response.response {
switch urlResponse.statusCode {
@@ -527,7 +569,23 @@ class NoteSessionManager {
}
}
completion?()
- }
+ }
+ }
+
+
+
+ func getAttachment(noteId: Int, path: String) async throws -> Data {
+ logger.notice("Getting attachment for noteId: \(noteId, privacy: .public), path: \(path, privacy: .public)")
+ let router = Router.getAttachment(noteId: noteId, path: path)
+
+ return try await session
+ .request(router)
+ .onURLRequestCreation { req in
+ self.logger.debug("URL: \(req.url?.absoluteString ?? "nil", privacy: .public)")
+ }
+ .validate(statusCode: 200..<300)
+ .serializingData()
+ .value
}
func update(note: NoteProtocol, completion: SyncCompletionBlock? = nil) {
@@ -591,6 +649,39 @@ class NoteSessionManager {
}
}
+ func createAttachment(noteId: Int,
+ fileData: Data,
+ filename: String,
+ mimeType: String,
+ completion: @escaping (Result) -> Void) {
+ struct AttachmentResponse: Decodable {
+ let filename: String
+ }
+
+ let router = Router.createAttachment(noteId: noteId)
+
+ session
+ .upload(multipartFormData: { form in
+ form.append(fileData,
+ withName: "file",
+ fileName: filename,
+ mimeType: mimeType)
+ }, with: router)
+ .validate(statusCode: 200..<300)
+ .responseDecodable(of: AttachmentResponse.self) { response in
+ switch response.result {
+ case .success(let payload):
+ completion(.success(payload.filename))
+ case .failure(let error):
+ let message = ErrorMessage(
+ title: NSLocalizedString("Error Creating Attachment", comment: ""),
+ body: error.localizedDescription
+ )
+ completion(.failure(NoteError(message: message)))
+ }
+ }
+ }
+
func delete(note: NoteProtocol, completion: SyncCompletionBlock? = nil) {
logger.notice("Deleting note...")
diff --git a/Networking/Router.swift b/Networking/Router.swift
index 89a02e78..e5f5c6f7 100644
--- a/Networking/Router.swift
+++ b/Networking/Router.swift
@@ -16,7 +16,9 @@ import Version
enum Router: URLRequestConvertible {
case allNotes(exclude: String)
case getNote(id: Int, exclude: String, etag: String)
+ case getAttachment(noteId: Int, path: String)
case createNote(parameters: Parameters)
+ case createAttachment(noteId: Int)
case updateNote(id: Int, paramters: Parameters)
case deleteNote(id: Int)
case settings
@@ -27,9 +29,9 @@ enum Router: URLRequestConvertible {
var method: HTTPMethod {
switch self {
- case .allNotes, .getNote, .settings:
+ case .allNotes, .getAttachment, .getNote, .settings:
return .get
- case .createNote:
+ case .createNote, .createAttachment:
return .post
case .updateNote, .updateSettings:
return .put
@@ -44,6 +46,10 @@ enum Router: URLRequestConvertible {
return "/notes"
case .getNote(let id , _, _):
return "/notes/\(id)"
+ case .getAttachment(let noteId, _):
+ return "/attachment/\(noteId)"
+ case .createAttachment(let noteId):
+ return "/attachment/\(noteId)"
case .createNote:
return "/notes"
case .updateNote(let id, _):
@@ -110,9 +116,20 @@ enum Router: URLRequestConvertible {
urlRequest.headers.add(.ifNoneMatch(etag))
}
+ urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
+ case .getAttachment(_, let attachmentPath):
+ // TODO: Find out why only explicit API version is supported
+ let baseURLString = "\(server)/index.php/apps/notes/api/v1.4"
+ let url = try baseURLString.asURL()
+ urlRequest.url = url.appendingPathComponent(self.path)
+
+ let parameters = ["path": attachmentPath] as [String: Any]
urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
case .createNote(let parameters):
urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
+ case .createAttachment:
+ // Multipart body will be added by AF.upload; nothing to encode here.
+ break
case .updateNote(_, let parameters):
urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
case .updateSettings(let notesPath, let fileSuffix):
diff --git a/Shared/AttachmentHelper.swift b/Shared/AttachmentHelper.swift
new file mode 100644
index 00000000..5a43453e
--- /dev/null
+++ b/Shared/AttachmentHelper.swift
@@ -0,0 +1,84 @@
+//
+// AttachmentHelper.swift
+// iOCNotes
+//
+// Created by oli-ver on 16/08/2025.
+// Copyright © 2025 Nextcloud GmbH. All rights reserved.
+//
+
+import Foundation
+import os
+
+class AttachmentHelper{
+ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AttachmentHelper")
+
+ public func extractRelativeAttachmentPaths(from markdown: String, removeUrlEncoding: Bool) -> [String] {
+ logger.notice("Parsing markdown: \(markdown, privacy: .sensitive)")
+ // Parse markdown attachments: 
+ let mdImage = try! NSRegularExpression(
+ pattern: #"""
+ !\[
+ [^\]]* # alt text (everything until closing square bracket)
+ \]
+ \(
+ \s*
+ (?[^)\s]+) # URL: until closing paranthesis or first space
+ (?:\s+["'][^"']*["'])? # optional title in '...' or "..."
+ \s*
+ \)
+ """#,
+ options: [.allowCommentsAndWhitespace]
+ )
+
+ // Parse HTML images:
or
+ let htmlImg = try! NSRegularExpression(
+ pattern: #"
]*\bsrc\s*=\s*(['"])(?.*?)\1"#,
+ options: [.caseInsensitive]
+ )
+
+ func matches(_ regex: NSRegularExpression, in s: String) -> [String] {
+ let ns = s as NSString
+ return regex.matches(in: s, range: NSRange(location: 0, length: ns.length)).compactMap {
+ let r = $0.range(withName: "url")
+ guard r.location != NSNotFound else { return nil }
+ return ns.substring(with: r).trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+ }
+
+ // collect download candidates
+ let candidates = matches(mdImage, in: markdown) + matches(htmlImg, in: markdown)
+ logger.notice("Found the following path candidates: \(candidates, privacy: .public)")
+
+ // only retain relative paths
+
+ let filteredCandidates = candidates.filter { raw in
+ let urlString = raw.trimmingCharacters(in: .whitespacesAndNewlines)
+ if urlString.isEmpty { return false }
+ if urlString.hasPrefix("/") { return false }
+ if urlString.lowercased().hasPrefix("http://") { return false }
+ if urlString.lowercased().hasPrefix("https://") { return false }
+ if urlString.lowercased().hasPrefix("data:") { return false }
+ // Remove schemas if any
+ if let colon = urlString.firstIndex(of: ":"), urlString[.. Bool {
+ KeychainHelper.notesApiVersion.compare(minVersion, options: .numeric, range: nil, locale: nil) != .orderedAscending
+ }
static var notesVersion: String {
get {
diff --git a/Source/CoreData/AttachmentStore.swift b/Source/CoreData/AttachmentStore.swift
new file mode 100644
index 00000000..573d8e6d
--- /dev/null
+++ b/Source/CoreData/AttachmentStore.swift
@@ -0,0 +1,55 @@
+//
+// AttachmentStore.swift
+// iOCNotes
+//
+// Created by oli-ver on 15/08/25.
+// Copyright © 2025 Nextcloud GmbH. All rights reserved.
+//
+
+import Foundation
+import os
+
+final class AttachmentStore {
+ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AttachmentStore")
+ static let shared = AttachmentStore()
+ private let root: URL
+
+ private init(fileManager: FileManager = .default) {
+ let base = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
+ self.root = base.appendingPathComponent("NoteAttachments", isDirectory: true)
+ try? fileManager.createDirectory(at: root, withIntermediateDirectories: true)
+ }
+
+ // Normalizes relative paths and stores them in .../Caches/NoteAttachments//.
+ func fileURL(noteId: Int, relativePath: String) -> URL {
+ let safe = relativePath
+ .replacingOccurrences(of: "://", with: "_")
+ .trimmingCharacters(in: .whitespacesAndNewlines)
+ let noteFolder = root.appendingPathComponent(String(noteId), isDirectory: true)
+ return noteFolder.appendingPathComponent(safe)
+ }
+
+ func contains(noteId: Int, path: String) -> Bool {
+ FileManager.default.fileExists(atPath: fileURL(noteId: noteId, relativePath: path).path)
+ }
+
+ @discardableResult
+ func store(data: Data, noteId: Int, path: String) throws -> URL {
+ let url = fileURL(noteId: noteId, relativePath: path)
+ try FileManager.default.createDirectory(at: url.deletingLastPathComponent(),
+ withIntermediateDirectories: true)
+ try data.write(to: url, options: .atomic)
+ logger.debug("Stored \(path, privacy: .public) for noteId \(noteId, privacy: .public) with url \(url, privacy: .public)")
+ return url
+ }
+
+ func removeAll(for noteId: Int) {
+ let dir = root.appendingPathComponent(String(noteId), isDirectory: true)
+ try? FileManager.default.removeItem(at: dir)
+ }
+
+ func purgeAll() {
+ try? FileManager.default.removeItem(at: root)
+ try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
+ }
+}
diff --git a/Source/EditorViewController.swift b/Source/EditorViewController.swift
index 467ec30c..80b22f03 100644
--- a/Source/EditorViewController.swift
+++ b/Source/EditorViewController.swift
@@ -209,6 +209,7 @@ class EditorViewController: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showPreview" {
if let preview = segue.destination as? PreviewViewController, let note = note {
+ preview.noteId = note.id
preview.content = noteView.text
preview.noteTitle = note.title
preview.noteDate = noteView.headerLabel.text
diff --git a/Source/PreviewViewController.swift b/Source/PreviewViewController.swift
index 5f179aac..11f5950e 100644
--- a/Source/PreviewViewController.swift
+++ b/Source/PreviewViewController.swift
@@ -10,10 +10,11 @@ import UIKit
class PreviewViewController: UIViewController {
+ var noteId: Int64?
var content: String?
var noteTitle: String?
var noteDate: String?
-
+
override func viewDidLoad() {
super.viewDidLoad()
var previewContent = ""
@@ -23,11 +24,11 @@ class PreviewViewController: UIViewController {
if let noteDate = noteDate {
previewContent.append("*\(noteDate)*\n\n")
}
- if let content = content {
+ if let content = content, let noteId = noteId {
do {
previewContent.append(content)
- let previewWebView = try PreviewWebView(markdown: content) {
+ let previewWebView = try PreviewWebView(markdown: content, noteId: noteId) {
print("Markdown was rendered.")
}
diff --git a/Source/PreviewWebView.swift b/Source/PreviewWebView.swift
index ebae5453..76f67666 100644
--- a/Source/PreviewWebView.swift
+++ b/Source/PreviewWebView.swift
@@ -9,11 +9,14 @@
import cmark_gfm_swift
import UIKit
import WebKit
+import UniformTypeIdentifiers
+import os
typealias LoadCompletion = () -> Void
class PreviewWebView: WKWebView {
+ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PreviewWebView")
let bundle: Bundle
private lazy var baseURL: URL = {
@@ -38,19 +41,20 @@ class PreviewWebView: WKWebView {
.strikethrough // Strikethrough
]
- public init(markdown: String, completion: LoadCompletion? = nil) throws {
+ public init(markdown: String, noteId: Int64, completion: LoadCompletion? = nil) throws {
let bundleUrl = Bundle.main.url(forResource: "Preview", withExtension: "bundle")!
self.bundle = Bundle(url: bundleUrl)!
loadCompletion = completion
-
- super.init(frame: .zero, configuration: WKWebViewConfiguration())
+ let configuration = WKWebViewConfiguration()
+ configuration.setURLSchemeHandler(AttachmentSchemeHandler(), forURLScheme: AttachmentURL.scheme)
+ super.init(frame: .zero, configuration: configuration)
navigationDelegate = self
do {
- try loadHTML(markdown)
+ try loadHTML(markdown, noteId: noteId)
} catch {
- //
+ logger.error("Error when loading HTML: \(error, privacy: .public)")
}
}
@@ -61,11 +65,13 @@ class PreviewWebView: WKWebView {
}
private extension PreviewWebView {
-
- func loadHTML(_ markdown: String) throws {
- let htmlString = Node(markdown: markdown, options: options, extensions: extensions)?.html
-
- let pageHTMLString = try htmlFromTemplate(htmlString ?? markdown)
+
+ func loadHTML(_ markdown: String, noteId: Int64) throws {
+ let htmlString = Node(markdown: markdown, options: options, extensions: extensions)?.html ?? markdown
+ let attachmentHelper: AttachmentHelper = AttachmentHelper()
+ let relPaths = attachmentHelper.extractRelativeAttachmentPaths(from: markdown, removeUrlEncoding: false)
+ let htmlRewritten = rewriteAttachmentURLs(in: htmlString, noteId: noteId, relativePaths: relPaths)
+ let pageHTMLString = try htmlFromTemplate(htmlRewritten)
loadHTMLString(pageHTMLString, baseURL: baseURL)
}
@@ -74,6 +80,93 @@ private extension PreviewWebView {
return template.replacingOccurrences(of: "PREVIEW_HTML", with: htmlString)
}
+ enum AttachmentURL {
+ static let scheme = "notes-attach"
+
+ static func make(noteId: Int64, relativePath: String) -> URL {
+ var comps = URLComponents()
+ comps.scheme = scheme
+ comps.host = String(noteId)
+ comps.path = "/" + relativePath
+ return comps.url!
+ }
+ }
+
+ private func rewriteAttachmentURLs(in html: String, noteId: Int64, relativePaths: [String]) -> String {
+ var out = html
+ var disallowed = CharacterSet.urlPathAllowed
+ disallowed.remove(charactersIn: "()")
+ for rel in relativePaths {
+ let encoded = rel.addingPercentEncoding(withAllowedCharacters: disallowed) ?? rel
+ let target = AttachmentURL.make(noteId: noteId, relativePath: rel).absoluteString
+ logger.debug("Replacing paths \(rel, privacy: .public) and \(encoded, privacy: .public) with \(target, privacy: .public)")
+ out = out.replacingOccurrences(of: "src=\"\(rel)\"", with: "src=\"\(target)\"")
+ .replacingOccurrences(of: "src=\"\(encoded)\"", with: "src=\"\(target)\"")
+ .replacingOccurrences(of: "href=\"\(rel)\"", with: "href=\"\(target)\"")
+ .replacingOccurrences(of: "href=\"\(encoded)\"", with: "href=\"\(target)\"")
+ }
+ return out
+ }
+
+ final class AttachmentSchemeHandler: NSObject, WKURLSchemeHandler {
+
+ func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
+ DispatchQueue.main.async { [weak urlSchemeTask] in
+ guard let task = urlSchemeTask else { return }
+ guard let url = task.request.url,
+ url.scheme == AttachmentURL.scheme,
+ let noteId = Int(url.host ?? "") else {
+ task.didFailWithError(NSError(domain: "Attachment", code: 400, userInfo: nil))
+ return
+ }
+
+ // Extract path and normalize
+ let relativePath = String(url.path.dropFirst()) // drop leading "/"
+ let encodedPath = relativePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? relativePath
+ let fileURL = AttachmentStore.shared.fileURL(noteId: noteId, relativePath: encodedPath)
+
+ guard FileManager.default.fileExists(atPath: fileURL.path) else {
+ task.didFailWithError(NSError(domain: "Attachment", code: 404, userInfo: [NSFilePathErrorKey: fileURL.path]))
+ return
+ }
+
+ do {
+ let data = try Data(contentsOf: fileURL)
+ let mime = self.mimeType(for: fileURL)
+ let resp = URLResponse(url: url, mimeType: mime, expectedContentLength: data.count, textEncodingName: "utf-8")
+ task.didReceive(resp)
+ task.didReceive(data)
+ task.didFinish()
+ } catch {
+ task.didFailWithError(error)
+ }
+ }
+ }
+
+ func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
+ // Do not send anything, the task is stopped
+ }
+
+ private func mimeType(for fileURL: URL) -> String {
+ if #available(iOS 14.0, *) {
+ if let type = UTType(filenameExtension: fileURL.pathExtension),
+ let preferred = type.preferredMIMEType {
+ return preferred
+ }
+ }
+ // Fallbacks
+ switch fileURL.pathExtension.lowercased() {
+ case "png": return "image/png"
+ case "jpg", "jpeg": return "image/jpeg"
+ case "gif": return "image/gif"
+ case "webp": return "image/webp"
+ case "svg": return "image/svg+xml"
+ case "pdf": return "application/pdf"
+ default: return "application/octet-stream"
+ }
+ }
+ }
+
}
extension PreviewWebView: WKNavigationDelegate {
diff --git a/iOCNotes.xcodeproj/project.pbxproj b/iOCNotes.xcodeproj/project.pbxproj
index 41b985b4..f638cd9a 100644
--- a/iOCNotes.xcodeproj/project.pbxproj
+++ b/iOCNotes.xcodeproj/project.pbxproj
@@ -7,6 +7,10 @@
objects = {
/* Begin PBXBuildFile section */
+ A772AB7D2E5AAC790049636E /* AttachmentHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A772AB7C2E5AAC790049636E /* AttachmentHelper.swift */; };
+ A7AE55FA2E4F4716006F079C /* AttachmentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7AE55F92E4F4716006F079C /* AttachmentStore.swift */; };
+ A7AE55FC2E507AC0006F079C /* Notification+Names.swift in Resources */ = {isa = PBXBuildFile; fileRef = A7AE55FB2E507AC0006F079C /* Notification+Names.swift */; };
+ A7AE55FD2E507E0E006F079C /* Notification+Names.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7AE55FB2E507AC0006F079C /* Notification+Names.swift */; };
AA182D002DDCD6520058C246 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = AA182CFF2DDCD6520058C246 /* CodeScanner */; };
AA3054382E12AF9B00D33159 /* StatusRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3054372E12AF9800D33159 /* StatusRouter.swift */; };
AA30543A2E12AFFD00D33159 /* OCSRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3054392E12AFFB00D33159 /* OCSRouter.swift */; };
@@ -111,6 +115,9 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ A772AB7C2E5AAC790049636E /* AttachmentHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentHelper.swift; sourceTree = ""; };
+ A7AE55F92E4F4716006F079C /* AttachmentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentStore.swift; sourceTree = ""; };
+ A7AE55FB2E507AC0006F079C /* Notification+Names.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification+Names.swift"; sourceTree = ""; };
AA3054372E12AF9800D33159 /* StatusRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusRouter.swift; sourceTree = ""; };
AA3054392E12AFFB00D33159 /* OCSRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCSRouter.swift; sourceTree = ""; };
AA30543B2E12B06700D33159 /* HTTPHeader+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+Extensions.swift"; sourceTree = ""; };
@@ -136,7 +143,8 @@
D00CDFC2194E65D4007505E9 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; };
D00CDFC5194E6835007505E9 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; };
D00CDFC6194E6835007505E9 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; };
- D01067DD220BCE3C0047E090 /* NoteSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteSessionManager.swift; sourceTree = ""; };
+ D00CDFC7194E6835007505E9 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; };
+ D01067DD220BCE3C0047E090 /* NoteSessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoteSessionManager.swift; sourceTree = ""; };
D01067E12213BB5E0047E090 /* NoteProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoteProtocol.swift; sourceTree = ""; };
D01067E32213BDF20047E090 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
D01067E52213C17D0047E090 /* NotesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesTableViewController.swift; sourceTree = ""; };
@@ -295,6 +303,7 @@
BD92271A22CD3743004E2408 /* UtilityExtensions.swift */,
BD653F87231C43FA00D59A88 /* Constants.swift */,
D09E644E248ABAEB003FB4C9 /* Throttler.swift */,
+ A772AB7C2E5AAC790049636E /* AttachmentHelper.swift */,
);
path = Shared;
sourceTree = "";
@@ -362,8 +371,6 @@
D0CFE53C1888A7BD00165839 /* Source */ = {
isa = PBXGroup;
children = (
- F3F734562C6FABF4007C8C0B /* Theming */,
- F3F734552C6FABEA007C8C0B /* Screens */,
D01067E32213BDF20047E090 /* AppDelegate.swift */,
D0BCFE6C2454BF40007C4CA3 /* Categories.storyboard */,
D018034923B037B1001CA4FC /* CategoriesSceneDelegate.swift */,
@@ -372,6 +379,7 @@
D0B6D4AC22F91DDA0037E073 /* CollapsibleTableViewHeaderView.swift */,
D0B6D4AD22F91DDA0037E073 /* CollapsibleTableViewHeaderView.xib */,
BDD015A0234CEF86000BA001 /* Colors.xcassets */,
+ F3F7344C2C6FA550007C8C0B /* CoreData */,
D06882ED22BB146200CEBC1F /* EditorViewController.swift */,
D09E430E1E2C46930010E4B3 /* Main_iPhone.storyboard */,
D0545F22230901D30001D165 /* PBHOpenInActivity.swift */,
@@ -380,10 +388,11 @@
D09538421D32A56A006BB78E /* PreviewViewController.swift */,
D0E60F27277801F8009CF78F /* PreviewWebView.swift */,
D004DB7E23948F3D0080A7D1 /* SceneDelegate.swift */,
- AABC95562DFC717A0023790D /* UIApplication+topViewController.swift */,
+ F3F734552C6FABEA007C8C0B /* Screens */,
D0225708189745C40038232A /* Settings.bundle */,
- F3F7344C2C6FA550007C8C0B /* CoreData */,
D0CFE53D1888A7BD00165839 /* Supporting Files */,
+ F3F734562C6FABF4007C8C0B /* Theming */,
+ AABC95562DFC717A0023790D /* UIApplication+topViewController.swift */,
);
path = Source;
sourceTree = "";
@@ -432,6 +441,7 @@
F3E3C8DA2C6B7D3B00A80504 /* Extensions */ = {
isa = PBXGroup;
children = (
+ A7AE55FB2E507AC0006F079C /* Notification+Names.swift */,
F3E3C8DB2C6B7DA600A80504 /* UIColor+Extension.swift */,
);
path = Extensions;
@@ -440,6 +450,7 @@
F3F7344C2C6FA550007C8C0B /* CoreData */ = {
isa = PBXGroup;
children = (
+ A7AE55F92E4F4716006F079C /* AttachmentStore.swift */,
F3F734472C6FA550007C8C0B /* CDNote+CoreDataClass.swift */,
F3F734482C6FA550007C8C0B /* CDNote+CoreDataProperties.swift */,
F3F7344A2C6FA550007C8C0B /* Notes.xcdatamodeld */,
@@ -637,6 +648,7 @@
BDD015A1234CF551000BA001 /* Colors.xcassets in Resources */,
F7F0812A299D0B53006A2041 /* NCViewerNextcloudText.storyboard in Resources */,
D00CDFBF194E65B3007505E9 /* Localizable.strings in Resources */,
+ A7AE55FC2E507AC0006F079C /* Notification+Names.swift in Resources */,
D004DB7723920E820080A7D1 /* Preview.bundle in Resources */,
F7BB8B7229915A650010D2F7 /* Launch Screen.storyboard in Resources */,
D0B6D4AF22F91DDA0037E073 /* CollapsibleTableViewHeaderView.xib in Resources */,
@@ -686,6 +698,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ A7AE55FD2E507E0E006F079C /* Notification+Names.swift in Sources */,
AABC95572DFC717F0023790D /* UIApplication+topViewController.swift in Sources */,
F3E3C8DC2C6B7DA600A80504 /* UIColor+Extension.swift in Sources */,
F3E3C8D92C6B7C2200A80504 /* NCBrand.swift in Sources */,
@@ -722,12 +735,14 @@
D0E60F59277FADD5009CF78F /* Style.swift in Sources */,
D03493B722F2837500E1A9B0 /* NoteTableViewCell.swift in Sources */,
F3F734522C6FA550007C8C0B /* NotesData.swift in Sources */,
+ A7AE55FA2E4F4716006F079C /* AttachmentStore.swift in Sources */,
D01067E22213BB5E0047E090 /* NoteProtocol.swift in Sources */,
D059F7DB1D40596D00C252F2 /* NoteExporter.swift in Sources */,
BD86DD8522B9CDD500115E5D /* KeychainHelper.swift in Sources */,
F3F7345B2C6FAD80007C8C0B /* BaseUIVIewController.swift in Sources */,
AA3054382E12AF9B00D33159 /* StatusRouter.swift in Sources */,
D08713ED1D18DA40001EAF82 /* HeaderTextView.swift in Sources */,
+ A772AB7D2E5AAC790049636E /* AttachmentHelper.swift in Sources */,
D0E60F5A277FADD5009CF78F /* UniversalTypes.swift in Sources */,
D0E60F28277801F8009CF78F /* PreviewWebView.swift in Sources */,
D03493B922F3CC5700E1A9B0 /* CategoryTableViewController.swift in Sources */,