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 {
|
||||
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
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ extension Defaults.Keys {
|
||||
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
|
||||
|
||||
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 showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
|
||||
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
|
||||
|
||||
@Default(.showHome) private var showHome
|
||||
@Default(.showDocuments) private var showDocuments
|
||||
@Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem
|
||||
@Default(.visibleSections) private var visibleSections
|
||||
|
||||
@ -26,6 +27,10 @@ struct AppTabNavigation: View {
|
||||
homeNavigationView
|
||||
}
|
||||
|
||||
if showDocuments {
|
||||
documentsNavigationView
|
||||
}
|
||||
|
||||
if !accounts.isEmpty {
|
||||
if subscriptionsVisible {
|
||||
subscriptionsNavigationView
|
||||
@ -73,6 +78,18 @@ struct AppTabNavigation: View {
|
||||
.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 {
|
||||
NavigationView {
|
||||
LazyView(SubscriptionsView())
|
||||
|
@ -6,6 +6,7 @@ struct Sidebar: View {
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
|
||||
@Default(.showHome) private var showHome
|
||||
@Default(.showDocuments) private var showDocuments
|
||||
@Default(.visibleSections) private var visibleSections
|
||||
|
||||
var body: some View {
|
||||
@ -48,6 +49,18 @@ struct Sidebar: View {
|
||||
}
|
||||
.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 visibleSections.contains(.subscriptions),
|
||||
accounts.app.supportsSubscriptions && accounts.signedIn
|
||||
|
@ -29,6 +29,22 @@ struct PlayerQueueRow: View {
|
||||
|
||||
var body: some View {
|
||||
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.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
|
||||
|
@ -14,6 +14,7 @@ struct BrowsingSettings: View {
|
||||
@Default(.channelOnThumbnail) private var channelOnThumbnail
|
||||
@Default(.timeOnThumbnail) private var timeOnThumbnail
|
||||
@Default(.showHome) private var showHome
|
||||
@Default(.showDocuments) private var showDocuments
|
||||
@Default(.showFavoritesInHome) private var showFavoritesInHome
|
||||
@Default(.showOpenActionsInHome) private var showOpenActionsInHome
|
||||
@Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem
|
||||
@ -85,6 +86,7 @@ struct BrowsingSettings: View {
|
||||
}
|
||||
}
|
||||
.multilineTextAlignment(.trailing)
|
||||
|
||||
if !accounts.isEmpty {
|
||||
Toggle("Show Favorites", isOn: $showFavoritesInHome)
|
||||
|
||||
@ -124,6 +126,8 @@ struct BrowsingSettings: View {
|
||||
Toggle("Show Open Videos toolbar button", isOn: $showOpenActionsToolbarItem)
|
||||
#endif
|
||||
#if os(iOS)
|
||||
Toggle("Show Documents", isOn: $showDocuments)
|
||||
|
||||
Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing)
|
||||
.onChange(of: lockPortraitWhenBrowsing) { lock in
|
||||
if lock {
|
||||
|
@ -22,7 +22,7 @@ struct VideoBanner: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: stackAlignment, spacing: 12) {
|
||||
VStack(spacing: thumbnailStackSpacing) {
|
||||
ZStack(alignment: .bottom) {
|
||||
smallThumbnail
|
||||
|
||||
#if !os(tvOS)
|
||||
@ -55,6 +55,20 @@ struct VideoBanner: View {
|
||||
if let video {
|
||||
if !video.isLocal || video.localStreamIsRemoteURL {
|
||||
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 {
|
||||
Text("Video Author")
|
||||
@ -69,8 +83,10 @@ struct VideoBanner: View {
|
||||
progressView
|
||||
#endif
|
||||
|
||||
Text((videoDuration ?? video?.length ?? 0).formattedAsPlaybackTime() ?? PlayerTimeModel.timePlaceholder)
|
||||
.fontWeight(.light)
|
||||
if !(video?.localStreamIsDirectory ?? false) {
|
||||
Text(videoDurationLabel)
|
||||
.fontWeight(.light)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@ -98,24 +114,15 @@ struct VideoBanner: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
private var thumbnailStackSpacing: Double {
|
||||
#if os(tvOS)
|
||||
8
|
||||
#else
|
||||
2
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder private var smallThumbnail: some View {
|
||||
Group {
|
||||
ZStack {
|
||||
Color("PlaceholderColor")
|
||||
if let video {
|
||||
if let thumbnail = video.thumbnailURL(quality: .medium) {
|
||||
WebImage(url: thumbnail, options: [.lowPriority])
|
||||
.resizable()
|
||||
} else if video.localStreamIsFile {
|
||||
Image(systemName: "folder")
|
||||
} else if video.localStreamIsRemoteURL {
|
||||
Image(systemName: "globe")
|
||||
} else if video.isLocal {
|
||||
Image(systemName: video.localStreamImageSystemName)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "ellipsis")
|
||||
@ -146,6 +153,11 @@ struct VideoBanner: View {
|
||||
#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 {
|
||||
Group {
|
||||
if !playbackTime.isNil, !(video?.live ?? false) {
|
||||
@ -157,11 +169,13 @@ struct VideoBanner: View {
|
||||
}
|
||||
|
||||
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 {
|
||||
videoDuration ?? video?.length ?? 1
|
||||
guard videoDuration != 0 else { return 1 }
|
||||
return videoDuration ?? video?.length ?? 1
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,11 +288,7 @@ struct ControlsBar: View {
|
||||
|
||||
Group {
|
||||
if let video = model.currentItem?.video, video.isLocal {
|
||||
if video.localStreamIsFile {
|
||||
Image(systemName: "folder")
|
||||
} else if video.localStreamIsRemoteURL {
|
||||
Image(systemName: "globe")
|
||||
}
|
||||
Image(systemName: video.localStreamImageSystemName)
|
||||
} else {
|
||||
Image(systemName: "play.rectangle")
|
||||
}
|
||||
|
@ -37,54 +37,64 @@ struct VideoContextMenuView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder var contextMenu: some View {
|
||||
if saveHistory {
|
||||
Section {
|
||||
if let watchedAtString {
|
||||
Text(watchedAtString)
|
||||
}
|
||||
if !video.localStreamIsDirectory {
|
||||
if saveHistory {
|
||||
Section {
|
||||
if let watchedAtString {
|
||||
Text(watchedAtString)
|
||||
}
|
||||
|
||||
if !watch.isNil, !watch!.finished, !watchingNow {
|
||||
continueButton
|
||||
}
|
||||
if !watch.isNil, !watch!.finished, !watchingNow {
|
||||
continueButton
|
||||
}
|
||||
|
||||
if !(watch?.finished ?? false) {
|
||||
markAsWatchedButton
|
||||
}
|
||||
if !(watch?.finished ?? false) {
|
||||
markAsWatchedButton
|
||||
}
|
||||
|
||||
if !watch.isNil, !watchingNow {
|
||||
removeFromHistoryButton
|
||||
if !watch.isNil, !watchingNow {
|
||||
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)
|
||||
playNowInPictureInPictureButton
|
||||
playNowInMusicMode
|
||||
Section {
|
||||
ShareButton(contentItem: .init(video: video))
|
||||
}
|
||||
#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)
|
||||
#if os(iOS)
|
||||
if video.isLocal, let url = video.localStream?.localURL, DocumentsModel.shared.isDocument(url) {
|
||||
Section {
|
||||
removeDocumentButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
Section {
|
||||
ShareButton(contentItem: .init(video: video))
|
||||
}
|
||||
#endif
|
||||
|
||||
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 {
|
||||
Button {
|
||||
NavigationModel.openChannel(
|
||||
|
@ -304,6 +304,8 @@
|
||||
37484C3126FCB8F900287258 /* 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 */; };
|
||||
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 */; };
|
||||
374AB3D828BCAF0000DF56FB /* 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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1821,6 +1825,14 @@
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
37494EA329200AD4000DF176 /* Documents */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37494EA429200B14000DF176 /* DocumentsView.swift */,
|
||||
);
|
||||
path = Documents;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3749BF6C27ADA135000480FF /* mpv */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -2022,6 +2034,7 @@
|
||||
37D4B0C12671614700C925CA /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37494EA329200AD4000DF176 /* Documents */,
|
||||
3788AC2126F683AB00F6BAA9 /* Home */,
|
||||
3761AC0526F0F96100AA496F /* Modifiers */,
|
||||
371AAE2326CEB9E800901972 /* Navigation */,
|
||||
@ -2124,6 +2137,7 @@
|
||||
373C8FE3275B955100CB5936 /* CommentsPage.swift */,
|
||||
37FB28402721B22200A57617 /* ContentItem.swift */,
|
||||
37141672267A8E10006CA35D /* Country.swift */,
|
||||
37494EA629200E0B000DF176 /* DocumentsModel.swift */,
|
||||
37599F2F272B42810087F250 /* FavoriteItem.swift */,
|
||||
37599F33272B44000087F250 /* FavoritesModel.swift */,
|
||||
37BC50AB2778BCBA00510953 /* HistoryModel.swift */,
|
||||
@ -2767,6 +2781,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
374710052755291C00CE0F87 /* SearchField.swift in Sources */,
|
||||
37494EA529200B14000DF176 /* DocumentsView.swift in Sources */,
|
||||
374DE88028BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */,
|
||||
37ECED56289FE166002BC2C9 /* SafeArea.swift in Sources */,
|
||||
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||
@ -2883,6 +2898,7 @@
|
||||
37E2EEAB270656EC00170416 /* BrowserPlayerControls.swift in Sources */,
|
||||
370B79CC286279BA0045DB77 /* UIViewController+HideHomeIndicator.swift in Sources */,
|
||||
37E8B0EC27B326C00024006F /* TimelineView.swift in Sources */,
|
||||
37494EA729200E0B000DF176 /* DocumentsModel.swift in Sources */,
|
||||
37136CAC286273060095C0CF /* PersistentSystemOverlays+Backport.swift in Sources */,
|
||||
374C053527242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,
|
||||
376A33E42720CB35000C1D6B /* Account.swift in Sources */,
|
||||
|
@ -61,5 +61,7 @@
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
Loading…
Reference in New Issue
Block a user