mirror of
https://github.com/yattee/yattee.git
synced 2025-08-06 10:44:06 +00:00
Unify forms, add to/remove from playlist on all platforms, UI improvements
This commit is contained in:
@@ -5,13 +5,10 @@ extension Defaults.Keys {
|
||||
static let accounts = Key<[Instance.Account]>("accounts", default: [])
|
||||
static let defaultAccountID = Key<String?>("defaultAccountID")
|
||||
|
||||
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
|
||||
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
|
||||
|
||||
static let selectedPlaylistID = Key<String?>("selectedPlaylistID")
|
||||
static let showingAddToPlaylist = Key<Bool>("showingAddToPlaylist", default: false)
|
||||
static let videoIDToAddToPlaylist = Key<String?>("videoIDToAddToPlaylist")
|
||||
static let quality = Key<Stream.ResolutionSetting>("quality", default: .hd720pFirstThenBest)
|
||||
|
||||
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
|
||||
static let quality = Key<Stream.ResolutionSetting>("quality", default: .hd720pFirstThenBest)
|
||||
|
||||
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
|
||||
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ struct ContentView: View {
|
||||
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
@EnvironmentObject<InstancesModel> private var instances
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@@ -44,6 +45,9 @@ struct ContentView: View {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $navigation.presentingAddToPlaylist) {
|
||||
AddToPlaylistView(video: navigation.videoToAddToPlaylist)
|
||||
}
|
||||
.sheet(isPresented: $navigation.presentingPlaylistForm) {
|
||||
PlaylistFormView(playlist: $navigation.editedPlaylist)
|
||||
}
|
||||
|
@@ -40,7 +40,7 @@ struct PearvidiousApp: App {
|
||||
search.api = api
|
||||
subscriptions.api = api
|
||||
|
||||
if let account = instances.defaultAccount {
|
||||
if let account = instances.defaultAccount, api.account.isNil {
|
||||
api.setAccount(account)
|
||||
}
|
||||
}
|
||||
|
178
Shared/Playlists/AddToPlaylistView.swift
Normal file
178
Shared/Playlists/AddToPlaylistView.swift
Normal file
@@ -0,0 +1,178 @@
|
||||
import Defaults
|
||||
import Siesta
|
||||
import SwiftUI
|
||||
|
||||
struct AddToPlaylistView: View {
|
||||
@EnvironmentObject<PlaylistsModel> private var model
|
||||
|
||||
@State var video: Video
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
VStack {
|
||||
if model.isEmpty {
|
||||
emptyPlaylistsMessage
|
||||
} else {
|
||||
header
|
||||
Spacer()
|
||||
form
|
||||
Spacer()
|
||||
footer
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 1000, maxHeight: height)
|
||||
}
|
||||
.onAppear {
|
||||
model.load {
|
||||
if let playlist = model.all.first {
|
||||
model.selectedPlaylistID = playlist.id
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.frame(width: 500, height: 270)
|
||||
.padding(.vertical)
|
||||
#elseif os(tvOS)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.background(.thickMaterial)
|
||||
#else
|
||||
.padding(.vertical)
|
||||
#endif
|
||||
}
|
||||
|
||||
var height: Double {
|
||||
#if os(tvOS)
|
||||
600
|
||||
#else
|
||||
.infinity
|
||||
#endif
|
||||
}
|
||||
|
||||
private var emptyPlaylistsMessage: some View {
|
||||
VStack(spacing: 20) {
|
||||
Text("You have no Playlists")
|
||||
.font(.title2.bold())
|
||||
Text("Open \"Playlists\" tab to create new one")
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack(alignment: .center) {
|
||||
Text("Add to Playlist")
|
||||
.font(.title2.bold())
|
||||
|
||||
Spacer()
|
||||
|
||||
#if !os(tvOS)
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
private var form: some View {
|
||||
VStack(alignment: formAlignment) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(video.title)
|
||||
.font(.headline)
|
||||
Text(video.author)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.vertical, 40)
|
||||
|
||||
VStack(alignment: formAlignment) {
|
||||
#if os(tvOS)
|
||||
|
||||
selectPlaylistButton
|
||||
#else
|
||||
Picker("Playlist", selection: $model.selectedPlaylistID) {
|
||||
ForEach(model.all) { playlist in
|
||||
Text(playlist.title).tag(playlist.id)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 500)
|
||||
#if os(iOS)
|
||||
.pickerStyle(.inline)
|
||||
#elseif os(macOS)
|
||||
.labelsHidden()
|
||||
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
private var formAlignment: HorizontalAlignment {
|
||||
#if os(tvOS)
|
||||
.trailing
|
||||
#else
|
||||
.center
|
||||
#endif
|
||||
}
|
||||
|
||||
private var footer: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Add to Playlist", action: addToPlaylist)
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
#endif
|
||||
.disabled(model.currentPlaylist.isNil)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
private var footerAlignment: HorizontalAlignment {
|
||||
#if os(tvOS)
|
||||
.trailing
|
||||
#else
|
||||
.leading
|
||||
#endif
|
||||
}
|
||||
|
||||
private var selectPlaylistButton: some View {
|
||||
Button(model.currentPlaylist?.title ?? "Select playlist") {
|
||||
guard model.currentPlaylist != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
model.selectedPlaylistID = model.all.next(after: model.currentPlaylist!)!.id
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(model.all) { playlist in
|
||||
Button(playlist.title) {
|
||||
model.selectedPlaylistID = playlist.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addToPlaylist() {
|
||||
guard model.currentPlaylist != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
model.addVideoToCurrentPlaylist(videoID: video.id) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddToPlaylistView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddToPlaylistView(video: Video.fixture)
|
||||
.environmentObject(PlaylistsModel([Playlist.fixture]))
|
||||
.environmentObject(SubscriptionsModel())
|
||||
.environmentObject(NavigationModel())
|
||||
}
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CoverSectionRowView<ControlContent: View>: View {
|
||||
let label: String?
|
||||
let controlView: ControlContent
|
||||
|
||||
init(_ label: String? = nil, @ViewBuilder controlView: @escaping () -> ControlContent) {
|
||||
self.label = label
|
||||
self.controlView = controlView()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(label ?? "")
|
||||
|
||||
Spacer()
|
||||
controlView
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,52 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CoverSectionView<Content: View>: View {
|
||||
let title: String?
|
||||
|
||||
let actionsView: Content
|
||||
let divider: Bool
|
||||
let inline: Bool
|
||||
|
||||
init(_ title: String? = nil, divider: Bool = true, inline: Bool = false, @ViewBuilder actionsView: @escaping () -> Content) {
|
||||
self.title = title
|
||||
self.divider = divider
|
||||
self.inline = inline
|
||||
self.actionsView = actionsView()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if inline {
|
||||
HStack {
|
||||
if title != nil {
|
||||
sectionTitle
|
||||
}
|
||||
|
||||
Spacer()
|
||||
actionsView
|
||||
}
|
||||
} else if title != nil {
|
||||
sectionTitle
|
||||
}
|
||||
|
||||
if !inline {
|
||||
actionsView
|
||||
}
|
||||
}
|
||||
|
||||
if divider {
|
||||
Divider()
|
||||
.padding(.vertical)
|
||||
}
|
||||
}
|
||||
|
||||
var sectionTitle: some View {
|
||||
Text(title ?? "")
|
||||
|
||||
.font(.title2)
|
||||
#if os(macOS)
|
||||
.bold()
|
||||
#endif
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
@@ -73,37 +73,13 @@ struct PlaylistFormView: View {
|
||||
#endif
|
||||
|
||||
#else
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
CoverSectionView(editing ? "Edit Playlist" : "Create Playlist") {
|
||||
CoverSectionRowView("Name") {
|
||||
TextField("Playlist Name", text: $name, onCommit: validate)
|
||||
.frame(maxWidth: 450)
|
||||
}
|
||||
|
||||
CoverSectionRowView("Visibility") { visibilityButton }
|
||||
}
|
||||
|
||||
CoverSectionRowView {
|
||||
Button("Save", action: submitForm).disabled(!valid)
|
||||
}
|
||||
|
||||
if editing {
|
||||
CoverSectionView("Delete Playlist", divider: false, inline: true) { deletePlaylistButton }
|
||||
.padding(.top, 50)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
VStack {
|
||||
Group {
|
||||
header
|
||||
form
|
||||
}
|
||||
.frame(maxWidth: 800)
|
||||
|
||||
Spacer()
|
||||
.frame(maxWidth: 1000)
|
||||
}
|
||||
.background(.thinMaterial)
|
||||
.onAppear {
|
||||
guard editing else {
|
||||
return
|
||||
@@ -114,9 +90,66 @@ struct PlaylistFormView: View {
|
||||
|
||||
validate()
|
||||
}
|
||||
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.background(.thickMaterial)
|
||||
#endif
|
||||
}
|
||||
|
||||
var header: some View {
|
||||
HStack(alignment: .center) {
|
||||
Text(editing ? "Edit Playlist" : "Create Playlist")
|
||||
.font(.title2.bold())
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
var form: some View {
|
||||
VStack(alignment: .trailing) {
|
||||
VStack {
|
||||
Text("Name")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
TextField("Playlist Name", text: $name, onCommit: validate)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Visibility")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
visibilityButton
|
||||
}
|
||||
.padding(.top, 10)
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button("Save", action: submitForm).disabled(!valid)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
|
||||
if editing {
|
||||
Divider()
|
||||
HStack {
|
||||
Text("Delete playlist")
|
||||
.font(.title2.bold())
|
||||
Spacer()
|
||||
deletePlaylistButton
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
func initializeForm() {
|
||||
focused = true
|
||||
|
||||
|
@@ -3,9 +3,10 @@ import Siesta
|
||||
import SwiftUI
|
||||
|
||||
struct PlaylistsView: View {
|
||||
@StateObject private var store = Store<[Playlist]>()
|
||||
@EnvironmentObject<PlaylistsModel> private var model
|
||||
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
|
||||
@State private var showingNewPlaylist = false
|
||||
@State private var createdPlaylist: Playlist?
|
||||
@@ -13,14 +14,11 @@ struct PlaylistsView: View {
|
||||
@State private var showingEditPlaylist = false
|
||||
@State private var editedPlaylist: Playlist?
|
||||
|
||||
@Default(.selectedPlaylistID) private var selectedPlaylistID
|
||||
|
||||
var resource: Resource {
|
||||
api.playlists
|
||||
}
|
||||
@State private var showingAddToPlaylist = false
|
||||
@State private var videoIDToAddToPlaylist = ""
|
||||
|
||||
var videos: [Video] {
|
||||
currentPlaylist?.videos ?? []
|
||||
model.currentPlaylist?.videos ?? []
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -31,9 +29,9 @@ struct PlaylistsView: View {
|
||||
.font(.system(size: 28))
|
||||
|
||||
#endif
|
||||
if currentPlaylist != nil, videos.isEmpty {
|
||||
if model.currentPlaylist != nil, videos.isEmpty {
|
||||
hintText("Playlist is empty\n\nTap and hold on a video and then tap \"Add to Playlist\"")
|
||||
} else if store.collection.isEmpty {
|
||||
} else if model.all.isEmpty {
|
||||
hintText("You have no playlists\n\nTap on \"New Playlist\" to create one")
|
||||
} else {
|
||||
VideosCellsVertical(videos: videos)
|
||||
@@ -58,11 +56,11 @@ struct PlaylistsView: View {
|
||||
.toolbar {
|
||||
ToolbarItemGroup {
|
||||
#if !os(iOS)
|
||||
if !store.collection.isEmpty {
|
||||
if !model.isEmpty {
|
||||
selectPlaylistButton
|
||||
}
|
||||
|
||||
if currentPlaylist != nil {
|
||||
if model.currentPlaylist != nil {
|
||||
editPlaylistButton
|
||||
}
|
||||
#endif
|
||||
@@ -72,7 +70,7 @@ struct PlaylistsView: View {
|
||||
#if os(iOS)
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
Group {
|
||||
if store.collection.isEmpty {
|
||||
if model.isEmpty {
|
||||
Text("No Playlists")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
@@ -84,7 +82,7 @@ struct PlaylistsView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
if currentPlaylist != nil {
|
||||
if model.currentPlaylist != nil {
|
||||
editPlaylistButton
|
||||
}
|
||||
}
|
||||
@@ -93,17 +91,13 @@ struct PlaylistsView: View {
|
||||
#endif
|
||||
}
|
||||
.onAppear {
|
||||
resource.addObserver(store)
|
||||
|
||||
resource.loadIfNeeded()?.onSuccess { _ in
|
||||
selectPlaylist(selectedPlaylistID)
|
||||
}
|
||||
model.load()
|
||||
}
|
||||
}
|
||||
|
||||
var toolbar: some View {
|
||||
HStack {
|
||||
if store.collection.isEmpty {
|
||||
if model.isEmpty {
|
||||
Text("No Playlists")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
@@ -117,7 +111,7 @@ struct PlaylistsView: View {
|
||||
Spacer()
|
||||
#endif
|
||||
|
||||
if currentPlaylist != nil {
|
||||
if model.currentPlaylist != nil {
|
||||
editPlaylistButton
|
||||
}
|
||||
|
||||
@@ -142,17 +136,15 @@ struct PlaylistsView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
func selectPlaylist(_ id: String?) {
|
||||
selectedPlaylistID = id
|
||||
}
|
||||
|
||||
func selectCreatedPlaylist() {
|
||||
guard createdPlaylist != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
resource.load().onSuccess { _ in
|
||||
self.selectPlaylist(createdPlaylist?.id)
|
||||
model.load(force: true) {
|
||||
if let id = createdPlaylist?.id {
|
||||
self.model.selectPlaylist(id)
|
||||
}
|
||||
|
||||
self.createdPlaylist = nil
|
||||
}
|
||||
@@ -160,41 +152,37 @@ struct PlaylistsView: View {
|
||||
|
||||
func selectEditedPlaylist() {
|
||||
if editedPlaylist.isNil {
|
||||
selectPlaylist(nil)
|
||||
model.selectPlaylist(nil)
|
||||
}
|
||||
|
||||
resource.load().onSuccess { _ in
|
||||
selectPlaylist(editedPlaylist?.id)
|
||||
model.load(force: true) {
|
||||
model.selectPlaylist(editedPlaylist?.id)
|
||||
|
||||
self.editedPlaylist = nil
|
||||
}
|
||||
}
|
||||
|
||||
var currentPlaylist: Playlist? {
|
||||
store.collection.first { $0.id == selectedPlaylistID } ?? store.collection.first
|
||||
}
|
||||
|
||||
var selectPlaylistButton: some View {
|
||||
#if os(tvOS)
|
||||
Button(currentPlaylist?.title ?? "Select playlist") {
|
||||
guard currentPlaylist != nil else {
|
||||
Button(model.currentPlaylist?.title ?? "Select playlist") {
|
||||
guard model.currentPlaylist != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
selectPlaylist(store.collection.next(after: currentPlaylist!)?.id)
|
||||
model.selectPlaylist(model.all.next(after: model.currentPlaylist!)?.id)
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(store.collection) { playlist in
|
||||
ForEach(model.all) { playlist in
|
||||
Button(playlist.title) {
|
||||
selectPlaylist(playlist.id)
|
||||
model.selectPlaylist(playlist.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
Menu(currentPlaylist?.title ?? "Select playlist") {
|
||||
ForEach(store.collection) { playlist in
|
||||
Button(action: { selectPlaylist(playlist.id) }) {
|
||||
if playlist == self.currentPlaylist {
|
||||
Menu(model.currentPlaylist?.title ?? "Select playlist") {
|
||||
ForEach(model.all) { playlist in
|
||||
Button(action: { model.selectPlaylist(playlist.id) }) {
|
||||
if playlist == model.currentPlaylist {
|
||||
Label(playlist.title, systemImage: "checkmark")
|
||||
} else {
|
||||
Text(playlist.title)
|
||||
@@ -207,7 +195,7 @@ struct PlaylistsView: View {
|
||||
|
||||
var editPlaylistButton: some View {
|
||||
Button(action: {
|
||||
self.editedPlaylist = self.currentPlaylist
|
||||
self.editedPlaylist = self.model.currentPlaylist
|
||||
self.showingEditPlaylist = true
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
|
@@ -20,12 +20,18 @@ struct AccountFormView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
header
|
||||
form
|
||||
footer
|
||||
Group {
|
||||
header
|
||||
form
|
||||
footer
|
||||
}
|
||||
.frame(maxWidth: 1000)
|
||||
}
|
||||
#if os(iOS)
|
||||
.padding(.vertical)
|
||||
#elseif os(tvOS)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.background(.thickMaterial)
|
||||
#else
|
||||
.frame(width: 400, height: 145)
|
||||
#endif
|
||||
@@ -48,18 +54,30 @@ struct AccountFormView: View {
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
var form: some View {
|
||||
Form {
|
||||
private var form: some View {
|
||||
Group {
|
||||
#if !os(tvOS)
|
||||
Form {
|
||||
formFields
|
||||
#if os(macOS)
|
||||
.padding(.horizontal)
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
formFields
|
||||
#endif
|
||||
}
|
||||
.onAppear(perform: initializeForm)
|
||||
.onChange(of: sid) { _ in validate() }
|
||||
}
|
||||
|
||||
var formFields: some View {
|
||||
Group {
|
||||
TextField("Name", text: $name, prompt: Text("Account Name (optional)"))
|
||||
.focused($focused)
|
||||
|
||||
TextField("SID", text: $sid, prompt: Text("Invidious SID Cookie"))
|
||||
}
|
||||
.onAppear(perform: initializeForm)
|
||||
.onChange(of: sid) { _ in validate() }
|
||||
#if os(macOS)
|
||||
.padding(.horizontal)
|
||||
#endif
|
||||
}
|
||||
|
||||
var footer: some View {
|
||||
@@ -75,6 +93,9 @@ struct AccountFormView: View {
|
||||
#endif
|
||||
}
|
||||
.frame(minHeight: 35)
|
||||
#if os(tvOS)
|
||||
.padding(.top, 30)
|
||||
#endif
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
|
@@ -15,15 +15,16 @@ struct InstanceDetailsSettingsView: View {
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("Accounts")) {
|
||||
ForEach(instances.accounts(instanceID)) { account in
|
||||
HStack(spacing: 2) {
|
||||
Text(account.description)
|
||||
if instances.defaultAccount == account {
|
||||
Text("— default")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
ForEach(instances.accounts(instanceID), id: \.self) { account in
|
||||
|
||||
#if !os(tvOS)
|
||||
HStack(spacing: 2) {
|
||||
Text(account.description)
|
||||
if instances.defaultAccount == account {
|
||||
Text("— default")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
if instances.defaultAccount != account {
|
||||
Button("Make Default", action: { makeDefault(account) })
|
||||
@@ -34,6 +35,21 @@ struct InstanceDetailsSettingsView: View {
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button("Remove", role: .destructive, action: { removeAccount(account) })
|
||||
}
|
||||
|
||||
#else
|
||||
Button(action: { toggleDefault(account) }) {
|
||||
HStack(spacing: 2) {
|
||||
Text(account.description)
|
||||
if instances.defaultAccount == account {
|
||||
Text("— default")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Toggle Default", action: { toggleDefault(account) })
|
||||
Button("Remove", role: .destructive, action: { removeAccount(account) })
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.redrawOn(change: accountsChanged)
|
||||
@@ -45,6 +61,8 @@ struct InstanceDetailsSettingsView: View {
|
||||
}
|
||||
#if os(iOS)
|
||||
.listStyle(.insetGrouped)
|
||||
#elseif os(tvOS)
|
||||
.frame(maxWidth: 1000)
|
||||
#endif
|
||||
|
||||
.navigationTitle(instance.shortDescription)
|
||||
@@ -58,6 +76,14 @@ struct InstanceDetailsSettingsView: View {
|
||||
accountsChanged.toggle()
|
||||
}
|
||||
|
||||
private func toggleDefault(_ account: Instance.Account) {
|
||||
if account == instances.defaultAccount {
|
||||
resetDefaultAccount()
|
||||
} else {
|
||||
makeDefault(account)
|
||||
}
|
||||
}
|
||||
|
||||
private func resetDefaultAccount() {
|
||||
instances.resetDefaultAccount()
|
||||
accountsChanged.toggle()
|
||||
|
@@ -19,69 +19,104 @@ struct InstanceFormView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .center) {
|
||||
Text("Add Instance")
|
||||
.font(.title2.bold())
|
||||
Group {
|
||||
header
|
||||
|
||||
Spacer()
|
||||
form
|
||||
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
footer
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
Form {
|
||||
TextField("Name", text: $name, prompt: Text("Instance Name (optional)"))
|
||||
.frame(maxWidth: 450)
|
||||
.focused($nameFieldFocused)
|
||||
|
||||
TextField("URL", text: $url, prompt: Text("https://invidious.home.net"))
|
||||
.frame(maxWidth: 450)
|
||||
}
|
||||
#if os(macOS)
|
||||
.padding(.horizontal)
|
||||
#endif
|
||||
|
||||
HStack {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: valid ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||
.foregroundColor(valid ? .green : .red)
|
||||
VStack(alignment: .leading) {
|
||||
Text(valid ? "Connected successfully" : "Connection failed")
|
||||
if !valid {
|
||||
Text(validationError ?? "Unknown Error")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.truncationMode(.tail)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 40)
|
||||
}
|
||||
.opacity(validated ? 1 : 0)
|
||||
Spacer()
|
||||
|
||||
Button("Save", action: submitForm)
|
||||
.disabled(!valid)
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
#endif
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.frame(maxWidth: 1000)
|
||||
}
|
||||
.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)
|
||||
#else
|
||||
.frame(width: 400, height: 150)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack(alignment: .center) {
|
||||
Text("Add Instance")
|
||||
.font(.title2.bold())
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
private var form: some View {
|
||||
#if !os(tvOS)
|
||||
Form {
|
||||
formFields
|
||||
#if os(macOS)
|
||||
.padding(.horizontal)
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
formFields
|
||||
#endif
|
||||
}
|
||||
|
||||
private var formFields: some View {
|
||||
Group {
|
||||
TextField("Name", text: $name, prompt: Text("Instance Name (optional)"))
|
||||
|
||||
.focused($nameFieldFocused)
|
||||
|
||||
TextField("URL", text: $url, prompt: Text("https://invidious.home.net"))
|
||||
}
|
||||
}
|
||||
|
||||
private var footer: some View {
|
||||
HStack(alignment: .center) {
|
||||
validationStatus
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Save", action: submitForm)
|
||||
.disabled(!valid)
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
#endif
|
||||
}
|
||||
#if os(tvOS)
|
||||
.padding(.top, 30)
|
||||
#endif
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
private var validationStatus: some View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: valid ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||
.foregroundColor(valid ? .green : .red)
|
||||
VStack(alignment: .leading) {
|
||||
Text(valid ? "Connected successfully" : "Connection failed")
|
||||
if !valid {
|
||||
Text(validationError ?? "Unknown Error")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.truncationMode(.tail)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 35)
|
||||
}
|
||||
.opacity(validated ? 1 : 0)
|
||||
}
|
||||
|
||||
var validator: AccountValidator {
|
||||
AccountValidator(
|
||||
url: url,
|
||||
|
@@ -34,7 +34,7 @@ struct InstancesSettingsView: View {
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
Section(header: instancesHeader, footer: instancesFooter) {
|
||||
Section(header: instancesHeader, footer: defaultAccountSection) {
|
||||
ForEach(instances) { instance in
|
||||
Button(action: {
|
||||
self.selectedInstanceID = instance.id
|
||||
@@ -59,11 +59,33 @@ struct InstancesSettingsView: View {
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
Button("Add Instance...") {
|
||||
presentingInstanceForm = true
|
||||
}
|
||||
addInstanceButton
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
#elseif os(tvOS)
|
||||
Section(header: instancesHeader) {
|
||||
ForEach(instances) { instance in
|
||||
Button(action: {
|
||||
self.selectedInstanceID = instance.id
|
||||
self.presentingInstanceDetails = true
|
||||
}) {
|
||||
Text(instance.description)
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Remove", role: .destructive) {
|
||||
instancesModel.remove(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addInstanceButton
|
||||
|
||||
defaultAccountSection
|
||||
}
|
||||
.frame(maxWidth: 1000, alignment: .leading)
|
||||
.sheet(isPresented: $presentingAccountForm) {
|
||||
AccountFormView(instance: selectedInstance, selectedAccount: $selectedAccount)
|
||||
}
|
||||
#else
|
||||
Section {
|
||||
Text("Instance")
|
||||
@@ -157,22 +179,33 @@ struct InstancesSettingsView: View {
|
||||
Text("Instances").background(instanceDetailsNavigationLink)
|
||||
}
|
||||
|
||||
var instancesFooter: some View {
|
||||
var defaultAccountSection: some View {
|
||||
Group {
|
||||
if let account = instancesModel.defaultAccount {
|
||||
HStack(spacing: 2) {
|
||||
Text("**\(account.description)** account on instance **\(account.instance.shortDescription)** is your default.")
|
||||
.truncationMode(.middle)
|
||||
.lineLimit(1)
|
||||
VStack {
|
||||
HStack(spacing: 2) {
|
||||
Text("**\(account.description)** account on instance **\(account.instance.shortDescription)** is your default.")
|
||||
.truncationMode(.middle)
|
||||
.lineLimit(1)
|
||||
|
||||
Button("Reset", action: resetDefaultAccount)
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(.red)
|
||||
#if !os(tvOS)
|
||||
|
||||
Button("Reset", action: resetDefaultAccount)
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(.red)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("You have no default account set")
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.foregroundColor(.gray)
|
||||
#elseif os(macOS)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
#endif
|
||||
}
|
||||
|
||||
var instanceDetailsNavigationLink: some View {
|
||||
@@ -181,25 +214,13 @@ struct InstancesSettingsView: View {
|
||||
destination: { InstanceDetailsSettingsView(instanceID: selectedInstanceID) },
|
||||
label: { EmptyView() }
|
||||
)
|
||||
.opacity(0)
|
||||
}
|
||||
|
||||
private var defaultAccountSection: some View {
|
||||
Group {
|
||||
if let account = instancesModel.defaultAccount {
|
||||
HStack(spacing: 2) {
|
||||
Text("**\(account.description)** account on instance **\(account.instance.shortDescription)** is your default.")
|
||||
.truncationMode(.middle)
|
||||
.lineLimit(1)
|
||||
Button("Reset", action: resetDefaultAccount)
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
} else {
|
||||
Text("You have no default account set")
|
||||
}
|
||||
private var addInstanceButton: some View {
|
||||
Button("Add Instance...") {
|
||||
presentingInstanceForm = true
|
||||
}
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
private func resetDefaultAccount() {
|
||||
|
@@ -15,6 +15,8 @@ struct PlaybackSettingsView: View {
|
||||
|
||||
#if os(iOS)
|
||||
.pickerStyle(.automatic)
|
||||
#elseif os(tvOS)
|
||||
.pickerStyle(.inline)
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
|
@@ -33,24 +33,31 @@ struct SettingsView: View {
|
||||
#else
|
||||
NavigationView {
|
||||
List {
|
||||
#if os(tvOS)
|
||||
AccountSelectionView()
|
||||
#endif
|
||||
InstancesSettingsView()
|
||||
PlaybackSettingsView()
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
#if !os(tvOS)
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 1000)
|
||||
#if os(iOS)
|
||||
.listStyle(.insetGrouped)
|
||||
#endif
|
||||
}
|
||||
#if os(tvOS)
|
||||
.background(.thickMaterial)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -58,6 +65,11 @@ struct SettingsView: View {
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView()
|
||||
.environmentObject(InstancesModel())
|
||||
.environmentObject(InvidiousAPI())
|
||||
.environmentObject(NavigationModel())
|
||||
.environmentObject(SearchModel())
|
||||
.environmentObject(SubscriptionsModel())
|
||||
#if os(macOS)
|
||||
.frame(width: 600, height: 300)
|
||||
#endif
|
||||
|
@@ -69,6 +69,9 @@ struct ChannelVideosView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if os(tvOS)
|
||||
.background(.thickMaterial)
|
||||
#endif
|
||||
.modifier(UnsubscribeAlertModifier())
|
||||
.onAppear {
|
||||
if store.item.isNil {
|
||||
|
@@ -37,7 +37,7 @@ struct SearchView: View {
|
||||
}
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
#else
|
||||
VideosCellsVertical(videos: state.store.collection)
|
||||
VideosCellsVertical(videos: state.store.collection)
|
||||
#endif
|
||||
|
||||
if noResults {
|
||||
|
@@ -47,6 +47,10 @@ struct SignInRequiredView<Content: View>: View {
|
||||
if instances.isEmpty {
|
||||
openSettingsButton
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
openSettingsButton
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,22 +4,24 @@ import SwiftUI
|
||||
struct VideoContextMenuView: View {
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
let video: Video
|
||||
|
||||
@Default(.showingAddToPlaylist) var showingAddToPlaylist
|
||||
@Default(.videoIDToAddToPlaylist) var videoIDToAddToPlaylist
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
openChannelButton
|
||||
|
||||
subscriptionButton
|
||||
|
||||
if case let .playlist(id) = navigation.tabSelection {
|
||||
removeFromPlaylistButton(playlistID: id)
|
||||
}
|
||||
|
||||
if navigation.tabSelection == .playlists {
|
||||
removeFromPlaylistButton
|
||||
removeFromPlaylistButton(playlistID: playlists.currentPlaylist!.id)
|
||||
} else {
|
||||
addToPlaylistButton
|
||||
}
|
||||
@@ -29,7 +31,7 @@ struct VideoContextMenuView: View {
|
||||
var openChannelButton: some View {
|
||||
Button("\(video.author) Channel") {
|
||||
let recent = RecentItem(from: video.channel)
|
||||
recents.open(recent)
|
||||
recents.add(recent)
|
||||
navigation.tabSelection = .recentlyOpened(recent.tag)
|
||||
navigation.isChannelOpen = true
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
@@ -58,17 +60,13 @@ struct VideoContextMenuView: View {
|
||||
|
||||
var addToPlaylistButton: some View {
|
||||
Button("Add to playlist...") {
|
||||
videoIDToAddToPlaylist = video.id
|
||||
showingAddToPlaylist = true
|
||||
navigation.presentAddToPlaylist(video)
|
||||
}
|
||||
}
|
||||
|
||||
var removeFromPlaylistButton: some View {
|
||||
func removeFromPlaylistButton(playlistID: String) -> some View {
|
||||
Button("Remove from playlist", role: .destructive) {
|
||||
let resource = api.playlistVideo(Defaults[.selectedPlaylistID]!, video.indexID!)
|
||||
resource.request(.delete).onSuccess { _ in
|
||||
api.playlists.load()
|
||||
}
|
||||
playlists.removeVideoFromPlaylist(videoIndexID: video.indexID!, playlistID: playlistID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ struct WatchNowSection: View {
|
||||
WatchNowSectionBody(label: label, videos: store.collection)
|
||||
.onAppear {
|
||||
resource.addObserver(store)
|
||||
resource.load()
|
||||
resource.loadIfNeeded()
|
||||
}
|
||||
.onChange(of: api.account) { _ in
|
||||
resource.load()
|
||||
|
Reference in New Issue
Block a user