mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 19:25:20 +00:00
Liquid Glass theme with in-app appearance switcher (#1337)
* Centralize UI theme colors into semantic ThemePalette tokens Introduce AppTheme/ThemePalette (Utils/Theme.swift) with an environment key and @ThemedPalette property wrapper, persisted via @AppStorage. Views now resolve background/primary/secondary/accentBlue/alertRed/ divider tokens from the environment instead of computing colors inline, removing the backgroundColor/textColor/secondaryTextColor prop-drilling through the header, composer, and people-sheet hierarchy. Matrix theme output is pixel-identical; this is groundwork for a user-selectable theme switcher. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add liquid glass theme with in-app appearance switcher Add a .liquidGlass AppTheme alongside the matrix terminal theme, selectable from a one-line APPEARANCE row in the app info sheet (persisted via @AppStorage, applies live). Liquid glass renders system fonts and colors over a subtle gradient backdrop, with the header and composer floating as Liquid Glass panels (real .glassEffect() on iOS/macOS 26, compiler-gated with an ultraThinMaterial fallback for older SDKs). The message list scrolls underneath the chrome via safe-area insets, and all sheets share the same backdrop and surface language. The matrix theme is unchanged. Details: - Theme is threaded through ChatMessageFormatter so message AttributedStrings switch font design per theme; the per-message format cache gains a variant key so themes never serve each other's cached strings - New palette tokens: accent (interactive tint) and locationAccent (geohash green), replacing scattered hardcoded greens/blues in the voice note, waveform, verification, and people-sheet views - Header controls get full-height tap targets and the people count becomes a real Button (previously a tap gesture on the cluster) - CommandSuggestionsView renders nothing when empty instead of a zero-height view that pushed the composer input off-center - New appearance strings localized for all 29 catalog languages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
jack
Claude Fable 5
parent
c8381737bb
commit
af954b05ea
@@ -8,8 +8,9 @@ struct ContentHeaderView: View {
|
||||
@EnvironmentObject private var verificationModel: VerificationModel
|
||||
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
|
||||
@EnvironmentObject private var peerListModel: PeerListModel
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||
@Environment(\.appTheme) private var theme
|
||||
@ThemedPalette private var palette
|
||||
|
||||
@Binding var showSidebar: Bool
|
||||
@Binding var showVerifySheet: Bool
|
||||
@@ -20,15 +21,12 @@ struct ContentHeaderView: View {
|
||||
let headerHeight: CGFloat
|
||||
let headerPeerIconSize: CGFloat
|
||||
let headerPeerCountFontSize: CGFloat
|
||||
let backgroundColor: Color
|
||||
let textColor: Color
|
||||
let secondaryTextColor: Color
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Text(verbatim: "bitchat/")
|
||||
.font(.bitchatSystem(size: 18, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(textColor)
|
||||
.bitchatFont(size: 18, weight: .medium)
|
||||
.foregroundColor(palette.primary)
|
||||
.onTapGesture(count: 3) {
|
||||
appChromeModel.panicClearAllData()
|
||||
}
|
||||
@@ -38,8 +36,8 @@ struct ContentHeaderView: View {
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text(verbatim: "@")
|
||||
.font(.bitchatSystem(size: 14, design: .monospaced))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
.bitchatFont(size: 14)
|
||||
.foregroundColor(palette.secondary)
|
||||
|
||||
TextField(
|
||||
"content.input.nickname_placeholder",
|
||||
@@ -49,9 +47,9 @@ struct ContentHeaderView: View {
|
||||
)
|
||||
)
|
||||
.textFieldStyle(.plain)
|
||||
.font(.bitchatSystem(size: 14, design: .monospaced))
|
||||
.bitchatFont(size: 14)
|
||||
.frame(maxWidth: 80)
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(palette.primary)
|
||||
.focused(isNicknameFieldFocused)
|
||||
.autocorrectionDisabled(true)
|
||||
#if os(iOS)
|
||||
@@ -79,12 +77,13 @@ struct ContentHeaderView: View {
|
||||
return countAndColor.0
|
||||
}()
|
||||
|
||||
HStack(spacing: 10) {
|
||||
HStack(spacing: 2) {
|
||||
if appChromeModel.hasUnreadPrivateMessages {
|
||||
Button(action: { appChromeModel.openMostRelevantPrivateChat() }) {
|
||||
Image(systemName: "envelope.fill")
|
||||
.font(.bitchatSystem(size: 12))
|
||||
.foregroundColor(Color.orange)
|
||||
.headerTapTarget()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(
|
||||
@@ -99,13 +98,10 @@ struct ContentHeaderView: View {
|
||||
notesGeohash = locationChannelsModel.currentBuildingGeohash
|
||||
showLocationNotes = true
|
||||
}) {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
Image(systemName: "note.text")
|
||||
.font(.bitchatSystem(size: 12))
|
||||
.foregroundColor(Color.orange.opacity(0.8))
|
||||
.padding(.top, 1)
|
||||
}
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
Image(systemName: "note.text")
|
||||
.font(.bitchatSystem(size: 12))
|
||||
.foregroundColor(Color.orange.opacity(0.8))
|
||||
.headerTapTarget()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(
|
||||
@@ -117,6 +113,7 @@ struct ContentHeaderView: View {
|
||||
Button(action: { locationChannelsModel.toggleBookmark(channel.geohash) }) {
|
||||
Image(systemName: locationChannelsModel.isBookmarked(channel.geohash) ? "bookmark.fill" : "bookmark")
|
||||
.font(.bitchatSystem(size: 12))
|
||||
.headerTapTarget()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(
|
||||
@@ -140,49 +137,54 @@ struct ContentHeaderView: View {
|
||||
case .mesh:
|
||||
return Color(hue: 0.60, saturation: 0.85, brightness: 0.82)
|
||||
case .location:
|
||||
return colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||
return palette.locationAccent
|
||||
}
|
||||
}()
|
||||
|
||||
Text(badgeText)
|
||||
.font(.bitchatSystem(size: 14, design: .monospaced))
|
||||
.bitchatFont(size: 14)
|
||||
.foregroundColor(badgeColor)
|
||||
.lineLimit(headerLineLimit)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
.layoutPriority(2)
|
||||
.padding(.horizontal, 6)
|
||||
.frame(maxHeight: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityLabel(
|
||||
String(localized: "content.accessibility.location_channels", comment: "Accessibility label for the location channels button")
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.leading, 4)
|
||||
.padding(.trailing, 2)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "person.2.fill")
|
||||
.font(.system(size: headerPeerIconSize, weight: .regular))
|
||||
.accessibilityLabel(
|
||||
String(
|
||||
format: String(localized: "content.accessibility.people_count", comment: "Accessibility label announcing number of people in header"),
|
||||
locale: .current,
|
||||
headerOtherPeersCount
|
||||
)
|
||||
)
|
||||
Text("\(headerOtherPeersCount)")
|
||||
.font(.system(size: headerPeerCountFontSize, weight: .regular, design: .monospaced))
|
||||
.accessibilityHidden(true)
|
||||
Button(action: {
|
||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
||||
showSidebar.toggle()
|
||||
}
|
||||
}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "person.2.fill")
|
||||
.font(.system(size: headerPeerIconSize, weight: .regular))
|
||||
Text("\(headerOtherPeersCount)")
|
||||
.font(.system(size: headerPeerCountFontSize, weight: .regular, design: theme.bodyFontDesign))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.foregroundColor(headerCountColor)
|
||||
.lineLimit(headerLineLimit)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
.padding(.leading, 6)
|
||||
.frame(maxHeight: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.foregroundColor(headerCountColor)
|
||||
.padding(.leading, 2)
|
||||
.lineLimit(headerLineLimit)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(
|
||||
String(
|
||||
format: String(localized: "content.accessibility.people_count", comment: "Accessibility label announcing number of people in header"),
|
||||
locale: .current,
|
||||
headerOtherPeersCount
|
||||
)
|
||||
)
|
||||
}
|
||||
.layoutPriority(3)
|
||||
.onTapGesture {
|
||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
||||
showSidebar.toggle()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showVerifySheet) {
|
||||
VerificationSheetView(isPresented: $showVerifySheet)
|
||||
.environmentObject(verificationModel)
|
||||
@@ -208,10 +210,7 @@ struct ContentHeaderView: View {
|
||||
} else {
|
||||
ContentLocationNotesUnavailableView(
|
||||
showLocationNotes: $showLocationNotes,
|
||||
headerHeight: headerHeight,
|
||||
backgroundColor: backgroundColor,
|
||||
textColor: textColor,
|
||||
secondaryTextColor: secondaryTextColor
|
||||
headerHeight: headerHeight
|
||||
)
|
||||
.environmentObject(locationChannelsModel)
|
||||
}
|
||||
@@ -249,7 +248,16 @@ struct ContentHeaderView: View {
|
||||
} message: {
|
||||
Text("content.alert.screenshot.message")
|
||||
}
|
||||
.background(backgroundColor.opacity(0.95))
|
||||
.themedChromePanel(edge: .top)
|
||||
}
|
||||
}
|
||||
|
||||
private extension View {
|
||||
/// Expands a small header icon to a comfortably tappable, full-bar-height
|
||||
/// hit area without changing its visual size.
|
||||
func headerTapTarget() -> some View {
|
||||
frame(minWidth: 30, maxHeight: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,8 +270,7 @@ private extension ContentHeaderView {
|
||||
switch locationChannelsModel.selectedChannel {
|
||||
case .location:
|
||||
let count = peerListModel.visibleGeohashPeerCount
|
||||
let standardGreen = colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||
return (count, count > 0 ? standardGreen : Color.secondary)
|
||||
return (count, count > 0 ? palette.locationAccent : Color.secondary)
|
||||
case .mesh:
|
||||
let meshBlue = Color(hue: 0.60, saturation: 0.85, brightness: 0.82)
|
||||
let color: Color = peerListModel.connectedMeshPeerCount > 0 ? meshBlue : Color.secondary
|
||||
@@ -274,24 +281,22 @@ private extension ContentHeaderView {
|
||||
|
||||
private struct ContentLocationNotesUnavailableView: View {
|
||||
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
|
||||
@ThemedPalette private var palette
|
||||
|
||||
@Binding var showLocationNotes: Bool
|
||||
|
||||
let headerHeight: CGFloat
|
||||
let backgroundColor: Color
|
||||
let textColor: Color
|
||||
let secondaryTextColor: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
HStack {
|
||||
Text("content.notes.title")
|
||||
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
|
||||
.bitchatFont(size: 16, weight: .bold)
|
||||
Spacer()
|
||||
Button(action: { showLocationNotes = false }) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
|
||||
.foregroundColor(textColor)
|
||||
.bitchatFont(size: 13, weight: .semibold)
|
||||
.foregroundColor(palette.primary)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -299,17 +304,17 @@ private struct ContentLocationNotesUnavailableView: View {
|
||||
}
|
||||
.frame(height: headerHeight)
|
||||
.padding(.horizontal, 12)
|
||||
.background(backgroundColor.opacity(0.95))
|
||||
.themedChromePanel(edge: .top)
|
||||
Text("content.notes.location_unavailable")
|
||||
.font(.bitchatSystem(size: 14, design: .monospaced))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
.bitchatFont(size: 14)
|
||||
.foregroundColor(palette.secondary)
|
||||
Button("content.location.enable") {
|
||||
locationChannelsModel.enableAndRefresh()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
Spacer()
|
||||
}
|
||||
.background(backgroundColor)
|
||||
.foregroundColor(textColor)
|
||||
.themedSheetBackground()
|
||||
.foregroundColor(palette.primary)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user