Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PIA-1345: Connection state on top navigation bar #80

Merged
merged 3 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class DashboardFactory {
guard let defaultAccountProvider = Client.providers.accountProvider as? DefaultAccountProvider else {
fatalError("Incorrect account provider type")
}
return DashboardViewModel(accountProvider: defaultAccountProvider, appRouter: AppRouterFactory.makeAppRouter(), navigationDestination: RegionsDestinations.serversList)
return DashboardViewModel(connectionStateMonitor: StateMonitorsFactory.makeConnectionStateMonitor)
}

static func makePIAConnectionButton() -> PIAConnectionButton {
Expand Down
43 changes: 32 additions & 11 deletions PIA VPN-tvOS/Dashboard/Presentation/DashboardViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@


import Foundation
import Combine
import SwiftUI

class DashboardViewModel: ObservableObject {

private let accountProvider: AccountProviderType
private let appRouter: AppRouter
private let navigationDestination: any Destinations
@Published var connectionTitle: String = ""
@Published var connectionTintColor = (titleTint: Color.clear, connectionBarTint: Color.clear)

init(accountProvider: AccountProviderType, appRouter: AppRouter, navigationDestination: any Destinations) {
self.accountProvider = accountProvider
self.appRouter = appRouter
self.navigationDestination = navigationDestination
}
private let connectionStateMonitor: ConnectionStateMonitorType
private var cancellables = Set<AnyCancellable>()

init(connectionStateMonitor: ConnectionStateMonitorType) {
self.connectionStateMonitor = connectionStateMonitor
subscribeToConnectionStateUpdates()
}

// TODO: Remove this functionality from Dashboard when we have it on the settings menu
func logOut() {
accountProvider.logout(nil)
private func subscribeToConnectionStateUpdates() {
connectionStateMonitor.connectionStatePublisher
.sink { [weak self] newConnectionState in
guard let self else { return }
self.connectionTitle = newConnectionState.title ?? ""
self.connectionTintColor = self.getTintColor(for: newConnectionState)
}.store(in: &cancellables)
}

internal func getTintColor(for connectionState: ConnectionState) -> (titleTint: Color, connectionBarTint: Color) {
switch connectionState {
case .connecting, .disconnecting:
return (titleTint: .pia_yellow_dark, connectionBarTint: .pia_yellow_dark)
case .connected:
return (titleTint: .pia_primary, connectionBarTint: .pia_primary)
case .disconnected:
return (titleTint: .pia_on_surface, connectionBarTint: .clear)
case .error(let error):
return (titleTint: .pia_red, connectionBarTint: .pia_red)
default:
return (.clear, .clear)
}
}

}
39 changes: 31 additions & 8 deletions PIA VPN-tvOS/Dashboard/UI/DashboardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,31 @@ struct DashboardView: View {
@ObservedObject var viewModel: DashboardViewModel

var body: some View {
VStack(alignment: .leading) {
VStack {
ConnectionStateBar(tintColor: viewModel.connectionTintColor.connectionBarTint)

DashboardConnectionButtonSection()
.padding(.bottom, 80)

SelectedServerSection()
.padding(.bottom, 40)

QuickConnectSection()
.frame(width: Spacing.dashboardViewWidth)

}
.frame(width: Spacing.dashboardViewWidth)

.withTopNavigationBarAndTitleView {
// View for the Title section of the Navigation bar
ConnectionStateTitle(title: viewModel.connectionTitle, tintColor: viewModel.connectionTintColor.titleTint)
}
}
}

// MARK: Dashboard sections

fileprivate struct DashboardConnectionButtonSection: View {
var body: some View {
HStack {
Spacer()
DashboardFactory.makePIAConnectionButton()
Spacer()
}
DashboardFactory.makePIAConnectionButton()
}
}

Expand All @@ -45,6 +46,28 @@ fileprivate struct QuickConnectSection: View {
}
}

fileprivate struct ConnectionStateTitle: View {
let title: String
let tintColor: Color

var body: some View {
Text(title)
.font(.system(size: 57, weight: .bold))
.foregroundColor(tintColor)
}
}


fileprivate struct ConnectionStateBar: View {
let tintColor: Color

var body: some View {
Rectangle().fill(tintColor)
.frame(width: Spacing.screenWidth, height: 10)
.edgesIgnoringSafeArea(.all)
}
}

#Preview {
DashboardView(
viewModel: DashboardFactory.makeDashboardViewModel())
Expand Down
29 changes: 17 additions & 12 deletions PIA VPN-tvOS/Dashboard/UI/PIAConnectionButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,25 @@ struct PIAConnectionButton: View {
Button {
viewModel.toggleConnection()
} label: {
ZStack {
animatedRing(with: viewModel.tintColor)
Circle()
.fill(Color.pia_background)
.frame(width: size - lineWidth)

if !viewModel.animating {
connectionStatusOuterRing()
HStack {
Spacer()
ZStack {
animatedRing(with: viewModel.tintColor)
Circle()
.fill(Color.pia_background)
.frame(width: size - lineWidth)

if !viewModel.animating {
connectionStatusOuterRing()
}
connectionStatusInnerImage()
}
connectionStatusInnerImage()
.frame(width: size + 40, height: size + 40)
.scaleEffect(isFocused ? 1.15 : 1)
.animation(.easeOut, value: isFocused)
Spacer()
}
.frame(width: size + 40, height: size + 40)
.scaleEffect(isFocused ? 1.15 : 1)
.animation(.easeOut, value: isFocused)
.frame(width: Spacing.screenWidth)

}
.focused($isFocused)
Expand Down
18 changes: 14 additions & 4 deletions PIA VPN-tvOS/Dashboard/UI/SelectedServerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,28 @@ struct SelectedServerView: View {
.foregroundColor(isButtonFocused ? .pia_on_primary : .pia_on_surface)
.frame(width: 52)
}
.padding(.leading, 30)
.padding(.horizontal, 40)
.padding(.vertical, 16)

}

var body: some View {
VStack {
Button {
viewModel.selectedServerSectionWasTapped()
} label: {
buttonView()
HStack {
Spacer()
buttonView()
.frame(width: Spacing.dashboardViewWidth)
.background(isButtonFocused ? Color.pia_primary : Color.pia_surface_container_primary)
.clipShape(RoundedRectangle(cornerSize: Spacing.tileCornerSize))
Spacer()
}
.frame(width: Spacing.screenWidth)

}
.background(isButtonFocused ? Color.pia_primary : Color.pia_surface_container_primary)
.clipShape(RoundedRectangle(cornerSize: Spacing.tileCornerSize))

.buttonStyle(BasicButtonStyle())
.focused($isButtonFocused)
.buttonBorderShape(.roundedRectangle(radius: Spacing.tileBorderRadius))
Expand Down
21 changes: 18 additions & 3 deletions PIA VPN-tvOS/Navigation/CompositionRoot/TopNavigationFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,30 @@ import Foundation
class TopNavigationFactory {

static func makeLeadingSegmentedNavigationView() -> LeadingSegmentedNavigationView {
return LeadingSegmentedNavigationView(viewModel: makeTopNavigationViewModel())
return leadingNavigationViewShared
}

static func makeTrailingNavigationView() -> TrailingNavigationView {
return TrailingNavigationView(viewModel: makeTopNavigationViewModel())
return trailingNavigationViewShared
}

static func makeTopNavigationViewModel() -> TopNavigationViewModel {
return TopNavigationViewModel(appRouter: AppRouterFactory.makeAppRouter())
return topNavigationViewModelShared
}


// MARK: - Private

private static var topNavigationViewModelShared: TopNavigationViewModel = {
TopNavigationViewModel(appRouter: AppRouterFactory.makeAppRouter())
}()

private static var leadingNavigationViewShared: LeadingSegmentedNavigationView = {
LeadingSegmentedNavigationView(viewModel: makeTopNavigationViewModel())
}()

private static var trailingNavigationViewShared: TrailingNavigationView = {
TrailingNavigationView(viewModel: makeTopNavigationViewModel())
}()

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ class TopNavigationViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()

let leadingSections: [Sections] = [.vpn, .locations]
let trailingSections: [Sections] = [.settings, .help]

// TODO: Add 'help' section in the trailing sections when we implement it
let trailingSections: [Sections] = [.settings]

enum Sections: Equatable, Hashable, Identifiable {
var id: Self {
Expand Down
47 changes: 47 additions & 0 deletions PIA VPN-tvOS/Navigation/UI/TopNavigationView+ViewModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// TopNavigationView+ViewModifier.swift
// PIA VPN-tvOS
//
// Created by Laura S on 2/14/24.
// Copyright © 2024 Private Internet Access Inc. All rights reserved.
//

import Foundation
import SwiftUI

struct TopNavigationBarAndTitleViewViewModifier: ViewModifier {
var titleView: any View

func body(content: Content) -> some View {
content
.navigationBarItems(leading: TopNavigationFactory.makeLeadingSegmentedNavigationView(), trailing: TopNavigationFactory.makeTrailingNavigationView())
.toolbar {
ToolbarItem(placement: .principal) {
AnyView(titleView)
}
}

}
}

struct TopNavigationBarViewModifier: ViewModifier {
var titleText: String?

func body(content: Content) -> some View {
content
.navigationBarItems(leading: TopNavigationFactory.makeLeadingSegmentedNavigationView(), trailing: TopNavigationFactory.makeTrailingNavigationView())
.navigationTitle(Text(titleText ?? ""))

}
}


extension View {
func withTopNavigationBarAndTitleView(titleView: @escaping () -> some View) -> some View {
modifier(TopNavigationBarAndTitleViewViewModifier(titleView: titleView()))
}

func withTopNavigationBar(with title: String?) -> some View {
modifier(TopNavigationBarViewModifier(titleText: title))
}
}
2 changes: 1 addition & 1 deletion PIA VPN-tvOS/Navigation/UI/TopNavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ struct TrailingNavigationView: View {
}
.focused($focusedSection, equals: section)
.buttonBorderShape(.circle)
.buttonStyle(.card)
.buttonStyle(BasicButtonStyle())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,12 @@ struct UserActivatedContainerView: View {
case .serversList:
RegionsSelectionFactory.makeRegionsContainerView()
.padding(.top, Spacing.screenTopPadding)
// TODO: Check .toolbar() to add the navigation bar because
// these APIs will be deprecated
.navigationBarItems(leading: TopNavigationFactory.makeLeadingSegmentedNavigationView())
.navigationTitle(L10n.Localizable.TopNavigationBar.LocationSelectionScreen.title)
.navigationBarItems(trailing: TopNavigationFactory.makeTrailingNavigationView())
.withTopNavigationBar(with: L10n.Localizable.TopNavigationBar.LocationSelectionScreen.title)
case .search:
RegionsSelectionFactory.makeSearchRegionsListView()
.navigationBarHidden(true)
}
}
.navigationBarItems(leading: TopNavigationFactory.makeLeadingSegmentedNavigationView())
.navigationBarItems(trailing: TopNavigationFactory.makeTrailingNavigationView())
.navigationTitle("") // TODO: Inject the VPN connection status here

}

Expand Down
Loading
Loading