Add hop list view
This commit is contained in:
@@ -21,7 +21,8 @@ let package = Package(
|
||||
targets: [
|
||||
.target(
|
||||
name: "Home",
|
||||
dependencies: ["UIComponents"]
|
||||
dependencies: ["UIComponents"],
|
||||
path: "Sources"
|
||||
),
|
||||
.testTarget(
|
||||
name: "HomeTests",
|
||||
|
||||
@@ -28,8 +28,7 @@ private extension HomeView {
|
||||
func navbar() -> some View {
|
||||
CustomNavBar(
|
||||
title: "NymVPN".localizedString,
|
||||
leftButton: CustomNavBarButton(type: .empty, action: {}),
|
||||
rightButton: CustomNavBarButton(type: .settingsGear, action: {})
|
||||
rightButton: CustomNavBarButton(type: .settings, action: {})
|
||||
)
|
||||
Spacer()
|
||||
.frame(height: 50)
|
||||
@@ -86,10 +85,10 @@ private extension HomeView {
|
||||
.frame(height: 24)
|
||||
|
||||
VStack {
|
||||
HopButton(country: Country(name: "Germany", code: "de"))
|
||||
HopButton(hopType: .first, country: Country(name: "Germany", code: "de"))
|
||||
Spacer()
|
||||
.frame(height: 24)
|
||||
HopButton(country: Country(name: "Switzerland", code: "ch"))
|
||||
HopButton(hopType: .last, country: Country(name: "Switzerland", code: "ch"))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import SwiftUI
|
||||
import Theme
|
||||
import UIComponents
|
||||
|
||||
public struct HopListView: View {
|
||||
private let viewModel: HopListViewModel
|
||||
|
||||
public init(viewModel: HopListViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack {
|
||||
navbar()
|
||||
Spacer()
|
||||
.frame(height: 24)
|
||||
|
||||
searchView()
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background {
|
||||
NymColor.background
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension HopListView {
|
||||
@ViewBuilder
|
||||
func navbar() -> some View {
|
||||
CustomNavBar(
|
||||
title: viewModel.type.selectHopLocalizedTitle,
|
||||
leftButton: CustomNavBarButton(type: .back, action: {})
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func searchView() -> some View {
|
||||
SearchView(viewModel: SearchViewModel())
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
|
||||
public struct HopListViewModel {
|
||||
public let type: HopType
|
||||
|
||||
public init(type: HopType) {
|
||||
self.type = type
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
|
||||
public enum HopType {
|
||||
case first
|
||||
case last
|
||||
|
||||
var selectHopLocalizedTitle: String {
|
||||
switch self {
|
||||
case .first:
|
||||
"firstHopSelection".localizedString
|
||||
case .last:
|
||||
"lastHopSelection".localizedString
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -5,9 +5,9 @@
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.080",
|
||||
"blue" : "0.443",
|
||||
"green" : "0.357",
|
||||
"red" : "0.384"
|
||||
"blue" : "0x71",
|
||||
"green" : "0x5B",
|
||||
"red" : "0x61"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
@@ -23,9 +23,9 @@
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.160",
|
||||
"blue" : "0.200",
|
||||
"green" : "0.188",
|
||||
"red" : "0.192"
|
||||
"blue" : "0x33",
|
||||
"green" : "0x30",
|
||||
"red" : "0x30"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
||||
+6
-6
@@ -5,9 +5,9 @@
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "31",
|
||||
"green" : "27",
|
||||
"red" : "28"
|
||||
"blue" : "255",
|
||||
"green" : "255",
|
||||
"red" : "255"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
@@ -23,9 +23,9 @@
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "255",
|
||||
"green" : "255",
|
||||
"red" : "255"
|
||||
"blue" : "31",
|
||||
"green" : "27",
|
||||
"red" : "28"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
||||
@@ -122,6 +122,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"firstHopSelection" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "First hop selection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"lastHop" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Last hop"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"lastHopSelection" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Last hop selection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"NymVPN" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -133,6 +166,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"search" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Search"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchCountry" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Search country"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selectNetwork" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ public struct ConnectButton: View {
|
||||
|
||||
public var body: some View {
|
||||
HStack {
|
||||
Text("Connect")
|
||||
Text("connect".localizedString)
|
||||
.foregroundStyle(NymColor.connectTitle)
|
||||
.textStyle(.Label.Huge.primary)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@ import SwiftUI
|
||||
import Theme
|
||||
|
||||
public struct HopButton: View {
|
||||
private let hopType: HopType
|
||||
private let country: Country
|
||||
|
||||
public init(country: Country) {
|
||||
public init(hopType: HopType, country: Country) {
|
||||
self.hopType = hopType
|
||||
self.country = country
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
StrokeBorderView(strokeTitle: hopType.localizedTitle) {
|
||||
HStack {
|
||||
Image(country.code)
|
||||
.resizable()
|
||||
@@ -27,22 +29,6 @@ public struct HopButton: View {
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
.padding(0)
|
||||
.frame(maxWidth: .infinity, minHeight: 56, maxHeight: 56)
|
||||
.cornerRadius(8)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.inset(by: 0.5)
|
||||
.stroke(Color(red: 0.29, green: 0.27, blue: 0.31), lineWidth: 1)
|
||||
}
|
||||
.overlay(alignment: .topLeading) {
|
||||
Text("firstHop".localizedString)
|
||||
.foregroundStyle(NymColor.sysOnSurface)
|
||||
.textStyle(.Body.Small.primary)
|
||||
.padding(4)
|
||||
.background(NymColor.background)
|
||||
.position(x: 40, y: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,3 +41,17 @@ public struct Country {
|
||||
self.code = code
|
||||
}
|
||||
}
|
||||
|
||||
public enum HopType {
|
||||
case first
|
||||
case last
|
||||
|
||||
var localizedTitle: String {
|
||||
switch self {
|
||||
case .first:
|
||||
"firstHop".localizedString
|
||||
case .last:
|
||||
"lastHop".localizedString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ public struct CustomNavBar: View {
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
leftButton: CustomNavBarButton? = nil,
|
||||
rightButton: CustomNavBarButton? = nil
|
||||
leftButton: CustomNavBarButton? = CustomNavBarButton(type: .empty, action: {}),
|
||||
rightButton: CustomNavBarButton? = CustomNavBarButton(type: .empty, action: {})
|
||||
) {
|
||||
self.title = title
|
||||
self.leftButton = leftButton
|
||||
|
||||
+6
-3
@@ -3,13 +3,16 @@ import Theme
|
||||
|
||||
public struct CustomNavBarButton: View {
|
||||
public enum ButtonType: String {
|
||||
case settingsGear
|
||||
case back
|
||||
case settings
|
||||
case empty
|
||||
|
||||
var imageName: String? {
|
||||
switch self {
|
||||
case .settingsGear:
|
||||
self.rawValue
|
||||
case .back:
|
||||
"arrowBack"
|
||||
case .settings:
|
||||
"settingsGear"
|
||||
case .empty:
|
||||
nil
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "arrow_back_24px.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icons/arrow_back_24px">
|
||||
<path id="icon" d="M20 11H7.83L13.42 5.41L12 4L4 12L12 20L13.41 18.59L7.83 13H20V11Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 240 B |
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "search_24px.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icons/search_24px">
|
||||
<path id="icon" fill-rule="evenodd" clip-rule="evenodd" d="M14.76 13.27L20.49 19L19 20.49L13.27 14.76C12.2 15.53 10.91 16 9.5 16C5.91 16 3 13.09 3 9.5C3 5.91 5.91 3 9.5 3C13.09 3 16 5.91 16 9.5C16 10.91 15.53 12.2 14.76 13.27ZM9.5 5C7.01 5 5 7.01 5 9.5C5 11.99 7.01 14 9.5 14C11.99 14 14 11.99 14 9.5C14 7.01 11.99 5 9.5 5Z" fill="#CAC4D0"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 477 B |
@@ -0,0 +1,45 @@
|
||||
import SwiftUI
|
||||
import Theme
|
||||
|
||||
public struct SearchView: View {
|
||||
@ObservedObject private var viewModel: SearchViewModel
|
||||
|
||||
public init(viewModel: SearchViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
StrokeBorderView(strokeTitle: viewModel.strokeTitle) {
|
||||
HStack {
|
||||
searchImage()
|
||||
searchTextfield()
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchView {
|
||||
@ViewBuilder
|
||||
func searchImage() -> some View {
|
||||
Image(viewModel.searchImageName, bundle: .module)
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.cornerRadius(50)
|
||||
.padding(16)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func searchTextfield() -> some View {
|
||||
ZStack(alignment: .leading) {
|
||||
if viewModel.searchText.isEmpty {
|
||||
Text(viewModel.searchCountryTitle)
|
||||
.foregroundStyle(NymColor.sysOutline)
|
||||
.textStyle(.Body.Large.primary)
|
||||
}
|
||||
TextField("", text: $viewModel.searchText)
|
||||
.foregroundStyle(NymColor.sysOnSurface)
|
||||
.textStyle(.Body.Large.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
public final class SearchViewModel: ObservableObject {
|
||||
let strokeTitle = "search".localizedString
|
||||
let searchCountryTitle = "searchCountry".localizedString
|
||||
let searchImageName = "searchIcon"
|
||||
|
||||
@Published var searchText = ""
|
||||
|
||||
public init() {}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import SwiftUI
|
||||
import Theme
|
||||
|
||||
public struct StrokeBorderView<Content: View>: View {
|
||||
@ViewBuilder private let content: Content
|
||||
private let strokeTitle: String
|
||||
|
||||
public init(strokeTitle: String, @ViewBuilder content: () -> Content) {
|
||||
self.strokeTitle = strokeTitle
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
content
|
||||
}
|
||||
.padding(0)
|
||||
.frame(maxWidth: .infinity, minHeight: 56, maxHeight: 56)
|
||||
.cornerRadius(8)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.inset(by: 0.5)
|
||||
.stroke(Color(red: 0.29, green: 0.27, blue: 0.31), lineWidth: 1)
|
||||
}
|
||||
.overlay(alignment: .topLeading) {
|
||||
Text(strokeTitle)
|
||||
.foregroundStyle(NymColor.sysOnSurface)
|
||||
.textStyle(.Body.Small.primary)
|
||||
.padding(4)
|
||||
.background(NymColor.background)
|
||||
.position(x: 40, y: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user