|
|
@@ -45,6 +45,11 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
45
|
45
|
static let selectionFillHover = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.2)
|
|
46
|
46
|
static let neutralHoverFill = NSColor(srgbRed: 240 / 255, green: 240 / 255, blue: 240 / 255, alpha: 1)
|
|
47
|
47
|
static let sidebarRowHoverFill = NSColor(srgbRed: 0, green: 0, blue: 0, alpha: 0.04)
|
|
|
48
|
+ static let settingsPageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
|
49
|
+ static let settingsGroupBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
|
50
|
+ static let settingsIconBackground = NSColor(srgbRed: 239 / 255, green: 244 / 255, blue: 252 / 255, alpha: 1)
|
|
|
51
|
+ static let settingsDivider = NSColor(srgbRed: 228 / 255, green: 228 / 255, blue: 228 / 255, alpha: 1)
|
|
|
52
|
+ static let settingsPrimaryText = NSColor(srgbRed: 45 / 255, green: 45 / 255, blue: 45 / 255, alpha: 1)
|
|
48
|
53
|
}
|
|
49
|
54
|
|
|
50
|
55
|
/// Multiline `NSTextField` often ignores `alignment` for wrapped runs; explicit paragraph alignment matches the title.
|
|
|
@@ -91,6 +96,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
91
|
96
|
private let savedJobsScrollView = NSScrollView()
|
|
92
|
97
|
private let savedJobsDocumentView = JobListingsDocumentView()
|
|
93
|
98
|
private let savedJobsStack = NSStackView()
|
|
|
99
|
+ private let settingsPageContainer = NSView()
|
|
|
100
|
+ private let themeControl = NSSegmentedControl(labels: ["System", "Light", "Dark"], trackingMode: .selectOne, target: nil, action: nil)
|
|
94
|
101
|
|
|
95
|
102
|
private var currentSidebarItems: [SidebarItem] = []
|
|
96
|
103
|
private var selectedSidebarIndex: Int = 0
|
|
|
@@ -769,8 +776,10 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
769
|
776
|
|
|
770
|
777
|
nonHomeGenericContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
771
|
778
|
savedJobsPageContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
779
|
+ settingsPageContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
772
|
780
|
nonHomeHost.addSubview(nonHomeGenericContainer)
|
|
773
|
781
|
nonHomeHost.addSubview(savedJobsPageContainer)
|
|
|
782
|
+ nonHomeHost.addSubview(settingsPageContainer)
|
|
774
|
783
|
|
|
775
|
784
|
NSLayoutConstraint.activate([
|
|
776
|
785
|
nonHomeGenericContainer.leadingAnchor.constraint(equalTo: nonHomeHost.leadingAnchor),
|
|
|
@@ -781,7 +790,12 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
781
|
790
|
savedJobsPageContainer.leadingAnchor.constraint(equalTo: nonHomeHost.leadingAnchor),
|
|
782
|
791
|
savedJobsPageContainer.trailingAnchor.constraint(equalTo: nonHomeHost.trailingAnchor),
|
|
783
|
792
|
savedJobsPageContainer.topAnchor.constraint(equalTo: nonHomeHost.topAnchor),
|
|
784
|
|
- savedJobsPageContainer.bottomAnchor.constraint(equalTo: nonHomeHost.bottomAnchor)
|
|
|
793
|
+ savedJobsPageContainer.bottomAnchor.constraint(equalTo: nonHomeHost.bottomAnchor),
|
|
|
794
|
+
|
|
|
795
|
+ settingsPageContainer.leadingAnchor.constraint(equalTo: nonHomeHost.leadingAnchor),
|
|
|
796
|
+ settingsPageContainer.trailingAnchor.constraint(equalTo: nonHomeHost.trailingAnchor),
|
|
|
797
|
+ settingsPageContainer.topAnchor.constraint(equalTo: nonHomeHost.topAnchor),
|
|
|
798
|
+ settingsPageContainer.bottomAnchor.constraint(equalTo: nonHomeHost.bottomAnchor)
|
|
785
|
799
|
])
|
|
786
|
800
|
|
|
787
|
801
|
nonHomeTitleLabel.font = .systemFont(ofSize: 22, weight: .bold)
|
|
|
@@ -872,6 +886,162 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
872
|
886
|
savedJobsDocumentView.leadingAnchor.constraint(equalTo: savedJobsScrollView.contentView.leadingAnchor),
|
|
873
|
887
|
savedJobsDocumentView.widthAnchor.constraint(equalTo: savedJobsScrollView.contentView.widthAnchor)
|
|
874
|
888
|
])
|
|
|
889
|
+
|
|
|
890
|
+ configureSettingsPage()
|
|
|
891
|
+ }
|
|
|
892
|
+
|
|
|
893
|
+ private func configureSettingsPage() {
|
|
|
894
|
+ settingsPageContainer.wantsLayer = true
|
|
|
895
|
+ settingsPageContainer.layer?.backgroundColor = Theme.settingsPageBackground.cgColor
|
|
|
896
|
+ settingsPageContainer.isHidden = true
|
|
|
897
|
+
|
|
|
898
|
+ let contentStack = NSStackView()
|
|
|
899
|
+ contentStack.orientation = .vertical
|
|
|
900
|
+ contentStack.spacing = 26
|
|
|
901
|
+ contentStack.alignment = .leading
|
|
|
902
|
+ contentStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
903
|
+
|
|
|
904
|
+ let settingsSection = makeSettingsSection(rows: [
|
|
|
905
|
+ makeSettingsRow(title: "Share App", systemImage: "square.and.arrow.up", accessory: nil),
|
|
|
906
|
+ makeSettingsRow(title: "Theme", systemImage: "circle.lefthalf.filled", accessory: makeThemeControl()),
|
|
|
907
|
+ makeSettingsRow(title: "More Apps", systemImage: "square.grid.2x2", accessory: nil)
|
|
|
908
|
+ ])
|
|
|
909
|
+
|
|
|
910
|
+ let aboutTitle = NSTextField(labelWithString: "About")
|
|
|
911
|
+ aboutTitle.font = .systemFont(ofSize: 15, weight: .bold)
|
|
|
912
|
+ aboutTitle.textColor = Theme.settingsPrimaryText
|
|
|
913
|
+ aboutTitle.alignment = .left
|
|
|
914
|
+
|
|
|
915
|
+ let aboutSection = makeSettingsSection(rows: [
|
|
|
916
|
+ makeSettingsRow(title: "Support", systemImage: "questionmark.circle", accessory: nil),
|
|
|
917
|
+ makeSettingsRow(title: "Terms of Use", systemImage: "doc.text", accessory: nil),
|
|
|
918
|
+ makeSettingsRow(title: "Privacy Policy", systemImage: "shield", accessory: nil)
|
|
|
919
|
+ ])
|
|
|
920
|
+
|
|
|
921
|
+ let aboutStack = NSStackView(views: [aboutTitle, aboutSection])
|
|
|
922
|
+ aboutStack.orientation = .vertical
|
|
|
923
|
+ aboutStack.spacing = 14
|
|
|
924
|
+ aboutStack.alignment = .leading
|
|
|
925
|
+ aboutStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
926
|
+
|
|
|
927
|
+ contentStack.addArrangedSubview(settingsSection)
|
|
|
928
|
+ contentStack.addArrangedSubview(aboutStack)
|
|
|
929
|
+ settingsPageContainer.addSubview(contentStack)
|
|
|
930
|
+
|
|
|
931
|
+ NSLayoutConstraint.activate([
|
|
|
932
|
+ contentStack.leadingAnchor.constraint(equalTo: settingsPageContainer.leadingAnchor, constant: 42),
|
|
|
933
|
+ contentStack.trailingAnchor.constraint(lessThanOrEqualTo: settingsPageContainer.trailingAnchor, constant: -42),
|
|
|
934
|
+ contentStack.topAnchor.constraint(equalTo: settingsPageContainer.topAnchor, constant: 48),
|
|
|
935
|
+ settingsSection.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
|
|
|
936
|
+ aboutStack.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
|
|
|
937
|
+ aboutSection.widthAnchor.constraint(equalTo: aboutStack.widthAnchor),
|
|
|
938
|
+ contentStack.widthAnchor.constraint(equalTo: settingsPageContainer.widthAnchor, constant: -84)
|
|
|
939
|
+ ])
|
|
|
940
|
+ }
|
|
|
941
|
+
|
|
|
942
|
+ private func makeThemeControl() -> NSSegmentedControl {
|
|
|
943
|
+ themeControl.target = self
|
|
|
944
|
+ themeControl.action = #selector(didChangeThemeSelection(_:))
|
|
|
945
|
+ themeControl.selectedSegment = 0
|
|
|
946
|
+ themeControl.segmentStyle = .rounded
|
|
|
947
|
+ themeControl.controlSize = .large
|
|
|
948
|
+ themeControl.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
|
949
|
+ themeControl.translatesAutoresizingMaskIntoConstraints = false
|
|
|
950
|
+ themeControl.widthAnchor.constraint(equalToConstant: 204).isActive = true
|
|
|
951
|
+ themeControl.heightAnchor.constraint(equalToConstant: 30).isActive = true
|
|
|
952
|
+ return themeControl
|
|
|
953
|
+ }
|
|
|
954
|
+
|
|
|
955
|
+ private func makeSettingsSection(rows: [NSView]) -> NSView {
|
|
|
956
|
+ let section = NSStackView()
|
|
|
957
|
+ section.orientation = .vertical
|
|
|
958
|
+ section.spacing = 0
|
|
|
959
|
+ section.alignment = .leading
|
|
|
960
|
+ section.translatesAutoresizingMaskIntoConstraints = false
|
|
|
961
|
+ section.wantsLayer = true
|
|
|
962
|
+ section.layer?.backgroundColor = Theme.settingsGroupBackground.cgColor
|
|
|
963
|
+ section.layer?.cornerRadius = 14
|
|
|
964
|
+ section.layer?.borderWidth = 1
|
|
|
965
|
+ section.layer?.borderColor = Theme.border.cgColor
|
|
|
966
|
+ section.layer?.masksToBounds = true
|
|
|
967
|
+
|
|
|
968
|
+ for (index, row) in rows.enumerated() {
|
|
|
969
|
+ section.addArrangedSubview(row)
|
|
|
970
|
+ row.widthAnchor.constraint(equalTo: section.widthAnchor).isActive = true
|
|
|
971
|
+
|
|
|
972
|
+ if index < rows.count - 1 {
|
|
|
973
|
+ let divider = NSView()
|
|
|
974
|
+ divider.translatesAutoresizingMaskIntoConstraints = false
|
|
|
975
|
+ divider.wantsLayer = true
|
|
|
976
|
+ divider.layer?.backgroundColor = Theme.settingsDivider.cgColor
|
|
|
977
|
+ section.addArrangedSubview(divider)
|
|
|
978
|
+ NSLayoutConstraint.activate([
|
|
|
979
|
+ divider.heightAnchor.constraint(equalToConstant: 1),
|
|
|
980
|
+ divider.leadingAnchor.constraint(equalTo: section.leadingAnchor),
|
|
|
981
|
+ divider.trailingAnchor.constraint(equalTo: section.trailingAnchor)
|
|
|
982
|
+ ])
|
|
|
983
|
+ }
|
|
|
984
|
+ }
|
|
|
985
|
+
|
|
|
986
|
+ return section
|
|
|
987
|
+ }
|
|
|
988
|
+
|
|
|
989
|
+ private func makeSettingsRow(title: String, systemImage: String, accessory: NSView?) -> NSView {
|
|
|
990
|
+ let row = NSView()
|
|
|
991
|
+ row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
992
|
+ row.wantsLayer = true
|
|
|
993
|
+
|
|
|
994
|
+ let iconTile = NSView()
|
|
|
995
|
+ iconTile.translatesAutoresizingMaskIntoConstraints = false
|
|
|
996
|
+ iconTile.wantsLayer = true
|
|
|
997
|
+ iconTile.layer?.backgroundColor = Theme.settingsIconBackground.cgColor
|
|
|
998
|
+ iconTile.layer?.cornerRadius = 9
|
|
|
999
|
+
|
|
|
1000
|
+ let icon = NSImageView()
|
|
|
1001
|
+ icon.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1002
|
+ icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
|
|
|
1003
|
+ icon.image = NSImage(systemSymbolName: systemImage, accessibilityDescription: title)
|
|
|
1004
|
+ icon.contentTintColor = Theme.brandBlue
|
|
|
1005
|
+
|
|
|
1006
|
+ let titleLabel = NSTextField(labelWithString: title)
|
|
|
1007
|
+ titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
|
|
|
1008
|
+ titleLabel.textColor = Theme.settingsPrimaryText
|
|
|
1009
|
+ titleLabel.alignment = .left
|
|
|
1010
|
+
|
|
|
1011
|
+ let rowStack = NSStackView()
|
|
|
1012
|
+ rowStack.orientation = .horizontal
|
|
|
1013
|
+ rowStack.spacing = 16
|
|
|
1014
|
+ rowStack.alignment = .centerY
|
|
|
1015
|
+ rowStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1016
|
+
|
|
|
1017
|
+ let spacer = NSView()
|
|
|
1018
|
+ spacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1019
|
+ spacer.setContentHuggingPriority(NSLayoutConstraint.Priority(1), for: .horizontal)
|
|
|
1020
|
+
|
|
|
1021
|
+ iconTile.addSubview(icon)
|
|
|
1022
|
+ rowStack.addArrangedSubview(iconTile)
|
|
|
1023
|
+ rowStack.addArrangedSubview(titleLabel)
|
|
|
1024
|
+ rowStack.addArrangedSubview(spacer)
|
|
|
1025
|
+ if let accessory {
|
|
|
1026
|
+ rowStack.addArrangedSubview(accessory)
|
|
|
1027
|
+ }
|
|
|
1028
|
+ row.addSubview(rowStack)
|
|
|
1029
|
+
|
|
|
1030
|
+ NSLayoutConstraint.activate([
|
|
|
1031
|
+ row.heightAnchor.constraint(equalToConstant: 68),
|
|
|
1032
|
+ iconTile.widthAnchor.constraint(equalToConstant: 38),
|
|
|
1033
|
+ iconTile.heightAnchor.constraint(equalToConstant: 38),
|
|
|
1034
|
+ icon.centerXAnchor.constraint(equalTo: iconTile.centerXAnchor),
|
|
|
1035
|
+ icon.centerYAnchor.constraint(equalTo: iconTile.centerYAnchor),
|
|
|
1036
|
+ icon.widthAnchor.constraint(equalToConstant: 20),
|
|
|
1037
|
+ icon.heightAnchor.constraint(equalToConstant: 20),
|
|
|
1038
|
+ rowStack.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 16),
|
|
|
1039
|
+ rowStack.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -16),
|
|
|
1040
|
+ rowStack.topAnchor.constraint(equalTo: row.topAnchor),
|
|
|
1041
|
+ rowStack.bottomAnchor.constraint(equalTo: row.bottomAnchor)
|
|
|
1042
|
+ ])
|
|
|
1043
|
+
|
|
|
1044
|
+ return row
|
|
875
|
1045
|
}
|
|
876
|
1046
|
|
|
877
|
1047
|
private func reloadSavedJobsListings() {
|
|
|
@@ -909,16 +1079,25 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
909
|
1079
|
return currentSidebarItems[index].title == "Home"
|
|
910
|
1080
|
}
|
|
911
|
1081
|
|
|
|
1082
|
+ private func isSettingsSidebarIndex(_ index: Int) -> Bool {
|
|
|
1083
|
+ guard index >= 0, index < currentSidebarItems.count else { return false }
|
|
|
1084
|
+ return currentSidebarItems[index].title == "Settings"
|
|
|
1085
|
+ }
|
|
|
1086
|
+
|
|
912
|
1087
|
private func updateMainContentVisibility() {
|
|
913
|
1088
|
let home = isHomeSidebarIndex(selectedSidebarIndex)
|
|
914
|
1089
|
let savedJobs = isSavedJobsSidebarIndex(selectedSidebarIndex)
|
|
|
1090
|
+ let settings = isSettingsSidebarIndex(selectedSidebarIndex)
|
|
915
|
1091
|
mainOverlay.isHidden = !home
|
|
916
|
1092
|
nonHomeHost.isHidden = home
|
|
917
|
|
- nonHomeGenericContainer.isHidden = savedJobs
|
|
|
1093
|
+ nonHomeGenericContainer.isHidden = savedJobs || settings
|
|
918
|
1094
|
savedJobsPageContainer.isHidden = !savedJobs
|
|
|
1095
|
+ settingsPageContainer.isHidden = !settings
|
|
919
|
1096
|
if !home, selectedSidebarIndex < currentSidebarItems.count {
|
|
920
|
1097
|
if savedJobs {
|
|
921
|
1098
|
reloadSavedJobsListings()
|
|
|
1099
|
+ } else if settings {
|
|
|
1100
|
+ window?.makeFirstResponder(nil)
|
|
922
|
1101
|
} else {
|
|
923
|
1102
|
nonHomeTitleLabel.stringValue = currentSidebarItems[selectedSidebarIndex].title
|
|
924
|
1103
|
}
|
|
|
@@ -1186,6 +1365,17 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
1186
|
1365
|
NSWorkspace.shared.open(url)
|
|
1187
|
1366
|
}
|
|
1188
|
1367
|
|
|
|
1368
|
+ @objc private func didChangeThemeSelection(_ sender: NSSegmentedControl) {
|
|
|
1369
|
+ switch sender.selectedSegment {
|
|
|
1370
|
+ case 1:
|
|
|
1371
|
+ NSApp.appearance = NSAppearance(named: .aqua)
|
|
|
1372
|
+ case 2:
|
|
|
1373
|
+ NSApp.appearance = NSAppearance(named: .darkAqua)
|
|
|
1374
|
+ default:
|
|
|
1375
|
+ NSApp.appearance = nil
|
|
|
1376
|
+ }
|
|
|
1377
|
+ }
|
|
|
1378
|
+
|
|
1189
|
1379
|
private func selectSidebarItem(at index: Int) {
|
|
1190
|
1380
|
guard index >= 0, index < currentSidebarItems.count else { return }
|
|
1191
|
1381
|
let selectingHome = isHomeSidebarIndex(index)
|