Add app icon asset, screenshot exporter, and misc updates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,12 @@
|
|||||||
"Bash(for:*)",
|
"Bash(for:*)",
|
||||||
"Bash(# Check Button and Label strings grep -rE ''\\(Button|Label|navigationTitle\\)\\\\\\(\"\"'' /Users/treyt/Desktop/code/Feels/Shared/ --include=\"\"*.swift\"\")",
|
"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(# 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": [
|
"ask": [
|
||||||
"Bash(git commit:*)",
|
"Bash(git commit:*)",
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
},
|
},
|
||||||
"subscriptionGroups" : [
|
"subscriptionGroups" : [
|
||||||
{
|
{
|
||||||
"id" : "2CFE4C4F",
|
"id" : "21914363",
|
||||||
"localizations" : [
|
"localizations" : [
|
||||||
|
|
||||||
],
|
],
|
||||||
@@ -92,10 +92,10 @@
|
|||||||
"locale" : "en_US"
|
"locale" : "en_US"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"productID" : "com.tt.feels.IAP.subscriptions.monthly",
|
"productID" : "com.88oakapps.feels.IAP.subscriptions.monthly",
|
||||||
"recurringSubscriptionPeriod" : "P1M",
|
"recurringSubscriptionPeriod" : "P1M",
|
||||||
"referenceName" : "Monthly",
|
"referenceName" : "Monthly",
|
||||||
"subscriptionGroupID" : "2CFE4C4F",
|
"subscriptionGroupID" : "21914363",
|
||||||
"type" : "RecurringSubscription",
|
"type" : "RecurringSubscription",
|
||||||
"winbackOffers" : [
|
"winbackOffers" : [
|
||||||
|
|
||||||
@@ -120,10 +120,10 @@
|
|||||||
"locale" : "en_US"
|
"locale" : "en_US"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"productID" : "com.tt.feels.IAP.subscriptions.yearly",
|
"productID" : "com.88oakapps.feels.IAP.subscriptions.yearly",
|
||||||
"recurringSubscriptionPeriod" : "P1Y",
|
"recurringSubscriptionPeriod" : "P1Y",
|
||||||
"referenceName" : "Yearly",
|
"referenceName" : "Yearly",
|
||||||
"subscriptionGroupID" : "2CFE4C4F",
|
"subscriptionGroupID" : "21914363",
|
||||||
"type" : "RecurringSubscription",
|
"type" : "RecurringSubscription",
|
||||||
"winbackOffers" : [
|
"winbackOffers" : [
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
<key>Feels (macOS).xcscheme_^#shared#^_</key>
|
<key>Feels (macOS).xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>3</integer>
|
<integer>2</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Feels Watch App.xcscheme_^#shared#^_</key>
|
<key>Feels Watch App.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>2</integer>
|
<integer>3</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>FeelsWidgetExtension.xcscheme_^#shared#^_</key>
|
<key>FeelsWidgetExtension.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -997,6 +997,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"%lld DAYS TRACKED" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"%lld entries" : {
|
"%lld entries" : {
|
||||||
"comment" : "A label showing the total number of mood entries recorded. The argument is the total number of 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" : {
|
"%lld percent" : {
|
||||||
"comment" : "A value indicating the percentage of health data that has been successfully synced with Apple Health.",
|
"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.",
|
"comment" : "A symbol that appears before a command in a terminal interface.",
|
||||||
@@ -1543,6 +1552,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"10 DAYS" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"12" : {
|
"12" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2200,6 +2212,9 @@
|
|||||||
"All styles & complications" : {
|
"All styles & complications" : {
|
||||||
"comment" : "A description of what the \"Export Watch Screenshots\" button does.",
|
"comment" : "A description of what the \"Export Watch Screenshots\" button does.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"All Time Moods" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Allow deleting mood entries by swiping" : {
|
"Allow deleting mood entries by swiping" : {
|
||||||
"comment" : "A hint describing the functionality of the \"Allow deleting mood entries by swiping\" toggle.",
|
"comment" : "A hint describing the functionality of the \"Allow deleting mood entries by swiping\" toggle.",
|
||||||
@@ -4836,6 +4851,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"DAYS" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Days Tracked" : {
|
"Days Tracked" : {
|
||||||
"comment" : "A label displayed below the number of days a user has tracked their mood.",
|
"comment" : "A label displayed below the number of days a user has tracked their mood.",
|
||||||
@@ -6279,6 +6297,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"entries tracked" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Entry Details" : {
|
"Entry Details" : {
|
||||||
"comment" : "The title of the view that displays detailed information about a mood entry.",
|
"comment" : "The title of the view that displays detailed information about a mood entry.",
|
||||||
@@ -6833,11 +6854,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Export Voting Layouts" : {
|
"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
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Export Watch Screenshots" : {
|
"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
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Export Widget Screenshots" : {
|
"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" : {
|
"Find Your\nInner Calm" : {
|
||||||
"comment" : "A title describing the main benefit of the premium subscription.",
|
"comment" : "A title describing the main benefit of the premium subscription.",
|
||||||
"isCommentAutoGenerated" : true,
|
"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" : {
|
"Get Mood Streak" : {
|
||||||
"comment" : "Title of an intent that checks the user's current mood logging streak.",
|
"comment" : "Title of an intent that checks the user's current mood logging streak.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -8706,6 +8735,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Longest Streak" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"LONGEST STREAK" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Make Tracking\nFun Again!" : {
|
"Make Tracking\nFun Again!" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -9669,6 +9704,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"No designs available" : {
|
||||||
|
"comment" : "A message displayed when there are no sharing design variations available.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"No entry" : {
|
"No entry" : {
|
||||||
"comment" : "A label indicating that there is no entry for a particular day.",
|
"comment" : "A label indicating that there is no entry for a particular day.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -9878,6 +9917,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"of feeling" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"OK" : {
|
"OK" : {
|
||||||
"comment" : "The text for an OK button.",
|
"comment" : "The text for an OK button.",
|
||||||
@@ -13294,6 +13336,10 @@
|
|||||||
"comment" : "A description of where the insights export file will be saved.",
|
"comment" : "A description of where the insights export file will be saved.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Saved to Documents/SharingExports" : {
|
||||||
|
"comment" : "A label indicating where the generated sharing screenshots are saved.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Saved to Documents/VotingLayoutExports" : {
|
"Saved to Documents/VotingLayoutExports" : {
|
||||||
"comment" : "A description of where the voting layouts are saved when exported.",
|
"comment" : "A description of where the voting layouts are saved when exported.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@@ -18031,6 +18077,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Your recent moods" : {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.1"
|
"version" : "1.1"
|
||||||
|
|||||||
21
Shared/Assets.xcassets/FeelsAppIcon.imageset/Contents.json
vendored
Normal file
21
Shared/Assets.xcassets/FeelsAppIcon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Shared/Assets.xcassets/FeelsAppIcon.imageset/FeelsAppIcon.png
vendored
Normal file
BIN
Shared/Assets.xcassets/FeelsAppIcon.imageset/FeelsAppIcon.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -42,11 +42,11 @@ class IAPManager: ObservableObject {
|
|||||||
|
|
||||||
// MARK: - Constants
|
// MARK: - Constants
|
||||||
|
|
||||||
static let subscriptionGroupID = "2CFE4C4F"
|
static let subscriptionGroupID = "21914363"
|
||||||
|
|
||||||
private let productIdentifiers: Set<String> = [
|
private let productIdentifiers: Set<String> = [
|
||||||
"com.tt.feels.IAP.subscriptions.monthly",
|
"com.88oakapps.feels.IAP.subscriptions.monthly",
|
||||||
"com.tt.feels.IAP.subscriptions.yearly"
|
"com.88oakapps.feels.IAP.subscriptions.yearly"
|
||||||
]
|
]
|
||||||
|
|
||||||
private let trialDays = 30
|
private let trialDays = 30
|
||||||
|
|||||||
@@ -79,6 +79,29 @@ extension DataController {
|
|||||||
saveAndRunDataListeners()
|
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] {
|
func longestStreak() -> [MoodEntryModel] {
|
||||||
let descriptor = FetchDescriptor<MoodEntryModel>(
|
let descriptor = FetchDescriptor<MoodEntryModel>(
|
||||||
sortBy: [SortDescriptor(\.forDate, order: .forward)]
|
sortBy: [SortDescriptor(\.forDate, order: .forward)]
|
||||||
|
|||||||
@@ -136,8 +136,11 @@ extension View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func asImage(size: CGSize) -> UIImage {
|
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.bounds = CGRect(origin: .zero, size: size)
|
||||||
|
controller.view.backgroundColor = .clear
|
||||||
|
controller.view.layoutIfNeeded()
|
||||||
let image = controller.view.asImage()
|
let image = controller.view.asImage()
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|||||||
176
Shared/Services/SharingScreenshotExporter.swift
Normal file
176
Shared/Services/SharingScreenshotExporter.swift
Normal 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
|
||||||
@@ -27,6 +27,8 @@ struct SettingsContentView: View {
|
|||||||
@State private var watchExportPath: URL?
|
@State private var watchExportPath: URL?
|
||||||
@State private var isExportingInsights = false
|
@State private var isExportingInsights = false
|
||||||
@State private var insightsExportPath: URL?
|
@State private var insightsExportPath: URL?
|
||||||
|
@State private var isGeneratingScreenshots = false
|
||||||
|
@State private var sharingExportPath: URL?
|
||||||
@State private var isDeletingHealthKitData = false
|
@State private var isDeletingHealthKitData = false
|
||||||
@State private var healthKitDeleteResult: String?
|
@State private var healthKitDeleteResult: String?
|
||||||
@StateObject private var healthService = HealthService.shared
|
@StateObject private var healthService = HealthService.shared
|
||||||
@@ -73,6 +75,7 @@ struct SettingsContentView: View {
|
|||||||
exportVotingLayoutsButton
|
exportVotingLayoutsButton
|
||||||
exportWatchViewsButton
|
exportWatchViewsButton
|
||||||
exportInsightsButton
|
exportInsightsButton
|
||||||
|
generateAndExportButton
|
||||||
deleteHealthKitDataButton
|
deleteHealthKitDataButton
|
||||||
|
|
||||||
clearDataButton
|
clearDataButton
|
||||||
@@ -654,6 +657,67 @@ struct SettingsContentView: View {
|
|||||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
.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 {
|
private var deleteHealthKitDataButton: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
theme.currentTheme.secondaryBGColor
|
theme.currentTheme.secondaryBGColor
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user