mirror of
https://github.com/yattee/yattee.git
synced 2025-08-04 01:34:10 +00:00
Opening videos by URL and local files
This commit is contained in:
@@ -21,6 +21,7 @@ extension Defaults.Keys {
|
||||
|
||||
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
|
||||
|
||||
static let homeHistoryItems = Key<Int>("homeHistoryItems", default: 30)
|
||||
static let favorites = Key<[FavoriteItem]>("favorites", default: [])
|
||||
|
||||
#if !os(tvOS)
|
||||
|
@@ -17,6 +17,7 @@ struct HomeView: View {
|
||||
#if !os(tvOS)
|
||||
@Default(.favorites) private var favorites
|
||||
#endif
|
||||
@Default(.homeHistoryItems) private var homeHistoryItems
|
||||
|
||||
private var navigation: NavigationModel { .shared }
|
||||
|
||||
@@ -56,7 +57,7 @@ struct HomeView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
HistoryView(limit: 100)
|
||||
HistoryView(limit: homeHistoryItems)
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
|
@@ -5,10 +5,18 @@ struct MenuCommands: Commands {
|
||||
@Binding var model: MenuModel
|
||||
|
||||
var body: some Commands {
|
||||
openVideosMenu
|
||||
navigationMenu
|
||||
playbackMenu
|
||||
}
|
||||
|
||||
private var openVideosMenu: some Commands {
|
||||
CommandGroup(after: .newItem) {
|
||||
Button("Open Videos...") { model.navigation?.presentingOpenVideos = true }
|
||||
.keyboardShortcut("t")
|
||||
}
|
||||
}
|
||||
|
||||
private var navigationMenu: some Commands {
|
||||
CommandGroup(before: .windowSize) {
|
||||
Button("Home") {
|
||||
|
@@ -6,13 +6,13 @@ import SwiftUI
|
||||
|
||||
struct AppSidebarNavigation: View {
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
|
||||
#if os(iOS)
|
||||
@State private var didApplyPrimaryViewWorkAround = false
|
||||
|
||||
@EnvironmentObject<CommentsModel> private var comments
|
||||
@EnvironmentObject<InstancesModel> private var instances
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@@ -74,7 +74,15 @@ struct AppSidebarNavigation: View {
|
||||
}
|
||||
#endif
|
||||
|
||||
ToolbarItem(placement: accountsMenuToolbarItemPlacement) {
|
||||
ToolbarItemGroup(placement: openVideosToolbarItemPlacement) {
|
||||
Button {
|
||||
navigation.presentingOpenVideos = true
|
||||
} label: {
|
||||
Label("Open Videos", systemImage: "play.circle.fill")
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItemGroup(placement: accountsMenuToolbarItemPlacement) {
|
||||
AccountsMenuView()
|
||||
.help(
|
||||
"Switch Instances and Accounts\n" +
|
||||
@@ -96,6 +104,14 @@ struct AppSidebarNavigation: View {
|
||||
}
|
||||
}
|
||||
|
||||
var openVideosToolbarItemPlacement: ToolbarItemPlacement {
|
||||
#if os(iOS)
|
||||
return .navigationBarLeading
|
||||
#else
|
||||
return .automatic
|
||||
#endif
|
||||
}
|
||||
|
||||
var accountsMenuToolbarItemPlacement: ToolbarItemPlacement {
|
||||
#if os(iOS)
|
||||
return .bottomBar
|
||||
|
@@ -139,6 +139,9 @@ struct AppTabNavigation: View {
|
||||
}
|
||||
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button(action: { navigation.presentingOpenVideos = true }) {
|
||||
Label("Open Videos", systemImage: "play.circle.fill")
|
||||
}
|
||||
AccountsMenuView()
|
||||
}
|
||||
}
|
||||
|
@@ -119,6 +119,11 @@ struct ContentView: View {
|
||||
}
|
||||
)
|
||||
#endif
|
||||
.background(
|
||||
EmptyView().sheet(isPresented: $navigation.presentingOpenVideos) {
|
||||
OpenVideosView()
|
||||
}
|
||||
)
|
||||
.background(playerViewInitialize)
|
||||
.alert(isPresented: $navigation.presentingAlert) { navigation.alert }
|
||||
}
|
||||
|
@@ -27,6 +27,11 @@ struct OpenURLHandler {
|
||||
}
|
||||
#endif
|
||||
|
||||
if url.isFileURL {
|
||||
OpenVideosModel.shared.open(url)
|
||||
return
|
||||
}
|
||||
|
||||
let parser = URLParser(url: urlByReplacingYatteeProtocol(url))
|
||||
|
||||
switch parser.destination {
|
||||
|
@@ -9,7 +9,7 @@ struct PlaybackStatsView: View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
mpvPlaybackStatRow("Hardware decoder".localized(), player.mpvBackend.hwDecoder)
|
||||
mpvPlaybackStatRow("Dropped frames".localized(), String(player.mpvBackend.frameDropCount))
|
||||
mpvPlaybackStatRow("Stream FPS".localized(), String(format: "%.2ffps", player.mpvBackend.outputFps))
|
||||
mpvPlaybackStatRow("Stream FPS".localized(), player.mpvBackend.formattedOutputFps)
|
||||
mpvPlaybackStatRow("Cached time".localized(), String(format: "%.2fs", player.mpvBackend.cacheDuration))
|
||||
}
|
||||
.padding(.top, 2)
|
||||
|
@@ -34,6 +34,7 @@ struct PlayerQueueRow: View {
|
||||
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
|
||||
|
||||
player.videoBeingOpened = item.video
|
||||
player.show()
|
||||
|
||||
if history {
|
||||
player.playHistory(item, at: watchStoppedAt)
|
||||
@@ -72,3 +73,11 @@ struct PlayerQueueRow: View {
|
||||
return .secondsInDefaultTimescale(seconds)
|
||||
}
|
||||
}
|
||||
|
||||
struct PlayerQueueRow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PlayerQueueRow(item: .init(
|
||||
.local(URL(string: "https://apple.com")!)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@@ -74,18 +74,20 @@ struct VideoDetails: View {
|
||||
"Info".localized(),
|
||||
"info.circle", .info, !video.isNil
|
||||
)
|
||||
pageButton(
|
||||
"Chapters".localized(),
|
||||
"bookmark", .chapters, !(video?.chapters.isEmpty ?? true)
|
||||
)
|
||||
pageButton(
|
||||
"Comments".localized(),
|
||||
"text.bubble", .comments, !video.isNil
|
||||
) { comments.load() }
|
||||
pageButton(
|
||||
"Related".localized(),
|
||||
"rectangle.stack.fill", .related, !video.isNil
|
||||
)
|
||||
if let video, !video.isLocal {
|
||||
pageButton(
|
||||
"Chapters".localized(),
|
||||
"bookmark", .chapters, !video.chapters.isEmpty && !video.isLocal
|
||||
)
|
||||
pageButton(
|
||||
"Comments".localized(),
|
||||
"text.bubble", .comments, !video.isLocal
|
||||
) { comments.load() }
|
||||
pageButton(
|
||||
"Related".localized(),
|
||||
"rectangle.stack.fill", .related, !video.isLocal
|
||||
)
|
||||
}
|
||||
pageButton(
|
||||
"Queue".localized(),
|
||||
"list.number", .queue, !player.queue.isEmpty
|
||||
@@ -100,6 +102,11 @@ struct VideoDetails: View {
|
||||
Pager(page: page, data: DetailsPage.allCases, id: \.self) {
|
||||
if !player.currentItem.isNil || page.index == DetailsPage.queue.index {
|
||||
detailsByPage($0)
|
||||
#if os(iOS)
|
||||
.padding(.bottom, SafeArea.insets.bottom)
|
||||
#else
|
||||
.padding(.bottom, 6)
|
||||
#endif
|
||||
} else {
|
||||
VStack {}
|
||||
}
|
||||
@@ -156,7 +163,7 @@ struct VideoDetails: View {
|
||||
}
|
||||
|
||||
private var contentItem: ContentItem {
|
||||
ContentItem(video: player.currentVideo!)
|
||||
ContentItem(video: player.currentVideo)
|
||||
}
|
||||
|
||||
func pageButton(
|
||||
@@ -228,12 +235,14 @@ struct VideoDetails: View {
|
||||
var detailsPage: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let video {
|
||||
VStack(spacing: 6) {
|
||||
videoProperties
|
||||
if !video.isLocal {
|
||||
VStack(spacing: 6) {
|
||||
videoProperties
|
||||
|
||||
Divider()
|
||||
Divider()
|
||||
}
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
.padding(.bottom, 6)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) {
|
||||
@@ -248,16 +257,82 @@ struct VideoDetails: View {
|
||||
#if os(iOS)
|
||||
.padding(.bottom, player.playingFullScreen ? 10 : SafeArea.insets.bottom)
|
||||
#endif
|
||||
} else {
|
||||
} else if !video.isLocal {
|
||||
Text("No description")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Group {
|
||||
if player.activeBackend == .mpv, player.mpvBackend.videoFormat != "unknown" {
|
||||
videoDetailGroupHeading("Video")
|
||||
|
||||
videoDetailRow("Format", value: player.mpvBackend.videoFormat)
|
||||
videoDetailRow("Codec", value: player.mpvBackend.videoCodec)
|
||||
videoDetailRow("Hardware Decoder", value: player.mpvBackend.hwDecoder)
|
||||
videoDetailRow("Driver", value: player.mpvBackend.currentVo)
|
||||
videoDetailRow("Size", value: player.formattedSize)
|
||||
videoDetailRow("FPS", value: player.mpvBackend.formattedOutputFps)
|
||||
} else if player.activeBackend == .appleAVPlayer, let width = player.backend.videoWidth, width > 0 {
|
||||
videoDetailGroupHeading("Video")
|
||||
videoDetailRow("Size", value: player.formattedSize)
|
||||
}
|
||||
}
|
||||
|
||||
if player.activeBackend == .mpv, player.mpvBackend.audioFormat != "unknown" {
|
||||
Group {
|
||||
videoDetailGroupHeading("Audio")
|
||||
videoDetailRow("Format", value: player.mpvBackend.audioFormat)
|
||||
videoDetailRow("Codec", value: player.mpvBackend.audioCodec)
|
||||
videoDetailRow("Driver", value: player.mpvBackend.currentAo)
|
||||
videoDetailRow("Channels", value: player.mpvBackend.audioChannels)
|
||||
videoDetailRow("Sample Rate", value: player.mpvBackend.audioSampleRate)
|
||||
}
|
||||
}
|
||||
|
||||
if video.localStream != nil || video.localStreamFileExtension != nil {
|
||||
videoDetailGroupHeading("File")
|
||||
}
|
||||
|
||||
if let fileExtension = video.localStreamFileExtension {
|
||||
videoDetailRow("File Extension", value: fileExtension)
|
||||
}
|
||||
|
||||
if let url = video.localStream?.localURL, video.localStreamIsRemoteURL {
|
||||
videoDetailRow("URL", value: url.absoluteString)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
@ViewBuilder func videoDetailGroupHeading(_ heading: String) -> some View {
|
||||
Text(heading.uppercased())
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
@ViewBuilder func videoDetailRow(_ detail: String, value: String) -> some View {
|
||||
HStack {
|
||||
Text(detail)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
let value = Text(value)
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
value
|
||||
#if !os(tvOS)
|
||||
.textSelection(.enabled)
|
||||
#endif
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
@ViewBuilder var videoProperties: some View {
|
||||
HStack(spacing: 2) {
|
||||
publishedDateSection
|
||||
|
@@ -328,7 +328,6 @@ struct VideoPlayerView: View {
|
||||
if !fullScreenPlayer {
|
||||
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails)
|
||||
#if os(iOS)
|
||||
// .zIndex(-1)
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
#endif
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
|
@@ -24,15 +24,38 @@ struct VideoBanner: View {
|
||||
#endif
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(video?.title ?? "Loading...".localized())
|
||||
.truncationMode(.middle)
|
||||
.lineLimit(2)
|
||||
.font(.headline)
|
||||
.frame(alignment: .leading)
|
||||
Group {
|
||||
if let video {
|
||||
HStack(alignment: .top) {
|
||||
Text(video.displayTitle + "\n")
|
||||
if video.isLocal, let fileExtension = video.localStreamFileExtension {
|
||||
Spacer()
|
||||
Text(fileExtension)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Loading contents of the video, please wait")
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
}
|
||||
.truncationMode(.middle)
|
||||
.lineLimit(2)
|
||||
.font(.headline)
|
||||
.frame(alignment: .leading)
|
||||
|
||||
HStack {
|
||||
Text(video?.author ?? "")
|
||||
.lineLimit(1)
|
||||
Group {
|
||||
if let video {
|
||||
if !video.isLocal || video.localStreamIsRemoteURL {
|
||||
Text(video.displayAuthor)
|
||||
}
|
||||
} else {
|
||||
Text("Video Author")
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
}
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -40,10 +63,8 @@ struct VideoBanner: View {
|
||||
progressView
|
||||
#endif
|
||||
|
||||
if let time = (videoDuration ?? video?.length ?? 0).formattedAsPlaybackTime() {
|
||||
Text(time)
|
||||
.fontWeight(.light)
|
||||
}
|
||||
Text((videoDuration ?? video?.length ?? 0).formattedAsPlaybackTime() ?? PlayerTimeModel.timePlaceholder)
|
||||
.fontWeight(.light)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@@ -71,20 +92,30 @@ struct VideoBanner: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private var smallThumbnail: some View {
|
||||
let url = video?.thumbnailURL(quality: .medium)
|
||||
|
||||
WebImage(url: url)
|
||||
.resizable()
|
||||
.placeholder {
|
||||
ProgressView()
|
||||
Group {
|
||||
if let video {
|
||||
if let thumbnail = video.thumbnailURL(quality: .medium) {
|
||||
WebImage(url: thumbnail)
|
||||
.resizable()
|
||||
.placeholder {
|
||||
ProgressView()
|
||||
}
|
||||
.indicator(.activity)
|
||||
} else if video.localStreamIsFile {
|
||||
Image(systemName: "folder")
|
||||
} else if video.localStreamIsRemoteURL {
|
||||
Image(systemName: "globe")
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
.indicator(.activity)
|
||||
}
|
||||
#if os(tvOS)
|
||||
.frame(width: thumbnailWidth, height: 140)
|
||||
.mask(RoundedRectangle(cornerRadius: 12))
|
||||
.frame(width: thumbnailWidth, height: thumbnailHeight)
|
||||
.mask(RoundedRectangle(cornerRadius: 12))
|
||||
#else
|
||||
.frame(width: thumbnailWidth, height: 60)
|
||||
.mask(RoundedRectangle(cornerRadius: 6))
|
||||
.frame(width: thumbnailWidth, height: thumbnailHeight)
|
||||
.mask(RoundedRectangle(cornerRadius: 6))
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -96,6 +127,14 @@ struct VideoBanner: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
private var thumbnailHeight: Double {
|
||||
#if os(tvOS)
|
||||
140
|
||||
#else
|
||||
60
|
||||
#endif
|
||||
}
|
||||
|
||||
private var progressView: some View {
|
||||
Group {
|
||||
if !playbackTime.isNil, !(video?.live ?? false) {
|
||||
@@ -120,6 +159,9 @@ struct VideoBanner_Previews: PreviewProvider {
|
||||
VStack(spacing: 20) {
|
||||
VideoBanner(video: Video.fixture, playbackTime: CMTime(seconds: 400, preferredTimescale: 10000))
|
||||
VideoBanner(video: Video.fixtureUpcomingWithoutPublishedOrViews)
|
||||
VideoBanner(video: .local(URL(string: "https://apple.com/a/directory/of/video+that+has+very+long+title+that+will+likely.mp4")!))
|
||||
VideoBanner(video: .local(URL(string: "file://a/b/c/d/e/f.mkv")!))
|
||||
VideoBanner()
|
||||
}
|
||||
.frame(maxWidth: 900)
|
||||
}
|
||||
|
@@ -157,7 +157,7 @@ struct ControlsBar: View {
|
||||
if let video = model.currentVideo {
|
||||
Group {
|
||||
Section {
|
||||
if accounts.app.supportsUserPlaylists && accounts.signedIn {
|
||||
if accounts.app.supportsUserPlaylists && accounts.signedIn, !video.isLocal {
|
||||
Section {
|
||||
Button {
|
||||
navigation.presentAddToPlaylist(video)
|
||||
@@ -180,36 +180,38 @@ struct ControlsBar: View {
|
||||
#endif
|
||||
|
||||
Section {
|
||||
Button {
|
||||
NavigationModel.openChannel(
|
||||
video.channel,
|
||||
player: model,
|
||||
recents: recents,
|
||||
navigation: navigation,
|
||||
navigationStyle: navigationStyle
|
||||
)
|
||||
} label: {
|
||||
Label("\(video.author) Channel", systemImage: "rectangle.stack.fill.badge.person.crop")
|
||||
}
|
||||
if !video.isLocal {
|
||||
Button {
|
||||
NavigationModel.openChannel(
|
||||
video.channel,
|
||||
player: model,
|
||||
recents: recents,
|
||||
navigation: navigation,
|
||||
navigationStyle: navigationStyle
|
||||
)
|
||||
} label: {
|
||||
Label("\(video.author) Channel", systemImage: "rectangle.stack.fill.badge.person.crop")
|
||||
}
|
||||
|
||||
if accounts.app.supportsSubscriptions, accounts.signedIn {
|
||||
if subscriptions.isSubscribing(video.channel.id) {
|
||||
Button {
|
||||
#if os(tvOS)
|
||||
subscriptions.unsubscribe(video.channel.id)
|
||||
#else
|
||||
navigation.presentUnsubscribeAlert(video.channel, subscriptions: subscriptions)
|
||||
#endif
|
||||
} label: {
|
||||
Label("Unsubscribe", systemImage: "xmark.circle")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
subscriptions.subscribe(video.channel.id) {
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
if accounts.app.supportsSubscriptions, accounts.signedIn {
|
||||
if subscriptions.isSubscribing(video.channel.id) {
|
||||
Button {
|
||||
#if os(tvOS)
|
||||
subscriptions.unsubscribe(video.channel.id)
|
||||
#else
|
||||
navigation.presentUnsubscribeAlert(video.channel, subscriptions: subscriptions)
|
||||
#endif
|
||||
} label: {
|
||||
Label("Unsubscribe", systemImage: "xmark.circle")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
subscriptions.subscribe(video.channel.id) {
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
}
|
||||
} label: {
|
||||
Label("Subscribe", systemImage: "star.circle")
|
||||
}
|
||||
} label: {
|
||||
Label("Subscribe", systemImage: "star.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,7 +230,7 @@ struct ControlsBar: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
let notPlaying = "Not Playing".localized()
|
||||
Text(model.currentVideo?.title ?? notPlaying)
|
||||
Text(model.currentVideo?.displayTitle ?? notPlaying)
|
||||
.font(.system(size: 14))
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(model.currentVideo.isNil ? .secondary : .accentColor)
|
||||
@@ -236,12 +238,12 @@ struct ControlsBar: View {
|
||||
.lineLimit(titleLineLimit)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
if let video = model.currentVideo {
|
||||
if let video = model.currentVideo, !video.localStreamIsFile {
|
||||
HStack(spacing: 2) {
|
||||
Text(video.author)
|
||||
Text(video.displayAuthor)
|
||||
.font(.system(size: 12))
|
||||
|
||||
if !presentingControls {
|
||||
if !presentingControls && !video.isLocal {
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "person.2.fill")
|
||||
|
||||
@@ -271,7 +273,7 @@ struct ControlsBar: View {
|
||||
|
||||
private var authorAvatar: some View {
|
||||
Group {
|
||||
if let video = model.currentItem?.video, let url = video.channel.thumbnailURL {
|
||||
if let url = model.currentItem?.video?.channel.thumbnailURL {
|
||||
WebImage(url: url)
|
||||
.resizable()
|
||||
.placeholder {
|
||||
@@ -284,10 +286,20 @@ struct ControlsBar: View {
|
||||
Color(white: 0.6)
|
||||
.opacity(0.5)
|
||||
|
||||
Image(systemName: "play.rectangle")
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.system(size: 20))
|
||||
.contentShape(Rectangle())
|
||||
Group {
|
||||
if let video = model.currentItem?.video, video.isLocal {
|
||||
if video.localStreamIsFile {
|
||||
Image(systemName: "folder")
|
||||
} else if video.localStreamIsRemoteURL {
|
||||
Image(systemName: "globe")
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "play.rectangle")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.system(size: 20))
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
167
Shared/Views/OpenVideosView.swift
Normal file
167
Shared/Views/OpenVideosView.swift
Normal file
@@ -0,0 +1,167 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OpenVideosView: View {
|
||||
@State private var presentingFileImporter = false
|
||||
@State private var urlsToOpenText = "https://r.yattee.stream/demo/mp4/1.mp4\nhttps://r.yattee.stream/demo/mp4/2.mp4\nhttps://r.yattee.stream/demo/mp4/3.mp4\nhttps://www.youtube.com/watch?v=N9WHp8DG2WY"
|
||||
@State private var playbackMode = OpenVideosModel.PlaybackMode.playNow
|
||||
@State private var removeQueueItems = false
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SearchModel> private var search
|
||||
|
||||
@Environment(\.openURL) private var openURL
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
openVideos
|
||||
.frame(minWidth: 600, maxWidth: 800, minHeight: 250)
|
||||
#else
|
||||
NavigationView {
|
||||
openVideos
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: { presentationMode.wrappedValue.dismiss() }) {
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.navigationTitle("Open Videos")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var openVideos: some View {
|
||||
VStack(alignment: .leading) {
|
||||
ZStack(alignment: .topLeading) {
|
||||
#if os(tvOS)
|
||||
TextField("URLs to Open", text: $urlsToOpenText)
|
||||
#else
|
||||
TextEditor(text: $urlsToOpenText)
|
||||
.padding(2)
|
||||
.border(Color(white: 0.8), width: 1)
|
||||
.frame(maxHeight: 200)
|
||||
#if !os(macOS)
|
||||
.keyboardType(.URL)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
Text("Enter or paste URLs to open, one per line")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Picker("Playback Mode", selection: $playbackMode) {
|
||||
ForEach(OpenVideosModel.PlaybackMode.allCases, id: \.rawValue) { mode in
|
||||
Text(mode.description).tag(mode)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.padding(.bottom, 5)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
Toggle(isOn: $removeQueueItems) {
|
||||
Text("Clear queue before opening")
|
||||
}
|
||||
.disabled(!playbackMode.allowsRemovingQueueItems)
|
||||
.padding(.bottom)
|
||||
|
||||
HStack {
|
||||
Group {
|
||||
Button {
|
||||
openURLs(urlsToOpenFromText)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "network")
|
||||
Text("Open URLs")
|
||||
.fontWeight(.bold)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.disabled(urlsToOpenFromText.isEmpty)
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
#endif
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
presentingFileImporter = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "folder")
|
||||
Text("Open Files")
|
||||
.fontWeight(.bold)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.foregroundColor(Color.accentColor.opacity(0.33))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
#if !os(tvOS)
|
||||
.fileImporter(
|
||||
isPresented: $presentingFileImporter,
|
||||
allowedContentTypes: [.audiovisualContent],
|
||||
allowsMultipleSelection: true
|
||||
) { result in
|
||||
do {
|
||||
let selectedFiles = try result.get()
|
||||
let urlsToOpen = selectedFiles.map { url in
|
||||
if let bookmarkURL = URLBookmarkModel.shared.loadBookmark(url) {
|
||||
return bookmarkURL
|
||||
}
|
||||
|
||||
if url.startAccessingSecurityScopedResource() {
|
||||
URLBookmarkModel.shared.saveBookmark(url)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
openURLs(selectedFiles)
|
||||
} catch {
|
||||
NavigationModel.shared.presentAlert(title: "Could not open Files")
|
||||
}
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var urlsToOpenFromText: [URL] {
|
||||
urlsToOpenText.split(whereSeparator: \.isNewline).compactMap { URL(string: String($0)) }
|
||||
}
|
||||
|
||||
func openURLs(_ urls: [URL]) {
|
||||
OpenVideosModel.shared.openURLs(urls, removeQueueItems: removeQueueItems, playbackMode: playbackMode)
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenVideosView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
OpenVideosView()
|
||||
#if os(iOS)
|
||||
.navigationViewStyle(.stack)
|
||||
#endif
|
||||
}
|
||||
}
|
@@ -70,7 +70,7 @@ struct VideoContextMenuView: View {
|
||||
addToQueueButton
|
||||
}
|
||||
|
||||
if accounts.app.supportsUserPlaylists, accounts.signedIn {
|
||||
if accounts.app.supportsUserPlaylists, accounts.signedIn, !video.isLocal {
|
||||
Section {
|
||||
addToPlaylistButton
|
||||
addToLastPlaylistButton
|
||||
@@ -87,7 +87,7 @@ struct VideoContextMenuView: View {
|
||||
}
|
||||
#endif
|
||||
|
||||
if !inChannelView, !inChannelPlaylistView {
|
||||
if !inChannelView, !inChannelPlaylistView, !video.isLocal {
|
||||
Section {
|
||||
openChannelButton
|
||||
|
||||
|
@@ -4,12 +4,16 @@
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.bookmarks.app-scope</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
Reference in New Issue
Block a user