2024-02-29 17:38:44 +00:00
//
// C o n t e n t V i e w . s w i f t
// B u r g e r n o t e s
//
// C r e a t e d b y f f q q o n 2 3 / 0 2 / 2 0 2 4 .
//
// C O N T E N T V I E W
//
// T h i s i s w h e r e t h e a p p l i v e s . E v e r y t h i n g t h a t h a p p e n s ,
// h a p p e n s h e r e .
//
// ( T h i s ' l l n e e d a r e w r i t e s o m e d a y )
import SwiftUI
import CryptoKit
import SafariServices
import KeychainSwift
import JavaScriptCore
struct ContentView : View {
// I n i t i a l i z e e v e r y t h i n g
@ State private var username = " "
@ State private var password = " "
@ State private var notes : [ Note ] = [ ]
@ State private var noteContent = " "
@ State private var selectedNote = " "
@ State private var showAddNote = false
// U s e r i n f o
@ State private var noteCount : Int = 0
@ State private var sessionID : Int = 0
@ State private var maxStorage = " "
@ State private var usedStorage = " "
// U I a c t i v a t o r s
@ State private var isEditing = false
@ State private var usingSettings = false
@ State private var isOnline = Reach ( ) . isConnectedToNetwork ( )
@ State private var loggingIn = true
@ State private var signingUp = false
@ State private var showErrorLabel = false
@ State private var errorMessage = " "
@ AppStorage ( " Username " ) var storedUsername : String ? // U s e r n a m e s a r e s t o r e d i n U s e r D e f a u l t s f o r t h e s a k e o f c o n v e n i e n c e
let keychain = KeychainSwift ( )
2024-03-03 16:38:45 +00:00
2024-02-29 17:38:44 +00:00
func login ( ) {
let hashHelper = HashHelper ( )
// U s e H a s h H e l p e r t o h a s h o u r p a s s w o r d
let hashedPassword = hashHelper . hashPassword_sha3 ( password )
let SHA512key = hashHelper . hashPassword_sha512 ( password )
if password . count < 8 {
showErrorLabel = true
errorMessage = " Password must be 8+ characters! "
return
}
if loggingIn {
// P r e p a r e d a t a
let parameters = [ " username " : username , " password " : hashedPassword , " passwordchange " : " no " , " newpass " : " null " ] // L e g a c y a c c o u n t s s h o u l d b e m i g r a t e d f r o m A r g o n 2 t h r o u g h t h e b r o w s e r .
// C r e a t e t h e U R L r e q u e s t
guard let url = URL ( string : " https://notes.hectabit.org/api/login " ) else {
showErrorLabel = true
errorMessage = " Invalid URL " // U s u a l l y w e ' d d o ` g u a r d l e t u r l = U R L ( s t r i n g : " h t t p s : / / f o o . b a r / a p i / f o o " ) e l s e { r e t u r n } ` , b u t w e w i l l d i s p l a y a u s e r - v i s i b l e e r r o r f o r t h e s a k e o f U X .
return
}
JSONHelper . sendJSONRequest ( url : url , parameters : parameters as [ String : Any ] ) { result in
switch result {
case . success ( let data ) :
if let data = data {
// H a n d l e t h e r e s p o n s e
if let json = try ? JSONSerialization . jsonObject ( with : data , options : [ ] ) as ? [ String : Any ] ,
let keyValue = JSONHelper . processJSONResponse ( json : json , key : " key " ) {
let secretKey = keyValue
keychain . delete ( " secretKey " )
keychain . set ( secretKey , forKey : " secretKey " )
storedUsername = username // T h i s w i l l a l s o t r i g g e r S w i f t U I t o c h a n g e v s t a c k s
keychain . delete ( " encryptionKey " )
keychain . set ( SHA512key , forKey : " encryptionKey " )
password = " "
}
} else {
// S h o w e r r o r l a b e l
showErrorLabel = true
errorMessage = " Unknown error "
}
case . failure ( let error ) :
if ( error as NSError ) . code = = 401 {
showErrorLabel = true
errorMessage = " Invalid username or password. "
} else {
// S h o w e r r o r l a b e l
showErrorLabel = true
errorMessage = error . localizedDescription
}
}
}
}
if signingUp {
// P r e p a r e d a t a
let parameters = [ " username " : username , " password " : hashedPassword ] // L e g a c y a c c o u n t s s h o u l d b e m i g r a t e d f r o m A r g o n 2 t h r o u g h t h e b r o w s e r .
// C r e a t e t h e U R L r e q u e s t
guard let url = URL ( string : " https://notes.hectabit.org/api/signup " ) else {
showErrorLabel = true
errorMessage = " Invalid URL " // U s u a l l y w e ' d d o ` g u a r d l e t u r l = U R L ( s t r i n g : " h t t p s : / / f o o . b a r / a p i / f o o " ) e l s e { r e t u r n } ` , b u t w e w i l l d i s p l a y a u s e r - v i s i b l e e r r o r f o r t h e s a k e o f U X .
return
}
JSONHelper . sendJSONRequest ( url : url , parameters : parameters as [ String : Any ] ) { result in
switch result {
case . success ( let data ) :
if let data = data {
// H a n d l e t h e r e s p o n s e
if let json = try ? JSONSerialization . jsonObject ( with : data , options : [ ] ) as ? [ String : Any ] ,
let keyValue = JSONHelper . processJSONResponse ( json : json , key : " key " ) {
let secretKey = keyValue
keychain . delete ( " secretKey " )
keychain . set ( secretKey , forKey : " secretKey " )
storedUsername = username // T h i s w i l l a l s o t r i g g e r S w i f t U I t o c h a n g e v s t a c k s
keychain . delete ( " encryptionKey " )
keychain . set ( SHA512key , forKey : " encryptionKey " )
signingUp = false
loggingIn = true
password = " "
}
} else {
// S h o w e r r o r l a b e l
showErrorLabel = true
errorMessage = " Unknown error "
}
case . failure ( let error ) :
if ( error as NSError ) . code = = 409 {
// S h o w e r r o r l a b e l
showErrorLabel = true
errorMessage = " Username already taken! "
} else {
// S h o w e r r o r l a b e l
showErrorLabel = true
errorMessage = error . localizedDescription
}
}
}
}
}
var body : some View {
if storedUsername != nil {
// N o t e l i s t
let secretKey = keychain . get ( " secretKey " )
let encryptionKey = keychain . get ( " encryptionKey " )
VStack {
HStack {
// R e f r e s h n o t e s
Button ( action : {
fetchNotes ( ) // R e f r e s h n o t e s
} ) {
Image ( systemName : " arrow.clockwise.circle " )
. font ( . title )
}
. disabled ( isEditing ) . disabled ( usingSettings ) . disabled ( ! isOnline ) // M a k e t h e b u t t o n s d i s a b l e d w h i l e t h e n o t e e d i t o r ( o r s e t t i n g s ) i s o p e n , o r w h i l e o f f l i n e .
Spacer ( )
// S e t t i n g s
Button ( action : {
if usingSettings = = false {
fetchUserInfo ( )
usingSettings = true
} else {
usingSettings = false
}
} ) {
Image ( systemName : " gear " )
. font ( . title )
}
. disabled ( isEditing ) . disabled ( ! isOnline ) // M a k e t h e b u t t o n s d i s a b l e d w h i l e t h e n o t e e d i t o r i s o p e n , o r w h i l e o f f l i n e .
// N e w n o t e
Button ( action : {
// N e w n o t e c r e a t i o n
let dialog = UIAlertController ( title : " New Note " , message : " Enter note name " , preferredStyle : . alert )
dialog . addTextField { $0 . placeholder = " My Diary " }
let cancelAction = UIAlertAction ( title : " Cancel " , style : . cancel )
let createAction = UIAlertAction ( title : " Create " , style : . default ) { _ in
if let noteName = dialog . textFields ? . first ? . text , let url = URL ( string : " https://notes.hectabit.org/api/newnote " ) {
let parameters = [ " secretKey " : " \( secretKey ? ? " bum " ) " , " noteName " : " \( noteName . encrypt ( password : encryptionKey ? ? " this bum ain't got an encryption key!!! " ) ) " ]
JSONHelper . sendJSONRequest ( url : url , parameters : parameters ) { result in
switch result {
case . success :
fetchNotes ( )
case . failure ( let error ) :
print ( " Error sending JSON request: \( error ) " )
}
}
}
fetchNotes ( )
}
dialog . addAction ( cancelAction ) ; dialog . addAction ( createAction )
if let topVC = UIApplication . shared . keyWindow ? . rootViewController { topVC . present ( dialog , animated : true ) } // W e c a n k e e p u s i n g k e y W i n d o w , i t ' s n o t a b i g d e a l f o r t h i s s p e c i f i c u s e c a s e t h a t i t ' s d e p r e c a t e d
} ) {
Image ( systemName : " plus.circle " )
. font ( . title )
}
. disabled ( isEditing ) . disabled ( usingSettings ) . disabled ( ! isOnline ) // M a k e t h e b u t t o n s d i s a b l e d w h i l e t h e n o t e e d i t o r ( o r s e t t i n g s ) i s o p e n , o r w h i l e o f f l i n e .
}
. padding ( )
Group {
if isOnline {
if ! usingSettings {
if ! isEditing {
2024-03-03 16:38:45 +00:00
Text ( " Burgernotes " )
. font ( . title )
. padding ( )
2024-02-29 17:38:44 +00:00
List {
ForEach ( notes , id : \ . id ) { note in
let noteTitle = note . title . decrypt ( password : encryptionKey ? ? " this bum ain't got an encryption key!!! " )
Button ( action : {
fetchNoteContent ( note : note )
} ) {
Text ( noteTitle )
}
. swipeActions ( edge : . trailing , allowsFullSwipe : false ) {
Button ( action : {
guard let url = URL ( string : " https://notes.hectabit.org/api/removenote " ) else { return }
let parameters = [ " secretKey " : " \( secretKey ? ? " bum " ) " , " noteId " : " \( note . id ) " ]
JSONHelper . sendJSONRequest ( url : url , parameters : parameters ) { result in
switch result {
case . success ( _ ) :
print ( " Note deleted successfully. " )
withAnimation {
notes . removeAll ( where : { $0 . id = = note . id } )
}
case . failure ( let error ) :
print ( " Error: \( error ) " )
}
}
} ) {
Image ( systemName : " trash " )
}
. tint ( . red )
. animation ( . default , value : notes )
}
}
}
. listStyle ( . plain )
Spacer ( )
}
}
}
if ! isOnline {
Text ( " You are currently offline. " )
. foregroundStyle ( Color . red )
Button ( action : {
isOnline = Reach ( ) . isConnectedToNetwork ( )
if isOnline {
fetchNotes ( )
}
} ) {
Text ( " Refresh connection status " )
. padding ( )
. foregroundStyle ( Color . blue )
}
Spacer ( )
}
if isEditing {
TextEditor ( text : $ noteContent )
. padding ( )
HStack {
Button ( action : {
isEditing = false
noteContent = " "
} ) {
Image ( systemName : " chevron.left " )
. imageScale ( . large )
2024-03-03 16:38:45 +00:00
Text ( " Back " )
2024-02-29 17:38:44 +00:00
}
. padding ( )
Spacer ( )
Button ( action : {
saveEditedNoteContent ( )
} ) {
Text ( " Save " )
}
. padding ( )
}
} else {
Text ( noteContent )
. padding ( )
. onTapGesture {
isEditing = true
}
}
}
. padding ( )
. onAppear {
fetchNotes ( )
}
}
if usingSettings {
List {
Section {
Text ( ( storedUsername ? ? " No stored username :( " ) )
} header : {
Text ( " Username " )
}
Section {
Text ( maxStorage )
} header : {
Text ( " Maximum available storage " )
}
Section {
Text ( usedStorage )
} header : {
Text ( " Used storage " )
}
Button ( action : {
let dialog = UIAlertController ( title : " Sign out " , message : " Are you sure you want to sign out? " , preferredStyle : . alert )
// I n t h e c a s e o f a s i g n o u t :
let signOut = UIAlertAction ( title : " Yes " , style : . destructive ) { _ in
fetchSessionID ( )
guard let url = URL ( string : " https://notes.hectabit.org/api/sessions/remove " ) else { return }
let parameters = [ " secretKey " : " \( secretKey ? ? " bum " ) " , " sessionId " : sessionID ]
JSONHelper . sendJSONRequest ( url : url , parameters : parameters ) { result in
switch result {
case . success :
keychain . delete ( " secretKey " )
UserDefaults . standard . removeObject ( forKey : " Username " ) // T h i s w i l l a l s o t r i g g e r S w i f t U I t o c h a n g e v s t a c k s
keychain . delete ( " encryptionKey " )
usingSettings = false
case . failure ( let error ) :
print ( " Failed to sign out: \( error ) " ) // T h i s i s n o t s u p p o s e d t o h a p p e n u n d e r a n y c i r c u m s t a n c e s u n l e s s t h e s e s s i o n w a s d e a u t h o r i z e d r e m o t e l y
}
}
}
let no = UIAlertAction ( title : " No " , style : . default )
dialog . addAction ( signOut ) ; dialog . addAction ( no )
if let topVC = UIApplication . shared . keyWindow ? . rootViewController { topVC . present ( dialog , animated : true ) } // W e c a n k e e p u s i n g k e y W i n d o w , i t ' s n o t a b i g d e a l f o r t h i s s p e c i f i c u s e c a s e t h a t i t ' s d e p r e c a t e d
} ) {
Text ( " Sign out " )
. foregroundStyle ( Color . red ) // S e t t h e t e x t c o l o r t o r e d
}
2024-03-03 16:38:45 +00:00
Button ( action : {
let dialog = UIAlertController ( title : " Delete my account " , message : " Are you sure you want to delete your account? THIS CANNOT BE REVERSED " , preferredStyle : . alert )
// I n t h e c a s e o f a s i g n o u t :
let deleteAccount = UIAlertAction ( title : " Yes " , style : . destructive ) { _ in
guard let url = URL ( string : " https://notes.hectabit.org/api/deleteaccount " ) else { return }
let parameters = [ " secretKey " : " \( secretKey ? ? " bum " ) " ]
JSONHelper . sendJSONRequest ( url : url , parameters : parameters ) { result in
switch result {
case . success :
keychain . delete ( " secretKey " )
UserDefaults . standard . removeObject ( forKey : " Username " ) // T h i s w i l l a l s o t r i g g e r S w i f t U I t o c h a n g e v s t a c k s
keychain . delete ( " encryptionKey " )
usingSettings = false
case . failure ( let error ) :
print ( " Failed to delete account: \( error ) " ) // T h i s i s n o t s u p p o s e d t o h a p p e n u n d e r a n y c i r c u m s t a n c e s u n l e s s t h e s e s s i o n w a s d e a u t h o r i z e d r e m o t e l y
}
}
}
let no = UIAlertAction ( title : " No " , style : . default )
dialog . addAction ( deleteAccount ) ; dialog . addAction ( no )
if let topVC = UIApplication . shared . keyWindow ? . rootViewController { topVC . present ( dialog , animated : true ) } // W e c a n k e e p u s i n g k e y W i n d o w , i t ' s n o t a b i g d e a l f o r t h i s s p e c i f i c u s e c a s e t h a t i t ' s d e p r e c a t e d
} ) {
Text ( " Delete my account " )
. foregroundStyle ( Color . red ) // S e t t h e t e x t c o l o r t o r e d
}
2024-02-29 17:38:44 +00:00
}
}
} else {
// L o g i n s c r e e n
VStack {
Image ( " org.hectabit.burgernotes " )
. resizable ( )
. frame ( width : 100 , height : 100 )
. padding ( . bottom , 16 )
Text ( " Burgernotes " )
. font ( . title )
. fontWeight ( . bold )
Text ( " Secure, encrypted notes. " )
. padding ( . bottom , 16 )
VStack ( spacing : 16 ) {
TextField ( " Username " , text : $ username )
. textFieldStyle ( RoundedBorderTextFieldStyle ( ) )
. autocapitalization ( . none )
SecureField ( " Password " , text : $ password )
. textFieldStyle ( RoundedBorderTextFieldStyle ( ) )
. autocapitalization ( . none )
Button ( action : login ) {
Text ( loggingIn ? " Sign In " : ( signingUp ? " Sign Up " : " " ) )
. foregroundColor ( . white )
. font ( . headline )
. frame ( maxWidth : . infinity )
. padding ( )
. background ( Color . accentColor )
. cornerRadius ( 8 )
}
HStack {
Button ( action : {
if signingUp {
signingUp = false
loggingIn = true
} else if loggingIn {
loggingIn = false
signingUp = true
}
} ) {
Text ( loggingIn ? " Sign up instead " : ( signingUp ? " Sign in instead " : " " ) )
. font ( . body )
. foregroundColor ( . blue )
}
Text ( " | " )
. font ( . body )
. foregroundColor ( . blue )
Button ( action : {
if let url = URL ( string : " https://notes.hectabit.org/privacy " ) {
let safariViewController = SFSafariViewController ( url : url )
UIApplication . shared . windows . first ? . rootViewController ? . present ( safariViewController , animated : true , completion : nil )
}
} ) {
Text ( " Privacy Policy " )
. font ( . body )
. foregroundColor ( . blue )
}
}
Text ( errorMessage )
. foregroundColor ( . red )
. opacity ( showErrorLabel ? 1 : 0 )
. padding ( . top , 8 )
}
. padding ( . horizontal , 32 )
. padding ( . bottom , 16 )
}
. padding ( )
. background ( Color ( . systemBackground ) )
. edgesIgnoringSafeArea ( . all )
}
}
2024-03-03 16:38:45 +00:00
2024-02-29 17:38:44 +00:00
// F e t c h n o t e s
private func fetchNotes ( ) {
let secretKey = keychain . get ( " secretKey " )
guard let url = URL ( string : " https://notes.hectabit.org/api/listnotes " ) else { return }
let parameters = [ " secretKey " : " \( secretKey ? ? " bum " ) " ]
// C a l l a J S O N r e q u e s t
JSONHelper . sendJSONRequest ( url : url , parameters : parameters ) { result in
switch result {
case . success ( let data ) :
if let data = data {
// D e c o d e t h e r e c e i v e d n o t e s
if let remoteNotes = try ? JSONDecoder ( ) . decode ( [ Note ] . self , from : data ) {
DispatchQueue . main . async {
// R e m o v e n o t e s t h a t a r e n o t p r e s e n t r e m o t e l y
notes = notes . filter { note in
remoteNotes . contains { $0 . id = = note . id }
}
// F i l t e r n e w n o t e s f r o m e x i s t i n g n o t e s
let newNotes = remoteNotes . filter { newNote in
! notes . contains { $0 . id = = newNote . id }
}
// A p p e n d t h e n e w n o t e s t o t h e e x i s t i n g a r r a y
notes . append ( contentsOf : newNotes )
}
}
}
case . failure ( let error ) :
print ( " Error: \( error ) " ) // A w w s h i t !
}
}
}
private func fetchNoteContent ( note : Note ) {
// W e n e e d t o g e t t h e s e c r e t a n d e n c r y p t i o n k e y s a g a i n h e r e
let secretKey = keychain . get ( " secretKey " )
let encryptionKey = keychain . get ( " encryptionKey " )
guard let url = URL ( string : " https://notes.hectabit.org/api/readnote " ) else { return }
let parameters = [ " secretKey " : " \( secretKey ? ? " bum " ) " , " noteId " : " \( note . id ) " ]
// C a l l a J S O N r e q u e s t
JSONHelper . sendJSONRequest ( url : url , parameters : parameters ) { result in
switch result {
case . success ( let data ) :
guard let data = data else {
print ( " No data received " )
return
}
do {
let json = try JSONSerialization . jsonObject ( with : data , options : [ ] )
if let responseDict = json as ? [ String : Any ] ,
let encryptedContent = responseDict [ " content " ] as ? String {
let decryptedContent = encryptedContent . decrypt ( password : encryptionKey ? ? " this bum ain't got an encryption key!!! " )
DispatchQueue . main . async {
// S e t u p & o p e n t h e t e x t e d i t o r
noteContent = decryptedContent
selectedNote = " \( note . id ) "
isEditing = true
}
}
} catch {
print ( " Error decoding JSON response 🙁: \( error ) " )
}
case . failure ( let error ) :
print ( " Error: \( error ) " ) // A w w s h i t !
}
}
}
// F e t c h u s e r i n f o ( f o r t h e s e t t i n g s p a g e )
private func fetchUserInfo ( ) {
let secretKey = keychain . get ( " secretKey " )
guard let url = URL ( string : " https://notes.hectabit.org/api/userinfo " ) else { return }
let parameters = [ " secretKey " : " \( secretKey ? ? " bum " ) " ]
// C a l l a J S O N r e q u e s t
JSONHelper . sendJSONRequest ( url : url , parameters : parameters ) { result in
switch result {
case . success ( let data ) :
if let data = data {
do {
let json = try JSONSerialization . jsonObject ( with : data , options : [ ] ) as ? [ String : Any ]
fetchSessionID ( )
DispatchQueue . main . async {
// U p d a t e t h e S t a t e s ( ' M U R I C A ! ! ! 🇺 🇸 🇺 🇸 🦅 ) t o m a t c h t h a t o f t h e A P I r e s p o n s e
noteCount = json ? [ " notecount " ] as ? Int ? ? 0
maxStorage = SizeHelper ( ) . humanReadable ( json ? [ " storagemax " ] as ? String ? ? " " ) // W h y i s t h i s a s t r i n g ?
usedStorage = SizeHelper ( ) . humanReadable ( json ? [ " storageused " ] as ? Int ? ? 0 )
}
} catch {
print ( " Error decoding JSON response 🙁: \( error ) " )
}
}
case . failure ( let error ) :
print ( " Error: \( error ) " ) // A w w s h i t !
}
}
}
// F e t c h s e s s i o n I D ( f o r l o g g i n g o u t )
private func fetchSessionID ( ) {
let secretKey = keychain . get ( " secretKey " )
guard let url = URL ( string : " https://notes.hectabit.org/api/sessions/list " ) else { return }
let parameters = [ " secretKey " : " \( secretKey ? ? " bum " ) " ]
// C a l l a J S O N r e q u e s t
JSONHelper . sendJSONRequest ( url : url , parameters : parameters ) { result in
switch result {
// I n t h e c a s e o f a s u c c e s s f u l J S O N r e s p o n s e
case . success ( let data ) :
if let data = data {
do {
let json = try JSONSerialization . jsonObject ( with : data , options : [ ] ) as ? [ [ String : Any ] ]
// S e a r c h f o r a n e n t r y w i t h " t h i s S e s s i o n " s e t t o t r u e
if let session = json ? . first ( where : { $0 [ " thisSession " ] as ? Bool = = true } ) {
DispatchQueue . main . async {
// S e t o u r s e s s i o n I D s t a t e t o " i d " f r o m t h i s r e s p o n s e
sessionID = session [ " id " ] as ? Int ? ? 0
}
} else {
// H O W w a s t h i s c a l l e d w h e n t h e u s e r w a s l o g g e d o f f ? ( I m e a n , t h i s w i l l b e u s e d f o r s e s s i o n a u t h o r i z a t i o n )
print ( " Is this guy even logged in? " )
}
} catch {
print ( " Error decoding JSON response 🙁: \( error ) " )
}
}
// I n t h e c a s e o f a f a i l e d j s o n r e s p o n s e
case . failure ( let error ) :
print ( " Error: \( error ) " ) // A w w s h i t !
}
}
}
private func saveEditedNoteContent ( ) {
// W e n e e d t o g e t t h e s e c r e t a n d e n c r y p t i o n k e y s a g a i n h e r e
let secretKey = keychain . get ( " secretKey " )
let encryptionKey = keychain . get ( " encryptionKey " )
guard let url = URL ( string : " https://notes.hectabit.org/api/editnote " ) else { return }
let formattedContent = noteContent . replacingOccurrences ( of : " \n " , with : " NEWLINEHERETHISISHACKY " ) // H a c k y s o l u t i o n t o a r e a l p r o b l e m
let encryptedContent = formattedContent . encrypt ( password : encryptionKey ? ? " this bum ain't got an encryption key!!! " )
let parameters = [ " secretKey " : " \( secretKey ? ? " bum " ) " , " noteId " : " \( selectedNote ) " , " content " : encryptedContent ]
// C a l l a J S O N r e q u e s t
JSONHelper . sendJSONRequest ( url : url , parameters : parameters ) { result in
switch result {
case . success ( _ ) :
print ( " Note edited and saved successfully " )
// C l o s e t h e t e x t e d i t o r
DispatchQueue . main . async {
isEditing = false
noteContent = " "
}
case . failure ( let error ) :
print ( " Error: \( error ) " ) // A w w s h i t !
}
}
}
}
struct Note : Codable , Equatable {
let id : Int
let title : String
}
struct ContentView_Previews : PreviewProvider {
static var previews : some View {
ForEach ( ColorScheme . allCases , id : \ . self , content : ContentView ( ) . preferredColorScheme )
}
}