From f1664d026c34cdd6a77c1649437270a135813924 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 4 Dec 2022 13:21:50 +0100 Subject: [PATCH] Fix bookmarks --- .../URL+ByReplacingYatteeProtocol.swift | 18 +++++++ Model/DocumentsModel.swift | 20 ++++---- Model/Player/Backends/MPVBackend.swift | 4 +- Model/URLBookmarkModel.swift | 47 ++++++++++++++++--- Open in Yattee/ShareViewController.swift | 23 ++++----- Shared/Constants.swift | 1 + Shared/Documents/DocumentsView.swift | 2 +- Shared/Documents/RecentDocumentsView.swift | 2 +- Shared/Navigation/ContentView.swift | 5 +- Shared/OpenURLHandler.swift | 17 +------ Shared/Player/VideoPlayerView.swift | 5 +- Yattee.xcodeproj/project.pbxproj | 14 +++++- 12 files changed, 107 insertions(+), 51 deletions(-) create mode 100644 Extensions/URL+ByReplacingYatteeProtocol.swift diff --git a/Extensions/URL+ByReplacingYatteeProtocol.swift b/Extensions/URL+ByReplacingYatteeProtocol.swift new file mode 100644 index 00000000..6f303810 --- /dev/null +++ b/Extensions/URL+ByReplacingYatteeProtocol.swift @@ -0,0 +1,18 @@ +import Foundation + +extension URL { + func byReplacingYatteeProtocol(with urlProtocol: String = "https") -> URL! { + var urlAbsoluteString = absoluteString + + guard urlAbsoluteString.hasPrefix(Constants.yatteeProtocol) else { + return self + } + + urlAbsoluteString = String(urlAbsoluteString.dropFirst(Constants.yatteeProtocol.count)) + if absoluteString.contains("://") { + return URL(string: urlAbsoluteString) + } + + return URL(string: "\(urlProtocol)://\(urlAbsoluteString)") + } +} diff --git a/Model/DocumentsModel.swift b/Model/DocumentsModel.swift index a27fbafc..a5a251c4 100644 --- a/Model/DocumentsModel.swift +++ b/Model/DocumentsModel.swift @@ -49,7 +49,7 @@ final class DocumentsModel: ObservableObject { var documentsDirectory: URL? { if let url = try? fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) { - return replacePrivateVar(url) + return standardizedURL(url) } return nil } @@ -69,12 +69,12 @@ final class DocumentsModel: ObservableObject { } func isDocument(_ video: Video) -> Bool { - guard video.isLocal, let url = video.localStream?.localURL, let url = replacePrivateVar(url) else { return false } + guard video.isLocal, let url = video.localStream?.localURL, let url = standardizedURL(url) else { return false } return isDocument(url) } func isDocument(_ url: URL) -> Bool { - guard let url = replacePrivateVar(url), let documentsDirectory else { return false } + guard let url = standardizedURL(url), let documentsDirectory else { return false } return url.absoluteString.starts(with: documentsDirectory.absoluteString) } @@ -91,7 +91,7 @@ final class DocumentsModel: ObservableObject { } func creationDate(_ video: Video) -> Date? { - guard video.isLocal, let url = video.localStream?.localURL, let url = replacePrivateVar(url) else { return nil } + guard video.isLocal, let url = video.localStream?.localURL, let url = standardizedURL(url) else { return nil } return creationDate(url) } @@ -100,7 +100,7 @@ final class DocumentsModel: ObservableObject { } func formattedCreationDate(_ video: Video) -> String? { - guard video.isLocal, let url = video.localStream?.localURL, let url = replacePrivateVar(url) else { return nil } + guard video.isLocal, let url = video.localStream?.localURL, let url = standardizedURL(url) else { return nil } return formattedCreationDate(url) } @@ -124,7 +124,7 @@ final class DocumentsModel: ObservableObject { } func size(_ video: Video) -> Int? { - guard video.isLocal, let url = video.localStream?.localURL, let url = replacePrivateVar(url) else { return nil } + guard video.isLocal, let url = video.localStream?.localURL, let url = standardizedURL(url) else { return nil } return size(url) } @@ -168,7 +168,7 @@ final class DocumentsModel: ObservableObject { var canGoBack: Bool { guard let directoryURL, let documentsDirectory else { return false } - return replacePrivateVar(directoryURL) != documentsDirectory + return standardizedURL(directoryURL) != documentsDirectory } func goToURL(_ url: URL) { @@ -187,9 +187,9 @@ final class DocumentsModel: ObservableObject { directoryURL?.deletingLastPathComponent() } - func replacePrivateVar(_ url: URL) -> URL? { - let urlStringWithPrivateVarRemoved = url.absoluteString.replacingFirstOccurrence(of: "/private/var/", with: "/var/") - return URL(string: urlStringWithPrivateVarRemoved) + func standardizedURL(_ url: URL) -> URL? { + let standardizedURL = NSString(string: url.absoluteString).standardizingPath + return URL(string: standardizedURL) } func refresh() { diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 5c352891..67fd30fb 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -291,7 +291,9 @@ final class MPVBackend: PlayerBackend { } if video.isLocal, video.localStreamIsFile { - _ = url.startAccessingSecurityScopedResource() + if url.startAccessingSecurityScopedResource() { + URLBookmarkModel.shared.saveBookmark(url) + } } self.client.loadFile(url, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in diff --git a/Model/URLBookmarkModel.swift b/Model/URLBookmarkModel.swift index 29e2b4bd..38ce0966 100644 --- a/Model/URLBookmarkModel.swift +++ b/Model/URLBookmarkModel.swift @@ -18,13 +18,39 @@ struct URLBookmarkModel { } func saveBookmark(_ url: URL) { + var urlForBookmark = url + if let yatteeSanitizedUrl = url.byReplacingYatteeProtocol() { + urlForBookmark = yatteeSanitizedUrl + } + + guard urlForBookmark.isFileURL else { + logger.error("trying to save bookmark for something that is not a file") + logger.error("not a file: \(urlForBookmark.absoluteString)") + return + } + guard let defaults = CacheModel.shared.bookmarksDefaults else { logger.error("could not open bookmarks defaults") return } + if let bookmarkData = try? urlForBookmark.bookmarkData(options: bookmarkCreationOptions, includingResourceValuesForKeys: nil, relativeTo: nil) { + defaults.set(bookmarkData, forKey: bookmarkKey(urlForBookmark)) + logger.info("saved bookmark for \(bookmarkKey(urlForBookmark))") + } else { + logger.error("no bookmark data for \(urlForBookmark)") + } + } + + func saveBookmark(_ url: NSURL) { guard url.isFileURL else { logger.error("trying to save bookmark for something that is not a file") + logger.error("not a file: \(url.absoluteString)") + return + } + + guard let defaults = CacheModel.shared.bookmarksDefaults else { + logger.error("could not open bookmarks defaults") return } @@ -37,14 +63,19 @@ struct URLBookmarkModel { } func loadBookmark(_ url: URL) -> URL? { - logger.info("loading bookmark for \(bookmarkKey(url))") + var urlForBookmark = url + if let yatteeSanitizedUrl = url.byReplacingYatteeProtocol() { + urlForBookmark = yatteeSanitizedUrl + } + + logger.info("loading bookmark for \(bookmarkKey(urlForBookmark))") guard let defaults = CacheModel.shared.bookmarksDefaults else { logger.error("could not open bookmarks defaults") return nil } - if let data = defaults.data(forKey: bookmarkKey(url)) { + if let data = defaults.data(forKey: bookmarkKey(urlForBookmark)) { do { var isStale = false let url = try URL( @@ -54,9 +85,9 @@ struct URLBookmarkModel { bookmarkDataIsStale: &isStale ) if isStale { - saveBookmark(url) + saveBookmark(urlForBookmark) } - logger.info("loaded bookmark for \(bookmarkKey(url))") + logger.info("loaded bookmark for \(bookmarkKey(urlForBookmark))") return url } catch { @@ -64,7 +95,7 @@ struct URLBookmarkModel { return nil } } else { - logger.warning("could not find bookmark for \(bookmarkKey(url))") + logger.warning("could not find bookmark for \(bookmarkKey(urlForBookmark))") return nil } } @@ -93,7 +124,11 @@ struct URLBookmarkModel { } private func bookmarkKey(_ url: URL) -> String { - "\(Self.bookmarkPrefix)\(url.absoluteString)" + "\(Self.bookmarkPrefix)\(NSString(string: url.absoluteString).standardizingPath)" + } + + private func bookmarkKey(_ url: NSURL) -> String { + "\(Self.bookmarkPrefix)\(url.standardizingPath?.absoluteString ?? "unknown")" } private func urlFromBookmark(_ key: String) -> URL? { diff --git a/Open in Yattee/ShareViewController.swift b/Open in Yattee/ShareViewController.swift index d44a9b1a..2fe6fba9 100644 --- a/Open in Yattee/ShareViewController.swift +++ b/Open in Yattee/ShareViewController.swift @@ -1,52 +1,49 @@ import Social import UIKit +import UniformTypeIdentifiers final class ShareViewController: SLComposeServiceViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) openExtensionContextURLs() - - extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } private func openExtensionContextURLs() { for item in extensionContext?.inputItems as! [NSExtensionItem] { if let attachments = item.attachments { - tryToOpenItemForPlainTextTypeIdentifier(attachments) tryToOpenItemForUrlTypeIdentifier(attachments) + tryToOpenItemForPlainTextTypeIdentifier(attachments) } } } private func tryToOpenItemForPlainTextTypeIdentifier(_ attachments: [NSItemProvider]) { - for itemProvider in attachments where itemProvider.hasItemConformingToTypeIdentifier("public.plain-text") { - itemProvider.loadItem(forTypeIdentifier: "public.plain-text", options: nil) { item, _ in + for itemProvider in attachments { + itemProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { item, _ in if let url = (item as? String), let absoluteURL = URL(string: url)?.absoluteURL { - URLBookmarkModel.shared.saveBookmark(absoluteURL) if let url = URL(string: "yattee://\(absoluteURL.absoluteString)") { self.open(url: url) } - } - self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + } } } } private func tryToOpenItemForUrlTypeIdentifier(_ attachments: [NSItemProvider]) { - for itemProvider in attachments where itemProvider.hasItemConformingToTypeIdentifier("public.url") { - itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil) { item, _ in + for itemProvider in attachments { + itemProvider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { item, _ in if let url = (item as? NSURL), let absoluteURL = url.absoluteURL { - URLBookmarkModel.shared.saveBookmark(absoluteURL) if let url = URL(string: "yattee://\(absoluteURL.absoluteString)") { self.open(url: url) } - } - self.extensionContext!.completeRequest(returningItems: nil, completionHandler: nil) + self.extensionContext!.completeRequest(returningItems: nil, completionHandler: nil) + } } } } diff --git a/Shared/Constants.swift b/Shared/Constants.swift index 5649c088..18cf3e6d 100644 --- a/Shared/Constants.swift +++ b/Shared/Constants.swift @@ -2,5 +2,6 @@ import Foundation import SwiftUI struct Constants { + static let yatteeProtocol = "yattee://" static let overlayAnimation = Animation.linear(duration: 0.2) } diff --git a/Shared/Documents/DocumentsView.swift b/Shared/Documents/DocumentsView.swift index caa0137a..bdb49fc3 100644 --- a/Shared/Documents/DocumentsView.swift +++ b/Shared/Documents/DocumentsView.swift @@ -10,7 +10,7 @@ struct DocumentsView: View { NoDocumentsView() } else { ForEach(model.sortedDirectoryContents, id: \.absoluteString) { url in - let video = Video.local(model.replacePrivateVar(url) ?? url) + let video = Video.local(model.standardizedURL(url) ?? url) PlayerQueueRow( item: PlayerQueueItem(video) ) diff --git a/Shared/Documents/RecentDocumentsView.swift b/Shared/Documents/RecentDocumentsView.swift index 7c50611e..3186fe61 100644 --- a/Shared/Documents/RecentDocumentsView.swift +++ b/Shared/Documents/RecentDocumentsView.swift @@ -11,7 +11,7 @@ struct RecentDocumentsView: View { NoDocumentsView() } else { ForEach(recentDocuments, id: \.absoluteString) { url in - let video = Video.local(model.replacePrivateVar(url) ?? url) + let video = Video.local(model.standardizedURL(url) ?? url) PlayerQueueRow( item: PlayerQueueItem(video) ) diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index d421595f..b616dd05 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -95,7 +95,10 @@ struct ContentView: View { NavigationModel.shared.presentingOpenVideos = false } - .onOpenURL(perform: OpenURLHandler.shared.handle) + .onOpenURL { url in + URLBookmarkModel.shared.saveBookmark(url) + OpenURLHandler.shared.handle(url) + } .background( EmptyView().sheet(isPresented: $navigation.presentingAddToPlaylist) { AddToPlaylistView(video: navigation.videoToAddToPlaylist) diff --git a/Shared/OpenURLHandler.swift b/Shared/OpenURLHandler.swift index 3acb2e1c..749a94fb 100644 --- a/Shared/OpenURLHandler.swift +++ b/Shared/OpenURLHandler.swift @@ -36,7 +36,7 @@ struct OpenURLHandler { } #endif - guard let url = urlByReplacingYatteeProtocol(url) else { return } + guard let url = url.byReplacingYatteeProtocol() else { return } let parser = URLParser(url: url) @@ -93,21 +93,6 @@ struct OpenURLHandler { navigation.presentingOpenVideos = false } - private func urlByReplacingYatteeProtocol(_ url: URL, with urlProtocol: String = "https") -> URL! { - var urlAbsoluteString = url.absoluteString - - guard urlAbsoluteString.hasPrefix(Self.yatteeProtocol) else { - return url - } - - urlAbsoluteString = String(urlAbsoluteString.dropFirst(Self.yatteeProtocol.count)) - if url.absoluteString.contains("://") { - return URL(string: urlAbsoluteString) - } - - return URL(string: "\(urlProtocol)://\(urlAbsoluteString)") - } - private func handleFileURLOpen(_ parser: URLParser) { guard let url = parser.fileURL else { return } diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 245c2a05..3045d732 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -109,7 +109,10 @@ struct VideoPlayerView: View { } } .alert(isPresented: $navigation.presentingAlertInVideoPlayer) { navigation.alert } - .onOpenURL(perform: OpenURLHandler.shared.handle) + .onOpenURL { url in + URLBookmarkModel.shared.saveBookmark(url) + OpenURLHandler.shared.handle(url) + } .frame(minWidth: 950, minHeight: 700) #else return GeometryReader { geometry in diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index a381b8d2..b3ab0f50 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -911,6 +911,11 @@ 37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD43E22704847C0073EE42 /* View+Fixtures.swift */; }; 37FD43E52704847C0073EE42 /* View+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD43E22704847C0073EE42 /* View+Fixtures.swift */; }; 37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; }; + 37FD77002932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD76FF2932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift */; }; + 37FD77012932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD76FF2932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift */; }; + 37FD77022932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD76FF2932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift */; }; + 37FD77032932C5EC00D91A5F /* URL+ByReplacingYatteeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD76FF2932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift */; }; + 37FD77042932C5FC00D91A5F /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3754B01428B7F84D009717C8 /* Constants.swift */; }; 37FEF11327EFD8580033912F /* PlaceholderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEF11227EFD8580033912F /* PlaceholderCell.swift */; }; 37FEF11427EFD8580033912F /* PlaceholderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEF11227EFD8580033912F /* PlaceholderCell.swift */; }; 37FEF11527EFD8580033912F /* PlaceholderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEF11227EFD8580033912F /* PlaceholderCell.swift */; }; @@ -1367,6 +1372,7 @@ 37FB285D272225E800A57617 /* ContentItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentItemView.swift; sourceTree = ""; }; 37FD43DB270470B70073EE42 /* InstancesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesSettings.swift; sourceTree = ""; }; 37FD43E22704847C0073EE42 /* View+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Fixtures.swift"; sourceTree = ""; }; + 37FD76FF2932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+ByReplacingYatteeProtocol.swift"; sourceTree = ""; }; 37FEF11227EFD8580033912F /* PlaceholderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderCell.swift; sourceTree = ""; }; 37FFC43F272734C3009FFD26 /* Throttle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Throttle.swift; sourceTree = ""; }; 3DA101AD287C30F50027D920 /* DEVELOPMENT_TEAM.template.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.template.xcconfig; sourceTree = ""; }; @@ -2048,11 +2054,12 @@ 3782B95C2755858100990149 /* NSTextField+FocusRingType.swift */, 377ABC47286E5887009C986F /* Sequence+Unique.swift */, 3782B9512755667600990149 /* String+Format.swift */, + 37270F1B28E06E3E00856150 /* String+Localizable.swift */, 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */, 37F7AB4C28A9361F00FB46B5 /* UIDevice+Cellular.swift */, 370B79CB286279BA0045DB77 /* UIViewController+HideHomeIndicator.swift */, + 37FD76FF2932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift */, 3743CA51270F284F00E4D32B /* View+Borders.swift */, - 37270F1B28E06E3E00856150 /* String+Localizable.swift */, ); path = Extensions; sourceTree = ""; @@ -2812,8 +2819,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 37FD77042932C5FC00D91A5F /* Constants.swift in Sources */, 37095E8E291DD5FE00301883 /* CacheModel.swift in Sources */, 37095E82291DC85400301883 /* ShareViewController.swift in Sources */, + 37FD77032932C5EC00D91A5F /* URL+ByReplacingYatteeProtocol.swift in Sources */, 37095E8D291DD5DA00301883 /* URLBookmarkModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2940,6 +2949,7 @@ 37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */, 376578892685471400D4EA09 /* Playlist.swift in Sources */, 37B4E803277D0A72004BF56A /* AppDelegate.swift in Sources */, + 37FD77002932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift in Sources */, 373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */, 3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */, 3782B9522755667600990149 /* String+Format.swift in Sources */, @@ -3089,6 +3099,7 @@ 37BE0BDC26A2367F0092E2DB /* AppleAVPlayerView.swift in Sources */, 3743CA53270F284F00E4D32B /* View+Borders.swift in Sources */, 37599F39272B4D740087F250 /* FavoriteButton.swift in Sources */, + 37FD77012932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift in Sources */, 374924F129216C630017D862 /* VideoActions.swift in Sources */, 37C3A24627235DA70087A57A /* ChannelPlaylist.swift in Sources */, 37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */, @@ -3519,6 +3530,7 @@ 37C069802725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, 37EBD8CC27AF26C200F1C24B /* MPVBackend.swift in Sources */, 3711404126B206A6005B3555 /* SearchModel.swift in Sources */, + 37FD77022932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift in Sources */, 37FEF11527EFD8580033912F /* PlaceholderCell.swift in Sources */, 37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */, 379775952689365600DD52A8 /* Array+Next.swift in Sources */,