Add app icon asset, screenshot exporter, and misc updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-10 09:31:01 -06:00
parent bfef0a4472
commit 65460c63b3
12 changed files with 355 additions and 14 deletions

View File

@@ -37,7 +37,12 @@
"Bash(for:*)",
"Bash(# Check Button and Label strings grep -rE ''\\(Button|Label|navigationTitle\\)\\\\\\(\"\"'' /Users/treyt/Desktop/code/Feels/Shared/ --include=\"\"*.swift\"\")",
"Bash(# Double-check a few of these strings to make sure they''re not used echo \"\"=== Checking ''Custom'' ===\"\" grep -rn ''\"\"Custom\"\"'' /Users/treyt/Desktop/code/Feels/Shared/ --include=\"\"*.swift\"\")",
"Bash(echo \"=== How ''3D card flip'' is used ===\" grep -rn \"3D card flip\" /Users/treyt/Desktop/code/Feels/Shared/ --include=\"*.swift\")"
"Bash(echo \"=== How ''3D card flip'' is used ===\" grep -rn \"3D card flip\" /Users/treyt/Desktop/code/Feels/Shared/ --include=\"*.swift\")",
"Bash(ffprobe:*)",
"Bash(npx remotion:*)",
"Bash(npx tsc:*)",
"Bash(npm start)",
"Bash(npm run:*)"
],
"ask": [
"Bash(git commit:*)",

View File

@@ -67,7 +67,7 @@
},
"subscriptionGroups" : [
{
"id" : "2CFE4C4F",
"id" : "21914363",
"localizations" : [
],
@@ -92,10 +92,10 @@
"locale" : "en_US"
}
],
"productID" : "com.tt.feels.IAP.subscriptions.monthly",
"productID" : "com.88oakapps.feels.IAP.subscriptions.monthly",
"recurringSubscriptionPeriod" : "P1M",
"referenceName" : "Monthly",
"subscriptionGroupID" : "2CFE4C4F",
"subscriptionGroupID" : "21914363",
"type" : "RecurringSubscription",
"winbackOffers" : [
@@ -120,10 +120,10 @@
"locale" : "en_US"
}
],
"productID" : "com.tt.feels.IAP.subscriptions.yearly",
"productID" : "com.88oakapps.feels.IAP.subscriptions.yearly",
"recurringSubscriptionPeriod" : "P1Y",
"referenceName" : "Yearly",
"subscriptionGroupID" : "2CFE4C4F",
"subscriptionGroupID" : "21914363",
"type" : "RecurringSubscription",
"winbackOffers" : [

View File

@@ -12,12 +12,12 @@
<key>Feels (macOS).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
<integer>2</integer>
</dict>
<key>Feels Watch App.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
<integer>3</integer>
</dict>
<key>FeelsWidgetExtension.xcscheme_^#shared#^_</key>
<dict>

View File

@@ -997,6 +997,9 @@
}
}
}
},
"%lld DAYS TRACKED" : {
},
"%lld entries" : {
"comment" : "A label showing the total number of mood entries recorded. The argument is the total number of entries.",
@@ -1039,6 +1042,9 @@
}
}
}
},
"%lld moods" : {
},
"%lld percent" : {
"comment" : "A value indicating the percentage of health data that has been successfully synced with Apple Health.",
@@ -1213,6 +1219,9 @@
}
}
}
},
"→" : {
},
">" : {
"comment" : "A symbol that appears before a command in a terminal interface.",
@@ -1543,6 +1552,9 @@
}
}
}
},
"10 DAYS" : {
},
"12" : {
"localizations" : {
@@ -2200,6 +2212,9 @@
"All styles & complications" : {
"comment" : "A description of what the \"Export Watch Screenshots\" button does.",
"isCommentAutoGenerated" : true
},
"All Time Moods" : {
},
"Allow deleting mood entries by swiping" : {
"comment" : "A hint describing the functionality of the \"Allow deleting mood entries by swiping\" toggle.",
@@ -4836,6 +4851,9 @@
}
}
}
},
"DAYS" : {
},
"Days Tracked" : {
"comment" : "A label displayed below the number of days a user has tracked their mood.",
@@ -6279,6 +6297,9 @@
}
}
}
},
"entries tracked" : {
},
"Entry Details" : {
"comment" : "The title of the view that displays detailed information about a mood entry.",
@@ -6833,11 +6854,11 @@
}
},
"Export Voting Layouts" : {
"comment" : "A button label that triggers the export of all voting layout configurations.",
"comment" : "A button label that allows users to export all voting layout configurations.",
"isCommentAutoGenerated" : true
},
"Export Watch Screenshots" : {
"comment" : "A button label that allows users to export all watch face and complication previews to a file.",
"comment" : "A button label that allows users to export watch view screenshots.",
"isCommentAutoGenerated" : true
},
"Export Widget Screenshots" : {
@@ -7262,6 +7283,10 @@
}
}
},
"Fill 2 years data + export PNGs" : {
"comment" : "A description of the action to generate and export sharing screenshots.",
"isCommentAutoGenerated" : true
},
"Find Your\nInner Calm" : {
"comment" : "A title describing the main benefit of the premium subscription.",
"isCommentAutoGenerated" : true,
@@ -7468,6 +7493,10 @@
}
}
},
"Generate & Export Sharing" : {
"comment" : "A button that, when tapped, generates and exports all sharing screenshots.",
"isCommentAutoGenerated" : true
},
"Get Mood Streak" : {
"comment" : "Title of an intent that checks the user's current mood logging streak.",
"isCommentAutoGenerated" : true,
@@ -8706,6 +8735,12 @@
}
}
}
},
"Longest Streak" : {
},
"LONGEST STREAK" : {
},
"Make Tracking\nFun Again!" : {
"localizations" : {
@@ -9669,6 +9704,10 @@
}
}
},
"No designs available" : {
"comment" : "A message displayed when there are no sharing design variations available.",
"isCommentAutoGenerated" : true
},
"No entry" : {
"comment" : "A label indicating that there is no entry for a particular day.",
"isCommentAutoGenerated" : true,
@@ -9878,6 +9917,9 @@
}
}
}
},
"of feeling" : {
},
"OK" : {
"comment" : "The text for an OK button.",
@@ -13294,6 +13336,10 @@
"comment" : "A description of where the insights export file will be saved.",
"isCommentAutoGenerated" : true
},
"Saved to Documents/SharingExports" : {
"comment" : "A label indicating where the generated sharing screenshots are saved.",
"isCommentAutoGenerated" : true
},
"Saved to Documents/VotingLayoutExports" : {
"comment" : "A description of where the voting layouts are saved when exported.",
"isCommentAutoGenerated" : true
@@ -18031,6 +18077,9 @@
}
}
}
},
"Your recent moods" : {
}
},
"version" : "1.1"

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "FeelsAppIcon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -42,11 +42,11 @@ class IAPManager: ObservableObject {
// MARK: - Constants
static let subscriptionGroupID = "2CFE4C4F"
static let subscriptionGroupID = "21914363"
private let productIdentifiers: Set<String> = [
"com.tt.feels.IAP.subscriptions.monthly",
"com.tt.feels.IAP.subscriptions.yearly"
"com.88oakapps.feels.IAP.subscriptions.monthly",
"com.88oakapps.feels.IAP.subscriptions.yearly"
]
private let trialDays = 30

View File

@@ -79,6 +79,29 @@ extension DataController {
saveAndRunDataListeners()
}
#if DEBUG
func populate2YearsData() {
clearDB()
for idx in 1...730 {
let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())!
var moodValue = Int.random(in: 3...4)
if Int.random(in: 0...400) % 5 == 0 {
moodValue = Int.random(in: 0...4)
}
let entry = MoodEntryModel(
forDate: date,
mood: Mood(rawValue: moodValue) ?? .average,
entryType: .listView
)
modelContext.insert(entry)
}
saveAndRunDataListeners()
}
#endif
func longestStreak() -> [MoodEntryModel] {
let descriptor = FetchDescriptor<MoodEntryModel>(
sortBy: [SortDescriptor(\.forDate, order: .forward)]

View File

@@ -136,8 +136,11 @@ extension View {
}
func asImage(size: CGSize) -> UIImage {
let controller = UIHostingController(rootView: self)
let wrapped = self.ignoresSafeArea().frame(width: size.width, height: size.height)
let controller = UIHostingController(rootView: wrapped)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.backgroundColor = .clear
controller.view.layoutIfNeeded()
let image = controller.view.asImage()
return image
}

View File

@@ -0,0 +1,176 @@
//
// SharingScreenshotExporter.swift
// Feels
//
// Debug utility to export sharing template screenshots.
//
#if DEBUG
import SwiftUI
import UIKit
/// Exports sharing template screenshots for App Store marketing
@MainActor
class SharingScreenshotExporter {
/// Exports all original templates + kept variations as PNGs
/// - Returns: URL to the export directory, or nil if failed
static func exportAllSharingScreenshots() async -> URL? {
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let exportPath = documentsPath.appendingPathComponent("SharingExports", isDirectory: true)
// Clean and create export directory
try? FileManager.default.removeItem(at: exportPath)
try? FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true)
// Create subdirectories
let origDir = exportPath.appendingPathComponent("originals", isDirectory: true)
let varDir = exportPath.appendingPathComponent("variations", isDirectory: true)
try? FileManager.default.createDirectory(at: origDir, withIntermediateDirectories: true)
try? FileManager.default.createDirectory(at: varDir, withIntermediateDirectories: true)
var totalExported = 0
let distantPast = Date(timeIntervalSince1970: 0)
let now = Date()
let calendar = Calendar.current
//
// Fetch shared data once for all variations
//
// All moods data
let allEntries = DataController.shared.getData(
startDate: distantPast, endDate: now, includedDays: [1,2,3,4,5,6,7]
)
let allMetrics = Random.createTotalPerc(fromEntries: allEntries)
.sorted(by: { $0.mood.rawValue > $1.mood.rawValue })
// Current streak data (last 10 days) fetch 12 days to handle boundary, take last 10
let streakFetchStart = calendar.date(byAdding: .day, value: -12, to: now)!
let streakEntries = Array(DataController.shared.getData(
startDate: streakFetchStart, endDate: now, includedDays: [1,2,3,4,5,6,7]
).suffix(10))
// Current month data
let currentMonthEntries = DataController.shared.getData(
startDate: now.startOfMonth, endDate: now.endOfMonth, includedDays: [1,2,3,4,5,6,7]
)
let currentMonthMetrics = Random.createTotalPerc(fromEntries: currentMonthEntries)
.sorted(by: { $0.mood.rawValue > $1.mood.rawValue })
let currentMonth = calendar.component(.month, from: now)
// Last month data
let lastMonthDate = calendar.date(byAdding: .month, value: -1, to: now)!
let lastMonthStart = lastMonthDate.startOfMonth
let lastMonthEnd = lastMonthDate.endOfMonth
let lastMonthEntries = DataController.shared.getData(
startDate: lastMonthStart, endDate: lastMonthEnd, includedDays: [1,2,3,4,5,6,7]
)
let lastMonthMetrics = Random.createTotalPerc(fromEntries: lastMonthEntries)
.sorted(by: { $0.mood.rawValue > $1.mood.rawValue })
let lastMonth = calendar.component(.month, from: lastMonthDate)
// Longest streak data (find longest "great" streak)
let selectedMood: Mood = .great
let longestStreakEntries: [MoodEntryModel] = {
var splitArrays = [[MoodEntryModel]]()
var currentSplit = [MoodEntryModel]()
for entry in allEntries {
if entry.mood == selectedMood {
currentSplit.append(entry)
} else {
splitArrays.append(currentSplit)
currentSplit.removeAll()
}
}
splitArrays.append(currentSplit)
return splitArrays.sorted(by: { $0.count > $1.count }).first ?? []
}()
//
// Export originals
//
let allMoods = AllMoodsTotalTemplate(isPreview: false, startDate: distantPast, endDate: now, fakeData: false)
if saveImage(allMoods.image, to: origDir, name: "all_moods_total") { totalExported += 1 }
let currentStreak = CurrentStreakTemplate(isPreview: false, startDate: streakFetchStart, endDate: now, fakeData: false)
if saveImage(currentStreak.image, to: origDir, name: "current_streak") { totalExported += 1 }
let monthTotal = MonthTotalTemplate(isPreview: false, startDate: now.startOfMonth, endDate: now.endOfMonth, fakeData: false)
if saveImage(monthTotal.image, to: origDir, name: "month_total") { totalExported += 1 }
let longestStreak = LongestStreakTemplate(isPreview: false, startDate: distantPast, endDate: now, fakeData: false)
if saveImage(longestStreak.image, to: origDir, name: "longest_streak") { totalExported += 1 }
//
// Export All Moods variations (666x1190)
// Kept: V2, V5
//
if saveImage(AllMoodsV2(metrics: allMetrics, totalCount: allEntries.count).image,
to: varDir, name: "all_moods_v2_gradient") { totalExported += 1 }
if saveImage(AllMoodsV5(metrics: allMetrics, totalCount: allEntries.count).image,
to: varDir, name: "all_moods_v5_colorblock") { totalExported += 1 }
//
// Export Current Streak variations (666x1190)
// Kept: V2, V5
//
if saveImage(CurrentStreakV2(moodEntries: streakEntries).image,
to: varDir, name: "current_streak_v2_gradient") { totalExported += 1 }
if saveImage(CurrentStreakV5(moodEntries: streakEntries).image,
to: varDir, name: "current_streak_v5_colorblock") { totalExported += 1 }
//
// Export Month Total variations (666x1190)
// Kept: V1, V5 exported for BOTH current and last month
//
// Current month
if saveImage(MonthTotalV1(moodMetrics: currentMonthMetrics, moodEntries: currentMonthEntries, month: currentMonth).image,
to: varDir, name: "month_total_v1_clean_current") { totalExported += 1 }
if saveImage(MonthTotalV5(moodMetrics: currentMonthMetrics, moodEntries: currentMonthEntries, month: currentMonth).image,
to: varDir, name: "month_total_v5_colorblock_current") { totalExported += 1 }
// Last month
if saveImage(MonthTotalV1(moodMetrics: lastMonthMetrics, moodEntries: lastMonthEntries, month: lastMonth).image,
to: varDir, name: "month_total_v1_clean_lastmonth") { totalExported += 1 }
if saveImage(MonthTotalV5(moodMetrics: lastMonthMetrics, moodEntries: lastMonthEntries, month: lastMonth).image,
to: varDir, name: "month_total_v5_colorblock_lastmonth") { totalExported += 1 }
//
// Export Longest Streak variations (650x400)
// Kept: V2, V3
//
if saveImage(LongestStreakV2(streakEntries: longestStreakEntries, selectedMood: selectedMood).image,
to: varDir, name: "longest_streak_v2_gradient") { totalExported += 1 }
if saveImage(LongestStreakV3(streakEntries: longestStreakEntries, selectedMood: selectedMood).image,
to: varDir, name: "longest_streak_v3_dark") { totalExported += 1 }
print("📸 Total \(totalExported) sharing screenshots exported to: \(exportPath.path)")
print(" originals/ — 4 original templates")
print(" variations/ — 12 design variations (2 all moods, 2 current streak, 4 month total, 2 longest streak)")
return exportPath
}
private static func saveImage(_ image: UIImage, to folder: URL, name: String) -> Bool {
let url = folder.appendingPathComponent("\(name).png")
if let data = image.pngData() {
do {
try data.write(to: url)
return true
} catch {
print("Failed to save \(name): \(error)")
}
}
return false
}
}
#endif

View File

@@ -27,6 +27,8 @@ struct SettingsContentView: View {
@State private var watchExportPath: URL?
@State private var isExportingInsights = false
@State private var insightsExportPath: URL?
@State private var isGeneratingScreenshots = false
@State private var sharingExportPath: URL?
@State private var isDeletingHealthKitData = false
@State private var healthKitDeleteResult: String?
@StateObject private var healthService = HealthService.shared
@@ -73,6 +75,7 @@ struct SettingsContentView: View {
exportVotingLayoutsButton
exportWatchViewsButton
exportInsightsButton
generateAndExportButton
deleteHealthKitDataButton
clearDataButton
@@ -654,6 +657,67 @@ struct SettingsContentView: View {
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}
private var generateAndExportButton: some View {
ZStack {
theme.currentTheme.secondaryBGColor
Button {
isGeneratingScreenshots = true
Task {
DataController.shared.populate2YearsData()
sharingExportPath = await SharingScreenshotExporter.exportAllSharingScreenshots()
isGeneratingScreenshots = false
if let path = sharingExportPath {
print("📸 Sharing screenshots exported to: \(path.path)")
openInFilesApp(path)
}
}
} label: {
HStack(spacing: 12) {
if isGeneratingScreenshots {
ProgressView()
.frame(width: 32)
} else {
Image(systemName: "photo.on.rectangle.angled")
.font(.title2)
.foregroundStyle(
LinearGradient(
colors: [.green, .blue],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(width: 32)
}
VStack(alignment: .leading, spacing: 2) {
Text("Generate & Export Sharing")
.foregroundColor(textColor)
if let path = sharingExportPath {
Text("Saved to Documents/SharingExports")
.font(.caption)
.foregroundColor(.green)
} else {
Text("Fill 2 years data + export PNGs")
.font(.caption)
.foregroundStyle(.secondary)
}
}
Spacer()
Image(systemName: "arrow.down.doc.fill")
.font(.caption)
.foregroundStyle(.tertiary)
}
.padding()
}
.disabled(isGeneratingScreenshots)
}
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}
private var deleteHealthKitDataButton: some View {
ZStack {
theme.currentTheme.secondaryBGColor

Binary file not shown.