mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Documents tab with file sharing
This commit is contained in:
parent
ccded28468
commit
4657af2f3d
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 {
|
enum TabSelection: Hashable {
|
||||||
case home
|
case home
|
||||||
|
case documents
|
||||||
case subscriptions
|
case subscriptions
|
||||||
case popular
|
case popular
|
||||||
case trending
|
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 {
|
var bookmarkCreationOptions: URL.BookmarkCreationOptions {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
return [.withSecurityScope, .securityScopeAllowOnlyReadAccess]
|
return [.withSecurityScope, .securityScopeAllowOnlyReadAccess]
|
||||||
|
@ -172,6 +172,19 @@ struct Video: Identifiable, Equatable, Hashable {
|
|||||||
return streams.first
|
return streams.first
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var localStreamImageSystemName: String {
|
||||||
|
guard localStream != nil else { return "" }
|
||||||
|
|
||||||
|
if localStreamIsDirectory {
|
||||||
|
return "folder"
|
||||||
|
}
|
||||||
|
if localStreamIsFile {
|
||||||
|
return "doc"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "globe"
|
||||||
|
}
|
||||||
|
|
||||||
var localStreamIsFile: Bool {
|
var localStreamIsFile: Bool {
|
||||||
guard let localStream else { return false }
|
guard let localStream else { return false }
|
||||||
return localStream.localURL.isFileURL
|
return localStream.localURL.isFileURL
|
||||||
@ -182,6 +195,15 @@ struct Video: Identifiable, Equatable, Hashable {
|
|||||||
return !localStream.localURL.isFileURL
|
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? {
|
var remoteUrlHost: String? {
|
||||||
localStreamURLComponents?.host
|
localStreamURLComponents?.host
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ extension Defaults.Keys {
|
|||||||
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
|
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
|
||||||
|
|
||||||
static let showHome = Key<Bool>("showHome", default: true)
|
static let showHome = Key<Bool>("showHome", default: true)
|
||||||
|
static let showDocuments = Key<Bool>("showDocuments", default: true)
|
||||||
static let showOpenActionsInHome = Key<Bool>("showOpenActionsInHome", default: true)
|
static let showOpenActionsInHome = Key<Bool>("showOpenActionsInHome", default: true)
|
||||||
static let showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
|
static let showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
|
||||||
static let showFavoritesInHome = Key<Bool>("showFavoritesInHome", default: true)
|
static let showFavoritesInHome = Key<Bool>("showFavoritesInHome", default: true)
|
||||||
|
82
Shared/Documents/DocumentsView.swift
Normal file
82
Shared/Documents/DocumentsView.swift
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct DocumentsView: View {
|
||||||
|
@ObservedObject private var model = DocumentsModel.shared
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
BrowserPlayerControls {
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
if model.directoryContents.isEmpty {
|
||||||
|
VStack(alignment: .center, spacing: 20) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "doc")
|
||||||
|
Text("No documents")
|
||||||
|
}
|
||||||
|
Text("Share files from Finder on a Mac\nor iTunes on Windows")
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} else {
|
||||||
|
ForEach(model.sortedDirectoryContents, id: \.absoluteString) { url in
|
||||||
|
let video = Video.local(model.replacePrivateVar(url) ?? url)
|
||||||
|
PlayerQueueRow(
|
||||||
|
item: PlayerQueueItem(video)
|
||||||
|
)
|
||||||
|
.contextMenu {
|
||||||
|
VideoContextMenuView(video: video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.id(model.refreshID)
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
|
Color.clear.padding(.bottom, 50)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if model.directoryURL.isNil {
|
||||||
|
model.goToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
model.goBack()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Label("Go back", systemImage: "chevron.left")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
|
.disabled(!model.canGoBack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(model.directoryLabel)
|
||||||
|
.padding()
|
||||||
|
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
|
||||||
|
.backport
|
||||||
|
.refreshable {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func refresh() {
|
||||||
|
withAnimation {
|
||||||
|
model.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DocumentsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
NavigationView {
|
||||||
|
DocumentsView()
|
||||||
|
}
|
||||||
|
.injectFixtureEnvironmentObjects()
|
||||||
|
.navigationViewStyle(.stack)
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ struct AppTabNavigation: View {
|
|||||||
@EnvironmentObject<ThumbnailsModel> private var thumbnailsModel
|
@EnvironmentObject<ThumbnailsModel> private var thumbnailsModel
|
||||||
|
|
||||||
@Default(.showHome) private var showHome
|
@Default(.showHome) private var showHome
|
||||||
|
@Default(.showDocuments) private var showDocuments
|
||||||
@Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem
|
@Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem
|
||||||
@Default(.visibleSections) private var visibleSections
|
@Default(.visibleSections) private var visibleSections
|
||||||
|
|
||||||
@ -26,6 +27,10 @@ struct AppTabNavigation: View {
|
|||||||
homeNavigationView
|
homeNavigationView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if showDocuments {
|
||||||
|
documentsNavigationView
|
||||||
|
}
|
||||||
|
|
||||||
if !accounts.isEmpty {
|
if !accounts.isEmpty {
|
||||||
if subscriptionsVisible {
|
if subscriptionsVisible {
|
||||||
subscriptionsNavigationView
|
subscriptionsNavigationView
|
||||||
@ -73,6 +78,18 @@ struct AppTabNavigation: View {
|
|||||||
.tag(TabSelection.home)
|
.tag(TabSelection.home)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var documentsNavigationView: some View {
|
||||||
|
NavigationView {
|
||||||
|
LazyView(DocumentsView())
|
||||||
|
.toolbar { toolbarContent }
|
||||||
|
}
|
||||||
|
.tabItem {
|
||||||
|
Label("Documents", systemImage: "folder")
|
||||||
|
.accessibility(label: Text("Documents"))
|
||||||
|
}
|
||||||
|
.tag(TabSelection.documents)
|
||||||
|
}
|
||||||
|
|
||||||
private var subscriptionsNavigationView: some View {
|
private var subscriptionsNavigationView: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
LazyView(SubscriptionsView())
|
LazyView(SubscriptionsView())
|
||||||
|
@ -6,6 +6,7 @@ struct Sidebar: View {
|
|||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
|
||||||
@Default(.showHome) private var showHome
|
@Default(.showHome) private var showHome
|
||||||
|
@Default(.showDocuments) private var showDocuments
|
||||||
@Default(.visibleSections) private var visibleSections
|
@Default(.visibleSections) private var visibleSections
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -48,6 +49,18 @@ struct Sidebar: View {
|
|||||||
}
|
}
|
||||||
.id("favorites")
|
.id("favorites")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
if showDocuments {
|
||||||
|
NavigationLink(destination: LazyView(DocumentsView()), tag: TabSelection.documents, selection: $navigation.tabSelection) {
|
||||||
|
Label("Documents", systemImage: "folder")
|
||||||
|
.accessibility(label: Text("Documents"))
|
||||||
|
}
|
||||||
|
.id("documents")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
if !accounts.isEmpty {
|
if !accounts.isEmpty {
|
||||||
if visibleSections.contains(.subscriptions),
|
if visibleSections.contains(.subscriptions),
|
||||||
accounts.app.supportsSubscriptions && accounts.signedIn
|
accounts.app.supportsSubscriptions && accounts.signedIn
|
||||||
|
@ -29,6 +29,22 @@ struct PlayerQueueRow: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button {
|
Button {
|
||||||
|
#if os(iOS)
|
||||||
|
guard !item.video.localStreamIsDirectory else {
|
||||||
|
if let url = item.video?.localStream?.localURL {
|
||||||
|
withAnimation {
|
||||||
|
DocumentsModel.shared.goToURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.video.localStreamIsFile, let url = item.video.localStream?.localURL {
|
||||||
|
URLBookmarkModel.shared.saveBookmark(url)
|
||||||
|
}
|
||||||
|
|
||||||
player.prepareCurrentItemForHistory()
|
player.prepareCurrentItemForHistory()
|
||||||
|
|
||||||
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
|
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
|
||||||
|
@ -14,6 +14,7 @@ struct BrowsingSettings: View {
|
|||||||
@Default(.channelOnThumbnail) private var channelOnThumbnail
|
@Default(.channelOnThumbnail) private var channelOnThumbnail
|
||||||
@Default(.timeOnThumbnail) private var timeOnThumbnail
|
@Default(.timeOnThumbnail) private var timeOnThumbnail
|
||||||
@Default(.showHome) private var showHome
|
@Default(.showHome) private var showHome
|
||||||
|
@Default(.showDocuments) private var showDocuments
|
||||||
@Default(.showFavoritesInHome) private var showFavoritesInHome
|
@Default(.showFavoritesInHome) private var showFavoritesInHome
|
||||||
@Default(.showOpenActionsInHome) private var showOpenActionsInHome
|
@Default(.showOpenActionsInHome) private var showOpenActionsInHome
|
||||||
@Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem
|
@Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem
|
||||||
@ -85,6 +86,7 @@ struct BrowsingSettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.multilineTextAlignment(.trailing)
|
.multilineTextAlignment(.trailing)
|
||||||
|
|
||||||
if !accounts.isEmpty {
|
if !accounts.isEmpty {
|
||||||
Toggle("Show Favorites", isOn: $showFavoritesInHome)
|
Toggle("Show Favorites", isOn: $showFavoritesInHome)
|
||||||
|
|
||||||
@ -124,6 +126,8 @@ struct BrowsingSettings: View {
|
|||||||
Toggle("Show Open Videos toolbar button", isOn: $showOpenActionsToolbarItem)
|
Toggle("Show Open Videos toolbar button", isOn: $showOpenActionsToolbarItem)
|
||||||
#endif
|
#endif
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
Toggle("Show Documents", isOn: $showDocuments)
|
||||||
|
|
||||||
Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing)
|
Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing)
|
||||||
.onChange(of: lockPortraitWhenBrowsing) { lock in
|
.onChange(of: lockPortraitWhenBrowsing) { lock in
|
||||||
if lock {
|
if lock {
|
||||||
|
@ -22,7 +22,7 @@ struct VideoBanner: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: stackAlignment, spacing: 12) {
|
HStack(alignment: stackAlignment, spacing: 12) {
|
||||||
VStack(spacing: thumbnailStackSpacing) {
|
ZStack(alignment: .bottom) {
|
||||||
smallThumbnail
|
smallThumbnail
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@ -55,6 +55,20 @@ struct VideoBanner: View {
|
|||||||
if let video {
|
if let video {
|
||||||
if !video.isLocal || video.localStreamIsRemoteURL {
|
if !video.isLocal || video.localStreamIsRemoteURL {
|
||||||
Text(video.displayAuthor)
|
Text(video.displayAuthor)
|
||||||
|
} else {
|
||||||
|
#if os(iOS)
|
||||||
|
if DocumentsModel.shared.isDocument(video) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
if let date = DocumentsModel.shared.formattedCreationDate(video) {
|
||||||
|
Text(date)
|
||||||
|
}
|
||||||
|
if let size = DocumentsModel.shared.formattedSize(video) {
|
||||||
|
Text("•")
|
||||||
|
Text(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text("Video Author")
|
Text("Video Author")
|
||||||
@ -69,8 +83,10 @@ struct VideoBanner: View {
|
|||||||
progressView
|
progressView
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Text((videoDuration ?? video?.length ?? 0).formattedAsPlaybackTime() ?? PlayerTimeModel.timePlaceholder)
|
if !(video?.localStreamIsDirectory ?? false) {
|
||||||
.fontWeight(.light)
|
Text(videoDurationLabel)
|
||||||
|
.fontWeight(.light)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
@ -98,24 +114,15 @@ struct VideoBanner: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var thumbnailStackSpacing: Double {
|
|
||||||
#if os(tvOS)
|
|
||||||
8
|
|
||||||
#else
|
|
||||||
2
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder private var smallThumbnail: some View {
|
@ViewBuilder private var smallThumbnail: some View {
|
||||||
Group {
|
ZStack {
|
||||||
|
Color("PlaceholderColor")
|
||||||
if let video {
|
if let video {
|
||||||
if let thumbnail = video.thumbnailURL(quality: .medium) {
|
if let thumbnail = video.thumbnailURL(quality: .medium) {
|
||||||
WebImage(url: thumbnail, options: [.lowPriority])
|
WebImage(url: thumbnail, options: [.lowPriority])
|
||||||
.resizable()
|
.resizable()
|
||||||
} else if video.localStreamIsFile {
|
} else if video.isLocal {
|
||||||
Image(systemName: "folder")
|
Image(systemName: video.localStreamImageSystemName)
|
||||||
} else if video.localStreamIsRemoteURL {
|
|
||||||
Image(systemName: "globe")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: "ellipsis")
|
Image(systemName: "ellipsis")
|
||||||
@ -146,6 +153,11 @@ struct VideoBanner: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var videoDurationLabel: String {
|
||||||
|
guard videoDuration != 0 else { return PlayerTimeModel.timePlaceholder }
|
||||||
|
return (videoDuration ?? video?.length ?? 0).formattedAsPlaybackTime() ?? PlayerTimeModel.timePlaceholder
|
||||||
|
}
|
||||||
|
|
||||||
private var progressView: some View {
|
private var progressView: some View {
|
||||||
Group {
|
Group {
|
||||||
if !playbackTime.isNil, !(video?.live ?? false) {
|
if !playbackTime.isNil, !(video?.live ?? false) {
|
||||||
@ -157,11 +169,13 @@ struct VideoBanner: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var progressViewValue: Double {
|
private var progressViewValue: Double {
|
||||||
[playbackTime?.seconds, videoDuration].compactMap { $0 }.min() ?? 0
|
guard videoDuration != 0 else { return 1 }
|
||||||
|
return [playbackTime?.seconds, videoDuration].compactMap { $0 }.min() ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private var progressViewTotal: Double {
|
private var progressViewTotal: Double {
|
||||||
videoDuration ?? video?.length ?? 1
|
guard videoDuration != 0 else { return 1 }
|
||||||
|
return videoDuration ?? video?.length ?? 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,11 +288,7 @@ struct ControlsBar: View {
|
|||||||
|
|
||||||
Group {
|
Group {
|
||||||
if let video = model.currentItem?.video, video.isLocal {
|
if let video = model.currentItem?.video, video.isLocal {
|
||||||
if video.localStreamIsFile {
|
Image(systemName: video.localStreamImageSystemName)
|
||||||
Image(systemName: "folder")
|
|
||||||
} else if video.localStreamIsRemoteURL {
|
|
||||||
Image(systemName: "globe")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: "play.rectangle")
|
Image(systemName: "play.rectangle")
|
||||||
}
|
}
|
||||||
|
@ -37,54 +37,64 @@ struct VideoContextMenuView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder var contextMenu: some View {
|
@ViewBuilder var contextMenu: some View {
|
||||||
if saveHistory {
|
if !video.localStreamIsDirectory {
|
||||||
Section {
|
if saveHistory {
|
||||||
if let watchedAtString {
|
Section {
|
||||||
Text(watchedAtString)
|
if let watchedAtString {
|
||||||
}
|
Text(watchedAtString)
|
||||||
|
}
|
||||||
|
|
||||||
if !watch.isNil, !watch!.finished, !watchingNow {
|
if !watch.isNil, !watch!.finished, !watchingNow {
|
||||||
continueButton
|
continueButton
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(watch?.finished ?? false) {
|
if !(watch?.finished ?? false) {
|
||||||
markAsWatchedButton
|
markAsWatchedButton
|
||||||
}
|
}
|
||||||
|
|
||||||
if !watch.isNil, !watchingNow {
|
if !watch.isNil, !watchingNow {
|
||||||
removeFromHistoryButton
|
removeFromHistoryButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
playNowButton
|
||||||
|
#if !os(tvOS)
|
||||||
|
playNowInPictureInPictureButton
|
||||||
|
playNowInMusicMode
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
playNextButton
|
||||||
|
addToQueueButton
|
||||||
|
}
|
||||||
|
|
||||||
|
if accounts.app.supportsUserPlaylists, accounts.signedIn, !video.isLocal {
|
||||||
|
Section {
|
||||||
|
addToPlaylistButton
|
||||||
|
addToLastPlaylistButton
|
||||||
|
|
||||||
|
if let id = navigation.tabSelection?.playlistID ?? playlistID {
|
||||||
|
removeFromPlaylistButton(playlistID: id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
playNowButton
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
playNowInPictureInPictureButton
|
Section {
|
||||||
playNowInMusicMode
|
ShareButton(contentItem: .init(video: video))
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
#if os(iOS)
|
||||||
playNextButton
|
if video.isLocal, let url = video.localStream?.localURL, DocumentsModel.shared.isDocument(url) {
|
||||||
addToQueueButton
|
Section {
|
||||||
}
|
removeDocumentButton
|
||||||
|
|
||||||
if accounts.app.supportsUserPlaylists, accounts.signedIn, !video.isLocal {
|
|
||||||
Section {
|
|
||||||
addToPlaylistButton
|
|
||||||
addToLastPlaylistButton
|
|
||||||
|
|
||||||
if let id = navigation.tabSelection?.playlistID ?? playlistID {
|
|
||||||
removeFromPlaylistButton(playlistID: id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#if !os(tvOS)
|
|
||||||
Section {
|
|
||||||
ShareButton(contentItem: .init(video: video))
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if !inChannelView, !inChannelPlaylistView, !video.isLocal {
|
if !inChannelView, !inChannelPlaylistView, !video.isLocal {
|
||||||
@ -215,6 +225,31 @@ struct VideoContextMenuView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
private var removeDocumentButton: some View {
|
||||||
|
Button {
|
||||||
|
if let url = video.localStream?.localURL {
|
||||||
|
NavigationModel.shared.presentAlert(
|
||||||
|
Alert(
|
||||||
|
title: Text("Are you sure you want to remove this document?"),
|
||||||
|
primaryButton: .destructive(Text("Remove")) {
|
||||||
|
do {
|
||||||
|
try DocumentsModel.shared.removeDocument(url)
|
||||||
|
} catch {
|
||||||
|
NavigationModel.shared.presentAlert(title: "Could not delete document", message: error.localizedDescription)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Remove...", systemImage: "trash.fill")
|
||||||
|
.foregroundColor(Color("AppRedColor"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
private var openChannelButton: some View {
|
private var openChannelButton: some View {
|
||||||
Button {
|
Button {
|
||||||
NavigationModel.openChannel(
|
NavigationModel.openChannel(
|
||||||
|
@ -304,6 +304,8 @@
|
|||||||
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
||||||
37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
||||||
37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
||||||
|
37494EA529200B14000DF176 /* DocumentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37494EA429200B14000DF176 /* DocumentsView.swift */; };
|
||||||
|
37494EA729200E0B000DF176 /* DocumentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37494EA629200E0B000DF176 /* DocumentsModel.swift */; };
|
||||||
374AB3D728BCAF0000DF56FB /* SeekModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3D628BCAF0000DF56FB /* SeekModel.swift */; };
|
374AB3D728BCAF0000DF56FB /* SeekModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3D628BCAF0000DF56FB /* SeekModel.swift */; };
|
||||||
374AB3D828BCAF0000DF56FB /* SeekModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3D628BCAF0000DF56FB /* SeekModel.swift */; };
|
374AB3D828BCAF0000DF56FB /* SeekModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3D628BCAF0000DF56FB /* SeekModel.swift */; };
|
||||||
374AB3D928BCAF0000DF56FB /* SeekModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3D628BCAF0000DF56FB /* SeekModel.swift */; };
|
374AB3D928BCAF0000DF56FB /* SeekModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3D628BCAF0000DF56FB /* SeekModel.swift */; };
|
||||||
@ -1093,6 +1095,8 @@
|
|||||||
37484C2826FC83FF00287258 /* AccountForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountForm.swift; sourceTree = "<group>"; };
|
37484C2826FC83FF00287258 /* AccountForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountForm.swift; sourceTree = "<group>"; };
|
||||||
37484C2C26FC844700287258 /* InstanceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSettings.swift; sourceTree = "<group>"; };
|
37484C2C26FC844700287258 /* InstanceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSettings.swift; sourceTree = "<group>"; };
|
||||||
37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = "<group>"; };
|
37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = "<group>"; };
|
||||||
|
37494EA429200B14000DF176 /* DocumentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentsView.swift; sourceTree = "<group>"; };
|
||||||
|
37494EA629200E0B000DF176 /* DocumentsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentsModel.swift; sourceTree = "<group>"; };
|
||||||
3749BF6F27ADA135000480FF /* client.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = client.h; sourceTree = "<group>"; };
|
3749BF6F27ADA135000480FF /* client.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = client.h; sourceTree = "<group>"; };
|
||||||
3749BF7027ADA135000480FF /* stream_cb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = stream_cb.h; sourceTree = "<group>"; };
|
3749BF7027ADA135000480FF /* stream_cb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = stream_cb.h; sourceTree = "<group>"; };
|
||||||
3749BF7127ADA135000480FF /* qthelper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = qthelper.hpp; sourceTree = "<group>"; };
|
3749BF7127ADA135000480FF /* qthelper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = qthelper.hpp; sourceTree = "<group>"; };
|
||||||
@ -1821,6 +1825,14 @@
|
|||||||
path = Settings;
|
path = Settings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
37494EA329200AD4000DF176 /* Documents */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
37494EA429200B14000DF176 /* DocumentsView.swift */,
|
||||||
|
);
|
||||||
|
path = Documents;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
3749BF6C27ADA135000480FF /* mpv */ = {
|
3749BF6C27ADA135000480FF /* mpv */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -2022,6 +2034,7 @@
|
|||||||
37D4B0C12671614700C925CA /* Shared */ = {
|
37D4B0C12671614700C925CA /* Shared */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
37494EA329200AD4000DF176 /* Documents */,
|
||||||
3788AC2126F683AB00F6BAA9 /* Home */,
|
3788AC2126F683AB00F6BAA9 /* Home */,
|
||||||
3761AC0526F0F96100AA496F /* Modifiers */,
|
3761AC0526F0F96100AA496F /* Modifiers */,
|
||||||
371AAE2326CEB9E800901972 /* Navigation */,
|
371AAE2326CEB9E800901972 /* Navigation */,
|
||||||
@ -2124,6 +2137,7 @@
|
|||||||
373C8FE3275B955100CB5936 /* CommentsPage.swift */,
|
373C8FE3275B955100CB5936 /* CommentsPage.swift */,
|
||||||
37FB28402721B22200A57617 /* ContentItem.swift */,
|
37FB28402721B22200A57617 /* ContentItem.swift */,
|
||||||
37141672267A8E10006CA35D /* Country.swift */,
|
37141672267A8E10006CA35D /* Country.swift */,
|
||||||
|
37494EA629200E0B000DF176 /* DocumentsModel.swift */,
|
||||||
37599F2F272B42810087F250 /* FavoriteItem.swift */,
|
37599F2F272B42810087F250 /* FavoriteItem.swift */,
|
||||||
37599F33272B44000087F250 /* FavoritesModel.swift */,
|
37599F33272B44000087F250 /* FavoritesModel.swift */,
|
||||||
37BC50AB2778BCBA00510953 /* HistoryModel.swift */,
|
37BC50AB2778BCBA00510953 /* HistoryModel.swift */,
|
||||||
@ -2767,6 +2781,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
374710052755291C00CE0F87 /* SearchField.swift in Sources */,
|
374710052755291C00CE0F87 /* SearchField.swift in Sources */,
|
||||||
|
37494EA529200B14000DF176 /* DocumentsView.swift in Sources */,
|
||||||
374DE88028BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */,
|
374DE88028BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */,
|
||||||
37ECED56289FE166002BC2C9 /* SafeArea.swift in Sources */,
|
37ECED56289FE166002BC2C9 /* SafeArea.swift in Sources */,
|
||||||
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||||
@ -2883,6 +2898,7 @@
|
|||||||
37E2EEAB270656EC00170416 /* BrowserPlayerControls.swift in Sources */,
|
37E2EEAB270656EC00170416 /* BrowserPlayerControls.swift in Sources */,
|
||||||
370B79CC286279BA0045DB77 /* UIViewController+HideHomeIndicator.swift in Sources */,
|
370B79CC286279BA0045DB77 /* UIViewController+HideHomeIndicator.swift in Sources */,
|
||||||
37E8B0EC27B326C00024006F /* TimelineView.swift in Sources */,
|
37E8B0EC27B326C00024006F /* TimelineView.swift in Sources */,
|
||||||
|
37494EA729200E0B000DF176 /* DocumentsModel.swift in Sources */,
|
||||||
37136CAC286273060095C0CF /* PersistentSystemOverlays+Backport.swift in Sources */,
|
37136CAC286273060095C0CF /* PersistentSystemOverlays+Backport.swift in Sources */,
|
||||||
374C053527242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,
|
374C053527242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,
|
||||||
376A33E42720CB35000C1D6B /* Account.swift in Sources */,
|
376A33E42720CB35000C1D6B /* Account.swift in Sources */,
|
||||||
|
@ -61,5 +61,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>audio</string>
|
<string>audio</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIFileSharingEnabled</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
Loading…
Reference in New Issue
Block a user