mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 13:12:53 +00:00
Add refresh, accounts, and link actions to the instance browse menu
The three-dots menu now offers Refresh, an accounts entry (sign in when nobody is, otherwise a submenu that switches the data context, adds a login, or signs out the active account with the shared removal confirmation), and Copy URL / Open in Browser on iOS and macOS. Login and sign-out reuse InstanceLoginView, AccountSignInHandler, and the same confirmation strings as the edit sheet's accounts section.
This commit is contained in:
@@ -5547,6 +5547,17 @@
|
||||
},
|
||||
"Initializing player..." : {
|
||||
|
||||
},
|
||||
"instance.browse.copyURL" : {
|
||||
"comment" : "Menu item to copy the browsed instance's URL",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Copy URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"instance.browse.edit %@" : {
|
||||
"comment" : "Menu item to edit the browsed instance, with its name",
|
||||
@@ -5569,6 +5580,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"instance.browse.openInBrowser" : {
|
||||
"comment" : "Menu item to open the browsed instance's URL in the browser",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Open in Browser"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"instance.browse.remove %@" : {
|
||||
"comment" : "Menu item to remove the browsed instance, with its name",
|
||||
"localizations" : {
|
||||
@@ -5590,6 +5612,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"instance.browse.signOut %@" : {
|
||||
"comment" : "Menu item to sign out and remove an account, with its name",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sign Out %@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"instances.type.invidious" : {
|
||||
"comment" : "Instance type: Invidious",
|
||||
"localizations" : {
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
struct InstanceBrowseView: View {
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
let instance: Instance
|
||||
let initialTab: BrowseTab?
|
||||
@@ -54,6 +55,12 @@ struct InstanceBrowseView: View {
|
||||
@State private var showViewOptions = false
|
||||
@State private var showEditInstance = false
|
||||
@State private var showRemoveConfirmation = false
|
||||
|
||||
// Account actions raised from the three-dots menu. Same shapes as the
|
||||
// edit sheet's accounts section: a nil-account login target means
|
||||
// "create one", and the sign-out is held until its confirmation.
|
||||
@State private var loginTarget: AccountLoginTarget?
|
||||
@State private var pendingSignOut: Account?
|
||||
@State private var viewWidth: CGFloat = 0
|
||||
@State private var watchEntriesMap: [String: WatchEntry] = [:]
|
||||
@State private var hasInitializedTab = false
|
||||
@@ -129,10 +136,53 @@ struct InstanceBrowseView: View {
|
||||
)
|
||||
}
|
||||
|
||||
/// Shared content of the three-dots menu: instance management on top,
|
||||
/// view options below.
|
||||
/// All accounts on this instance, in the accounts submenu.
|
||||
private var instanceAccounts: [Account] {
|
||||
appEnvironment?.accountsManager.accounts(forInstance: instance.id) ?? []
|
||||
}
|
||||
|
||||
/// The account this screen's authenticated tabs actually use.
|
||||
private var effectiveAccount: Account? {
|
||||
appEnvironment?.account(forInstance: instance)
|
||||
}
|
||||
|
||||
private func accountDisplayName(_ account: Account) -> String {
|
||||
appEnvironment?.dataContextValidator.displayName(for: account) ?? instance.displayName
|
||||
}
|
||||
|
||||
/// Shared content of the three-dots menu: refresh and accounts on top,
|
||||
/// link actions and instance management below, view options last.
|
||||
@ViewBuilder
|
||||
private var instanceMenuContent: some View {
|
||||
Button {
|
||||
Task { await startContentLoad(forceRefresh: true) }
|
||||
} label: {
|
||||
Label(String(localized: "common.refresh"), systemImage: "arrow.clockwise")
|
||||
}
|
||||
|
||||
if instance.supportsAuthentication {
|
||||
Divider()
|
||||
accountsMenuContent
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
Divider()
|
||||
|
||||
Button {
|
||||
copyInstanceURL()
|
||||
} label: {
|
||||
Label(String(localized: "instance.browse.copyURL"), systemImage: "doc.on.doc")
|
||||
}
|
||||
|
||||
Button {
|
||||
openURL(instance.url)
|
||||
} label: {
|
||||
Label(String(localized: "instance.browse.openInBrowser"), systemImage: "safari")
|
||||
}
|
||||
#endif
|
||||
|
||||
Divider()
|
||||
|
||||
Button {
|
||||
showEditInstance = true
|
||||
} label: {
|
||||
@@ -154,6 +204,66 @@ struct InstanceBrowseView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign-in entry when nobody is signed in; otherwise an accounts submenu
|
||||
/// that switches the data context, adds a login, or signs the current one
|
||||
/// out. Switching goes through the global context — the same semantics as
|
||||
/// the switcher — which this view already reacts to.
|
||||
@ViewBuilder
|
||||
private var accountsMenuContent: some View {
|
||||
if instanceAccounts.isEmpty {
|
||||
Button {
|
||||
loginTarget = .newAccount
|
||||
} label: {
|
||||
Label(String(localized: "login.loginToAccount"), systemImage: "person.badge.plus")
|
||||
}
|
||||
} else {
|
||||
Menu {
|
||||
ForEach(instanceAccounts) { account in
|
||||
Button {
|
||||
appEnvironment?.switchDataContext(to: .account(account.id))
|
||||
} label: {
|
||||
if account.id == effectiveAccount?.id {
|
||||
Label(accountDisplayName(account), systemImage: "checkmark")
|
||||
} else {
|
||||
Text(accountDisplayName(account))
|
||||
}
|
||||
}
|
||||
.disabled(account.id == effectiveAccount?.id)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button {
|
||||
loginTarget = .newAccount
|
||||
} label: {
|
||||
Label(String(localized: "accounts.addAccount"), systemImage: "person.badge.plus")
|
||||
}
|
||||
|
||||
if let active = effectiveAccount {
|
||||
Button(role: .destructive) {
|
||||
pendingSignOut = active
|
||||
} label: {
|
||||
Label(
|
||||
String(localized: "instance.browse.signOut \(accountDisplayName(active))"),
|
||||
systemImage: "rectangle.portrait.and.arrow.forward"
|
||||
)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label(String(localized: "accounts.title"), systemImage: "person.crop.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func copyInstanceURL() {
|
||||
#if os(iOS)
|
||||
UIPasteboard.general.string = instance.url.absoluteString
|
||||
#elseif os(macOS)
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(instance.url.absoluteString, forType: .string)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// View options button lives on the leading edge on macOS, trailing elsewhere.
|
||||
private var viewOptionsPlacement: ToolbarItemPlacement {
|
||||
#if os(macOS)
|
||||
@@ -466,6 +576,36 @@ struct InstanceBrowseView: View {
|
||||
Text(accounts)
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.fullScreenCover(item: $loginTarget) { target in
|
||||
InstanceLoginView(instance: instance, initialUsername: storedUsername(for: target)) { credential, username in
|
||||
storeLogin(credential: credential, username: username, target: target)
|
||||
}
|
||||
}
|
||||
#else
|
||||
.sheet(item: $loginTarget) { target in
|
||||
InstanceLoginView(instance: instance, initialUsername: storedUsername(for: target)) { credential, username in
|
||||
storeLogin(credential: credential, username: username, target: target)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.confirmationDialog(
|
||||
String(localized: "accounts.remove.confirm.title \(pendingSignOut.map(accountDisplayName) ?? instance.displayName)"),
|
||||
isPresented: Binding(
|
||||
get: { pendingSignOut != nil },
|
||||
set: { if !$0 { pendingSignOut = nil } }
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(String(localized: "common.remove"), role: .destructive) {
|
||||
confirmSignOut()
|
||||
}
|
||||
Button(String(localized: "common.cancel"), role: .cancel) {
|
||||
pendingSignOut = nil
|
||||
}
|
||||
} message: {
|
||||
Text(String(localized: "accounts.remove.confirm.message"))
|
||||
}
|
||||
.task {
|
||||
// Initialize search view model
|
||||
if let appEnvironment {
|
||||
@@ -1361,6 +1501,32 @@ struct InstanceBrowseView: View {
|
||||
isLoggedIn = appEnvironment?.isSignedIn(to: instance) ?? false
|
||||
}
|
||||
|
||||
/// Files a successful login against an account, mirroring the edit sheet.
|
||||
@discardableResult
|
||||
private func storeLogin(credential: String, username: String, target: AccountLoginTarget) -> Bool {
|
||||
guard let appEnvironment else { return false }
|
||||
return AccountSignInHandler(appEnvironment: appEnvironment, instance: instance)
|
||||
.storeLogin(credential: credential, username: username, target: target)
|
||||
}
|
||||
|
||||
/// The identifier an existing account last signed in with, when this device
|
||||
/// holds it. A new account has nobody to name yet.
|
||||
private func storedUsername(for target: AccountLoginTarget) -> String? {
|
||||
guard let account = target.account else { return nil }
|
||||
return appEnvironment?.accountCredentialsManager(for: account)?.username(for: account)
|
||||
}
|
||||
|
||||
private func confirmSignOut() {
|
||||
guard let appEnvironment, let account = pendingSignOut else { return }
|
||||
defer { pendingSignOut = nil }
|
||||
|
||||
// Re-resolved rather than trusted: the dialog can outlive the account,
|
||||
// which another device may have deleted while it was up.
|
||||
guard let account = appEnvironment.accountsManager.account(id: account.id) else { return }
|
||||
|
||||
appEnvironment.signOutAndRemoveAccount(account)
|
||||
}
|
||||
|
||||
private func loadWatchEntries() {
|
||||
watchEntriesMap = appEnvironment?.dataManager.watchEntriesMap() ?? [:]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user