mirror of
https://github.com/yattee/yattee.git
synced 2025-08-09 20:24:06 +00:00
iOS 14/macOS Big Sur Support
This commit is contained 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 {
|
||||
|
Reference in New Issue
Block a user