diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index f95d1e5a3..86800856e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -77,6 +77,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/com/exptech/dpip/MainActivity.kt b/android/app/src/main/kotlin/com/exptech/dpip/MainActivity.kt
index a751c415c..0d65246ba 100644
--- a/android/app/src/main/kotlin/com/exptech/dpip/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/exptech/dpip/MainActivity.kt
@@ -1,5 +1,62 @@
package com.exptech.dpip
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.MethodChannel
-class MainActivity : FlutterActivity()
+class MainActivity : FlutterActivity() {
+ private val WIDGET_CHANNEL = "com.exptech.dpip/widget"
+
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+
+ MethodChannel(flutterEngine.dartExecutor.binaryMessenger, WIDGET_CHANNEL).setMethodCallHandler { call, result ->
+ when (call.method) {
+ "updateWidgets" -> {
+ try {
+ updateAllWidgets()
+ result.success(true)
+ } catch (e: Exception) {
+ result.error("UPDATE_ERROR", "Failed to update widgets: ${e.message}", null)
+ }
+ }
+ else -> {
+ result.notImplemented()
+ }
+ }
+ }
+ }
+
+ /**
+ * 手動觸發所有 widget 實例的更新
+ * 發送 APPWIDGET_UPDATE broadcast 來觸發 onUpdate 方法
+ */
+ private fun updateAllWidgets() {
+ val context = applicationContext
+
+ // 更新標準版 widget
+ val standardManager = AppWidgetManager.getInstance(context)
+ val standardComponent = ComponentName(context, WeatherWidgetProvider::class.java)
+ val standardIds = standardManager.getAppWidgetIds(standardComponent)
+ if (standardIds.isNotEmpty()) {
+ val intent = Intent(context, WeatherWidgetProvider::class.java)
+ intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, standardIds)
+ context.sendBroadcast(intent)
+ }
+
+ // 更新小方形版 widget
+ val smallComponent = ComponentName(context, WeatherWidgetSmallProvider::class.java)
+ val smallIds = standardManager.getAppWidgetIds(smallComponent)
+ if (smallIds.isNotEmpty()) {
+ val intent = Intent(context, WeatherWidgetSmallProvider::class.java)
+ intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, smallIds)
+ context.sendBroadcast(intent)
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/com/exptech/dpip/WeatherWidgetProvider.kt b/android/app/src/main/kotlin/com/exptech/dpip/WeatherWidgetProvider.kt
new file mode 100644
index 000000000..b4f801f6b
--- /dev/null
+++ b/android/app/src/main/kotlin/com/exptech/dpip/WeatherWidgetProvider.kt
@@ -0,0 +1,138 @@
+package com.exptech.dpip
+
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.Context
+import android.widget.RemoteViews
+import es.antonborri.home_widget.HomeWidgetPlugin
+
+/**
+ * DPIP 天氣桌面小部件
+ * 顯示即時天氣資訊
+ */
+class WeatherWidgetProvider : AppWidgetProvider() {
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ // 更新所有小部件實例
+ for (appWidgetId in appWidgetIds) {
+ updateAppWidget(context, appWidgetManager, appWidgetId)
+ }
+ }
+
+ override fun onEnabled(context: Context) {
+ // 第一次添加小部件時呼叫
+ }
+
+ override fun onDisabled(context: Context) {
+ // 最後一個小部件被移除時呼叫
+ }
+
+ companion object {
+ /**
+ * 更新單個小部件
+ */
+ internal fun updateAppWidget(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetId: Int
+ ) {
+ // 從 SharedPreferences 讀取資料
+ val widgetData = HomeWidgetPlugin.getData(context)
+ val views = RemoteViews(context.packageName, R.layout.weather_widget)
+
+ // 檢查是否有錯誤或沒有資料
+ val hasError = widgetData.getBoolean("has_error", false)
+ val hasData = widgetData.contains("temperature")
+
+ if (hasError || !hasData) {
+ val errorMessage = widgetData.getString("error_message", "無法載入天氣")
+ views.setTextViewText(R.id.weather_status, errorMessage)
+ views.setTextViewText(R.id.temperature, "--°")
+ } else {
+ // 天氣狀態
+ val weatherStatus = widgetData.getString("weather_status", "晴天")
+ views.setTextViewText(R.id.weather_status, weatherStatus)
+
+ // 溫度
+ val temperature = widgetData.readIntValue("temperature") ?: 0
+ views.setTextViewText(R.id.temperature, "${temperature}°")
+
+ // 體感溫度
+ val feelsLike = widgetData.readIntValue("feels_like") ?: 0
+ views.setTextViewText(R.id.feels_like, "體感 ${feelsLike}°")
+
+ // 濕度
+ val humidity = widgetData.readIntValue("humidity") ?: 0
+ views.setTextViewText(R.id.humidity, "${humidity}%")
+
+ // 風速
+ val windSpeed = widgetData.readNumber("wind_speed") ?: 0.0
+ views.setTextViewText(R.id.wind_speed, String.format("%.1fm/s", windSpeed))
+
+ // 風向
+ val windDirection = widgetData.getString("wind_direction", "-")
+ views.setTextViewText(R.id.wind_direction, windDirection)
+
+ // 降雨
+ val rain = widgetData.readNumber("rain") ?: 0.0
+ views.setTextViewText(R.id.rain, String.format("%.1fmm", rain))
+
+ // 氣象站
+ val stationName = widgetData.getString("station_name", "")
+ val stationDistance = widgetData.readNumber("station_distance") ?: 0.0
+ views.setTextViewText(
+ R.id.station_info,
+ "${stationName}氣象站 · ${String.format("%.1f", stationDistance)}km"
+ )
+
+ // 更新時間
+ val updateTime = widgetData.readTimestampMillis("update_time")
+ if (updateTime != null && updateTime > 0) {
+ val calendar = java.util.Calendar.getInstance()
+ calendar.timeInMillis = updateTime
+ val timeStr = String.format(
+ "%02d:%02d",
+ calendar.get(java.util.Calendar.HOUR_OF_DAY),
+ calendar.get(java.util.Calendar.MINUTE)
+ )
+ views.setTextViewText(R.id.update_time, timeStr)
+ }
+
+ // 天氣圖示 (根據 weatherCode 設定)
+ val weatherCode = widgetData.getInt("weather_code", 1)
+ val iconRes = getWeatherIcon(weatherCode)
+ views.setImageViewResource(R.id.weather_icon, iconRes)
+ }
+
+ // 點擊小部件開啟 App
+ val pendingIntent = es.antonborri.home_widget.HomeWidgetLaunchIntent.getActivity(
+ context,
+ MainActivity::class.java
+ )
+ views.setOnClickPendingIntent(R.id.widget_container, pendingIntent)
+
+ // 更新小部件
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ }
+
+ /**
+ * 根據天氣代碼返回對應圖示
+ * 對應到 Flutter 的 WeatherIcons.getWeatherIcon
+ */
+ fun getWeatherIcon(code: Int): Int {
+ return when (code) {
+ 1 -> android.R.drawable.ic_menu_day // 晴天
+ 2, 3 -> android.R.drawable.ic_partial_secure // 多雲
+ 4, 5, 6, 7 -> android.R.drawable.ic_dialog_alert // 陰天/霧
+ 8, 9, 10, 11, 12, 13, 14 -> android.R.drawable.ic_dialog_info // 雨天
+ 15, 16, 17, 18 -> android.R.drawable.ic_lock_power_off // 雷雨
+ else -> android.R.drawable.ic_menu_day
+ }
+ // 注意: 實際使用時應該使用自訂圖示,這裡使用系統圖示作為範例
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/com/exptech/dpip/WeatherWidgetSmallProvider.kt b/android/app/src/main/kotlin/com/exptech/dpip/WeatherWidgetSmallProvider.kt
new file mode 100644
index 000000000..7065fc994
--- /dev/null
+++ b/android/app/src/main/kotlin/com/exptech/dpip/WeatherWidgetSmallProvider.kt
@@ -0,0 +1,106 @@
+package com.exptech.dpip
+
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.Context
+import android.widget.RemoteViews
+import es.antonborri.home_widget.HomeWidgetPlugin
+
+/**
+ * DPIP 天氣桌面小部件 (小方形版)
+ * 2x2 尺寸的緊湊版本
+ */
+class WeatherWidgetSmallProvider : AppWidgetProvider() {
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ // 更新所有小部件實例
+ for (appWidgetId in appWidgetIds) {
+ updateAppWidget(context, appWidgetManager, appWidgetId)
+ }
+ }
+
+ override fun onEnabled(context: Context) {
+ // 第一次添加小部件時呼叫
+ }
+
+ override fun onDisabled(context: Context) {
+ // 最後一個小部件被移除時呼叫
+ }
+
+ companion object {
+ /**
+ * 更新單個小部件 (小方形版)
+ */
+ internal fun updateAppWidget(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetId: Int
+ ) {
+ // 從 SharedPreferences 讀取資料
+ val widgetData = HomeWidgetPlugin.getData(context)
+ val views = RemoteViews(context.packageName, R.layout.weather_widget_small)
+
+ // 檢查是否有錯誤或沒有資料
+ val hasError = widgetData.getBoolean("has_error", false)
+ val hasData = widgetData.contains("temperature")
+
+ if (hasError || !hasData) {
+ val errorMessage = widgetData.getString("error_message", "無法載入")
+ views.setTextViewText(R.id.weather_status, errorMessage)
+ views.setTextViewText(R.id.temperature, "--°")
+ } else {
+ // 天氣狀態
+ val weatherStatus = widgetData.getString("weather_status", "晴天")
+ views.setTextViewText(R.id.weather_status, weatherStatus)
+
+ // 溫度
+ val temperature = widgetData.readIntValue("temperature") ?: 0
+ views.setTextViewText(R.id.temperature, "${temperature}°")
+
+ // 體感溫度
+ val feelsLike = widgetData.readIntValue("feels_like") ?: 0
+ views.setTextViewText(R.id.feels_like, "體感 ${feelsLike}°")
+
+ // 濕度
+ val humidity = widgetData.readIntValue("humidity") ?: 0
+ views.setTextViewText(R.id.humidity, "${humidity}%")
+
+ // 風速
+ val windSpeed = widgetData.readNumber("wind_speed") ?: 0.0
+ views.setTextViewText(R.id.wind_speed, String.format("%.1fm/s", windSpeed))
+
+ // 更新時間
+ val updateTime = widgetData.readTimestampMillis("update_time")
+ if (updateTime != null && updateTime > 0) {
+ val calendar = java.util.Calendar.getInstance()
+ calendar.timeInMillis = updateTime
+ val timeStr = String.format(
+ "%02d:%02d",
+ calendar.get(java.util.Calendar.HOUR_OF_DAY),
+ calendar.get(java.util.Calendar.MINUTE)
+ )
+ views.setTextViewText(R.id.update_time, timeStr)
+ }
+
+ // 天氣圖示
+ val weatherCode = widgetData.getInt("weather_code", 1)
+ val iconRes = WeatherWidgetProvider.getWeatherIcon(weatherCode)
+ views.setImageViewResource(R.id.weather_icon, iconRes)
+ }
+
+ // 點擊小部件開啟 App
+ val pendingIntent = es.antonborri.home_widget.HomeWidgetLaunchIntent.getActivity(
+ context,
+ MainActivity::class.java
+ )
+ views.setOnClickPendingIntent(R.id.widget_container_small, pendingIntent)
+
+ // 更新小部件
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/com/exptech/dpip/WidgetDataExtensions.kt b/android/app/src/main/kotlin/com/exptech/dpip/WidgetDataExtensions.kt
new file mode 100644
index 000000000..c55b50459
--- /dev/null
+++ b/android/app/src/main/kotlin/com/exptech/dpip/WidgetDataExtensions.kt
@@ -0,0 +1,37 @@
+package com.exptech.dpip
+
+import android.content.SharedPreferences
+import kotlin.math.roundToInt
+
+private const val DOUBLE_FLAG_PREFIX = "home_widget.double."
+
+/**
+ * 從 SharedPreferences 讀取數值型資料,並自動處理由 home_widget
+ * 以 Long 形式儲存的 Double。
+ */
+fun SharedPreferences.readNumber(key: String): Double? {
+ val raw = all[key] ?: return null
+ return when (raw) {
+ is Int -> raw.toDouble()
+ is Float -> raw.toDouble()
+ is Long ->
+ if (getBoolean("$DOUBLE_FLAG_PREFIX$key", false)) {
+ java.lang.Double.longBitsToDouble(raw)
+ } else {
+ raw.toDouble()
+ }
+ is Double -> raw
+ is String -> raw.toDoubleOrNull()
+ else -> null
+ }
+}
+
+fun SharedPreferences.readIntValue(key: String): Int? = readNumber(key)?.roundToInt()
+
+fun SharedPreferences.readFloatValue(key: String): Float? = readNumber(key)?.toFloat()
+
+fun SharedPreferences.readTimestampMillis(key: String): Long? {
+ val value = readNumber(key)?.toLong() ?: return null
+ return if (value < 1_000_000_000_000L) value * 1000L else value
+}
+
diff --git a/android/app/src/main/res/drawable/feels_like_background.xml b/android/app/src/main/res/drawable/feels_like_background.xml
new file mode 100644
index 000000000..a22d2e19f
--- /dev/null
+++ b/android/app/src/main/res/drawable/feels_like_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/widget_background.xml b/android/app/src/main/res/drawable/widget_background.xml
new file mode 100644
index 000000000..f2e440e17
--- /dev/null
+++ b/android/app/src/main/res/drawable/widget_background.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/weather_widget.xml b/android/app/src/main/res/layout/weather_widget.xml
new file mode 100644
index 000000000..679c1f2bf
--- /dev/null
+++ b/android/app/src/main/res/layout/weather_widget.xml
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/weather_widget_small.xml b/android/app/src/main/res/layout/weather_widget_small.xml
new file mode 100644
index 000000000..7b39a6612
--- /dev/null
+++ b/android/app/src/main/res/layout/weather_widget_small.xml
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 69de199fa..fd969dabe 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -1,3 +1,5 @@
DPIP
+ 顯示所在地即時天氣資訊
+ 緊湊的天氣小部件
\ No newline at end of file
diff --git a/android/app/src/main/res/xml/weather_widget_info.xml b/android/app/src/main/res/xml/weather_widget_info.xml
new file mode 100644
index 000000000..fabd73cd2
--- /dev/null
+++ b/android/app/src/main/res/xml/weather_widget_info.xml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/android/app/src/main/res/xml/weather_widget_small_info.xml b/android/app/src/main/res/xml/weather_widget_small_info.xml
new file mode 100644
index 000000000..0f378422f
--- /dev/null
+++ b/android/app/src/main/res/xml/weather_widget_small_info.xml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
index 1dc6cf765..163000d85 100644
--- a/ios/Flutter/AppFrameworkInfo.plist
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 13.0
+ 14.0
diff --git a/ios/Podfile b/ios/Podfile
index 5bc37a053..5be9ced56 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-platform :ios, '13.0'
+platform :ios, '14.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 424784b87..56330b6b7 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -125,6 +125,8 @@ PODS:
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
+ - home_widget (0.0.1):
+ - Flutter
- in_app_purchase_storekit (0.0.1):
- Flutter
- FlutterMacOS
@@ -163,6 +165,8 @@ PODS:
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
+ - workmanager_apple (0.0.1):
+ - Flutter
DEPENDENCIES:
- awesome_notifications (from `.symlinks/plugins/awesome_notifications/ios`)
@@ -176,6 +180,7 @@ DEPENDENCIES:
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
+ - home_widget (from `.symlinks/plugins/home_widget/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@@ -185,6 +190,7 @@ DEPENDENCIES:
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+ - workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
SPEC REPOS:
trunk:
@@ -229,6 +235,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/gal/darwin"
geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/darwin"
+ home_widget:
+ :path: ".symlinks/plugins/home_widget/ios"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
maplibre_gl:
@@ -247,6 +255,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
+ workmanager_apple:
+ :path: ".symlinks/plugins/workmanager_apple/ios"
SPEC CHECKSUMS:
awesome_notifications: 0f432b28098d193920b11a44cfa9d2d9313a3888
@@ -271,6 +281,7 @@ SPEC CHECKSUMS:
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
+ home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
IosAwnCore: 653786a911089012092ce831f2945cd339855a89
IosAwnFcmCore: 1bdb9054b2e00187d00f1ffcfbb1855949a7b82f
@@ -286,7 +297,8 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
+ workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
-PODFILE CHECKSUM: 3d88bce62bfe048ac33ca00d3fb1bc02caeda4d3
+PODFILE CHECKSUM: 8497909ca3d416990a4d0537da60f192f3ac56a8
COCOAPODS: 1.16.2
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 3d2985742..6b715bb9a 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -12,8 +12,10 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
529C27C82C93F7B200AAFAB6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 529C27C62C93F7B200AAFAB6 /* InfoPlist.strings */; };
52FA5E152DC8A9EA0008FEB0 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52FA5E142DC8A9EA0008FEB0 /* StoreKit.framework */; };
- 6037C7C000DE0752E32B9E54 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6838AADB908B55100C5691B /* Pods_Runner.framework */; };
632125292C2EA17900A088F8 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 632125282C2EA17900A088F8 /* GoogleService-Info.plist */; };
+ 63D34C722ECDB4FC0007BD42 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63D34C712ECDB4FC0007BD42 /* WidgetKit.framework */; };
+ 63D34C742ECDB4FC0007BD42 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63D34C732ECDB4FC0007BD42 /* SwiftUI.framework */; };
+ 63D34C832ECDB4FC0007BD42 /* WeatherWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 63D34C702ECDB4FC0007BD42 /* WeatherWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
63F1FCBE2C8D48D300693F0C /* update.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCBB2C8D48D300693F0C /* update.aiff */; };
63F1FCBF2C8D48D300693F0C /* rain.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCB82C8D48D300693F0C /* rain.aiff */; };
63F1FCC02C8D48D300693F0C /* eew.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCB22C8D48D300693F0C /* eew.aiff */; };
@@ -30,7 +32,8 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
- B3C0548ABD2F128E6EE128FD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A29CCFC3290C53FDF724288 /* Pods_RunnerTests.framework */; };
+ C9F33FFEA952CCCCD4060245 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 636E797A6D7770211BDECF3B /* Pods_RunnerTests.framework */; };
+ FD7D076CB1AC5B6DAA2B2107 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5CB2A3AC234172E4E1A2989 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -41,9 +44,27 @@
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
+ 63D34C812ECDB4FC0007BD42 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 63D34C6F2ECDB4FC0007BD42;
+ remoteInfo = WeatherWidgetExtension;
+ };
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
+ 63D34C842ECDB4FC0007BD42 /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ 63D34C832ECDB4FC0007BD42 /* WeatherWidgetExtension.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -57,13 +78,14 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 017380C769F48CBC9977E490 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
- 17F64C367FDB32D5C770A605 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 37FA8D23CEE08B979348D11D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
- 4B4531DD011F7A688ACAA691 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
+ 45A64791FD9DF576EA95E92D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
5228AD5A2C2EE45D007635F5 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
523A5FD82EB21EC0006F93FC /* Profile.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Profile.xcconfig; path = Flutter/Profile.xcconfig; sourceTree = ""; };
529C27C92C93F7B900AAFAB6 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/InfoPlist.strings; sourceTree = ""; };
@@ -73,6 +95,11 @@
52FA5E142DC8A9EA0008FEB0 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
632125282C2EA17900A088F8 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; };
6321252A2C2EA20700A088F8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = ""; };
+ 636E797A6D7770211BDECF3B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 63D34C702ECDB4FC0007BD42 /* WeatherWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WeatherWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ 63D34C712ECDB4FC0007BD42 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
+ 63D34C732ECDB4FC0007BD42 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
+ 63D34C922ECDB5A10007BD42 /* WeatherWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WeatherWidgetExtension.entitlements; sourceTree = ""; };
63F1FCB22C8D48D300693F0C /* eew.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = eew.aiff; sourceTree = ""; };
63F1FCB32C8D48D300693F0C /* eew_alert.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = eew_alert.aiff; sourceTree = ""; };
63F1FCB42C8D48D300693F0C /* eq.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = eq.aiff; sourceTree = ""; };
@@ -85,12 +112,10 @@
63F1FCBB2C8D48D300693F0C /* update.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = update.aiff; sourceTree = ""; };
63F1FCBC2C8D48D300693F0C /* warn.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = warn.aiff; sourceTree = ""; };
63F1FCBD2C8D48D300693F0C /* weather.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = weather.aiff; sourceTree = ""; };
+ 653D9CBB41B0381966E5B4DA /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
- 7F429ED347E85746A9B0DD8B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
- 86D7674C61D44CF5E96872D5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
- 8A29CCFC3290C53FDF724288 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -98,17 +123,51 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- 9A85FE1E2D76EB310CAE72FD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
- C6838AADB908B55100C5691B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- DF6CF45C0AFEE964FC43297A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
+ A5CB2A3AC234172E4E1A2989 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ E765C58D8150E00566EE7372 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ FDC71F4A7B7E218D2DB6F3E1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ 63D34C882ECDB4FC0007BD42 /* Exceptions for "WeatherWidget" folder in "WeatherWidgetExtension" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Info.plist,
+ );
+ target = 63D34C6F2ECDB4FC0007BD42 /* WeatherWidgetExtension */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ 63D34C752ECDB4FC0007BD42 /* WeatherWidget */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ exceptions = (
+ 63D34C882ECDB4FC0007BD42 /* Exceptions for "WeatherWidget" folder in "WeatherWidgetExtension" target */,
+ );
+ explicitFileTypes = {
+ };
+ explicitFolders = (
+ );
+ path = WeatherWidget;
+ sourceTree = "";
+ };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
/* Begin PBXFrameworksBuildPhase section */
0166E768B450CE02B1DB74B0 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- B3C0548ABD2F128E6EE128FD /* Pods_RunnerTests.framework in Frameworks */,
+ C9F33FFEA952CCCCD4060245 /* Pods_RunnerTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 63D34C6D2ECDB4FC0007BD42 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 63D34C742ECDB4FC0007BD42 /* SwiftUI.framework in Frameworks */,
+ 63D34C722ECDB4FC0007BD42 /* WidgetKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -117,7 +176,7 @@
buildActionMask = 2147483647;
files = (
52FA5E152DC8A9EA0008FEB0 /* StoreKit.framework in Frameworks */,
- 6037C7C000DE0752E32B9E54 /* Pods_Runner.framework in Frameworks */,
+ FD7D076CB1AC5B6DAA2B2107 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -135,12 +194,12 @@
5437AE1A0D322629A5F820A0 /* Pods */ = {
isa = PBXGroup;
children = (
- 7F429ED347E85746A9B0DD8B /* Pods-Runner.debug.xcconfig */,
- 9A85FE1E2D76EB310CAE72FD /* Pods-Runner.release.xcconfig */,
- 17F64C367FDB32D5C770A605 /* Pods-Runner.profile.xcconfig */,
- 4B4531DD011F7A688ACAA691 /* Pods-RunnerTests.debug.xcconfig */,
- 86D7674C61D44CF5E96872D5 /* Pods-RunnerTests.release.xcconfig */,
- DF6CF45C0AFEE964FC43297A /* Pods-RunnerTests.profile.xcconfig */,
+ E765C58D8150E00566EE7372 /* Pods-Runner.debug.xcconfig */,
+ FDC71F4A7B7E218D2DB6F3E1 /* Pods-Runner.release.xcconfig */,
+ 37FA8D23CEE08B979348D11D /* Pods-Runner.profile.xcconfig */,
+ 45A64791FD9DF576EA95E92D /* Pods-RunnerTests.debug.xcconfig */,
+ 653D9CBB41B0381966E5B4DA /* Pods-RunnerTests.release.xcconfig */,
+ 017380C769F48CBC9977E490 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "";
@@ -160,8 +219,10 @@
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
+ 63D34C922ECDB5A10007BD42 /* WeatherWidgetExtension.entitlements */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
+ 63D34C752ECDB4FC0007BD42 /* WeatherWidget */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
5437AE1A0D322629A5F820A0 /* Pods */,
@@ -174,6 +235,7 @@
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+ 63D34C702ECDB4FC0007BD42 /* WeatherWidgetExtension.appex */,
);
name = Products;
sourceTree = "";
@@ -213,8 +275,10 @@
isa = PBXGroup;
children = (
52FA5E142DC8A9EA0008FEB0 /* StoreKit.framework */,
- C6838AADB908B55100C5691B /* Pods_Runner.framework */,
- 8A29CCFC3290C53FDF724288 /* Pods_RunnerTests.framework */,
+ 63D34C712ECDB4FC0007BD42 /* WidgetKit.framework */,
+ 63D34C732ECDB4FC0007BD42 /* SwiftUI.framework */,
+ A5CB2A3AC234172E4E1A2989 /* Pods_Runner.framework */,
+ 636E797A6D7770211BDECF3B /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "";
@@ -226,7 +290,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
- 745AFA5621F4079D8F5F66F5 /* [CP] Check Pods Manifest.lock */,
+ 8AD09EF76284A1643F17A971 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
0166E768B450CE02B1DB74B0 /* Frameworks */,
@@ -241,23 +305,45 @@
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
+ 63D34C6F2ECDB4FC0007BD42 /* WeatherWidgetExtension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 63D34C892ECDB4FC0007BD42 /* Build configuration list for PBXNativeTarget "WeatherWidgetExtension" */;
+ buildPhases = (
+ 63D34C6C2ECDB4FC0007BD42 /* Sources */,
+ 63D34C6D2ECDB4FC0007BD42 /* Frameworks */,
+ 63D34C6E2ECDB4FC0007BD42 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ 63D34C752ECDB4FC0007BD42 /* WeatherWidget */,
+ );
+ name = WeatherWidgetExtension;
+ productName = WeatherWidgetExtension;
+ productReference = 63D34C702ECDB4FC0007BD42 /* WeatherWidgetExtension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
- 6BBD6C8E8A651C2033EB74CF /* [CP] Check Pods Manifest.lock */,
+ DBDD4869969CBE55CEEF60A7 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
+ 90F524049F0F0D728ACA776F /* [CP] Embed Pods Frameworks */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 63D34C842ECDB4FC0007BD42 /* Embed Foundation Extensions */,
+ 139FACA711B6678C67D7D4CA /* [CP] Copy Pods Resources */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
- 127FFB7A93668EDC7C84088F /* [CP] Embed Pods Frameworks */,
- 81A7F11822CA9F8CECB2FD48 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
+ 63D34C822ECDB4FC0007BD42 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
@@ -271,6 +357,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
+ LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@@ -278,6 +365,9 @@
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
+ 63D34C6F2ECDB4FC0007BD42 = {
+ CreatedOnToolsVersion = 26.0;
+ };
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
@@ -304,6 +394,7 @@
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
+ 63D34C6F2ECDB4FC0007BD42 /* WeatherWidgetExtension */,
);
};
/* End PBXProject section */
@@ -316,6 +407,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 63D34C6E2ECDB4FC0007BD42 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -344,21 +442,21 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
- 127FFB7A93668EDC7C84088F /* [CP] Embed Pods Frameworks */ = {
+ 139FACA711B6678C67D7D4CA /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
- name = "[CP] Embed Pods Frameworks";
+ name = "[CP] Copy Pods Resources";
outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
@@ -377,7 +475,7 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
- 6BBD6C8E8A651C2033EB74CF /* [CP] Check Pods Manifest.lock */ = {
+ 8AD09EF76284A1643F17A971 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -392,66 +490,66 @@
outputFileListPaths = (
);
outputPaths = (
- "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
- 745AFA5621F4079D8F5F66F5 /* [CP] Check Pods Manifest.lock */ = {
+ 90F524049F0F0D728ACA776F /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
- inputPaths = (
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
- "${PODS_ROOT}/Manifest.lock",
- );
- name = "[CP] Check Pods Manifest.lock";
+ name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
- );
- outputPaths = (
- "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
- 81A7F11822CA9F8CECB2FD48 /* [CP] Copy Pods Resources */ = {
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
- inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
+ inputPaths = (
);
- name = "[CP] Copy Pods Resources";
- outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
+ name = "Run Script";
+ outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
- showEnvVarsInLog = 0;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
- 9740EEB61CF901F6004384FC /* Run Script */ = {
+ DBDD4869969CBE55CEEF60A7 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
+ inputFileListPaths = (
+ );
inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
);
- name = "Run Script";
outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
@@ -464,6 +562,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 63D34C6C2ECDB4FC0007BD42 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -481,6 +586,11 @@
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
+ 63D34C822ECDB4FC0007BD42 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 63D34C6F2ECDB4FC0007BD42 /* WeatherWidgetExtension */;
+ targetProxy = 63D34C812ECDB4FC0007BD42 /* PBXContainerItemProxy */;
+ };
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -559,7 +669,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -590,7 +700,7 @@
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "x86_64 i386";
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = DPIP;
- IPHONEOS_DEPLOYMENT_TARGET = 13;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -606,13 +716,14 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 4B4531DD011F7A688ACAA691 /* Pods-RunnerTests.debug.xcconfig */;
+ baseConfigurationReference = 45A64791FD9DF576EA95E92D /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 98Q7JARYZF;
GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 14;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -625,13 +736,14 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 86D7674C61D44CF5E96872D5 /* Pods-RunnerTests.release.xcconfig */;
+ baseConfigurationReference = 653D9CBB41B0381966E5B4DA /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 98Q7JARYZF;
GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 14;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -642,13 +754,14 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = DF6CF45C0AFEE964FC43297A /* Pods-RunnerTests.profile.xcconfig */;
+ baseConfigurationReference = 017380C769F48CBC9977E490 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 98Q7JARYZF;
GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 14;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -657,6 +770,140 @@
};
name = Profile;
};
+ 63D34C852ECDB4FC0007BD42 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = WeatherWidgetExtension.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 98Q7JARYZF;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = WeatherWidget/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = WeatherWidget;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 18;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.dpip.WeatherWidget;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 63D34C862ECDB4FC0007BD42 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = WeatherWidgetExtension.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 98Q7JARYZF;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = WeatherWidget/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = WeatherWidget;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 18;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.dpip.WeatherWidget;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 63D34C872ECDB4FC0007BD42 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = WeatherWidgetExtension.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 98Q7JARYZF;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = WeatherWidget/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = WeatherWidget;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 18;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.dpip.WeatherWidget;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Profile;
+ };
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -708,7 +955,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -762,7 +1009,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LLVM_LTO = YES_THIN;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
@@ -798,7 +1045,7 @@
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = DPIP;
- IPHONEOS_DEPLOYMENT_TARGET = 13;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -837,7 +1084,7 @@
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "x86_64 i386";
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = DPIP;
- IPHONEOS_DEPLOYMENT_TARGET = 13;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -864,6 +1111,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ 63D34C892ECDB4FC0007BD42 /* Build configuration list for PBXNativeTarget "WeatherWidgetExtension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 63D34C852ECDB4FC0007BD42 /* Debug */,
+ 63D34C862ECDB4FC0007BD42 /* Release */,
+ 63D34C872ECDB4FC0007BD42 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 9d977a1cd..ab5f32e96 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -2,6 +2,7 @@ import CoreLocation
import Flutter
import UIKit
import UserNotifications
+import WidgetKit
@UIApplicationMain
@objc
@@ -9,6 +10,7 @@ class AppDelegate: FlutterAppDelegate, CLLocationManagerDelegate {
// MARK: - Properties
private var locationChannel: FlutterMethodChannel?
+ private var widgetChannel: FlutterMethodChannel?
private var locationManager: CLLocationManager!
private var lastSentLocation: CLLocation?
private var isLocationEnabled: Bool = false
@@ -59,6 +61,14 @@ class AppDelegate: FlutterAppDelegate, CLLocationManagerDelegate {
locationChannel?.setMethodCallHandler { [weak self] (call, result) in
self?.handleLocationChannelCall(call, result: result)
}
+
+ widgetChannel = FlutterMethodChannel(
+ name: "com.exptech.dpip/widget",
+ binaryMessenger: controller.binaryMessenger)
+
+ widgetChannel?.setMethodCallHandler { [weak self] (call, result) in
+ self?.handleWidgetChannelCall(call, result: result)
+ }
}
private func setupLocationManager() {
@@ -102,6 +112,16 @@ class AppDelegate: FlutterAppDelegate, CLLocationManagerDelegate {
result("Location toggled")
}
+ private func handleWidgetChannelCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ switch call.method {
+ case "reloadWidgetTimeline":
+ reloadWidgetTimeline()
+ result(true)
+ default:
+ result(FlutterMethodNotImplemented)
+ }
+ }
+
// MARK: - Location Management
private func toggleLocation(isEnabled: Bool) {
@@ -176,9 +196,13 @@ class AppDelegate: FlutterAppDelegate, CLLocationManagerDelegate {
private func sendLocationToServer(location: CLLocation) {
guard isLocationEnabled else { return }
guard let token = apnsToken else { return }
-
+
let latitude = location.coordinate.latitude
let longitude = location.coordinate.longitude
+
+ // 保存座標到 widget shared UserDefaults
+ saveLocationToWidget(latitude: latitude, longitude: longitude)
+
let appVersion =
Bundle.main.object(
forInfoDictionaryKey: "CFBundleShortVersionString") as? String
@@ -207,6 +231,31 @@ class AppDelegate: FlutterAppDelegate, CLLocationManagerDelegate {
task.resume()
}
+
+ // MARK: - Widget Data Management
+
+ /// 保存位置資訊到 Widget
+ private func saveLocationToWidget(latitude: Double, longitude: Double) {
+ guard let sharedDefaults = UserDefaults(suiteName: "group.com.exptech.dpip") else {
+ print("Failed to get shared UserDefaults for widget")
+ return
+ }
+
+ sharedDefaults.set(latitude, forKey: "widget_latitude")
+ sharedDefaults.set(longitude, forKey: "widget_longitude")
+ sharedDefaults.synchronize()
+
+ print("Widget location saved: \(latitude), \(longitude)")
+ }
+
+ /// 重新載入 Widget Timeline
+ /// 主動請求系統重新載入 widget 的時間線,確保 widget 顯示最新資料
+ private func reloadWidgetTimeline() {
+ if #available(iOS 14.0, *) {
+ WidgetCenter.shared.reloadTimelines(ofKind: "WeatherWidget")
+ print("Widget timeline reload requested")
+ }
+ }
// MARK: - Background Task Management
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
index 2a160d6b9..dd5100db0 100644
--- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -1,5 +1,13 @@
{
"images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
{
"filename" : "LaunchImage@3x.png",
"idiom" : "universal",
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
index be4adcbe1..20704ecc5 100644
--- a/ios/Runner/Runner.entitlements
+++ b/ios/Runner/Runner.entitlements
@@ -6,5 +6,9 @@
development
com.apple.developer.usernotifications.critical-alerts
+ com.apple.security.application-groups
+
+ group.com.exptech.dpip
+
diff --git a/ios/Runner/RunnerProfile.entitlements b/ios/Runner/RunnerProfile.entitlements
index be4adcbe1..20704ecc5 100644
--- a/ios/Runner/RunnerProfile.entitlements
+++ b/ios/Runner/RunnerProfile.entitlements
@@ -6,5 +6,9 @@
development
com.apple.developer.usernotifications.critical-alerts
+ com.apple.security.application-groups
+
+ group.com.exptech.dpip
+
diff --git a/ios/WeatherWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/ios/WeatherWidget/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 000000000..eb8789700
--- /dev/null
+++ b/ios/WeatherWidget/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/WeatherWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/WeatherWidget/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..230588010
--- /dev/null
+++ b/ios/WeatherWidget/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -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
+ }
+}
diff --git a/ios/WeatherWidget/Assets.xcassets/Contents.json b/ios/WeatherWidget/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/ios/WeatherWidget/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/WeatherWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/ios/WeatherWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json
new file mode 100644
index 000000000..eb8789700
--- /dev/null
+++ b/ios/WeatherWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/WeatherWidget/Info.plist b/ios/WeatherWidget/Info.plist
new file mode 100644
index 000000000..0f118fb75
--- /dev/null
+++ b/ios/WeatherWidget/Info.plist
@@ -0,0 +1,11 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.widgetkit-extension
+
+
+
diff --git a/ios/WeatherWidget/WeatherWidget.swift b/ios/WeatherWidget/WeatherWidget.swift
new file mode 100644
index 000000000..1e1a342cb
--- /dev/null
+++ b/ios/WeatherWidget/WeatherWidget.swift
@@ -0,0 +1,393 @@
+//
+// WeatherWidget.swift
+// WeatherWidget
+//
+// Created by YuYu 1015 on 11/19/R7.
+// DPIP 天氣桌面小部件
+//
+
+import WidgetKit
+import SwiftUI
+
+// MARK: - 天氣資料模型
+struct WeatherData {
+ let weatherStatus: String
+ let weatherCode: Int
+ let temperature: Double
+ let feelsLike: Double
+ let humidity: Double
+ let windSpeed: Double
+ let windDirection: String
+ let rain: Double
+ let stationName: String
+ let stationDistance: Double
+ let updateTime: Int
+ let hasError: Bool
+ let errorMessage: String
+
+ static var placeholder: WeatherData {
+ WeatherData(
+ weatherStatus: "晴天",
+ weatherCode: 1,
+ temperature: 25.0,
+ feelsLike: 23.0,
+ humidity: 65.0,
+ windSpeed: 2.5,
+ windDirection: "東北",
+ rain: 0.0,
+ stationName: "中央氣象站",
+ stationDistance: 2.5,
+ updateTime: Int(Date().timeIntervalSince1970),
+ hasError: false,
+ errorMessage: ""
+ )
+ }
+}
+
+// MARK: - Timeline Provider
+struct WeatherProvider: TimelineProvider {
+ func placeholder(in context: Context) -> WeatherEntry {
+ WeatherEntry(date: Date(), weather: .placeholder)
+ }
+
+ func getSnapshot(in context: Context, completion: @escaping (WeatherEntry) -> ()) {
+ let entry = WeatherEntry(date: Date(), weather: loadWeatherData())
+ completion(entry)
+ }
+
+ func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) {
+ let currentDate = Date()
+ let weather = loadWeatherData()
+ let entry = WeatherEntry(date: currentDate, weather: weather)
+
+ // 設定下次更新時間 (15分鐘後 - iOS WidgetKit 建議的最小更新間隔)
+ let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)!
+ let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
+
+ completion(timeline)
+ }
+
+ // 從 UserDefaults 讀取天氣資料
+ private func loadWeatherData() -> WeatherData {
+ let sharedDefaults = UserDefaults(suiteName: "group.com.exptech.dpip")
+
+ guard let defaults = sharedDefaults else {
+ return WeatherData.placeholder
+ }
+
+ let hasError = defaults.bool(forKey: "has_error")
+
+ if hasError {
+ let errorMessage = defaults.string(forKey: "error_message") ?? "無法載入天氣"
+ return WeatherData(
+ weatherStatus: errorMessage,
+ weatherCode: 0,
+ temperature: 0,
+ feelsLike: 0,
+ humidity: 0,
+ windSpeed: 0,
+ windDirection: "-",
+ rain: 0,
+ stationName: "",
+ stationDistance: 0,
+ updateTime: 0,
+ hasError: true,
+ errorMessage: errorMessage
+ )
+ }
+
+ let temperature = defaults.numberValue(forKey: "temperature") ?? 0
+ let feelsLike = defaults.numberValue(forKey: "feels_like") ?? 0
+ let humidity = defaults.numberValue(forKey: "humidity") ?? 0
+ let windSpeed = defaults.numberValue(forKey: "wind_speed") ?? 0
+ let rain = defaults.numberValue(forKey: "rain") ?? 0
+ let stationDistance = defaults.numberValue(forKey: "station_distance") ?? 0
+ let updateRaw = defaults.numberValue(forKey: "update_time") ?? 0
+ let updateSeconds = updateRaw >= 1_000_000_000_000 ? updateRaw / 1000 : updateRaw
+
+ return WeatherData(
+ weatherStatus: defaults.string(forKey: "weather_status") ?? "晴天",
+ weatherCode: defaults.integer(forKey: "weather_code"),
+ temperature: temperature,
+ feelsLike: feelsLike,
+ humidity: humidity,
+ windSpeed: windSpeed,
+ windDirection: defaults.string(forKey: "wind_direction") ?? "-",
+ rain: rain,
+ stationName: defaults.string(forKey: "station_name") ?? "",
+ stationDistance: stationDistance,
+ updateTime: Int(updateSeconds),
+ hasError: false,
+ errorMessage: ""
+ )
+ }
+}
+
+private extension UserDefaults {
+ func numberValue(forKey key: String) -> Double? {
+ if let number = value(forKey: key) as? NSNumber {
+ return number.doubleValue
+ }
+ if let string = string(forKey: key), let value = Double(string) {
+ return value
+ }
+ return nil
+ }
+}
+
+// MARK: - Timeline Entry
+struct WeatherEntry: TimelineEntry {
+ let date: Date
+ let weather: WeatherData
+}
+
+// MARK: - Widget View
+struct WeatherWidgetEntryView : View {
+ var entry: WeatherProvider.Entry
+
+ @Environment(\.widgetFamily) var widgetFamily
+
+ var body: some View {
+ contentView()
+ }
+
+ @ViewBuilder
+ private func contentView() -> some View {
+ if entry.weather.hasError {
+ errorView()
+ } else {
+ switch widgetFamily {
+ case .systemSmall:
+ smallLayout()
+ default:
+ mediumLayout()
+ }
+ }
+ }
+
+ @ViewBuilder
+ private func errorView() -> some View {
+ VStack(spacing: 8) {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .font(.system(size: 32))
+ .foregroundColor(.white.opacity(0.7))
+
+ Text(entry.weather.errorMessage)
+ .font(.system(size: 14, weight: .medium))
+ .foregroundColor(.white)
+ .multilineTextAlignment(.center)
+ .lineLimit(2)
+ .minimumScaleFactor(0.8)
+ }
+ }
+
+ @ViewBuilder
+ private func mediumLayout() -> some View {
+ VStack(alignment: .leading, spacing: 6) {
+ // 頂部:天氣狀態和時間
+ HStack(spacing: 8) {
+ Image(systemName: getWeatherIcon(code: entry.weather.weatherCode))
+ .font(.system(size: 22))
+ .foregroundColor(.white)
+
+ Text(entry.weather.weatherStatus)
+ .font(.system(size: 15, weight: .bold))
+ .foregroundColor(.white)
+ .lineLimit(1)
+ .minimumScaleFactor(0.8)
+
+ Spacer()
+
+ Text(formatTime(timestamp: entry.weather.updateTime))
+ .font(.system(size: 11))
+ .foregroundColor(.white.opacity(0.8))
+ }
+
+ Spacer(minLength: 0)
+
+ // 中間:溫度資訊
+ VStack(spacing: 4) {
+ Text("\(Int(entry.weather.temperature))°")
+ .font(.system(size: 40, weight: .thin))
+ .foregroundColor(.white)
+ .minimumScaleFactor(0.7)
+
+ Text("體感 \(Int(entry.weather.feelsLike))°")
+ .font(.system(size: 11, weight: .medium))
+ .foregroundColor(.white.opacity(0.9))
+ .padding(.horizontal, 10)
+ .padding(.vertical, 3)
+ .background(Color.white.opacity(0.2))
+ .clipShape(RoundedRectangle(cornerRadius: 10))
+ }
+ .frame(maxWidth: .infinity)
+
+ Spacer(minLength: 0)
+
+ // 底部:詳細資訊
+ HStack(spacing: 6) {
+ InfoItem(label: "濕度", value: "\(Int(entry.weather.humidity))%")
+ InfoItem(label: "風速", value: String(format: "%.1fm/s", entry.weather.windSpeed))
+ InfoItem(label: "風向", value: entry.weather.windDirection)
+ InfoItem(label: "降雨", value: String(format: "%.1fmm", entry.weather.rain))
+ }
+
+ // 氣象站資訊
+ if !entry.weather.stationName.isEmpty {
+ Text("\(entry.weather.stationName)氣象站 · \(String(format: "%.1f", entry.weather.stationDistance))km")
+ .font(.system(size: 9))
+ .foregroundColor(.white.opacity(0.7))
+ .frame(maxWidth: .infinity, alignment: .center)
+ .lineLimit(1)
+ .minimumScaleFactor(0.8)
+ }
+ }
+ }
+
+ @ViewBuilder
+ private func smallLayout() -> some View {
+ VStack(alignment: .leading, spacing: 6) {
+ HStack(spacing: 6) {
+ Image(systemName: getWeatherIcon(code: entry.weather.weatherCode))
+ .font(.system(size: 20))
+ .foregroundColor(.white)
+
+ Text(entry.weather.weatherStatus)
+ .font(.system(size: 14, weight: .semibold))
+ .foregroundColor(.white)
+ .lineLimit(1)
+ .minimumScaleFactor(0.7)
+ }
+
+ Text("\(Int(entry.weather.temperature))°")
+ .font(.system(size: 38, weight: .thin))
+ .foregroundColor(.white)
+ .lineLimit(1)
+ .minimumScaleFactor(0.6)
+
+ Text("體感 \(Int(entry.weather.feelsLike))°")
+ .font(.system(size: 11))
+ .foregroundColor(.white.opacity(0.9))
+ .lineLimit(1)
+ .minimumScaleFactor(0.8)
+
+ HStack(spacing: 8) {
+ MiniInfoItem(label: "濕度", value: "\(Int(entry.weather.humidity))%")
+ MiniInfoItem(label: "風速", value: String(format: "%.1fm/s", entry.weather.windSpeed))
+ }
+
+ Spacer(minLength: 2)
+
+ Text(formatTime(timestamp: entry.weather.updateTime))
+ .font(.system(size: 10))
+ .foregroundColor(.white.opacity(0.8))
+ .frame(maxWidth: .infinity, alignment: .trailing)
+ }
+ }
+
+ // 詳細資訊項目
+ private func InfoItem(label: String, value: String) -> some View {
+ VStack(spacing: 2) {
+ Text(label)
+ .font(.system(size: 9))
+ .foregroundColor(.white.opacity(0.7))
+
+ Text(value)
+ .font(.system(size: 11, weight: .semibold))
+ .foregroundColor(.white)
+ .lineLimit(1)
+ .minimumScaleFactor(0.8)
+ }
+ .frame(maxWidth: .infinity)
+ }
+
+ private func MiniInfoItem(label: String, value: String) -> some View {
+ VStack(spacing: 1) {
+ Text(label)
+ .font(.system(size: 8))
+ .foregroundColor(.white.opacity(0.7))
+ .lineLimit(1)
+
+ Text(value)
+ .font(.system(size: 10, weight: .semibold))
+ .foregroundColor(.white)
+ .lineLimit(1)
+ .minimumScaleFactor(0.7)
+ }
+ .frame(maxWidth: .infinity)
+ }
+
+ // 取得天氣圖示
+ private func getWeatherIcon(code: Int) -> String {
+ switch code {
+ case 1: return "sun.max.fill" // 晴天
+ case 2, 3: return "cloud.sun.fill" // 多雲
+ case 4, 5, 6, 7: return "cloud.fill" // 陰天/霧
+ case 8, 9, 10, 11, 12, 13, 14: return "cloud.rain.fill" // 雨天
+ case 15, 16, 17, 18: return "cloud.bolt.rain.fill" // 雷雨
+ default: return "sun.max.fill"
+ }
+ }
+
+ // 格式化時間
+ private func formatTime(timestamp: Int) -> String {
+ let date = Date(timeIntervalSince1970: TimeInterval(timestamp))
+ let formatter = DateFormatter()
+ formatter.dateFormat = "HH:mm"
+ formatter.timeZone = TimeZone.current
+ return formatter.string(from: date)
+ }
+}
+
+// MARK: - Widget Configuration
+struct WeatherWidget: Widget {
+ let kind: String = "WeatherWidget"
+
+ var body: some WidgetConfiguration {
+ StaticConfiguration(kind: kind, provider: WeatherProvider()) { entry in
+ if #available(iOS 17.0, *) {
+ WeatherWidgetEntryView(entry: entry)
+ .padding(16)
+ .containerBackground(for: .widget) {
+ WeatherWidget.backgroundGradient
+ }
+ } else {
+ WeatherWidgetEntryView(entry: entry)
+ .padding(16)
+ .background(WeatherWidget.backgroundGradient)
+ }
+ }
+ .configurationDisplayName("即時天氣")
+ .description("顯示所在地即時天氣資訊")
+ .supportedFamilies([.systemSmall, .systemMedium])
+ }
+
+ private static var backgroundGradient: LinearGradient {
+ LinearGradient(
+ gradient: Gradient(colors: [
+ Color(red: 0.12, green: 0.53, blue: 0.90),
+ Color(red: 0.10, green: 0.46, blue: 0.82),
+ Color(red: 0.08, green: 0.40, blue: 0.75)
+ ]),
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ )
+ }
+}
+
+// MARK: - Preview
+#if DEBUG
+@available(iOS 17.0, *)
+#Preview(as: .systemSmall) {
+ WeatherWidget()
+} timeline: {
+ WeatherEntry(date: .now, weather: .placeholder)
+}
+
+@available(iOS 17.0, *)
+#Preview(as: .systemMedium) {
+ WeatherWidget()
+} timeline: {
+ WeatherEntry(date: .now, weather: .placeholder)
+}
+#endif
diff --git a/ios/WeatherWidget/WeatherWidgetBundle.swift b/ios/WeatherWidget/WeatherWidgetBundle.swift
new file mode 100644
index 000000000..b58960d52
--- /dev/null
+++ b/ios/WeatherWidget/WeatherWidgetBundle.swift
@@ -0,0 +1,18 @@
+//
+// WeatherWidgetBundle.swift
+// WeatherWidget
+//
+// Created by YuYu 1015 on 11/19/R7.
+//
+
+import WidgetKit
+import SwiftUI
+
+@main
+struct WeatherWidgetBundle: WidgetBundle {
+ var body: some Widget {
+ WeatherWidget()
+ WeatherWidgetControl()
+ WeatherWidgetLiveActivity()
+ }
+}
diff --git a/ios/WeatherWidget/WeatherWidgetControl.swift b/ios/WeatherWidget/WeatherWidgetControl.swift
new file mode 100644
index 000000000..0af535e1c
--- /dev/null
+++ b/ios/WeatherWidget/WeatherWidgetControl.swift
@@ -0,0 +1,54 @@
+//
+// WeatherWidgetControl.swift
+// WeatherWidget
+//
+// Created by YuYu 1015 on 11/19/R7.
+//
+
+import AppIntents
+import SwiftUI
+import WidgetKit
+
+struct WeatherWidgetControl: ControlWidget {
+ var body: some ControlWidgetConfiguration {
+ StaticControlConfiguration(
+ kind: "com.exptech.dpip.dpip.WeatherWidget",
+ provider: Provider()
+ ) { value in
+ ControlWidgetToggle(
+ "Start Timer",
+ isOn: value,
+ action: StartTimerIntent()
+ ) { isRunning in
+ Label(isRunning ? "On" : "Off", systemImage: "timer")
+ }
+ }
+ .displayName("Timer")
+ .description("A an example control that runs a timer.")
+ }
+}
+
+extension WeatherWidgetControl {
+ struct Provider: ControlValueProvider {
+ var previewValue: Bool {
+ false
+ }
+
+ func currentValue() async throws -> Bool {
+ let isRunning = true // Check if the timer is running
+ return isRunning
+ }
+ }
+}
+
+struct StartTimerIntent: SetValueIntent {
+ static let title: LocalizedStringResource = "Start a timer"
+
+ @Parameter(title: "Timer is running")
+ var value: Bool
+
+ func perform() async throws -> some IntentResult {
+ // Start / stop the timer based on `value`.
+ return .result()
+ }
+}
diff --git a/ios/WeatherWidget/WeatherWidgetLiveActivity.swift b/ios/WeatherWidget/WeatherWidgetLiveActivity.swift
new file mode 100644
index 000000000..071ab422a
--- /dev/null
+++ b/ios/WeatherWidget/WeatherWidgetLiveActivity.swift
@@ -0,0 +1,80 @@
+//
+// WeatherWidgetLiveActivity.swift
+// WeatherWidget
+//
+// Created by YuYu 1015 on 11/19/R7.
+//
+
+import ActivityKit
+import WidgetKit
+import SwiftUI
+
+struct WeatherWidgetAttributes: 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 WeatherWidgetLiveActivity: Widget {
+ var body: some WidgetConfiguration {
+ ActivityConfiguration(for: WeatherWidgetAttributes.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 WeatherWidgetAttributes {
+ fileprivate static var preview: WeatherWidgetAttributes {
+ WeatherWidgetAttributes(name: "World")
+ }
+}
+
+extension WeatherWidgetAttributes.ContentState {
+ fileprivate static var smiley: WeatherWidgetAttributes.ContentState {
+ WeatherWidgetAttributes.ContentState(emoji: "😀")
+ }
+
+ fileprivate static var starEyes: WeatherWidgetAttributes.ContentState {
+ WeatherWidgetAttributes.ContentState(emoji: "🤩")
+ }
+}
+
+#Preview("Notification", as: .content, using: WeatherWidgetAttributes.preview) {
+ WeatherWidgetLiveActivity()
+} contentStates: {
+ WeatherWidgetAttributes.ContentState.smiley
+ WeatherWidgetAttributes.ContentState.starEyes
+}
diff --git a/ios/WeatherWidgetExtension.entitlements b/ios/WeatherWidgetExtension.entitlements
new file mode 100644
index 000000000..b194b1d1d
--- /dev/null
+++ b/ios/WeatherWidgetExtension.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.com.exptech.dpip
+
+
+
diff --git a/lib/app/home/page.dart b/lib/app/home/page.dart
index 6552e0f47..c2522f6e0 100644
--- a/lib/app/home/page.dart
+++ b/lib/app/home/page.dart
@@ -26,6 +26,8 @@ import 'package:dpip/core/gps_location.dart';
import 'package:dpip/core/i18n.dart';
import 'package:dpip/core/preference.dart';
import 'package:dpip/core/providers.dart';
+import 'package:dpip/core/widget_background.dart';
+import 'package:dpip/core/widget_service.dart';
import 'package:dpip/global.dart';
import 'package:dpip/utils/constants.dart';
import 'package:dpip/models/settings/ui.dart';
@@ -72,11 +74,23 @@ class _HomePageState extends State with WidgetsBindingObserver {
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
- WidgetsBinding.instance.addPostFrameCallback((_) => _checkVersion());
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _checkVersion();
+ _initializeWidget();
+ });
GlobalProviders.location.$code.addListener(_refresh);
_refresh();
}
+ /// 初始化桌面小部件
+ Future _initializeWidget() async {
+ // 註冊週期性背景更新 (每15分鐘 - Android WorkManager 最小值)
+ await WidgetBackground.registerPeriodicUpdate(frequencyMinutes: 15);
+
+ // 立即更新一次小部件
+ await WidgetService.updateWidget();
+ }
+
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
@@ -130,7 +144,13 @@ class _HomePageState extends State with WidgetsBindingObserver {
_refreshIndicatorKey.currentState?.show();
- await Future.wait([_fetchWeather(code), _fetchRealtimeRegion(code), _fetchHistory(code, isOutOfService)]);
+ await Future.wait([
+ _fetchWeather(code),
+ _fetchRealtimeRegion(code),
+ _fetchHistory(code, isOutOfService),
+ // 同時更新桌面小部件
+ WidgetService.updateWidget(),
+ ]);
if (mounted) {
setState(() => _isLoading = false);
diff --git a/lib/app/settings/location/select/[city]/page.dart b/lib/app/settings/location/select/[city]/page.dart
index 667faa159..2d7f2eeed 100644
--- a/lib/app/settings/location/select/[city]/page.dart
+++ b/lib/app/settings/location/select/[city]/page.dart
@@ -10,6 +10,7 @@ import 'package:dpip/api/exptech.dart';
import 'package:dpip/app/settings/location/page.dart';
import 'package:dpip/core/i18n.dart';
import 'package:dpip/core/preference.dart';
+import 'package:dpip/core/widget_service.dart';
import 'package:dpip/global.dart';
import 'package:dpip/models/settings/location.dart';
import 'package:dpip/utils/extensions/build_context.dart';
@@ -18,6 +19,7 @@ import 'package:dpip/utils/toast.dart';
import 'package:dpip/widgets/list/list_section.dart';
import 'package:dpip/widgets/list/list_tile.dart';
import 'package:dpip/widgets/ui/loading_icon.dart';
+import 'package:home_widget/home_widget.dart';
class SettingsLocationSelectCityPage extends StatefulWidget {
final String city;
@@ -77,7 +79,14 @@ class _SettingsLocationSelectCityPageState extends State('widget_latitude', town.lat);
+ await HomeWidget.saveWidgetData('widget_longitude', town.lng);
+
+ // 5. 更新 widget
+ await WidgetService.updateWidget();
+
+ // 6. 返回所在地設定頁面
if (!context.mounted) return;
context.popUntil(SettingsLocationPage.route);
} catch (e, s) {
diff --git a/lib/core/service.dart b/lib/core/service.dart
index 48b3ece32..c6adf9f4d 100644
--- a/lib/core/service.dart
+++ b/lib/core/service.dart
@@ -14,6 +14,7 @@ import 'package:dpip/utils/log.dart';
import 'package:flutter/services.dart';
import 'package:geojson_vi/geojson_vi.dart';
import 'package:geolocator/geolocator.dart';
+import 'package:home_widget/home_widget.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -409,5 +410,22 @@ class LocationService {
Preference.locationCode = result?.code;
Preference.locationLatitude = position?.latitude;
Preference.locationLongitude = position?.longitude;
+
+ // 保存座標到 widget 用的 SharedPreferences
+ if (position != null) {
+ await _$saveLocationToWidget(position.latitude, position.longitude);
+ }
+ }
+
+ /// 保存位置資訊到 Widget
+ @pragma('vm:entry-point')
+ static Future _$saveLocationToWidget(double latitude, double longitude) async {
+ try {
+ await HomeWidget.saveWidgetData('widget_latitude', latitude);
+ await HomeWidget.saveWidgetData('widget_longitude', longitude);
+ TalkerManager.instance.debug('Widget location saved: $latitude, $longitude');
+ } catch (e, s) {
+ TalkerManager.instance.error('Failed to save location to widget', e, s);
+ }
}
}
diff --git a/lib/core/widget_background.dart b/lib/core/widget_background.dart
new file mode 100644
index 000000000..c5887b7de
--- /dev/null
+++ b/lib/core/widget_background.dart
@@ -0,0 +1,137 @@
+import 'dart:io';
+import 'package:dpip/core/widget_service.dart';
+import 'package:dpip/global.dart';
+import 'package:dpip/utils/log.dart';
+import 'package:workmanager/workmanager.dart';
+
+final talker = TalkerManager.instance;
+
+/// 背景任務處理器
+/// 由 Workmanager 呼叫,在背景執行小部件更新
+@pragma('vm:entry-point')
+void callbackDispatcher() {
+ Workmanager().executeTask((task, inputData) async {
+ talker.debug('[WidgetBackground] 執行背景任務: $task');
+
+ try {
+ switch (task) {
+ case WidgetBackground.taskUpdateWidget:
+ // 初始化必要的全域資料
+ await Global.init();
+
+ // 更新小部件
+ await WidgetService.updateWidget();
+ break;
+
+ default:
+ talker.warning('[WidgetBackground] 未知任務: $task');
+ }
+
+ return Future.value(true);
+ } catch (e, stack) {
+ talker.error('[WidgetBackground] 背景任務失敗', e, stack);
+ return Future.value(false);
+ }
+ });
+}
+
+/// 小部件背景更新管理
+class WidgetBackground {
+ static const String taskUpdateWidget = 'widget_update_weather';
+
+ /// 初始化背景任務
+ static Future initialize() async {
+ // 只有 Android 需要初始化 Workmanager
+ // iOS 使用 WidgetKit 的內建 Timeline 機制
+ if (!Platform.isAndroid) {
+ talker.info('[WidgetBackground] iOS 使用 WidgetKit Timeline,無需額外初始化');
+ return;
+ }
+
+ try {
+ await Workmanager().initialize(
+ callbackDispatcher,
+ isInDebugMode: false, // 設為 true 可查看詳細日誌
+ );
+
+ talker.info('[WidgetBackground] Workmanager 初始化成功');
+ } catch (e, stack) {
+ talker.error('[WidgetBackground] Workmanager 初始化失敗', e, stack);
+ }
+ }
+
+ /// 註冊週期性更新任務
+ ///
+ /// [frequencyMinutes] - 更新頻率(分鐘),最小值為15分鐘 (Android WorkManager 系統限制)
+ /// iOS 不需要註冊週期性任務,使用 WidgetKit Timeline
+ static Future registerPeriodicUpdate({int frequencyMinutes = 15}) async {
+ // iOS 使用 WidgetKit 的 Timeline,在 Swift 端自動處理
+ if (!Platform.isAndroid) {
+ talker.info('[WidgetBackground] iOS 使用 WidgetKit Timeline (${frequencyMinutes}分鐘自動更新)');
+ return;
+ }
+
+ try {
+ // 確保頻率不低於15分鐘 (Android WorkManager 限制)
+ final frequency = frequencyMinutes < 15 ? 15 : frequencyMinutes;
+
+ await Workmanager().registerPeriodicTask(
+ taskUpdateWidget,
+ taskUpdateWidget,
+ frequency: Duration(minutes: frequency),
+ constraints: Constraints(
+ networkType: NetworkType.connected, // 需要網路連線
+ requiresBatteryNotLow: false, // 電量低時也執行
+ requiresCharging: false, // 不需要充電
+ requiresDeviceIdle: false, // 不需要裝置閒置
+ requiresStorageNotLow: false, // 不需要儲存空間充足
+ ),
+ existingWorkPolicy: ExistingPeriodicWorkPolicy.replace, // 替換現有任務
+ backoffPolicy: BackoffPolicy.exponential, // 失敗後的重試策略
+ backoffPolicyDelay: Duration(minutes: 5), // 重試延遲
+ );
+
+ talker.info('[WidgetBackground] 已註冊週期性更新,頻率: $frequency 分鐘');
+ } catch (e, stack) {
+ talker.error('[WidgetBackground] 註冊週期性更新失敗', e, stack);
+ }
+ }
+
+ /// 註冊一次性立即更新
+ static Future registerImmediateUpdate() async {
+ try {
+ await Workmanager().registerOneOffTask(
+ '${taskUpdateWidget}_immediate',
+ taskUpdateWidget,
+ constraints: Constraints(
+ networkType: NetworkType.connected,
+ ),
+ existingWorkPolicy: ExistingWorkPolicy.replace,
+ );
+
+ talker.debug('[WidgetBackground] 已註冊立即更新任務');
+ } catch (e, stack) {
+ talker.error('[WidgetBackground] 註冊立即更新失敗', e, stack);
+ }
+ }
+
+ /// 取消所有背景任務
+ static Future cancelAll() async {
+ try {
+ await Workmanager().cancelAll();
+ talker.info('[WidgetBackground] 已取消所有背景任務');
+ } catch (e, stack) {
+ talker.error('[WidgetBackground] 取消背景任務失敗', e, stack);
+ }
+ }
+
+ /// 取消特定任務
+ static Future cancelTask(String taskName) async {
+ try {
+ await Workmanager().cancelByUniqueName(taskName);
+ talker.info('[WidgetBackground] 已取消任務: $taskName');
+ } catch (e, stack) {
+ talker.error('[WidgetBackground] 取消任務失敗: $taskName', e, stack);
+ }
+ }
+}
diff --git a/lib/core/widget_service.dart b/lib/core/widget_service.dart
new file mode 100644
index 000000000..06df699b8
--- /dev/null
+++ b/lib/core/widget_service.dart
@@ -0,0 +1,221 @@
+import 'dart:io';
+import 'dart:math';
+import 'package:dpip/api/exptech.dart';
+import 'package:dpip/api/model/weather_schema.dart';
+import 'package:dpip/core/gps_location.dart';
+import 'package:dpip/core/preference.dart';
+import 'package:dpip/global.dart';
+import 'package:dpip/utils/log.dart';
+import 'package:flutter/services.dart';
+import 'package:home_widget/home_widget.dart';
+
+final talker = TalkerManager.instance;
+
+/// 天氣桌面小部件服務
+/// 負責獲取天氣資料並更新桌面小部件
+class WidgetService {
+ static const String _widgetNameAndroid = 'WeatherWidgetProvider';
+ static const String _widgetNameAndroidSmall = 'WeatherWidgetSmallProvider';
+ static const String _widgetNameIOS = 'WeatherWidget';
+
+ /// 更新小部件資料
+ static Future updateWidget() async {
+ try {
+ // iOS 需要設定 App Group
+ if (Platform.isIOS) {
+ await HomeWidget.setAppGroupId('group.com.exptech.dpip');
+ }
+
+ talker.debug('[WidgetService] 開始更新小部件');
+
+ // 1. 取得位置資訊
+ await _ensureLocationData();
+
+ final lat = Preference.locationLatitude;
+ final lon = Preference.locationLongitude;
+
+ if (lat == null || lon == null) {
+ talker.warning('[WidgetService] 位置資訊不可用');
+ await _saveErrorState('位置未設定');
+ return;
+ }
+
+ // 2. 獲取天氣資料
+ final weather = await _fetchWeatherData(lat, lon);
+
+ if (weather == null) {
+ talker.warning('[WidgetService] 無法獲取天氣資料');
+ await _saveErrorState('無法獲取天氣');
+ return;
+ }
+
+ // 3. 計算體感溫度
+ final feelsLike = _calculateFeelsLike(
+ weather.data.temperature,
+ weather.data.humidity,
+ weather.data.wind.speed,
+ );
+
+ // 4. 儲存資料到 SharedPreferences/UserDefaults
+ await _saveWidgetData(weather, feelsLike);
+
+ // 5. 觸發小部件更新 (更新所有小部件變體)
+ if (Platform.isAndroid) {
+ // 更新標準版和小方形版
+ // 注意:在背景更新時,需要確保真正觸發 widget UI 更新
+ await HomeWidget.updateWidget(androidName: _widgetNameAndroid);
+ await HomeWidget.updateWidget(androidName: _widgetNameAndroidSmall);
+
+ // 額外確保:手動觸發 widget 更新(用於背景更新場景)
+ // 這會發送 APPWIDGET_UPDATE broadcast 來觸發 onUpdate 方法
+ await _forceAndroidWidgetUpdate();
+ } else {
+ await HomeWidget.updateWidget(iOSName: _widgetNameIOS);
+
+ // iOS: 主動請求 Timeline 重新載入
+ await _reloadIOSTimeline();
+ }
+
+ talker.info('[WidgetService] 小部件更新成功');
+ } catch (e, stack) {
+ talker.error('[WidgetService] 更新失敗', e, stack);
+ await _saveErrorState('更新失敗');
+ }
+ }
+
+ /// 確保位置資料可用
+ static Future _ensureLocationData() async {
+ await Preference.reload();
+
+ // 如果是自動定位模式,更新GPS位置
+ if (Preference.locationAuto == true) {
+ await updateLocationFromGPS();
+ } else {
+ // 使用手動設定的位置
+ final code = Preference.locationCode;
+ if (code != null) {
+ final location = Global.location[code];
+ if (location != null) {
+ Preference.locationLatitude = location.lat;
+ Preference.locationLongitude = location.lng;
+ }
+ }
+ }
+ }
+
+ /// 獲取天氣資料
+ static Future _fetchWeatherData(double lat, double lon) async {
+ try {
+ final response = await ExpTech().getWeatherRealtimeByCoords(lat, lon);
+ return response;
+ } catch (e) {
+ talker.error('[WidgetService] 獲取天氣資料失敗', e);
+ return null;
+ }
+ }
+
+ /// 計算體感溫度 (與 weather_header.dart 相同邏輯)
+ static double _calculateFeelsLike(double temperature, double humidity, double windSpeed) {
+ final e = humidity / 100 * 6.105 * exp(17.27 * temperature / (temperature + 237.3));
+ return temperature + 0.33 * e - 0.7 * windSpeed - 4.0;
+ }
+
+ /// 儲存小部件資料
+ static Future _saveWidgetData(RealtimeWeather weather, double feelsLike) async {
+ // 基本天氣資訊
+ await HomeWidget.saveWidgetData('weather_status', weather.data.weather);
+ await HomeWidget.saveWidgetData('weather_code', weather.data.weatherCode);
+ await HomeWidget.saveWidgetData('temperature', weather.data.temperature);
+ await HomeWidget.saveWidgetData('feels_like', feelsLike);
+
+ // 詳細氣象資料
+ await HomeWidget.saveWidgetData('humidity', weather.data.humidity);
+ await HomeWidget.saveWidgetData('wind_speed', weather.data.wind.speed);
+ await HomeWidget.saveWidgetData('wind_direction', weather.data.wind.direction);
+ await HomeWidget.saveWidgetData('wind_beaufort', weather.data.wind.beaufort);
+ await HomeWidget.saveWidgetData('pressure', weather.data.pressure);
+ await HomeWidget.saveWidgetData('rain', weather.data.rain);
+ await HomeWidget.saveWidgetData('visibility', weather.data.visibility);
+
+ // 陣風資料 (選用)
+ if (weather.data.gust.speed > 0) {
+ await HomeWidget.saveWidgetData('gust_speed', weather.data.gust.speed);
+ await HomeWidget.saveWidgetData('gust_beaufort', weather.data.gust.beaufort);
+ }
+
+ // 日照時數 (選用)
+ if (weather.data.sunshine >= 0) {
+ await HomeWidget.saveWidgetData('sunshine', weather.data.sunshine);
+ }
+
+ // 氣象站資訊
+ await HomeWidget.saveWidgetData('station_name', weather.station.name);
+ await HomeWidget.saveWidgetData('station_distance', weather.station.distance);
+
+ // 更新時間
+ await HomeWidget.saveWidgetData('update_time', weather.time);
+
+ // 狀態標記
+ await HomeWidget.saveWidgetData('has_error', false);
+ await HomeWidget.saveWidgetData('error_message', '');
+ }
+
+ /// 儲存錯誤狀態
+ static Future _saveErrorState(String message) async {
+ await HomeWidget.saveWidgetData('has_error', true);
+ await HomeWidget.saveWidgetData('error_message', message);
+
+ if (Platform.isAndroid) {
+ await HomeWidget.updateWidget(androidName: _widgetNameAndroid);
+ await HomeWidget.updateWidget(androidName: _widgetNameAndroidSmall);
+ await _forceAndroidWidgetUpdate();
+ } else {
+ await HomeWidget.updateWidget(iOSName: _widgetNameIOS);
+ await _reloadIOSTimeline();
+ }
+ }
+
+ /// 清除小部件資料 (用於登出或重置)
+ static Future clearWidget() async {
+ await HomeWidget.saveWidgetData('has_error', true);
+ await HomeWidget.saveWidgetData('error_message', '已清除');
+
+ if (Platform.isAndroid) {
+ await HomeWidget.updateWidget(androidName: _widgetNameAndroid);
+ await HomeWidget.updateWidget(androidName: _widgetNameAndroidSmall);
+ await _forceAndroidWidgetUpdate();
+ } else {
+ await HomeWidget.updateWidget(iOSName: _widgetNameIOS);
+ await _reloadIOSTimeline();
+ }
+ }
+
+ /// 強制觸發 Android widget 更新(用於背景更新場景)
+ /// 發送 APPWIDGET_UPDATE broadcast 來確保 widget UI 真正更新
+ static Future _forceAndroidWidgetUpdate() async {
+ try {
+ const platform = MethodChannel('com.exptech.dpip/widget');
+ await platform.invokeMethod('updateWidgets');
+ talker.debug('[WidgetService] 已手動觸發 Android widget 更新');
+ } catch (e) {
+ // 如果方法通道不存在,使用備用方案:再次調用 updateWidget
+ talker.debug('[WidgetService] 方法通道不可用,使用備用更新方式: $e');
+ // 備用方案:再次調用 updateWidget 確保更新
+ await HomeWidget.updateWidget(androidName: _widgetNameAndroid);
+ await HomeWidget.updateWidget(androidName: _widgetNameAndroidSmall);
+ }
+ }
+
+ /// 重新載入 iOS widget Timeline
+ /// 使用 WidgetCenter 主動請求系統重新載入 Timeline
+ static Future _reloadIOSTimeline() async {
+ try {
+ const platform = MethodChannel('com.exptech.dpip/widget');
+ await platform.invokeMethod('reloadWidgetTimeline');
+ talker.debug('[WidgetService] 已請求 iOS widget Timeline 重新載入');
+ } catch (e) {
+ talker.warning('[WidgetService] 無法重新載入 iOS Timeline: $e');
+ // iOS Timeline 會自動在設定的時間更新,這裡只是主動請求
+ }
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
index 85265e6f4..28c53a6e3 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -10,6 +10,7 @@ import 'package:dpip/core/preference.dart';
import 'package:dpip/core/providers.dart';
import 'package:dpip/core/service.dart';
import 'package:dpip/core/update.dart';
+import 'package:dpip/core/widget_background.dart';
import 'package:dpip/global.dart';
import 'package:dpip/utils/log.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
@@ -59,6 +60,7 @@ void main() async {
_loggedTask('AppLocalizations.load', AppLocalizations.load()),
_loggedTask('LocationNameLocalizations.load', LocationNameLocalizations.load()),
_loggedTask('WeatherStationLocalizations.load', WeatherStationLocalizations.load()),
+ _loggedTask('WidgetBackground.initialize', WidgetBackground.initialize()),
]);
final futureWaitEnd = DateTime.now();
diff --git a/pubspec.lock b/pubspec.lock
index e4539b58d..2aaec0d09 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -592,6 +592,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.3.4"
+ home_widget:
+ dependency: "direct main"
+ description:
+ name: home_widget
+ sha256: "908d033514a981f829fd98213909e11a428104327be3b422718aa643ac9d084a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.1"
http:
dependency: "direct main"
description:
@@ -1485,6 +1493,39 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
+ workmanager:
+ dependency: "direct main"
+ description:
+ path: workmanager
+ ref: main
+ resolved-ref: "7f4f870d593f858b0f55bd344c9863a6099cb30f"
+ url: "https://github.com/fluttercommunity/flutter_workmanager.git"
+ source: git
+ version: "0.9.0+3"
+ workmanager_android:
+ dependency: transitive
+ description:
+ name: workmanager_android
+ sha256: "9ae744db4ef891f5fcd2fb8671fccc712f4f96489a487a1411e0c8675e5e8cb7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.0+2"
+ workmanager_apple:
+ dependency: transitive
+ description:
+ name: workmanager_apple
+ sha256: "1cc12ae3cbf5535e72f7ba4fde0c12dd11b757caf493a28e22d684052701f2ca"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.1+2"
+ workmanager_platform_interface:
+ dependency: transitive
+ description:
+ name: workmanager_platform_interface
+ sha256: f40422f10b970c67abb84230b44da22b075147637532ac501729256fcea10a47
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.1+1"
xdg_directories:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 1319e6bef..fd1aadacc 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -69,6 +69,12 @@ dependencies:
talker_flutter: ^5.0.2
timezone: ^0.10.0
url_launcher: ^6.3.1
+ home_widget: ^0.8.1
+ workmanager:
+ git:
+ url: https://github.com/fluttercommunity/flutter_workmanager.git
+ path: workmanager
+ ref: main
dev_dependencies:
build_runner: ^2.10.0