wip
This commit is contained in:
16
Shared/Views/ActivityViewController.swift
Normal file
16
Shared/Views/ActivityViewController.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
struct ActivityViewController: UIViewControllerRepresentable {
|
||||
|
||||
var activityItems: [Any]
|
||||
var applicationActivities: [UIActivity]? = nil
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
|
||||
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
|
||||
|
||||
}
|
||||
86
Shared/Views/AddMoodHeaderView.swift
Normal file
86
Shared/Views/AddMoodHeaderView.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// AddMoodHeaderView.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/5/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct AddMoodHeaderView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
@State var onboardingData = OnboardingDataDataManager.shared.savedOnboardingData
|
||||
|
||||
let addItemHeaderClosure: ((Mood, Date) -> Void)
|
||||
|
||||
init(addItemHeaderClosure: @escaping ((Mood, Date) -> Void)) {
|
||||
self.addItemHeaderClosure = addItemHeaderClosure
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
VStack {
|
||||
Text(ShowBasedOnVoteLogics.getVotingTitle(onboardingData: onboardingData))
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.padding()
|
||||
HStack{
|
||||
ForEach(Mood.allValues) { mood in
|
||||
VStack {
|
||||
Button(action: {
|
||||
addItem(withMood: mood)
|
||||
}, label: {
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: CGFloat(50), height: CGFloat(50), alignment: .center)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
})
|
||||
|
||||
//Text(mood.strValue)
|
||||
}.frame(minWidth: 0, maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.leading, .trailing, .bottom])
|
||||
}
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.frame(minHeight: 88, maxHeight: 150)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
}
|
||||
|
||||
private func addItem(withMood mood: Mood) {
|
||||
let date = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData)
|
||||
addItemHeaderClosure(mood, date)
|
||||
}
|
||||
}
|
||||
|
||||
struct AddMoodHeaderView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
AddMoodHeaderView(addItemHeaderClosure: { (_,_) in
|
||||
|
||||
}).environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
||||
|
||||
AddMoodHeaderView(addItemHeaderClosure: { (_,_) in
|
||||
|
||||
}).preferredColorScheme(.dark).environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
||||
|
||||
AddMoodHeaderView(addItemHeaderClosure: { (_,_) in
|
||||
|
||||
}).environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
||||
|
||||
AddMoodHeaderView(addItemHeaderClosure: { (_,_) in
|
||||
|
||||
}).preferredColorScheme(.dark).environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Shared/Views/BGView.swift
Normal file
93
Shared/Views/BGView.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// IconView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 1/20/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BGViewItem: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
|
||||
let mood: Mood
|
||||
let size: CGSize
|
||||
var color: Color
|
||||
let animate: Bool
|
||||
let yRowPosition: Float
|
||||
|
||||
init(mood: Mood, size: CGSize, animate: Bool, yRowPosition: Float, color: Color) {
|
||||
self.color = color
|
||||
self.mood = mood
|
||||
self.size = size
|
||||
self.yRowPosition = yRowPosition
|
||||
self.animate = animate
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
FontAwesomeMoodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
.frame(width: size.width, height: size.height)
|
||||
.foregroundColor(DefaultMoodTint.color(forMood: mood))
|
||||
// .blur(radius: 3)
|
||||
.opacity(0.1)
|
||||
}
|
||||
}
|
||||
|
||||
struct BGView: View, Equatable {
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
|
||||
var numAcross: Int
|
||||
var numDown: Int
|
||||
let iconSize = 35
|
||||
|
||||
init() {
|
||||
let screenWidth = UIScreen.main.bounds.width
|
||||
numAcross = Int(screenWidth)/iconSize
|
||||
|
||||
let screenHeight = UIScreen.main.bounds.height
|
||||
numDown = Int(screenHeight)/iconSize
|
||||
}
|
||||
|
||||
var randomMood: Mood? {
|
||||
return Mood.allValues.randomElement()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ForEach(0...numDown, id: \.self) { row in
|
||||
HStack {
|
||||
ForEach(0...numAcross, id: \.self) { _ in
|
||||
if let randomMood = randomMood {
|
||||
BGViewItem(mood: randomMood,
|
||||
size: .init(width: iconSize,height: iconSize),
|
||||
animate: false,
|
||||
yRowPosition: Float(row)/Float(numDown),
|
||||
color: moodTint.color(forMood:randomMood))
|
||||
}
|
||||
}.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.padding([.trailing, .leading], 13.5)
|
||||
}
|
||||
.padding(.top, -9)
|
||||
}
|
||||
}
|
||||
.padding(.top, -50)
|
||||
}
|
||||
|
||||
static func == (lhs: BGView, rhs: BGView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
struct BGView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BGView().environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
||||
.onAppear(perform: {
|
||||
PersistenceController.shared.populateMemory()
|
||||
})
|
||||
|
||||
BGView()
|
||||
.preferredColorScheme(.dark)
|
||||
.environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
||||
}
|
||||
}
|
||||
366
Shared/Views/CustomIcon/CreateWidgetView.swift
Normal file
366
Shared/Views/CustomIcon/CreateWidgetView.swift
Normal file
@@ -0,0 +1,366 @@
|
||||
//
|
||||
// CreateIconView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/13/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CreateWidgetView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
@StateObject private var customWidget: CustomWidgetModel
|
||||
|
||||
@State private var mouth: CustomWidgetMouthOptions = CustomWidgetMouthOptions.defaultOption
|
||||
@State private var showRightEyeImagePicker: Bool = false
|
||||
@State private var showLeftEyeImagePicker: Bool = false
|
||||
@State private var showMuthImagePicker: Bool = false
|
||||
|
||||
var widgetView: CustomWidgetView
|
||||
|
||||
private var randomElements: [AnyView]
|
||||
|
||||
init(customWidget: CustomWidgetModel, randomElements: [AnyView] = [AnyView]()) {
|
||||
self.randomElements = [
|
||||
AnyView(Image(CustomWidgetBackGroundOptions.selectable.randomElement()!.rawValue)
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)),
|
||||
AnyView(Image(CustomWidgetBackGroundOptions.selectable.randomElement()!.rawValue)
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)),
|
||||
AnyView(Image(CustomWidgetBackGroundOptions.selectable.randomElement()!.rawValue)
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)),
|
||||
AnyView(Image(CustomWidgetBackGroundOptions.selectable.randomElement()!.rawValue)
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20))
|
||||
]
|
||||
|
||||
_customWidget = StateObject(wrappedValue: customWidget)
|
||||
|
||||
widgetView = CustomWidgetView(customWidgetModel: customWidget)
|
||||
}
|
||||
|
||||
func update(eye: CustomWidgetEyes, eyeOption: CustomWidgeImageOptions) {
|
||||
EventLogger.log(event: "create_widget_view_update_eye",
|
||||
withData: ["eye_value": eye.rawValue, "eye_option_value": eyeOption.rawValue])
|
||||
switch eye {
|
||||
case .left:
|
||||
customWidget.leftEye = eyeOption
|
||||
case .right:
|
||||
customWidget.rightEye = eyeOption
|
||||
}
|
||||
}
|
||||
|
||||
func createRandom() {
|
||||
EventLogger.log(event: "create_widget_view_create_random")
|
||||
customWidget.bgColor = Color.random()
|
||||
customWidget.innerColor = Color.random()
|
||||
customWidget.bgOverlayColor = Color.random()
|
||||
customWidget.circleStrokeColor = Color.random()
|
||||
customWidget.leftEyeColor = Color.random()
|
||||
customWidget.rightEyeColor = Color.random()
|
||||
customWidget.mouthColor = Color.random()
|
||||
|
||||
update(eye: .left, eyeOption: CustomWidgeImageOptions.allCases.randomElement()!)
|
||||
update(eye: .right, eyeOption: CustomWidgeImageOptions.allCases.randomElement()!)
|
||||
update(mouthOption: CustomWidgeImageOptions.allCases.randomElement()!)
|
||||
|
||||
update(background: CustomWidgetBackGroundOptions.allCases.randomElement()!)
|
||||
}
|
||||
|
||||
func update(mouthOption: CustomWidgeImageOptions) {
|
||||
EventLogger.log(event: "create_widget_view_update_mouth",
|
||||
withData: ["mouthOption": mouthOption.rawValue])
|
||||
customWidget.mouth = mouthOption
|
||||
}
|
||||
|
||||
func update(background: CustomWidgetBackGroundOptions) {
|
||||
EventLogger.log(event: "create_widget_view_update_background",
|
||||
withData: ["background": background.rawValue])
|
||||
customWidget.background = background
|
||||
}
|
||||
|
||||
var mixBG: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
randomElements[0]
|
||||
randomElements[1]
|
||||
}
|
||||
HStack {
|
||||
randomElements[2]
|
||||
randomElements[3]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var bottomBarButtons: some View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Button(action: {
|
||||
EventLogger.log(event: "create_widget_view_shuffle")
|
||||
createRandom()
|
||||
}, label: {
|
||||
Image(systemName: "shuffle")
|
||||
.font(.title)
|
||||
.foregroundColor(.white)
|
||||
.padding([.top, .bottom])
|
||||
})
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.frame(minHeight: 40, maxHeight: .infinity)
|
||||
.background(.blue)
|
||||
|
||||
Button(action: {
|
||||
EventLogger.log(event: "create_widget_view_save_widget")
|
||||
UserDefaultsStore.saveCustomWidget(widgetModel: customWidget, inUse: false)
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text(String(localized: "create_widget_save"))
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.white)
|
||||
.padding([.top, .bottom])
|
||||
|
||||
})
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.frame(minHeight: 40, maxHeight: .infinity)
|
||||
.background(.green)
|
||||
|
||||
Button(action: {
|
||||
EventLogger.log(event: "customize_view_use_widget")
|
||||
UserDefaultsStore.saveCustomWidget(widgetModel: customWidget, inUse: true)
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text(String(localized: "create_widget_use"))
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.white)
|
||||
.padding([.top, .bottom])
|
||||
|
||||
})
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.frame(minHeight: 40, maxHeight: .infinity)
|
||||
.background(.pink)
|
||||
|
||||
if customWidget.isSaved {
|
||||
Button(action: {
|
||||
EventLogger.log(event: "customize_view_delete_widget")
|
||||
UserDefaultsStore.deleteCustomWidget(withUUID: customWidget.uuid)
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
dismiss()
|
||||
}, label: {
|
||||
Image(systemName: "trash")
|
||||
.font(.title)
|
||||
.foregroundColor(.white)
|
||||
.padding([.top, .bottom])
|
||||
|
||||
})
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.frame(minHeight: 40, maxHeight: .infinity)
|
||||
.background(.orange)
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 40, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
var colorOptions: some View {
|
||||
VStack {
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_background_color"))
|
||||
ColorPicker("", selection: $customWidget.bgColor)
|
||||
.onChange(of: customWidget.mouthColor, perform: { newValue in
|
||||
EventLogger.log(event: "create_widget_view_update_background_color")
|
||||
})
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_inner_color"))
|
||||
ColorPicker("", selection: $customWidget.innerColor)
|
||||
.onChange(of: customWidget.mouthColor, perform: { newValue in
|
||||
EventLogger.log(event: "create_widget_view_update_inner_color")
|
||||
})
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_face_outline_color"))
|
||||
ColorPicker("", selection: $customWidget.circleStrokeColor)
|
||||
.onChange(of: customWidget.mouthColor, perform: { newValue in
|
||||
EventLogger.log(event: "create_widget_view_update_outline_color")
|
||||
})
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
}
|
||||
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_view_left_eye_color"))
|
||||
ColorPicker("", selection: $customWidget.leftEyeColor)
|
||||
.onChange(of: customWidget.mouthColor, perform: { newValue in
|
||||
EventLogger.log(event: "create_widget_view_update_left_eye_color")
|
||||
})
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_view_right_eye_color"))
|
||||
ColorPicker("", selection: $customWidget.rightEyeColor)
|
||||
.onChange(of: customWidget.mouthColor, perform: { newValue in
|
||||
EventLogger.log(event: "create_widget_view_update_right_eye_color")
|
||||
})
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_view_mouth_color"))
|
||||
ColorPicker("", selection: $customWidget.mouthColor)
|
||||
.onChange(of: customWidget.mouthColor, perform: { newValue in
|
||||
EventLogger.log(event: "create_widget_view_update_mouth_color")
|
||||
})
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
}
|
||||
|
||||
var bgImageOptions: some View {
|
||||
HStack {
|
||||
ForEach(CustomWidgetBackGroundOptions.selectable, id: \.self) { bg in
|
||||
Image(bg.rawValue, bundle: .main)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(minWidth: 10, idealWidth: 40, maxWidth: 40,
|
||||
minHeight: 10, idealHeight: 40, maxHeight: 40,
|
||||
alignment: .center)
|
||||
.onTapGesture {
|
||||
update(background: bg)
|
||||
}
|
||||
}
|
||||
mixBG
|
||||
.onTapGesture {
|
||||
update(background: .random)
|
||||
}
|
||||
Divider()
|
||||
ColorPicker("", selection: $customWidget.bgOverlayColor)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
}
|
||||
|
||||
var faceImageOptions: some View {
|
||||
HStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_view_left_eye"))
|
||||
.onTapGesture(perform: {
|
||||
showLeftEyeImagePicker.toggle()
|
||||
})
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(textColor)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
Divider()
|
||||
Text(String(localized: "create_widget_view_right_eye"))
|
||||
.onTapGesture(perform: {
|
||||
showRightEyeImagePicker.toggle()
|
||||
})
|
||||
.foregroundColor(textColor)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
Divider()
|
||||
Text(String(localized: "create_widget_view_mouth"))
|
||||
.onTapGesture(perform: {
|
||||
showMuthImagePicker.toggle()
|
||||
})
|
||||
.foregroundColor(textColor)
|
||||
.foregroundColor(textColor)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
VStack(spacing: 0) {
|
||||
widgetView
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
.frame(width: geo.size.width, height: geo.size.width)
|
||||
|
||||
Spacer()
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
Divider().background(Color(UIColor.tertiarySystemBackground))
|
||||
|
||||
faceImageOptions
|
||||
|
||||
Divider().background(Color(UIColor.tertiarySystemBackground))
|
||||
|
||||
bgImageOptions
|
||||
|
||||
Divider().background(Color(UIColor.tertiarySystemBackground))
|
||||
|
||||
colorOptions
|
||||
|
||||
Divider().background(Color(UIColor.tertiarySystemBackground))
|
||||
|
||||
bottomBarButtons
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showRightEyeImagePicker) {
|
||||
ImagePickerGridView(pickedImageClosure: { image in
|
||||
update(eye: .right, eyeOption: image)
|
||||
})
|
||||
}
|
||||
.sheet(isPresented: $showLeftEyeImagePicker) {
|
||||
ImagePickerGridView(pickedImageClosure: { image in
|
||||
update(eye: .left, eyeOption: image)
|
||||
})
|
||||
}
|
||||
.sheet(isPresented: $showMuthImagePicker) {
|
||||
ImagePickerGridView(pickedImageClosure: { image in
|
||||
update(mouthOption: image)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateIconView_Previews: PreviewProvider {
|
||||
static var widget: CustomWidgetModel {
|
||||
let _widget = CustomWidgetModel.randomWidget
|
||||
_widget.isSaved = true
|
||||
return _widget
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
CreateWidgetView(customWidget: CreateIconView_Previews.widget)
|
||||
|
||||
CreateWidgetView(customWidget: CustomWidgetModel.randomWidget)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
115
Shared/Views/CustomIcon/IconView.swift
Normal file
115
Shared/Views/CustomIcon/IconView.swift
Normal file
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// IconView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/20/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct IconView: View {
|
||||
@State public var iconViewModel: IconViewModel
|
||||
|
||||
private let facePercSize = 0.6
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(
|
||||
iconViewModel.bgColor
|
||||
)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
LazyVGrid(columns: columns, alignment: .leading, spacing: 0) {
|
||||
ForEach(iconViewModel.background, id: \.self.1) { (bgOption, uuid) in
|
||||
bgOption
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.foregroundColor(iconViewModel.bgOverlayColor)
|
||||
}
|
||||
}
|
||||
.scaleEffect(1.1)
|
||||
.clipped()
|
||||
.background(
|
||||
.clear
|
||||
)
|
||||
|
||||
Circle()
|
||||
.strokeBorder(iconViewModel.bgColor, lineWidth: geo.size.width * 0.045)
|
||||
.background(Circle().fill(.clear))
|
||||
.frame(width: geo.size.width*facePercSize,
|
||||
height: geo.size.height*facePercSize,
|
||||
alignment: .center)
|
||||
.alignmentGuide(.top, computeValue: { _ in
|
||||
return geo.size.width/2
|
||||
})
|
||||
|
||||
Circle()
|
||||
.fill(iconViewModel.innerColor)
|
||||
.frame(width: geo.size.width*facePercSize,
|
||||
height: geo.size.height*facePercSize,
|
||||
alignment: .center)
|
||||
.alignmentGuide(.top, computeValue: { _ in
|
||||
return geo.size.width/2
|
||||
})
|
||||
|
||||
iconViewModel.centerImage
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: geo.size.width*facePercSize,
|
||||
height: geo.size.height*facePercSize,
|
||||
alignment: .center)
|
||||
.foregroundColor(iconViewModel.bgOverlayColor)
|
||||
.alignmentGuide(.top, computeValue: { _ in
|
||||
return geo.size.width/2
|
||||
})
|
||||
|
||||
}
|
||||
.position(x: geo.size.width/2,
|
||||
y: geo.size.height/2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IconView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
IconView(iconViewModel: IconViewModel.great)
|
||||
.frame(width: 256, height: 256, alignment: .center)
|
||||
|
||||
IconView(iconViewModel: IconViewModel.good)
|
||||
.frame(width: 256, height: 256, alignment: .center)
|
||||
|
||||
IconView(iconViewModel: IconViewModel.average)
|
||||
.frame(width: 256, height: 256, alignment: .center)
|
||||
|
||||
IconView(iconViewModel: IconViewModel.bad)
|
||||
.frame(width: 256, height: 256, alignment: .center)
|
||||
|
||||
IconView(iconViewModel: IconViewModel.horrible)
|
||||
.frame(width: 256, height: 256, alignment: .center)
|
||||
//
|
||||
// IconView(iconViewModel: IconViewModel(backgroundImage: EmojiMoodImages.icon(forMood: .horrible),
|
||||
// bgColor: MoodTints.Neon.color(forMood: .horrible),
|
||||
// bgOverlayColor: MoodTints.Neon.color(forMood: .horrible),
|
||||
// centerImage: EmojiMoodImages.icon(forMood: .horrible)),
|
||||
// isPreview: true)
|
||||
// .frame(width: 256, height: 256, alignment: .center)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
88
Shared/Views/CustomIcon/IconViewModel.swift
Normal file
88
Shared/Views/CustomIcon/IconViewModel.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// CustomIcon.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/20/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class IconViewModel: ObservableObject {
|
||||
static let numberOfBGItems = 109
|
||||
|
||||
static let great = IconViewModel(backgroundImage: MoodImages.FontAwesome.icon(forMood: .great),
|
||||
bgColor: Color(hex: "31d158"),
|
||||
bgOverlayColor: Color(hex: "208939"),
|
||||
centerImage: MoodImages.FontAwesome.icon(forMood: .great),
|
||||
innerColor: Color(hex: "31d158"))
|
||||
|
||||
static let good = IconViewModel(backgroundImage: MoodImages.FontAwesome.icon(forMood: .good),
|
||||
bgColor: Color(hex: "ffd709"),
|
||||
bgOverlayColor: Color(hex: "9d8405"),
|
||||
centerImage: MoodImages.FontAwesome.icon(forMood: .good),
|
||||
innerColor: Color(hex: "ffd709"))
|
||||
|
||||
static let average = IconViewModel(backgroundImage: MoodImages.FontAwesome.icon(forMood: .average),
|
||||
bgColor: Color(hex: "0b84ff"),
|
||||
bgOverlayColor: Color(hex: "074f9a"),
|
||||
centerImage: MoodImages.FontAwesome.icon(forMood: .average),
|
||||
innerColor: Color(hex: "0b84ff"))
|
||||
|
||||
static let bad = IconViewModel(backgroundImage: MoodImages.FontAwesome.icon(forMood: .bad),
|
||||
bgColor: Color(hex: "ff9f0b"),
|
||||
bgOverlayColor: Color(hex: "a06407"),
|
||||
centerImage: MoodImages.FontAwesome.icon(forMood: .bad),
|
||||
innerColor: Color(hex: "ff9f0b"))
|
||||
|
||||
static let horrible = IconViewModel(backgroundImage: MoodImages.FontAwesome.icon(forMood: .horrible),
|
||||
bgColor: Color(hex: "fe5257"),
|
||||
bgOverlayColor: Color(hex: "a92b26"),
|
||||
centerImage: MoodImages.FontAwesome.icon(forMood: .horrible),
|
||||
innerColor: Color(hex: "fe5257"))
|
||||
|
||||
init(backgroundImage: Image,
|
||||
bgColor: Color,
|
||||
bgOverlayColor: Color,
|
||||
centerImage: Image,
|
||||
innerColor: Color
|
||||
) {
|
||||
|
||||
var blah = [(Image, UUID)]()
|
||||
for _ in 0...IconViewModel.numberOfBGItems {
|
||||
blah.append((backgroundImage, UUID()))
|
||||
}
|
||||
|
||||
self.background = blah
|
||||
self.bgColor = bgColor
|
||||
self.bgOverlayColor = bgOverlayColor
|
||||
self.centerImage = centerImage
|
||||
self.innerColor = innerColor
|
||||
}
|
||||
|
||||
@Published var background: [(Image, UUID)]
|
||||
@Published var bgColor: Color
|
||||
@Published var bgOverlayColor: Color
|
||||
@Published var centerImage: Image
|
||||
@Published var innerColor: Color
|
||||
}
|
||||
|
||||
enum CustomIconBackGroundOptions: String, CaseIterable, Codable {
|
||||
case horrible
|
||||
case bad
|
||||
case average
|
||||
case good
|
||||
case great
|
||||
case random
|
||||
|
||||
static var selectable: [CustomIconBackGroundOptions] {
|
||||
return [.great, .good, .average, .bad, .horrible]
|
||||
}
|
||||
|
||||
static public var defaultOption: CustomIconBackGroundOptions {
|
||||
CustomIconBackGroundOptions.random
|
||||
}
|
||||
|
||||
public var image: Image {
|
||||
return Image(self.rawValue, bundle: .main)
|
||||
}
|
||||
}
|
||||
374
Shared/Views/CustomWidget/CustomWidgetModel.swift
Normal file
374
Shared/Views/CustomWidget/CustomWidgetModel.swift
Normal file
@@ -0,0 +1,374 @@
|
||||
//
|
||||
// CustomIcon.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/13/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class CustomWidgetModel: ObservableObject, Codable, NSCopying {
|
||||
static let numberOfBGItems = 109
|
||||
|
||||
static var randomWidget: CustomWidgetModel {
|
||||
return CustomWidgetModel(leftEye: CustomWidgeImageOptions.defaultOption,
|
||||
rightEye: CustomWidgeImageOptions.defaultOption,
|
||||
mouth: CustomWidgeImageOptions.defaultOption,
|
||||
background: CustomWidgetBackGroundOptions.defaultOption,
|
||||
bgColor: Color.random(),
|
||||
innerColor: Color.random(),
|
||||
bgOverlayColor: Color.random(),
|
||||
rightEyeColor: Color.random(),
|
||||
leftEyeColor: Color.random(),
|
||||
mouthColor: Color.random(),
|
||||
circleStrokeColor: Color.random(),
|
||||
inUse: true,
|
||||
uuid: UUID().uuidString,
|
||||
isSaved: false,
|
||||
createdDate: Date())
|
||||
}
|
||||
|
||||
init(leftEye: CustomWidgeImageOptions,
|
||||
rightEye: CustomWidgeImageOptions,
|
||||
mouth: CustomWidgeImageOptions,
|
||||
background: CustomWidgetBackGroundOptions,
|
||||
bgColor: Color,
|
||||
innerColor: Color,
|
||||
bgOverlayColor: Color,
|
||||
rightEyeColor: Color,
|
||||
leftEyeColor: Color,
|
||||
mouthColor: Color,
|
||||
circleStrokeColor: Color,
|
||||
inUse: Bool,
|
||||
uuid: String,
|
||||
isSaved: Bool,
|
||||
createdDate: Date) {
|
||||
self.leftEye = leftEye
|
||||
self.rightEye = rightEye
|
||||
self.mouth = mouth
|
||||
self.background = background
|
||||
self.bgColor = bgColor
|
||||
self.innerColor = innerColor
|
||||
self.bgOverlayColor = bgOverlayColor
|
||||
self.rightEyeColor = rightEyeColor
|
||||
self.leftEyeColor = leftEyeColor
|
||||
self.mouthColor = mouthColor
|
||||
self.circleStrokeColor = circleStrokeColor
|
||||
self.inUse = inUse
|
||||
self.uuid = uuid
|
||||
self.isSaved = isSaved
|
||||
self.createdDate = createdDate
|
||||
}
|
||||
|
||||
@Published var leftEye: CustomWidgeImageOptions
|
||||
@Published var rightEye: CustomWidgeImageOptions
|
||||
@Published var mouth: CustomWidgeImageOptions
|
||||
|
||||
@Published var background: CustomWidgetBackGroundOptions
|
||||
@Published var bgColor: Color
|
||||
@Published var innerColor: Color
|
||||
@Published var bgOverlayColor: Color
|
||||
|
||||
@Published var leftEyeColor: Color
|
||||
@Published var rightEyeColor: Color
|
||||
@Published var mouthColor: Color
|
||||
|
||||
@Published var circleStrokeColor: Color
|
||||
@Published var inUse: Bool
|
||||
@Published var uuid: String
|
||||
@Published var isSaved: Bool
|
||||
@Published var createdDate: Date
|
||||
|
||||
private var oldBackground: CustomWidgetBackGroundOptions?
|
||||
private var oldBackgroundImages: [(Image, String)]?
|
||||
|
||||
public var backgroundImages : [(Image, String)] {
|
||||
if let oldBackgroundImages = oldBackgroundImages,
|
||||
oldBackground == background {
|
||||
return oldBackgroundImages
|
||||
}
|
||||
|
||||
oldBackground = background
|
||||
|
||||
if background == .random {
|
||||
var blah = [(Image, String)]()
|
||||
for _ in 0...CustomWidgetModel.numberOfBGItems {
|
||||
let image = CustomWidgetBackGroundOptions.selectable.randomElement()?.image ?? CustomWidgetBackGroundOptions.defaultOption.image
|
||||
blah.append((image, UUID().uuidString))
|
||||
}
|
||||
oldBackgroundImages = blah
|
||||
} else {
|
||||
var blah = [(Image, String)]()
|
||||
for _ in 0...CustomWidgetModel.numberOfBGItems {
|
||||
blah.append((background.image, UUID().uuidString))
|
||||
}
|
||||
oldBackgroundImages = blah
|
||||
}
|
||||
return oldBackgroundImages!
|
||||
}
|
||||
|
||||
enum CodingKeys: CodingKey {
|
||||
case leftEye, rightEye, mouth, background, bgColor, innerColor, bgOverlayColor, leftEyeColor, rightEyeColor, mouthColor, circleStrokeColor, inUse, uuid, isSaved, createdDate
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
leftEye = try container.decode(CustomWidgeImageOptions.self, forKey: .leftEye)
|
||||
rightEye = try container.decode(CustomWidgeImageOptions.self, forKey: .rightEye)
|
||||
mouth = try container.decode(CustomWidgeImageOptions.self, forKey: .mouth)
|
||||
|
||||
background = try container.decode(CustomWidgetBackGroundOptions.self, forKey: .background)
|
||||
bgColor = try container.decode(Color.self, forKey: .bgColor)
|
||||
innerColor = try container.decode(Color.self, forKey: .innerColor)
|
||||
bgOverlayColor = try container.decode(Color.self, forKey: .bgOverlayColor)
|
||||
leftEyeColor = try container.decode(Color.self, forKey: .leftEyeColor)
|
||||
rightEyeColor = try container.decode(Color.self, forKey: .rightEyeColor)
|
||||
mouthColor = try container.decode(Color.self, forKey: .mouthColor)
|
||||
circleStrokeColor = try container.decode(Color.self, forKey: .circleStrokeColor)
|
||||
inUse = try container.decode(Bool.self, forKey: .inUse)
|
||||
uuid = try container.decode(String.self, forKey: .uuid)
|
||||
isSaved = try container.decode(Bool.self, forKey: .isSaved)
|
||||
createdDate = try container.decode(Date.self, forKey: .createdDate)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(leftEye, forKey: .leftEye)
|
||||
try container.encode(rightEye, forKey: .rightEye)
|
||||
try container.encode(mouth, forKey: .mouth)
|
||||
|
||||
try container.encode(background, forKey: .background)
|
||||
try container.encode(bgColor, forKey: .bgColor)
|
||||
try container.encode(innerColor, forKey: .innerColor)
|
||||
try container.encode(bgOverlayColor, forKey: .bgOverlayColor)
|
||||
try container.encode(leftEyeColor, forKey: .leftEyeColor)
|
||||
try container.encode(rightEyeColor, forKey: .rightEyeColor)
|
||||
try container.encode(mouthColor, forKey: .mouthColor)
|
||||
try container.encode(circleStrokeColor, forKey: .circleStrokeColor)
|
||||
try container.encode(inUse, forKey: .inUse)
|
||||
try container.encode(uuid, forKey: .uuid)
|
||||
try container.encode(isSaved, forKey: .isSaved)
|
||||
try container.encode(createdDate, forKey: .createdDate)
|
||||
}
|
||||
|
||||
func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = CustomWidgetModel(leftEye: self.leftEye,
|
||||
rightEye: self.rightEye,
|
||||
mouth: self.mouth,
|
||||
background: self.background,
|
||||
bgColor: self.bgColor,
|
||||
innerColor: self.innerColor,
|
||||
bgOverlayColor: self.bgOverlayColor,
|
||||
rightEyeColor: self.rightEyeColor,
|
||||
leftEyeColor: self.leftEyeColor,
|
||||
mouthColor: self.mouthColor,
|
||||
circleStrokeColor: self.circleStrokeColor,
|
||||
inUse: self.inUse,
|
||||
uuid: self.uuid,
|
||||
isSaved: self.isSaved,
|
||||
createdDate: self.createdDate)
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
||||
enum CustomWidgetBackGroundOptions: String, CaseIterable, Codable {
|
||||
case horrible
|
||||
case bad
|
||||
case average
|
||||
case good
|
||||
case great
|
||||
case random
|
||||
|
||||
static var selectable: [CustomWidgetBackGroundOptions] {
|
||||
return [.great, .good, .average, .bad, .horrible]
|
||||
}
|
||||
|
||||
static public var defaultOption: CustomWidgetBackGroundOptions {
|
||||
CustomWidgetBackGroundOptions.random
|
||||
}
|
||||
|
||||
public var image: Image {
|
||||
return Image(self.rawValue, bundle: .main)
|
||||
}
|
||||
}
|
||||
|
||||
enum CustomWidgetEyes: String, Codable {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
enum CustomWidgeImageOptions: String, CaseIterable, Codable {
|
||||
case star_solid = "custom_icon/star-solid"
|
||||
case jet_fighter_solid = "custom_icon/jet-fighter-solid"
|
||||
case circle_xmark_solid = "custom_icon/circle-xmark-solid"
|
||||
case frown_regular = "custom_icon/frown-regular"
|
||||
case virus_covid_solid = "custom_icon/virus-covid-solid"
|
||||
case bullhorn_solid = "custom_icon/bullhorn-solid"
|
||||
case caret_down_solid = "custom_icon/caret-down-solid"
|
||||
case meteor_solid = "custom_icon/meteor-solid"
|
||||
case eye_solid = "custom_icon/eye-solid"
|
||||
case battery_half_solid = "custom_icon/battery-half-solid"
|
||||
case heart_crack_solid = "custom_icon/heart-crack-solid"
|
||||
case heart_solid = "custom_icon/heart-solid"
|
||||
case divide_solid = "custom_icon/divide-solid"
|
||||
case location_crosshairs_solid = "custom_icon/location-crosshairs-solid"
|
||||
case bitcoin_brands = "custom_icon/bitcoin-brands"
|
||||
case baby_solid = "custom_icon/baby-solid"
|
||||
case grin_regular = "custom_icon/grin-regular"
|
||||
case poo_storm_solid = "custom_icon/poo-storm-solid"
|
||||
case btc_brands = "custom_icon/btc-brands"
|
||||
case shuttle_space_solid = "custom_icon/shuttle-space-solid"
|
||||
case chess_king_solid = "custom_icon/chess-king-solid"
|
||||
case chess_queen_solid = "custom_icon/chess-queen-solid"
|
||||
case lightbulb_solid = "custom_icon/lightbulb-solid"
|
||||
case skull_solid = "custom_icon/skull-solid"
|
||||
case dice_one_solid = "custom_icon/dice-one-solid"
|
||||
case fan_solid = "custom_icon/fan-solid"
|
||||
case bolt_solid = "custom_icon/bolt-solid"
|
||||
case dharmachakra_solid = "custom_icon/dharmachakra-solid"
|
||||
case ban_solid = "custom_icon/ban-solid"
|
||||
case skull_crossbones_solid = "custom_icon/skull-crossbones-solid"
|
||||
case sad_tear_regular = "custom_icon/sad-tear-regular"
|
||||
case floppy_disk_solid = "custom_icon/floppy-disk-solid"
|
||||
case crosshairs_solid = "custom_icon/crosshairs-solid"
|
||||
case exclamation_solid = "custom_icon/exclamation-solid"
|
||||
case lemon_solid = "custom_icon/lemon-solid"
|
||||
case caret_right_solid = "custom_icon/caret-right-solid"
|
||||
case poo_solid = "custom_icon/poo-solid"
|
||||
case rainbow_solid = "custom_icon/rainbow-solid"
|
||||
case apple_brands = "custom_icon/apple-brands"
|
||||
case x_solid = "custom_icon/x-solid"
|
||||
case pizza_slice_solid = "custom_icon/pizza-slice-solid"
|
||||
case empire_brands = "custom_icon/empire-brands"
|
||||
case caret_up_solid = "custom_icon/caret-up-solid"
|
||||
case life_ring_solid = "custom_icon/life-ring-solid"
|
||||
case dragon_solid = "custom_icon/dragon-solid"
|
||||
case cannabis_solid = "custom_icon/cannabis-solid"
|
||||
case bullseye_solid = "custom_icon/bullseye-solid"
|
||||
case code_compare_solid = "custom_icon/code-compare-solid"
|
||||
case battery_empty_solid = "custom_icon/battery-empty-solid"
|
||||
case moon_solid = "custom_icon/moon-solid"
|
||||
case android_brands = "custom_icon/android-brands"
|
||||
case smile_beam_regular = "custom_icon/smile-beam-regular"
|
||||
case futbol_solid = "custom_icon/futbol-solid"
|
||||
case dollar_sign_solid = "custom_icon/dollar-sign-solid"
|
||||
case cross_solid = "custom_icon/cross-solid"
|
||||
case bomb_solid = "custom_icon/bomb-solid"
|
||||
case battery_full_solid = "custom_icon/battery-full-solid"
|
||||
case arrow_up_solid = "custom_icon/arrow-up-solid"
|
||||
case gem_solid = "custom_icon/gem-solid"
|
||||
case code_solid = "custom_icon/code-solid"
|
||||
case bolt_lightning_solid = "custom_icon/bolt-lightning-solid"
|
||||
case caret_left_solid = "custom_icon/caret-left-solid"
|
||||
case fort_awesome_brands = "custom_icon/fort-awesome-brands"
|
||||
case fire_solid = "custom_icon/fire-solid"
|
||||
case hotjar_brands = "custom_icon/hotjar-brands"
|
||||
case burger_solid = "custom_icon/burger-solid"
|
||||
case egg_solid = "custom_icon/egg-solid"
|
||||
case meh_regular = "custom_icon/meh-regular"
|
||||
case battery_three_quarters_solid = "custom_icon/battery-three-quarters-solid"
|
||||
case crown_solid = "custom_icon/crown-solid"
|
||||
case clock_solid = "custom_icon/clock-solid"
|
||||
case battery_quarter_solid = "custom_icon/battery-quarter-solid"
|
||||
case fly_brands = "custom_icon/fly-brands"
|
||||
case baseball_solid = "custom_icon/baseball-solid"
|
||||
case dice_d20_solid = "custom_icon/dice-d20-solid"
|
||||
case microphone_solid = "custom_icon/microphone-solid"
|
||||
case peace_solid = "custom_icon/peace-solid"
|
||||
|
||||
static public var defaultOption: CustomWidgeImageOptions {
|
||||
CustomWidgeImageOptions.bomb_solid
|
||||
}
|
||||
|
||||
public var image: Image {
|
||||
return Image(self.rawValue, bundle: .main)
|
||||
}
|
||||
}
|
||||
|
||||
enum CustomWidgetMouthOptions: String, CaseIterable, Codable {
|
||||
case star_solid = "custom_icon/star-solid"
|
||||
case jet_fighter_solid = "custom_icon/jet-fighter-solid"
|
||||
case circle_xmark_solid = "custom_icon/circle-xmark-solid"
|
||||
case frown_regular = "custom_icon/frown-regular"
|
||||
case virus_covid_solid = "custom_icon/virus-covid-solid"
|
||||
case bullhorn_solid = "custom_icon/bullhorn-solid"
|
||||
case caret_down_solid = "custom_icon/caret-down-solid"
|
||||
case meteor_solid = "custom_icon/meteor-solid"
|
||||
case eye_solid = "custom_icon/eye-solid"
|
||||
case battery_half_solid = "custom_icon/battery-half-solid"
|
||||
case heart_crack_solid = "custom_icon/heart-crack-solid"
|
||||
case heart_solid = "custom_icon/heart-solid"
|
||||
case divide_solid = "custom_icon/divide-solid"
|
||||
case location_crosshairs_solid = "custom_icon/location-crosshairs-solid"
|
||||
case bitcoin_brands = "custom_icon/bitcoin-brands"
|
||||
case baby_solid = "custom_icon/baby-solid"
|
||||
case grin_regular = "custom_icon/grin-regular"
|
||||
case poo_storm_solid = "custom_icon/poo-storm-solid"
|
||||
case btc_brands = "custom_icon/btc-brands"
|
||||
case shuttle_space_solid = "custom_icon/shuttle-space-solid"
|
||||
case chess_king_solid = "custom_icon/chess-king-solid"
|
||||
case chess_queen_solid = "custom_icon/chess-queen-solid"
|
||||
case lightbulb_solid = "custom_icon/lightbulb-solid"
|
||||
case skull_solid = "custom_icon/skull-solid"
|
||||
case dice_one_solid = "custom_icon/dice-one-solid"
|
||||
case fan_solid = "custom_icon/fan-solid"
|
||||
case bolt_solid = "custom_icon/bolt-solid"
|
||||
case dharmachakra_solid = "custom_icon/dharmachakra-solid"
|
||||
case ban_solid = "custom_icon/ban-solid"
|
||||
case skull_crossbones_solid = "custom_icon/skull-crossbones-solid"
|
||||
case sad_tear_regular = "custom_icon/sad-tear-regular"
|
||||
case floppy_disk_solid = "custom_icon/floppy-disk-solid"
|
||||
case crosshairs_solid = "custom_icon/crosshairs-solid"
|
||||
case exclamation_solid = "custom_icon/exclamation-solid"
|
||||
case lemon_solid = "custom_icon/lemon-solid"
|
||||
case caret_right_solid = "custom_icon/caret-right-solid"
|
||||
case poo_solid = "custom_icon/poo-solid"
|
||||
case rainbow_solid = "custom_icon/rainbow-solid"
|
||||
case apple_brands = "custom_icon/apple-brands"
|
||||
case x_solid = "custom_icon/x-solid"
|
||||
case pizza_slice_solid = "custom_icon/pizza-slice-solid"
|
||||
case empire_brands = "custom_icon/empire-brands"
|
||||
case caret_up_solid = "custom_icon/caret-up-solid"
|
||||
case life_ring_solid = "custom_icon/life-ring-solid"
|
||||
case dragon_solid = "custom_icon/dragon-solid"
|
||||
case cannabis_solid = "custom_icon/cannabis-solid"
|
||||
case bullseye_solid = "custom_icon/bullseye-solid"
|
||||
case code_compare_solid = "custom_icon/code-compare-solid"
|
||||
case battery_empty_solid = "custom_icon/battery-empty-solid"
|
||||
case moon_solid = "custom_icon/moon-solid"
|
||||
case android_brands = "custom_icon/android-brands"
|
||||
case smile_beam_regular = "custom_icon/smile-beam-regular"
|
||||
case futbol_solid = "custom_icon/futbol-solid"
|
||||
case dollar_sign_solid = "custom_icon/dollar-sign-solid"
|
||||
case cross_solid = "custom_icon/cross-solid"
|
||||
case bomb_solid = "custom_icon/bomb-solid"
|
||||
case battery_full_solid = "custom_icon/battery-full-solid"
|
||||
case arrow_up_solid = "custom_icon/arrow-up-solid"
|
||||
case gem_solid = "custom_icon/gem-solid"
|
||||
case code_solid = "custom_icon/code-solid"
|
||||
case bolt_lightning_solid = "custom_icon/bolt-lightning-solid"
|
||||
case caret_left_solid = "custom_icon/caret-left-solid"
|
||||
case fort_awesome_brands = "custom_icon/fort-awesome-brands"
|
||||
case fire_solid = "custom_icon/fire-solid"
|
||||
case hotjar_brands = "custom_icon/hotjar-brands"
|
||||
case burger_solid = "custom_icon/burger-solid"
|
||||
case egg_solid = "custom_icon/egg-solid"
|
||||
case meh_regular = "custom_icon/meh-regular"
|
||||
case battery_three_quarters_solid = "custom_icon/battery-three-quarters-solid"
|
||||
case crown_solid = "custom_icon/crown-solid"
|
||||
case clock_solid = "custom_icon/clock-solid"
|
||||
case battery_quarter_solid = "custom_icon/battery-quarter-solid"
|
||||
case fly_brands = "custom_icon/fly-brands"
|
||||
case baseball_solid = "custom_icon/baseball-solid"
|
||||
case dice_d20_solid = "custom_icon/dice-d20-solid"
|
||||
case microphone_solid = "custom_icon/microphone-solid"
|
||||
case peace_solid = "custom_icon/peace-solid"
|
||||
|
||||
static public var defaultOption: CustomWidgetMouthOptions {
|
||||
CustomWidgetMouthOptions.bomb_solid
|
||||
}
|
||||
|
||||
public var image: Image {
|
||||
return Image(self.rawValue, bundle: .main)
|
||||
}
|
||||
}
|
||||
103
Shared/Views/CustomWidget/CustomWidgetView.swift
Normal file
103
Shared/Views/CustomWidget/CustomWidgetView.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// IconView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/13/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CustomWidgetView: View {
|
||||
@StateObject public var customWidgetModel: CustomWidgetModel
|
||||
|
||||
private let facePercSize = 0.6
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1),
|
||||
GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(
|
||||
customWidgetModel.bgColor
|
||||
)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
LazyVGrid(columns: columns, alignment: .leading, spacing: 0) {
|
||||
ForEach(customWidgetModel.backgroundImages, id: \.self.1) { (bgOption, uuid) in
|
||||
bgOption
|
||||
.resizable()
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
.foregroundColor(customWidgetModel.bgOverlayColor)
|
||||
}
|
||||
}
|
||||
.scaleEffect(1.1)
|
||||
.clipped()
|
||||
.background(
|
||||
.clear
|
||||
)
|
||||
|
||||
Circle()
|
||||
.strokeBorder(customWidgetModel.circleStrokeColor, lineWidth: geo.size.width * 0.045)
|
||||
.background(Circle().fill(customWidgetModel.innerColor))
|
||||
.frame(width: geo.size.width*facePercSize,
|
||||
height: geo.size.height*facePercSize,
|
||||
alignment: .center)
|
||||
.alignmentGuide(.top, computeValue: { _ in
|
||||
return geo.size.width/2
|
||||
})
|
||||
|
||||
customWidgetModel.leftEye.image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: geo.size.width*0.12,
|
||||
height: geo.size.height*0.12,
|
||||
alignment: .center)
|
||||
.position(x: geo.size.width*0.4,
|
||||
y: geo.size.height*0.45)
|
||||
.foregroundColor(customWidgetModel.leftEyeColor)
|
||||
|
||||
customWidgetModel.rightEye.image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: geo.size.width*0.12,
|
||||
height: geo.size.height*0.12,
|
||||
alignment: .center)
|
||||
.position(x: geo.size.width*0.6,
|
||||
y: geo.size.height*0.45)
|
||||
.foregroundColor(customWidgetModel.rightEyeColor)
|
||||
|
||||
customWidgetModel.mouth.image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: geo.size.width*0.12,
|
||||
height: geo.size.height*0.12,
|
||||
alignment: .center)
|
||||
.position(x: geo.size.width*0.5,
|
||||
y: geo.size.height*0.65)
|
||||
.foregroundColor(customWidgetModel.mouthColor)
|
||||
}
|
||||
.position(x: geo.size.width/2,
|
||||
y: geo.size.height/2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WidgetView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CustomWidgetView(customWidgetModel: CustomWidgetModel.randomWidget)
|
||||
.frame(width: 256, height: 256, alignment: .center)
|
||||
|
||||
}
|
||||
}
|
||||
75
Shared/Views/CustomizeView/CustomizeView.swift
Normal file
75
Shared/Views/CustomizeView/CustomizeView.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// CustomizeView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/19/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CustomizeView: View {
|
||||
@State private var showSettings = false
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
settingsButtonView
|
||||
VStack {
|
||||
Group {
|
||||
CustomWigetView()
|
||||
IconPickerView()
|
||||
ThemePickerView()
|
||||
Divider()
|
||||
SampleEntryView()
|
||||
ImagePackPickerView()
|
||||
}
|
||||
Group {
|
||||
TintPickerView()
|
||||
TextColorPickerView()
|
||||
}
|
||||
Divider()
|
||||
DayFilterPickerView()
|
||||
Divider()
|
||||
ShapePickerView()
|
||||
Divider()
|
||||
PersonalityPackPickerView()
|
||||
}
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_customize_view")
|
||||
})
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView()
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
)
|
||||
}
|
||||
|
||||
private var settingsButtonView: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
showSettings.toggle()
|
||||
}, label: {
|
||||
Image(systemName: "gear")
|
||||
.foregroundColor(Color(UIColor.darkGray))
|
||||
.font(.system(size: 20))
|
||||
}).padding(.trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomizeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
CustomizeView()
|
||||
|
||||
CustomizeView()
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Shared/Views/CustomizeView/SubViews/CustomWigetView.swift
Normal file
67
Shared/Views/CustomizeView/SubViews/CustomWigetView.swift
Normal file
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// CustomWigetView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 4/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CustomWigetView: View {
|
||||
@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
|
||||
@StateObject private var selectedWidget = StupidAssCustomWidgetObservableObject()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
VStack {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
ForEach(UserDefaultsStore.getCustomWidgets(), id: \.uuid) { widget in
|
||||
CustomWidgetView(customWidgetModel: widget)
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
EventLogger.log(event: "show_widget")
|
||||
selectedWidget.fuckingWrapped = widget.copy() as? CustomWidgetModel
|
||||
selectedWidget.showFuckingSheet = true
|
||||
}
|
||||
}
|
||||
RoundedRectangle(cornerRadius: 10).fill().foregroundColor(theme.currentTheme.secondaryBGColor)
|
||||
.frame(width: 50, height: 50)
|
||||
.overlay(
|
||||
Image(systemName: "plus")
|
||||
)
|
||||
.onTapGesture {
|
||||
EventLogger.log(event: "tap_create_new_widget")
|
||||
selectedWidget.fuckingWrapped = CustomWidgetModel.randomWidget
|
||||
selectedWidget.showFuckingSheet = true
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill().foregroundColor(theme.currentTheme.bgColor))
|
||||
.padding()
|
||||
.cornerRadius(10)
|
||||
|
||||
Text("[\(String(localized: "how_to_add_widget"))](https://support.apple.com/guide/iphone/add-widgets-iphb8f1bf206/ios)")
|
||||
.accentColor(textColor)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.sheet(isPresented: $selectedWidget.showFuckingSheet) {
|
||||
if let fuckingWrapped = selectedWidget.fuckingWrapped {
|
||||
CreateWidgetView(customWidget: fuckingWrapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomWigetView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CustomWigetView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// DayFilterPickerView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 4/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DayFilterPickerView: View {
|
||||
@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
|
||||
@StateObject private var filteredDays = DaysFilterClass.shared
|
||||
|
||||
let weekdays = [(Calendar.current.shortWeekdaySymbols[0], 1),
|
||||
(Calendar.current.shortWeekdaySymbols[1], 2),
|
||||
(Calendar.current.shortWeekdaySymbols[2], 3),
|
||||
(Calendar.current.shortWeekdaySymbols[3], 4),
|
||||
(Calendar.current.shortWeekdaySymbols[4], 5),
|
||||
(Calendar.current.shortWeekdaySymbols[5], 6),
|
||||
(Calendar.current.shortWeekdaySymbols[6], 7)]
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
ForEach(weekdays.indices, id: \.self) { dayIdx in
|
||||
let day = String(weekdays[dayIdx].0)
|
||||
let value = weekdays[dayIdx].1
|
||||
Button(day.capitalized, action: {
|
||||
if filteredDays.currentFilters.contains(value) {
|
||||
filteredDays.removeFilter(filter: value)
|
||||
} else {
|
||||
filteredDays.addFilter(newFilter: value)
|
||||
}
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
})
|
||||
.frame(maxWidth: .infinity)
|
||||
.foregroundColor(filteredDays.currentFilters.contains(value) ? .green : .red)
|
||||
}
|
||||
}
|
||||
Text(String(localized: "day_picker_view_text"))
|
||||
.padding(.top)
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
}
|
||||
|
||||
struct DayFilterPickerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DayFilterPickerView()
|
||||
}
|
||||
}
|
||||
99
Shared/Views/CustomizeView/SubViews/IconPickerView.swift
Normal file
99
Shared/Views/CustomizeView/SubViews/IconPickerView.swift
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// IconPickerView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 4/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct IconPickerView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
let iconSets: [(String,String)] = [
|
||||
("AppIconGoodImage", "AppIconGood"),
|
||||
("AppIconAverageImage", "AppIconAverage"),
|
||||
("AppIconBadImage", "AppIconBad"),
|
||||
("AppIconBlueGreenImage", "AppIconBlueGreen"),
|
||||
("AppIconNeonGreenImage", "AppIconNeonGreen"),
|
||||
("AppIconPinkImage", "AppIconPink"),
|
||||
("AppIconPurpleImage", "AppIconPurple"),
|
||||
("AppIconCustom1Image", "AppIconCustom1"),
|
||||
("AppIconCustom2Image", "AppIconCustom2"),
|
||||
("AppIconCustom3Image", "AppIconCustom3"),
|
||||
("AppIconCustom4Image", "AppIconCustom4"),
|
||||
("AppIconCustom5Image", "AppIconCustom5"),
|
||||
("AppIconCustom6Image", "AppIconCustom6"),
|
||||
("AppIconCustom7Image", "AppIconCustom7"),
|
||||
("AppIconCustom8Image", "AppIconCustom8"),
|
||||
("AppIconCustom9Image", "AppIconCustom9"),
|
||||
("AppIconCustom10Image", "AppIconCustom10"),
|
||||
("AppIconCustom11Image", "AppIconCustom11"),
|
||||
("AppIconCustom12Image", "AppIconCustom12"),
|
||||
("AppIconCustom13Image", "AppIconCustom13"),
|
||||
("AppIconCustom14Image", "AppIconCustom14"),
|
||||
("AppIconCustom15Image", "AppIconCustom15"),
|
||||
("AppIconCustom16Image", "AppIconCustom16"),
|
||||
("AppIconCustom17Image", "AppIconCustom17"),
|
||||
("AppIconCustom18Image", "AppIconCustom18"),
|
||||
("AppIconCustom19Image", "AppIconCustom19"),
|
||||
("AppIconCustom20Image", "AppIconCustom20"),
|
||||
("AppIconCustom21Image", "AppIconCustom21"),
|
||||
("AppIconCustom22Image", "AppIconCustom22"),
|
||||
("AppIconCustom23Image", "AppIconCustom23"),
|
||||
("AppIconCustom24Image", "AppIconCustom24"),
|
||||
("AppIconCustom25Image", "AppIconCustom25"),
|
||||
("AppIconCustom26Image", "AppIconCustom26"),
|
||||
("AppIconCustom27Image", "AppIconCustom27"),
|
||||
("AppIconCustom28Image", "AppIconCustom28"),
|
||||
("AppIconCustom29Image", "AppIconCustom29")
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
VStack {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
UIApplication.shared.setAlternateIconName(nil)
|
||||
EventLogger.log(event: "change_icon_title", withData: ["title": "default"])
|
||||
}, label: {
|
||||
Image("AppIconImage", bundle: .main)
|
||||
.resizable()
|
||||
.frame(width: 50, height:50)
|
||||
.cornerRadius(10)
|
||||
})
|
||||
|
||||
|
||||
ForEach(iconSets, id: \.self.0){ iconSet in
|
||||
Button(action: {
|
||||
UIApplication.shared.setAlternateIconName(iconSet.1) { (error) in
|
||||
// FIXME: Handle error
|
||||
}
|
||||
EventLogger.log(event: "change_icon_title", withData: ["title": iconSet.1])
|
||||
}, label: {
|
||||
Image(iconSet.0, bundle: .main)
|
||||
.resizable()
|
||||
.frame(width: 50, height:50)
|
||||
.cornerRadius(10)
|
||||
})
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill().foregroundColor(theme.currentTheme.bgColor))
|
||||
.padding()
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
}
|
||||
|
||||
struct IconPickerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
IconPickerView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// ImagePackPickerView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 4/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ImagePackPickerView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome
|
||||
@AppStorage(UserDefaultsStore.Keys.customMoodTintUpdateNumber.rawValue, store: GroupUserDefaults.groupDefaults) private var customMoodTintUpdateNumber: Int = 0
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Text(String(customMoodTintUpdateNumber))
|
||||
.hidden()
|
||||
theme.currentTheme.secondaryBGColor
|
||||
VStack {
|
||||
ForEach(MoodImages.allCases, id: \.rawValue) { images in
|
||||
ZStack {
|
||||
VStack {
|
||||
HStack {
|
||||
ForEach(Mood.allValues, id: \.self) { mood in
|
||||
images.icon(forMood: mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 35, height: 35)
|
||||
.foregroundColor(
|
||||
moodTint.color(forMood: mood)
|
||||
)
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(imagePack == images ? theme.currentTheme.bgColor : .clear)
|
||||
.padding([.top, .bottom], -3)
|
||||
)
|
||||
.onTapGesture {
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
imagePack = images
|
||||
EventLogger.log(event: "change_image_pack_id", withData: ["id": images.rawValue])
|
||||
}
|
||||
if images.rawValue != (MoodImages.allCases.sorted(by: { $0.rawValue > $1.rawValue }).first?.rawValue) ?? 0 {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
}
|
||||
|
||||
struct ImagePackPickerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImagePackPickerView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// PersonalityPackPickerView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 4/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PersonalityPackPickerView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.personalityPack.rawValue, store: GroupUserDefaults.groupDefaults) private var personalityPack: PersonalityPack = .Default
|
||||
@State private var showOver18Alert = false
|
||||
@AppStorage(UserDefaultsStore.Keys.showNSFW.rawValue, store: GroupUserDefaults.groupDefaults) private var showNSFW: Bool = false
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
VStack {
|
||||
ForEach(PersonalityPack.allCases, id: \.self) { aPack in
|
||||
VStack(spacing: 10) {
|
||||
Text(String(aPack.title()))
|
||||
.font(.body)
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(aPack.randomPushNotificationStrings().title)
|
||||
.font(.body)
|
||||
.foregroundColor(Color(UIColor.systemGray))
|
||||
Text(aPack.randomPushNotificationStrings().body)
|
||||
.font(.body)
|
||||
.foregroundColor(Color(UIColor.systemGray))
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.padding()
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(personalityPack == aPack ? theme.currentTheme.bgColor : .clear)
|
||||
.padding(5)
|
||||
)
|
||||
.onTapGesture {
|
||||
if aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW {
|
||||
showOver18Alert = true
|
||||
EventLogger.log(event: "show_over_18_alert")
|
||||
} else {
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
personalityPack = aPack
|
||||
EventLogger.log(event: "change_personality_pack", withData: ["pack_title": aPack.title()])
|
||||
LocalNotification.rescheduleNotifiations()
|
||||
}
|
||||
}
|
||||
.blur(radius: aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW ? 5 : 0)
|
||||
.alert(isPresented: $showOver18Alert) {
|
||||
let primaryButton = Alert.Button.default(Text(String(localized: "customize_view_over18alert_ok"))) {
|
||||
showNSFW = true
|
||||
}
|
||||
let secondaryButton = Alert.Button.cancel(Text(String(localized: "customize_view_over18alert_no"))) {
|
||||
showNSFW = false
|
||||
}
|
||||
return Alert(title: Text(String(localized: "customize_view_over18alert_title")),
|
||||
message: Text(String(localized: "customize_view_over18alert_body")),
|
||||
primaryButton: primaryButton,
|
||||
secondaryButton: secondaryButton)
|
||||
}
|
||||
if aPack.rawValue != (PersonalityPack.allCases.sorted(by: { $0.rawValue > $1.rawValue }).first?.rawValue) ?? 0 {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
}
|
||||
|
||||
struct PersonalityPackPickerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PersonalityPackPickerView()
|
||||
}
|
||||
}
|
||||
71
Shared/Views/CustomizeView/SubViews/ShapePickerView.swift
Normal file
71
Shared/Views/CustomizeView/SubViews/ShapePickerView.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// ShapePickerView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 4/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ShapePickerView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@State var shapeRefreshToggleThing: Bool = false
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.shape.rawValue, store: GroupUserDefaults.groupDefaults) private var shape: BGShape = .circle
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
|
||||
|
||||
VStack(alignment:.leading) {
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(shapeRefreshToggleThing.description.localizedLowercase)
|
||||
.hidden()
|
||||
Image(systemName: "arrow.triangle.2.circlepath.circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20, alignment: .trailing)
|
||||
.foregroundColor(Color(UIColor.systemGray))
|
||||
.onTapGesture {
|
||||
shapeRefreshToggleThing.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
ForEach(BGShape.allCases, id: \.rawValue) { ashape in
|
||||
ashape.view(withText: Text("20"),
|
||||
bgColor: moodTint.color(forMood: Mood.allValues.randomElement()!), textColor: textColor)
|
||||
.frame(height: 50)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onTapGesture {
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
shape = ashape
|
||||
EventLogger.log(event: "change_mood_shape_id", withData: ["id": shape.rawValue])
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(shape == ashape ? theme.currentTheme.bgColor : .clear)
|
||||
.padding(-5)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
}
|
||||
|
||||
struct ShapePickerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ShapePickerView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// TextColorPickerView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 4/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TextColorPickerView: View {
|
||||
@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
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
ColorPicker(String(localized: "customize_view_view_text_color"), selection: $textColor)
|
||||
.padding()
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
}
|
||||
|
||||
struct TextColorPickerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TextColorPickerView()
|
||||
}
|
||||
}
|
||||
84
Shared/Views/CustomizeView/SubViews/ThemePickerView.swift
Normal file
84
Shared/Views/CustomizeView/SubViews/ThemePickerView.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// ThemePicker.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 4/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ThemePickerView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@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
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
ForEach(Theme.allCases, id:\.rawValue) { aTheme in
|
||||
Button(action: {
|
||||
theme = aTheme
|
||||
changeTextColor(forTheme: aTheme)
|
||||
EventLogger.log(event: "change_theme_id", withData: ["id": aTheme.rawValue])
|
||||
}, label: {
|
||||
VStack {
|
||||
aTheme.currentTheme.preview
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color(UIColor.systemGray), style: StrokeStyle(lineWidth: 2))
|
||||
)
|
||||
Text(aTheme.title)
|
||||
.foregroundColor(textColor)
|
||||
.font(.body)
|
||||
}
|
||||
})
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(theme == aTheme ? theme.currentTheme.bgColor : .clear)
|
||||
.padding(-5)
|
||||
|
||||
)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private func changeTextColor(forTheme theme: Theme) {
|
||||
if [Theme.iFeel, Theme.system].contains(theme) {
|
||||
let currentSystemScheme = UITraitCollection.current.userInterfaceStyle
|
||||
switch currentSystemScheme {
|
||||
case .unspecified:
|
||||
textColor = .black
|
||||
case .light:
|
||||
textColor = .black
|
||||
case .dark:
|
||||
textColor = .white
|
||||
@unknown default:
|
||||
textColor = .black
|
||||
}
|
||||
}
|
||||
if theme == Theme.dark {
|
||||
textColor = .white
|
||||
}
|
||||
if theme == Theme.light {
|
||||
textColor = .black
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ThemePickerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ThemePickerView()
|
||||
}
|
||||
}
|
||||
125
Shared/Views/CustomizeView/SubViews/TintPickerView.swift
Normal file
125
Shared/Views/CustomizeView/SubViews/TintPickerView.swift
Normal file
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// TintPickerView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 4/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TintPickerView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.customMoodTintUpdateNumber.rawValue, store: GroupUserDefaults.groupDefaults) private var customMoodTintUpdateNumber: Int = 0
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@StateObject private var customMoodTint = UserDefaultsStore.getCustomMoodTint()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
VStack {
|
||||
ForEach(MoodTints.defaultOptions, id: \.rawValue) { tint in
|
||||
HStack {
|
||||
ForEach(Mood.allValues, id: \.self) { mood in
|
||||
Circle()
|
||||
.frame(width: 35, height: 35)
|
||||
.foregroundColor(
|
||||
tint.color(forMood: mood)
|
||||
)
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(moodTint == tint ? theme.currentTheme.bgColor : .clear)
|
||||
.padding([.top, .bottom], -3)
|
||||
|
||||
)
|
||||
.onTapGesture {
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
moodTint = tint
|
||||
EventLogger.log(event: "change_mood_tint_id", withData: ["id": tint.rawValue])
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
ZStack {
|
||||
Color.clear
|
||||
|
||||
Rectangle()
|
||||
.frame(height: 35)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.foregroundColor(.clear)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
moodTint = .Custom
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
}
|
||||
|
||||
HStack {
|
||||
ColorPicker("", selection: $customMoodTint.colorOne)
|
||||
.onChange(of: customMoodTint.colorOne, perform: { _ in
|
||||
saveCustomMoodTint()
|
||||
})
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
|
||||
ColorPicker("", selection: $customMoodTint.colorTwo)
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onChange(of: customMoodTint.colorTwo, perform: { _ in
|
||||
saveCustomMoodTint()
|
||||
})
|
||||
|
||||
ColorPicker("", selection: $customMoodTint.colorThree)
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onChange(of: customMoodTint.colorThree, perform: { _ in
|
||||
saveCustomMoodTint()
|
||||
})
|
||||
|
||||
ColorPicker("", selection: $customMoodTint.colorFour)
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onChange(of: customMoodTint.colorFour, perform: { _ in
|
||||
saveCustomMoodTint()
|
||||
})
|
||||
|
||||
ColorPicker("", selection: $customMoodTint.colorFive)
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onChange(of: customMoodTint.colorFive, perform: { _ in
|
||||
saveCustomMoodTint()
|
||||
})
|
||||
}
|
||||
.background(
|
||||
Color.clear
|
||||
)
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(moodTint == .Custom ? theme.currentTheme.bgColor : .clear)
|
||||
.padding([.top, .bottom], -3)
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private func saveCustomMoodTint() {
|
||||
UserDefaultsStore.saveCustomMoodTint(customTint: customMoodTint)
|
||||
moodTint = .Custom
|
||||
EventLogger.log(event: "change_mood_tint_id", withData: ["id": MoodTints.Custom.rawValue])
|
||||
customMoodTintUpdateNumber += 1
|
||||
}
|
||||
}
|
||||
|
||||
struct TintPickerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TintPickerView()
|
||||
}
|
||||
}
|
||||
38
Shared/Views/DayChartView.swift
Normal file
38
Shared/Views/DayChartView.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// CircleView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 1/13/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct DayChartView: ChartViewItemBuildable, View, Hashable {
|
||||
init(color: Color, weekDay: Int, shape: BGShape) {
|
||||
self.color = color
|
||||
self.weekDay = weekDay
|
||||
self.shape = shape
|
||||
}
|
||||
|
||||
var id = UUID()
|
||||
var color: Color
|
||||
var weekDay: Int
|
||||
var shape: BGShape
|
||||
|
||||
var body: some View {
|
||||
shape.view(withText: Text(""), bgColor: color, textColor: .clear)
|
||||
.frame(minWidth: 5, idealWidth: 50, maxWidth: 50,
|
||||
minHeight: 5, idealHeight: 20, maxHeight: 50,
|
||||
alignment: .center)
|
||||
.opacity(color == Mood.missing.color ? 0.5 : 1.0)
|
||||
}
|
||||
|
||||
var filteredDaysView: some View {
|
||||
shape.view(withText: Text(""), bgColor: Mood.missing.color, textColor: .clear)
|
||||
.frame(minWidth: 5, idealWidth: 50, maxWidth: 50,
|
||||
minHeight: 5, idealHeight: 20, maxHeight: 50,
|
||||
alignment: .center)
|
||||
.opacity(color == Mood.missing.color ? 0.5 : 1.0)
|
||||
}
|
||||
}
|
||||
206
Shared/Views/DayView/DayView.swift
Normal file
206
Shared/Views/DayView/DayView.swift
Normal file
@@ -0,0 +1,206 @@
|
||||
//
|
||||
// HomeView.swift
|
||||
// Shared
|
||||
//
|
||||
// Created by Trey Tartt on 1/5/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import Charts
|
||||
|
||||
struct DayViewConstants {
|
||||
static let maxHeaderHeight = 200.0
|
||||
static let minHeaderHeight = 120.0
|
||||
}
|
||||
|
||||
struct DayView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
||||
@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.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
// store a value that gets changed when user updates custom colors to update the view since the moodTint doesn't change
|
||||
@AppStorage(UserDefaultsStore.Keys.customMoodTintUpdateNumber.rawValue, store: GroupUserDefaults.groupDefaults) private var customMoodTintUpdateNumber: Int = 0
|
||||
|
||||
// MARK: edit row properties
|
||||
@State private var showingSheet = false
|
||||
@State private var selectedEntry: MoodEntry?
|
||||
//
|
||||
|
||||
// MARK: ?? properties
|
||||
@State private var showTodayInput = true
|
||||
@State private var showUpdateEntryAlert = false
|
||||
@StateObject private var onboardingData = OnboardingDataDataManager.shared
|
||||
@StateObject private var filteredDays = DaysFilterClass.shared
|
||||
@EnvironmentObject var iapManager: IAPManager
|
||||
|
||||
@ObservedObject var viewModel: DayViewViewModel
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Text(String(customMoodTintUpdateNumber))
|
||||
.hidden()
|
||||
|
||||
mainView
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_home_view")
|
||||
})
|
||||
.sheet(isPresented: $showingSheet) {
|
||||
SettingsView()
|
||||
}
|
||||
.alert(DayViewViewModel.updateTitleHeader(forEntry: selectedEntry),
|
||||
isPresented: $showUpdateEntryAlert) {
|
||||
ForEach(Mood.allValues) { mood in
|
||||
Button(mood.strValue, action: {
|
||||
if let selectedEntry = selectedEntry {
|
||||
viewModel.update(entry: selectedEntry, toMood: mood)
|
||||
}
|
||||
showUpdateEntryAlert = false
|
||||
selectedEntry = nil
|
||||
})
|
||||
}
|
||||
|
||||
if let selectedEntry = selectedEntry,
|
||||
deleteEnabled,
|
||||
selectedEntry.mood != .missing {
|
||||
Button(String(localized: "content_view_delete_entry"), action: {
|
||||
viewModel.update(entry: selectedEntry, toMood: Mood.missing)
|
||||
showUpdateEntryAlert = false
|
||||
})
|
||||
}
|
||||
|
||||
Button(String(localized: "content_view_fill_in_missing_entry_cancel"), role: .cancel, action: {
|
||||
selectedEntry = nil
|
||||
showUpdateEntryAlert = false
|
||||
})
|
||||
}
|
||||
}
|
||||
.padding([.top])
|
||||
}
|
||||
|
||||
|
||||
// MARK: Views
|
||||
public var mainView: some View {
|
||||
VStack {
|
||||
if viewModel.hasNoData {
|
||||
Spacer()
|
||||
EmptyHomeView(showVote: true, viewModel: viewModel)
|
||||
Spacer()
|
||||
} else {
|
||||
VStack {
|
||||
headerView
|
||||
|
||||
listView
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
|
||||
PersistenceController.shared.fillInMissingDates()
|
||||
viewModel.updateData()
|
||||
}
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
)
|
||||
}
|
||||
|
||||
private var headerView: some View {
|
||||
VStack {
|
||||
if ShowBasedOnVoteLogics.isMissingCurrentVote(onboardingData: UserDefaultsStore.getOnboarding()) {
|
||||
AddMoodHeaderView(addItemHeaderClosure: { (mood, date) in
|
||||
viewModel.add(mood: mood, forDate: date, entryType: .header)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var listView: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 5, pinnedViews: [.sectionHeaders]) {
|
||||
ForEach(viewModel.grouped.sorted(by: {
|
||||
$0.key > $1.key
|
||||
}), id: \.key) { year, months in
|
||||
|
||||
// for reach month
|
||||
ForEach(months.sorted(by: {
|
||||
$0.key > $1.key
|
||||
}), id: \.key) { month, entries in
|
||||
Section(header: SectionHeaderView(month: month, year: year)) {
|
||||
monthListView(month: month, year: year, entries: entries)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(
|
||||
GeometryReader { proxy in
|
||||
let offset = proxy.frame(in: .named("scroll")).minY
|
||||
Color.clear.preference(key: ViewOffsetKey.self, value: offset)
|
||||
}
|
||||
)
|
||||
}
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight])
|
||||
}
|
||||
}
|
||||
|
||||
// view that make up the list body
|
||||
extension DayView {
|
||||
private func SectionHeaderView(month: Int, year: Int) -> some View {
|
||||
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
}
|
||||
|
||||
private func monthListView(month: Int, year: Int, entries: [MoodEntry]) -> some View {
|
||||
VStack {
|
||||
// for reach all entries
|
||||
ForEach(entries.sorted(by: {
|
||||
return $0.forDate! > $1.forDate!
|
||||
}), id: \.self) { entry in
|
||||
if filteredDays.currentFilters.contains(Int(entry.weekDay)) {
|
||||
// let _ = print(entry.forDate, entry.weekDay, filteredDays.currentFilters)
|
||||
EntryListView(entry: entry)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture(perform: {
|
||||
selectedEntry = entry
|
||||
showUpdateEntryAlert = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ViewOffsetKey: PreferenceKey {
|
||||
typealias Value = CGFloat
|
||||
static var defaultValue = CGFloat.zero
|
||||
static func reduce(value: inout Value, nextValue: () -> Value) {
|
||||
value += nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
struct DayView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)).environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
||||
.onAppear(perform: {
|
||||
PersistenceController.shared.populateMemory()
|
||||
})
|
||||
|
||||
DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false))
|
||||
.preferredColorScheme(.dark)
|
||||
.environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
||||
}
|
||||
}
|
||||
127
Shared/Views/DayView/DayViewViewModel.swift
Normal file
127
Shared/Views/DayView/DayViewViewModel.swift
Normal file
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// ContentModeViewModel.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 1/20/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
class DayViewViewModel: ObservableObject {
|
||||
@Published var grouped = [Int: [Int: [MoodEntry]]]()
|
||||
@Published var numberOfItems = 0
|
||||
|
||||
let addMonthStartWeekdayPadding: Bool
|
||||
|
||||
var hasNoData: Bool {
|
||||
grouped.isEmpty
|
||||
}
|
||||
|
||||
private var numberOfEntries: Int {
|
||||
var num = 0
|
||||
grouped.keys.forEach({
|
||||
let year = grouped[$0]
|
||||
let monthKeys = year?.keys
|
||||
monthKeys?.forEach({
|
||||
num += year![$0]!.count
|
||||
})
|
||||
})
|
||||
return num
|
||||
|
||||
// grouped.keys.map{
|
||||
// grouped[$0]!.values.reduce(0) { sum, array in
|
||||
// sum + array.count
|
||||
// }
|
||||
// }.reduce(0, +)
|
||||
}
|
||||
|
||||
init(addMonthStartWeekdayPadding: Bool) {
|
||||
self.addMonthStartWeekdayPadding = addMonthStartWeekdayPadding
|
||||
|
||||
PersistenceController.shared.switchContainerListeners.append {
|
||||
self.getGroupedData(addMonthStartWeekdayPadding: self.addMonthStartWeekdayPadding)
|
||||
}
|
||||
|
||||
PersistenceController.shared.addNewDataListener {
|
||||
withAnimation{
|
||||
self.updateData()
|
||||
}
|
||||
}
|
||||
updateData()
|
||||
}
|
||||
|
||||
private func getGroupedData(addMonthStartWeekdayPadding: Bool) {
|
||||
var newStuff = PersistenceController.shared.splitIntoYearMonth(includedDays: [1,2,3,4,5,6,7])
|
||||
if addMonthStartWeekdayPadding {
|
||||
newStuff = MoodEntryFunctions.padMoodEntriesForCalendar(entries: newStuff)
|
||||
}
|
||||
grouped = newStuff
|
||||
numberOfItems = numberOfEntries
|
||||
}
|
||||
|
||||
public func updateData() {
|
||||
getGroupedData(addMonthStartWeekdayPadding: self.addMonthStartWeekdayPadding)
|
||||
}
|
||||
|
||||
public func add(mood: Mood, forDate date: Date, entryType: EntryType) {
|
||||
PersistenceController.shared.add(mood: mood, forDate: date, entryType: entryType)
|
||||
}
|
||||
|
||||
public func update(entry: MoodEntry, toMood mood: Mood) {
|
||||
if !PersistenceController.shared.update(entryDate: entry.forDate!, withModd: mood) {
|
||||
#warning("show error")
|
||||
}
|
||||
}
|
||||
|
||||
public func delete(offsets: IndexSet, inMonth month: Int, inYear year: Int) {
|
||||
if let monthEntries = grouped[year],
|
||||
let entries = monthEntries[month] {
|
||||
var mutableEntries = entries.sorted(by: {
|
||||
$0.forDate! > $1.forDate!
|
||||
})
|
||||
var entriesToDelete = [MoodEntry]()
|
||||
for idx in offsets {
|
||||
let obj = mutableEntries.remove(at: idx)
|
||||
entriesToDelete.append(obj)
|
||||
}
|
||||
entriesToDelete.forEach({ entry in
|
||||
let entryDate = entry.forDate!
|
||||
PersistenceController.shared.viewContext.delete(entry)
|
||||
self.add(mood: .missing, forDate: entryDate, entryType: .listView)
|
||||
})
|
||||
}
|
||||
|
||||
do {
|
||||
try PersistenceController.shared.viewContext.save()
|
||||
} catch {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
|
||||
static func updateTitleHeader(forEntry entry: MoodEntry?) -> String {
|
||||
guard let entry = entry else {
|
||||
return ""
|
||||
}
|
||||
|
||||
guard let forDate = entry.forDate else {
|
||||
return ""
|
||||
}
|
||||
|
||||
let components = Calendar.current.dateComponents([.day, .month, .year], from: forDate)
|
||||
// let day = components.day!
|
||||
let month = components.month!
|
||||
let year = components.year!
|
||||
|
||||
let monthName = Random.monthName(fromMonthInt: month)
|
||||
let weekday = Random.weekdayName(fromDate:entry.forDate!)
|
||||
let dayz = Random.dayFormat(fromDate:entry.forDate!)
|
||||
|
||||
let string = weekday + " " + monthName + " " + dayz + " " + String(year)
|
||||
|
||||
return String(format: String(localized: "content_view_fill_in_missing_entry"), string)
|
||||
}
|
||||
}
|
||||
54
Shared/Views/EmptyView.swift
Normal file
54
Shared/Views/EmptyView.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// EmptyView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/10/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EmptyHomeView: View {
|
||||
@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
|
||||
|
||||
let showVote: Bool
|
||||
let viewModel: DayViewViewModel?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
VStack {
|
||||
if showVote {
|
||||
AddMoodHeaderView(addItemHeaderClosure: { (mood, date) in
|
||||
withAnimation {
|
||||
viewModel?.add(mood: mood, forDate: date, entryType: .header)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(String(localized: "view_no_data"))
|
||||
.font(.title)
|
||||
.padding()
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(textColor)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyHomeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
EmptyHomeView(showVote: true, viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false))
|
||||
|
||||
EmptyHomeView(showVote: false, viewModel: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Shared/Views/EntryListView.swift
Normal file
56
Shared/Views/EntryListView.swift
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// EntryListView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 3/6/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EntryListView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
public let entry: MoodEntry
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
imagePack.icon(forMood: entry.mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40, height: 40, alignment: .center)
|
||||
.foregroundColor(moodTint.color(forMood: entry.mood))
|
||||
.padding(.leading, 15)
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Text(Random.weekdayName(fromDate:entry.forDate!))
|
||||
.font(.title3)
|
||||
.foregroundColor(textColor)
|
||||
Text(" - ")
|
||||
.padding([.leading, .trailing], -10)
|
||||
.foregroundColor(textColor)
|
||||
Text(Random.dayFormat(fromDate:entry.forDate!))
|
||||
.font(.title3)
|
||||
.foregroundColor(textColor)
|
||||
Spacer()
|
||||
}
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
Text(entry.moodValue == Mood.missing.rawValue ? String(localized: "mood_value_missing_tap_to_add") : "\(entry.moodString)")
|
||||
.font(.body)
|
||||
.foregroundColor(Color(UIColor.systemGray))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EntryListView_Previews: PreviewProvider {
|
||||
static let fakeData = PersistenceController.shared.randomEntries(count: 1).first!
|
||||
|
||||
static var previews: some View {
|
||||
EntryListView(entry: EntryListView_Previews.fakeData)
|
||||
}
|
||||
}
|
||||
47
Shared/Views/FeelsSubscriptionStoreView.swift
Normal file
47
Shared/Views/FeelsSubscriptionStoreView.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// FeelsSubscriptionStoreView.swift
|
||||
// Feels
|
||||
//
|
||||
// Native StoreKit 2 subscription purchase view.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import StoreKit
|
||||
|
||||
struct FeelsSubscriptionStoreView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@EnvironmentObject var iapManager: IAPManager
|
||||
|
||||
var body: some View {
|
||||
SubscriptionStoreView(groupID: IAPManager.subscriptionGroupID) {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "heart.fill")
|
||||
.font(.system(size: 60))
|
||||
.foregroundStyle(.pink)
|
||||
|
||||
Text(String(localized: "subscription_store_title"))
|
||||
.font(.title)
|
||||
.bold()
|
||||
|
||||
Text(String(localized: "subscription_store_subtitle"))
|
||||
.font(.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.subscriptionStoreControlStyle(.prominentPicker)
|
||||
.storeButton(.visible, for: .restorePurchases)
|
||||
.subscriptionStoreButtonLabel(.multiline)
|
||||
.onInAppPurchaseCompletion { _, result in
|
||||
if case .success(.success(_)) = result {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
FeelsSubscriptionStoreView()
|
||||
.environmentObject(IAPManager())
|
||||
}
|
||||
146
Shared/Views/GraphView.swift
Normal file
146
Shared/Views/GraphView.swift
Normal file
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// GraphView.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/8/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import Charts
|
||||
|
||||
struct GraphView: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(UIColor.secondarySystemBackground)
|
||||
VStack {
|
||||
VStack {
|
||||
HStack {
|
||||
ZStack {
|
||||
Color(UIColor.systemBackground)
|
||||
BarChartGraph(entries: [
|
||||
BarChartDataEntry(x: 1, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 2, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 3, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 4, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 5, y: Double(Int.random(in: 0...10)))
|
||||
])
|
||||
}
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.padding()
|
||||
|
||||
ZStack {
|
||||
Color(UIColor.systemBackground)
|
||||
BarChartGraph(entries: [
|
||||
BarChartDataEntry(x: 1, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 2, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 3, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 4, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 5, y: Double(Int.random(in: 0...10)))
|
||||
])
|
||||
}
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.padding()
|
||||
}
|
||||
|
||||
ZStack {
|
||||
Color(UIColor.systemBackground)
|
||||
BarChartGraph(entries: [
|
||||
BarChartDataEntry(x: 1, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 2, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 3, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 4, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 5, y: Double(Int.random(in: 0...10)))
|
||||
])
|
||||
}
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.padding()
|
||||
|
||||
ZStack {
|
||||
Color(UIColor.systemBackground)
|
||||
BarChartGraph(entries: [
|
||||
BarChartDataEntry(x: 1, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 2, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 3, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 4, y: Double(Int.random(in: 0...10))),
|
||||
BarChartDataEntry(x: 5, y: Double(Int.random(in: 0...10)))
|
||||
])
|
||||
}
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BarChartGraph: UIViewRepresentable {
|
||||
//Bar chart accepts data as array of BarChartDataEntry objects
|
||||
var entries : [BarChartDataEntry]
|
||||
|
||||
// this func is required to conform to UIViewRepresentable protocol
|
||||
func makeUIView(context: Context) -> BarChartView {
|
||||
//crate new chart
|
||||
let chart = BarChartView()
|
||||
chart.drawGridBackgroundEnabled = false
|
||||
chart.drawValueAboveBarEnabled = false
|
||||
|
||||
chart.xAxis.drawAxisLineEnabled = false
|
||||
chart.xAxis.labelTextColor = .clear
|
||||
|
||||
chart.rightAxis.drawAxisLineEnabled = false
|
||||
chart.rightAxis.labelTextColor = .clear
|
||||
|
||||
chart.leftAxis.drawAxisLineEnabled = false
|
||||
chart.leftAxis.labelTextColor = .clear
|
||||
|
||||
chart.xAxis.drawGridLinesEnabled = false
|
||||
chart.leftAxis.drawGridLinesEnabled = false
|
||||
chart.leftAxis.axisLineColor = .clear
|
||||
chart.rightAxis.axisLineColor = .clear
|
||||
|
||||
chart.legend.textColor = .clear
|
||||
chart.legend.enabled = false
|
||||
|
||||
chart.drawBordersEnabled = false
|
||||
chart.drawMarkers = false
|
||||
// chart.yAxis.drawGridLinesEnabled = false
|
||||
chart.rightAxis.drawGridLinesEnabled = false
|
||||
chart.borderColor = .clear
|
||||
//it is convenient to form chart data in a separate func
|
||||
chart.data = addData()
|
||||
return chart
|
||||
}
|
||||
|
||||
// this func is required to conform to UIViewRepresentable protocol
|
||||
func updateUIView(_ uiView: BarChartView, context: Context) {
|
||||
//when data changes chartd.data update is required
|
||||
uiView.data = addData()
|
||||
}
|
||||
|
||||
func addData() -> BarChartData{
|
||||
let data = BarChartData()
|
||||
//BarChartDataSet is an object that contains information about your data, styling and more
|
||||
let dataSet = BarChartDataSet(entries: entries)
|
||||
// change bars color to green
|
||||
dataSet.colors = [NSUIColor.green]
|
||||
//change data label
|
||||
data.append(dataSet)
|
||||
return data
|
||||
}
|
||||
|
||||
typealias UIViewType = BarChartView
|
||||
}
|
||||
|
||||
|
||||
struct GraphView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
GraphView()
|
||||
|
||||
GraphView()
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
119
Shared/Views/HeaderPercView.swift
Normal file
119
Shared/Views/HeaderPercView.swift
Normal file
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// HeaderPercView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 1/29/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum PercViewType {
|
||||
case text
|
||||
case shape
|
||||
}
|
||||
|
||||
struct HeaderPercView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.shape.rawValue, store: GroupUserDefaults.groupDefaults) private var shape: BGShape = .circle
|
||||
|
||||
@State private var entries = [MoodMetrics]()
|
||||
let backDays: Int
|
||||
let type: PercViewType
|
||||
|
||||
init(fakeData: Bool, backDays: Int, type: PercViewType) {
|
||||
self.type = type
|
||||
self.backDays = backDays
|
||||
var moodEntries: [MoodEntry]?
|
||||
|
||||
if fakeData {
|
||||
moodEntries = PersistenceController.shared.randomEntries(count: 10)
|
||||
} else {
|
||||
var daysAgo = Calendar.current.date(byAdding: .day, value: -backDays, to: Date())!
|
||||
daysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: daysAgo)!
|
||||
|
||||
moodEntries = PersistenceController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7])
|
||||
}
|
||||
|
||||
if let moodEntries = moodEntries {
|
||||
let moodMetrics = Random.createTotalPerc(fromEntries: moodEntries)
|
||||
|
||||
_entries = State(initialValue: moodMetrics.sorted(by: {
|
||||
$0.mood.rawValue > $1.mood.rawValue
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
private var textViews: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
ForEach(entries.prefix(3), id: \.id) { model in
|
||||
Text("\(model.percent, specifier: "%.0f")%")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(moodTint.color(forMood: model.mood))
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
HStack {
|
||||
ForEach(entries.suffix(2), id: \.id) { model in
|
||||
Text("\(model.percent, specifier: "%.0f")%")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(moodTint.color(forMood: model.mood))
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
private var shapeViews: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
ForEach(entries.prefix(3), id: \.id) { model in
|
||||
shape.view(withText: Text("\(model.percent, specifier: "%.0f")%"),
|
||||
bgColor: moodTint.color(forMood: model.mood),
|
||||
textColor: textColor)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
HStack {
|
||||
ForEach(entries.suffix(2), id: \.id) { model in
|
||||
shape.view(withText: Text("\(model.percent, specifier: "%.0f")%"),
|
||||
bgColor: moodTint.color(forMood: model.mood),
|
||||
textColor: textColor)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
switch self.type {
|
||||
case .text:
|
||||
textViews
|
||||
case .shape:
|
||||
shapeViews
|
||||
.padding([.leading, .trailing])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HeaderPercView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HeaderPercView(fakeData: true, backDays: 30, type: .text)
|
||||
|
||||
HeaderPercView(fakeData: true, backDays: 30, type: .shape)
|
||||
}
|
||||
}
|
||||
135
Shared/Views/HeaderStatsView.swift
Normal file
135
Shared/Views/HeaderStatsView.swift
Normal file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// HeaderStatsView.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/8/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Charts
|
||||
|
||||
struct HeaderStatsView : UIViewRepresentable {
|
||||
//Bar chart accepts data as array of BarChartDataEntry objects
|
||||
var entries : [BarChartDataEntry]
|
||||
var moodTints: [Color]
|
||||
var textColor: Color
|
||||
|
||||
var tmpHolderToMakeViewDiffefrent: Color
|
||||
|
||||
init(fakeData: Bool, backDays: Int, moodTint: [Color], textColor: Color) {
|
||||
self.moodTints = moodTint
|
||||
self.textColor = textColor
|
||||
guard moodTints.count == 5 else {
|
||||
fatalError("mood tint count dont match")
|
||||
}
|
||||
self.tmpHolderToMakeViewDiffefrent = Color.random()
|
||||
entries = [BarChartDataEntry]()
|
||||
|
||||
var moodEntries: [MoodEntry]?
|
||||
|
||||
if fakeData {
|
||||
moodEntries = PersistenceController.shared.randomEntries(count: 10)
|
||||
} else {
|
||||
var daysAgo = Calendar.current.date(byAdding: .day, value: -backDays, to: Date())!
|
||||
daysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: daysAgo)!
|
||||
|
||||
moodEntries = PersistenceController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7])
|
||||
}
|
||||
if let moodEntries = moodEntries {
|
||||
for (index, mood) in Mood.allValues.enumerated() {
|
||||
entries.append(
|
||||
BarChartDataEntry(x: Double(index + 1),
|
||||
y: Double(moodEntries.filter({
|
||||
Int($0.moodValue) == mood.rawValue
|
||||
}).count))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this func is required to conform to UIViewRepresentable protocol
|
||||
func makeUIView(context: Context) -> BarChartView {
|
||||
//crate new chart
|
||||
let chart = BarChartView()
|
||||
chart.drawGridBackgroundEnabled = false
|
||||
chart.drawValueAboveBarEnabled = false
|
||||
|
||||
chart.xAxis.drawAxisLineEnabled = false
|
||||
chart.xAxis.labelTextColor = .clear
|
||||
|
||||
chart.rightAxis.drawAxisLineEnabled = false
|
||||
chart.rightAxis.labelTextColor = .clear
|
||||
|
||||
chart.leftAxis.drawAxisLineEnabled = false
|
||||
chart.leftAxis.labelTextColor = .clear
|
||||
|
||||
chart.xAxis.drawGridLinesEnabled = false
|
||||
chart.leftAxis.drawGridLinesEnabled = false
|
||||
chart.rightAxis.drawGridLinesEnabled = false
|
||||
|
||||
chart.leftAxis.axisLineColor = .clear
|
||||
chart.rightAxis.axisLineColor = .clear
|
||||
|
||||
chart.legend.textColor = .clear
|
||||
chart.legend.enabled = false
|
||||
|
||||
chart.drawBordersEnabled = false
|
||||
chart.drawMarkers = false
|
||||
chart.borderColor = .clear
|
||||
|
||||
chart.doubleTapToZoomEnabled = false
|
||||
chart.leftAxis.axisMinimum = 0
|
||||
|
||||
chart.minOffset = 0
|
||||
|
||||
let data = BarChartData()
|
||||
let dataSet = dataSet()
|
||||
data.append(dataSet)
|
||||
|
||||
chart.data = data
|
||||
dataSet.valueFormatter = DefaultValueFormatter(decimals: 0)
|
||||
|
||||
return chart
|
||||
}
|
||||
|
||||
// this func is required to conform to UIViewRepresentable protocol
|
||||
func updateUIView(_ uiView: BarChartView, context: Context) {
|
||||
let data = BarChartData()
|
||||
let dataSet = dataSet()
|
||||
data.append(dataSet)
|
||||
uiView.data = data
|
||||
|
||||
dataSet.valueFormatter = DefaultValueFormatter(decimals: 0)
|
||||
}
|
||||
|
||||
func dataSet() -> BarChartDataSet {
|
||||
let dataSet = BarChartDataSet(entries: entries)
|
||||
|
||||
// change bars color to green
|
||||
dataSet.colors = moodTints.map({ NSUIColor( $0 ) })
|
||||
dataSet.secondaryTextColor = UIColor(textColor)
|
||||
dataSet.valueColors = [UIColor(textColor)]
|
||||
dataSet.highlightAlpha = 0.0
|
||||
dataSet.roundedCornerValue = 10
|
||||
|
||||
if let descriptor = UIFontDescriptor.preferredFontDescriptor(
|
||||
withTextStyle: .title1).withSymbolicTraits([.traitBold]) {
|
||||
dataSet.valueFont = UIFont(descriptor: descriptor, size: 0)
|
||||
} else {
|
||||
dataSet.valueFont = UIFont.preferredFont(forTextStyle: .title1)
|
||||
}
|
||||
|
||||
return dataSet
|
||||
}
|
||||
|
||||
typealias UIViewType = BarChartView
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct HeaderStatsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HeaderStatsView(fakeData: true, backDays: 30, moodTint: [Color.green, Color.blue, Color.yellow, Color.red, Color.orange], textColor: .white).frame(minHeight: 85, maxHeight: 90)
|
||||
}
|
||||
}
|
||||
57
Shared/Views/IAPWarningView.swift
Normal file
57
Shared/Views/IAPWarningView.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// IAPWarningView.swift
|
||||
// Feels
|
||||
//
|
||||
// Trial warning banner shown at bottom of Month/Year views.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct IAPWarningView: View {
|
||||
@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
|
||||
|
||||
@ObservedObject var iapManager: IAPManager
|
||||
|
||||
@State private var showSubscriptionStore = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
HStack {
|
||||
Image(systemName: "clock")
|
||||
.foregroundColor(.orange)
|
||||
|
||||
Text(String(localized: "iap_warning_view_title"))
|
||||
.font(.body)
|
||||
.foregroundColor(textColor)
|
||||
|
||||
if let expirationDate = iapManager.trialExpirationDate {
|
||||
Text(expirationDate, style: .relative)
|
||||
.font(.body)
|
||||
.bold()
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
showSubscriptionStore = true
|
||||
} label: {
|
||||
Text(String(localized: "iap_warning_view_buy_button"))
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color.pink))
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(theme.currentTheme.secondaryBGColor)
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
IAPWarningView(iapManager: IAPManager())
|
||||
}
|
||||
54
Shared/Views/ImagePickerGridView.swift
Normal file
54
Shared/Views/ImagePickerGridView.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// ImagePickerGrid.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 3/12/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ImagePickerGridView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@State var column = Array(repeating: GridItem(.flexible(), spacing: 10), count: 7)
|
||||
let pickedImageClosure: ((CustomWidgeImageOptions) -> Void)
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
let imageOptions = CustomWidgeImageOptions.allCases.sorted(by: {
|
||||
$0.rawValue < $1.rawValue
|
||||
})
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: column,spacing: 10, content: {
|
||||
ForEach(imageOptions, id:\.self.rawValue) { item in
|
||||
Image(item.rawValue)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(textColor)
|
||||
.onTapGesture {
|
||||
pickedImageClosure(item)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ImagePickerGridView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImagePickerGridView(pickedImageClosure: { image in
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
88
Shared/Views/MainTabView.swift
Normal file
88
Shared/Views/MainTabView.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// MainTabView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/18/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MainTabView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.needsOnboarding.rawValue, store: GroupUserDefaults.groupDefaults) private var needsOnboarding = true
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
|
||||
let onboardingData = OnboardingDataDataManager.shared.savedOnboardingData
|
||||
|
||||
let dayView: DayView
|
||||
let monthView: MonthView
|
||||
let yearView: YearView
|
||||
let customizeView: CustomizeView
|
||||
|
||||
var body: some View {
|
||||
return TabView {
|
||||
dayView
|
||||
.tabItem {
|
||||
Label(String(localized: "content_view_tab_main"), systemImage: "list.dash")
|
||||
}
|
||||
|
||||
monthView
|
||||
.tabItem {
|
||||
Label(String(localized: "content_view_tab_month"), systemImage: "calendar")
|
||||
}
|
||||
|
||||
yearView
|
||||
.tabItem {
|
||||
Label(String(localized: "content_view_tab_filter"), systemImage: "line.3.horizontal.decrease.circle")
|
||||
}
|
||||
|
||||
customizeView
|
||||
.tabItem {
|
||||
Label(String(localized: "content_view_tab_customize"), systemImage: "pencil")
|
||||
}
|
||||
}
|
||||
.accentColor(textColor)
|
||||
.sheet(isPresented: $needsOnboarding, onDismiss: { }, content: {
|
||||
OnboardingMain(onboardingData: onboardingData,
|
||||
updateBoardingDataClosure: { onboardingData in
|
||||
needsOnboarding = false
|
||||
OnboardingDataDataManager.shared.updateOnboardingData(onboardingData: onboardingData)
|
||||
})
|
||||
})
|
||||
.onAppear(perform: {
|
||||
switch theme {
|
||||
case .system:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .unspecified
|
||||
case .iFeel:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .unspecified
|
||||
case .dark:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .dark
|
||||
case .light:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .light
|
||||
}
|
||||
})
|
||||
.onChange(of: theme, perform: { value in
|
||||
switch theme {
|
||||
case .system:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .unspecified
|
||||
case .iFeel:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .unspecified
|
||||
case .dark:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .dark
|
||||
case .light:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .light
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct MainTabView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MainTabView(dayView: DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)),
|
||||
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
|
||||
yearView: YearView(viewModel: YearViewModel()),
|
||||
customizeView: CustomizeView())
|
||||
}
|
||||
}
|
||||
228
Shared/Views/MonthView/MonthDetailView.swift
Normal file
228
Shared/Views/MonthView/MonthDetailView.swift
Normal file
@@ -0,0 +1,228 @@
|
||||
//
|
||||
// MonthDetailView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/18/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MonthDetailView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
@StateObject private var shareImage = StupidAssShareObservableObject()
|
||||
|
||||
@State private var showingSheet = false
|
||||
@State private var selectedEntry: MoodEntry?
|
||||
@State private var showingUpdateEntryAlert = false
|
||||
@State private var showUpdateEntryAlert = false
|
||||
|
||||
let monthInt: Int
|
||||
let yearInt: Int
|
||||
@State var entries: [MoodEntry]
|
||||
var parentViewModel: DayViewViewModel
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(minimum: 5, maximum: 500)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 500)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 500)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 500)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 500)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 500)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 500))
|
||||
]
|
||||
|
||||
var image: UIImage {
|
||||
let image = shareView.asImage(size: CGSize(width: 666, height: 1190))
|
||||
return image
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Text("\(Random.monthName(fromMonthInt: monthInt)) \(String(yearInt))")
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
|
||||
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.foregroundColor(textColor)
|
||||
.padding(.trailing)
|
||||
.onTapGesture {
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
|
||||
let _image = self.image
|
||||
self.shareImage.showFuckingSheet = true
|
||||
self.shareImage.fuckingWrappedShrable = _image
|
||||
}
|
||||
}
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
|
||||
createListView()
|
||||
.padding([.leading, .trailing])
|
||||
|
||||
monthDetails
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_month_detail_view")
|
||||
})
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
)
|
||||
.sheet(isPresented: self.$shareImage.showFuckingSheet) {
|
||||
if let uiImage = self.shareImage.fuckingWrappedShrable {
|
||||
ShareSheet(photo: uiImage)
|
||||
}
|
||||
}
|
||||
.alert(DayViewViewModel.updateTitleHeader(forEntry: selectedEntry), isPresented: $showUpdateEntryAlert) {
|
||||
ForEach(Mood.allValues) { mood in
|
||||
Button(mood.strValue, action: {
|
||||
if let selectedEntry = selectedEntry {
|
||||
PersistenceController.shared.update(entryDate: selectedEntry.forDate!, withModd: mood)
|
||||
}
|
||||
updateEntries()
|
||||
showUpdateEntryAlert = false
|
||||
selectedEntry = nil
|
||||
})
|
||||
}
|
||||
|
||||
if let selectedEntry = selectedEntry,
|
||||
deleteEnabled,
|
||||
selectedEntry.mood != .missing {
|
||||
Button(String(localized: "content_view_delete_entry"), action: {
|
||||
PersistenceController.shared.update(entryDate: selectedEntry.forDate!, withModd: .missing)
|
||||
updateEntries()
|
||||
showUpdateEntryAlert = false
|
||||
})
|
||||
}
|
||||
|
||||
Button(String(localized: "content_view_fill_in_missing_entry_cancel"), role: .cancel, action: {
|
||||
updateEntries()
|
||||
selectedEntry = nil
|
||||
showUpdateEntryAlert = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private var shareView: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Text("\(Random.monthName(fromMonthInt: monthInt)) \(String(yearInt))")
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
}
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
|
||||
createListView()
|
||||
.padding([.leading, .trailing])
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
|
||||
monthDetails
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
}
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
)
|
||||
}
|
||||
|
||||
private func createListView() -> some View {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: columns, spacing: 25) {
|
||||
ForEach(entries, id: \.self) { entry in
|
||||
listViewEntry(forEntry: entry)
|
||||
.onTapGesture(perform: {
|
||||
if entry.canEdit {
|
||||
selectedEntry = entry
|
||||
showUpdateEntryAlert = true
|
||||
}
|
||||
})
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func listViewEntry(forEntry entry: MoodEntry) -> some View {
|
||||
VStack {
|
||||
if entry.mood == .placeholder {
|
||||
Text(" ")
|
||||
.font(.title3)
|
||||
.foregroundColor(Mood.placeholder.color)
|
||||
|
||||
Circle()
|
||||
.frame(minWidth: 5,
|
||||
maxWidth: 50,
|
||||
minHeight: 5,
|
||||
maxHeight: 50,
|
||||
alignment: .center)
|
||||
.foregroundColor(moodTint.color(forMood: entry.mood))
|
||||
} else {
|
||||
Text(entry.forDate!,
|
||||
format: Date.FormatStyle().day())
|
||||
.font(.title3)
|
||||
.foregroundColor(textColor)
|
||||
|
||||
entry.mood.icon
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(minWidth: 5,
|
||||
maxWidth: 50,
|
||||
minHeight: 5,
|
||||
maxHeight: 50,
|
||||
alignment: .center)
|
||||
.foregroundColor(moodTint.color(forMood: entry.mood))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateEntries() {
|
||||
parentViewModel.updateData()
|
||||
let (startDate, endDate) = Date.dateRange(monthInt: monthInt, yearInt: yearInt)
|
||||
let updatedEntries = PersistenceController.shared.getData(startDate: startDate, endDate: endDate, includedDays: [1,2,3,4,5,6,7])
|
||||
let padded = MoodEntryFunctions.padMoodEntriesMonth(monthEntries: updatedEntries)
|
||||
entries = padded
|
||||
}
|
||||
|
||||
private var monthDetails: some View {
|
||||
VStack {
|
||||
SmallRollUpHeaderView(entries: entries,
|
||||
viewType: .constant(.total))
|
||||
.frame(minHeight: 0, maxHeight: 100)
|
||||
|
||||
SmallRollUpHeaderView(entries: entries,
|
||||
viewType: .constant(.percentageShape))
|
||||
.frame(minHeight: 0, maxHeight: 100)
|
||||
.padding(.top, -20)
|
||||
}
|
||||
.frame(minHeight: 0, maxHeight: 200)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct MonthDetailView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MonthDetailView(monthInt: 5, yearInt: 2022, entries:
|
||||
PersistenceController.shared.randomEntries(count: 30).sorted(by: {
|
||||
$0.forDate! < $1.forDate!
|
||||
}), parentViewModel: DayViewViewModel(addMonthStartWeekdayPadding: true))
|
||||
}
|
||||
}
|
||||
276
Shared/Views/MonthView/MonthView.swift
Normal file
276
Shared/Views/MonthView/MonthView.swift
Normal file
@@ -0,0 +1,276 @@
|
||||
//
|
||||
// HomeViewTwo.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/18/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MonthView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.needsOnboarding.rawValue, store: GroupUserDefaults.groupDefaults) private var needsOnboarding = true
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.shape.rawValue, store: GroupUserDefaults.groupDefaults) private var shape: BGShape = .circle
|
||||
|
||||
@StateObject private var shareImage = StupidAssShareObservableObject()
|
||||
|
||||
// store a value that gets changed when user updates custom colors to update the view since the moodTint doesn't change
|
||||
@AppStorage(UserDefaultsStore.Keys.customMoodTintUpdateNumber.rawValue, store: GroupUserDefaults.groupDefaults) private var customMoodTintUpdateNumber: Int = 0
|
||||
|
||||
@EnvironmentObject var iapManager: IAPManager
|
||||
@StateObject private var selectedDetail = StupidAssDetailViewObservableObject()
|
||||
@State private var showingSheet = false
|
||||
@StateObject private var onboardingData = OnboardingDataDataManager.shared
|
||||
@StateObject private var filteredDays = DaysFilterClass.shared
|
||||
|
||||
class StupidAssDetailViewObservableObject: ObservableObject {
|
||||
@Published var fuckingWrapped: MonthDetailView? = nil
|
||||
@Published var showFuckingSheet = false
|
||||
}
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(minimum: 5, maximum: 400)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 400)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 400)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 400)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 400)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 400)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 400))
|
||||
]
|
||||
|
||||
@ObservedObject var viewModel: DayViewViewModel
|
||||
@State private var trialWarningHidden = false
|
||||
@State private var showSubscriptionStore = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if viewModel.hasNoData {
|
||||
EmptyHomeView(showVote: false, viewModel: nil)
|
||||
.padding()
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(spacing: 5) {
|
||||
ForEach(viewModel.grouped.sorted(by: { $0.key < $1.key }), id: \.key) { year, months in
|
||||
|
||||
// for reach month
|
||||
ForEach(months.sorted(by: { $0.key < $1.key }), id: \.key) { month, entries in
|
||||
Section() {
|
||||
homeViewTwoMonthListView(month: month, year: year, entries: entries)
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.foregroundColor(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
)
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.background(
|
||||
GeometryReader { proxy in
|
||||
let offset = proxy.frame(in: .named("scroll")).minY
|
||||
Color.clear.preference(key: ViewOffsetKey.self, value: offset)
|
||||
}
|
||||
)
|
||||
}
|
||||
.disabled(iapManager.shouldShowPaywall)
|
||||
}
|
||||
|
||||
if iapManager.shouldShowPaywall {
|
||||
// Paywall overlay - tap to show subscription store
|
||||
Color.black.opacity(0.3)
|
||||
.ignoresSafeArea()
|
||||
.onTapGesture {
|
||||
showSubscriptionStore = true
|
||||
}
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
Button {
|
||||
showSubscriptionStore = true
|
||||
} label: {
|
||||
Text(String(localized: "subscription_required_button"))
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color.pink))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
} else if iapManager.shouldShowTrialWarning {
|
||||
VStack {
|
||||
Spacer()
|
||||
if !trialWarningHidden {
|
||||
IAPWarningView(iapManager: iapManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_month_view")
|
||||
})
|
||||
.padding([.top])
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
)
|
||||
.sheet(isPresented: $selectedDetail.showFuckingSheet,
|
||||
onDismiss: didDismiss) {
|
||||
selectedDetail.fuckingWrapped
|
||||
}
|
||||
.sheet(isPresented: self.$shareImage.showFuckingSheet) {
|
||||
if let uiImage = self.shareImage.fuckingWrappedShrable {
|
||||
ShareSheet(photo: uiImage)
|
||||
}
|
||||
}
|
||||
.onPreferenceChange(ViewOffsetKey.self) { value in
|
||||
withAnimation {
|
||||
trialWarningHidden = value < 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func didDismiss() {
|
||||
selectedDetail.showFuckingSheet = false
|
||||
selectedDetail.fuckingWrapped = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension MonthView {
|
||||
private var settingsButtonView: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
Button(action: {
|
||||
showingSheet.toggle()
|
||||
}, label: {
|
||||
Image(systemName: "gear")
|
||||
.foregroundColor(Color(UIColor.darkGray))
|
||||
.font(.system(size: 20))
|
||||
}).sheet(isPresented: $showingSheet) {
|
||||
SettingsView()
|
||||
}
|
||||
.padding(.top, 60)
|
||||
.padding(.trailing)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// view that make up the list body
|
||||
extension MonthView {
|
||||
private func monthCountView(forMonth month: Int, year: Int) -> [MoodMetrics] {
|
||||
let (startDate, endDate) = Date.dateRange(monthInt: month, yearInt: year)
|
||||
let entries = PersistenceController.shared.getData(startDate: startDate, endDate: endDate, includedDays: [1,2,3,4,5,6,7])
|
||||
return Random.createTotalPerc(fromEntries: entries)
|
||||
}
|
||||
|
||||
|
||||
private func homeViewTwoSectionHeaderView(month: Int, year: Int) -> some View {
|
||||
ZStack {
|
||||
HStack {
|
||||
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
||||
.font(.body)
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
ForEach(monthCountView(forMonth: month, year: year)) {
|
||||
Text("\($0.total)")
|
||||
.font(.body)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor($0.mood.color)
|
||||
}
|
||||
}
|
||||
Text(String(customMoodTintUpdateNumber))
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
|
||||
private func shareViewImage(month: Int, year: Int, entries: [MoodEntry]) -> some View {
|
||||
ZStack {
|
||||
VStack {
|
||||
HStack {
|
||||
homeViewTwoSectionHeaderView(month: month, year: year)
|
||||
}
|
||||
Divider()
|
||||
LazyVGrid(columns: columns, spacing: 15) {
|
||||
ForEach(entries, id: \.self) { entry in
|
||||
shape.view(withText: Text(""), bgColor: entry.mood == .placeholder ? .clear : moodTint.color(forMood: entry.mood),
|
||||
textColor: .clear)
|
||||
.frame(minHeight: 25, idealHeight: 25, maxHeight: 50, alignment: .center)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.foregroundColor(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
)
|
||||
.padding(.bottom, 55)
|
||||
}
|
||||
|
||||
private func homeViewTwoMonthListView(month: Int, year: Int, entries: [MoodEntry]) -> some View {
|
||||
VStack {
|
||||
HStack {
|
||||
homeViewTwoSectionHeaderView(month: month, year: year)
|
||||
}
|
||||
Divider()
|
||||
LazyVGrid(columns: columns, spacing: 15) {
|
||||
ForEach(entries, id: \.self) { entry in
|
||||
if filteredDays.currentFilters.contains(Int(entry.weekDay)) {
|
||||
shape.view(withText: Text(""),
|
||||
bgColor: entry.mood == .placeholder ? .clear : moodTint.color(forMood: entry.mood),
|
||||
textColor: .clear)
|
||||
.frame(minHeight: 25, idealHeight: 25, maxHeight: 50, alignment: .center)
|
||||
} else {
|
||||
shape.view(withText: Text(""),
|
||||
bgColor: .clear,
|
||||
textColor: .clear)
|
||||
.frame(minHeight: 25, idealHeight: 25, maxHeight: 50, alignment: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture{
|
||||
let deailView = MonthDetailView(monthInt: month,
|
||||
yearInt: year,
|
||||
entries: entries,
|
||||
parentViewModel: viewModel)
|
||||
|
||||
selectedDetail.fuckingWrapped = deailView
|
||||
selectedDetail.showFuckingSheet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MonthView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true))
|
||||
}
|
||||
}
|
||||
200
Shared/Views/PurchaseButtonView.swift
Normal file
200
Shared/Views/PurchaseButtonView.swift
Normal file
@@ -0,0 +1,200 @@
|
||||
//
|
||||
// PurchaseButtonView.swift
|
||||
// Feels
|
||||
//
|
||||
// Subscription status and purchase view for settings.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import StoreKit
|
||||
|
||||
struct PurchaseButtonView: View {
|
||||
@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
|
||||
|
||||
@ObservedObject var iapManager: IAPManager
|
||||
|
||||
@State private var showSubscriptionStore = false
|
||||
@State private var showManageSubscriptions = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
if iapManager.isLoading {
|
||||
loadingView
|
||||
} else if iapManager.isSubscribed {
|
||||
subscribedView
|
||||
} else {
|
||||
notSubscribedView
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(theme.currentTheme.secondaryBGColor)
|
||||
.cornerRadius(10)
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
}
|
||||
.manageSubscriptionsSheet(isPresented: $showManageSubscriptions)
|
||||
}
|
||||
|
||||
// MARK: - Loading View
|
||||
|
||||
private var loadingView: some View {
|
||||
VStack(spacing: 12) {
|
||||
ProgressView()
|
||||
Text(String(localized: "purchase_view_loading"))
|
||||
.font(.body)
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
|
||||
// MARK: - Subscribed View
|
||||
|
||||
private var subscribedView: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(String(localized: "purchase_view_current_subscription"))
|
||||
.font(.title3)
|
||||
.bold()
|
||||
.foregroundColor(textColor)
|
||||
|
||||
if let product = iapManager.currentProduct {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(product.displayName)
|
||||
.font(.headline)
|
||||
.foregroundColor(textColor)
|
||||
Text(product.displayPrice)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
subscriptionStatusBadge
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// Manage subscription button
|
||||
Button {
|
||||
showManageSubscriptions = true
|
||||
} label: {
|
||||
Text(String(localized: "purchase_view_manage_subscription"))
|
||||
.font(.body)
|
||||
.foregroundColor(.blue)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
// Show other subscription options
|
||||
if iapManager.sortedProducts.count > 1 {
|
||||
Button {
|
||||
showSubscriptionStore = true
|
||||
} label: {
|
||||
Text(String(localized: "purchase_view_change_plan"))
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var subscriptionStatusBadge: some View {
|
||||
Group {
|
||||
if case .subscribed(_, let willAutoRenew) = iapManager.state {
|
||||
if willAutoRenew {
|
||||
Text(String(localized: "subscription_status_active"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.green)
|
||||
.cornerRadius(4)
|
||||
} else {
|
||||
Text(String(localized: "subscription_status_expires"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.orange)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Not Subscribed View
|
||||
|
||||
private var notSubscribedView: some View {
|
||||
VStack(spacing: 16) {
|
||||
Text(String(localized: "purchase_view_title"))
|
||||
.font(.title3)
|
||||
.bold()
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
// Trial status
|
||||
if iapManager.shouldShowTrialWarning {
|
||||
trialStatusView
|
||||
} else if iapManager.shouldShowPaywall {
|
||||
Text(String(localized: "purchase_view_trial_expired"))
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text(String(localized: "purchase_view_current_why_subscribe"))
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
// Subscribe button
|
||||
Button {
|
||||
showSubscriptionStore = true
|
||||
} label: {
|
||||
Text(String(localized: "purchase_view_subscribe_button"))
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.pink)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
// Restore purchases
|
||||
Button {
|
||||
Task {
|
||||
await iapManager.restore()
|
||||
}
|
||||
} label: {
|
||||
Text(String(localized: "purchase_view_restore"))
|
||||
.font(.body)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var trialStatusView: some View {
|
||||
HStack {
|
||||
Image(systemName: "clock")
|
||||
.foregroundColor(.orange)
|
||||
|
||||
if let expirationDate = iapManager.trialExpirationDate {
|
||||
Text(String(localized: "purchase_view_trial_expires_in"))
|
||||
.foregroundColor(textColor)
|
||||
+
|
||||
Text(" ")
|
||||
+
|
||||
Text(expirationDate, style: .relative)
|
||||
.foregroundColor(.orange)
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PurchaseButtonView(iapManager: IAPManager())
|
||||
}
|
||||
47
Shared/Views/SampleEntryView.swift
Normal file
47
Shared/Views/SampleEntryView.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// SampleEntryView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 4/5/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SampleEntryView: View {
|
||||
@State private var sampleListEntry = PersistenceController.shared.generateObjectNotInArray(forDate: Date(), withMood: Mood.great)
|
||||
@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
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: "arrow.triangle.2.circlepath.circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20, alignment: .trailing)
|
||||
.foregroundColor(Color(UIColor.systemGray))
|
||||
.onTapGesture {
|
||||
sampleListEntry = PersistenceController.shared.generateObjectNotInArray(forDate: Date(), withMood: sampleListEntry.mood.next)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}.padding()
|
||||
|
||||
VStack(alignment:.leading) {
|
||||
EntryListView(entry: sampleListEntry)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
}
|
||||
|
||||
struct SampleEntryView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SampleEntryView()
|
||||
}
|
||||
}
|
||||
627
Shared/Views/SettingsView/SettingsView.swift
Normal file
627
Shared/Views/SettingsView/SettingsView.swift
Normal file
@@ -0,0 +1,627 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
161
Shared/Views/Sharing/SharingListView.swift
Normal file
161
Shared/Views/Sharing/SharingListView.swift
Normal file
@@ -0,0 +1,161 @@
|
||||
//
|
||||
// SharingView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/6/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WrappedSharable: Hashable, Equatable {
|
||||
static func == (lhs: WrappedSharable, rhs: WrappedSharable) -> Bool {
|
||||
lhs.id == rhs.id && lhs.description == rhs.description
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
|
||||
let id = UUID()
|
||||
let preview: AnyView
|
||||
let destination: AnyView
|
||||
let description: String
|
||||
}
|
||||
|
||||
struct SharingListView: View {
|
||||
@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
|
||||
|
||||
class StupidAssObservableObject: ObservableObject {
|
||||
@Published var fuckingWrappedShrable: WrappedSharable? = nil
|
||||
@Published var showFuckingSheet = false
|
||||
}
|
||||
|
||||
@StateObject private var selectedShare = StupidAssObservableObject()
|
||||
var sharebleItems = [WrappedSharable]()
|
||||
|
||||
init() {
|
||||
self.sharebleItems = [
|
||||
WrappedSharable(preview: AnyView(
|
||||
AllMoodsTotalTemplate(isPreview: true,
|
||||
startDate: PersistenceController.shared.earliestEntry?.forDate ?? Date(),
|
||||
endDate: Date(),
|
||||
fakeData: false)
|
||||
),destination: AnyView(
|
||||
AllMoodsTotalTemplate(isPreview: false,
|
||||
startDate: PersistenceController.shared.earliestEntry?.forDate ?? Date(),
|
||||
endDate: Date(),
|
||||
fakeData: false)
|
||||
),description: AllMoodsTotalTemplate.description),
|
||||
//////////////////////////////////////////////////////////
|
||||
WrappedSharable(preview: AnyView(
|
||||
CurrentStreakTemplate(isPreview: true,
|
||||
startDate: Calendar.current.date(byAdding: .day, value: -10, to: Date())!,
|
||||
endDate: Date(),
|
||||
fakeData: false)
|
||||
), destination: AnyView(
|
||||
CurrentStreakTemplate(isPreview: false,
|
||||
startDate: Calendar.current.date(byAdding: .day, value: -10, to: Date())!,
|
||||
endDate: Date(),
|
||||
fakeData: false)
|
||||
), description: CurrentStreakTemplate.description),
|
||||
//////////////////////////////////////////////////////////
|
||||
WrappedSharable(preview: AnyView(
|
||||
LongestStreakTemplate(isPreview: true,
|
||||
startDate: PersistenceController.shared.earliestEntry?.forDate ?? Date(),
|
||||
endDate: Date(),
|
||||
fakeData: false)
|
||||
), destination: AnyView(
|
||||
LongestStreakTemplate(isPreview: false,
|
||||
startDate: PersistenceController.shared.earliestEntry?.forDate ?? Date(),
|
||||
endDate: Date(),
|
||||
fakeData: false)
|
||||
), description: LongestStreakTemplate.description),
|
||||
//////////////////////////////////////////////////////////
|
||||
WrappedSharable(preview: AnyView(
|
||||
MonthTotalTemplate(isPreview: true,
|
||||
startDate: Date().startOfMonth,
|
||||
endDate: Date().endOfMonth,
|
||||
fakeData: false)
|
||||
), destination: AnyView(
|
||||
MonthTotalTemplate(isPreview: false,
|
||||
startDate: Date().startOfMonth,
|
||||
endDate: Date().endOfMonth,
|
||||
fakeData: false)
|
||||
), description: MonthTotalTemplate.description)
|
||||
//////////////////////////////////////////////////////////
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
func didDismiss() {
|
||||
selectedShare.showFuckingSheet = false
|
||||
selectedShare.fuckingWrappedShrable = nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollView {
|
||||
ForEach(sharebleItems, id: \.self) { item in
|
||||
Button(action: {
|
||||
selectedShare.fuckingWrappedShrable = item
|
||||
selectedShare.showFuckingSheet = true
|
||||
}, label: {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
item.preview
|
||||
.frame(height: 88)
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(item.description)
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.fontWeight(.bold)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.frame(height: 44)
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
.opacity(0.9)
|
||||
}
|
||||
}
|
||||
.frame(height: 88)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.scaledToFill()
|
||||
.clipped()
|
||||
.contentShape(Path(CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 88)))
|
||||
.padding([.leading, .trailing])
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.padding([.top, .bottom])
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
)
|
||||
.sheet(isPresented: $selectedShare.showFuckingSheet,
|
||||
onDismiss: didDismiss) {
|
||||
selectedShare.fuckingWrappedShrable?.destination
|
||||
}
|
||||
}
|
||||
|
||||
func share(image: UIImage) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct SharingView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
SharingListView()
|
||||
|
||||
SharingListView()
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
227
Shared/Views/SharingTemplates/AllMoodsTotalTemplate.swift
Normal file
227
Shared/Views/SharingTemplates/AllMoodsTotalTemplate.swift
Normal file
@@ -0,0 +1,227 @@
|
||||
//
|
||||
// AllMoods.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/6/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AllMoodsTotalTemplate: View, SharingTemplate {
|
||||
static var description: String {
|
||||
"All Time Count"
|
||||
}
|
||||
|
||||
var isPreview: Bool
|
||||
var startDate: Date
|
||||
var endDate: Date
|
||||
var totalEntryCount: Int = 0
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@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 = .white
|
||||
|
||||
@StateObject private var shareImage = StupidAssShareObservableObject()
|
||||
private var entries = [MoodMetrics]()
|
||||
init(isPreview: Bool, startDate: Date, endDate: Date, fakeData: Bool) {
|
||||
self.isPreview = isPreview
|
||||
self.startDate = startDate
|
||||
self.endDate = endDate
|
||||
|
||||
var moodEntries: [MoodEntry]?
|
||||
|
||||
if fakeData {
|
||||
moodEntries = PersistenceController.shared.randomEntries(count: 10)
|
||||
} else {
|
||||
|
||||
moodEntries = PersistenceController.shared.getData(startDate:startDate,
|
||||
endDate: endDate,
|
||||
includedDays: [1,2,3,4,5,6,7])
|
||||
}
|
||||
|
||||
totalEntryCount = moodEntries?.count ?? 0
|
||||
|
||||
if let moodEntries = moodEntries {
|
||||
entries = Random.createTotalPerc(fromEntries: moodEntries)
|
||||
entries = entries.sorted(by: {
|
||||
$0.percent > $1.percent
|
||||
})
|
||||
} else {
|
||||
fatalError("no data")
|
||||
}
|
||||
}
|
||||
|
||||
var image: UIImage {
|
||||
let image = shareView.asImage(size: CGSize(width: 666, height: 1190))
|
||||
return image
|
||||
}
|
||||
|
||||
var preview: some View {
|
||||
circularViews
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.frame(height: 100)
|
||||
}
|
||||
|
||||
private var circularViews: some View {
|
||||
VStack {
|
||||
VStack {
|
||||
HStack {
|
||||
ForEach(entries.prefix(2), id: \.mood) { model in
|
||||
ZStack {
|
||||
Circle().fill(moodTint.color(forMood: model.mood))
|
||||
|
||||
Text("\(model.percent, specifier: "%.0f")%")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.scaledToFill()
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(textColor)
|
||||
.padding(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
HStack {
|
||||
ForEach(entries.suffix(3), id: \.mood) { model in
|
||||
ZStack {
|
||||
Circle().fill(moodTint.color(forMood: model.mood))
|
||||
|
||||
Text("\(model.percent, specifier: "%.0f")%")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.scaledToFill()
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(textColor)
|
||||
.padding(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
|
||||
HStack {
|
||||
ForEach(entries, id: \.mood) { model in
|
||||
ZStack {
|
||||
Circle().fill(moodTint.color(forMood: model.mood))
|
||||
|
||||
Text("\(model.total)")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.scaledToFill()
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(textColor)
|
||||
.padding(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var shareView: some View {
|
||||
VStack {
|
||||
Text(String(format: String(localized: "share_view_all_moods_total_template_title"), totalEntryCount))
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding()
|
||||
|
||||
HStack {
|
||||
ForEach(Mood.allValues, id: \.self) { mood in
|
||||
Circle()
|
||||
.frame(minWidth: 10, idealWidth: 70, maxWidth: 100, minHeight: 10, idealHeight: 70, maxHeight: 100)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.overlay(
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding(9)
|
||||
.foregroundColor(textColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
circularViews
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.background(Color(UIColor.secondarySystemBackground))
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
}
|
||||
|
||||
var mainView: some View {
|
||||
VStack {
|
||||
shareView
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Button(action: {
|
||||
let _image = self.image
|
||||
self.shareImage.showFuckingSheet = true
|
||||
self.shareImage.fuckingWrappedShrable = _image
|
||||
}, label: {
|
||||
Text("Share")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color.white)
|
||||
.padding(.top, 20)
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.background(
|
||||
Color.green
|
||||
)
|
||||
.padding(.trailing, -5)
|
||||
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
Text("Exit")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color.white)
|
||||
.padding(.top, 20)
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.background(
|
||||
Color.red
|
||||
)
|
||||
.padding(.leading, -5)
|
||||
}
|
||||
.padding([.leading, .trailing], -20)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if isPreview {
|
||||
shareView
|
||||
.scaleEffect(2)
|
||||
} else {
|
||||
mainView
|
||||
.sheet(isPresented: self.$shareImage.showFuckingSheet) {
|
||||
if let uiImage = self.shareImage.fuckingWrappedShrable {
|
||||
ShareSheet(photo: uiImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AllMoodsSharingTemplate_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
AllMoodsTotalTemplate(isPreview: true, startDate: Date(), endDate: Date(), fakeData: true)
|
||||
|
||||
AllMoodsTotalTemplate(isPreview: false, startDate: Date(), endDate: Date(), fakeData: true)
|
||||
|
||||
AllMoodsTotalTemplate(isPreview: false, startDate: Date(), endDate: Date(), fakeData: true).shareView
|
||||
}
|
||||
}
|
||||
}
|
||||
170
Shared/Views/SharingTemplates/CurrentStreakTemplate.swift
Normal file
170
Shared/Views/SharingTemplates/CurrentStreakTemplate.swift
Normal file
@@ -0,0 +1,170 @@
|
||||
//
|
||||
// CurrentStreakTemplate.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/9/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CurrentStreakTemplate: View, SharingTemplate {
|
||||
static var description: String {
|
||||
"Last 10 Days"
|
||||
}
|
||||
|
||||
var isPreview: Bool
|
||||
var startDate: Date
|
||||
var endDate: Date
|
||||
let moodEntries: [MoodEntry]
|
||||
|
||||
@State var showSharingTemplate = false
|
||||
@StateObject private var shareImage = StupidAssShareObservableObject()
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@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
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center)
|
||||
]
|
||||
|
||||
init(isPreview: Bool, startDate: Date, endDate: Date, fakeData: Bool) {
|
||||
self.isPreview = isPreview
|
||||
self.startDate = startDate
|
||||
self.endDate = endDate
|
||||
|
||||
var _moodEntries: [MoodEntry]?
|
||||
|
||||
if fakeData {
|
||||
_moodEntries = PersistenceController.shared.randomEntries(count: 10)
|
||||
} else {
|
||||
|
||||
_moodEntries = PersistenceController.shared.getData(startDate:startDate,
|
||||
endDate: endDate,
|
||||
includedDays: [1,2,3,4,5,6,7])
|
||||
}
|
||||
|
||||
self.moodEntries = _moodEntries ?? [MoodEntry]()
|
||||
}
|
||||
|
||||
var image: UIImage {
|
||||
let image = shareView.asImage(size: CGSize(width: 666, height: 1190))
|
||||
return image
|
||||
}
|
||||
|
||||
var preview: some View {
|
||||
HStack {
|
||||
VStack {
|
||||
LazyVGrid(columns: columns, spacing: 0) {
|
||||
ForEach(moodEntries) { entry in
|
||||
entry.mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(moodTint.color(forMood: entry.mood))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 100)
|
||||
}
|
||||
|
||||
var shareView: some View {
|
||||
VStack {
|
||||
Text(String(format: String(localized: "share_view_current_streak_template_title")))
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.top)
|
||||
|
||||
HStack {
|
||||
VStack {
|
||||
LazyVGrid(columns: columns, spacing: 10) {
|
||||
ForEach(moodEntries) { entry in
|
||||
entry.mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(moodTint.color(forMood: entry.mood))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(Color(UIColor.secondarySystemBackground))
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
}
|
||||
|
||||
var mainView: some View {
|
||||
VStack {
|
||||
shareView
|
||||
|
||||
Spacer()
|
||||
HStack(alignment: .center) {
|
||||
HStack(alignment: .center) {
|
||||
Button(action: {
|
||||
let _image = self.image
|
||||
self.shareImage.showFuckingSheet = true
|
||||
self.shareImage.fuckingWrappedShrable = _image
|
||||
}, label: {
|
||||
Text("Share")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color.white)
|
||||
.padding(.top, 20)
|
||||
})
|
||||
.sheet(isPresented: self.$shareImage.showFuckingSheet) {
|
||||
if let uiImage = self.shareImage.fuckingWrappedShrable {
|
||||
ShareSheet(photo: uiImage)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.background(
|
||||
Color.green
|
||||
)
|
||||
.padding(.trailing, -5)
|
||||
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
Text("Exit")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color.white)
|
||||
.padding(.top, 20)
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.background(
|
||||
Color.red
|
||||
)
|
||||
.padding(.leading, -5)
|
||||
}
|
||||
.padding([.leading, .trailing], -20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if isPreview {
|
||||
shareView
|
||||
.scaleEffect(2)
|
||||
} else {
|
||||
mainView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CurrentStreakTemplate_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CurrentStreakTemplate(isPreview: true, startDate: Date(), endDate: Date(), fakeData: true)
|
||||
|
||||
CurrentStreakTemplate(isPreview: false, startDate: Date(), endDate: Date(), fakeData: true)
|
||||
|
||||
CurrentStreakTemplate(isPreview: false, startDate: Date(), endDate: Date(), fakeData: true).shareView
|
||||
}
|
||||
}
|
||||
248
Shared/Views/SharingTemplates/LongestStreakTemplate.swift
Normal file
248
Shared/Views/SharingTemplates/LongestStreakTemplate.swift
Normal file
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// AllMoods.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/6/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Algorithms
|
||||
|
||||
struct LongestStreakTemplate: View, SharingTemplate {
|
||||
static var description: String {
|
||||
"Longest Streak"
|
||||
}
|
||||
|
||||
private let itemFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .short
|
||||
return formatter
|
||||
}()
|
||||
|
||||
var isPreview: Bool
|
||||
var startDate: Date
|
||||
var endDate: Date
|
||||
var fakeData: Bool
|
||||
|
||||
@State var moodEntries = [MoodEntry]()
|
||||
@State var selectedMood: Mood = .great
|
||||
|
||||
@State var showSharingTemplate = false
|
||||
@StateObject private var shareImage = StupidAssShareObservableObject()
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@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 = .white
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center)
|
||||
]
|
||||
|
||||
init(isPreview: Bool, startDate: Date, endDate: Date, fakeData: Bool) {
|
||||
self.isPreview = isPreview
|
||||
self.startDate = startDate
|
||||
self.endDate = endDate
|
||||
self.fakeData = fakeData
|
||||
|
||||
configureData(fakeData: self.fakeData, mood: self.selectedMood)
|
||||
}
|
||||
|
||||
var image: UIImage {
|
||||
let image = shareView.asImage(size: CGSize(width: 650, height: 400))
|
||||
return image
|
||||
}
|
||||
|
||||
private func configureData(fakeData: Bool, mood: Mood) {
|
||||
var _moodEntries: [MoodEntry]?
|
||||
|
||||
if fakeData {
|
||||
_moodEntries = PersistenceController.shared.randomEntries(count: 10)
|
||||
} else {
|
||||
_moodEntries = PersistenceController.shared.getData(startDate:startDate,
|
||||
endDate: endDate,
|
||||
includedDays: [1,2,3,4,5,6,7])
|
||||
}
|
||||
let data = _moodEntries ?? [MoodEntry]()
|
||||
|
||||
var splitArrays = createSubArrays(fromMoodEntries: data, splitOn: mood)
|
||||
splitArrays = splitArrays.sorted(by: {
|
||||
$0.count > $1.count
|
||||
} )
|
||||
self.moodEntries = splitArrays.first ?? [MoodEntry]()
|
||||
}
|
||||
|
||||
private func createSubArrays(fromMoodEntries: [MoodEntry], splitOn: Mood) -> [[MoodEntry]] {
|
||||
var splitArrays = [[MoodEntry]]()
|
||||
var currentSplit = [MoodEntry]()
|
||||
for entry in fromMoodEntries {
|
||||
if entry.mood == splitOn {
|
||||
currentSplit.append(entry)
|
||||
} else {
|
||||
splitArrays.append(currentSplit)
|
||||
currentSplit.removeAll()
|
||||
}
|
||||
}
|
||||
// append the last grouping
|
||||
splitArrays.append(currentSplit)
|
||||
return splitArrays
|
||||
}
|
||||
|
||||
var preview: some View {
|
||||
HStack {
|
||||
VStack {
|
||||
LazyVGrid(columns: columns, spacing: 0) {
|
||||
ForEach(moodEntries) { entry in
|
||||
entry.mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(moodTint.color(forMood: entry.mood))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 88)
|
||||
.clipped()
|
||||
.onAppear(perform: {
|
||||
self.configureData(fakeData: self.fakeData, mood: self.selectedMood)
|
||||
})
|
||||
}
|
||||
|
||||
var shareView: some View {
|
||||
VStack {
|
||||
Text(String(format: String(localized: "share_view_longest_streak_template_title"), self.selectedMood.strValue))
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding()
|
||||
|
||||
selectedMood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 75, height: 75)
|
||||
.foregroundColor(moodTint.color(forMood: selectedMood))
|
||||
|
||||
VStack {
|
||||
Text(self.moodEntries.first?.forDate ?? Date(), formatter: itemFormatter)
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.top, 1)
|
||||
|
||||
Text("-")
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.top, 1)
|
||||
|
||||
Text(self.moodEntries.last?.forDate ?? Date(), formatter: itemFormatter)
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.top, 1)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.onAppear(perform: {
|
||||
self.configureData(fakeData: self.fakeData, mood: self.selectedMood)
|
||||
})
|
||||
.background(Color(UIColor.secondarySystemBackground))
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
}
|
||||
|
||||
var mainView: some View {
|
||||
VStack {
|
||||
shareView
|
||||
|
||||
Spacer()
|
||||
Menu(content: {
|
||||
ForEach(Mood.allValues) { mood in
|
||||
Button(mood.strValue, action: {
|
||||
selectedMood = mood
|
||||
configureData(fakeData: self.fakeData, mood: self.selectedMood)
|
||||
})
|
||||
}
|
||||
}, label: {
|
||||
Text("Pick Mood")
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.padding()
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
.padding([.leading, .trailing], -5)
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Button(action: {
|
||||
let _image = self.image
|
||||
self.shareImage.showFuckingSheet = true
|
||||
self.shareImage.fuckingWrappedShrable = _image
|
||||
}, label: {
|
||||
Text("Share")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color.white)
|
||||
.padding(.top, 20)
|
||||
})
|
||||
.sheet(isPresented: self.$shareImage.showFuckingSheet) {
|
||||
if let uiImage = self.shareImage.fuckingWrappedShrable {
|
||||
ShareSheet(photo: uiImage)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.background(
|
||||
Color.green
|
||||
)
|
||||
.padding(.trailing, -5)
|
||||
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
Text("Exit")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color.white)
|
||||
.padding(.top, 20)
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.background(
|
||||
Color.red
|
||||
)
|
||||
.padding(.leading, -5)
|
||||
}
|
||||
.padding([.leading, .trailing], -20)
|
||||
}.sheet(isPresented: $showSharingTemplate) {
|
||||
ActivityViewController(activityItems: [self.image])
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if isPreview {
|
||||
shareView
|
||||
.scaleEffect(2)
|
||||
} else {
|
||||
mainView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CurrentStreakSharingTemplate_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
LongestStreakTemplate(isPreview: true, startDate: Date(), endDate: Date(), fakeData: true)
|
||||
|
||||
LongestStreakTemplate(isPreview: false, startDate: Date(), endDate: Date(), fakeData: true)
|
||||
|
||||
LongestStreakTemplate(isPreview: false, startDate: Date(), endDate: Date(), fakeData: true).shareView
|
||||
}
|
||||
}
|
||||
}
|
||||
214
Shared/Views/SharingTemplates/MonthTotalTemplate.swift
Normal file
214
Shared/Views/SharingTemplates/MonthTotalTemplate.swift
Normal file
@@ -0,0 +1,214 @@
|
||||
//
|
||||
// MonthTotalTemplate.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/9/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MonthTotalTemplate: View, SharingTemplate {
|
||||
static var description: String {
|
||||
"This Month"
|
||||
}
|
||||
|
||||
var isPreview: Bool
|
||||
var startDate: Date
|
||||
var endDate: Date
|
||||
var totalEntryCount: Int = 0
|
||||
|
||||
private var month = Calendar.current.dateComponents([.month], from: Date()).month!
|
||||
|
||||
@State var showSharingTemplate = false
|
||||
@StateObject private var shareImage = StupidAssShareObservableObject()
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@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 = .white
|
||||
|
||||
private var moodMetrics = [MoodMetrics]()
|
||||
private var moodEntries = [MoodEntry]()
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center)
|
||||
]
|
||||
|
||||
init(isPreview: Bool, startDate: Date, endDate: Date, fakeData: Bool) {
|
||||
self.isPreview = isPreview
|
||||
self.startDate = startDate
|
||||
self.endDate = endDate
|
||||
|
||||
var _moodEntries: [MoodEntry]?
|
||||
|
||||
if fakeData {
|
||||
_moodEntries = PersistenceController.shared.randomEntries(count: 10)
|
||||
} else {
|
||||
|
||||
_moodEntries = PersistenceController.shared.getData(startDate: startDate,
|
||||
endDate: endDate,
|
||||
includedDays: [1,2,3,4,5,6,7])
|
||||
|
||||
// _moodEntries = PersistenceController.shared.getData(startDate:Calendar.current.date(byAdding: .day, value: -33, to: Date())!,
|
||||
// endDate: Date(),
|
||||
// includedDays: [1,2,3,4,5,6,7])
|
||||
|
||||
}
|
||||
|
||||
moodEntries = _moodEntries ?? [MoodEntry]()
|
||||
|
||||
totalEntryCount = moodEntries.count
|
||||
moodMetrics = Random.createTotalPerc(fromEntries: moodEntries)
|
||||
moodMetrics = moodMetrics.sorted(by: {
|
||||
$0.mood.rawValue > $1.mood.rawValue
|
||||
})
|
||||
}
|
||||
|
||||
var image: UIImage {
|
||||
let image = shareView.asImage(size: CGSize(width: 666, height: 1190))
|
||||
return image
|
||||
}
|
||||
|
||||
var preview: some View {
|
||||
circularViews
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.frame(height: 100)
|
||||
}
|
||||
|
||||
private var circularViews: some View {
|
||||
HStack {
|
||||
ForEach(moodMetrics, id: \.mood) { model in
|
||||
ZStack {
|
||||
Circle().fill(moodTint.color(forMood: model.mood))
|
||||
|
||||
Text("\(model.percent, specifier: "%.0f")%")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(textColor)
|
||||
.padding(5)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.frame(height: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var shareView: some View {
|
||||
VStack {
|
||||
Text(String(format: String(localized: "share_view_month_moods_total_template_title"), Random.monthName(fromMonthInt: month), moodEntries.count))
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding()
|
||||
|
||||
HStack {
|
||||
ForEach(Mood.allValues, id: \.self) { mood in
|
||||
Circle()
|
||||
.frame(minWidth: 10, idealWidth: 70, maxWidth: 100, minHeight: 10, idealHeight: 70, maxHeight: 100)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.overlay(
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(textColor)
|
||||
.padding(9)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
VStack {
|
||||
LazyVGrid(columns: columns, spacing: 10) {
|
||||
ForEach(moodEntries) { entry in
|
||||
entry.mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(moodTint.color(forMood: entry.mood))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
circularViews
|
||||
.padding()
|
||||
}
|
||||
.background(Color(UIColor.secondarySystemBackground))
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
}
|
||||
|
||||
var mainView: some View {
|
||||
VStack {
|
||||
shareView
|
||||
|
||||
Spacer()
|
||||
HStack(alignment: .center) {
|
||||
Button(action: {
|
||||
let _image = self.image
|
||||
self.shareImage.showFuckingSheet = true
|
||||
self.shareImage.fuckingWrappedShrable = _image
|
||||
}, label: {
|
||||
Text("Share")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color.white)
|
||||
.padding(.top, 20)
|
||||
})
|
||||
.sheet(isPresented: self.$shareImage.showFuckingSheet) {
|
||||
if let uiImage = self.shareImage.fuckingWrappedShrable {
|
||||
ShareSheet(photo: uiImage)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.background(
|
||||
Color.green
|
||||
)
|
||||
.padding(.trailing, -5)
|
||||
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
Text("Exit")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color.white)
|
||||
.padding(.top, 20)
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.background(
|
||||
Color.red
|
||||
)
|
||||
.padding(.leading, -5)
|
||||
}
|
||||
.padding([.leading, .trailing], -20)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if isPreview {
|
||||
shareView
|
||||
.scaleEffect(2)
|
||||
} else {
|
||||
mainView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MonthTotalTemplate_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MonthTotalTemplate(isPreview: true, startDate: Date().startOfMonth, endDate: Date().endOfMonth, fakeData: true)
|
||||
|
||||
MonthTotalTemplate(isPreview: false, startDate: Date().startOfMonth, endDate: Date().endOfMonth, fakeData: true)
|
||||
|
||||
MonthTotalTemplate(isPreview: false, startDate: Date().startOfMonth, endDate: Date().endOfMonth, fakeData: true).shareView
|
||||
}
|
||||
}
|
||||
48
Shared/Views/SharingTemplates/WeekTotalTemplate.swift
Normal file
48
Shared/Views/SharingTemplates/WeekTotalTemplate.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// WeekTotalTemplate.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 2/9/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WeekTotalTemplate: View, SharingTemplate {
|
||||
static var description: String {
|
||||
"WeekTotalTemplate"
|
||||
}
|
||||
|
||||
var image: UIImage {
|
||||
return UIImage(systemName: "square.and.arrow.up")!
|
||||
}
|
||||
|
||||
var isPreview: Bool
|
||||
|
||||
var startDate: Date
|
||||
var endDate: Date
|
||||
|
||||
var preview: some View {
|
||||
Rectangle()
|
||||
.frame(width: 150, height: 50, alignment: .leading)
|
||||
}
|
||||
|
||||
var mainView: some View {
|
||||
Text("WeekTotalTemplate body")
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if isPreview {
|
||||
preview
|
||||
} else {
|
||||
mainView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WeekTotalTemplate_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
WeekTotalTemplate(isPreview: true, startDate: Date(), endDate: Date())
|
||||
|
||||
WeekTotalTemplate(isPreview: false, startDate: Date(), endDate: Date())
|
||||
}
|
||||
}
|
||||
101
Shared/Views/SmallRollUpHeaderView.swift
Normal file
101
Shared/Views/SmallRollUpHeaderView.swift
Normal file
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// SmallHeaderView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 1/28/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SmallRollUpHeaderView: View {
|
||||
@Binding var viewType: MainSwitchableViewType
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.shape.rawValue, store: GroupUserDefaults.groupDefaults) private var shape: BGShape = .circle
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
let entries: [MoodEntry]
|
||||
private var moodMetrics = [MoodMetrics]()
|
||||
|
||||
init(entries: [MoodEntry], viewType: Binding<MainSwitchableViewType>) {
|
||||
self.entries = entries
|
||||
self._viewType = viewType
|
||||
|
||||
moodMetrics = Random.createTotalPerc(fromEntries: entries)
|
||||
}
|
||||
|
||||
private func textView(forModel model: MoodMetrics) -> Text {
|
||||
switch viewType {
|
||||
case .total:
|
||||
return Text(String(model.total))
|
||||
case .percentageShape:
|
||||
return Text("\(model.percent, specifier: "%.0f")%")
|
||||
case .percentage:
|
||||
return Text("\(model.percent, specifier: "%.0f")%")
|
||||
}
|
||||
}
|
||||
|
||||
private var onlyTextView: some View {
|
||||
HStack() {
|
||||
ForEach(moodMetrics, id: \.id) { model in
|
||||
textView(forModel: model)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(moodTint.color(forMood: model.mood))
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding([.top, .bottom])
|
||||
}
|
||||
|
||||
private var shapeView: some View {
|
||||
HStack {
|
||||
ForEach(moodMetrics, id: \.id) { model in
|
||||
HStack {
|
||||
shape.view(withText: textView(forModel: model),
|
||||
bgColor: moodTint.color(forMood: model.mood),
|
||||
textColor: textColor)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.top, .bottom])
|
||||
}
|
||||
|
||||
private var viewOnViewtype : some View {
|
||||
HStack {
|
||||
switch viewType {
|
||||
case .total, .percentageShape:
|
||||
shapeView
|
||||
case .percentage:
|
||||
onlyTextView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
viewOnViewtype
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
struct SmallHeaderView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
SmallRollUpHeaderView(entries: PersistenceController.shared.randomEntries(count: 10),
|
||||
viewType: .constant(.total))
|
||||
|
||||
SmallRollUpHeaderView(entries: PersistenceController.shared.randomEntries(count: 10),
|
||||
viewType: .constant(.percentageShape))
|
||||
.background(.gray)
|
||||
|
||||
SmallRollUpHeaderView(entries: PersistenceController.shared.randomEntries(count: 10),
|
||||
viewType: .constant(.percentage))
|
||||
|
||||
.background(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
117
Shared/Views/SwitchableView.swift
Normal file
117
Shared/Views/SwitchableView.swift
Normal file
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// SwitchableView.swift
|
||||
// Feels (iOS)
|
||||
//
|
||||
// Created by Trey Tartt on 1/30/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum MainSwitchableViewType: Int, CaseIterable {
|
||||
case total
|
||||
case percentageShape
|
||||
case percentage
|
||||
|
||||
func next() -> MainSwitchableViewType {
|
||||
let currentValue = self.rawValue
|
||||
var next = currentValue + 1
|
||||
if next == MainSwitchableViewType.allCases.count {
|
||||
next = 0
|
||||
}
|
||||
return MainSwitchableViewType(rawValue: next) ?? .total
|
||||
}
|
||||
}
|
||||
|
||||
struct SwitchableView: View {
|
||||
@Binding var viewType: MainSwitchableViewType
|
||||
var headerTypeChanged: ((MainSwitchableViewType) -> Void)
|
||||
let daysBack: Int
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = .white
|
||||
|
||||
// store a value that gets changed when user updates custom colors to update the view since the moodTint doesn't change
|
||||
@AppStorage(UserDefaultsStore.Keys.customMoodTintUpdateNumber.rawValue, store: GroupUserDefaults.groupDefaults) private var customMoodTintUpdateNumber: Int = 0
|
||||
|
||||
init(daysBack: Int, viewType: Binding<MainSwitchableViewType>, headerTypeChanged: @escaping ((MainSwitchableViewType) -> Void)) {
|
||||
self.daysBack = daysBack
|
||||
self.headerTypeChanged = headerTypeChanged
|
||||
self._viewType = viewType
|
||||
}
|
||||
|
||||
private var mainViews: some View {
|
||||
switch viewType {
|
||||
case .total:
|
||||
return AnyView(
|
||||
HeaderStatsView(fakeData: false, backDays: daysBack, moodTint: [
|
||||
moodTint.color(forMood: .great),
|
||||
moodTint.color(forMood: .good),
|
||||
moodTint.color(forMood: .average),
|
||||
moodTint.color(forMood: .bad),
|
||||
moodTint.color(forMood: .horrible)
|
||||
], textColor: textColor))
|
||||
.allowsHitTesting(false)
|
||||
case .percentageShape:
|
||||
return AnyView(
|
||||
HeaderPercView(fakeData: false, backDays: daysBack, type: .shape))
|
||||
.allowsHitTesting(false)
|
||||
case .percentage:
|
||||
return AnyView(
|
||||
HeaderPercView(fakeData: false, backDays: daysBack, type: .text))
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
Text(String(customMoodTintUpdateNumber))
|
||||
.hidden()
|
||||
|
||||
mainViews
|
||||
.padding([.top, .bottom])
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: "arrow.triangle.2.circlepath.circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20, alignment: .trailing)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.trailing, 8)
|
||||
.padding(.top, 12)
|
||||
.allowsHitTesting(false)
|
||||
.foregroundColor(Color(UIColor.systemGray))
|
||||
}
|
||||
.padding(.top, -7)
|
||||
|
||||
Text(String(format: String(localized: "content_view_header_title"), daysBack))
|
||||
.font(.body)
|
||||
.foregroundColor(Color(UIColor.systemGray))
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.top, -12)
|
||||
}
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.padding(.bottom, 30)
|
||||
.onTapGesture {
|
||||
viewType = viewType.next()
|
||||
self.headerTypeChanged(viewType)
|
||||
EventLogger.log(event: "switchable_view_header_changed",
|
||||
withData: ["view_type_id": viewType.rawValue, "days_back": daysBack])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SwitchableView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SwitchableView(daysBack: 30, viewType: .constant(.total), headerTypeChanged: { _ in
|
||||
})
|
||||
}
|
||||
}
|
||||
230
Shared/Views/YearView/YearView.swift
Normal file
230
Shared/Views/YearView/YearView.swift
Normal file
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// FilterView.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/12/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct YearView: View {
|
||||
let months = [(0, "J"), (1, "F"), (2,"M"), (3,"A"), (4,"M"), (5, "J"), (6,"J"), (7,"A"), (8,"S"), (9,"O"), (10, "N"), (11,"D")]
|
||||
|
||||
@State private var toggle = true
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \MoodEntry.forDate, ascending: false)],
|
||||
animation: .spring())
|
||||
private var items: FetchedResults<MoodEntry>
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
@EnvironmentObject var iapManager: IAPManager
|
||||
@StateObject public var viewModel: YearViewModel
|
||||
@StateObject private var filteredDays = DaysFilterClass.shared
|
||||
@State private var trialWarningHidden = false
|
||||
@State private var showSubscriptionStore = false
|
||||
//[
|
||||
// 2001: [0: [], 1: [], 2: []],
|
||||
// 2002: [0: [], 1: [], 2: []]
|
||||
// ]
|
||||
let columns = [
|
||||
GridItem(.flexible(minimum: 5, maximum: 50)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 50)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 50)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 50)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 50)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 50)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 50)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 50)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 50)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 50)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 50)),
|
||||
GridItem(.flexible(minimum: 5, maximum: 50)),
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if self.viewModel.data.keys.isEmpty {
|
||||
EmptyHomeView(showVote: false, viewModel: nil)
|
||||
.padding()
|
||||
} else {
|
||||
ScrollView {
|
||||
gridView
|
||||
.background(
|
||||
GeometryReader { proxy in
|
||||
let offset = proxy.frame(in: .named("scroll")).minY
|
||||
Color.clear.preference(key: ViewOffsetKey.self, value: offset)
|
||||
}
|
||||
)
|
||||
}
|
||||
.disabled(iapManager.shouldShowPaywall)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
|
||||
if iapManager.shouldShowPaywall {
|
||||
// Paywall overlay - tap to show subscription store
|
||||
Color.black.opacity(0.3)
|
||||
.ignoresSafeArea()
|
||||
.onTapGesture {
|
||||
showSubscriptionStore = true
|
||||
}
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
Button {
|
||||
showSubscriptionStore = true
|
||||
} label: {
|
||||
Text(String(localized: "subscription_required_button"))
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color.pink))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
} else if iapManager.shouldShowTrialWarning {
|
||||
VStack {
|
||||
Spacer()
|
||||
if !trialWarningHidden {
|
||||
IAPWarningView(iapManager: iapManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
}
|
||||
.onAppear(perform: {
|
||||
self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date())
|
||||
})
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
)
|
||||
.onPreferenceChange(ViewOffsetKey.self) { value in
|
||||
withAnimation {
|
||||
trialWarningHidden = value < 0
|
||||
}
|
||||
}
|
||||
.padding([.top])
|
||||
}
|
||||
|
||||
private var monthsHeader: some View {
|
||||
LazyVGrid(columns: columns, spacing: 0) {
|
||||
ForEach(months, id: \.self.0) { item in
|
||||
Text(item.1)
|
||||
.textCase(.uppercase)
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
}.padding([.leading, .trailing, .top])
|
||||
}
|
||||
|
||||
private var gridView: some View {
|
||||
VStack {
|
||||
VStack {
|
||||
ForEach(Array(self.viewModel.data.keys.sorted(by: >)), id: \.self) { yearKey in
|
||||
let yearData = self.viewModel.data[yearKey]!
|
||||
|
||||
let firstOfYear = Calendar.current.date(from: DateComponents(year: Int(yearKey), month: 1, day: 1))!
|
||||
let lastOfYear = Calendar.current.date(from: DateComponents(year: Int(yearKey)+1, month: 1, day: 1))!
|
||||
|
||||
let yearEntries = PersistenceController.shared.getData(startDate: firstOfYear,
|
||||
endDate: lastOfYear,
|
||||
includedDays: filteredDays.currentFilters)
|
||||
Text(String(yearKey))
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
ForEach(Mood.allValues, id: \.self) { mood in
|
||||
VStack {
|
||||
Text(String(Stats.getCountFor(moodType: mood,
|
||||
inData: yearEntries)))
|
||||
.font(.title)
|
||||
.foregroundColor(textColor)
|
||||
Text(mood.strValue)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.cornerRadius(10)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 90, maxHeight: 90)
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
|
||||
Text(String(localized: "filter_view_total") + ": \(yearEntries.count)")
|
||||
.font(.title2)
|
||||
.foregroundColor(textColor)
|
||||
monthsHeader
|
||||
.cornerRadius(10)
|
||||
yearGridView(yearData: yearData, columns: columns)
|
||||
.background(
|
||||
theme.currentTheme.secondaryBGColor
|
||||
)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.padding([.top, .leading, .trailing])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct yearGridView: View {
|
||||
let yearData: [Int: [DayChartView]]
|
||||
let columns: [GridItem]
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
LazyVGrid(columns: columns, spacing: 0) {
|
||||
ForEach(Array(yearData.keys.sorted(by: <)), id: \.self) { monthKey in
|
||||
let monthData = yearData[monthKey]!
|
||||
VStack {
|
||||
monthGridView(monthData: monthData)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.leading, .trailing, .top, .bottom])
|
||||
}
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}
|
||||
|
||||
private struct monthGridView: View {
|
||||
@StateObject private var filteredDays = DaysFilterClass.shared
|
||||
|
||||
let monthData: [DayChartView]
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ForEach(monthData, id: \.self) { view in
|
||||
if filteredDays.currentFilters.contains(view.weekDay) {
|
||||
view
|
||||
} else {
|
||||
view.filteredDaysView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct YearView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
YearView(viewModel: YearViewModel())
|
||||
|
||||
YearView(viewModel: YearViewModel())
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Shared/Views/YearView/YearViewModel.swift
Normal file
50
Shared/Views/YearView/YearViewModel.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// FilterViewModel.swift
|
||||
// Feels
|
||||
//
|
||||
// Created by Trey Tartt on 1/17/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class YearViewModel: ObservableObject {
|
||||
@Published public var entryStartDate: Date = Date()
|
||||
@Published public var entryEndDate: Date = Date()
|
||||
@Published var selectedDays = [Int]()
|
||||
|
||||
// year, month, items
|
||||
@Published public private(set) var data = [Int: [Int: [DayChartView]]]()
|
||||
@Published public private(set) var numberOfRatings: Int = 0
|
||||
public private(set) var uncategorizedData = [MoodEntry]() {
|
||||
didSet {
|
||||
self.numberOfRatings = uncategorizedData.count
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
updateData()
|
||||
}
|
||||
|
||||
private func updateData() {
|
||||
let filteredEntries = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0),
|
||||
endDate: Date(),
|
||||
includedDays: selectedDays)
|
||||
|
||||
if let fuckingDAte = filteredEntries.sorted(by: { $0.forDate! < $1.forDate! }).first?.forDate {
|
||||
self.entryStartDate = fuckingDAte
|
||||
}
|
||||
self.entryEndDate = Date()
|
||||
}
|
||||
|
||||
private let chartViewBuilder = DayChartViewChartBuilder()
|
||||
|
||||
public func filterEntries(startDate: Date, endDate: Date) {
|
||||
let filteredEntries = PersistenceController.shared.getData(startDate: startDate,
|
||||
endDate: endDate,
|
||||
includedDays: selectedDays)
|
||||
data.removeAll()
|
||||
let filledOutData = chartViewBuilder.buildGridData(withData: filteredEntries)
|
||||
data = filledOutData
|
||||
uncategorizedData = filteredEntries
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user