Add debug widget/live activity export and competitor research
Debug features (DEBUG builds only): - WidgetExporter: Export all widget variants to PNG (light/dark modes) - Live Activity lock screen export with configurable streak - Test notifications button to preview all personality packs - Settings buttons for export and notification testing Research: - Competitor analysis docs (Daylio, Bearable, Reflectly, etc.) - App Store screenshot reference materials 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@@ -6241,6 +6241,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Export Widget Screenshots" : {
|
||||
"comment" : "A button label that prompts the user to download their light and dark mode widget screenshots.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Export your mood data as CSV or PDF" : {
|
||||
"comment" : "A hint that describes the functionality of the \"Export Data\" button in the Settings view.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -8016,6 +8020,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Light & dark mode PNGs" : {
|
||||
"comment" : "A description of what the \"Export Widget Screenshots\" button does.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Like ink on paper, each mood\nleaves its mark. Premium reveals the pattern." : {
|
||||
"comment" : "A description of how the premium version reveals patterns in user moods.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -13742,6 +13750,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Saved to Documents/WidgetExports" : {
|
||||
"comment" : "A description of where the exported widget screenshots are saved.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Say \"Hey Siri, log my mood as great in Feels\" for hands-free logging." : {
|
||||
"comment" : "A tip message for using Siri to log moods.",
|
||||
"extractionState" : "stale",
|
||||
@@ -14033,6 +14045,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Send 5 personality pack notifications" : {
|
||||
"comment" : "A description of the action that can be performed when tapping the \"Test All Notifications\" button in the Settings app.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Set Trial Start Date" : {
|
||||
"comment" : "The title of a screen that lets a user set the start date of a free trial.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -16177,6 +16193,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Test All Notifications" : {
|
||||
"comment" : "A button label that tests sending notifications.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Test builds only" : {
|
||||
"comment" : "A section header that indicates that the settings view contains only test data.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
|
||||
@@ -128,4 +128,46 @@ class LocalNotification {
|
||||
public class func removeNotificaiton() {
|
||||
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
|
||||
}
|
||||
|
||||
// MARK: - Debug: Send All Personality Pack Notifications
|
||||
|
||||
#if DEBUG
|
||||
/// Sends one notification from each personality pack, staggered over 10 seconds for screenshot
|
||||
public class func sendAllPersonalityNotificationsForScreenshot() {
|
||||
let _ = createNotificationCategory()
|
||||
|
||||
let packs: [(PersonalityPack, Double)] = [
|
||||
(.Default, 5),
|
||||
(.MotivationalCoach, 6),
|
||||
(.ZenMaster, 7),
|
||||
(.BestFriend, 8),
|
||||
(.DataAnalyst, 9)
|
||||
]
|
||||
|
||||
for (pack, delay) in packs {
|
||||
let content = UNMutableNotificationContent()
|
||||
let strings = pack.randomPushNotificationStrings()
|
||||
content.title = strings.title
|
||||
content.body = strings.body
|
||||
content.sound = .default
|
||||
content.categoryIdentifier = LocalNotification.categoryName
|
||||
content.interruptionLevel = .timeSensitive
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay, repeats: false)
|
||||
let request = UNNotificationRequest(
|
||||
identifier: "debug-\(pack.rawValue)-\(UUID().uuidString)",
|
||||
content: content,
|
||||
trigger: trigger
|
||||
)
|
||||
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
if let error = error {
|
||||
print("Failed to schedule \(pack) notification: \(error)")
|
||||
} else {
|
||||
print("Scheduled \(pack) notification in \(delay)s")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
779
Shared/Services/WidgetExporter.swift
Normal file
@@ -0,0 +1,779 @@
|
||||
//
|
||||
// WidgetExporter.swift
|
||||
// Feels
|
||||
//
|
||||
// Debug utility to export all widget previews to PNG files
|
||||
//
|
||||
|
||||
#if DEBUG
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
/// Exports widget previews to PNG files for App Store screenshots
|
||||
@MainActor
|
||||
class WidgetExporter {
|
||||
|
||||
// MARK: - Widget Sizes (iPhone 15 Pro Max @ 3x)
|
||||
|
||||
enum WidgetSize {
|
||||
case small // 170x170 pt = 510x510 px
|
||||
case medium // 364x170 pt = 1092x510 px
|
||||
case large // 364x382 pt = 1092x1146 px
|
||||
|
||||
var pointSize: CGSize {
|
||||
switch self {
|
||||
case .small: return CGSize(width: 170, height: 170)
|
||||
case .medium: return CGSize(width: 364, height: 170)
|
||||
case .large: return CGSize(width: 382, height: 382)
|
||||
}
|
||||
}
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .small: return "small"
|
||||
case .medium: return "medium"
|
||||
case .large: return "large"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Export All Widgets
|
||||
|
||||
static func exportAllWidgets() async -> URL? {
|
||||
// Create export directory
|
||||
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let exportPath = documentsPath.appendingPathComponent("WidgetExports", isDirectory: true)
|
||||
|
||||
try? FileManager.default.removeItem(at: exportPath)
|
||||
try? FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true)
|
||||
|
||||
// Export each widget type in both color schemes
|
||||
for colorScheme in [ColorScheme.light, ColorScheme.dark] {
|
||||
let schemeName = colorScheme == .light ? "light" : "dark"
|
||||
|
||||
// Vote Widget - Not Voted
|
||||
await exportVoteWidget(hasVoted: false, mood: nil, size: .small, colorScheme: colorScheme, to: exportPath, name: "vote_\(schemeName)_small_notvoted")
|
||||
await exportVoteWidget(hasVoted: false, mood: nil, size: .medium, colorScheme: colorScheme, to: exportPath, name: "vote_\(schemeName)_medium_notvoted")
|
||||
|
||||
// Vote Widget - Voted (all moods)
|
||||
for mood in Mood.allValues {
|
||||
await exportVoteWidget(hasVoted: true, mood: mood, size: .small, colorScheme: colorScheme, to: exportPath, name: "vote_\(schemeName)_small_\(mood.strValue.lowercased())")
|
||||
await exportVoteWidget(hasVoted: true, mood: mood, size: .medium, colorScheme: colorScheme, to: exportPath, name: "vote_\(schemeName)_medium_\(mood.strValue.lowercased())")
|
||||
}
|
||||
|
||||
// Timeline Widget - Logged
|
||||
await exportTimelineWidget(hasVoted: true, size: .small, colorScheme: colorScheme, to: exportPath, name: "timeline_\(schemeName)_small_logged")
|
||||
await exportTimelineWidget(hasVoted: true, size: .medium, colorScheme: colorScheme, to: exportPath, name: "timeline_\(schemeName)_medium_logged")
|
||||
await exportTimelineWidget(hasVoted: true, size: .large, colorScheme: colorScheme, to: exportPath, name: "timeline_\(schemeName)_large_logged")
|
||||
|
||||
// Timeline Widget - Voting
|
||||
await exportTimelineWidget(hasVoted: false, size: .small, colorScheme: colorScheme, to: exportPath, name: "timeline_\(schemeName)_small_voting")
|
||||
await exportTimelineWidget(hasVoted: false, size: .medium, colorScheme: colorScheme, to: exportPath, name: "timeline_\(schemeName)_medium_voting")
|
||||
await exportTimelineWidget(hasVoted: false, size: .large, colorScheme: colorScheme, to: exportPath, name: "timeline_\(schemeName)_large_voting")
|
||||
|
||||
// Live Activity - Lock Screen (213 streak, all moods + not logged)
|
||||
await exportLiveActivity(hasLogged: false, mood: nil, streak: 213, colorScheme: colorScheme, to: exportPath, name: "liveactivity_\(schemeName)_notlogged")
|
||||
for mood in Mood.allValues {
|
||||
await exportLiveActivity(hasLogged: true, mood: mood, streak: 213, colorScheme: colorScheme, to: exportPath, name: "liveactivity_\(schemeName)_\(mood.strValue.lowercased())")
|
||||
}
|
||||
}
|
||||
|
||||
print("📸 Widgets exported to: \(exportPath.path)")
|
||||
return exportPath
|
||||
}
|
||||
|
||||
// MARK: - Vote Widget Export
|
||||
|
||||
private static func exportVoteWidget(hasVoted: Bool, mood: Mood?, size: WidgetSize, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
||||
let content: AnyView
|
||||
if hasVoted, let mood = mood {
|
||||
// Voted state
|
||||
content = AnyView(
|
||||
ExportVotedStatsView(mood: mood, totalEntries: 117, isSmall: size == .small)
|
||||
)
|
||||
} else {
|
||||
// Not voted state - show voting buttons
|
||||
content = AnyView(
|
||||
ExportVotingView(isSmall: size == .small)
|
||||
)
|
||||
}
|
||||
|
||||
let view = WidgetContainer(size: size, colorScheme: colorScheme, content: content)
|
||||
await renderAndSave(view: view, size: size, to: folder, name: name)
|
||||
}
|
||||
|
||||
// MARK: - Timeline Widget Export
|
||||
|
||||
private static func exportTimelineWidget(hasVoted: Bool, size: WidgetSize, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
||||
let timelineData = createSampleTimelineData(count: size == .large ? 10 : (size == .medium ? 5 : 1))
|
||||
|
||||
let content: AnyView
|
||||
switch size {
|
||||
case .small:
|
||||
content = AnyView(
|
||||
TimelineSmallExportView(timelineData: timelineData.first, hasVoted: hasVoted)
|
||||
)
|
||||
case .medium:
|
||||
content = AnyView(
|
||||
TimelineMediumExportView(timelineData: Array(timelineData.prefix(5)), hasVoted: hasVoted)
|
||||
)
|
||||
case .large:
|
||||
content = AnyView(
|
||||
TimelineLargeExportView(timelineData: timelineData, hasVoted: hasVoted)
|
||||
)
|
||||
}
|
||||
|
||||
let view = WidgetContainer(size: size, colorScheme: colorScheme, content: content, useSystemBackground: hasVoted)
|
||||
await renderAndSave(view: view, size: size, to: folder, name: name)
|
||||
}
|
||||
|
||||
// MARK: - Live Activity Export
|
||||
|
||||
/// Live Activity lock screen size (iPhone 15 Pro Max)
|
||||
static let liveActivitySize = CGSize(width: 370, height: 100)
|
||||
|
||||
private static func exportLiveActivity(hasLogged: Bool, mood: Mood?, streak: Int, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
||||
let content = ExportLiveActivityView(
|
||||
streak: streak,
|
||||
hasLoggedToday: hasLogged,
|
||||
mood: mood
|
||||
)
|
||||
|
||||
let view = content
|
||||
.frame(width: liveActivitySize.width, height: liveActivitySize.height)
|
||||
.background(
|
||||
colorScheme == .dark
|
||||
? Color(UIColor.systemBackground).opacity(0.8)
|
||||
: Color(UIColor.secondarySystemBackground)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
|
||||
.environment(\.colorScheme, colorScheme)
|
||||
|
||||
await renderAndSaveLiveActivity(view: view, to: folder, name: name)
|
||||
}
|
||||
|
||||
private static func renderAndSaveLiveActivity<V: View>(view: V, to folder: URL, name: String) async {
|
||||
let renderer = ImageRenderer(content: view.frame(width: liveActivitySize.width, height: liveActivitySize.height))
|
||||
renderer.scale = 3.0
|
||||
|
||||
if let image = renderer.uiImage {
|
||||
let url = folder.appendingPathComponent("\(name).png")
|
||||
if let data = image.pngData() {
|
||||
try? data.write(to: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Render and Save
|
||||
|
||||
private static func renderAndSave<V: View>(view: V, size: WidgetSize, to folder: URL, name: String) async {
|
||||
let renderer = ImageRenderer(content: view.frame(width: size.pointSize.width, height: size.pointSize.height))
|
||||
renderer.scale = 3.0 // 3x for high res
|
||||
|
||||
if let image = renderer.uiImage {
|
||||
let url = folder.appendingPathComponent("\(name).png")
|
||||
if let data = image.pngData() {
|
||||
try? data.write(to: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sample Data
|
||||
|
||||
struct TimelineDataItem: Identifiable {
|
||||
let id = UUID()
|
||||
let mood: Mood
|
||||
let date: Date
|
||||
let color: Color
|
||||
let image: Image
|
||||
}
|
||||
|
||||
private static func createSampleTimelineData(count: Int) -> [TimelineDataItem] {
|
||||
let moods: [Mood] = [.great, .good, .average, .good, .great, .average, .bad, .good, .great, .good]
|
||||
let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable()
|
||||
let moodImages: MoodImagable.Type = UserDefaultsStore.moodMoodImagable()
|
||||
|
||||
return (0..<count).map { index in
|
||||
let mood = moods[index % moods.count]
|
||||
let date = Calendar.current.date(byAdding: .day, value: -index, to: Date())!
|
||||
return TimelineDataItem(
|
||||
mood: mood,
|
||||
date: date,
|
||||
color: moodTint.color(forMood: mood),
|
||||
image: moodImages.icon(forMood: mood)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Widget Container View
|
||||
|
||||
private struct WidgetContainer<Content: View>: View {
|
||||
let size: WidgetExporter.WidgetSize
|
||||
let colorScheme: ColorScheme
|
||||
let content: Content
|
||||
var useSystemBackground: Bool = false
|
||||
|
||||
var body: some View {
|
||||
content
|
||||
.frame(width: size.pointSize.width, height: size.pointSize.height)
|
||||
.background(useSystemBackground ? Color(UIColor.systemBackground) : Color(UIColor.tertiarySystemFill))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
|
||||
.environment(\.colorScheme, colorScheme)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Export Voting View (static, no interactive intents)
|
||||
|
||||
private struct ExportVotingView: View {
|
||||
let isSmall: Bool
|
||||
|
||||
private var moodTint: MoodTintable.Type {
|
||||
UserDefaultsStore.moodTintable()
|
||||
}
|
||||
|
||||
private var moodImages: MoodImagable.Type {
|
||||
UserDefaultsStore.moodMoodImagable()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if isSmall {
|
||||
smallLayout
|
||||
} else {
|
||||
mediumLayout
|
||||
}
|
||||
}
|
||||
|
||||
private var smallLayout: some View {
|
||||
VStack(spacing: 8) {
|
||||
// Top row: Great, Good, Average
|
||||
HStack(spacing: 12) {
|
||||
ForEach([Mood.great, .good, .average], id: \.rawValue) { mood in
|
||||
moodIcon(for: mood, size: 40)
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom row: Bad, Horrible
|
||||
HStack(spacing: 12) {
|
||||
ForEach([Mood.bad, .horrible], id: \.rawValue) { mood in
|
||||
moodIcon(for: mood, size: 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
private var mediumLayout: some View {
|
||||
VStack(spacing: 12) {
|
||||
Text("How are you feeling?")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
||||
moodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 36, height: 36)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
|
||||
private func moodIcon(for mood: Mood, size: CGFloat) -> some View {
|
||||
moodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Export Voted Stats View
|
||||
|
||||
private struct ExportVotedStatsView: View {
|
||||
let mood: Mood
|
||||
let totalEntries: Int
|
||||
let isSmall: Bool
|
||||
|
||||
private var moodTint: MoodTintable.Type {
|
||||
UserDefaultsStore.moodTintable()
|
||||
}
|
||||
|
||||
private var moodImages: MoodImagable.Type {
|
||||
UserDefaultsStore.moodMoodImagable()
|
||||
}
|
||||
|
||||
private let moodCounts: [Mood: Int] = [.great: 45, .good: 42, .average: 18, .bad: 8, .horrible: 4]
|
||||
|
||||
var body: some View {
|
||||
if isSmall {
|
||||
smallLayout
|
||||
} else {
|
||||
mediumLayout
|
||||
}
|
||||
}
|
||||
|
||||
private var smallLayout: some View {
|
||||
VStack(spacing: 8) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
moodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 56, height: 56)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.headline)
|
||||
.foregroundColor(.green)
|
||||
.background(Circle().fill(.white).frame(width: 14, height: 14))
|
||||
.offset(x: 4, y: 4)
|
||||
}
|
||||
|
||||
Text("Today")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("\(totalEntries) day streak")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(12)
|
||||
}
|
||||
|
||||
private var mediumLayout: some View {
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
VStack(spacing: 6) {
|
||||
moodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 48, height: 48)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
|
||||
Text(mood.widgetDisplayName)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
|
||||
Text("Today")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("\(totalEntries) entries")
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
HStack(spacing: 6) {
|
||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { m in
|
||||
let count = moodCounts[m, default: 0]
|
||||
if count > 0 {
|
||||
HStack(spacing: 2) {
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: m))
|
||||
.frame(width: 8, height: 8)
|
||||
Text("\(count)")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GeometryReader { geo in
|
||||
HStack(spacing: 1) {
|
||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { m in
|
||||
let percentage = Double(moodCounts[m, default: 0]) / Double(totalEntries) * 100
|
||||
if percentage > 0 {
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(moodTint.color(forMood: m))
|
||||
.frame(width: max(4, geo.size.width * CGFloat(percentage) / 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 10)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Timeline Export Views
|
||||
|
||||
private struct TimelineSmallExportView: View {
|
||||
let timelineData: WidgetExporter.TimelineDataItem?
|
||||
let hasVoted: Bool
|
||||
|
||||
private var dayFormatter: DateFormatter {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "EEEE"
|
||||
return f
|
||||
}
|
||||
|
||||
private var dateFormatter: DateFormatter {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "MMM d"
|
||||
return f
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !hasVoted {
|
||||
ExportVotingView(isSmall: true)
|
||||
} else if let today = timelineData {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
today.image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 70, height: 70)
|
||||
.foregroundColor(today.color)
|
||||
|
||||
Spacer().frame(height: 12)
|
||||
|
||||
VStack(spacing: 2) {
|
||||
Text(dayFormatter.string(from: today.date))
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.textCase(.uppercase)
|
||||
|
||||
Text(dateFormatter.string(from: today.date))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct TimelineMediumExportView: View {
|
||||
let timelineData: [WidgetExporter.TimelineDataItem]
|
||||
let hasVoted: Bool
|
||||
|
||||
private var dayFormatter: DateFormatter {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "EEE"
|
||||
return f
|
||||
}
|
||||
|
||||
private var dateFormatter: DateFormatter {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "d"
|
||||
return f
|
||||
}
|
||||
|
||||
private var headerDateRange: String {
|
||||
guard let first = timelineData.first, let last = timelineData.last else { return "" }
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMM d"
|
||||
return "\(formatter.string(from: last.date)) - \(formatter.string(from: first.date))"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !hasVoted {
|
||||
ExportVotingView(isSmall: false)
|
||||
} else {
|
||||
GeometryReader { geo in
|
||||
let cellHeight = geo.size.height - 36
|
||||
|
||||
VStack(spacing: 4) {
|
||||
HStack {
|
||||
Text("Last 5 Days")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
Text("·")
|
||||
.foregroundStyle(.secondary)
|
||||
Text(headerDateRange)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.top, 10)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
ForEach(Array(timelineData.enumerated()), id: \.element.id) { index, item in
|
||||
ExportMediumDayCell(
|
||||
dayLabel: dayFormatter.string(from: item.date),
|
||||
dateLabel: dateFormatter.string(from: item.date),
|
||||
image: item.image,
|
||||
color: item.color,
|
||||
isToday: index == 0,
|
||||
height: cellHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ExportMediumDayCell: View {
|
||||
let dayLabel: String
|
||||
let dateLabel: String
|
||||
let image: Image
|
||||
let color: Color
|
||||
let isToday: Bool
|
||||
let height: CGFloat
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(color.opacity(isToday ? 0.25 : 0.12))
|
||||
.frame(height: height)
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text(dayLabel)
|
||||
.font(.caption2.weight(isToday ? .bold : .medium))
|
||||
.foregroundStyle(isToday ? .primary : .secondary)
|
||||
.textCase(.uppercase)
|
||||
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 36, height: 36)
|
||||
.foregroundColor(color)
|
||||
|
||||
Text(dateLabel)
|
||||
.font(.caption.weight(isToday ? .bold : .semibold))
|
||||
.foregroundStyle(isToday ? color : .secondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
private struct TimelineLargeExportView: View {
|
||||
let timelineData: [WidgetExporter.TimelineDataItem]
|
||||
let hasVoted: Bool
|
||||
|
||||
private var dayFormatter: DateFormatter {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "EEE"
|
||||
return f
|
||||
}
|
||||
|
||||
private var dateFormatter: DateFormatter {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "d"
|
||||
return f
|
||||
}
|
||||
|
||||
private var headerDateRange: String {
|
||||
guard let first = timelineData.first, let last = timelineData.last else { return "" }
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMM d"
|
||||
return "\(formatter.string(from: last.date)) - \(formatter.string(from: first.date))"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !hasVoted {
|
||||
ExportLargeVotingView()
|
||||
} else {
|
||||
GeometryReader { geo in
|
||||
let cellHeight = (geo.size.height - 70) / 2
|
||||
|
||||
VStack(spacing: 6) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Last 10 Days")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
Text(headerDateRange)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 8)
|
||||
|
||||
VStack(spacing: 6) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(Array(timelineData.prefix(5).enumerated()), id: \.element.id) { index, item in
|
||||
ExportDayCell(
|
||||
dayLabel: dayFormatter.string(from: item.date),
|
||||
dateLabel: dateFormatter.string(from: item.date),
|
||||
image: item.image,
|
||||
color: item.color,
|
||||
isToday: index == 0,
|
||||
height: cellHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 6) {
|
||||
ForEach(Array(timelineData.suffix(5).enumerated()), id: \.element.id) { _, item in
|
||||
ExportDayCell(
|
||||
dayLabel: dayFormatter.string(from: item.date),
|
||||
dateLabel: dateFormatter.string(from: item.date),
|
||||
image: item.image,
|
||||
color: item.color,
|
||||
isToday: false,
|
||||
height: cellHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ExportLargeVotingView: View {
|
||||
private var moodTint: MoodTintable.Type {
|
||||
UserDefaultsStore.moodTintable()
|
||||
}
|
||||
|
||||
private var moodImages: MoodImagable.Type {
|
||||
UserDefaultsStore.moodMoodImagable()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Spacer()
|
||||
|
||||
Text("How are you feeling?")
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
||||
moodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.padding(10)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(moodTint.color(forMood: mood).opacity(0.15))
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ExportDayCell: View {
|
||||
let dayLabel: String
|
||||
let dateLabel: String
|
||||
let image: Image
|
||||
let color: Color
|
||||
let isToday: Bool
|
||||
let height: CGFloat
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 2) {
|
||||
Text(dayLabel)
|
||||
.font(.caption2.weight(isToday ? .bold : .medium))
|
||||
.foregroundStyle(isToday ? .primary : .secondary)
|
||||
.textCase(.uppercase)
|
||||
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(color.opacity(isToday ? 0.25 : 0.12))
|
||||
.frame(height: height - 16)
|
||||
|
||||
VStack(spacing: 6) {
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 38, height: 38)
|
||||
.foregroundColor(color)
|
||||
|
||||
Text(dateLabel)
|
||||
.font(.caption.weight(isToday ? .bold : .semibold))
|
||||
.foregroundStyle(isToday ? color : .secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Export Live Activity View (Lock Screen)
|
||||
|
||||
private struct ExportLiveActivityView: View {
|
||||
let streak: Int
|
||||
let hasLoggedToday: Bool
|
||||
let mood: Mood?
|
||||
|
||||
private var moodTint: MoodTintable.Type {
|
||||
UserDefaultsStore.moodTintable()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
// Streak indicator
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: "flame.fill")
|
||||
.font(.title)
|
||||
.foregroundColor(.orange)
|
||||
Text("\(streak)")
|
||||
.font(.title.bold())
|
||||
Text("day streak")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.frame(height: 50)
|
||||
|
||||
// Status
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if hasLoggedToday, let mood = mood {
|
||||
HStack(spacing: 8) {
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: mood))
|
||||
.frame(width: 24, height: 24)
|
||||
VStack(alignment: .leading) {
|
||||
Text("Today's mood")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(mood.widgetDisplayName)
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Don't break your streak!")
|
||||
.font(.headline)
|
||||
Text("Tap to log your mood")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -19,6 +19,8 @@ struct SettingsContentView: View {
|
||||
@State private var showReminderTimePicker = false
|
||||
@State private var showSubscriptionStore = false
|
||||
@State private var showTrialDatePicker = false
|
||||
@State private var isExportingWidgets = false
|
||||
@State private var widgetExportPath: URL?
|
||||
@StateObject private var healthService = HealthService.shared
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
@@ -58,7 +60,9 @@ struct SettingsContentView: View {
|
||||
animationLabButton
|
||||
paywallPreviewButton
|
||||
tipsPreviewButton
|
||||
|
||||
testNotificationsButton
|
||||
exportWidgetsButton
|
||||
|
||||
clearDataButton
|
||||
#endif
|
||||
|
||||
@@ -382,6 +386,95 @@ struct SettingsContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var testNotificationsButton: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button {
|
||||
LocalNotification.sendAllPersonalityNotificationsForScreenshot()
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "bell.badge.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(.red)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Test All Notifications")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text("Send 5 personality pack notifications")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var exportWidgetsButton: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button {
|
||||
isExportingWidgets = true
|
||||
Task {
|
||||
widgetExportPath = await WidgetExporter.exportAllWidgets()
|
||||
isExportingWidgets = false
|
||||
// Open Files app to the export location
|
||||
if let path = widgetExportPath {
|
||||
// Show share sheet or alert with path
|
||||
print("📸 Widgets exported to: \(path.path)")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
if isExportingWidgets {
|
||||
ProgressView()
|
||||
.frame(width: 32)
|
||||
} else {
|
||||
Image(systemName: "square.grid.2x2.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(.purple)
|
||||
.frame(width: 32)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Export Widget Screenshots")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
if let path = widgetExportPath {
|
||||
Text("Saved to Documents/WidgetExports")
|
||||
.font(.caption)
|
||||
.foregroundColor(.green)
|
||||
} else {
|
||||
Text("Light & dark mode PNGs")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "arrow.down.doc.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.disabled(isExportingWidgets)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var clearDataButton: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
BIN
docs/AppStoreScreens.pxd
Normal file
94
docs/competitors/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Competitor Analysis - Mood Tracking Apps
|
||||
|
||||
Analysis of top 5 mood tracking app competitors on the iOS App Store.
|
||||
|
||||
---
|
||||
|
||||
## Overview Comparison
|
||||
|
||||
| App | Rating | Reviews | Price | Differentiator |
|
||||
|-----|--------|---------|-------|----------------|
|
||||
| [Daylio](./daylio/) | 4.8 | 57K | Free + IAP | No-typing micro-diary |
|
||||
| [Reflectly](./reflectly/) | 4.6 | 82K | Free + IAP | AI-powered journaling |
|
||||
| [Moodnotes](./moodnotes/) | 4.7 | 11K | Free + IAP | CBT-based, clinical psychology |
|
||||
| [Moodistory](./moodistory/) | 4.8 | 846 | Free + IAP | Privacy-first, lightweight |
|
||||
| [Bearable](./bearable/) | 4.8 | 4.7K | Free + IAP | Comprehensive health/symptom tracking |
|
||||
|
||||
---
|
||||
|
||||
## App Store URLs
|
||||
|
||||
1. **Daylio:** https://apps.apple.com/us/app/daylio-journal-daily-diary/id1194023242
|
||||
2. **Reflectly:** https://apps.apple.com/us/app/reflectly-journal-ai-diary/id1241229134
|
||||
3. **Moodnotes:** https://apps.apple.com/us/app/moodnotes-mood-tracker/id1019230398
|
||||
4. **Moodistory:** https://apps.apple.com/us/app/emotion-tracker-moodistory/id1335347860
|
||||
5. **Bearable:** https://apps.apple.com/us/app/bearable-symptom-tracker/id1482581097
|
||||
|
||||
---
|
||||
|
||||
## Market Positioning
|
||||
|
||||
### By Review Count (Market Size Indicator)
|
||||
1. Reflectly - 82K reviews (largest user base)
|
||||
2. Daylio - 57K reviews
|
||||
3. Moodnotes - 11K reviews
|
||||
4. Bearable - 4.7K reviews
|
||||
5. Moodistory - 846 reviews (indie)
|
||||
|
||||
### By Approach
|
||||
- **Quick Logging:** Daylio, Moodistory (tap-based, no typing)
|
||||
- **AI/Smart:** Reflectly (AI-driven prompts)
|
||||
- **Clinical/CBT:** Moodnotes (psychology-backed)
|
||||
- **Health Focused:** Bearable (chronic illness, symptoms)
|
||||
|
||||
### By Price Point (Monthly)
|
||||
- Moodnotes: $14.99/mo (highest)
|
||||
- Reflectly: $9.99/mo
|
||||
- Bearable: $6.99/mo
|
||||
- Daylio: Variable tiers
|
||||
- Moodistory: Variable tiers (lowest)
|
||||
|
||||
---
|
||||
|
||||
## Feature Comparison
|
||||
|
||||
| Feature | Daylio | Reflectly | Moodnotes | Moodistory | Bearable |
|
||||
|---------|--------|-----------|-----------|------------|----------|
|
||||
| Quick tap logging | Yes | No | No | Yes | Yes |
|
||||
| AI features | No | Yes | No | No | No |
|
||||
| CBT-based | No | Yes | Yes | No | No |
|
||||
| Apple Watch | Yes | No | Yes | Yes | No |
|
||||
| Apple Health | Yes | No | No | Yes | Yes |
|
||||
| Year in Pixels | Yes | No | No | Yes | No |
|
||||
| Privacy focus | Medium | Low | Medium | High | Medium |
|
||||
| Widgets | Yes | Yes | No | Yes | No |
|
||||
| Voice input | Yes | Yes | No | No | No |
|
||||
| Export data | Yes | Yes | Yes | Yes | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Downloaded Assets
|
||||
|
||||
### Screenshots Available
|
||||
- Moodnotes: 7 screenshots
|
||||
- Moodistory: 10 screenshots
|
||||
- Reflectly: 8 screenshots
|
||||
|
||||
### App Icons Only (Screenshots not via API)
|
||||
- Daylio
|
||||
- Bearable
|
||||
|
||||
---
|
||||
|
||||
## Key Takeaways for Feels
|
||||
|
||||
1. **Quick logging is valued** - Daylio and Moodistory emphasize speed
|
||||
2. **Privacy is a differentiator** - Moodistory's privacy-first approach stands out
|
||||
3. **AI is trending** - Reflectly's AI positioning attracts users
|
||||
4. **Visual insights matter** - Year in Pixels and calendars are common
|
||||
5. **Apple ecosystem integration** - Watch, Health, Widgets are expected
|
||||
6. **CBT/psychology backing** - Adds credibility for mental health positioning
|
||||
|
||||
---
|
||||
|
||||
*Analysis conducted January 2026*
|
||||
114
docs/competitors/bearable/README.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Bearable - Symptom Tracker
|
||||
|
||||
## App Store Information
|
||||
|
||||
**App Store URL:** https://apps.apple.com/us/app/bearable-symptom-tracker/id1482581097
|
||||
|
||||
**App ID:** 1482581097
|
||||
|
||||
---
|
||||
|
||||
## Basic Details
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Title** | Bearable - Symptom Tracker |
|
||||
| **Subtitle** | Mood, Health, Migraine, Period |
|
||||
| **Developer** | Bearable Ltd |
|
||||
| **Category** | Health & Fitness |
|
||||
| **Age Rating** | 4+ |
|
||||
| **Price** | Free (with In-App Purchases) |
|
||||
|
||||
---
|
||||
|
||||
## Ratings & Reviews
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| **Average Rating** | 4.8 / 5 |
|
||||
| **Total Reviews** | 4,679 |
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
| Spec | Value |
|
||||
|------|-------|
|
||||
| **Version** | 1.0.579 |
|
||||
| **Size** | 102.6 MB |
|
||||
| **iOS Requirement** | iOS 15.1 or later |
|
||||
| **Languages** | English only |
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Feeling overwhelmed by your health? Bearable can help you feel more in control of your symptoms, flare-ups, and overall well-being.
|
||||
|
||||
Join over 900,000 people securely managing their Chronic Illnesses, Migraine, Pain, Headaches, Fatigue, IBS, PoTS, PCOS, EDS, Period, Mood, Mental Health and more - with Bearable.
|
||||
|
||||
Bearable helps you discover and manage triggers for changes in your health. Our simple, customizable tracking tools make it easy to understand the correlation between anything you do and how you feel.
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
- Symptom and mood tracking
|
||||
- Medication and treatment logging
|
||||
- Pain and headache monitoring
|
||||
- Period and PCOS cycle tracking
|
||||
- PoTS and EDS flare-up documentation
|
||||
- Apple Health and Fitbit integration
|
||||
- Data export functionality
|
||||
- Customizable reminders
|
||||
- Dark mode support
|
||||
- Encrypted, secure data storage
|
||||
- Correlation analysis between activities and symptoms
|
||||
|
||||
---
|
||||
|
||||
## Pricing Options
|
||||
|
||||
| Plan | Price |
|
||||
|------|-------|
|
||||
| Monthly | $6.99 |
|
||||
| Yearly | $34.99 |
|
||||
| Various other tiers | $4.49 - $49.99 |
|
||||
|
||||
---
|
||||
|
||||
## What's New (v1.0.579 - Dec 2025)
|
||||
|
||||
Bug fixes and quality-of-life design improvements.
|
||||
|
||||
---
|
||||
|
||||
## Assets
|
||||
|
||||
- `app_icon.jpg` - App icon (512x512)
|
||||
- Screenshots not available via API
|
||||
|
||||
---
|
||||
|
||||
## Competitive Analysis Notes
|
||||
|
||||
**Strengths:**
|
||||
- Comprehensive health tracking beyond just mood
|
||||
- 900,000+ user base
|
||||
- Strong chronic illness community focus
|
||||
- Correlation analysis between factors
|
||||
- Created by people with chronic conditions
|
||||
|
||||
**Target Audience:**
|
||||
- Users with chronic illnesses (migraine, IBS, fibromyalgia, etc.)
|
||||
- People tracking medication effectiveness
|
||||
- Users needing symptom-to-activity correlation
|
||||
- Healthcare provider communication needs
|
||||
|
||||
**Differentiators:**
|
||||
- Broader health scope (not just mood)
|
||||
- Chronic illness specific features
|
||||
- Medical conversation support
|
||||
- Symptom correlation algorithms
|
||||
|
||||
**Note:** This is more of a health/symptom tracker than pure mood tracker - different market positioning.
|
||||
BIN
docs/competitors/bearable/app_icon.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
105
docs/competitors/daylio/README.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Daylio Journal - Daily Diary
|
||||
|
||||
## App Store Information
|
||||
|
||||
**App Store URL:** https://apps.apple.com/us/app/daylio-journal-daily-diary/id1194023242
|
||||
|
||||
**App ID:** 1194023242
|
||||
|
||||
---
|
||||
|
||||
## Basic Details
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Title** | Daylio Journal - Daily Diary |
|
||||
| **Subtitle** | Mood Tracker, Health, Habits |
|
||||
| **Developer** | Relaxio s.r.o. |
|
||||
| **Category** | Lifestyle |
|
||||
| **Age Rating** | 4+ |
|
||||
| **Price** | Free (with In-App Purchases) |
|
||||
|
||||
---
|
||||
|
||||
## Ratings & Reviews
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| **Average Rating** | 4.8 / 5 |
|
||||
| **Total Reviews** | 57,048 |
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
| Spec | Value |
|
||||
|------|-------|
|
||||
| **Version** | 1.69.2 |
|
||||
| **Size** | 284.6 MB |
|
||||
| **iOS Requirement** | iOS 14.0 or later |
|
||||
| **Languages** | 30 languages |
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Self-Care Bullet Journal with Goals - Mood Diary & Happiness Tracker
|
||||
|
||||
Daylio enables you to keep a private journal without having to type a single line. Try this beautifully designed & stunningly simple micro-diary app right now for FREE!
|
||||
|
||||
### What is Daylio
|
||||
|
||||
Daylio is a very versatile app, and you can turn it in whatever you need to track. Your fitness goal pal. Your mental health coach. Your food log. Your gratitude diary. Mood tracker. Exercise, meditate, eat, and be grateful. Take care of yourself.
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
- Multiple daily mood entries
|
||||
- Customizable activities and moods with emoji support
|
||||
- Weekly, monthly, yearly analytics
|
||||
- "Year in Pixels" visualization
|
||||
- Dark mode and color theme customization
|
||||
- Goal and habit tracking
|
||||
- Voice memo recording
|
||||
- Activity widgets
|
||||
- PDF/CSV export capability
|
||||
- PIN lock security
|
||||
- iCloud backup integration
|
||||
- Apple Health data support
|
||||
|
||||
---
|
||||
|
||||
## Pricing Options
|
||||
|
||||
| Plan | Price |
|
||||
|------|-------|
|
||||
| Various Premium Tiers | $4.99, $17.99, $23.99, $35.99, $59.99 |
|
||||
|
||||
---
|
||||
|
||||
## What's New (v1.69.2 - Dec 2025)
|
||||
|
||||
"Fresh new look" with new themes and colorful emojis that match your vibe.
|
||||
|
||||
---
|
||||
|
||||
## Assets
|
||||
|
||||
- `app_icon.jpg` - App icon (512x512)
|
||||
- Screenshots not available via API
|
||||
|
||||
---
|
||||
|
||||
## Competitive Analysis Notes
|
||||
|
||||
**Strengths:**
|
||||
- Highest review count in category (57K+)
|
||||
- No typing required - tap-based logging
|
||||
- Extensive customization options
|
||||
- Year in Pixels visualization is unique
|
||||
|
||||
**Target Audience:**
|
||||
- Users who want quick, low-effort mood logging
|
||||
- People tracking habits alongside moods
|
||||
- Users who prefer visual over text-based journaling
|
||||
BIN
docs/competitors/daylio/app_icon.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
1236
docs/competitors/index.html
Normal file
106
docs/competitors/moodistory/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Emotion Tracker: Moodistory
|
||||
|
||||
## App Store Information
|
||||
|
||||
**App Store URL:** https://apps.apple.com/us/app/emotion-tracker-moodistory/id1335347860
|
||||
|
||||
**App ID:** 1335347860
|
||||
|
||||
---
|
||||
|
||||
## Basic Details
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Title** | Emotion Tracker: Moodistory |
|
||||
| **Subtitle** | Mood Journal & Mental Health |
|
||||
| **Developer** | Christoph Matzka |
|
||||
| **Category** | Health & Fitness |
|
||||
| **Age Rating** | 9+ |
|
||||
| **Price** | Free (with In-App Purchases) |
|
||||
|
||||
---
|
||||
|
||||
## Ratings & Reviews
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| **Average Rating** | 4.8 / 5 |
|
||||
| **Total Reviews** | 846 |
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
| Spec | Value |
|
||||
|------|-------|
|
||||
| **Version** | 3.5 |
|
||||
| **Size** | 35.8 MB |
|
||||
| **iOS Requirement** | iOS 16+ |
|
||||
| **Languages** | 8 languages (EN, FR, DE, JA, KO, RU, ZH, ES) |
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Moodistory is a low-effort mood tracker with a unique and beautiful design that respects 100% your privacy. Create journal entries in less than 5 seconds, without writing a single word. Use the integrated mood calendar (or year in pixels view) to easily find mood patterns. Become aware of your mood highs and lows and analyze the cause of mood swings. Better manage anxiety, discover triggers for a positive mood, and ultimately establish awareness about your mental health.
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
- 180+ customizable events/activities across 10 categories
|
||||
- Interactive mood calendars (yearly, monthly, daily views)
|
||||
- Apple Health integration for sleep, daylight, exercise tracking
|
||||
- Face ID/Touch ID/Passcode security
|
||||
- PDF and CSV export functionality
|
||||
- 5 home screen widgets
|
||||
- Apple Watch app support
|
||||
- Custom mood scales (2-11 point range)
|
||||
- iCloud and manual backup options
|
||||
- Customizable color themes
|
||||
- **100% privacy focused - data stays on device**
|
||||
|
||||
---
|
||||
|
||||
## Pricing Options
|
||||
|
||||
| Plan | Price |
|
||||
|------|-------|
|
||||
| Various Premium Tiers | $2.99, $6.99, $14.99, $19.99, $39.99 |
|
||||
|
||||
---
|
||||
|
||||
## What's New (v3.5 - Oct 2025)
|
||||
|
||||
Added Apple Health connectivity for analyzing mood influences from sleep, daylight, exercise, steps, and active energy.
|
||||
|
||||
---
|
||||
|
||||
## Assets
|
||||
|
||||
- `app_icon.jpg` - App icon (512x512)
|
||||
- `screenshot_1.png` through `screenshot_10.png` - App Store screenshots
|
||||
|
||||
---
|
||||
|
||||
## Competitive Analysis Notes
|
||||
|
||||
**Strengths:**
|
||||
- Smallest app size (35.8 MB) - very lightweight
|
||||
- Strong privacy focus (data stays on device)
|
||||
- Beautiful, unique design
|
||||
- Indie developer (personal touch)
|
||||
- Extensive Apple Health integration
|
||||
|
||||
**Target Audience:**
|
||||
- Privacy-conscious users
|
||||
- Users who want quick, no-typing logging
|
||||
- Apple ecosystem users (Watch, Health integration)
|
||||
- Users who prefer lightweight apps
|
||||
|
||||
**Differentiators:**
|
||||
- Privacy-first approach (no external servers)
|
||||
- 5-second logging claim
|
||||
- Year in pixels visualization
|
||||
- Custom mood scales (2-11 points)
|
||||
BIN
docs/competitors/moodistory/app_icon.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/competitors/moodistory/screenshot_1.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/competitors/moodistory/screenshot_10.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
docs/competitors/moodistory/screenshot_2.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
docs/competitors/moodistory/screenshot_3.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
docs/competitors/moodistory/screenshot_4.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
docs/competitors/moodistory/screenshot_5.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/competitors/moodistory/screenshot_6.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
docs/competitors/moodistory/screenshot_7.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
docs/competitors/moodistory/screenshot_8.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/competitors/moodistory/screenshot_9.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
107
docs/competitors/moodnotes/README.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Moodnotes - Mood Tracker
|
||||
|
||||
## App Store Information
|
||||
|
||||
**App Store URL:** https://apps.apple.com/us/app/moodnotes-mood-tracker/id1019230398
|
||||
|
||||
**App ID:** 1019230398
|
||||
|
||||
---
|
||||
|
||||
## Basic Details
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Title** | Moodnotes - Mood Tracker |
|
||||
| **Subtitle** | Daily Self Care Journal & CBT |
|
||||
| **Developer** | Mosaic S.r.l. |
|
||||
| **Category** | Health & Fitness |
|
||||
| **Age Rating** | 13+ |
|
||||
| **Price** | Free (with In-App Purchases) |
|
||||
|
||||
---
|
||||
|
||||
## Ratings & Reviews
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| **Average Rating** | 4.7 / 5 |
|
||||
| **Total Reviews** | 10,942 |
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
| Spec | Value |
|
||||
|------|-------|
|
||||
| **Version** | 3.14.8 |
|
||||
| **Size** | 153.5 MB |
|
||||
| **iOS Requirement** | iOS 13.0 or later |
|
||||
| **Languages** | 8 languages (EN, FR, DE, IT, RU, ZH, ES, SV) |
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Don't let your mood affect your life. Take control over it!
|
||||
|
||||
Meet Moodnotes - a super easy mood tracker & journaling app to capture your mood and help you improve your thinking habits. Moodnotes empowers you to track your mood over time, avoid common thinking traps, and develop perspectives associated with increased happiness and well-being.
|
||||
|
||||
Created by design experts and clinical psychologists (creators of MoodKit), Moodnotes is grounded in the scientifically-supported content of cognitive behavior therapy and positive psychology.
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
- Mood tracking with emotion identification
|
||||
- Automatic face scanning for mood detection
|
||||
- Photo/memory attachment capability
|
||||
- Expert-written mental health articles
|
||||
- Thinking pattern recognition ("traps")
|
||||
- Anxiety reduction tools
|
||||
- Apple Watch compatibility
|
||||
- iCloud sync/backup
|
||||
- Premium: unlimited entries, statistics, insights, enhanced articles
|
||||
|
||||
---
|
||||
|
||||
## Pricing Options
|
||||
|
||||
| Plan | Price |
|
||||
|------|-------|
|
||||
| Monthly | $14.99 |
|
||||
| Yearly | $29.99 - $89.99 |
|
||||
|
||||
---
|
||||
|
||||
## What's New (v3.14.8 - Oct 2024)
|
||||
|
||||
Bug fixes and performance improvements.
|
||||
|
||||
---
|
||||
|
||||
## Assets
|
||||
|
||||
- `app_icon.jpg` - App icon (512x512)
|
||||
- `screenshot_1.jpg` through `screenshot_7.jpg` - App Store screenshots
|
||||
|
||||
---
|
||||
|
||||
## Competitive Analysis Notes
|
||||
|
||||
**Strengths:**
|
||||
- CBT-based approach (clinical psychology foundation)
|
||||
- Face scanning for automatic mood detection
|
||||
- Created by clinical psychologists
|
||||
- Strong mental health article content
|
||||
|
||||
**Target Audience:**
|
||||
- Users interested in CBT/therapy approaches
|
||||
- People wanting to understand thinking patterns
|
||||
- Users dealing with anxiety
|
||||
- Those who want science-backed mood tracking
|
||||
|
||||
**Differentiators:**
|
||||
- "Thinking traps" identification
|
||||
- Clinical psychology pedigree
|
||||
- Face scanning technology
|
||||
BIN
docs/competitors/moodnotes/app_icon.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/competitors/moodnotes/screenshot_1.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/competitors/moodnotes/screenshot_2.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/competitors/moodnotes/screenshot_3.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/competitors/moodnotes/screenshot_4.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/competitors/moodnotes/screenshot_5.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/competitors/moodnotes/screenshot_6.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/competitors/moodnotes/screenshot_7.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
113
docs/competitors/reflectly/README.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Reflectly - Journal & AI Diary
|
||||
|
||||
## App Store Information
|
||||
|
||||
**App Store URL:** https://apps.apple.com/us/app/reflectly-journal-ai-diary/id1241229134
|
||||
|
||||
**App ID:** 1241229134
|
||||
|
||||
---
|
||||
|
||||
## Basic Details
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Title** | Reflectly - Journal & AI Diary |
|
||||
| **Subtitle** | Mood Tracker & Daily Quotes |
|
||||
| **Developer** | Kodeon, Inc. |
|
||||
| **Category** | Health & Fitness |
|
||||
| **Age Rating** | 4+ |
|
||||
| **Price** | Free (with In-App Purchases) |
|
||||
|
||||
---
|
||||
|
||||
## Ratings & Reviews
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| **Average Rating** | 4.6 / 5 |
|
||||
| **Total Reviews** | 81,727 |
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
| Spec | Value |
|
||||
|------|-------|
|
||||
| **Version** | 4.15.0 |
|
||||
| **Size** | 72.3 MB |
|
||||
| **iOS Requirement** | iOS 15.0 or later |
|
||||
| **Languages** | English |
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Reflectly is the #1 journaling app that's like your best friend. Vent your thoughts & feelings to improve your mood and practice mindfulness. Write down how you feel each day in your own mood diary. It's the world's first intelligent journal app & mood tracker that gives you personalized motivation and prompts the more you use it.
|
||||
|
||||
### The Best Journal App for Self-Care and Mindfulness
|
||||
|
||||
How you're feeling on a daily basis matters. Reflectly is a personal journal driven by AI to help you deal with negative thoughts and increase positivity. It uses positive psychology, mindfulness, and cognitive behavioral therapy (CBT) to help you thrive.
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
- Daily mood tracking with correlations and graphs
|
||||
- Personalized journaling prompts and reminders
|
||||
- Mood diary entries with voice-to-text capability
|
||||
- Daily, weekly, and monthly insights
|
||||
- Habit tracker with achievement system
|
||||
- Advanced statistics (premium feature)
|
||||
- Morning motivation quotes and evening reflections
|
||||
- Previous entry editing and review
|
||||
- Lock screen widgets (iOS 16+)
|
||||
- AI-driven personalized prompts
|
||||
|
||||
---
|
||||
|
||||
## Pricing Options
|
||||
|
||||
| Plan | Price |
|
||||
|------|-------|
|
||||
| Monthly | $9.99 |
|
||||
| Yearly | $59.99 |
|
||||
|
||||
---
|
||||
|
||||
## What's New (v4.15.0 - Nov 2025)
|
||||
|
||||
Bug fixes and performance improvements.
|
||||
|
||||
---
|
||||
|
||||
## Assets
|
||||
|
||||
- `app_icon.jpg` - App icon (512x512)
|
||||
- `screenshot_1.jpg` through `screenshot_8.jpg` - App Store screenshots
|
||||
|
||||
---
|
||||
|
||||
## Competitive Analysis Notes
|
||||
|
||||
**Strengths:**
|
||||
- Highest review count (81K+) - massive user base
|
||||
- AI-powered personalization
|
||||
- "World's first intelligent journal" positioning
|
||||
- Strong marketing/branding ("like your best friend")
|
||||
- Part of Growth Bundle (cross-app ecosystem)
|
||||
|
||||
**Target Audience:**
|
||||
- Users who prefer guided journaling
|
||||
- People wanting AI-driven insights
|
||||
- Mindfulness/self-care focused users
|
||||
- Those who like motivational content
|
||||
|
||||
**Differentiators:**
|
||||
- AI-powered prompts and personalization
|
||||
- "Best friend" emotional positioning
|
||||
- Voice-to-text journaling
|
||||
- Growth Bundle ecosystem integration
|
||||
- Lock screen widgets
|
||||
|
||||
**Pricing Note:** Most expensive monthly option ($9.99/mo) but positions as premium AI experience.
|
||||
BIN
docs/competitors/reflectly/app_icon.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/competitors/reflectly/screenshot_1.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/competitors/reflectly/screenshot_2.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
docs/competitors/reflectly/screenshot_3.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/competitors/reflectly/screenshot_4.jpg
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
docs/competitors/reflectly/screenshot_5.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/competitors/reflectly/screenshot_6.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/competitors/reflectly/screenshot_7.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/competitors/reflectly/screenshot_8.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |