// // SettingsView.swift // Feels // // Created by Trey Tartt on 1/8/22. // import SwiftUI import CloudKitSyncMonitor import UniformTypeIdentifiers import StoreKit struct SettingsView: View { @Environment(\.dismiss) var dismiss @Environment(\.openURL) var openURL @EnvironmentObject var iapManager: IAPManager @State private var showingExporter = false @State private var showingImporter = false @State private var importContent = "" @State private var showOnboarding = false @State private var showSpecialThanks = false @State private var showWhyBGMode = false @ObservedObject var syncMonitor = SyncMonitor.shared @AppStorage(UserDefaultsStore.Keys.useCloudKit.rawValue, store: GroupUserDefaults.groupDefaults) private var useCloudKit = false @AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor @AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date() var body: some View { ScrollView { VStack { Group { closeButtonView .padding() // cloudKitEnable subscriptionInfoView canDelete showOnboardingButton eulaButton privacyButton // specialThanksCell } #if DEBUG Group { Divider() Text("Test builds only") addTestDataCell clearDB // randomIcons if useCloudKit { cloudKitStatus } // fixWeekday exportData importData editFirstLaunchDatePast resetLaunchDate Divider() } Spacer() #endif Text("\(Bundle.main.appName) v \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))") .font(.body) } .padding() }.sheet(isPresented: $showOnboarding) { OnboardingMain(onboardingData: UserDefaultsStore.getOnboarding(), updateBoardingDataClosure: { onboardingData in OnboardingDataDataManager.shared.updateOnboardingData(onboardingData: onboardingData) showOnboarding = false }) } .onAppear(perform: { EventLogger.log(event: "show_settings_view") }) .background( theme.currentTheme.bg .edgesIgnoringSafeArea(.all) ) .fileExporter(isPresented: $showingExporter, documents: [ TextFile() ], contentType: .plainText, onCompletion: { result in switch result { case .success(let url): EventLogger.log(event: "exported_file") print("Saved to \(url)") case .failure(let error): print(error.localizedDescription) } }) .fileImporter(isPresented: $showingImporter, allowedContentTypes: [.text], allowsMultipleSelection: false) { result in do { guard let selectedFile: URL = try result.get().first else { return } if selectedFile.startAccessingSecurityScopedResource() { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" dateFormatter.timeZone = TimeZone(abbreviation: "UTC") guard let input = String(data: try Data(contentsOf: selectedFile), encoding: .utf8) else { return } defer { selectedFile.stopAccessingSecurityScopedResource() } var rows = input.components(separatedBy: "\n") rows.removeFirst() for row in rows { let stripped = row.replacingOccurrences(of: " +0000", with: "") let columns = stripped.components(separatedBy: ",") if columns.count != 7 { continue } let moodEntry = MoodEntry(context: PersistenceController.shared.viewContext) moodEntry.canDelete = Bool(columns[0])! moodEntry.canEdit = Bool(columns[1])! moodEntry.entryType = Int16(columns[2])! moodEntry.forDate = dateFormatter.date(from: columns[3])! moodEntry.moodValue = Int16(columns[4])! moodEntry.timestamp = dateFormatter.date(from: columns[5])! let localTime = dateFormatter.date(from: columns[3])! moodEntry.weekDay = Int16(Calendar.current.component(.weekday, from: localTime)) // let _ = print("import info: ", columns[3], dateFormatter.date(from: columns[3]), localTime, Int16(Calendar.current.component(.weekday, from: localTime))) try! PersistenceController.shared.viewContext.save() } PersistenceController.shared.saveAndRunDataListerners() EventLogger.log(event: "import_file") } else { EventLogger.log(event: "error_import_file") } } catch { // Handle failure. EventLogger.log(event: "error_import_file", withData: ["error": error.localizedDescription]) print("Unable to read file contents") print(error.localizedDescription) } } } private var subscriptionInfoView: some View { PurchaseButtonView(iapManager: iapManager) } private var closeButtonView: some View { HStack{ Spacer() Button(action: { EventLogger.log(event: "tap_settings_close") dismiss() }, label: { Text(String(localized: "settings_view_exit")) .font(.body) .foregroundColor(Color(UIColor.systemBlue)) }) } } private var specialThanksCell: some View { ZStack { theme.currentTheme.secondaryBGColor VStack { Button(action: { EventLogger.log(event: "tap_show_special_thanks") withAnimation{ showSpecialThanks.toggle() } }, label: { Text(String(localized: "settings_view_special_thanks_to_title")) .foregroundColor(textColor) }) .padding() if showSpecialThanks { Divider() Link("Font Awesome", destination: URL(string: "https://fontawesome.com")!) .accentColor(textColor) .padding(.bottom) Divider() Link("Charts", destination: URL(string: "https://github.com/danielgindi/Charts")!) .accentColor(textColor) .padding(.bottom) } } } .frame(minWidth: 0, maxWidth: .infinity) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var addTestDataCell: some View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { PersistenceController.shared.populateTestData() }, label: { Text("Add test data") .foregroundColor(textColor) }) .padding() } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var editFirstLaunchDatePast: some View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { var tmpDate = Date() tmpDate = Calendar.current.date(byAdding: .day, value: -29, to: tmpDate)! tmpDate = Calendar.current.date(byAdding: .hour, value: -23, to: tmpDate)! tmpDate = Calendar.current.date(byAdding: .minute, value: -59, to: tmpDate)! tmpDate = Calendar.current.date(byAdding: .second, value: -45, to: tmpDate)! firstLaunchDate = tmpDate Task { await iapManager.checkSubscriptionStatus() } }, label: { Text("Set first launch date back 29 days, 23 hrs, 45 seconds") .foregroundColor(textColor) }) .padding() } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var resetLaunchDate: some View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { firstLaunchDate = Date() Task { await iapManager.checkSubscriptionStatus() } }, label: { Text("Reset luanch date to current date") .foregroundColor(textColor) }) .padding() } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var clearDB: some View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { PersistenceController.shared.clearDB() }, label: { Text("Clear DB") .foregroundColor(textColor) }) .padding() } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var fixWeekday: some View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { PersistenceController.shared.fixWrongWeekdays() }, label: { Text("Fix Weekday") .foregroundColor(textColor) }) .padding() } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var whyBackgroundMode: some View { ZStack { theme.currentTheme.secondaryBGColor VStack { Button(action: { withAnimation{ showWhyBGMode.toggle() } }, label: { Text(String(localized: "settings_view_why_bg_mode_title")) .foregroundColor(textColor) }) .padding() if showWhyBGMode { Text(String(localized: "settings_view_why_bg_mode_body")) .foregroundColor(textColor) .padding() } } } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var showOnboardingButton: some View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { EventLogger.log(event: "tap_show_onboarding") showOnboarding.toggle() }, label: { Text(String(localized: "settings_view_show_onboarding")) .foregroundColor(textColor) }) .padding() } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var eulaButton: some View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { EventLogger.log(event: "show_eula") openURL(URL(string: "https://ifeels.app/eula.html")!) }, label: { Text(String(localized: "settings_view_show_eula")) .foregroundColor(textColor) }) .padding() } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var privacyButton: some View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { EventLogger.log(event: "show_privacy") openURL(URL(string: "https://ifeels.app/privacy.html")!) }, label: { Text(String(localized: "settings_view_show_privacy")) .foregroundColor(textColor) }) .padding() } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var cloudKitEnable: some View { ZStack { theme.currentTheme.secondaryBGColor VStack { Toggle(isOn: $useCloudKit, label: { Text(String(localized: "settings_use_cloudkit_title")) .foregroundColor(textColor) }) .onChange(of: useCloudKit) { newValue in EventLogger.log(event: "toggle_use_cloudkit", withData: ["value": newValue]) } .padding() Text(String(localized: "settings_use_cloudkit_body")) .foregroundColor(textColor) } .padding(.bottom) } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var cloudKitStatus: some View { ZStack { theme.currentTheme.secondaryBGColor VStack { Image(systemName: syncMonitor.syncStateSummary.symbolName) .foregroundColor(syncMonitor.syncStateSummary.symbolColor) Text( syncMonitor.syncStateSummary.isBroken ? "cloudkit is broken" : "cloudkit is good") .foregroundColor(textColor) } .padding() } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var canDelete: some View { ZStack { theme.currentTheme.secondaryBGColor VStack { Toggle(String(localized: "settings_use_delete_enable"), isOn: $deleteEnabled) .onChange(of: deleteEnabled) { newValue in EventLogger.log(event: "toggle_can_delete", withData: ["value": newValue]) } .foregroundColor(textColor) .padding() } } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var exportData: some View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { showingExporter.toggle() EventLogger.log(event: "export_data", withData: ["title": "default"]) }, label: { Text("Export") .foregroundColor(textColor) }) .padding() } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var importData: some View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { showingImporter.toggle() EventLogger.log(event: "import_data", withData: ["title": "default"]) }, label: { Text("Import") .foregroundColor(textColor) }) .padding() } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var randomIcons: some View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { var iconViews = [UIImage]() // for _ in 0...300 { // iconViews.append( // IconView(iconViewModel: IconViewModel( // backgroundImage: MoodImages.FontAwesome.icon(forMood: .great), // bgColor: Color.random(), // bgOverlayColor: Color.random(), // centerImage: MoodImages.FontAwesome.icon(forMood: .great), // innerColor: Color.random()) // ).asImage(size: CGSize(width: 1024, height: 1024))) // } iconViews.append( IconView(iconViewModel: IconViewModel( backgroundImage: MoodImages.FontAwesome.icon(forMood: .great), bgColor: IconViewModel.great.bgColor, bgOverlayColor: IconViewModel.great.bgOverlayColor, centerImage: MoodImages.FontAwesome.icon(forMood: .great), innerColor: IconViewModel.great.innerColor) ).asImage(size: CGSize(width: 1024, height: 1024)) ) iconViews.append( IconView(iconViewModel: IconViewModel( backgroundImage: MoodImages.FontAwesome.icon(forMood: .great), bgColor: IconViewModel.good.bgColor, bgOverlayColor: IconViewModel.good.bgOverlayColor, centerImage: MoodImages.FontAwesome.icon(forMood: .great), innerColor: IconViewModel.good.innerColor) ).asImage(size: CGSize(width: 1024, height: 1024)) ) iconViews.append( IconView(iconViewModel: IconViewModel( backgroundImage: MoodImages.FontAwesome.icon(forMood: .great), bgColor: IconViewModel.average.bgColor, bgOverlayColor: IconViewModel.average.bgOverlayColor, centerImage: MoodImages.FontAwesome.icon(forMood: .great), innerColor: IconViewModel.average.innerColor) ).asImage(size: CGSize(width: 1024, height: 1024)) ) iconViews.append( IconView(iconViewModel: IconViewModel( backgroundImage: MoodImages.FontAwesome.icon(forMood: .great), bgColor: IconViewModel.bad.bgColor, bgOverlayColor: IconViewModel.bad.bgOverlayColor, centerImage: MoodImages.FontAwesome.icon(forMood: .great), innerColor: IconViewModel.bad.innerColor) ).asImage(size: CGSize(width: 1024, height: 1024)) ) iconViews.append( IconView(iconViewModel: IconViewModel( backgroundImage: MoodImages.FontAwesome.icon(forMood: .great), bgColor: IconViewModel.horrible.bgColor, bgOverlayColor: IconViewModel.horrible.bgOverlayColor, centerImage: MoodImages.FontAwesome.icon(forMood: .great), innerColor: IconViewModel.horrible.innerColor) ).asImage(size: CGSize(width: 1024, height: 1024)) ) // iconViews.append( // IconView(iconViewModel: IconViewModel( // backgroundImage: MoodImages.FontAwesome.icon(forMood: .great), // bgColor: Color(hex: "EF0CF3"), // bgOverlayColor: Color(hex: "EF0CF3").darker(by: 40), // centerImage: MoodImages.FontAwesome.icon(forMood: .great), // innerColor: Color(hex: "EF0CF3")) // ).asImage(size: CGSize(width: 1024, height: 1024)) // ) // // iconViews.append( // IconView(iconViewModel: IconViewModel( // backgroundImage: MoodImages.FontAwesome.icon(forMood: .great), // bgColor: Color(hex: "1AE5D6"), // bgOverlayColor: Color(hex: "1AE5D6").darker(by: 40), // centerImage: MoodImages.FontAwesome.icon(forMood: .great), // innerColor: Color(hex: "1AE5D6")) // ).asImage(size: CGSize(width: 1024, height: 1024)) // ) // // iconViews.append( // IconView(iconViewModel: IconViewModel( // backgroundImage: MoodImages.FontAwesome.icon(forMood: .great), // bgColor: Color(hex: "633EC1"), // bgOverlayColor: Color(hex: "633EC1").darker(by: 40), // centerImage: MoodImages.FontAwesome.icon(forMood: .great), // innerColor: Color(hex: "633EC1")) // ).asImage(size: CGSize(width: 1024, height: 1024)) // ) // // iconViews.append( // IconView(iconViewModel: IconViewModel( // backgroundImage: MoodImages.FontAwesome.icon(forMood: .great), // bgColor: Color(hex: "10F30C"), // bgOverlayColor: Color(hex: "10F30C").darker(by: 40), // centerImage: MoodImages.FontAwesome.icon(forMood: .great), // innerColor: Color(hex: "10F30C")) // ).asImage(size: CGSize(width: 1024, height: 1024)) // ) for (idx, image) in iconViews.enumerated() { let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) var path = paths[0].appendingPathComponent("icons").path path = path.appending("\(idx).jpg") let url = URL(fileURLWithPath: path) do { try image.jpegData(compressionQuality: 1.0)?.write(to: url, options: .atomic) print(url) } catch { print(error.localizedDescription) } } }, label: { Text("Create random icons") .foregroundColor(textColor) }) .padding() } .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } } struct TextFile: FileDocument { // tell the system we support only plain text static var readableContentTypes = [UTType.plainText] // by default our document is empty var text = "" // a simple initializer that creates new, empty documents init() { let entries = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0), endDate: Date(), includedDays: []) var csvString = "canDelete,canEdit,entryType,forDate,moodValue,timestamp,weekDay\n" for entry in entries { let canDelete = entry.canDelete let canEdit = entry.canEdit let entryType = entry.entryType let forDate = entry.forDate! let moodValue = entry.moodValue let timestamp = entry.timestamp! let weekDay = entry.weekDay let dataString = "\(canDelete),\(canEdit),\(entryType),\(String(describing: forDate)),\(moodValue),\(String(describing:timestamp)),\(weekDay)\n" // print("DATA: \(dataString)") csvString = csvString.appending(dataString) } text = csvString } // this initializer loads data that has been saved previously init(configuration: ReadConfiguration) throws { if let data = configuration.file.regularFileContents { text = String(decoding: data, as: UTF8.self) } } // this will be called when the system wants to write our data to disk func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let data = Data(text.utf8) return FileWrapper(regularFileWithContents: data) } } struct SettingsView_Previews: PreviewProvider { static var previews: some View { SettingsView() SettingsView() .preferredColorScheme(.dark) } }