mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
Refresh typography and channel sheet styling
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
MARKETING_VERSION = 1.4.2
|
MARKETING_VERSION = 1.4.3
|
||||||
CURRENT_PROJECT_VERSION = 1
|
CURRENT_PROJECT_VERSION = 1
|
||||||
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0
|
||||||
|
|||||||
@@ -20,15 +20,19 @@ private extension Font.TextStyle {
|
|||||||
return .caption2
|
return .caption2
|
||||||
case ..<13.0:
|
case ..<13.0:
|
||||||
return .caption
|
return .caption
|
||||||
case ..<14.0:
|
case ..<13.75:
|
||||||
return .footnote
|
return .footnote
|
||||||
case ..<16.5:
|
case ..<15.5:
|
||||||
|
return .subheadline
|
||||||
|
case ..<17.5:
|
||||||
|
return .callout
|
||||||
|
case ..<19.5:
|
||||||
return .body
|
return .body
|
||||||
case ..<19.0:
|
case ..<22.5:
|
||||||
return .title3
|
return .title3
|
||||||
case ..<23.0:
|
case ..<27.5:
|
||||||
return .title2
|
return .title2
|
||||||
case ..<30.0:
|
case ..<34.0:
|
||||||
return .title
|
return .title
|
||||||
default:
|
default:
|
||||||
return .largeTitle
|
return .largeTitle
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ struct ContentView: View {
|
|||||||
@State private var notesGeohash: String? = nil
|
@State private var notesGeohash: String? = nil
|
||||||
@State private var sheetNotesCount: Int = 0
|
@State private var sheetNotesCount: Int = 0
|
||||||
@ScaledMetric(relativeTo: .body) private var headerHeight: CGFloat = 44
|
@ScaledMetric(relativeTo: .body) private var headerHeight: CGFloat = 44
|
||||||
|
@ScaledMetric(relativeTo: .subheadline) private var headerPeerIconSize: CGFloat = 11
|
||||||
|
@ScaledMetric(relativeTo: .subheadline) private var headerPeerCountFontSize: CGFloat = 12
|
||||||
// Timer-based refresh removed; use LocationChannelManager live updates instead
|
// Timer-based refresh removed; use LocationChannelManager live updates instead
|
||||||
// Window sizes for rendering (infinite scroll up)
|
// Window sizes for rendering (infinite scroll up)
|
||||||
@State private var windowCountPublic: Int = 300
|
@State private var windowCountPublic: Int = 300
|
||||||
@@ -1216,10 +1218,10 @@ struct ContentView: View {
|
|||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
// People icon with count
|
// People icon with count
|
||||||
Image(systemName: "person.2.fill")
|
Image(systemName: "person.2.fill")
|
||||||
.font(.bitchatSystem(size: 11))
|
.font(.system(size: headerPeerIconSize, weight: .regular))
|
||||||
.accessibilityLabel("\(headerOtherPeersCount) people")
|
.accessibilityLabel("\(headerOtherPeersCount) people")
|
||||||
Text("\(headerOtherPeersCount)")
|
Text("\(headerOtherPeersCount)")
|
||||||
.font(.bitchatSystem(size: 12, design: .monospaced))
|
.font(.system(size: headerPeerCountFontSize, weight: .regular, design: .monospaced))
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
.foregroundColor(headerCountColor)
|
.foregroundColor(headerCountColor)
|
||||||
|
|||||||
@@ -107,188 +107,212 @@ struct LocationChannelsSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var channelList: some View {
|
private var channelList: some View {
|
||||||
List {
|
ScrollView {
|
||||||
// Mesh option first (no bookmark)
|
LazyVStack(spacing: 0) {
|
||||||
channelRow(title: meshTitleWithCount(), subtitlePrefix: "#bluetooth • \(bluetoothRangeString())", isSelected: isMeshSelected, titleColor: standardBlue, titleBold: meshCount() > 0) {
|
channelRow(title: meshTitleWithCount(), subtitlePrefix: "#bluetooth • \(bluetoothRangeString())", isSelected: isMeshSelected, titleColor: standardBlue, titleBold: meshCount() > 0) {
|
||||||
manager.select(ChannelID.mesh)
|
manager.select(ChannelID.mesh)
|
||||||
isPresented = false
|
isPresented = false
|
||||||
}
|
}
|
||||||
.listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0))
|
.padding(.vertical, 6)
|
||||||
|
|
||||||
// Nearby options
|
let nearby = manager.availableChannels.filter { $0.level != .building }
|
||||||
if !manager.availableChannels.isEmpty {
|
if !nearby.isEmpty {
|
||||||
ForEach(manager.availableChannels.filter { $0.level != .building }) { channel in
|
ForEach(nearby) { channel in
|
||||||
let coverage = coverageString(forPrecision: channel.geohash.count)
|
sectionDivider
|
||||||
let nameBase = locationName(for: channel.level)
|
let coverage = coverageString(forPrecision: channel.geohash.count)
|
||||||
let namePart = nameBase.map { formattedNamePrefix(for: channel.level) + $0 }
|
let nameBase = locationName(for: channel.level)
|
||||||
let subtitlePrefix = "#\(channel.geohash) • \(coverage)"
|
let namePart = nameBase.map { formattedNamePrefix(for: channel.level) + $0 }
|
||||||
let highlight = viewModel.geohashParticipantCount(for: channel.geohash) > 0
|
let subtitlePrefix = "#\(channel.geohash) • \(coverage)"
|
||||||
|
let highlight = viewModel.geohashParticipantCount(for: channel.geohash) > 0
|
||||||
|
channelRow(
|
||||||
|
title: geohashTitleWithCount(for: channel),
|
||||||
|
subtitlePrefix: subtitlePrefix,
|
||||||
|
subtitleName: namePart,
|
||||||
|
isSelected: isSelected(channel),
|
||||||
|
titleBold: highlight,
|
||||||
|
trailingAccessory: {
|
||||||
|
Button(action: { bookmarks.toggle(channel.geohash) }) {
|
||||||
|
Image(systemName: bookmarks.isBookmarked(channel.geohash) ? "bookmark.fill" : "bookmark")
|
||||||
|
.font(.bitchatSystem(size: 14))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.padding(.leading, 8)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
manager.markTeleported(for: channel.geohash, false)
|
||||||
|
manager.select(ChannelID.location(channel))
|
||||||
|
isPresented = false
|
||||||
|
}
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sectionDivider
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
ProgressView()
|
||||||
|
Text("finding nearby channels…")
|
||||||
|
.font(.bitchatSystem(size: 12, design: .monospaced))
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionDivider
|
||||||
|
customTeleportSection
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
|
let bookmarkedList = bookmarks.bookmarks
|
||||||
|
if !bookmarkedList.isEmpty {
|
||||||
|
sectionDivider
|
||||||
|
bookmarkedSection(bookmarkedList)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager.permissionState == LocationChannelManager.PermissionState.authorized {
|
||||||
|
sectionDivider
|
||||||
|
torToggleSection
|
||||||
|
.padding(.top, 12)
|
||||||
|
Button(action: {
|
||||||
|
openSystemLocationSettings()
|
||||||
|
}) {
|
||||||
|
Text("remove location access")
|
||||||
|
.font(.bitchatSystem(size: 12, design: .monospaced))
|
||||||
|
.foregroundColor(Color(red: 0.75, green: 0.1, blue: 0.1))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(Color.red.opacity(0.08))
|
||||||
|
.cornerRadius(6)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(backgroundColor)
|
||||||
|
}
|
||||||
|
.background(backgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sectionDivider: some View {
|
||||||
|
Rectangle()
|
||||||
|
.fill(dividerColor)
|
||||||
|
.frame(height: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dividerColor: Color {
|
||||||
|
colorScheme == .dark ? Color.white.opacity(0.12) : Color.black.opacity(0.08)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var customTeleportSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
Text("#")
|
||||||
|
.font(.bitchatSystem(size: 14, design: .monospaced))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
TextField("geohash", text: $customGeohash)
|
||||||
|
#if os(iOS)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
.autocorrectionDisabled(true)
|
||||||
|
.keyboardType(.asciiCapable)
|
||||||
|
#endif
|
||||||
|
.font(.bitchatSystem(size: 14, design: .monospaced))
|
||||||
|
.onChange(of: customGeohash) { newValue in
|
||||||
|
let allowed = Set("0123456789bcdefghjkmnpqrstuvwxyz")
|
||||||
|
let filtered = newValue
|
||||||
|
.lowercased()
|
||||||
|
.replacingOccurrences(of: "#", with: "")
|
||||||
|
.filter { allowed.contains($0) }
|
||||||
|
if filtered.count > 12 {
|
||||||
|
customGeohash = String(filtered.prefix(12))
|
||||||
|
} else if filtered != newValue {
|
||||||
|
customGeohash = filtered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let normalized = customGeohash
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased()
|
||||||
|
.replacingOccurrences(of: "#", with: "")
|
||||||
|
let isValid = validateGeohash(normalized)
|
||||||
|
Button(action: {
|
||||||
|
let gh = normalized
|
||||||
|
guard isValid else { customError = "invalid geohash"; return }
|
||||||
|
let level = levelForLength(gh.count)
|
||||||
|
let ch = GeohashChannel(level: level, geohash: gh)
|
||||||
|
manager.markTeleported(for: ch.geohash, true)
|
||||||
|
manager.select(ChannelID.location(ch))
|
||||||
|
isPresented = false
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text("teleport")
|
||||||
|
.font(.bitchatSystem(size: 14, design: .monospaced))
|
||||||
|
Image(systemName: "face.dashed")
|
||||||
|
.font(.bitchatSystem(size: 14))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.font(.bitchatSystem(size: 14, design: .monospaced))
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.background(Color.secondary.opacity(0.12))
|
||||||
|
.cornerRadius(6)
|
||||||
|
.opacity(isValid ? 1.0 : 0.4)
|
||||||
|
.disabled(!isValid)
|
||||||
|
}
|
||||||
|
if let err = customError {
|
||||||
|
Text(err)
|
||||||
|
.font(.bitchatSystem(size: 12, design: .monospaced))
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bookmarkedSection(_ entries: [String]) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("bookmarked")
|
||||||
|
.font(.bitchatSystem(size: 12, design: .monospaced))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
LazyVStack(spacing: 0) {
|
||||||
|
ForEach(Array(entries.enumerated()), id: \.offset) { index, gh in
|
||||||
|
let level = levelForLength(gh.count)
|
||||||
|
let channel = GeohashChannel(level: level, geohash: gh)
|
||||||
|
let coverage = coverageString(forPrecision: gh.count)
|
||||||
|
let subtitle = "#\(gh) • \(coverage)"
|
||||||
|
let name = bookmarks.bookmarkNames[gh]
|
||||||
channelRow(
|
channelRow(
|
||||||
title: geohashTitleWithCount(for: channel),
|
title: geohashHashTitleWithCount(gh),
|
||||||
subtitlePrefix: subtitlePrefix,
|
subtitlePrefix: subtitle,
|
||||||
subtitleName: namePart,
|
subtitleName: name.map { formattedNamePrefix(for: level) + $0 },
|
||||||
isSelected: isSelected(channel),
|
isSelected: isSelected(channel),
|
||||||
titleBold: highlight,
|
|
||||||
trailingAccessory: {
|
trailingAccessory: {
|
||||||
Button(action: { bookmarks.toggle(channel.geohash) }) {
|
Button(action: { bookmarks.toggle(gh) }) {
|
||||||
Image(systemName: bookmarks.isBookmarked(channel.geohash) ? "bookmark.fill" : "bookmark")
|
Image(systemName: bookmarks.isBookmarked(gh) ? "bookmark.fill" : "bookmark")
|
||||||
.font(.bitchatSystem(size: 14))
|
.font(.bitchatSystem(size: 14))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.padding(.leading, 8)
|
.padding(.leading, 8)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
// Selecting a suggested nearby channel is not a teleport. Persist this.
|
let inRegional = manager.availableChannels.contains { $0.geohash == gh }
|
||||||
manager.markTeleported(for: channel.geohash, false)
|
if !inRegional && !manager.availableChannels.isEmpty {
|
||||||
|
manager.markTeleported(for: gh, true)
|
||||||
|
} else {
|
||||||
|
manager.markTeleported(for: gh, false)
|
||||||
|
}
|
||||||
manager.select(ChannelID.location(channel))
|
manager.select(ChannelID.location(channel))
|
||||||
isPresented = false
|
isPresented = false
|
||||||
}
|
}
|
||||||
.listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
HStack {
|
|
||||||
ProgressView()
|
|
||||||
Text("finding nearby channels…")
|
|
||||||
.font(.bitchatSystem(size: 12, design: .monospaced))
|
|
||||||
}
|
|
||||||
.listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom geohash teleport
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
HStack(spacing: 2) {
|
|
||||||
Text("#")
|
|
||||||
.font(.bitchatSystem(size: 14, design: .monospaced))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
TextField("geohash", text: $customGeohash)
|
|
||||||
#if os(iOS)
|
|
||||||
.textInputAutocapitalization(.never)
|
|
||||||
.autocorrectionDisabled(true)
|
|
||||||
.keyboardType(.asciiCapable)
|
|
||||||
#endif
|
|
||||||
.font(.bitchatSystem(size: 14, design: .monospaced))
|
|
||||||
.onChange(of: customGeohash) { newValue in
|
|
||||||
// Allow only geohash base32 characters, strip '#', limit length
|
|
||||||
let allowed = Set("0123456789bcdefghjkmnpqrstuvwxyz")
|
|
||||||
let filtered = newValue
|
|
||||||
.lowercased()
|
|
||||||
.replacingOccurrences(of: "#", with: "")
|
|
||||||
.filter { allowed.contains($0) }
|
|
||||||
if filtered.count > 12 {
|
|
||||||
customGeohash = String(filtered.prefix(12))
|
|
||||||
} else if filtered != newValue {
|
|
||||||
customGeohash = filtered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let normalized = customGeohash.trimmingCharacters(in: .whitespacesAndNewlines).lowercased().replacingOccurrences(of: "#", with: "")
|
|
||||||
let isValid = validateGeohash(normalized)
|
|
||||||
Button(action: {
|
|
||||||
let gh = normalized
|
|
||||||
guard isValid else { customError = "invalid geohash"; return }
|
|
||||||
let level = levelForLength(gh.count)
|
|
||||||
let ch = GeohashChannel(level: level, geohash: gh)
|
|
||||||
// Mark this selection as a manual teleport
|
|
||||||
manager.markTeleported(for: ch.geohash, true)
|
|
||||||
manager.select(ChannelID.location(ch))
|
|
||||||
isPresented = false
|
|
||||||
}) {
|
|
||||||
HStack(spacing: 6) {
|
|
||||||
Text("teleport")
|
|
||||||
.font(.bitchatSystem(size: 14, design: .monospaced))
|
|
||||||
Image(systemName: "face.dashed")
|
|
||||||
.font(.bitchatSystem(size: 14))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.font(.bitchatSystem(size: 14, design: .monospaced))
|
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.background(Color.secondary.opacity(0.12))
|
.onAppear { bookmarks.resolveNameIfNeeded(for: gh) }
|
||||||
.cornerRadius(6)
|
|
||||||
.opacity(isValid ? 1.0 : 0.4)
|
|
||||||
.disabled(!isValid)
|
|
||||||
}
|
|
||||||
if let err = customError {
|
|
||||||
Text(err)
|
|
||||||
.font(.bitchatSystem(size: 12, design: .monospaced))
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0))
|
|
||||||
|
|
||||||
// Bookmarked geohashes
|
if index < entries.count - 1 {
|
||||||
if !bookmarks.bookmarks.isEmpty {
|
sectionDivider
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("bookmarked")
|
|
||||||
.font(.bitchatSystem(size: 12, design: .monospaced))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
VStack(spacing: 6) {
|
|
||||||
ForEach(bookmarks.bookmarks, id: \.self) { gh in
|
|
||||||
let level = levelForLength(gh.count)
|
|
||||||
let channel = GeohashChannel(level: level, geohash: gh)
|
|
||||||
let coverage = coverageString(forPrecision: gh.count)
|
|
||||||
let subtitle = "#\(gh) • \(coverage)"
|
|
||||||
let name = bookmarks.bookmarkNames[gh]
|
|
||||||
channelRow(
|
|
||||||
title: geohashHashTitleWithCount(gh),
|
|
||||||
subtitlePrefix: subtitle,
|
|
||||||
subtitleName: name.map { formattedNamePrefix(for: level) + $0 },
|
|
||||||
isSelected: isSelected(channel),
|
|
||||||
trailingAccessory: {
|
|
||||||
Button(action: { bookmarks.toggle(gh) }) {
|
|
||||||
Image(systemName: bookmarks.isBookmarked(gh) ? "bookmark.fill" : "bookmark")
|
|
||||||
.font(.bitchatSystem(size: 14))
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.padding(.leading, 8)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
// For bookmarked selection, mark teleported based on regional membership
|
|
||||||
let inRegional = manager.availableChannels.contains { $0.geohash == gh }
|
|
||||||
if !inRegional && !manager.availableChannels.isEmpty {
|
|
||||||
manager.markTeleported(for: gh, true)
|
|
||||||
} else {
|
|
||||||
manager.markTeleported(for: gh, false)
|
|
||||||
}
|
|
||||||
manager.select(ChannelID.location(channel))
|
|
||||||
isPresented = false
|
|
||||||
}
|
|
||||||
.onAppear { bookmarks.resolveNameIfNeeded(for: gh) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(12)
|
|
||||||
.background(Color.secondary.opacity(0.12))
|
|
||||||
.cornerRadius(8)
|
|
||||||
}
|
}
|
||||||
.listRowSeparator(.hidden)
|
|
||||||
.listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Footer action inside the list
|
|
||||||
if manager.permissionState == LocationChannelManager.PermissionState.authorized {
|
|
||||||
torToggleSection
|
|
||||||
.listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 4, trailing: 0))
|
|
||||||
Button(action: {
|
|
||||||
openSystemLocationSettings()
|
|
||||||
}) {
|
|
||||||
Text("remove location access")
|
|
||||||
.font(.bitchatSystem(size: 12, design: .monospaced))
|
|
||||||
.foregroundColor(Color(red: 0.75, green: 0.1, blue: 0.1))
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
.background(Color.red.opacity(0.08))
|
|
||||||
.cornerRadius(6)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.listRowSeparator(.hidden)
|
|
||||||
.listRowBackground(Color.clear)
|
|
||||||
.listRowInsets(EdgeInsets(top: 4, leading: 0, bottom: 10, trailing: 0))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
|
||||||
.scrollContentBackground(.hidden)
|
|
||||||
.background(backgroundColor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func isSelected(_ channel: GeohashChannel) -> Bool {
|
private func isSelected(_ channel: GeohashChannel) -> Bool {
|
||||||
if case .location(let ch) = manager.selectedChannel {
|
if case .location(let ch) = manager.selectedChannel {
|
||||||
return ch == channel
|
return ch == channel
|
||||||
@@ -437,8 +461,6 @@ extension LocationChannelsSheet {
|
|||||||
.padding(12)
|
.padding(12)
|
||||||
.background(Color.secondary.opacity(0.12))
|
.background(Color.secondary.opacity(0.12))
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
.listRowSeparator(.hidden)
|
|
||||||
.listRowBackground(Color.clear)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var standardGreen: Color {
|
private var standardGreen: Color {
|
||||||
|
|||||||
Reference in New Issue
Block a user