mirror of
https://github.com/yattee/yattee.git
synced 2025-08-04 01:34:10 +00:00
Documents tab with file sharing
This commit is contained in:
@@ -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(
|
||||
|
Reference in New Issue
Block a user