Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions app/ios/BatteryWidget-Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Omi Battery</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
6 changes: 6 additions & 0 deletions app/ios/BatteryWidget/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "logo_transparent.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "logo_transparent.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "logo_transparent.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions app/ios/BatteryWidget/BatteryWidget.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?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>com.apple.security.application-groups</key>
<array>
<string>group.com.friend-app-with-wearable.ios12</string>
</array>
</dict>
</plist>
215 changes: 215 additions & 0 deletions app/ios/BatteryWidget/BatteryWidget.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import WidgetKit
import SwiftUI

// MARK: - Timeline Entry

struct BatteryEntry: TimelineEntry {
let date: Date
let info: DeviceBatteryInfo
}

// MARK: - Timeline Provider

struct BatteryTimelineProvider: TimelineProvider {
func placeholder(in context: Context) -> BatteryEntry {
BatteryEntry(
date: Date(),
info: DeviceBatteryInfo(
deviceName: "Omi",
batteryLevel: 85,
deviceType: "omi",
isConnected: true,
lastUpdated: Date(),
isMuted: false
)
)
}

func getSnapshot(in context: Context, completion: @escaping (BatteryEntry) -> Void) {
completion(BatteryEntry(date: Date(), info: DeviceBatteryInfo.fromSharedDefaults()))
}

func getTimeline(in context: Context, completion: @escaping (Timeline<BatteryEntry>) -> Void) {
let info = DeviceBatteryInfo.fromSharedDefaults()
let entry = BatteryEntry(date: Date(), info: info)
// 5-minute fallback refresh; the app pushes instant updates via
// WidgetCenter.shared.reloadAllTimelines on battery or mute state changes.
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 5, to: Date())!
completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
}
}

// MARK: - Widget Definition

struct OmiBatteryWidget: Widget {
let kind: String = "OmiBatteryWidget"

var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: BatteryTimelineProvider()) { entry in
if #available(iOS 17.0, *) {
BatteryWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
BatteryWidgetEntryView(entry: entry)
}
}
.configurationDisplayName("Omi Battery")
.description("Shows your Omi device battery level and mic state.")
.supportedFamilies([.accessoryRectangular, .accessoryInline])
}
}

// MARK: - Widget Entry View

struct BatteryWidgetEntryView: View {
@Environment(\.widgetFamily) var family
var entry: BatteryEntry

var body: some View {
switch family {
case .accessoryRectangular:
AccessoryRectangularView(info: entry.info)
default:
AccessoryInlineView(info: entry.info)
}
}
}

// MARK: - Lock Screen: Rectangular

struct AccessoryRectangularView: View {
let info: DeviceBatteryInfo

var body: some View {
HStack(spacing: 0) {
// Left — Omi logo
Image("omi-logo")
.resizable()
.renderingMode(.template)
.scaledToFit()
.frame(width: 36, height: 36)
.foregroundColor(.primary)

Spacer(minLength: 4)

if info.isConnected {
// Center — battery %
Group {
if info.batteryLevel >= 0 {
Text("\(info.batteryLevel)%")
} else {
Text("--%")
.foregroundColor(.secondary)
}
}
.font(.system(size: 22, weight: .bold, design: .rounded))
.lineLimit(1)
.minimumScaleFactor(0.6)

Spacer(minLength: 4)

// Right — mute state
Image(systemName: info.isMuted ? "mic.slash.fill" : "mic.fill")
.font(.system(size: 22, weight: .medium))
.foregroundColor(info.isMuted ? Color(red: 1.0, green: 0.23, blue: 0.19) : .primary)
.frame(width: 36)
} else {
// Disconnected
Text("Connect\ndevice")
.font(.system(size: 15, weight: .bold))
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
}
}
.padding(.horizontal, 10)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(.white.opacity(0.25))
)
}
}

// MARK: - Lock Screen: Inline

struct AccessoryInlineView: View {
let info: DeviceBatteryInfo

var body: some View {
if info.isConnected && info.batteryLevel >= 0 {
Label("\(displayName) \(info.batteryLevel)%", systemImage: deviceIcon)
} else {
Label("\(displayName) --", systemImage: deviceIcon)
}
}

private var displayName: String {
info.deviceName.isEmpty || info.deviceName == "Unknown" ? "Omi" : info.deviceName
}

private var deviceIcon: String {
switch info.deviceType.lowercased() {
case "applewatch": return "applewatch"
case "openglass", "frame": return "eyeglasses"
default: return "mic.fill"
}
}
}

// MARK: - Previews

#if DEBUG
struct OmiBatteryWidget_Previews: PreviewProvider {
static var previews: some View {
let connected = BatteryEntry(
date: Date(),
info: DeviceBatteryInfo(
deviceName: "Omi DevKit",
batteryLevel: 98,
deviceType: "omi",
isConnected: true,
lastUpdated: Date(),
isMuted: false
)
)
let muted = BatteryEntry(
date: Date(),
info: DeviceBatteryInfo(
deviceName: "Omi",
batteryLevel: 72,
deviceType: "omi",
isConnected: true,
lastUpdated: Date(),
isMuted: true
)
)
let disconnected = BatteryEntry(
date: Date(),
info: DeviceBatteryInfo(
deviceName: "Omi",
batteryLevel: -1,
deviceType: "omi",
isConnected: false,
lastUpdated: Date.distantPast,
isMuted: false
)
)

Group {
BatteryWidgetEntryView(entry: connected)
.previewContext(WidgetPreviewContext(family: .accessoryRectangular))
.previewDisplayName("Rectangular – Connected")
BatteryWidgetEntryView(entry: muted)
.previewContext(WidgetPreviewContext(family: .accessoryRectangular))
.previewDisplayName("Rectangular – Muted")
BatteryWidgetEntryView(entry: disconnected)
.previewContext(WidgetPreviewContext(family: .accessoryRectangular))
.previewDisplayName("Rectangular – Disconnected")
BatteryWidgetEntryView(entry: connected)
.previewContext(WidgetPreviewContext(family: .accessoryInline))
.previewDisplayName("Inline")
}
}
}
#endif
9 changes: 9 additions & 0 deletions app/ios/BatteryWidget/BatteryWidgetBundle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import WidgetKit
import SwiftUI

@main
struct BatteryWidgetBundle: WidgetBundle {
var body: some Widget {
OmiBatteryWidget()
}
}
43 changes: 43 additions & 0 deletions app/ios/BatteryWidget/SharedDefaults.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation

/// App Group identifier shared between the main app and the widget extension.
let appGroupIdentifier = "group.com.friend-app-with-wearable.ios12"

/// Keys used to store device battery data in the shared UserDefaults.
enum BatteryWidgetKeys {
static let deviceName = "widget_device_name"
static let batteryLevel = "widget_battery_level"
static let deviceType = "widget_device_type"
static let isConnected = "widget_is_connected"
static let lastUpdated = "widget_last_updated"
static let isMuted = "widget_is_muted"
}

/// Model representing the device battery state shown in the widget.
struct DeviceBatteryInfo {
let deviceName: String
let batteryLevel: Int
let deviceType: String
let isConnected: Bool
let lastUpdated: Date
let isMuted: Bool

/// Reads the latest device battery info from the shared App Group UserDefaults.
static func fromSharedDefaults() -> DeviceBatteryInfo {
let defaults = UserDefaults(suiteName: appGroupIdentifier)
let name = defaults?.string(forKey: BatteryWidgetKeys.deviceName) ?? "Omi"
let battery = defaults?.integer(forKey: BatteryWidgetKeys.batteryLevel) ?? -1
let type = defaults?.string(forKey: BatteryWidgetKeys.deviceType) ?? "omi"
let connected = defaults?.bool(forKey: BatteryWidgetKeys.isConnected) ?? false
let updated = defaults?.object(forKey: BatteryWidgetKeys.lastUpdated) as? Date ?? Date.distantPast
let muted = defaults?.bool(forKey: BatteryWidgetKeys.isMuted) ?? false
return DeviceBatteryInfo(
deviceName: name,
batteryLevel: battery,
deviceType: type,
isConnected: connected,
lastUpdated: updated,
isMuted: muted
)
}
}
7 changes: 0 additions & 7 deletions app/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,6 @@ PODS:
- Flutter
- pasteboard (0.0.1):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- PromisesObjC (2.4.0)
Expand Down Expand Up @@ -316,7 +313,6 @@ DEPENDENCIES:
- opus_flutter_ios (from `.symlinks/plugins/opus_flutter_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
Expand Down Expand Up @@ -440,8 +436,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
pasteboard:
:path: ".symlinks/plugins/pasteboard/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
share_plus:
Expand Down Expand Up @@ -525,7 +519,6 @@ SPEC CHECKSUMS:
opus_flutter_ios: f16ed3599997ced564ad44509e87003159a86def
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
Expand Down
Loading