Add app icon asset, screenshot exporter, and misc updates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
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
|
||||
|
||||
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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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 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
|
||||
|
||||
Reference in New Issue
Block a user