@@ -5038,61 +5038,174 @@ private struct SessionChart: View {
50385038
50395039struct SettingsScreen : View {
50405040 @EnvironmentObject var state : AppState
5041+
50415042 var body : some View {
5042- ScreenScroll {
5043- settingsCard ( title: " Appearance " ) {
5044- VStack ( alignment: . leading, spacing: 14 ) {
5045- HStack {
5046- Text ( " Theme " ) . font ( . system( size: 13 , weight: . medium) ) . frame ( width: 160 , alignment: . leading)
5047- Picker ( " " , selection: $state. themeRaw) {
5048- Text ( " Light " ) . tag ( " light " )
5049- Text ( " Dark " ) . tag ( " dark " )
5050- } . pickerStyle ( . segmented) . frame ( width: 220 )
5051- }
5052- HStack {
5053- Text ( " Accent " ) . font ( . system( size: 13 , weight: . medium) ) . frame ( width: 160 , alignment: . leading)
5054- HStack ( spacing: 8 ) {
5055- ForEach ( AccentKey . allCases) { k in
5056- Button { state. accent = k } label: {
5057- Circle ( ) . fill ( k. value)
5058- . frame ( width: 22 , height: 22 )
5059- . overlay ( Circle ( ) . stroke ( . white, lineWidth: state. accent == k ? 2 : 0 ) )
5060- . overlay ( Circle ( ) . stroke ( Tokens . border, lineWidth: 1 ) )
5061- } . buttonStyle ( . plain)
5043+ ScrollView {
5044+ VStack ( alignment: . leading, spacing: 28 ) {
5045+ title
5046+ appearanceSection
5047+ generalSection
5048+ permissionsSection
5049+ aboutSection
5050+ }
5051+ . padding ( . horizontal, 32 ) . padding ( . top, 24 ) . padding ( . bottom, 40 )
5052+ . frame ( maxWidth: 640 , alignment: . leading)
5053+ }
5054+ . frame ( maxWidth: . infinity, maxHeight: . infinity, alignment: . topLeading)
5055+ . background ( Tokens . bgWindow)
5056+ }
5057+
5058+ // MARK: Title
5059+
5060+ private var title : some View {
5061+ Text ( " Settings " ) . font ( . system( size: 24 , weight: . bold) )
5062+ }
5063+
5064+ // MARK: Appearance
5065+
5066+ private var appearanceSection : some View {
5067+ section ( " Appearance " ) {
5068+ settingsRow ( label: " Theme " ) {
5069+ Picker ( " " , selection: $state. themeRaw) {
5070+ Text ( " Light " ) . tag ( " light " )
5071+ Text ( " Dark " ) . tag ( " dark " )
5072+ }
5073+ . pickerStyle ( . segmented)
5074+ . labelsHidden ( )
5075+ . frame ( width: 180 )
5076+ }
5077+ divider
5078+ settingsRow ( label: " Accent color " ) {
5079+ HStack ( spacing: 10 ) {
5080+ ForEach ( AccentKey . allCases) { k in
5081+ Button { state. accent = k } label: {
5082+ ZStack {
5083+ Circle ( ) . fill ( k. value) . frame ( width: 20 , height: 20 )
5084+ if state. accent == k {
5085+ Image ( systemName: " checkmark " )
5086+ . font ( . system( size: 9 , weight: . heavy) )
5087+ . foregroundStyle ( . white)
5088+ }
50625089 }
50635090 }
5091+ . buttonStyle ( . plain)
5092+ . help ( k. rawValue. capitalized)
50645093 }
50655094 }
50665095 }
5067- settingsCard ( title: " Modes " ) {
5068- VStack ( spacing: 12 ) {
5069- HStack {
5070- Text ( " Menu bar widget " ) . font ( . system( size: 13 , weight: . medium) ) . frame ( maxWidth: . infinity, alignment: . leading)
5071- AppSwitch ( on: $state. menubarWidgetEnabled)
5072- }
5073- HStack {
5074- Text ( " Replay first scan " ) . font ( . system( size: 13 , weight: . medium) ) . frame ( maxWidth: . infinity, alignment: . leading)
5075- Btn ( label: " Replay " , style: . secondary) { state. replayOnboarding ( ) }
5096+ }
5097+ }
5098+
5099+ // MARK: General
5100+
5101+ private var generalSection : some View {
5102+ section ( " General " ) {
5103+ settingsRow ( label: " Menu bar widget " ,
5104+ sublabel: " Quick storage / RAM / network in your menu bar. " ) {
5105+ Toggle ( " " , isOn: $state. menubarWidgetEnabled)
5106+ . labelsHidden ( ) . toggleStyle ( . switch)
5107+ }
5108+ divider
5109+ settingsRow ( label: " Onboarding " ,
5110+ sublabel: " Replay the welcome scan animation. " ) {
5111+ Button ( " Replay " ) { state. replayOnboarding ( ) }
5112+ . buttonStyle ( . bordered)
5113+ . controlSize ( . small)
5114+ }
5115+ }
5116+ }
5117+
5118+ // MARK: Permissions
5119+
5120+ private var permissionsSection : some View {
5121+ section ( " Permissions " ) {
5122+ settingsRow ( label: " Full Disk Access " ,
5123+ sublabel: state. needsFDA
5124+ ? " Not granted — some scans run with reduced detail. "
5125+ : " Granted. BloatMac can read protected user dirs. " ) {
5126+ HStack ( spacing: 8 ) {
5127+ Button ( " Re-check " ) { state. refreshPermissions ( ) }
5128+ . buttonStyle ( . bordered) . controlSize ( . small)
5129+ Button ( " Open settings " ) {
5130+ state. reopenPermissionsGate ( )
50765131 }
5132+ . buttonStyle ( . borderedProminent) . controlSize ( . small)
50775133 }
50785134 }
5079- settingsCard ( title: " About " ) {
5080- VStack ( alignment: . leading, spacing: 4 ) {
5081- Text ( " BloatMac " ) . font ( . system( size: 14 , weight: . bold) )
5082- Text ( " v 2.4.1 — design preview " ) . font ( . system( size: 11.5 ) ) . foregroundStyle ( Tokens . text3)
5135+ }
5136+ }
5137+
5138+ // MARK: About
5139+
5140+ private var aboutSection : some View {
5141+ section ( " About " ) {
5142+ HStack ( alignment: . center, spacing: 14 ) {
5143+ BrandMark ( size: 44 )
5144+ VStack ( alignment: . leading, spacing: 2 ) {
5145+ Text ( " BloatMac " ) . font ( . system( size: 16 , weight: . bold) )
5146+ Text ( appVersionLabel + buildSuffix)
5147+ . font ( . system( size: 11.5 ) ) . foregroundStyle ( Tokens . text3)
5148+ . monospacedDigit ( )
50835149 }
5150+ Spacer ( )
5151+ Link ( destination: URL ( string: " https://github.com/akhil-gautam/bloat " ) !) {
5152+ HStack ( spacing: 4 ) {
5153+ Text ( " View on GitHub " )
5154+ Image ( systemName: " arrow.up.right.square " )
5155+ }
5156+ . font ( . system( size: 11.5 , weight: . medium) )
5157+ }
5158+ . buttonStyle ( . plain)
5159+ . foregroundStyle ( state. accent. value)
50845160 }
5161+ . padding ( . horizontal, 16 ) . padding ( . vertical, 14 )
5162+ }
5163+ }
5164+
5165+ private var buildSuffix : String {
5166+ // CFBundleVersion sometimes mirrors the marketing version; only show
5167+ // it when it adds information (i.e. it's different).
5168+ let info = Bundle . main. infoDictionary
5169+ let short = info ? [ " CFBundleShortVersionString " ] as? String ?? " "
5170+ let build = info ? [ " CFBundleVersion " ] as? String ?? " "
5171+ return ( build. isEmpty || build == short) ? " " : " ( \( build) ) "
5172+ }
5173+
5174+ // MARK: Building blocks
5175+
5176+ @ViewBuilder
5177+ private func section< C: View > ( _ title: String , @ViewBuilder _ content: ( ) -> C ) -> some View {
5178+ VStack ( alignment: . leading, spacing: 8 ) {
5179+ Text ( title. uppercased ( ) )
5180+ . font ( . system( size: 11 , weight: . semibold) )
5181+ . tracking ( 0.6 )
5182+ . foregroundStyle ( Tokens . text3)
5183+ VStack ( spacing: 0 ) { content ( ) }
5184+ . background ( Tokens . bgPanel)
5185+ . overlay ( RoundedRectangle ( cornerRadius: 10 ) . stroke ( Tokens . border) )
5186+ . clipShape ( RoundedRectangle ( cornerRadius: 10 ) )
50855187 }
50865188 }
50875189
50885190 @ViewBuilder
5089- private func settingsCard< C: View > ( title: String , @ViewBuilder _ content: ( ) -> C ) -> some View {
5090- VStack ( alignment: . leading, spacing: 12 ) {
5091- Text ( title) . font ( . system( size: 14 , weight: . bold) )
5092- content ( )
5191+ private func settingsRow< C: View > ( label: String ,
5192+ sublabel: String ? = nil ,
5193+ @ViewBuilder _ control: ( ) -> C ) -> some View {
5194+ HStack ( alignment: . center, spacing: 16 ) {
5195+ VStack ( alignment: . leading, spacing: 2 ) {
5196+ Text ( label) . font ( . system( size: 13 , weight: . regular) ) . foregroundStyle ( Tokens . text)
5197+ if let sub = sublabel {
5198+ Text ( sub) . font ( . system( size: 11 ) ) . foregroundStyle ( Tokens . text3)
5199+ . fixedSize ( horizontal: false , vertical: true )
5200+ }
5201+ }
5202+ Spacer ( )
5203+ control ( )
50935204 }
5094- . padding ( 18 )
5095- . frame ( maxWidth: . infinity, alignment: . leading)
5096- . metallic ( radius: Tokens . Radius. lg)
5205+ . padding ( . horizontal, 16 ) . padding ( . vertical, 12 )
5206+ }
5207+
5208+ private var divider : some View {
5209+ Divider ( ) . padding ( . leading, 16 ) . opacity ( 0.6 )
50975210 }
50985211}
0 commit comments