iOS 14/macOS Big Sur Support

This commit is contained in:
Arkadiusz Fal
2021-11-28 15:37:55 +01:00
parent 696751e07c
commit 5ef89ac9f4
57 changed files with 1147 additions and 813 deletions

View File

@@ -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 {

View 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)
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View 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()
}
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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 {