inisights use selected theme
This commit is contained in:
@@ -6,37 +6,88 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import StoreKit
|
||||||
|
|
||||||
struct CustomizeView: View {
|
struct CustomizeView: View {
|
||||||
@State private var showSettings = false
|
@State private var showSettings = false
|
||||||
|
@State private var showSubscriptionStore = false
|
||||||
|
@EnvironmentObject var iapManager: IAPManager
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
@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 {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
settingsButtonView
|
VStack(spacing: 24) {
|
||||||
VStack {
|
// Header
|
||||||
Group {
|
headerView
|
||||||
CustomWigetView()
|
|
||||||
IconPickerView()
|
// Subscription Banner
|
||||||
ThemePickerView()
|
SubscriptionBannerView(showSubscriptionStore: $showSubscriptionStore)
|
||||||
Divider()
|
.environmentObject(iapManager)
|
||||||
VotingLayoutPickerView()
|
|
||||||
Divider()
|
// Preview showing current style
|
||||||
SampleEntryView()
|
SampleEntryView()
|
||||||
ImagePackPickerView()
|
|
||||||
|
// APPEARANCE
|
||||||
|
SettingsSection(title: "Appearance") {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
// Theme
|
||||||
|
SettingsRow(title: "Theme") {
|
||||||
|
ThemePickerCompact()
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// Text Color
|
||||||
|
SettingsRow(title: "Text Color") {
|
||||||
|
TextColorPickerCompact()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Group {
|
|
||||||
TintPickerView()
|
// MOOD STYLE
|
||||||
TextColorPickerView()
|
SettingsSection(title: "Mood Style") {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
// Icon Style
|
||||||
|
SettingsRow(title: "Icons") {
|
||||||
|
ImagePackPickerCompact()
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// Mood Colors
|
||||||
|
SettingsRow(title: "Colors") {
|
||||||
|
TintPickerCompact()
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// Voting Layout
|
||||||
|
SettingsRow(title: "Voting Layout") {
|
||||||
|
VotingLayoutPickerCompact()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WIDGETS
|
||||||
|
SettingsSection(title: "Widgets") {
|
||||||
|
CustomWidgetSection()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTIFICATIONS
|
||||||
|
SettingsSection(title: "Notifications") {
|
||||||
|
PersonalityPackPickerCompact()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FILTERS
|
||||||
|
SettingsSection(title: "Day Filter") {
|
||||||
|
DayFilterPickerCompact()
|
||||||
}
|
}
|
||||||
Divider()
|
|
||||||
DayFilterPickerView()
|
|
||||||
Divider()
|
|
||||||
ShapePickerView()
|
|
||||||
Divider()
|
|
||||||
PersonalityPackPickerView()
|
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.bottom, 32)
|
||||||
}
|
}
|
||||||
.onAppear(perform: {
|
.onAppear(perform: {
|
||||||
EventLogger.log(event: "show_customize_view")
|
EventLogger.log(event: "show_customize_view")
|
||||||
@@ -44,23 +95,656 @@ struct CustomizeView: View {
|
|||||||
.sheet(isPresented: $showSettings) {
|
.sheet(isPresented: $showSettings) {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
}
|
}
|
||||||
.padding()
|
.sheet(isPresented: $showSubscriptionStore) {
|
||||||
|
FeelsSubscriptionStoreView()
|
||||||
|
}
|
||||||
.background(
|
.background(
|
||||||
theme.currentTheme.bg
|
theme.currentTheme.bg
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var settingsButtonView: some View {
|
private var headerView: some View {
|
||||||
HStack {
|
HStack {
|
||||||
|
Text("Customize")
|
||||||
|
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
showSettings.toggle()
|
showSettings.toggle()
|
||||||
}, label: {
|
}) {
|
||||||
Image(systemName: "gear")
|
Image(systemName: "gear")
|
||||||
.foregroundColor(Color(UIColor.darkGray))
|
|
||||||
.font(.system(size: 20))
|
.font(.system(size: 20))
|
||||||
}).padding(.trailing)
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Settings Section
|
||||||
|
struct SettingsSection<Content: View>: View {
|
||||||
|
let title: String
|
||||||
|
@ViewBuilder let content: Content
|
||||||
|
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text(title.uppercased())
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
|
.tracking(0.5)
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 16)
|
||||||
|
.fill(colorScheme == .dark ? Color(.systemGray6) : .white)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Settings Row
|
||||||
|
struct SettingsRow<Content: View>: View {
|
||||||
|
let title: String
|
||||||
|
@ViewBuilder let content: Content
|
||||||
|
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 15, weight: .medium))
|
||||||
|
.foregroundColor(textColor.opacity(0.7))
|
||||||
|
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Theme Picker
|
||||||
|
struct ThemePickerCompact: 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 {
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
ForEach(Theme.allCases, id: \.rawValue) { aTheme in
|
||||||
|
Button(action: {
|
||||||
|
theme = aTheme
|
||||||
|
changeTextColor(forTheme: aTheme)
|
||||||
|
EventLogger.log(event: "change_theme_id", withData: ["id": aTheme.rawValue])
|
||||||
|
}) {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
ZStack {
|
||||||
|
aTheme.currentTheme.preview
|
||||||
|
.overlay(
|
||||||
|
Circle()
|
||||||
|
.stroke(theme == aTheme ? Color.accentColor : Color(.systemGray4), lineWidth: 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
if theme == aTheme {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.font(.system(size: 18))
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.background(Circle().fill(.white).padding(2))
|
||||||
|
.offset(x: 14, y: 14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(aTheme.title)
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundColor(theme == aTheme ? .accentColor : textColor.opacity(0.6))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func changeTextColor(forTheme theme: Theme) {
|
||||||
|
if [Theme.iFeel, Theme.system].contains(theme) {
|
||||||
|
let currentSystemScheme = UITraitCollection.current.userInterfaceStyle
|
||||||
|
textColor = currentSystemScheme == .dark ? .white : .black
|
||||||
|
}
|
||||||
|
if theme == Theme.dark { textColor = .white }
|
||||||
|
if theme == Theme.light { textColor = .black }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Text Color Picker
|
||||||
|
struct TextColorPickerCompact: View {
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
ColorPicker("", selection: $textColor)
|
||||||
|
.labelsHidden()
|
||||||
|
|
||||||
|
Text("Sample Text")
|
||||||
|
.font(.system(size: 16, weight: .medium))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Image Pack Picker
|
||||||
|
struct ImagePackPickerCompact: View {
|
||||||
|
@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
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(String(customMoodTintUpdateNumber)).hidden().frame(height: 0)
|
||||||
|
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
ForEach(MoodImages.allCases, id: \.rawValue) { images in
|
||||||
|
Button(action: {
|
||||||
|
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
impactMed.impactOccurred()
|
||||||
|
imagePack = images
|
||||||
|
EventLogger.log(event: "change_image_pack_id", withData: ["id": images.rawValue])
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
ForEach(Mood.allValues, id: \.self) { mood in
|
||||||
|
images.icon(forMood: mood)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 28, height: 28)
|
||||||
|
.foregroundColor(moodTint.color(forMood: mood))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if imagePack == images {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.font(.system(size: 22))
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(14)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(imagePack == images
|
||||||
|
? Color.accentColor.opacity(0.08)
|
||||||
|
: (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Tint Picker
|
||||||
|
struct TintPickerCompact: View {
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.customMoodTintUpdateNumber.rawValue, store: GroupUserDefaults.groupDefaults) private var customMoodTintUpdateNumber: Int = 0
|
||||||
|
@StateObject private var customMoodTint = UserDefaultsStore.getCustomMoodTint()
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
ForEach(MoodTints.defaultOptions, id: \.rawValue) { tint in
|
||||||
|
Button(action: {
|
||||||
|
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
impactMed.impactOccurred()
|
||||||
|
moodTint = tint
|
||||||
|
EventLogger.log(event: "change_mood_tint_id", withData: ["id": tint.rawValue])
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
ForEach(Mood.allValues, id: \.self) { mood in
|
||||||
|
Circle()
|
||||||
|
.fill(tint.color(forMood: mood))
|
||||||
|
.frame(width: 28, height: 28)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if moodTint == tint {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.font(.system(size: 22))
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(14)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(moodTint == tint
|
||||||
|
? Color.accentColor.opacity(0.08)
|
||||||
|
: (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom colors
|
||||||
|
Button(action: {
|
||||||
|
moodTint = .Custom
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
ForEach(0..<5, id: \.self) { index in
|
||||||
|
ColorPicker("", selection: colorBinding(for: index))
|
||||||
|
.labelsHidden()
|
||||||
|
.onChange(of: colorBinding(for: index).wrappedValue) { _ in
|
||||||
|
saveCustomMoodTint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if moodTint == .Custom {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.font(.system(size: 22))
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
} else {
|
||||||
|
Text("Custom")
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(14)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(moodTint == .Custom
|
||||||
|
? Color.accentColor.opacity(0.08)
|
||||||
|
: (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func colorBinding(for index: Int) -> Binding<Color> {
|
||||||
|
switch index {
|
||||||
|
case 0: return $customMoodTint.colorOne
|
||||||
|
case 1: return $customMoodTint.colorTwo
|
||||||
|
case 2: return $customMoodTint.colorThree
|
||||||
|
case 3: return $customMoodTint.colorFour
|
||||||
|
default: return $customMoodTint.colorFive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveCustomMoodTint() {
|
||||||
|
UserDefaultsStore.saveCustomMoodTint(customTint: customMoodTint)
|
||||||
|
moodTint = .Custom
|
||||||
|
EventLogger.log(event: "change_mood_tint_id", withData: ["id": MoodTints.Custom.rawValue])
|
||||||
|
customMoodTintUpdateNumber += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Voting Layout Picker
|
||||||
|
struct VotingLayoutPickerCompact: View {
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var votingLayoutStyle: Int = 0
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
|
private var currentLayout: VotingLayoutStyle {
|
||||||
|
VotingLayoutStyle(rawValue: votingLayoutStyle) ?? .horizontal
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
|
||||||
|
Button(action: {
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
votingLayoutStyle = layout.rawValue
|
||||||
|
}
|
||||||
|
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
||||||
|
}) {
|
||||||
|
VStack(spacing: 6) {
|
||||||
|
layoutIcon(for: layout)
|
||||||
|
.frame(width: 44, height: 44)
|
||||||
|
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.4))
|
||||||
|
|
||||||
|
Text(layout.displayName)
|
||||||
|
.font(.system(size: 11, weight: .medium))
|
||||||
|
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.5))
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(currentLayout == layout
|
||||||
|
? Color.accentColor.opacity(0.1)
|
||||||
|
: (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func layoutIcon(for layout: VotingLayoutStyle) -> some View {
|
||||||
|
switch layout {
|
||||||
|
case .horizontal:
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
ForEach(0..<5, id: \.self) { _ in Circle().frame(width: 7, height: 7) }
|
||||||
|
}
|
||||||
|
case .cards:
|
||||||
|
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 4) {
|
||||||
|
ForEach(0..<6, id: \.self) { _ in RoundedRectangle(cornerRadius: 3).frame(width: 10, height: 12) }
|
||||||
|
}
|
||||||
|
case .radial:
|
||||||
|
ZStack {
|
||||||
|
ForEach(0..<5, id: \.self) { index in
|
||||||
|
Circle()
|
||||||
|
.frame(width: 7, height: 7)
|
||||||
|
.offset(radialOffset(index: index, total: 5, radius: 15))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .stacked:
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
ForEach(0..<4, id: \.self) { _ in RoundedRectangle(cornerRadius: 2).frame(width: 32, height: 7) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func radialOffset(index: Int, total: Int, radius: CGFloat) -> CGSize {
|
||||||
|
let angle = Double.pi - (Double.pi * Double(index) / Double(total - 1))
|
||||||
|
return CGSize(width: radius * CGFloat(cos(angle)), height: -radius * CGFloat(sin(angle)) + 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Custom Widget Section
|
||||||
|
struct CustomWidgetSection: 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 {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
ForEach(UserDefaultsStore.getCustomWidgets(), id: \.uuid) { widget in
|
||||||
|
CustomWidgetView(customWidgetModel: widget)
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
.cornerRadius(12)
|
||||||
|
.onTapGesture {
|
||||||
|
EventLogger.log(event: "show_widget")
|
||||||
|
selectedWidget.fuckingWrapped = widget.copy() as? CustomWidgetModel
|
||||||
|
selectedWidget.showFuckingSheet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add button
|
||||||
|
Button(action: {
|
||||||
|
EventLogger.log(event: "tap_create_new_widget")
|
||||||
|
selectedWidget.fuckingWrapped = CustomWidgetModel.randomWidget
|
||||||
|
selectedWidget.showFuckingSheet = true
|
||||||
|
}) {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(Color(.systemGray5))
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
|
||||||
|
Image(systemName: "plus")
|
||||||
|
.font(.system(size: 24, weight: .medium))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Link(destination: URL(string: "https://support.apple.com/guide/iphone/add-widgets-iphb8f1bf206/ios")!) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "questionmark.circle")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
Text("How to add widgets")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
}
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $selectedWidget.showFuckingSheet) {
|
||||||
|
if let fuckingWrapped = selectedWidget.fuckingWrapped {
|
||||||
|
CreateWidgetView(customWidget: fuckingWrapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Personality Pack Picker
|
||||||
|
struct PersonalityPackPickerCompact: View {
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.personalityPack.rawValue, store: GroupUserDefaults.groupDefaults) private var personalityPack: PersonalityPack = .Default
|
||||||
|
@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
|
||||||
|
@State private var showOver18Alert = false
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
ForEach(PersonalityPack.allCases, id: \.self) { aPack in
|
||||||
|
Button(action: {
|
||||||
|
if aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW {
|
||||||
|
showOver18Alert = true
|
||||||
|
EventLogger.log(event: "show_over_18_alert")
|
||||||
|
} else {
|
||||||
|
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
impactMed.impactOccurred()
|
||||||
|
personalityPack = aPack
|
||||||
|
EventLogger.log(event: "change_personality_pack", withData: ["pack_title": aPack.title()])
|
||||||
|
LocalNotification.rescheduleNotifiations()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(String(aPack.title()))
|
||||||
|
.font(.system(size: 15, weight: .semibold))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
let strings = aPack.randomPushNotificationStrings()
|
||||||
|
Text(strings.body)
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if personalityPack == aPack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.font(.system(size: 22))
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(14)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(personalityPack == aPack
|
||||||
|
? Color.accentColor.opacity(0.08)
|
||||||
|
: (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.blur(radius: aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW ? 4 : 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(isPresented: $showOver18Alert) {
|
||||||
|
Alert(
|
||||||
|
title: Text(String(localized: "customize_view_over18alert_title")),
|
||||||
|
message: Text(String(localized: "customize_view_over18alert_body")),
|
||||||
|
primaryButton: .default(Text(String(localized: "customize_view_over18alert_ok"))) {
|
||||||
|
showNSFW = true
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel(Text(String(localized: "customize_view_over18alert_no"))) {
|
||||||
|
showNSFW = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Day Filter Picker
|
||||||
|
struct DayFilterPickerCompact: View {
|
||||||
|
@StateObject private var filteredDays = DaysFilterClass.shared
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
|
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 {
|
||||||
|
VStack(spacing: 14) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
ForEach(weekdays.indices, id: \.self) { dayIdx in
|
||||||
|
let day = String(weekdays[dayIdx].0)
|
||||||
|
let value = weekdays[dayIdx].1
|
||||||
|
let isActive = filteredDays.currentFilters.contains(value)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
if isActive {
|
||||||
|
filteredDays.removeFilter(filter: value)
|
||||||
|
} else {
|
||||||
|
filteredDays.addFilter(newFilter: value)
|
||||||
|
}
|
||||||
|
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
impactMed.impactOccurred()
|
||||||
|
}) {
|
||||||
|
Text(day.prefix(2).uppercased())
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.foregroundColor(isActive ? .white : textColor.opacity(0.5))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 40)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.fill(isActive ? Color.accentColor : (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(String(localized: "day_picker_view_text"))
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Subscription Banner
|
||||||
|
struct SubscriptionBannerView: View {
|
||||||
|
@Binding var showSubscriptionStore: Bool
|
||||||
|
@EnvironmentObject var iapManager: IAPManager
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if iapManager.isSubscribed {
|
||||||
|
subscribedView
|
||||||
|
} else {
|
||||||
|
notSubscribedView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var subscribedView: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "checkmark.seal.fill")
|
||||||
|
.font(.system(size: 28))
|
||||||
|
.foregroundColor(.green)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Premium Active")
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
|
||||||
|
Text("You have full access")
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button("Manage") {
|
||||||
|
Task {
|
||||||
|
await openSubscriptionManagement()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(Capsule().fill(Color.green.opacity(0.15)))
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 16)
|
||||||
|
.fill(colorScheme == .dark ? Color(.systemGray6) : .white)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var notSubscribedView: some View {
|
||||||
|
Button(action: {
|
||||||
|
EventLogger.log(event: "customize_subscribe_tapped")
|
||||||
|
showSubscriptionStore = true
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "crown.fill")
|
||||||
|
.font(.system(size: 28))
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Unlock Premium")
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
.foregroundColor(colorScheme == .dark ? .white : .black)
|
||||||
|
|
||||||
|
Text("Month & Year views, Insights & more")
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 16)
|
||||||
|
.fill(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [Color.orange.opacity(0.12), Color.pink.opacity(0.08)],
|
||||||
|
startPoint: .leading,
|
||||||
|
endPoint: .trailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openSubscriptionManagement() async {
|
||||||
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
||||||
|
do {
|
||||||
|
try await AppStore.showManageSubscriptions(in: windowScene)
|
||||||
|
} catch {
|
||||||
|
print("Failed to open subscription management: \(error)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,9 +753,11 @@ struct CustomizeView_Previews: PreviewProvider {
|
|||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group {
|
Group {
|
||||||
CustomizeView()
|
CustomizeView()
|
||||||
|
.environmentObject(IAPManager())
|
||||||
|
|
||||||
CustomizeView()
|
CustomizeView()
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
|
.environmentObject(IAPManager())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ struct InsightsView: View {
|
|||||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
@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.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.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
@StateObject private var viewModel = InsightsViewModel()
|
@StateObject private var viewModel = InsightsViewModel()
|
||||||
@EnvironmentObject var iapManager: IAPManager
|
@EnvironmentObject var iapManager: IAPManager
|
||||||
@@ -24,7 +25,7 @@ struct InsightsView: View {
|
|||||||
// Header
|
// Header
|
||||||
HStack {
|
HStack {
|
||||||
Text("Insights")
|
Text("Insights")
|
||||||
.font(.largeTitle.bold())
|
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
@@ -38,7 +39,7 @@ struct InsightsView: View {
|
|||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
moodTint: moodTint,
|
moodTint: moodTint,
|
||||||
imagePack: imagePack,
|
imagePack: imagePack,
|
||||||
theme: theme
|
colorScheme: colorScheme
|
||||||
)
|
)
|
||||||
|
|
||||||
// This Year Section
|
// This Year Section
|
||||||
@@ -49,7 +50,7 @@ struct InsightsView: View {
|
|||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
moodTint: moodTint,
|
moodTint: moodTint,
|
||||||
imagePack: imagePack,
|
imagePack: imagePack,
|
||||||
theme: theme
|
colorScheme: colorScheme
|
||||||
)
|
)
|
||||||
|
|
||||||
// All Time Section
|
// All Time Section
|
||||||
@@ -60,7 +61,7 @@ struct InsightsView: View {
|
|||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
moodTint: moodTint,
|
moodTint: moodTint,
|
||||||
imagePack: imagePack,
|
imagePack: imagePack,
|
||||||
theme: theme
|
colorScheme: colorScheme
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(.vertical)
|
.padding(.vertical)
|
||||||
@@ -114,7 +115,7 @@ struct InsightsSectionView: View {
|
|||||||
let textColor: Color
|
let textColor: Color
|
||||||
let moodTint: MoodTints
|
let moodTint: MoodTints
|
||||||
let imagePack: MoodImages
|
let imagePack: MoodImages
|
||||||
let theme: Theme
|
let colorScheme: ColorScheme
|
||||||
|
|
||||||
@State private var isExpanded = true
|
@State private var isExpanded = true
|
||||||
|
|
||||||
@@ -124,18 +125,18 @@ struct InsightsSectionView: View {
|
|||||||
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() } }) {
|
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() } }) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.title3)
|
.font(.system(size: 18, weight: .medium))
|
||||||
.foregroundColor(textColor.opacity(0.7))
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
|
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.title2.bold())
|
.font(.system(size: 20, weight: .bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
||||||
.font(.caption.weight(.semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 14)
|
.padding(.vertical, 14)
|
||||||
@@ -144,13 +145,14 @@ struct InsightsSectionView: View {
|
|||||||
|
|
||||||
// Insights List (collapsible)
|
// Insights List (collapsible)
|
||||||
if isExpanded {
|
if isExpanded {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 10) {
|
||||||
ForEach(insights) { insight in
|
ForEach(insights) { insight in
|
||||||
InsightCardView(
|
InsightCardView(
|
||||||
insight: insight,
|
insight: insight,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
moodTint: moodTint,
|
moodTint: moodTint,
|
||||||
imagePack: imagePack
|
imagePack: imagePack,
|
||||||
|
colorScheme: colorScheme
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,7 +163,7 @@ struct InsightsSectionView: View {
|
|||||||
}
|
}
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 16)
|
RoundedRectangle(cornerRadius: 16)
|
||||||
.fill(theme.currentTheme.secondaryBGColor)
|
.fill(colorScheme == .dark ? Color(.systemGray6) : .white)
|
||||||
)
|
)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
@@ -173,12 +175,13 @@ struct InsightCardView: View {
|
|||||||
let textColor: Color
|
let textColor: Color
|
||||||
let moodTint: MoodTints
|
let moodTint: MoodTints
|
||||||
let imagePack: MoodImages
|
let imagePack: MoodImages
|
||||||
|
let colorScheme: ColorScheme
|
||||||
|
|
||||||
private var accentColor: Color {
|
private var accentColor: Color {
|
||||||
if let mood = insight.mood {
|
if let mood = insight.mood {
|
||||||
return moodTint.color(forMood: mood)
|
return moodTint.color(forMood: mood)
|
||||||
}
|
}
|
||||||
return textColor.opacity(0.6)
|
return .accentColor
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -205,11 +208,11 @@ struct InsightCardView: View {
|
|||||||
// Text Content
|
// Text Content
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(insight.title)
|
Text(insight.title)
|
||||||
.font(.subheadline.weight(.semibold))
|
.font(.system(size: 15, weight: .semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Text(insight.description)
|
Text(insight.description)
|
||||||
.font(.subheadline)
|
.font(.system(size: 14))
|
||||||
.foregroundColor(textColor.opacity(0.7))
|
.foregroundColor(textColor.opacity(0.7))
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
@@ -219,7 +222,7 @@ struct InsightCardView: View {
|
|||||||
.padding(14)
|
.padding(14)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 12)
|
RoundedRectangle(cornerRadius: 12)
|
||||||
.fill(accentColor.opacity(0.08))
|
.fill(colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user