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()
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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 = []
|
||||
|
|
Loading…
Reference in New Issue