mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(client/ios): favorites menu (#6298)
  --------- Signed-off-by: Reactor Scram <ReactorScram@users.noreply.github.com> Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
@@ -70,7 +70,7 @@ struct FirezoneApp: App {
|
||||
|
||||
func applicationDidFinishLaunching(_: Notification) {
|
||||
if let store = store {
|
||||
menuBar = MenuBar(favorites: favorites, model: SessionViewModel(store: store))
|
||||
menuBar = MenuBar(model: SessionViewModel(favorites: favorites, store: store))
|
||||
}
|
||||
|
||||
// SwiftUI will show the first window group, so close it on launch
|
||||
|
||||
@@ -2,9 +2,11 @@ import Foundation
|
||||
|
||||
public class Favorites: ObservableObject {
|
||||
private static let key = "favoriteResourceIDs"
|
||||
@Published private(set) var ids: Set<String> = Favorites.load()
|
||||
@Published private(set) var ids: Set<String>
|
||||
|
||||
public init() {}
|
||||
public init() {
|
||||
ids = Favorites.load()
|
||||
}
|
||||
|
||||
func contains(_ id: String) -> Bool {
|
||||
return ids.contains(id)
|
||||
@@ -13,24 +15,25 @@ public class Favorites: ObservableObject {
|
||||
func reset() {
|
||||
objectWillChange.send()
|
||||
ids = Set()
|
||||
Favorites.save(ids)
|
||||
save()
|
||||
}
|
||||
|
||||
func add(_ id: String) {
|
||||
objectWillChange.send()
|
||||
ids.insert(id)
|
||||
Favorites.save(ids)
|
||||
save()
|
||||
}
|
||||
|
||||
func remove(_ id: String) {
|
||||
objectWillChange.send()
|
||||
ids.remove(id)
|
||||
Favorites.save(ids)
|
||||
save()
|
||||
}
|
||||
|
||||
private static func save(_ ids: Set<String>) {
|
||||
private func save() {
|
||||
// It's a run-time exception if we pass the `Set` directly here
|
||||
UserDefaults.standard.set(Array(ids), forKey: key)
|
||||
let ids = Array(ids)
|
||||
UserDefaults.standard.set(ids, forKey: Favorites.key)
|
||||
}
|
||||
|
||||
private static func load() -> Set<String> {
|
||||
|
||||
@@ -85,7 +85,7 @@ public struct AppView: View {
|
||||
}
|
||||
case (_, _):
|
||||
iOSNavigationView(model: model) {
|
||||
SessionView(model: SessionViewModel(store: model.store))
|
||||
SessionView(model: SessionViewModel(favorites: model.favorites, store: model.store))
|
||||
}
|
||||
}
|
||||
#elseif os(macOS)
|
||||
|
||||
@@ -24,7 +24,6 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
private var lastShownOthers: [Resource] = []
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
@ObservedObject var favorites: Favorites
|
||||
@ObservedObject var model: SessionViewModel
|
||||
|
||||
private lazy var signedOutIcon = NSImage(named: "MenuBarIconSignedOut")
|
||||
@@ -38,9 +37,8 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
private var connectingAnimationImageIndex: Int = 0
|
||||
private var connectingAnimationTimer: Timer?
|
||||
|
||||
public init(favorites: Favorites, model: SessionViewModel) {
|
||||
public init(model: SessionViewModel) {
|
||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
self.favorites = favorites
|
||||
self.model = model
|
||||
|
||||
super.init()
|
||||
@@ -58,7 +56,7 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
favorites.$ids
|
||||
model.favorites.$ids
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] ids in
|
||||
guard let self = self else { return }
|
||||
@@ -417,23 +415,23 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
|
||||
private func populateResourceMenus(_ newResources: [Resource]) {
|
||||
// If we have no favorites, then everything is a favorite
|
||||
let hasAnyFavorites = newResources.contains { self.favorites.contains($0.id) }
|
||||
let hasAnyFavorites = newResources.contains { model.favorites.contains($0.id) }
|
||||
let newFavorites = if (hasAnyFavorites) {
|
||||
newResources.filter { self.favorites.contains($0.id) }
|
||||
newResources.filter { model.favorites.contains($0.id) }
|
||||
} else {
|
||||
newResources
|
||||
}
|
||||
let newOthers: [Resource] = if hasAnyFavorites {
|
||||
newResources.filter { !self.favorites.contains($0.id) }
|
||||
newResources.filter { !model.favorites.contains($0.id) }
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
populateFavoriteResourcesMenu(newFavorites, hasAnyFavorites)
|
||||
populateFavoriteResourcesMenu(newFavorites)
|
||||
populateOtherResourcesMenu(newOthers)
|
||||
}
|
||||
|
||||
private func populateFavoriteResourcesMenu(_ newFavorites: [Resource], _ hasAnyFavorites: Bool) {
|
||||
private func populateFavoriteResourcesMenu(_ newFavorites: [Resource]) {
|
||||
// Update the menu in place so everything won't vanish if it's open when it updates
|
||||
let diff = (newFavorites).difference(
|
||||
from: lastShownFavorites,
|
||||
@@ -548,7 +546,7 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
resourceAddressItem.target = self
|
||||
subMenu.addItem(resourceAddressItem)
|
||||
|
||||
if favorites.contains(resource.id) {
|
||||
if model.favorites.contains(resource.id) {
|
||||
toggleFavoriteItem.action = #selector(removeFavoriteTapped(_:))
|
||||
toggleFavoriteItem.title = "Remove from favorites"
|
||||
toggleFavoriteItem.toolTip = "Click to remove this Resource from Favorites"
|
||||
@@ -643,9 +641,9 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
|
||||
private func setFavorited(id: String, favorited: Bool) {
|
||||
if favorited {
|
||||
favorites.add(id)
|
||||
model.favorites.add(id)
|
||||
} else {
|
||||
favorites.remove(id)
|
||||
model.favorites.remove(id)
|
||||
}
|
||||
favoritesChanged()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
|
||||
#if os(iOS)
|
||||
struct ResourceView: View {
|
||||
@ObservedObject var model: SessionViewModel
|
||||
var resource: Resource
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
@@ -68,6 +69,28 @@ struct ResourceView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(model.favorites.ids.contains(resource.id)) {
|
||||
Button(action: {
|
||||
model.favorites.remove(resource.id)
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "star")
|
||||
Text("Remove from favorites")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button(action: {
|
||||
model.favorites.add(resource.id)
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "star.fill")
|
||||
Text("Add to favorites")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let site = resource.sites.first {
|
||||
|
||||
@@ -12,17 +12,26 @@ import SwiftUI
|
||||
@MainActor
|
||||
public final class SessionViewModel: ObservableObject {
|
||||
@Published private(set) var actorName: String? = nil
|
||||
@Published private(set) var favorites: Favorites
|
||||
@Published private(set) var resources: ResourceList = ResourceList.loading
|
||||
@Published private(set) var status: NEVPNStatus? = nil
|
||||
|
||||
|
||||
let store: Store
|
||||
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
public init(store: Store) {
|
||||
public init(favorites: Favorites, store: Store) {
|
||||
self.favorites = favorites
|
||||
self.store = store
|
||||
|
||||
favorites.$ids
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] ids in
|
||||
guard let self = self else { return }
|
||||
self.objectWillChange.send()
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
store.$actorName
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] actorName in
|
||||
@@ -72,24 +81,27 @@ struct SessionView: View {
|
||||
if resources.isEmpty {
|
||||
Text("No Resources. Contact your admin to be granted access.")
|
||||
} else {
|
||||
List(resources) { resource in
|
||||
HStack {
|
||||
NavigationLink { ResourceView(resource: resource) }
|
||||
label: {
|
||||
HStack {
|
||||
Text(resource.name)
|
||||
if resource.canBeDisabled {
|
||||
Spacer()
|
||||
Toggle("Enabled", isOn: Binding<Bool>(
|
||||
get: { model.isResourceEnabled(resource.id) },
|
||||
set: { newValue in
|
||||
model.store.toggleResourceDisabled(resource: resource.id, enabled: newValue)
|
||||
}
|
||||
)).labelsHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("All Resources")
|
||||
List {
|
||||
let hasAnyFavorites = resources.contains { model.favorites.contains($0.id) }
|
||||
if hasAnyFavorites {
|
||||
Section("Favorites") {
|
||||
ResourceSection(
|
||||
resources: resources.filter { model.favorites.contains($0.id) },
|
||||
model: model
|
||||
)
|
||||
}
|
||||
|
||||
Section("Other Resources") {
|
||||
ResourceSection(
|
||||
resources: resources.filter { !model.favorites.contains($0.id) },
|
||||
model: model
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ResourceSection(
|
||||
resources: resources,
|
||||
model: model
|
||||
)
|
||||
}
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
@@ -112,4 +124,32 @@ struct SessionView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ResourceSection: View {
|
||||
let resources: [Resource]
|
||||
@ObservedObject var model: SessionViewModel
|
||||
|
||||
var body: some View {
|
||||
ForEach(resources) { resource in
|
||||
HStack {
|
||||
NavigationLink { ResourceView(model: model, resource: resource) }
|
||||
label: {
|
||||
HStack {
|
||||
Text(resource.name)
|
||||
if resource.canBeDisabled {
|
||||
Spacer()
|
||||
Toggle("Enabled", isOn: Binding<Bool>(
|
||||
get: { model.isResourceEnabled(resource.id) },
|
||||
set: { newValue in
|
||||
model.store.toggleResourceDisabled(resource: resource.id, enabled: newValue)
|
||||
}
|
||||
)).labelsHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("All Resources")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function Apple() {
|
||||
Implements glob-like matching of domains for DNS resources.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="6186">
|
||||
macOS: Adds the ability to mark Resources as favorites.
|
||||
Adds the ability to mark Resources as favorites.
|
||||
</ChangeItem>
|
||||
</ul>
|
||||
</Entry> */}
|
||||
|
||||
Reference in New Issue
Block a user