Initial FeelAloud project

This commit is contained in:
MrDiderot
2026-07-22 22:18:33 +02:00
commit 10fca0756d
48 changed files with 8942 additions and 0 deletions

92
Moment/AppIntent.swift Normal file
View File

@@ -0,0 +1,92 @@
import AppIntents
import Foundation
import WidgetKit
enum MomentRatingValue: String, AppEnum {
case veryGood = "Sehr gut"
case good = "Gut"
case middle = "Mittel"
case bad = "Schlecht"
case veryBad = "Sehr schlecht"
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Stimmung")
static var caseDisplayRepresentations: [MomentRatingValue: DisplayRepresentation] = [
.veryGood: "Sehr gut",
.good: "Gut",
.middle: "Mittel",
.bad: "Schlecht",
.veryBad: "Sehr schlecht"
]
var emoji: String {
switch self {
case .veryGood: "😄"
case .good: "🙂"
case .middle: "😐"
case .bad: "🙁"
case .veryBad: "😞"
}
}
}
struct TrackMomentIntent: AppIntent {
static var title: LocalizedStringResource = "Momentaufnahme speichern"
static var description = IntentDescription("Speichert eine schnelle Stimmungsabfrage aus dem Homescreen-Widget.")
static var openAppWhenRun = true
@Parameter(title: "Stimmung")
var rating: MomentRatingValue
init() {
rating = .middle
}
init(rating: MomentRatingValue) {
self.rating = rating
}
func perform() async throws -> some IntentResult {
MomentWidgetStorage.enqueue(ratingRawValue: rating.rawValue)
WidgetCenter.shared.reloadTimelines(ofKind: Moment.kind)
return .result()
}
}
enum MomentWidgetStorage {
static let appGroupIdentifier = "group.de.feelaloud"
static let pendingSnapshotsKey = "pendingWidgetSnapshots"
static let lastRatingKey = "lastWidgetSnapshotRating"
static let lastRecordedAtKey = "lastWidgetSnapshotDate"
static var defaults: UserDefaults {
UserDefaults(suiteName: appGroupIdentifier) ?? .standard
}
static func enqueue(ratingRawValue: String) {
let defaults = defaults
var snapshots = pendingSnapshots(from: defaults)
snapshots.append(PendingWidgetSnapshot(ratingRawValue: ratingRawValue, createdAt: .now))
if let data = try? JSONEncoder().encode(snapshots) {
defaults.set(data, forKey: pendingSnapshotsKey)
}
defaults.set(ratingRawValue, forKey: lastRatingKey)
defaults.set(Date.now, forKey: lastRecordedAtKey)
}
private static func pendingSnapshots(from defaults: UserDefaults) -> [PendingWidgetSnapshot] {
guard let data = defaults.data(forKey: pendingSnapshotsKey),
let snapshots = try? JSONDecoder().decode([PendingWidgetSnapshot].self, from: data) else {
return []
}
return snapshots
}
}
struct PendingWidgetSnapshot: Codable {
var ratingRawValue: String
var createdAt: Date
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

11
Moment/Info.plist Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

134
Moment/Moment.swift Normal file
View File

@@ -0,0 +1,134 @@
import AppIntents
import Foundation
import SwiftUI
import WidgetKit
struct MomentProvider: TimelineProvider {
func placeholder(in context: Context) -> MomentEntry {
MomentEntry(date: .now, lastRating: nil, lastRecordedAt: nil)
}
func getSnapshot(in context: Context, completion: @escaping (MomentEntry) -> Void) {
completion(currentEntry())
}
func getTimeline(in context: Context, completion: @escaping (Timeline<MomentEntry>) -> Void) {
completion(Timeline(entries: [currentEntry()], policy: .never))
}
private func currentEntry() -> MomentEntry {
let defaults = MomentWidgetStorage.defaults
let lastRating = defaults.string(forKey: MomentWidgetStorage.lastRatingKey)
.flatMap(MomentRatingValue.init(rawValue:))
let lastRecordedAt = defaults.object(forKey: MomentWidgetStorage.lastRecordedAtKey) as? Date
return MomentEntry(date: .now, lastRating: lastRating, lastRecordedAt: lastRecordedAt)
}
}
struct MomentEntry: TimelineEntry {
let date: Date
let lastRating: MomentRatingValue?
let lastRecordedAt: Date?
}
struct MomentEntryView: View {
let entry: MomentEntry
private let ratings: [MomentRatingValue] = [
.veryGood,
.good,
.middle,
.bad,
.veryBad
]
var body: some View {
VStack(alignment: .leading, spacing: 14) {
HStack(alignment: .firstTextBaseline) {
Text("Wie geht es dir?")
.font(.system(size: 18, weight: .bold))
.foregroundStyle(.white)
.lineLimit(1)
.minimumScaleFactor(0.82)
.shadow(color: .black.opacity(0.12), radius: 2, y: 1)
Spacer(minLength: 6)
if showsSavedState {
Text("Gespeichert")
.font(.system(size: 10, weight: .bold))
.foregroundStyle(.white)
.lineLimit(1)
.minimumScaleFactor(0.75)
}
}
HStack(spacing: 7) {
ForEach(ratings, id: \.self) { rating in
Button(intent: TrackMomentIntent(rating: rating)) {
ZStack(alignment: .topTrailing) {
Text(rating.emoji)
.font(.system(size: entry.lastRating == rating ? 28 : 25, weight: .bold))
.frame(maxWidth: .infinity)
.frame(height: 42)
.background(buttonBackground(for: rating), in: RoundedRectangle(cornerRadius: 8))
if showsSavedState, entry.lastRating == rating {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 12, weight: .bold))
.foregroundStyle(.white, Color.green)
.offset(x: 4, y: -4)
}
}
}
.buttonStyle(.plain)
.accessibilityLabel(Text(rating.rawValue))
}
}
}
.padding(14)
.containerBackground(for: .widget) {
LinearGradient(
colors: [
Color(red: 0.82, green: 0.90, blue: 0.76),
Color(red: 0.66, green: 0.78, blue: 0.60)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
}
private var showsSavedState: Bool {
guard let lastRecordedAt = entry.lastRecordedAt else { return false }
return Date.now.timeIntervalSince(lastRecordedAt) < 90
}
private func buttonBackground(for rating: MomentRatingValue) -> Color {
guard showsSavedState, entry.lastRating == rating else {
return .white.opacity(0.68)
}
return .white.opacity(0.92)
}
}
struct Moment: Widget {
nonisolated static let kind = "Moment"
var body: some WidgetConfiguration {
StaticConfiguration(kind: Self.kind, provider: MomentProvider()) { entry in
MomentEntryView(entry: entry)
}
.configurationDisplayName("Momentaufnahme")
.description("Tracke deine Stimmung direkt vom Homescreen.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
#Preview(as: .systemSmall) {
Moment()
} timeline: {
MomentEntry(date: .now, lastRating: .good, lastRecordedAt: .now)
}

16
Moment/MomentBundle.swift Normal file
View File

@@ -0,0 +1,16 @@
//
// MomentBundle.swift
// Moment
//
// Created by Tom Misch on 22.07.26.
//
import WidgetKit
import SwiftUI
@main
struct MomentBundle: WidgetBundle {
var body: some Widget {
Moment()
}
}

View File

@@ -0,0 +1,77 @@
//
// MomentControl.swift
// Moment
//
// Created by Tom Misch on 22.07.26.
//
import AppIntents
import SwiftUI
import WidgetKit
struct MomentControl: ControlWidget {
static let kind: String = "de.feelaloud.Moment"
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
kind: Self.kind,
provider: Provider()
) { value in
ControlWidgetToggle(
"Start Timer",
isOn: value.isRunning,
action: StartTimerIntent(value.name)
) { isRunning in
Label(isRunning ? "On" : "Off", systemImage: "timer")
}
}
.displayName("Timer")
.description("A an example control that runs a timer.")
}
}
extension MomentControl {
struct Value {
var isRunning: Bool
var name: String
}
struct Provider: AppIntentControlValueProvider {
func previewValue(configuration: TimerConfiguration) -> Value {
MomentControl.Value(isRunning: false, name: configuration.timerName)
}
func currentValue(configuration: TimerConfiguration) async throws -> Value {
let isRunning = true // Check if the timer is running
return MomentControl.Value(isRunning: isRunning, name: configuration.timerName)
}
}
}
struct TimerConfiguration: ControlConfigurationIntent {
static let title: LocalizedStringResource = "Timer Name Configuration"
@Parameter(title: "Timer Name", default: "Timer")
var timerName: String
}
struct StartTimerIntent: SetValueIntent {
static let title: LocalizedStringResource = "Start a timer"
@Parameter(title: "Timer Name")
var name: String
@Parameter(title: "Timer is running")
var value: Bool
init() {}
init(_ name: String) {
self.name = name
}
func perform() async throws -> some IntentResult {
// Start the timer
return .result()
}
}

View File

@@ -0,0 +1,80 @@
//
// MomentLiveActivity.swift
// Moment
//
// Created by Tom Misch on 22.07.26.
//
import ActivityKit
import WidgetKit
import SwiftUI
struct MomentAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Dynamic stateful properties about your activity go here!
var emoji: String
}
// Fixed non-changing properties about your activity go here!
var name: String
}
struct MomentLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: MomentAttributes.self) { context in
// Lock screen/banner UI goes here
VStack {
Text("Hello \(context.state.emoji)")
}
.activityBackgroundTint(Color.cyan)
.activitySystemActionForegroundColor(Color.black)
} dynamicIsland: { context in
DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through
// various regions, like leading/trailing/center/bottom
DynamicIslandExpandedRegion(.leading) {
Text("Leading")
}
DynamicIslandExpandedRegion(.trailing) {
Text("Trailing")
}
DynamicIslandExpandedRegion(.bottom) {
Text("Bottom \(context.state.emoji)")
// more content
}
} compactLeading: {
Text("L")
} compactTrailing: {
Text("T \(context.state.emoji)")
} minimal: {
Text(context.state.emoji)
}
.widgetURL(URL(string: "http://www.apple.com"))
.keylineTint(Color.red)
}
}
}
extension MomentAttributes {
fileprivate static var preview: MomentAttributes {
MomentAttributes(name: "World")
}
}
extension MomentAttributes.ContentState {
fileprivate static var smiley: MomentAttributes.ContentState {
MomentAttributes.ContentState(emoji: "😀")
}
fileprivate static var starEyes: MomentAttributes.ContentState {
MomentAttributes.ContentState(emoji: "🤩")
}
}
#Preview("Notification", as: .content, using: MomentAttributes.preview) {
MomentLiveActivity()
} contentStates: {
MomentAttributes.ContentState.smiley
MomentAttributes.ContentState.starEyes
}