mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-02-16 12:52:11 +00:00
Use hidden icons for stable alignment (#844)
Align SF Symbols to the text baseline, but also include all possible icons in ProfileAttributesView to precalculate a stable height for the HStack.
This commit is contained in:
parent
7719630cdd
commit
d6ac4cd818
@ -0,0 +1,124 @@
|
|||||||
|
//
|
||||||
|
// ProfileAttributesView.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 11/10/24.
|
||||||
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of Passepartout.
|
||||||
|
//
|
||||||
|
// Passepartout is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Passepartout is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ProfileAttributesView: View {
|
||||||
|
enum Attribute {
|
||||||
|
case shared
|
||||||
|
|
||||||
|
case tv
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributes: [Attribute]
|
||||||
|
|
||||||
|
let isRemoteImportingEnabled: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if !attributes.isEmpty {
|
||||||
|
ZStack(alignment: .centerFirstTextBaseline) {
|
||||||
|
Group {
|
||||||
|
ThemeImage(.cloudOn)
|
||||||
|
ThemeImage(.cloudOff)
|
||||||
|
ThemeImage(.tvOn)
|
||||||
|
ThemeImage(.tvOff)
|
||||||
|
}
|
||||||
|
.hidden()
|
||||||
|
|
||||||
|
HStack(alignment: .firstTextBaseline) {
|
||||||
|
ForEach(imageModels, id: \.name) {
|
||||||
|
ThemeImage($0.name)
|
||||||
|
.help($0.help)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageModels: [(name: Theme.ImageName, help: String)] {
|
||||||
|
attributes.map {
|
||||||
|
switch $0 {
|
||||||
|
case .shared:
|
||||||
|
return (
|
||||||
|
isRemoteImportingEnabled ? .cloudOn : .cloudOff,
|
||||||
|
Strings.Modules.General.Rows.shared
|
||||||
|
)
|
||||||
|
|
||||||
|
case .tv:
|
||||||
|
return (
|
||||||
|
isRemoteImportingEnabled ? .tvOn : .tvOff,
|
||||||
|
Strings.Modules.General.Rows.appleTv(Strings.Unlocalized.appleTV)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
struct ContentView: View {
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var isRemoteImportingEnabled = false
|
||||||
|
|
||||||
|
let timer = Timer.publish(every: 1, on: .main, in: .common)
|
||||||
|
.autoconnect()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ProfileAttributesView(
|
||||||
|
attributes: [.shared, .tv],
|
||||||
|
isRemoteImportingEnabled: isRemoteImportingEnabled
|
||||||
|
)
|
||||||
|
.onReceive(timer) { _ in
|
||||||
|
isRemoteImportingEnabled.toggle()
|
||||||
|
}
|
||||||
|
.border(.black)
|
||||||
|
.padding()
|
||||||
|
.withMockEnvironment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ContentView()
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Row Alignment") {
|
||||||
|
IconsPreview()
|
||||||
|
.withMockEnvironment()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IconsPreview: View {
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
HStack(alignment: .firstTextBaseline) {
|
||||||
|
ThemeImage(.cloudOn)
|
||||||
|
ThemeImage(.cloudOff)
|
||||||
|
ThemeImage(.tvOn)
|
||||||
|
ThemeImage(.tvOff)
|
||||||
|
ThemeImage(.info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.themeForm()
|
||||||
|
}
|
||||||
|
}
|
@ -63,7 +63,10 @@ struct ProfileRowView: View, Routable {
|
|||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
HStack(spacing: 10.0) {
|
HStack(spacing: 10.0) {
|
||||||
attributesView
|
ProfileAttributesView(
|
||||||
|
attributes: attributes,
|
||||||
|
isRemoteImportingEnabled: profileManager.isRemoteImportingEnabled
|
||||||
|
)
|
||||||
ProfileInfoButton(header: header) {
|
ProfileInfoButton(header: header) {
|
||||||
flow?.onEditProfile($0)
|
flow?.onEditProfile($0)
|
||||||
}
|
}
|
||||||
@ -131,20 +134,14 @@ private extension ProfileRowView {
|
|||||||
)
|
)
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Attributes
|
var attributes: [ProfileAttributesView.Attribute] {
|
||||||
|
if isTV {
|
||||||
private extension ProfileRowView {
|
return [.tv]
|
||||||
var attributesView: some View {
|
} else if isShared {
|
||||||
Group {
|
return [.shared]
|
||||||
if isTV {
|
|
||||||
tvImage
|
|
||||||
} else if isShared {
|
|
||||||
sharedImage
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.foregroundStyle(.secondary)
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
var isShared: Bool {
|
var isShared: Bool {
|
||||||
@ -154,14 +151,34 @@ private extension ProfileRowView {
|
|||||||
var isTV: Bool {
|
var isTV: Bool {
|
||||||
isShared && profileManager.isAvailableForTV(profileWithId: header.id)
|
isShared && profileManager.isAvailableForTV(profileWithId: header.id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
var sharedImage: some View {
|
|
||||||
ThemeImage(profileManager.isRemoteImportingEnabled ? .cloudOn : .cloudOff)
|
// MARK: - Previews
|
||||||
.help(Strings.Modules.General.Rows.shared)
|
|
||||||
}
|
#Preview {
|
||||||
|
let profile: Profile = .mock
|
||||||
var tvImage: some View {
|
let profileManager: ProfileManager = .mock
|
||||||
ThemeImage(profileManager.isRemoteImportingEnabled ? .tvOn : .tvOff)
|
|
||||||
.help(Strings.Modules.General.Rows.appleTv(Strings.Unlocalized.appleTV))
|
return Form {
|
||||||
}
|
ProfileRowView(
|
||||||
|
style: .compact,
|
||||||
|
profileManager: profileManager,
|
||||||
|
tunnel: .mock,
|
||||||
|
header: profile.header(),
|
||||||
|
interactiveManager: InteractiveManager(),
|
||||||
|
errorHandler: .default(),
|
||||||
|
nextProfileId: .constant(nil),
|
||||||
|
withMarker: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
do {
|
||||||
|
try await profileManager.observeRemote(true)
|
||||||
|
try await profileManager.save(profile, force: true, remotelyShared: true)
|
||||||
|
} catch {
|
||||||
|
fatalError(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.themeForm()
|
||||||
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,9 @@ public final class ProfileManager: ObservableObject {
|
|||||||
public init(profiles: [Profile]) {
|
public init(profiles: [Profile]) {
|
||||||
repository = InMemoryProfileRepository(profiles: profiles)
|
repository = InMemoryProfileRepository(profiles: profiles)
|
||||||
backupRepository = nil
|
backupRepository = nil
|
||||||
remoteRepositoryBlock = nil
|
remoteRepositoryBlock = { _ in
|
||||||
|
InMemoryProfileRepository()
|
||||||
|
}
|
||||||
mirrorsRemoteRepository = false
|
mirrorsRemoteRepository = false
|
||||||
processor = nil
|
processor = nil
|
||||||
self.profiles = []
|
self.profiles = []
|
||||||
|
Loading…
Reference in New Issue
Block a user