mirror of
https://github.com/yattee/yattee.git
synced 2025-08-09 04:04:07 +00:00
iOS 14/macOS Big Sur Support
This commit is contained in:
@@ -51,7 +51,7 @@ struct FavoritesView: View {
|
||||
.navigationTitle("Favorites")
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.background()
|
||||
.background(Color.tertiaryBackground)
|
||||
.frame(minWidth: 360)
|
||||
#endif
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ struct MenuCommands: Commands {
|
||||
}
|
||||
|
||||
private var navigationMenu: some Commands {
|
||||
CommandMenu("Navigation") {
|
||||
CommandGroup(before: .windowSize) {
|
||||
Button("Favorites") {
|
||||
model.navigation?.tabSelection = .favorites
|
||||
}
|
||||
@@ -19,7 +19,7 @@ struct MenuCommands: Commands {
|
||||
Button("Subscriptions") {
|
||||
model.navigation?.tabSelection = .subscriptions
|
||||
}
|
||||
.disabled(!(model.accounts?.app.supportsSubscriptions ?? true))
|
||||
.disabled(subscriptionsDisabled)
|
||||
.keyboardShortcut("2")
|
||||
|
||||
Button("Popular") {
|
||||
@@ -37,9 +37,17 @@ struct MenuCommands: Commands {
|
||||
model.navigation?.tabSelection = .search
|
||||
}
|
||||
.keyboardShortcut("f")
|
||||
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
|
||||
private var subscriptionsDisabled: Bool {
|
||||
!(
|
||||
(model.accounts?.app.supportsSubscriptions ?? false) && model.accounts?.signedIn ?? false
|
||||
)
|
||||
}
|
||||
|
||||
private var playbackMenu: some Commands {
|
||||
CommandMenu("Playback") {
|
||||
Button((model.player?.isPlaying ?? true) ? "Pause" : "Play") {
|
||||
|
@@ -1,26 +0,0 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct UnsubscribeAlertModifier: ViewModifier {
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.alert(unsubscribeAlertTitle, isPresented: $navigation.presentingUnsubscribeAlert) {
|
||||
if let channel = navigation.channelToUnsubscribe {
|
||||
Button("Unsubscribe", role: .destructive) {
|
||||
subscriptions.unsubscribe(channel.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var unsubscribeAlertTitle: String {
|
||||
if let channel = navigation.channelToUnsubscribe {
|
||||
return "Unsubscribe from \(channel.name)"
|
||||
}
|
||||
|
||||
return "Unknown channel"
|
||||
}
|
||||
}
|
@@ -15,13 +15,25 @@ struct AccountsMenuView: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label(model.current?.description ?? "Select Account", systemImage: "person.crop.circle")
|
||||
.labelStyle(.titleAndIcon)
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
label
|
||||
.labelStyle(.titleAndIcon)
|
||||
} else {
|
||||
HStack {
|
||||
Image(systemName: "person.crop.circle")
|
||||
label
|
||||
.labelStyle(.titleOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(instances.isEmpty)
|
||||
.transaction { t in t.animation = .none }
|
||||
}
|
||||
|
||||
private var label: some View {
|
||||
Label(model.current?.description ?? "Select Account", systemImage: "person.crop.circle")
|
||||
}
|
||||
|
||||
private var allAccounts: [Account] {
|
||||
accounts + instances.map(\.anonymousAccount)
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ struct AppSidebarPlaylists: View {
|
||||
LazyView(PlaylistVideosView(playlist))
|
||||
} label: {
|
||||
Label(playlist.title, systemImage: AppSidebarNavigation.symbolSystemImage(playlist.title))
|
||||
.backport
|
||||
.badge(Text("\(playlist.videos.count)"))
|
||||
}
|
||||
.id(playlist.id)
|
||||
|
@@ -18,7 +18,6 @@ struct AppSidebarSubscriptions: View {
|
||||
navigation.presentUnsubscribeAlert(channel)
|
||||
}
|
||||
}
|
||||
.modifier(UnsubscribeAlertModifier())
|
||||
.id("channel\(channel.id)")
|
||||
}
|
||||
}
|
||||
|
@@ -41,6 +41,8 @@ struct AppTabNavigation: View {
|
||||
} else {
|
||||
trendingNavigationView
|
||||
}
|
||||
} else {
|
||||
trendingNavigationView
|
||||
}
|
||||
} else {
|
||||
if accounts.app.supportsPopular {
|
||||
@@ -62,26 +64,7 @@ struct AppTabNavigation: View {
|
||||
}
|
||||
|
||||
NavigationView {
|
||||
LazyView(
|
||||
SearchView()
|
||||
.toolbar { toolbarContent }
|
||||
.searchable(text: $search.queryText, placement: .navigationBarDrawer(displayMode: .always)) {
|
||||
ForEach(search.querySuggestions.collection, id: \.self) { suggestion in
|
||||
Text(suggestion)
|
||||
.searchCompletion(suggestion)
|
||||
}
|
||||
}
|
||||
.onChange(of: search.queryText) { query in
|
||||
search.loadSuggestions(query)
|
||||
}
|
||||
.onSubmit(of: .search) {
|
||||
search.changeQuery { query in
|
||||
query.query = search.queryText
|
||||
}
|
||||
|
||||
recents.addQuery(search.queryText)
|
||||
}
|
||||
)
|
||||
LazyView(SearchView())
|
||||
}
|
||||
.tabItem {
|
||||
Label("Search", systemImage: "magnifyingglass")
|
||||
@@ -129,7 +112,7 @@ struct AppTabNavigation: View {
|
||||
.toolbar { toolbarContent }
|
||||
}
|
||||
.tabItem {
|
||||
Label("Popular", systemImage: "chart.bar")
|
||||
Label("Popular", systemImage: "arrow.up.right.circle")
|
||||
.accessibility(label: Text("Popular"))
|
||||
}
|
||||
.tag(TabSelection.popular)
|
||||
@@ -141,7 +124,7 @@ struct AppTabNavigation: View {
|
||||
.toolbar { toolbarContent }
|
||||
}
|
||||
.tabItem {
|
||||
Label("Trending", systemImage: "chart.line.uptrend.xyaxis")
|
||||
Label("Trending", systemImage: "chart.bar")
|
||||
.accessibility(label: Text("Trending"))
|
||||
}
|
||||
.tag(TabSelection.trending)
|
||||
|
@@ -46,7 +46,7 @@ struct Sidebar: View {
|
||||
}
|
||||
|
||||
var mainNavigationLinks: some View {
|
||||
Section("Videos") {
|
||||
Section(header: Text("Videos")) {
|
||||
NavigationLink(destination: LazyView(FavoritesView()), tag: TabSelection.favorites, selection: $navigation.tabSelection) {
|
||||
Label("Favorites", systemImage: "heart")
|
||||
.accessibility(label: Text("Favorites"))
|
||||
@@ -60,13 +60,13 @@ struct Sidebar: View {
|
||||
|
||||
if accounts.app.supportsPopular {
|
||||
NavigationLink(destination: LazyView(PopularView()), tag: TabSelection.popular, selection: $navigation.tabSelection) {
|
||||
Label("Popular", systemImage: "chart.bar")
|
||||
Label("Popular", systemImage: "arrow.up.right.circle")
|
||||
.accessibility(label: Text("Popular"))
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(destination: LazyView(TrendingView()), tag: TabSelection.trending, selection: $navigation.tabSelection) {
|
||||
Label("Trending", systemImage: "chart.line.uptrend.xyaxis")
|
||||
Label("Trending", systemImage: "chart.bar")
|
||||
.accessibility(label: Text("Trending"))
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,8 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct PlaybackBar: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@@ -64,18 +65,20 @@ struct PlaybackBar: View {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.alert(player.playerError?.localizedDescription ?? "", isPresented: $player.presentingErrorDetails) {
|
||||
Button("OK") {}
|
||||
.alert(isPresented: $player.presentingErrorDetails) {
|
||||
Alert(
|
||||
title: Text("Error"),
|
||||
message: Text(player.playerError?.localizedDescription ?? "")
|
||||
)
|
||||
}
|
||||
.environment(\.colorScheme, .dark)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.padding(4)
|
||||
.background(.black)
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
}
|
||||
|
||||
private var closeButton: some View {
|
||||
Button {
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} label: {
|
||||
Label(
|
||||
"Close",
|
||||
@@ -105,10 +108,18 @@ struct PlaybackBar: View {
|
||||
return "less than a minute"
|
||||
}
|
||||
|
||||
let timeFinishAt = Date.now.addingTimeInterval(remainingSeconds)
|
||||
let timeFinishAtString = timeFinishAt.formatted(date: .omitted, time: .shortened)
|
||||
let timeFinishAt = Date().addingTimeInterval(remainingSeconds)
|
||||
|
||||
return "ends at \(timeFinishAtString)"
|
||||
return "ends at \(formattedTimeFinishAt(timeFinishAt))"
|
||||
}
|
||||
|
||||
private func formattedTimeFinishAt(_ date: Date) -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
|
||||
dateFormatter.dateStyle = .none
|
||||
dateFormatter.timeStyle = .short
|
||||
|
||||
return dateFormatter.string(from: date)
|
||||
}
|
||||
|
||||
private var rateMenu: some View {
|
||||
|
@@ -44,16 +44,18 @@ struct PlayerQueueView: View {
|
||||
}
|
||||
|
||||
ForEach(player.queue) { item in
|
||||
PlayerQueueRow(item: item, fullScreen: $fullScreen)
|
||||
let row = PlayerQueueRow(item: item, fullScreen: $fullScreen)
|
||||
.contextMenu {
|
||||
removeButton(item, history: false)
|
||||
removeAllButton(history: false)
|
||||
}
|
||||
#if os(iOS)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
row.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
removeButton(item, history: false)
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
row
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,14 +65,18 @@ struct PlayerQueueView: View {
|
||||
if !player.history.isEmpty {
|
||||
Section(header: Text("Played Previously")) {
|
||||
ForEach(player.history) { item in
|
||||
PlayerQueueRow(item: item, history: true, fullScreen: $fullScreen)
|
||||
let row = PlayerQueueRow(item: item, history: true, fullScreen: $fullScreen)
|
||||
.contextMenu {
|
||||
removeButton(item, history: true)
|
||||
removeAllButton(history: true)
|
||||
}
|
||||
#if os(iOS)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
removeButton(item, history: true)
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
row.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
removeButton(item, history: true)
|
||||
}
|
||||
} else {
|
||||
row
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -100,28 +106,44 @@ struct PlayerQueueView: View {
|
||||
}
|
||||
|
||||
private func removeButton(_ item: PlayerQueueItem, history: Bool) -> some View {
|
||||
Button(role: .destructive) {
|
||||
if history {
|
||||
player.removeHistory(item)
|
||||
} else {
|
||||
player.remove(item)
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
return Button(role: .destructive) {
|
||||
removeButtonAction(item, history: history)
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}
|
||||
} else {
|
||||
return Button {
|
||||
removeButtonAction(item, history: history)
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
private func removeButtonAction(_ item: PlayerQueueItem, history: Bool) {
|
||||
_ = history ? player.removeHistory(item) : player.remove(item)
|
||||
}
|
||||
|
||||
private func removeAllButton(history: Bool) -> some View {
|
||||
Button(role: .destructive) {
|
||||
if history {
|
||||
player.removeHistoryItems()
|
||||
} else {
|
||||
player.removeQueueItems()
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
return Button(role: .destructive) {
|
||||
removeAllButtonAction(history: history)
|
||||
} label: {
|
||||
Label("Remove All", systemImage: "trash.fill")
|
||||
}
|
||||
} else {
|
||||
return Button {
|
||||
removeAllButtonAction(history: history)
|
||||
} label: {
|
||||
Label("Remove All", systemImage: "trash.fill")
|
||||
}
|
||||
} label: {
|
||||
Label("Remove All", systemImage: "trash.fill")
|
||||
}
|
||||
}
|
||||
|
||||
private func removeAllButtonAction(history: Bool) {
|
||||
_ = history ? player.removeHistoryItems() : player.removeQueueItems()
|
||||
}
|
||||
}
|
||||
|
||||
struct PlayerQueueView_Previews: PreviewProvider {
|
||||
|
@@ -11,14 +11,14 @@ struct VideoDetails: View {
|
||||
@Binding var fullScreen: Bool
|
||||
|
||||
@State private var subscribed = false
|
||||
@State private var confirmationShown = false
|
||||
@State private var presentingUnsubscribeAlert = false
|
||||
@State private var presentingAddToPlaylist = false
|
||||
@State private var presentingShareSheet = false
|
||||
@State private var shareURL: URL?
|
||||
|
||||
@State private var currentPage = Page.details
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@@ -82,7 +82,7 @@ struct VideoDetails: View {
|
||||
if fullScreen {
|
||||
fullScreen = false
|
||||
} else {
|
||||
self.dismiss()
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,19 +184,26 @@ struct VideoDetails: View {
|
||||
Section {
|
||||
if subscribed {
|
||||
Button("Unsubscribe") {
|
||||
confirmationShown = true
|
||||
presentingUnsubscribeAlert = true
|
||||
}
|
||||
#if os(iOS)
|
||||
.backport
|
||||
.tint(.gray)
|
||||
#endif
|
||||
.confirmationDialog("Are you you want to unsubscribe from \(video!.channel.name)?", isPresented: $confirmationShown) {
|
||||
Button("Unsubscribe") {
|
||||
subscriptions.unsubscribe(video!.channel.id)
|
||||
.alert(isPresented: $presentingUnsubscribeAlert) {
|
||||
Alert(
|
||||
title: Text(
|
||||
"Are you you want to unsubscribe from \(video!.channel.name)?"
|
||||
),
|
||||
primaryButton: .destructive(Text("Unsubscribe")) {
|
||||
subscriptions.unsubscribe(video!.channel.id)
|
||||
|
||||
withAnimation {
|
||||
subscribed.toggle()
|
||||
}
|
||||
}
|
||||
withAnimation {
|
||||
subscribed.toggle()
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Button("Subscribe") {
|
||||
@@ -206,12 +213,12 @@ struct VideoDetails: View {
|
||||
subscribed.toggle()
|
||||
}
|
||||
}
|
||||
.backport
|
||||
.tint(.blue)
|
||||
}
|
||||
}
|
||||
.font(.system(size: 13))
|
||||
.buttonStyle(.borderless)
|
||||
.buttonBorderShape(.roundedRectangle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,13 +248,13 @@ struct VideoDetails: View {
|
||||
Text(published)
|
||||
}
|
||||
|
||||
if let publishedAt = video.publishedAt {
|
||||
if let date = video.publishedAt {
|
||||
if video.publishedDate != nil {
|
||||
Text("•")
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(0.3)
|
||||
}
|
||||
Text(publishedAt.formatted(date: .abbreviated, time: .omitted))
|
||||
Text(formattedPublishedAt(date))
|
||||
}
|
||||
}
|
||||
.font(.system(size: 12))
|
||||
@@ -257,6 +264,15 @@ struct VideoDetails: View {
|
||||
}
|
||||
}
|
||||
|
||||
func formattedPublishedAt(_ date: Date) -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
|
||||
dateFormatter.dateStyle = .short
|
||||
dateFormatter.timeStyle = .none
|
||||
|
||||
return dateFormatter.string(from: date)
|
||||
}
|
||||
|
||||
var countsSection: some View {
|
||||
Group {
|
||||
if let video = player.currentVideo {
|
||||
@@ -338,11 +354,17 @@ struct VideoDetails: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if let description = video.description {
|
||||
Text(description)
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.font(.caption)
|
||||
.padding(.bottom, 4)
|
||||
Group {
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
Text(description)
|
||||
.textSelection(.enabled)
|
||||
} else {
|
||||
Text(description)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.font(.caption)
|
||||
.padding(.bottom, 4)
|
||||
} else {
|
||||
Text("No description")
|
||||
.foregroundColor(.secondary)
|
||||
|
@@ -16,8 +16,10 @@ struct VideoPlayerView: View {
|
||||
@State private var playerSize: CGSize = .zero
|
||||
@State private var fullScreen = false
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
#endif
|
||||
@@ -82,11 +84,11 @@ struct VideoPlayerView: View {
|
||||
fullScreen = true
|
||||
}
|
||||
},
|
||||
down: { dismiss() }
|
||||
down: { presentationMode.wrappedValue.dismiss() }
|
||||
)
|
||||
#endif
|
||||
|
||||
.background(.black)
|
||||
.background(Color.black)
|
||||
|
||||
Group {
|
||||
#if os(iOS)
|
||||
@@ -98,7 +100,7 @@ struct VideoPlayerView: View {
|
||||
VideoDetails(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreen)
|
||||
#endif
|
||||
}
|
||||
.background()
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
.modifier(VideoDetailsPaddingModifier(geometry: geometry, aspectRatio: player.controller?.aspectRatio, fullScreen: fullScreen))
|
||||
}
|
||||
#endif
|
||||
|
@@ -7,7 +7,7 @@ struct AddToPlaylistView: View {
|
||||
|
||||
@State private var selectedPlaylistID: Playlist.ID = ""
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@EnvironmentObject<PlaylistsModel> private var model
|
||||
|
||||
var body: some View {
|
||||
@@ -37,7 +37,7 @@ struct AddToPlaylistView: View {
|
||||
.padding(.vertical)
|
||||
#elseif os(tvOS)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.background(.thickMaterial)
|
||||
.background(Color.tertiaryBackground)
|
||||
#else
|
||||
.padding(.vertical)
|
||||
#endif
|
||||
@@ -70,7 +70,7 @@ struct AddToPlaylistView: View {
|
||||
|
||||
#if !os(tvOS)
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
@@ -155,7 +155,7 @@ struct AddToPlaylistView: View {
|
||||
Defaults[.lastUsedPlaylistID] = id
|
||||
|
||||
model.addVideo(playlistID: id, videoID: video.videoID) {
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,9 +10,7 @@ struct PlaylistFormView: View {
|
||||
@State private var valid = false
|
||||
@State private var showingDeleteConfirmation = false
|
||||
|
||||
@FocusState private var focused: Bool
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
@@ -22,77 +20,68 @@ struct PlaylistFormView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS) || os(iOS)
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .center) {
|
||||
Text(editing ? "Edit Playlist" : "Create Playlist")
|
||||
.font(.title2.bold())
|
||||
Group {
|
||||
#if os(macOS) || os(iOS)
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .center) {
|
||||
Text(editing ? "Edit Playlist" : "Create Playlist")
|
||||
.font(.title2.bold())
|
||||
|
||||
Spacer()
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}.keyboardShortcut(.cancelAction)
|
||||
Button("Cancel") {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}.keyboardShortcut(.cancelAction)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
Form {
|
||||
TextField("Name", text: $name, onCommit: validate)
|
||||
.frame(maxWidth: 450)
|
||||
.padding(.leading, 10)
|
||||
|
||||
visibilityFormItem
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
#if os(macOS)
|
||||
.padding(.horizontal)
|
||||
#endif
|
||||
|
||||
HStack {
|
||||
if editing {
|
||||
deletePlaylistButton
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Save", action: submitForm)
|
||||
.disabled(!valid)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
.frame(minHeight: 35)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
Form {
|
||||
TextField("Name", text: $name, onCommit: validate)
|
||||
.frame(maxWidth: 450)
|
||||
.padding(.leading, 10)
|
||||
.focused($focused)
|
||||
|
||||
visibilityFormItem
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
#if os(macOS)
|
||||
.padding(.horizontal)
|
||||
#if os(iOS)
|
||||
.padding(.vertical)
|
||||
#else
|
||||
.frame(width: 400, height: 150)
|
||||
#endif
|
||||
|
||||
HStack {
|
||||
if editing {
|
||||
deletePlaylistButton
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Save", action: submitForm)
|
||||
.disabled(!valid)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
.frame(minHeight: 35)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.onChange(of: name) { _ in validate() }
|
||||
.onAppear(perform: initializeForm)
|
||||
#if os(iOS)
|
||||
.padding(.vertical)
|
||||
#else
|
||||
.frame(width: 400, height: 150)
|
||||
VStack {
|
||||
Group {
|
||||
header
|
||||
form
|
||||
}
|
||||
.frame(maxWidth: 1000)
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.background(Color.tertiaryBackground)
|
||||
#endif
|
||||
|
||||
#else
|
||||
VStack {
|
||||
Group {
|
||||
header
|
||||
form
|
||||
}
|
||||
.frame(maxWidth: 1000)
|
||||
}
|
||||
.onAppear {
|
||||
guard editing else {
|
||||
return
|
||||
}
|
||||
|
||||
self.name = self.playlist.title
|
||||
self.visibility = self.playlist.visibility
|
||||
|
||||
validate()
|
||||
}
|
||||
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.background(.thickMaterial)
|
||||
#endif
|
||||
}
|
||||
.onChange(of: name) { _ in validate() }
|
||||
.onAppear(perform: initializeForm)
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
@@ -152,16 +141,16 @@ struct PlaylistFormView: View {
|
||||
#endif
|
||||
|
||||
func initializeForm() {
|
||||
focused = true
|
||||
|
||||
guard editing else {
|
||||
return
|
||||
}
|
||||
|
||||
name = playlist.title
|
||||
visibility = playlist.visibility
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
name = playlist.title
|
||||
visibility = playlist.visibility
|
||||
|
||||
validate()
|
||||
validate()
|
||||
}
|
||||
}
|
||||
|
||||
func validate() {
|
||||
@@ -182,7 +171,7 @@ struct PlaylistFormView: View {
|
||||
|
||||
playlists.load(force: true)
|
||||
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +183,7 @@ struct PlaylistFormView: View {
|
||||
#if os(macOS)
|
||||
Picker("Visibility", selection: $visibility) {
|
||||
ForEach(Playlist.Visibility.allCases) { visibility in
|
||||
Text(visibility.name)
|
||||
Text(visibility.name).tag(visibility)
|
||||
}
|
||||
}
|
||||
#else
|
||||
@@ -216,9 +205,10 @@ struct PlaylistFormView: View {
|
||||
}
|
||||
|
||||
var deletePlaylistButton: some View {
|
||||
Button("Delete", role: .destructive) {
|
||||
Button("Delete") {
|
||||
showingDeleteConfirmation = true
|
||||
}.alert(isPresented: $showingDeleteConfirmation) {
|
||||
}
|
||||
.alert(isPresented: $showingDeleteConfirmation) {
|
||||
Alert(
|
||||
title: Text("Are you sure you want to delete playlist?"),
|
||||
message: Text("Playlist \"\(playlist.title)\" will be deleted.\nIt cannot be undone."),
|
||||
@@ -226,16 +216,14 @@ struct PlaylistFormView: View {
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
#if os(macOS)
|
||||
.foregroundColor(.red)
|
||||
#endif
|
||||
}
|
||||
|
||||
func deletePlaylistAndDismiss() {
|
||||
accounts.api.playlist(playlist.id)?.request(.delete).onSuccess { _ in
|
||||
playlist = nil
|
||||
playlists.load(force: true)
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -71,17 +71,20 @@ struct PlaylistsView: View {
|
||||
ToolbarItemGroup {
|
||||
#if !os(iOS)
|
||||
if !model.isEmpty {
|
||||
selectPlaylistButton
|
||||
.prefersDefaultFocus(in: focusNamespace)
|
||||
if #available(macOS 12.0, *) {
|
||||
selectPlaylistButton
|
||||
.prefersDefaultFocus(in: focusNamespace)
|
||||
} else {
|
||||
selectPlaylistButton
|
||||
}
|
||||
}
|
||||
|
||||
if currentPlaylist != nil {
|
||||
editPlaylistButton
|
||||
}
|
||||
#endif
|
||||
FavoriteButton(item: FavoriteItem(section: .playlist(selectedPlaylistID)))
|
||||
|
||||
newPlaylistButton
|
||||
FavoriteButton(item: FavoriteItem(section: .playlist(selectedPlaylistID)))
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
@@ -99,6 +102,8 @@ struct PlaylistsView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
newPlaylistButton
|
||||
|
||||
if currentPlaylist != nil {
|
||||
editPlaylistButton
|
||||
}
|
||||
@@ -168,7 +173,7 @@ struct PlaylistsView: View {
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
#if os(macOS)
|
||||
.background()
|
||||
.background(Color.tertiaryBackground)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
80
Shared/Search/SearchField.swift
Normal file
80
Shared/Search/SearchField.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SearchTextField: View {
|
||||
@Environment(\.navigationStyle) private var navigationStyle
|
||||
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SearchModel> private var state
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
#if os(macOS)
|
||||
fieldBorder
|
||||
#endif
|
||||
|
||||
HStack(spacing: 0) {
|
||||
#if os(macOS)
|
||||
Image(systemName: "magnifyingglass")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 12, height: 12)
|
||||
.padding(.horizontal, 8)
|
||||
.opacity(0.8)
|
||||
#endif
|
||||
TextField("Search...", text: $state.queryText) {
|
||||
state.changeQuery { query in query.query = state.queryText }
|
||||
recents.addQuery(state.queryText)
|
||||
}
|
||||
.onChange(of: state.queryText) { _ in
|
||||
if state.query.query.compare(state.queryText, options: .caseInsensitive) == .orderedSame {
|
||||
state.fieldIsFocused = true
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.textFieldStyle(.plain)
|
||||
#else
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.leading)
|
||||
.padding(.trailing, 15)
|
||||
#endif
|
||||
if !self.state.queryText.isEmpty {
|
||||
clearButton
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, navigationStyle == .tab ? 10 : 0)
|
||||
}
|
||||
|
||||
private var fieldBorder: some View {
|
||||
RoundedRectangle(cornerRadius: 5, style: .continuous)
|
||||
.fill(Color.background)
|
||||
.frame(width: 250, height: 32)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 5, style: .continuous)
|
||||
.stroke(
|
||||
state.fieldIsFocused ? Color.blue.opacity(0.7) : Color.gray.opacity(0.4),
|
||||
lineWidth: state.fieldIsFocused ? 3 : 1
|
||||
)
|
||||
.frame(width: 250, height: 31)
|
||||
)
|
||||
}
|
||||
|
||||
private var clearButton: some View {
|
||||
Button(action: {
|
||||
self.state.queryText = ""
|
||||
}) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
#if os(macOS)
|
||||
.frame(width: 14, height: 14)
|
||||
#else
|
||||
.frame(width: 18, height: 18)
|
||||
#endif
|
||||
.padding(.trailing, 3)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.padding(.trailing, 10)
|
||||
.opacity(0.7)
|
||||
}
|
||||
}
|
77
Shared/Search/SearchSuggestions.swift
Normal file
77
Shared/Search/SearchSuggestions.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SearchSuggestions: View {
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SearchModel> private var state
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Button {
|
||||
state.changeQuery { query in
|
||||
query.query = state.queryText
|
||||
state.fieldIsFocused = false
|
||||
}
|
||||
|
||||
recents.addQuery(state.queryText)
|
||||
} label: {
|
||||
HStack(spacing: 5) {
|
||||
Label(state.queryText, systemImage: "magnifyingglass")
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.onHover(perform: onHover(_:))
|
||||
#endif
|
||||
|
||||
ForEach(visibleSuggestions, id: \.self) { suggestion in
|
||||
Button {
|
||||
state.queryText = suggestion
|
||||
} label: {
|
||||
HStack(spacing: 0) {
|
||||
Label(state.queryText, systemImage: "arrow.up.left.circle")
|
||||
.lineLimit(1)
|
||||
.layoutPriority(2)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(querySuffix(suggestion))
|
||||
.lineLimit(1)
|
||||
.layoutPriority(1)
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.onHover(perform: onHover(_:))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.buttonStyle(.link)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var visibleSuggestions: [String] {
|
||||
state.querySuggestions.collection.filter {
|
||||
$0.compare(state.queryText, options: .caseInsensitive) != .orderedSame
|
||||
}
|
||||
}
|
||||
|
||||
private func querySuffix(_ suggestion: String) -> String {
|
||||
suggestion.replacingFirstOccurrence(of: state.queryText.lowercased(), with: "")
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private func onHover(_ inside: Bool) {
|
||||
if inside {
|
||||
NSCursor.pointingHand.push()
|
||||
} else {
|
||||
NSCursor.pop()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
struct SearchSuggestions_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SearchSuggestions()
|
||||
.injectFixtureEnvironmentObjects()
|
||||
}
|
||||
}
|
@@ -9,7 +9,6 @@ struct SearchView: View {
|
||||
@State private var searchDate = SearchQuery.Date.any
|
||||
@State private var searchDuration = SearchQuery.Duration.any
|
||||
|
||||
@State private var presentingClearConfirmation = false
|
||||
@State private var recentsChanged = false
|
||||
|
||||
#if os(tvOS)
|
||||
@@ -31,50 +30,37 @@ struct SearchView: View {
|
||||
state.store.collection.sorted { $0 < $1 }
|
||||
}
|
||||
|
||||
init(_ query: SearchQuery? = nil, videos: [Video] = [Video]()) {
|
||||
init(_ query: SearchQuery? = nil, videos: [Video] = []) {
|
||||
self.query = query
|
||||
self.videos = videos
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
PlayerControlsView {
|
||||
VStack {
|
||||
if showRecentQueries {
|
||||
recentQueries
|
||||
} else {
|
||||
#if os(tvOS)
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
HStack(spacing: 0) {
|
||||
if accounts.app.supportsSearchFilters {
|
||||
filtersHorizontalStack
|
||||
}
|
||||
#if os(iOS)
|
||||
VStack {
|
||||
SearchTextField()
|
||||
|
||||
if let favoriteItem = favoriteItem {
|
||||
FavoriteButton(item: favoriteItem)
|
||||
.id(favoriteItem.id)
|
||||
.labelStyle(.iconOnly)
|
||||
.font(.system(size: 25))
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalCells(items: items)
|
||||
}
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
#else
|
||||
VerticalCells(items: items)
|
||||
#endif
|
||||
|
||||
if noResults {
|
||||
Text("No results")
|
||||
|
||||
if searchFiltersActive {
|
||||
Button("Reset search filters", action: resetFilters)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty {
|
||||
SearchSuggestions()
|
||||
} else {
|
||||
results
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
ZStack {
|
||||
results
|
||||
|
||||
if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty {
|
||||
HStack {
|
||||
Spacer()
|
||||
SearchSuggestions()
|
||||
.borderLeading(width: 1, color: Color("ControlsBorderColor"))
|
||||
.frame(maxWidth: 280)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.toolbar {
|
||||
#if !os(tvOS)
|
||||
@@ -118,6 +104,10 @@ struct SearchView: View {
|
||||
if accounts.app.supportsSearchFilters {
|
||||
filtersMenu
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
SearchTextField()
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -132,10 +122,11 @@ struct SearchView: View {
|
||||
state.store.replace(ContentItem.array(of: videos))
|
||||
}
|
||||
}
|
||||
.searchable(text: $state.queryText, placement: searchFieldPlacement) {
|
||||
ForEach(state.querySuggestions.collection, id: \.self) { suggestion in
|
||||
Text(suggestion)
|
||||
.searchCompletion(suggestion)
|
||||
.onChange(of: state.query.query) { newQuery in
|
||||
if newQuery.isEmpty {
|
||||
favoriteItem = nil
|
||||
} else {
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
.onChange(of: state.queryText) { newQuery in
|
||||
@@ -161,11 +152,7 @@ struct SearchView: View {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onSubmit(of: .search) {
|
||||
state.changeQuery { query in query.query = state.queryText }
|
||||
recents.addQuery(state.queryText)
|
||||
updateFavoriteItem()
|
||||
}
|
||||
|
||||
.onChange(of: searchSortOrder) { order in
|
||||
state.changeQuery { query in
|
||||
query.sortBy = order
|
||||
@@ -185,19 +172,55 @@ struct SearchView: View {
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
.navigationTitle("Search")
|
||||
#endif
|
||||
}
|
||||
|
||||
var searchFieldPlacement: SearchFieldPlacement {
|
||||
#if os(iOS)
|
||||
.navigationBarDrawer(displayMode: .always)
|
||||
#else
|
||||
.automatic
|
||||
.navigationBarHidden(true)
|
||||
#endif
|
||||
}
|
||||
|
||||
var toolbarPlacement: ToolbarItemPlacement {
|
||||
private var results: some View {
|
||||
VStack {
|
||||
if showRecentQueries {
|
||||
recentQueries
|
||||
} else {
|
||||
#if os(tvOS)
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
HStack(spacing: 0) {
|
||||
if accounts.app.supportsSearchFilters {
|
||||
filtersHorizontalStack
|
||||
}
|
||||
|
||||
if let favoriteItem = favoriteItem {
|
||||
FavoriteButton(item: favoriteItem)
|
||||
.id(favoriteItem.id)
|
||||
.labelStyle(.iconOnly)
|
||||
.font(.system(size: 25))
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalCells(items: items)
|
||||
}
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
#else
|
||||
VerticalCells(items: items)
|
||||
#endif
|
||||
|
||||
if noResults {
|
||||
Text("No results")
|
||||
|
||||
if searchFiltersActive {
|
||||
Button("Reset search filters", action: resetFilters)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var toolbarPlacement: ToolbarItemPlacement {
|
||||
#if os(iOS)
|
||||
.bottomBar
|
||||
#else
|
||||
@@ -205,25 +228,25 @@ struct SearchView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
fileprivate var showRecentQueries: Bool {
|
||||
private var showRecentQueries: Bool {
|
||||
navigationStyle == .tab && state.queryText.isEmpty
|
||||
}
|
||||
|
||||
fileprivate var filtersActive: Bool {
|
||||
private var filtersActive: Bool {
|
||||
searchDuration != .any || searchDate != .any
|
||||
}
|
||||
|
||||
fileprivate func resetFilters() {
|
||||
private func resetFilters() {
|
||||
searchSortOrder = .relevance
|
||||
searchDate = .any
|
||||
searchDuration = .any
|
||||
}
|
||||
|
||||
fileprivate var noResults: Bool {
|
||||
private var noResults: Bool {
|
||||
items.isEmpty && !state.isLoading && !state.query.isEmpty
|
||||
}
|
||||
|
||||
var recentQueries: some View {
|
||||
private var recentQueries: some View {
|
||||
VStack {
|
||||
List {
|
||||
Section(header: Text("Recents")) {
|
||||
@@ -237,22 +260,13 @@ struct SearchView: View {
|
||||
state.changeQuery { query in query.query = item.title }
|
||||
updateFavoriteItem()
|
||||
}
|
||||
#if os(iOS)
|
||||
.swipeActions(edge: .trailing) {
|
||||
deleteButton(item)
|
||||
}
|
||||
#elseif os(tvOS)
|
||||
.contextMenu {
|
||||
deleteButton(item)
|
||||
deleteAllButton
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.redrawOn(change: recentsChanged)
|
||||
|
||||
if !recentItems.isEmpty {
|
||||
clearAllButton
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
@@ -260,37 +274,33 @@ struct SearchView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
func deleteButton(_ item: RecentItem) -> some View {
|
||||
Button(role: .destructive) {
|
||||
recents.close(item)
|
||||
recentsChanged.toggle()
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var clearAllButton: some View {
|
||||
Button("Clear All", role: .destructive) {
|
||||
presentingClearConfirmation = true
|
||||
}
|
||||
.confirmationDialog("Clear All", isPresented: $presentingClearConfirmation) {
|
||||
Button("Clear All", role: .destructive) {
|
||||
recents.clearQueries()
|
||||
}
|
||||
private func deleteButton(_ item: RecentItem) -> some View {
|
||||
Button {
|
||||
recents.close(item)
|
||||
recentsChanged.toggle()
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
var searchFiltersActive: Bool {
|
||||
private var deleteAllButton: some View {
|
||||
Button {
|
||||
recents.clearQueries()
|
||||
recentsChanged.toggle()
|
||||
} label: {
|
||||
Label("Delete All", systemImage: "trash.fill")
|
||||
}
|
||||
}
|
||||
|
||||
private var searchFiltersActive: Bool {
|
||||
searchDate != .any || searchDuration != .any
|
||||
}
|
||||
|
||||
var recentItems: [RecentItem] {
|
||||
private var recentItems: [RecentItem] {
|
||||
Defaults[.recentlyOpened].filter { $0.type == .query }.reversed()
|
||||
}
|
||||
|
||||
var searchSortOrderPicker: some View {
|
||||
private var searchSortOrderPicker: some View {
|
||||
Picker("Sort", selection: $searchSortOrder) {
|
||||
ForEach(SearchQuery.SortOrder.allCases) { sortOrder in
|
||||
Text(sortOrder.name).tag(sortOrder)
|
||||
@@ -299,7 +309,7 @@ struct SearchView: View {
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
var searchSortOrderButton: some View {
|
||||
private var searchSortOrderButton: some View {
|
||||
Button(action: { self.searchSortOrder = self.searchSortOrder.next() }) { Text(self.searchSortOrder.name)
|
||||
.font(.system(size: 30))
|
||||
.padding(.horizontal)
|
||||
@@ -315,7 +325,7 @@ struct SearchView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var searchDateButton: some View {
|
||||
private var searchDateButton: some View {
|
||||
Button(action: { self.searchDate = self.searchDate.next() }) {
|
||||
Text(self.searchDate.name)
|
||||
.font(.system(size: 30))
|
||||
@@ -332,7 +342,7 @@ struct SearchView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var searchDurationButton: some View {
|
||||
private var searchDurationButton: some View {
|
||||
Button(action: { self.searchDuration = self.searchDuration.next() }) {
|
||||
Text(self.searchDuration.name)
|
||||
.font(.system(size: 30))
|
||||
@@ -349,7 +359,7 @@ struct SearchView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var filtersHorizontalStack: some View {
|
||||
private var filtersHorizontalStack: some View {
|
||||
HStack {
|
||||
HStack(spacing: 30) {
|
||||
Text("Sort")
|
||||
@@ -375,7 +385,7 @@ struct SearchView: View {
|
||||
.font(.system(size: 30))
|
||||
}
|
||||
#else
|
||||
var filtersMenu: some View {
|
||||
private var filtersMenu: some View {
|
||||
Menu(filtersActive ? "Filter: active" : "Filter") {
|
||||
Picker(selection: $searchDuration, label: Text("Duration")) {
|
||||
ForEach(SearchQuery.Duration.allCases) { duration in
|
@@ -15,9 +15,7 @@ struct AccountForm: View {
|
||||
@State private var validationError: String?
|
||||
@State private var validationDebounce = Debounce()
|
||||
|
||||
@FocusState private var focused: Bool
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@@ -32,7 +30,7 @@ struct AccountForm: View {
|
||||
.padding(.vertical)
|
||||
#elseif os(tvOS)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.background(.thickMaterial)
|
||||
.background(Color.tertiaryBackground)
|
||||
#else
|
||||
.frame(width: 400, height: 145)
|
||||
#endif
|
||||
@@ -46,7 +44,7 @@ struct AccountForm: View {
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
@@ -68,7 +66,6 @@ struct AccountForm: View {
|
||||
formFields
|
||||
#endif
|
||||
}
|
||||
.onAppear(perform: initializeForm)
|
||||
.onChange(of: username) { _ in validate() }
|
||||
.onChange(of: password) { _ in validate() }
|
||||
}
|
||||
@@ -76,24 +73,23 @@ struct AccountForm: View {
|
||||
var formFields: some View {
|
||||
Group {
|
||||
if !instance.app.accountsUsePassword {
|
||||
TextField("Name", text: $name, prompt: Text("Account Name (optional)"))
|
||||
.focused($focused)
|
||||
TextField("Name", text: $name)
|
||||
}
|
||||
|
||||
TextField("Username", text: $username, prompt: usernamePrompt)
|
||||
TextField(usernamePrompt, text: $username)
|
||||
|
||||
if instance.app.accountsUsePassword {
|
||||
SecureField("Password", text: $password, prompt: Text("Password"))
|
||||
SecureField("Password", text: $password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var usernamePrompt: Text {
|
||||
var usernamePrompt: String {
|
||||
switch instance.app {
|
||||
case .invidious:
|
||||
return Text("SID Cookie")
|
||||
return "SID Cookie"
|
||||
default:
|
||||
return Text("Username")
|
||||
return "Username"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,10 +117,6 @@ struct AccountForm: View {
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
private func initializeForm() {
|
||||
focused = true
|
||||
}
|
||||
|
||||
private func validate() {
|
||||
isValid = false
|
||||
validationDebounce.invalidate()
|
||||
@@ -151,7 +143,7 @@ struct AccountForm: View {
|
||||
let account = AccountsModel.add(instance: instance, name: name, username: username, password: password)
|
||||
selectedAccount?.wrappedValue = account
|
||||
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
|
||||
private var validator: AccountValidator {
|
||||
|
31
Shared/Settings/AccountsNavigationLink.swift
Normal file
31
Shared/Settings/AccountsNavigationLink.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AccountsNavigationLink: View {
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
var instance: Instance
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(instance.longDescription) {
|
||||
InstanceSettings(instanceID: instance.id)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contextMenu {
|
||||
removeInstanceButton(instance)
|
||||
}
|
||||
}
|
||||
|
||||
private func removeInstanceButton(_ instance: Instance) -> some View {
|
||||
if #available(iOS 15.0, *) {
|
||||
return Button("Remove", role: .destructive) { removeAction(instance) }
|
||||
} else {
|
||||
return Button("Remove") { removeAction(instance) }
|
||||
}
|
||||
}
|
||||
|
||||
private func removeAction(_ instance: Instance) {
|
||||
if accounts.current?.instance == instance {
|
||||
accounts.setCurrent(nil)
|
||||
}
|
||||
InstancesModel.remove(instance)
|
||||
}
|
||||
}
|
@@ -1,102 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AccountsSettings: View {
|
||||
let instanceID: Instance.ID?
|
||||
|
||||
@State private var accountsChanged = false
|
||||
@State private var presentingAccountForm = false
|
||||
|
||||
@State private var frontendURL = ""
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var model
|
||||
@EnvironmentObject<InstancesModel> private var instances
|
||||
|
||||
var instance: Instance! {
|
||||
InstancesModel.find(instanceID)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if instance.app.hasFrontendURL {
|
||||
Section(header: Text("Frontend URL")) {
|
||||
TextField(
|
||||
"Frontend URL",
|
||||
text: $frontendURL,
|
||||
prompt: Text("To enable videos, channels and playlists sharing")
|
||||
)
|
||||
.onAppear {
|
||||
frontendURL = instance.frontendURL ?? ""
|
||||
}
|
||||
.onChange(of: frontendURL) { newValue in
|
||||
InstancesModel.setFrontendURL(instance, newValue)
|
||||
}
|
||||
.labelsHidden()
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Accounts"), footer: sectionFooter) {
|
||||
if instance.app.supportsAccounts {
|
||||
accounts
|
||||
} else {
|
||||
Text("Accounts are not supported for the application of this instance")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.frame(maxWidth: 1000)
|
||||
#endif
|
||||
|
||||
.navigationTitle(instance.description)
|
||||
}
|
||||
|
||||
var accounts: some View {
|
||||
Group {
|
||||
ForEach(InstancesModel.accounts(instanceID), id: \.self) { account in
|
||||
#if os(tvOS)
|
||||
Button(account.description) {}
|
||||
.contextMenu {
|
||||
Button("Remove", role: .destructive) { removeAccount(account) }
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
#else
|
||||
Text(account.description)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button("Remove", role: .destructive) { removeAccount(account) }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.redrawOn(change: accountsChanged)
|
||||
|
||||
Button("Add account...") {
|
||||
presentingAccountForm = true
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $presentingAccountForm, onDismiss: { accountsChanged.toggle() }) {
|
||||
AccountForm(instance: instance)
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.listStyle(.insetGrouped)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var sectionFooter: some View {
|
||||
if !instance.app.supportsAccounts {
|
||||
return Text("")
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
return Text("Swipe to remove account")
|
||||
#else
|
||||
return Text("Tap and hold to remove account")
|
||||
.foregroundColor(.secondary)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func removeAccount(_ account: Account) {
|
||||
AccountsModel.remove(account)
|
||||
accountsChanged.toggle()
|
||||
}
|
||||
}
|
@@ -13,9 +13,7 @@ struct InstanceForm: View {
|
||||
@State private var validationError: String?
|
||||
@State private var validationDebounce = Debounce()
|
||||
|
||||
@FocusState private var nameFieldFocused: Bool
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
@@ -30,12 +28,11 @@ struct InstanceForm: View {
|
||||
}
|
||||
.onChange(of: app) { _ in validate() }
|
||||
.onChange(of: url) { _ in validate() }
|
||||
.onAppear(perform: initializeForm)
|
||||
#if os(iOS)
|
||||
.padding(.vertical)
|
||||
#elseif os(tvOS)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.background(.thickMaterial)
|
||||
.background(Color.tertiaryBackground)
|
||||
#else
|
||||
.frame(width: 400, height: 190)
|
||||
#endif
|
||||
@@ -49,7 +46,7 @@ struct InstanceForm: View {
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
@@ -80,10 +77,9 @@ struct InstanceForm: View {
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
TextField("Name", text: $name, prompt: Text("Instance Name (optional)"))
|
||||
.focused($nameFieldFocused)
|
||||
TextField("Name", text: $name)
|
||||
|
||||
TextField("API URL", text: $url, prompt: Text("https://invidious.home.net"))
|
||||
TextField("API URL", text: $url)
|
||||
|
||||
#if !os(macOS)
|
||||
.autocapitalization(.none)
|
||||
@@ -138,10 +134,6 @@ struct InstanceForm: View {
|
||||
}
|
||||
}
|
||||
|
||||
func initializeForm() {
|
||||
nameFieldFocused = true
|
||||
}
|
||||
|
||||
func submitForm() {
|
||||
guard isValid else {
|
||||
return
|
||||
@@ -149,7 +141,7 @@ struct InstanceForm: View {
|
||||
|
||||
savedInstanceID = InstancesModel.add(app: app, name: name, url: url).id
|
||||
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
104
Shared/Settings/InstanceSettings.swift
Normal file
104
Shared/Settings/InstanceSettings.swift
Normal file
@@ -0,0 +1,104 @@
|
||||
import SwiftUI
|
||||
|
||||
struct InstanceSettings: View {
|
||||
let instanceID: Instance.ID?
|
||||
|
||||
@State private var accountsChanged = false
|
||||
@State private var presentingAccountForm = false
|
||||
|
||||
@State private var frontendURL = ""
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var model
|
||||
@EnvironmentObject<InstancesModel> private var instances
|
||||
|
||||
var instance: Instance! {
|
||||
InstancesModel.find(instanceID)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if instance.app.hasFrontendURL {
|
||||
Section(header: Text("Frontend URL")) {
|
||||
TextField(
|
||||
"Frontend URL",
|
||||
text: $frontendURL
|
||||
)
|
||||
.onAppear {
|
||||
frontendURL = instance.frontendURL ?? ""
|
||||
}
|
||||
.onChange(of: frontendURL) { newValue in
|
||||
InstancesModel.setFrontendURL(instance, newValue)
|
||||
}
|
||||
.labelsHidden()
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Accounts"), footer: sectionFooter) {
|
||||
if instance.app.supportsAccounts {
|
||||
ForEach(InstancesModel.accounts(instanceID), id: \.self) { account in
|
||||
#if os(tvOS)
|
||||
Button(account.description) {}
|
||||
.contextMenu {
|
||||
Button("Remove") { removeAccount(account) }
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
#else
|
||||
ZStack {
|
||||
NavigationLink(destination: EmptyView()) {
|
||||
EmptyView()
|
||||
}
|
||||
.disabled(true)
|
||||
.hidden()
|
||||
|
||||
HStack {
|
||||
Text(account.description)
|
||||
Spacer()
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Remove") { removeAccount(account) }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.redrawOn(change: accountsChanged)
|
||||
|
||||
Button("Add account...") {
|
||||
presentingAccountForm = true
|
||||
}
|
||||
.sheet(isPresented: $presentingAccountForm, onDismiss: { accountsChanged.toggle() }) {
|
||||
AccountForm(instance: instance)
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.listStyle(.insetGrouped)
|
||||
#endif
|
||||
} else {
|
||||
Text("Accounts are not supported for the application of this instance")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.frame(maxWidth: 1000)
|
||||
#elseif os(iOS)
|
||||
.listStyle(.insetGrouped)
|
||||
#endif
|
||||
|
||||
.navigationTitle(instance.description)
|
||||
}
|
||||
|
||||
private var sectionFooter: some View {
|
||||
if !instance.app.supportsAccounts {
|
||||
return Text("")
|
||||
}
|
||||
|
||||
return Text("Tap and hold to remove account")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
private func removeAccount(_ account: Account) {
|
||||
AccountsModel.remove(account)
|
||||
accountsChanged.toggle()
|
||||
}
|
||||
}
|
@@ -1,70 +0,0 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct InstancesSettings: View {
|
||||
@Default(.instances) private var instances
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
|
||||
@State private var selectedInstanceID: Instance.ID?
|
||||
@State private var selectedAccount: Account?
|
||||
|
||||
@State private var presentingInstanceForm = false
|
||||
@State private var savedFormInstanceID: Instance.ID?
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
Section(header: SettingsHeader(text: "Instances")) {
|
||||
ForEach(instances) { instance in
|
||||
Group {
|
||||
NavigationLink(instance.longDescription) {
|
||||
AccountsSettings(instanceID: instance.id)
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
removeInstanceButton(instance)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
#else
|
||||
.contextMenu {
|
||||
removeInstanceButton(instance)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
addInstanceButton
|
||||
}
|
||||
#if os(iOS)
|
||||
.listStyle(.insetGrouped)
|
||||
#endif
|
||||
}
|
||||
.sheet(isPresented: $presentingInstanceForm) {
|
||||
InstanceForm(savedInstanceID: $savedFormInstanceID)
|
||||
}
|
||||
}
|
||||
|
||||
private var addInstanceButton: some View {
|
||||
Button("Add Instance...") {
|
||||
presentingInstanceForm = true
|
||||
}
|
||||
}
|
||||
|
||||
private func removeInstanceButton(_ instance: Instance) -> some View {
|
||||
Button("Remove", role: .destructive) {
|
||||
if accounts.current?.instance == instance {
|
||||
accounts.setCurrent(nil)
|
||||
}
|
||||
InstancesModel.remove(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InstancesSettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
InstancesSettings()
|
||||
}
|
||||
.frame(width: 400, height: 270)
|
||||
}
|
||||
}
|
@@ -9,8 +9,7 @@ struct ServicesSettings: View {
|
||||
Section(header: SettingsHeader(text: "SponsorBlock API")) {
|
||||
TextField(
|
||||
"SponsorBlock API Instance",
|
||||
text: $sponsorBlockInstance,
|
||||
prompt: Text("SponsorBlock API URL, leave blank to disable")
|
||||
text: $sponsorBlockInstance
|
||||
)
|
||||
.labelsHidden()
|
||||
#if !os(macOS)
|
||||
@@ -21,7 +20,7 @@ struct ServicesSettings: View {
|
||||
|
||||
Section(header: SettingsHeader(text: "Categories to Skip")) {
|
||||
#if os(macOS)
|
||||
List(SponsorBlockAPI.categories, id: \.self) { category in
|
||||
let list = List(SponsorBlockAPI.categories, id: \.self) { category in
|
||||
SponsorBlockCategorySelectionRow(
|
||||
title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown",
|
||||
selected: sponsorBlockCategories.contains(category)
|
||||
@@ -29,7 +28,16 @@ struct ServicesSettings: View {
|
||||
toggleCategory(category, value: value)
|
||||
}
|
||||
}
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
|
||||
Group {
|
||||
if #available(macOS 12.0, *) {
|
||||
list
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
} else {
|
||||
list
|
||||
.listStyle(.inset)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
#else
|
||||
ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
||||
|
@@ -10,11 +10,16 @@ struct SettingsView: View {
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
#endif
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
|
||||
@State private var presentingInstanceForm = false
|
||||
@State private var savedFormInstanceID: Instance.ID?
|
||||
|
||||
@Default(.instances) private var instances
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
TabView {
|
||||
@@ -65,8 +70,14 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
InstancesSettings()
|
||||
.environmentObject(accounts)
|
||||
|
||||
Section(header: Text("Instances")) {
|
||||
ForEach(instances) { instance in
|
||||
AccountsNavigationLink(instance: instance)
|
||||
}
|
||||
addInstanceButton
|
||||
}
|
||||
|
||||
BrowsingSettings()
|
||||
PlaybackSettings()
|
||||
ServicesSettings()
|
||||
@@ -76,7 +87,7 @@ struct SettingsView: View {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
#if !os(tvOS)
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
@@ -87,11 +98,20 @@ struct SettingsView: View {
|
||||
.listStyle(.insetGrouped)
|
||||
#endif
|
||||
}
|
||||
.sheet(isPresented: $presentingInstanceForm) {
|
||||
InstanceForm(savedInstanceID: $savedFormInstanceID)
|
||||
}
|
||||
#if os(tvOS)
|
||||
.background(.black)
|
||||
.background(Color.black)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
private var addInstanceButton: some View {
|
||||
Button("Add Instance...") {
|
||||
presentingInstanceForm = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
|
@@ -9,51 +9,34 @@ struct TrendingCountry: View {
|
||||
@State private var query: String = ""
|
||||
@State private var selection: Country?
|
||||
|
||||
@FocusState var countryIsFocused
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
#if os(macOS)
|
||||
#if !os(tvOS)
|
||||
HStack {
|
||||
TextField("Country", text: $query, prompt: Text(TrendingCountry.prompt))
|
||||
.focused($countryIsFocused)
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
TextField("Country", text: $query, prompt: Text(TrendingCountry.prompt))
|
||||
} else {
|
||||
TextField(TrendingCountry.prompt, text: $query)
|
||||
}
|
||||
|
||||
Button("Done") { selectCountryAndDismiss() }
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
}
|
||||
.padding([.horizontal, .top])
|
||||
|
||||
countriesList
|
||||
#else
|
||||
NavigationView {
|
||||
countriesList
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
Button("Done") { selectCountryAndDismiss() }
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.navigationBarTitle("Trending Country", displayMode: .automatic)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
countriesList
|
||||
}
|
||||
.onAppear {
|
||||
countryIsFocused = true
|
||||
}
|
||||
.onSubmit { selectCountryAndDismiss() }
|
||||
#if !os(macOS)
|
||||
.searchable(text: $query, placement: searchPlacement, prompt: Text(TrendingCountry.prompt))
|
||||
#endif
|
||||
#if os(tvOS)
|
||||
.background(.thinMaterial)
|
||||
.searchable(text: $query, placement: .automatic, prompt: Text(TrendingCountry.prompt))
|
||||
.background(Color.black)
|
||||
#endif
|
||||
}
|
||||
|
||||
var countriesList: some View {
|
||||
ScrollViewReader { _ in
|
||||
let list = ScrollViewReader { _ in
|
||||
List(store.collection, selection: $selection) { country in
|
||||
#if os(macOS)
|
||||
Text(country.name)
|
||||
@@ -71,29 +54,29 @@ struct TrendingCountry: View {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
.padding(.bottom, 5)
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
var searchPlacement: SearchFieldPlacement {
|
||||
#if os(iOS)
|
||||
.navigationBarDrawer(displayMode: .always)
|
||||
return Group {
|
||||
#if os(macOS)
|
||||
if #available(macOS 12.0, *) {
|
||||
list
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
} else {
|
||||
list
|
||||
}
|
||||
#else
|
||||
.automatic
|
||||
list
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.padding(.bottom, 5)
|
||||
#endif
|
||||
}
|
||||
|
||||
func selectCountryAndDismiss(_ country: Country? = nil) {
|
||||
if let selected = country ?? selection {
|
||||
selectedCountry = selected
|
||||
}
|
||||
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -172,11 +172,12 @@ struct TrendingView: View {
|
||||
}
|
||||
|
||||
#else
|
||||
Picker("Category", selection: $category) {
|
||||
Picker(category.controlLabel, selection: $category) {
|
||||
ForEach(TrendingCategory.allCases) { category in
|
||||
Text(category.controlLabel).tag(category)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@@ -19,7 +19,7 @@ struct VerticalCells: View {
|
||||
}
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
#if os(macOS)
|
||||
.background()
|
||||
.background(Color.tertiaryBackground)
|
||||
.frame(minWidth: 360)
|
||||
#endif
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ struct VideoCell: View {
|
||||
@Environment(\.horizontalCells) private var horizontalCells
|
||||
#endif
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<ThumbnailsModel> private var thumbnails
|
||||
|
||||
@@ -38,7 +39,13 @@ struct VideoCell: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contentShape(RoundedRectangle(cornerRadius: 12))
|
||||
.contextMenu { VideoContextMenuView(video: video, playerNavigationLinkActive: $player.playerNavigationLinkActive) }
|
||||
.contextMenu {
|
||||
VideoContextMenuView(
|
||||
video: video,
|
||||
playerNavigationLinkActive: $player.playerNavigationLinkActive
|
||||
)
|
||||
.environmentObject(accounts)
|
||||
}
|
||||
}
|
||||
|
||||
var content: some View {
|
||||
@@ -55,7 +62,7 @@ struct VideoCell: View {
|
||||
#endif
|
||||
}
|
||||
#if os(macOS)
|
||||
.background()
|
||||
.background(Color.tertiaryBackground)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@@ -83,7 +83,7 @@ struct ChannelPlaylistView: View {
|
||||
.navigationTitle(playlist.title)
|
||||
|
||||
#else
|
||||
.background(.thickMaterial)
|
||||
.background(Color.tertiaryBackground)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ struct ChannelVideosView: View {
|
||||
|
||||
@StateObject private var store = Store<Channel>()
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
|
||||
#if os(iOS)
|
||||
@@ -43,7 +43,7 @@ struct ChannelVideosView: View {
|
||||
}
|
||||
|
||||
var content: some View {
|
||||
VStack {
|
||||
let content = VStack {
|
||||
#if os(tvOS)
|
||||
HStack {
|
||||
Text(navigationTitle)
|
||||
@@ -65,40 +65,43 @@ struct ChannelVideosView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
#endif
|
||||
|
||||
VerticalCells(items: videos)
|
||||
|
||||
#if !os(iOS)
|
||||
.prefersDefaultFocus(in: focusNamespace)
|
||||
#if os(iOS)
|
||||
VerticalCells(items: videos)
|
||||
#else
|
||||
if #available(macOS 12.0, *) {
|
||||
VerticalCells(items: videos)
|
||||
.prefersDefaultFocus(in: focusNamespace)
|
||||
} else {
|
||||
VerticalCells(items: videos)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.environment(\.inChannelView, true)
|
||||
#if !os(iOS)
|
||||
.focusScope(focusNamespace)
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigation) {
|
||||
ShareButton(
|
||||
contentItem: contentItem,
|
||||
presentingShareSheet: $presentingShareSheet,
|
||||
shareURL: $shareURL
|
||||
)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigation) {
|
||||
ShareButton(
|
||||
contentItem: contentItem,
|
||||
presentingShareSheet: $presentingShareSheet,
|
||||
shareURL: $shareURL
|
||||
)
|
||||
}
|
||||
|
||||
ToolbarItem {
|
||||
HStack {
|
||||
Text("**\(store.item?.subscriptionsString ?? "loading")** subscribers")
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(store.item?.subscriptionsString != nil ? 1 : 0)
|
||||
ToolbarItem {
|
||||
HStack {
|
||||
Text("**\(store.item?.subscriptionsString ?? "loading")** subscribers")
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(store.item?.subscriptionsString != nil ? 1 : 0)
|
||||
|
||||
subscriptionToggleButton
|
||||
subscriptionToggleButton
|
||||
|
||||
FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name)))
|
||||
FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
.background(.thickMaterial)
|
||||
.background(Color.tertiaryBackground)
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.sheet(isPresented: $presentingShareSheet) {
|
||||
@@ -107,7 +110,6 @@ struct ChannelVideosView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.modifier(UnsubscribeAlertModifier())
|
||||
.onAppear {
|
||||
if store.item.isNil {
|
||||
resource.addObserver(store)
|
||||
@@ -115,6 +117,17 @@ struct ChannelVideosView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle(navigationTitle)
|
||||
|
||||
return Group {
|
||||
if #available(macOS 12.0, *) {
|
||||
content
|
||||
#if !os(iOS)
|
||||
.focusScope(focusNamespace)
|
||||
#endif
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var resource: Resource {
|
||||
|
@@ -26,8 +26,13 @@ struct DetailBadge: View {
|
||||
|
||||
struct DefaultStyleModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background(.thinMaterial)
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
content
|
||||
.background(.thinMaterial)
|
||||
} else {
|
||||
content
|
||||
.background(Color.background)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,15 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OpenSettingsButton: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
#if !os(macOS)
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
dismiss()
|
||||
let button = Button {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
|
||||
#if os(macOS)
|
||||
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
|
||||
@@ -19,7 +19,13 @@ struct OpenSettingsButton: View {
|
||||
} label: {
|
||||
Label("Open Settings", systemImage: "gearshape.2")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
button
|
||||
.buttonStyle(.borderedProminent)
|
||||
} else {
|
||||
button
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -25,7 +25,7 @@ struct PlayerControlsView<Content: View>: View {
|
||||
}
|
||||
|
||||
private var controls: some View {
|
||||
HStack {
|
||||
let controls = HStack {
|
||||
Button(action: {
|
||||
model.presentingPlayer.toggle()
|
||||
}) {
|
||||
@@ -92,14 +92,23 @@ struct PlayerControlsView<Content: View>: View {
|
||||
.padding(.horizontal)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 55)
|
||||
.padding(.vertical, 0)
|
||||
.background(.ultraThinMaterial)
|
||||
.borderTop(height: 0.4, color: Color("PlayerControlsBorderColor"))
|
||||
.borderBottom(height: navigationStyle == .sidebar ? 0 : 0.4, color: Color("PlayerControlsBorderColor"))
|
||||
.borderTop(height: 0.4, color: Color("ControlsBorderColor"))
|
||||
.borderBottom(height: navigationStyle == .sidebar ? 0 : 0.4, color: Color("ControlsBorderColor"))
|
||||
#if !os(tvOS)
|
||||
.onSwipeGesture(up: {
|
||||
model.presentingPlayer = true
|
||||
})
|
||||
#endif
|
||||
|
||||
return Group {
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
controls
|
||||
.background(Material.ultraThinMaterial)
|
||||
} else {
|
||||
controls
|
||||
.background(Color.tertiaryBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var appVersion: String {
|
||||
|
@@ -31,9 +31,6 @@ struct SubscriptionsView: View {
|
||||
FavoriteButton(item: FavoriteItem(section: .subscriptions))
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
loadResources(force: true)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func loadResources(force: Bool = false) {
|
||||
|
@@ -113,7 +113,7 @@ struct VideoContextMenuView: View {
|
||||
private var subscriptionButton: some View {
|
||||
Group {
|
||||
if subscriptions.isSubscribing(video.channel.id) {
|
||||
Button(role: .destructive) {
|
||||
Button {
|
||||
#if os(tvOS)
|
||||
subscriptions.unsubscribe(video.channel.id)
|
||||
#else
|
||||
@@ -143,7 +143,7 @@ struct VideoContextMenuView: View {
|
||||
}
|
||||
|
||||
func removeFromPlaylistButton(playlistID: String) -> some View {
|
||||
Button(role: .destructive) {
|
||||
Button {
|
||||
playlists.removeVideo(videoIndexID: video.indexID!, playlistID: playlistID)
|
||||
} label: {
|
||||
Label("Remove from playlist", systemImage: "text.badge.minus")
|
||||
|
@@ -2,14 +2,14 @@ import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct WelcomeScreen: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
|
||||
@Default(.accounts) private var allAccounts
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
let welcomeScreen = VStack {
|
||||
Spacer()
|
||||
|
||||
Text("Welcome")
|
||||
@@ -26,7 +26,7 @@ struct WelcomeScreen: View {
|
||||
AccountSelectionView(showHeader: false)
|
||||
|
||||
Button {
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} label: {
|
||||
Text("Start")
|
||||
}
|
||||
@@ -36,7 +36,7 @@ struct WelcomeScreen: View {
|
||||
#else
|
||||
AccountsMenuView()
|
||||
.onChange(of: accounts.current) { _ in
|
||||
dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
#if os(macOS)
|
||||
.frame(maxWidth: 280)
|
||||
@@ -50,10 +50,16 @@ struct WelcomeScreen: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.interactiveDismissDisabled()
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 400, minHeight: 400)
|
||||
.frame(minWidth: 400, minHeight: 400)
|
||||
#endif
|
||||
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
welcomeScreen
|
||||
.interactiveDismissDisabled()
|
||||
} else {
|
||||
welcomeScreen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user