This commit is contained in:
Trey t
2025-12-09 23:36:57 -06:00
parent 316513e516
commit 3a10b4b8d6
1586 changed files with 0 additions and 7710 deletions

View 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>) {}
}

View 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)
}
}
}

View 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)
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}
}

View 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()
}
}

View File

@@ -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()
}
}

View 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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View 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()
}
}

View File

@@ -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()
}
}

View 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()
}
}

View 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()
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}
}

View 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)
}
}

View 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())
}

View 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)
}
}
}

View 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)
}
}

View 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)
}
}

View 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())
}

View 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
})
}
}

View 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())
}
}

View 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))
}
}

View 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))
}
}

View 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())
}

View 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()
}
}

View 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)
}
}

View 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)
}
}
}

View 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
}
}
}

View 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
}
}

View 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
}
}
}

View 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
}
}

View 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())
}
}

View 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)
}
}
}

View 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
})
}
}

View 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)
}
}
}

View 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
}
}