Files
ente/mobile/ios/EnteMemoryWidget/EnteMemoryWidget.swift
2025-06-04 17:41:22 +05:30

257 lines
10 KiB
Swift

//
// EnteMemoryWidget.swift
// EnteMemoryWidget
import SwiftUI
import UIKit
import WidgetKit
private let widgetGroupId = "group.io.ente.frame.EnteMemoryWidget"
struct Provider: TimelineProvider {
let minutes = 15
let data = UserDefaults(suiteName: widgetGroupId)
func placeholder(in _: Context) -> FileEntry {
FileEntry(
date: Date(), index: nil, imageData: nil, title: "Title", subTitle: "Sub Title",
generatedId: nil)
}
func getSnapshot(in _: Context, completion: @escaping (FileEntry) -> Void) {
let entry = FileEntry(
date: Date(), index: -2, imageData: nil, title: "Over the hills",
subTitle: "May 23, 2021",
generatedId: nil)
completion(entry)
}
func getTimeline(in _: Context, completion: @escaping (Timeline<Entry>) -> Void) {
var entries: [FileEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Calendar.current.nextDate(
after: Date(), matching: DateComponents(second: 0), matchingPolicy: .nextTime,
direction: .backward
)!
var totalMemories =
data?.integer(forKey: "totalMemories")
if totalMemories != nil && totalMemories! > 0 {
let count = totalMemories! > 5 ? 5 : totalMemories
for offset in 0..<count! {
let randomInt = Int.random(in: 0..<totalMemories!)
let entryDate = Calendar.current.date(
byAdding: .minute, value: minutes * offset, to: currentDate
)!
let imageData =
data?.string(forKey: "memory_widget_" + String(randomInt))
let dictionary = data?.dictionary(
forKey: "memory_widget_" + String(randomInt) + "_data")
let generatedId = dictionary?["generatedId"] as? Int
let subTitle = dictionary?["subText"] as? String
let title = dictionary?["title"] as? String
let entry = FileEntry(
date: entryDate, index: randomInt, imageData: imageData, title: title,
subTitle: subTitle, generatedId: generatedId)
entries.append(entry)
}
} else {
let entry = FileEntry(
date: Date(), index: -1, imageData: nil, title: nil, subTitle: nil, generatedId: nil
)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
// func relevances() async -> WidgetRelevances<Void> {
// // Generate a list containing the contexts this widget is relevant in.
// }
}
struct FileEntry: TimelineEntry {
let date: Date
let index: Int?
let imageData: String?
let title: String?
let subTitle: String?
var generatedId: Int?
}
struct EnteMemoryWidgetEntryView: View {
var entry: Provider.Entry
let data = UserDefaults.init(suiteName: widgetGroupId)
var body: some View {
GeometryReader { geometry in
ZStack {
if let imageData = entry.imageData,
let uiImage = UIImage(contentsOfFile: imageData)
{
Image(uiImage: uiImage)
.resizable()
.backwardWidgetFullColorRenderingMode()
.aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width, height: geometry.size.height)
.overlay(
LinearGradient(
gradient: Gradient(colors: [Color.black.opacity(0.7), Color.clear]),
startPoint: .bottom,
endPoint: .top
)
.frame(height: geometry.size.height * 0.4)
.frame(maxHeight: .infinity, alignment: .bottom)
.backwardWidgetAccentable(true)
)
.overlay(
VStack(alignment: .leading, spacing: 2) {
Text(entry.title ?? "").font(
.custom("Inter", size: 14, relativeTo: .caption)
) // Custom with fallback
.bold()
.foregroundStyle(.white)
.shadow(radius: 20)
Text(entry.subTitle ?? "")
.font(.custom("Inter", size: 12, relativeTo: .caption2))
.foregroundStyle(.white)
.shadow(radius: 20)
}
.padding(.leading, geometry.size.width * 0.05)
.padding(.bottom, geometry.size.height * 0.05),
alignment: .bottomLeading
)
} else if entry.index == -2 {
if let uiImage = UIImage(named: "MemoriesWidgetPreview") {
Image(uiImage: uiImage)
.resizable()
.backwardWidgetFullColorRenderingMode()
.aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width, height: geometry.size.height)
.overlay(
LinearGradient(
gradient: Gradient(colors: [
Color.black.opacity(0.7), Color.clear,
]),
startPoint: .bottom,
endPoint: .top
)
.frame(height: geometry.size.height * 0.4)
.frame(maxHeight: .infinity, alignment: .bottom)
.backwardWidgetAccentable(true)
)
.overlay(
VStack(alignment: .leading, spacing: 2) {
Text(entry.title ?? "").font(
.custom("Inter", size: 14, relativeTo: .caption)
) // Custom with fallback
.bold()
.foregroundStyle(.white)
.shadow(radius: 20)
Text(entry.subTitle ?? "")
.font(.custom("Inter", size: 12, relativeTo: .caption2))
.foregroundStyle(.white)
.shadow(radius: 20)
}
.padding(.leading, geometry.size.width * 0.05)
.padding(.bottom, geometry.size.height * 0.05),
alignment: .bottomLeading
)
}
} else if let uiImage = UIImage(named: "MemoriesWidgetDefault") {
VStack(spacing: 8) {
Spacer()
Image(uiImage: uiImage)
.resizable()
.backwardWidgetFullColorRenderingMode()
.aspectRatio(contentMode: .fit)
.padding(8)
Text("Go to Settings -> General to customise the widget")
.font(.custom("Inter", size: 12, relativeTo: .caption))
.foregroundStyle(.white) // Tint-aware color
.multilineTextAlignment(.center)
.padding(.bottom, 12)
.padding(.horizontal, 8)
.backwardWidgetAccentable(true)
Spacer()
}
.frame(width: geometry.size.width, height: geometry.size.height)
} else {
Color.gray
}
}
.clipped()
.edgesIgnoringSafeArea(.all)
.widgetURL(
URL(
string:
"memorywidget://message?generatedId=\(entry.generatedId != nil ? String(entry.generatedId!) : "nan")&homeWidget"
)
)
}
}
}
struct EnteMemoryWidget: Widget {
let kind: String = "EnteMemoryWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
if #available(iOS 17.0, *) {
EnteMemoryWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
EnteMemoryWidgetEntryView(entry: entry)
.padding()
.background()
}
}
.configurationDisplayName("Memories")
.description("See special moments from the past")
.contentMarginsDisabled()
}
}
#Preview(as: .systemSmall) {
EnteMemoryWidget()
} timeline: {
FileEntry(
date: .now, index: -2, imageData: nil, title: nil, subTitle: nil, generatedId: nil)
FileEntry(
date: .now, index: -2, imageData: nil, title: nil, subTitle: nil, generatedId: nil)
}
extension View {
@ViewBuilder
func backwardWidgetAccentable(_ accentable: Bool = true) -> some View {
if #available(iOS 16.0, *) {
self.widgetAccentable(accentable)
} else {
self
}
}
}
extension Image {
@ViewBuilder
func backwardWidgetAccentedRenderingMode(_ isAccentedRenderingMode: Bool = true) -> some View {
if #available(iOS 18.0, *) {
self.widgetAccentedRenderingMode(isAccentedRenderingMode ? .accented : .fullColor)
} else {
self
}
}
@ViewBuilder
func backwardWidgetFullColorRenderingMode() -> some View {
backwardWidgetAccentedRenderingMode(false)
}
}