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:
Davide 2024-11-10 20:54:00 +01:00 committed by GitHub
parent 7719630cdd
commit d6ac4cd818
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 167 additions and 24 deletions

View File

@ -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()
}
}

View File

@ -63,7 +63,10 @@ struct ProfileRowView: View, Routable {
}
Spacer()
HStack(spacing: 10.0) {
attributesView
ProfileAttributesView(
attributes: attributes,
isRemoteImportingEnabled: profileManager.isRemoteImportingEnabled
)
ProfileInfoButton(header: header) {
flow?.onEditProfile($0)
}
@ -131,20 +134,14 @@ private extension ProfileRowView {
)
.foregroundStyle(.primary)
}
}
// MARK: - Attributes
private extension ProfileRowView {
var attributesView: some View {
Group {
if isTV {
tvImage
} else if isShared {
sharedImage
}
var attributes: [ProfileAttributesView.Attribute] {
if isTV {
return [.tv]
} else if isShared {
return [.shared]
}
.foregroundStyle(.secondary)
return []
}
var isShared: Bool {
@ -154,14 +151,34 @@ private extension ProfileRowView {
var isTV: Bool {
isShared && profileManager.isAvailableForTV(profileWithId: header.id)
}
var sharedImage: some View {
ThemeImage(profileManager.isRemoteImportingEnabled ? .cloudOn : .cloudOff)
.help(Strings.Modules.General.Rows.shared)
}
var tvImage: some View {
ThemeImage(profileManager.isRemoteImportingEnabled ? .tvOn : .tvOff)
.help(Strings.Modules.General.Rows.appleTv(Strings.Unlocalized.appleTV))
}
}
// MARK: - Previews
#Preview {
let profile: Profile = .mock
let profileManager: ProfileManager = .mock
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()
}

View File

@ -73,7 +73,9 @@ public final class ProfileManager: ObservableObject {
public init(profiles: [Profile]) {
repository = InMemoryProfileRepository(profiles: profiles)
backupRepository = nil
remoteRepositoryBlock = nil
remoteRepositoryBlock = { _ in
InMemoryProfileRepository()
}
mirrorsRemoteRepository = false
processor = nil
self.profiles = []