ui background matches theme

ui bg is 70% opacity
This commit is contained in:
neon443
2025-06-29 18:51:52 +01:00
parent a4a71968fe
commit 1e730582c0
6 changed files with 240 additions and 213 deletions

View File

@@ -14,30 +14,35 @@ struct ContentView: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
List { ZStack {
SessionsListView( hostsManager.selectedTheme.background.suiColor.opacity(0.7)
handler: handler, .ignoresSafeArea(.all)
hostsManager: hostsManager, List {
keyManager: keyManager SessionsListView(
) handler: handler,
hostsManager: hostsManager,
HostsView( keyManager: keyManager
handler: handler, )
hostsManager: hostsManager,
keyManager: keyManager HostsView(
) handler: handler,
hostsManager: hostsManager,
NavigationLink { keyManager: keyManager
KeyManagerView(hostsManager: hostsManager, keyManager: keyManager) )
} label: {
Label("Keys", systemImage: "key.fill") NavigationLink {
} KeyManagerView(hostsManager: hostsManager, keyManager: keyManager)
} label: {
NavigationLink { Label("Keys", systemImage: "key.fill")
HostkeysView(hostsManager: hostsManager) }
} label: {
Label("Hostkey Fingerprints", systemImage: "lock.display") NavigationLink {
HostkeysView(hostsManager: hostsManager)
} label: {
Label("Hostkey Fingerprints", systemImage: "lock.display")
}
} }
.scrollContentBackground(.hidden)
} }
} }
} }

View File

@@ -24,7 +24,9 @@ struct ConnectionView: View {
@State var hostKeyChangedAlert: Bool = false @State var hostKeyChangedAlert: Bool = false
var body: some View { var body: some View {
NavigationStack { ZStack {
hostsManager.selectedTheme.background.suiColor.opacity(0.7)
.ignoresSafeArea(.all)
List { List {
Section { Section {
ScrollView(.horizontal) { ScrollView(.horizontal) {
@@ -128,6 +130,34 @@ struct ConnectionView: View {
} }
} }
} }
.scrollContentBackground(.hidden)
.transition(.opacity)
.onChange(of: handler.host.key) { _ in
guard let previousKnownHost = hostsManager.getHostMatching(handler.host) else { return }
guard handler.host.key == previousKnownHost.key else {
hostKeyChangedAlert = true
return
}
}
.onDisappear {
hostsManager.updateHost(handler.host)
}
.task {
if let publicKeyData = handler.host.publicKey {
pubkeyStr = String(data: publicKeyData, encoding: .utf8) ?? ""
}
if let privateKeyData = handler.host.privateKey {
privkeyStr = String(data: privateKeyData, encoding: .utf8) ?? ""
}
}
.onAppear {
if shellView == nil {
shellView = ShellView(handler: handler, hostsManager: hostsManager)
}
}
.onAppear {
hostsManager.addHostIfNeeded(handler.host)
}
.alert("Hostkey changed", isPresented: $hostKeyChangedAlert) { .alert("Hostkey changed", isPresented: $hostKeyChangedAlert) {
Button("Accept New Hostkey", role: .destructive) { Button("Accept New Hostkey", role: .destructive) {
hostsManager.updateHost(handler.host) hostsManager.updateHost(handler.host)
@@ -141,7 +171,6 @@ struct ConnectionView: View {
} message: { } message: {
Text("Expected \(handler.host.key ?? "nil")\nbut recieved \(handler.getHostkey() ?? "nil") from the server") Text("Expected \(handler.host.key ?? "nil")\nbut recieved \(handler.getHostkey() ?? "nil") from the server")
} }
.transition(.opacity)
.toolbar { .toolbar {
ToolbarItem() { ToolbarItem() {
Button() { Button() {
@@ -156,40 +185,14 @@ struct ConnectionView: View {
.disabled(handler.hostInvalid()) .disabled(handler.hostInvalid())
} }
} }
} .fullScreenCover(isPresented: $showTerminal) {
.fullScreenCover(isPresented: $showTerminal) { if let shellView {
if let shellView { shellView
shellView } else {
} else { Text("no shellview")
Text("no shellview") }
} }
} }
.onChange(of: handler.host.key) { _ in
guard let previousKnownHost = hostsManager.getHostMatching(handler.host) else { return }
guard handler.host.key == previousKnownHost.key else {
hostKeyChangedAlert = true
return
}
}
.onDisappear {
hostsManager.updateHost(handler.host)
}
.task {
if let publicKeyData = handler.host.publicKey {
pubkeyStr = String(data: publicKeyData, encoding: .utf8) ?? ""
}
if let privateKeyData = handler.host.privateKey {
privkeyStr = String(data: privateKeyData, encoding: .utf8) ?? ""
}
}
.onAppear {
if shellView == nil {
shellView = ShellView(handler: handler, hostsManager: hostsManager)
}
}
.onAppear {
hostsManager.addHostIfNeeded(handler.host)
}
} }
} }

View File

@@ -10,47 +10,52 @@ import SwiftUI
struct HostkeysView: View { struct HostkeysView: View {
@ObservedObject var hostsManager: HostsManager @ObservedObject var hostsManager: HostsManager
var body: some View { var body: some View {
NavigationStack { ZStack {
List { hostsManager.selectedTheme.background.suiColor.opacity(0.7)
if hostsManager.hosts.isEmpty { .ignoresSafeArea(.all)
VStack(alignment: .leading) { NavigationStack {
Text("Looking empty 'round here...") List {
.font(.title3) if hostsManager.hosts.isEmpty {
.bold()
.padding(.bottom)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Connect to some hosts to collect more hostkeys!") Text("Looking empty 'round here...")
.font(.title3)
.bold()
.padding(.bottom) .padding(.bottom)
Text("ShhShell remembers hostkey fingerprints for you, and can alert you if they change.") VStack(alignment: .leading) {
.font(.subheadline) Text("Connect to some hosts to collect more hostkeys!")
Text("This could be due a man in the middle attack, where a bad actor tries to impersonate your server.") .padding(.bottom)
.font(.subheadline) Text("ShhShell remembers hostkey fingerprints for you, and can alert you if they change.")
.font(.subheadline)
Text("This could be due a man in the middle attack, where a bad actor tries to impersonate your server.")
.font(.subheadline)
}
} }
} }
}
ForEach(hostsManager.hosts) { host in
ForEach(hostsManager.hosts) { host in VStack(alignment: .leading) {
VStack(alignment: .leading) { if !host.name.isEmpty {
if !host.name.isEmpty { Text("name")
Text("name") .foregroundStyle(.gray)
.font(.caption)
Text(host.name)
.bold()
}
Text("address")
.foregroundStyle(.gray) .foregroundStyle(.gray)
.font(.caption) .font(.caption)
Text(host.name) Text(host.address)
.bold() .bold()
Text(host.key ?? "nil")
} }
Text("address")
.foregroundStyle(.gray)
.font(.caption)
Text(host.address)
.bold()
Text(host.key ?? "nil")
} }
} }
.scrollContentBackground(.hidden)
.navigationTitle("Hostkeys")
} }
.navigationTitle("Hostkeys")
} }
} }
} }
#Preview { #Preview {

View File

@@ -12,63 +12,68 @@ struct KeyDetailView: View {
@State var keypair: Keypair @State var keypair: Keypair
@State private var reveal: Bool = false @State private var reveal: Bool = false
var body: some View { var body: some View {
List { ZStack {
VStack(alignment: .leading) { hostsManager.selectedTheme.background.suiColor.opacity(0.7)
Text("Used on") .ignoresSafeArea(.all)
.bold() List {
ForEach(hostsManager.getHostsKeysUsedOn([keypair])) { host in VStack(alignment: .leading) {
HStack { Text("Used on")
SymbolPreview(symbol: host.symbol, label: host.label) .bold()
.frame(width: 40, height: 40) ForEach(hostsManager.getHostsKeysUsedOn([keypair])) { host in
Text(hostsManager.makeLabel(forHost: host)) HStack {
} SymbolPreview(symbol: host.symbol, label: host.label)
} .frame(width: 40, height: 40)
} Text(hostsManager.makeLabel(forHost: host))
VStack(alignment: .leading) {
Text("Public key")
.bold()
Text(String(data: keypair.publicKey!, encoding: .utf8) ?? "nil")
}
VStack(alignment: .leading) {
Text("Private key")
.bold()
.frame(maxWidth: .infinity)
ZStack(alignment: .center) {
Text(String(data: keypair.privateKey!, encoding: .utf8) ?? "nil")
.blur(radius: reveal ? 0 : 5)
VStack {
Image(systemName: "eye.slash.fill")
.resizable().scaledToFit()
.frame(width: 50)
Text("Tap to reveal")
}
.opacity(reveal ? 0 : 1)
}
.frame(maxWidth: .infinity)
.onTapGesture {
Task {
if !reveal {
guard await hostsManager.authWithBiometrics() else { return }
} }
withAnimation(.spring) { reveal.toggle() }
} }
} }
} VStack(alignment: .leading) {
Text("Public key")
Button { .bold()
Task { Text(String(data: keypair.publicKey!, encoding: .utf8) ?? "nil")
guard await hostsManager.authWithBiometrics() else { return } }
if let privateKey = keypair.privateKey { VStack(alignment: .leading) {
UIPasteboard.general.string = String(data: privateKey, encoding: .utf8) Text("Private key")
.bold()
.frame(maxWidth: .infinity)
ZStack(alignment: .center) {
Text(String(data: keypair.privateKey!, encoding: .utf8) ?? "nil")
.blur(radius: reveal ? 0 : 5)
VStack {
Image(systemName: "eye.slash.fill")
.resizable().scaledToFit()
.frame(width: 50)
Text("Tap to reveal")
}
.opacity(reveal ? 0 : 1)
}
.frame(maxWidth: .infinity)
.onTapGesture {
Task {
if !reveal {
guard await hostsManager.authWithBiometrics() else { return }
}
withAnimation(.spring) { reveal.toggle() }
}
} }
} }
} label: {
CenteredLabel(title: "Copy private key", systemName: "document.on.document") Button {
Task {
guard await hostsManager.authWithBiometrics() else { return }
if let privateKey = keypair.privateKey {
UIPasteboard.general.string = String(data: privateKey, encoding: .utf8)
}
}
} label: {
CenteredLabel(title: "Copy private key", systemName: "document.on.document")
}
.listRowSeparator(.hidden)
} }
.listRowSeparator(.hidden) .scrollContentBackground(.hidden)
} }
} }
} }
#Preview { #Preview {

View File

@@ -12,32 +12,37 @@ struct KeyManagerView: View {
@ObservedObject var keyManager: KeyManager @ObservedObject var keyManager: KeyManager
var body: some View { var body: some View {
NavigationStack { ZStack {
List { hostsManager.selectedTheme.background.suiColor.opacity(0.7)
Section { .ignoresSafeArea(.all)
ForEach(hostsManager.getKeys()) { keypair in NavigationStack {
NavigationLink { List {
KeyDetailView(hostsManager: hostsManager, keypair: keypair) Section {
} label: { ForEach(hostsManager.getKeys()) { keypair in
if let publicKey = keypair.publicKey { NavigationLink {
Text(String(data: publicKey, encoding: .utf8) ?? "nil") KeyDetailView(hostsManager: hostsManager, keypair: keypair)
} label: {
if let publicKey = keypair.publicKey {
Text(String(data: publicKey, encoding: .utf8) ?? "nil")
}
} }
} }
} }
}
Button("ed25519") {
Button("ed25519") { keyManager.generateEd25519()
keyManager.generateEd25519() }
} Button("rsa") {
Button("rsa") { do {
do { try keyManager.generateRSA()
try keyManager.generateRSA() } catch {
} catch { print(error.localizedDescription)
print(error.localizedDescription) }
} }
} }
.scrollContentBackground(.hidden)
.navigationTitle("Keys")
} }
.navigationTitle("Keys")
} }
} }
} }

View File

@@ -25,75 +25,79 @@ struct ThemeManagerView: View {
) )
var body: some View { var body: some View {
GeometryReader { geo in ZStack {
let columns: Int = Int(geo.size.width)/200 hostsManager.selectedTheme.background.suiColor.opacity(0.7)
let layout = Array(repeating: grid, count: columns) .ignoresSafeArea(.all)
ScrollView { GeometryReader { geo in
if hostsManager.themes.isEmpty { let columns: Int = Int(geo.size.width)/200
VStack(alignment: .leading) { let layout = Array(repeating: grid, count: columns)
Image(systemName: "paintpalette") ScrollView {
.resizable().scaledToFit() if hostsManager.themes.isEmpty {
.symbolRenderingMode(.multicolor) VStack(alignment: .leading) {
.frame(width: 50) Image(systemName: "paintpalette")
Text("No themes (yet)") .resizable().scaledToFit()
.font(.title) .symbolRenderingMode(.multicolor)
.padding(.vertical, 10) .frame(width: 50)
.bold() Text("No themes (yet)")
Text("Tap the Safari icon at the top right to find themes!") .font(.title)
Text("Once you find one that you like, copy it's link and enter it here using the link button.") .padding(.vertical, 10)
.bold()
Text("Tap the Safari icon at the top right to find themes!")
Text("Once you find one that you like, copy it's link and enter it here using the link button.")
}
} else {
LazyVGrid(columns: layout, alignment: .center, spacing: 8) {
ForEach(hostsManager.themes) { theme in
ThemePreview(hostsManager: hostsManager, theme: theme, canModify: true)
}
}
.padding(.horizontal)
.animation(.default, value: hostsManager.themes)
}
HStack {
Text("Built-in Themes")
.padding(.top)
.padding(.horizontal)
.font(.headline)
Spacer()
} }
} else {
LazyVGrid(columns: layout, alignment: .center, spacing: 8) { LazyVGrid(columns: layout, alignment: .center, spacing: 8) {
ForEach(hostsManager.themes) { theme in ForEach(Theme.builtinThemes) { theme in
ThemePreview(hostsManager: hostsManager, theme: theme, canModify: true) ThemePreview(hostsManager: hostsManager, theme: theme, canModify: false)
} }
} }
.padding(.horizontal) .padding(.horizontal)
.animation(.default, value: hostsManager.themes) .animation(.default, value: hostsManager.themes)
} }
.navigationTitle("Themes")
HStack { .alert("Enter URL", isPresented: $showAlert) {
Text("Built-in Themes") TextField("", text: $importURL, prompt: Text("URL"))
.padding(.top) Button("Cancel") {}
.padding(.horizontal)
.font(.headline)
Spacer()
}
LazyVGrid(columns: layout, alignment: .center, spacing: 8) {
ForEach(Theme.builtinThemes) { theme in
ThemePreview(hostsManager: hostsManager, theme: theme, canModify: false)
}
}
.padding(.horizontal)
.animation(.default, value: hostsManager.themes)
}
.navigationTitle("Themes")
.alert("Enter URL", isPresented: $showAlert) {
TextField("", text: $importURL, prompt: Text("URL"))
Button("Cancel") {}
Button() {
hostsManager.downloadTheme(fromUrl: URL(string: importURL))
importURL = ""
} label: {
Label("Import", systemImage: "square.and.arrow.down")
.bold()
}
}
.toolbar {
ToolbarItem() {
Button() { Button() {
UIApplication.shared.open(URL(string: "https://iterm2colorschemes.com")!) hostsManager.downloadTheme(fromUrl: URL(string: importURL))
importURL = ""
} label: { } label: {
Label("Open themes site", systemImage: "safari") Label("Import", systemImage: "square.and.arrow.down")
.bold()
} }
} }
ToolbarItem() { .toolbar {
Button() { ToolbarItem() {
showAlert.toggle() Button() {
} label: { UIApplication.shared.open(URL(string: "https://iterm2colorschemes.com")!)
Label("From URL", systemImage: "link") } label: {
Label("Open themes site", systemImage: "safari")
}
}
ToolbarItem() {
Button() {
showAlert.toggle()
} label: {
Label("From URL", systemImage: "link")
}
} }
} }
} }
} }