mirror of
https://github.com/neon443/ShhShell.git
synced 2026-03-11 13:26:16 +00:00
theme picker ui is pretty now
added a bunch of builtin htemes applytheme takes a theme added builtin themes enum added decodeloacltheme func added builtinthemes array property selectedtheme is now an actual theme instead of an index its really annoying it doesnt really work rn
This commit is contained in:
@@ -10,18 +10,18 @@ import LocalAuthentication
|
||||
import SwiftUI
|
||||
|
||||
class HostsManager: ObservableObject, @unchecked Sendable {
|
||||
private let userDefaults = NSUbiquitousKeyValueStore.default
|
||||
private let userDefaults = UserDefaults.standard
|
||||
|
||||
@Published var hosts: [Host] = []
|
||||
@Published var themes: [Theme] = []
|
||||
@Published var selectedThemeIndex: Int = -1
|
||||
@Published var selectedTheme: Theme = Theme.defaultTheme
|
||||
|
||||
init() {
|
||||
loadHosts()
|
||||
loadThemes()
|
||||
print(selectedTheme == Theme.defaultTheme)
|
||||
}
|
||||
|
||||
|
||||
func loadThemes() {
|
||||
guard let dataTheme = userDefaults.data(forKey: "themes") else { return }
|
||||
guard let dataThemeNames = userDefaults.data(forKey: "themeNames") else { return }
|
||||
@@ -34,6 +34,12 @@ class HostsManager: ObservableObject, @unchecked Sendable {
|
||||
guard let synthedTheme = Theme.decodeTheme(name: decodedThemeNames[index], data: encoded) else { return }
|
||||
self.themes.append(synthedTheme)
|
||||
}
|
||||
|
||||
|
||||
guard let dataSelTheme = userDefaults.data(forKey: "selectedTheme") else { return }
|
||||
guard let decodedSelTheme = Theme.decodeTheme(name: "", data: dataSelTheme) else { return }
|
||||
//name doesnt matter
|
||||
self.selectedTheme = decodedSelTheme
|
||||
}
|
||||
|
||||
func downloadTheme(fromUrl: URL?) {
|
||||
@@ -50,16 +56,16 @@ class HostsManager: ObservableObject, @unchecked Sendable {
|
||||
}
|
||||
|
||||
func selectTheme(_ selectedTheme: Theme) {
|
||||
guard let index = themes.firstIndex(where: { $0 == selectedTheme }) else {
|
||||
withAnimation { selectedThemeIndex = -1 }
|
||||
return
|
||||
}
|
||||
withAnimation { selectedThemeIndex = index }
|
||||
withAnimation { self.selectedTheme = selectedTheme }
|
||||
print("selected: \(selectedTheme.name) \(selectedTheme.id)")
|
||||
saveThemes()
|
||||
}
|
||||
|
||||
func isThemeSelected(_ themeInQuestion: Theme) -> Bool {
|
||||
guard let index = themes.firstIndex(where: { $0 == themeInQuestion }) else { return false }
|
||||
return index == selectedThemeIndex
|
||||
var themeInQWithSameID = themeInQuestion
|
||||
themeInQWithSameID.id = selectedTheme.id
|
||||
|
||||
return themeInQuestion.id == self.selectedTheme.id
|
||||
}
|
||||
|
||||
func renameTheme(_ theme: Theme?, to newName: String) {
|
||||
@@ -68,7 +74,7 @@ class HostsManager: ObservableObject, @unchecked Sendable {
|
||||
guard let index = themes.firstIndex(where: {$0.id == theme.id}) else { return }
|
||||
var newTheme = themes[index]
|
||||
newTheme.name = newName
|
||||
newTheme.id = UUID()
|
||||
newTheme.id = UUID().uuidString
|
||||
withAnimation { themes[index] = newTheme }
|
||||
saveThemes()
|
||||
}
|
||||
@@ -89,12 +95,19 @@ class HostsManager: ObservableObject, @unchecked Sendable {
|
||||
|
||||
func saveThemes() {
|
||||
let encoder = JSONEncoder()
|
||||
// map the theme to themecodable
|
||||
guard let encodedThemes = try? encoder.encode(themes.map({$0.themeCodable})) else { return }
|
||||
//map the themes to get their names
|
||||
guard let encodedThemeNames = try? encoder.encode(themes.map{$0.name}) else { return }
|
||||
|
||||
userDefaults.set(encodedThemes, forKey: "themes")
|
||||
userDefaults.set(encodedThemeNames, forKey: "themeNames")
|
||||
|
||||
guard let encodedSelectedTheme = try? encoder.encode(selectedTheme.themeCodable) else { return }
|
||||
userDefaults.set(encodedSelectedTheme, forKey: "selectedTheme")
|
||||
userDefaults.synchronize()
|
||||
print(Theme.decodeTheme(name: "", data: userDefaults.data(forKey: "selectedTheme")))
|
||||
print("saved themes")
|
||||
}
|
||||
|
||||
func getHostIndexMatching(_ hostSearchingFor: Host) -> Int? {
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftTerm
|
||||
import SwiftUI
|
||||
|
||||
struct Theme: Hashable, Equatable, Identifiable {
|
||||
var id = UUID()
|
||||
var id: String = UUID().uuidString
|
||||
var name: String
|
||||
var ansi: [SwiftTerm.Color]
|
||||
var foreground: SwiftTerm.Color
|
||||
@@ -23,6 +23,7 @@ struct Theme: Hashable, Equatable, Identifiable {
|
||||
|
||||
var themeCodable: ThemeCodable {
|
||||
return ThemeCodable(
|
||||
name: name,
|
||||
ansi0: ansi[0].colorCodable,
|
||||
ansi1: ansi[1].colorCodable,
|
||||
ansi2: ansi[2].colorCodable,
|
||||
@@ -50,7 +51,7 @@ struct Theme: Hashable, Equatable, Identifiable {
|
||||
}
|
||||
|
||||
static func decodeTheme(name: String, data: Data?) -> Theme? {
|
||||
guard let data else { return nil }
|
||||
guard let data else { fatalError() }
|
||||
|
||||
let plistDecoder = PropertyListDecoder()
|
||||
let jsonDecoder = JSONDecoder()
|
||||
@@ -58,9 +59,9 @@ struct Theme: Hashable, Equatable, Identifiable {
|
||||
guard let decoded =
|
||||
(try? plistDecoder.decode(ThemeCodable.self, from: data)) ??
|
||||
(try? jsonDecoder.decode(ThemeCodable.self, from: data))
|
||||
else { return nil }
|
||||
let theme = Theme(
|
||||
name: name,
|
||||
else { fatalError() }
|
||||
var theme = Theme(
|
||||
name: decoded.name ?? name,
|
||||
ansi: decoded.ansi,
|
||||
foreground: Color(decoded.foreground),
|
||||
background: Color(decoded.background),
|
||||
@@ -72,10 +73,46 @@ struct Theme: Hashable, Equatable, Identifiable {
|
||||
)
|
||||
return theme
|
||||
}
|
||||
|
||||
static func decodeLocalTheme(fileName: String) -> Theme? {
|
||||
guard let path = Bundle.main.url(forResource: fileName, withExtension: "plist") else { return nil }
|
||||
let themeName = path.lastPathComponent.replacingOccurrences(of: ".plist", with: "")
|
||||
|
||||
guard let fileContents = try? Data(contentsOf: path) else { return nil }
|
||||
|
||||
guard var theme = Theme.decodeTheme(name: themeName, data: fileContents) else { return nil }
|
||||
theme.name = themeName
|
||||
theme.id = themeName
|
||||
return theme
|
||||
}
|
||||
|
||||
static var defaultTheme: Theme {
|
||||
return decodeLocalTheme(fileName: "defaultTheme")!
|
||||
}
|
||||
|
||||
static var builtinThemes: [Theme] {
|
||||
return ThemesBuiltin.allCases.map({ decodeLocalTheme(fileName: $0.rawValue)! })
|
||||
}
|
||||
}
|
||||
|
||||
enum ThemesBuiltin: String, CaseIterable, Hashable, Equatable {
|
||||
case defaultTheme = "defaultTheme"
|
||||
case xcodedark = "xcodedark"
|
||||
case xcodedarkhc = "xcodedarkhc"
|
||||
case xcodewwdc = "xcodewwdc"
|
||||
case tomorrowNight = "tomorrowNight"
|
||||
case zeroXNineSixF = "0x96f"
|
||||
case iTerm2SolarizedDark = "iTerm2SolarizedDark"
|
||||
case iTerm2SolarizedLight = "iTerm2SolarizedLight"
|
||||
case catppuccinFrappe = "catppuccinFrappe"
|
||||
case catppuccinMocha = "catppuccinMocha"
|
||||
case dracula = "dracula"
|
||||
case gruvboxDark = "gruvboxDark"
|
||||
case ubuntu = "ubuntu"
|
||||
}
|
||||
|
||||
struct ThemeCodable: Codable {
|
||||
var name: String?
|
||||
var ansi0: ColorCodable
|
||||
var ansi1: ColorCodable
|
||||
var ansi2: ColorCodable
|
||||
|
||||
@@ -19,7 +19,10 @@ final class SSHTerminalDelegate: TerminalView, Sendable, @preconcurrency Termina
|
||||
self.handler = handler
|
||||
self.hostsManager = hostsManager
|
||||
|
||||
applyTheme(index: hostsManager.selectedThemeIndex)
|
||||
print(getTerminal().backgroundColor)
|
||||
print(getTerminal().foregroundColor)
|
||||
|
||||
applyTheme(hostsManager.selectedTheme)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
Task {
|
||||
@@ -52,11 +55,7 @@ final class SSHTerminalDelegate: TerminalView, Sendable, @preconcurrency Termina
|
||||
}
|
||||
}
|
||||
|
||||
func applyTheme(index themeIndex: Int) {
|
||||
guard themeIndex != -1 else { return }
|
||||
guard let hostsManager = hostsManager else { return }
|
||||
|
||||
let theme = hostsManager.themes[themeIndex]
|
||||
func applyTheme(_ theme: Theme) {
|
||||
getTerminal().installPalette(colors: theme.ansi)
|
||||
getTerminal().foregroundColor = theme.foreground
|
||||
getTerminal().backgroundColor = theme.background
|
||||
|
||||
@@ -25,69 +25,93 @@ struct ThemeManagerView: View {
|
||||
)
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
NavigationStack {
|
||||
ScrollView(.horizontal) {
|
||||
LazyHGrid(rows: [grid, grid], alignment: .center, spacing: 8) {
|
||||
ForEach(hostsManager.themes) { theme in
|
||||
ThemePreview(theme: theme)
|
||||
.scaleEffect(hostsManager.isThemeSelected(theme) ? 1.2 : 1)
|
||||
.onTapGesture {
|
||||
hostsManager.selectTheme(theme)
|
||||
}
|
||||
.contextMenu {
|
||||
Button() {
|
||||
themeToRename = theme
|
||||
rename = theme.name
|
||||
showRenameAlert.toggle()
|
||||
} label: {
|
||||
Label("Rename", systemImage: "pencil")
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
hostsManager.deleteTheme(theme)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
NavigationStack {
|
||||
List {
|
||||
Section("Your Themes") {
|
||||
if hostsManager.themes.isEmpty {
|
||||
VStack(alignment: .leading) {
|
||||
Image(systemName: "paintpalette")
|
||||
.resizable().scaledToFit()
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.frame(width: 50)
|
||||
Text("No themes (yet)")
|
||||
.font(.title)
|
||||
.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.")
|
||||
}
|
||||
}
|
||||
.animation(.default, value: hostsManager.themes)
|
||||
.alert("", isPresented: $showRenameAlert) {
|
||||
TextField("", text: $rename)
|
||||
Button("OK") {
|
||||
hostsManager.renameTheme(themeToRename, to: rename)
|
||||
rename = ""
|
||||
} else {
|
||||
ScrollView(.horizontal) {
|
||||
LazyHGrid(rows: [grid, grid], alignment: .center, spacing: 8) {
|
||||
ForEach(hostsManager.themes) { theme in
|
||||
ThemePreview(hostsManager: hostsManager, theme: theme)
|
||||
.contextMenu {
|
||||
Button() {
|
||||
themeToRename = theme
|
||||
rename = theme.name
|
||||
showRenameAlert.toggle()
|
||||
} label: {
|
||||
Label("Rename", systemImage: "pencil")
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
hostsManager.deleteTheme(theme)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.default, value: hostsManager.themes)
|
||||
.alert("", isPresented: $showRenameAlert) {
|
||||
TextField("", text: $rename)
|
||||
Button("OK") {
|
||||
hostsManager.renameTheme(themeToRename, to: rename)
|
||||
rename = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.scrollIndicators(.hidden)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.scrollIndicators(.hidden)
|
||||
.navigationTitle("Themes")
|
||||
.alert("Enter URL", isPresented: $showAlert) {
|
||||
TextField("", text: $importURL, prompt: Text("URL"))
|
||||
|
||||
Section("Builtin Themes") {
|
||||
ScrollView(.horizontal) {
|
||||
LazyHGrid(rows: [grid, grid], alignment: .center, spacing: 8) {
|
||||
ForEach(Theme.builtinThemes) { theme in
|
||||
ThemePreview(hostsManager: hostsManager, theme: theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Themes")
|
||||
.alert("Enter URL", isPresented: $showAlert) {
|
||||
TextField("", text: $importURL, prompt: Text("URL"))
|
||||
Button() {
|
||||
hostsManager.downloadTheme(fromUrl: URL(string: importURL))
|
||||
importURL = ""
|
||||
} label: {
|
||||
Label("Import", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
Button("Cancel") {}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem() {
|
||||
Button() {
|
||||
hostsManager.downloadTheme(fromUrl: URL(string: importURL))
|
||||
importURL = ""
|
||||
UIApplication.shared.open(URL(string: "https://iterm2colorschemes.com")!)
|
||||
} label: {
|
||||
Label("Import", systemImage: "square.and.arrow.down")
|
||||
Label("Open themes site", systemImage: "safari")
|
||||
}
|
||||
Button("Cancel") {}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem() {
|
||||
Button() {
|
||||
UIApplication.shared.open(URL(string: "https://iterm2colorschemes.com")!)
|
||||
} label: {
|
||||
Label("Open themes site", systemImage: "safari")
|
||||
}
|
||||
}
|
||||
ToolbarItem() {
|
||||
Button() {
|
||||
showAlert.toggle()
|
||||
} label: {
|
||||
Label("From URL", systemImage: "link")
|
||||
}
|
||||
ToolbarItem() {
|
||||
Button() {
|
||||
showAlert.toggle()
|
||||
} label: {
|
||||
Label("From URL", systemImage: "link")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,25 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ThemePreview: View {
|
||||
@ObservedObject var hostsManager: HostsManager
|
||||
@State var theme: Theme
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .center) {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
Rectangle()
|
||||
.fill(Color.accentColor)
|
||||
|
||||
Rectangle()
|
||||
.fill(theme.background.suiColor)
|
||||
.frame(
|
||||
width: hostsManager.isThemeSelected(theme) ? 190 : 200,
|
||||
height: hostsManager.isThemeSelected(theme) ? 80 : 90
|
||||
)
|
||||
.clipShape(
|
||||
RoundedRectangle(
|
||||
cornerRadius: hostsManager.isThemeSelected(theme) ? 5 : 10
|
||||
)
|
||||
)
|
||||
VStack(alignment: .leading) {
|
||||
Text(theme.name)
|
||||
.foregroundStyle(theme.foreground.suiColor)
|
||||
@@ -37,6 +50,10 @@ struct ThemePreview: View {
|
||||
.padding(8)
|
||||
}
|
||||
.frame(maxWidth: 200, maxHeight: 90)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.onTapGesture {
|
||||
hostsManager.selectTheme(theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +62,7 @@ struct ThemePreview: View {
|
||||
let data = try! Data(contentsOf: url)
|
||||
|
||||
ThemePreview(
|
||||
hostsManager: HostsManager(),
|
||||
theme: Theme.decodeTheme(name: "theme", data: data)!
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user