mirror of
https://github.com/yattee/yattee.git
synced 2025-10-10 01:18:16 +00:00
Documents tab with file sharing
This commit is contained in:
185
Model/DocumentsModel.swift
Normal file
185
Model/DocumentsModel.swift
Normal file
@@ -0,0 +1,185 @@
|
||||
import Foundation
|
||||
|
||||
final class DocumentsModel: ObservableObject {
|
||||
static var shared = DocumentsModel()
|
||||
|
||||
@Published private(set) var directoryURL: URL!
|
||||
@Published private(set) var refreshID = UUID()
|
||||
|
||||
typealias AreInIncreasingOrder = (URL, URL) -> Bool
|
||||
|
||||
init(directoryURL: URL! = nil) {
|
||||
self.directoryURL = directoryURL
|
||||
}
|
||||
|
||||
private var fileManager: FileManager {
|
||||
.default
|
||||
}
|
||||
|
||||
var directoryLabel: String {
|
||||
guard let directoryURL else { return "Documents" }
|
||||
return displayLabelForDocument(directoryURL)
|
||||
}
|
||||
|
||||
var sortPredicates: [AreInIncreasingOrder] {
|
||||
[
|
||||
{ self.isDirectory($0) && !self.isDirectory($1) },
|
||||
{ $0.lastPathComponent.caseInsensitiveCompare($1.lastPathComponent) == .orderedAscending }
|
||||
]
|
||||
}
|
||||
|
||||
var sortedDirectoryContents: [URL] {
|
||||
directoryContents.sorted { lhs, rhs in
|
||||
for predicate in sortPredicates {
|
||||
if !predicate(lhs, rhs), !predicate(rhs, lhs) {
|
||||
continue
|
||||
}
|
||||
|
||||
return predicate(lhs, rhs)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var directoryContents: [URL] {
|
||||
guard let directoryURL else { return [] }
|
||||
return contents(of: directoryURL)
|
||||
}
|
||||
|
||||
var documentsDirectory: URL? {
|
||||
if let url = try? fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) {
|
||||
return replacePrivateVar(url)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDocument(_ video: Video) -> Bool {
|
||||
guard video.isLocal, let url = video.localStream?.localURL, let url = replacePrivateVar(url) else { return false }
|
||||
return isDocument(url)
|
||||
}
|
||||
|
||||
func isDocument(_ url: URL) -> Bool {
|
||||
guard let url = replacePrivateVar(url), let documentsDirectory else { return false }
|
||||
return url.absoluteString.starts(with: documentsDirectory.absoluteString)
|
||||
}
|
||||
|
||||
func isDirectory(_ url: URL) -> Bool {
|
||||
(try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false
|
||||
}
|
||||
|
||||
var creationDateFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
|
||||
formatter.dateStyle = .short
|
||||
formatter.timeStyle = .short
|
||||
|
||||
return formatter
|
||||
}
|
||||
|
||||
func creationDate(_ video: Video) -> Date? {
|
||||
guard video.isLocal, let url = video.localStream?.localURL, let url = replacePrivateVar(url) else { return nil }
|
||||
return creationDate(url)
|
||||
}
|
||||
|
||||
func creationDate(_ url: URL) -> Date? {
|
||||
try? url.resourceValues(forKeys: [.creationDateKey]).creationDate
|
||||
}
|
||||
|
||||
func formattedCreationDate(_ video: Video) -> String? {
|
||||
guard video.isLocal, let url = video.localStream?.localURL, let url = replacePrivateVar(url) else { return nil }
|
||||
return formattedCreationDate(url)
|
||||
}
|
||||
|
||||
func formattedCreationDate(_ url: URL) -> String? {
|
||||
if let date = try? url.resourceValues(forKeys: [.creationDateKey]).creationDate {
|
||||
return creationDateFormatter.string(from: date)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var sizeFormatter: ByteCountFormatter {
|
||||
let formatter = ByteCountFormatter()
|
||||
|
||||
formatter.allowedUnits = .useAll
|
||||
formatter.countStyle = .file
|
||||
formatter.includesUnit = true
|
||||
formatter.isAdaptive = true
|
||||
|
||||
return formatter
|
||||
}
|
||||
|
||||
func size(_ video: Video) -> Int? {
|
||||
guard video.isLocal, let url = video.localStream?.localURL, let url = replacePrivateVar(url) else { return nil }
|
||||
return size(url)
|
||||
}
|
||||
|
||||
func size(_ url: URL) -> Int? {
|
||||
try? url.resourceValues(forKeys: [.fileAllocatedSizeKey]).fileAllocatedSize
|
||||
}
|
||||
|
||||
func formattedSize(_ video: Video) -> String? {
|
||||
guard let size = size(video) else { return nil }
|
||||
return sizeFormatter.string(fromByteCount: Int64(size))
|
||||
}
|
||||
|
||||
func formattedSize(_ url: URL) -> String? {
|
||||
guard let size = size(url) else { return nil }
|
||||
return sizeFormatter.string(fromByteCount: Int64(size))
|
||||
}
|
||||
|
||||
func removeDocument(_ url: URL) throws {
|
||||
guard isDocument(url) else { return }
|
||||
try fileManager.removeItem(at: url)
|
||||
URLBookmarkModel.shared.removeBookmark(url)
|
||||
refresh()
|
||||
}
|
||||
|
||||
private func contents(of directory: URL) -> [URL] {
|
||||
(try? fileManager.contentsOfDirectory(
|
||||
at: directory,
|
||||
includingPropertiesForKeys: [.creationDateKey, .fileAllocatedSizeKey, .isDirectoryKey],
|
||||
options: [.includesDirectoriesPostOrder, .skipsHiddenFiles]
|
||||
)) ?? []
|
||||
}
|
||||
|
||||
private func displayLabelForDocument(_ file: URL) -> String {
|
||||
let components = file.absoluteString.components(separatedBy: "/Documents/")
|
||||
if components.count == 2 {
|
||||
let component = components[1]
|
||||
return component.isEmpty ? "Documents" : component.removingPercentEncoding ?? component
|
||||
}
|
||||
return "Document"
|
||||
}
|
||||
|
||||
var canGoBack: Bool {
|
||||
guard let directoryURL, let documentsDirectory else { return false }
|
||||
return replacePrivateVar(directoryURL) != documentsDirectory
|
||||
}
|
||||
|
||||
func goToURL(_ url: URL) {
|
||||
directoryURL = url
|
||||
}
|
||||
|
||||
func goBack() {
|
||||
directoryURL = urlToGoBack
|
||||
}
|
||||
|
||||
func goToTop() {
|
||||
directoryURL = documentsDirectory
|
||||
}
|
||||
|
||||
private var urlToGoBack: URL? {
|
||||
directoryURL?.deletingLastPathComponent()
|
||||
}
|
||||
|
||||
func replacePrivateVar(_ url: URL) -> URL? {
|
||||
let urlStringWithPrivateVarRemoved = url.absoluteString.replacingFirstOccurrence(of: "/private/var/", with: "/var/")
|
||||
return URL(string: urlStringWithPrivateVarRemoved)
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
refreshID = UUID()
|
||||
}
|
||||
}
|
@@ -6,6 +6,7 @@ final class NavigationModel: ObservableObject {
|
||||
|
||||
enum TabSelection: Hashable {
|
||||
case home
|
||||
case documents
|
||||
case subscriptions
|
||||
case popular
|
||||
case trending
|
||||
|
@@ -52,6 +52,17 @@ struct URLBookmarkModel {
|
||||
}
|
||||
}
|
||||
|
||||
func removeBookmark(_ url: URL) {
|
||||
logger.info("removing bookmark for \(url.absoluteString)")
|
||||
|
||||
guard let defaults = CacheModel.shared.bookmarksDefaults else {
|
||||
logger.error("could not open bookmarks defaults")
|
||||
return
|
||||
}
|
||||
|
||||
defaults.removeObject(forKey: url.absoluteString)
|
||||
}
|
||||
|
||||
var bookmarkCreationOptions: URL.BookmarkCreationOptions {
|
||||
#if os(macOS)
|
||||
return [.withSecurityScope, .securityScopeAllowOnlyReadAccess]
|
||||
|
@@ -172,6 +172,19 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
return streams.first
|
||||
}
|
||||
|
||||
var localStreamImageSystemName: String {
|
||||
guard localStream != nil else { return "" }
|
||||
|
||||
if localStreamIsDirectory {
|
||||
return "folder"
|
||||
}
|
||||
if localStreamIsFile {
|
||||
return "doc"
|
||||
}
|
||||
|
||||
return "globe"
|
||||
}
|
||||
|
||||
var localStreamIsFile: Bool {
|
||||
guard let localStream else { return false }
|
||||
return localStream.localURL.isFileURL
|
||||
@@ -182,6 +195,15 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
return !localStream.localURL.isFileURL
|
||||
}
|
||||
|
||||
var localStreamIsDirectory: Bool {
|
||||
guard let localStream else { return false }
|
||||
#if os(iOS)
|
||||
return DocumentsModel.shared.isDirectory(localStream.localURL)
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
var remoteUrlHost: String? {
|
||||
localStreamURLComponents?.host
|
||||
}
|
||||
|
Reference in New Issue
Block a user