kasy-cli 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kasy.js +122 -7
- package/lib/commands/add.js +2 -2
- package/lib/commands/codemagic.js +11 -4
- package/lib/commands/deploy.js +3 -3
- package/lib/commands/favicon.js +115 -0
- package/lib/commands/icon.js +143 -0
- package/lib/commands/ios.js +20 -5
- package/lib/commands/new.js +8 -20
- package/lib/commands/remove.js +1 -1
- package/lib/commands/reset.js +287 -0
- package/lib/commands/run.js +24 -17
- package/lib/commands/splash.js +3 -4
- package/lib/commands/update.js +1 -1
- package/lib/scaffold/backends/api/patch/README.md +1 -1
- package/lib/scaffold/backends/api/patch/android/app/src/main/AndroidManifest.xml +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +53 -0
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +11 -1
- package/lib/scaffold/backends/firebase/tokens.js +2 -2
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -2
- package/lib/scaffold/backends/supabase/migrations/20240101000011_dedupe_device_tokens.sql +34 -0
- package/lib/scaffold/backends/supabase/patch/README.md +1 -1
- package/lib/scaffold/backends/supabase/patch/android/app/src/main/AndroidManifest.xml +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +43 -0
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +11 -1
- package/lib/utils/apple-release.js +85 -16
- package/lib/utils/checks.js +4 -105
- package/lib/utils/flutter-run.js +173 -0
- package/lib/utils/i18n.js +335 -0
- package/lib/utils/mobile-identity.js +35 -0
- package/lib/utils/ui.js +114 -0
- package/package.json +1 -2
- package/templates/firebase/README.en.md +1 -1
- package/templates/firebase/README.es.md +1 -1
- package/templates/firebase/README.md +1 -1
- package/templates/firebase/android/app/build.gradle.kts +10 -1
- package/templates/firebase/android/app/src/main/AndroidManifest.xml +1 -1
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +25 -1
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +160 -11
- package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +15 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +9 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +12 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +5 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +17 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +5 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_loading.xml +8 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +46 -0
- package/templates/firebase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/xml/mywidget_info.xml +9 -3
- package/templates/firebase/assets/images/favicon.png +0 -0
- package/templates/firebase/assets/images/icon.png +0 -0
- package/templates/firebase/firestore.indexes.json +10 -0
- package/templates/firebase/functions/src/core/data/entities/user_device_entity.ts +3 -0
- package/templates/firebase/functions/src/core/data/repositories/user_device_repository.ts +17 -1
- package/templates/firebase/functions/src/index.ts +1 -0
- package/templates/firebase/functions/src/notifications/device_triggers.ts +58 -0
- package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +116 -33
- package/templates/firebase/ios/Runner/AppDelegate.swift +17 -1
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
- package/templates/firebase/ios/Runner/Info.plist +2 -2
- package/templates/firebase/ios/Runner/es.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/ios/Runner/pt-BR.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/ios/Runner/pt.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/lib/components/kasy_button.dart +8 -0
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +18 -0
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +67 -19
- package/templates/firebase/lib/core/home_widgets/home_widget_service.dart +22 -8
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +16 -4
- package/templates/firebase/lib/core/states/components/maybeshow_component.dart +4 -8
- package/templates/firebase/lib/core/states/user_state_notifier.dart +13 -1
- package/templates/firebase/lib/features/home/home_page.dart +0 -6
- package/templates/firebase/lib/features/notifications/api/device_api.dart +57 -0
- package/templates/firebase/lib/features/notifications/providers/models/notification.dart +11 -1
- package/templates/firebase/lib/features/notifications/repositories/device_repository.dart +9 -0
- package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart +1 -4
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +28 -8
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +44 -11
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +31 -29
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +21 -5
- package/templates/firebase/lib/i18n/en.i18n.json +4 -1
- package/templates/firebase/lib/i18n/es.i18n.json +4 -1
- package/templates/firebase/lib/i18n/pt.i18n.json +4 -1
- package/templates/firebase/pubspec.yaml +6 -1
- package/templates/firebase/test/features/notifications/data/device_api_fake.dart +9 -0
- package/templates/firebase/web/favicon.png +0 -0
- package/templates/firebase/web/icons/Icon-192.png +0 -0
- package/templates/firebase/web/icons/Icon-512.png +0 -0
- package/templates/firebase/web/icons/Icon-maskable-192.png +0 -0
- package/templates/firebase/web/icons/Icon-maskable-512.png +0 -0
- package/templates/firebase/web/index.html +3 -0
- package/templates/firebase/web/manifest.json +3 -3
- package/templates/firebase/assets/images/app_icon.png +0 -0
- package/templates/firebase/assets/images/onboarding/img1.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/onboarding.png +0 -0
- package/templates/firebase/lib/core/states/components/maybe_ask_biometric_setup.dart +0 -88
- package/templates/firebase/lib/features/notifications/shared/notification_permission_bottom_sheet.dart +0 -144
package/lib/utils/ui.js
CHANGED
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const clack = require('@clack/prompts');
|
|
16
|
+
const kleur = require('kleur');
|
|
17
|
+
|
|
18
|
+
function formatElapsedSeconds(startTime) {
|
|
19
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
20
|
+
if (elapsed < 60) return `${elapsed}s`;
|
|
21
|
+
const m = Math.floor(elapsed / 60);
|
|
22
|
+
const s = elapsed % 60;
|
|
23
|
+
return `${m}m ${s}s`;
|
|
24
|
+
}
|
|
16
25
|
|
|
17
26
|
function handleCancel(result, onCancel) {
|
|
18
27
|
if (clack.isCancel(result)) {
|
|
@@ -85,6 +94,109 @@ function cancel(message) { clack.cancel(message); }
|
|
|
85
94
|
*/
|
|
86
95
|
function spinner() { return clack.spinner(); }
|
|
87
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Spinner with an automatic elapsed-time suffix that ticks every second.
|
|
99
|
+
* Same API as spinner() — useful for operations that take >30s so the user
|
|
100
|
+
* sees the clock moving even when the underlying tool emits no progress.
|
|
101
|
+
*
|
|
102
|
+
* const s = ui.timedSpinner();
|
|
103
|
+
* s.start('Deploying…'); // "Deploying… [0s]"
|
|
104
|
+
* // ... 73 seconds later
|
|
105
|
+
* s.stop('Deploy done'); // "Deploy done [1m 13s]"
|
|
106
|
+
*/
|
|
107
|
+
function timedSpinner() {
|
|
108
|
+
const s = clack.spinner();
|
|
109
|
+
let startTime = null;
|
|
110
|
+
let currentMessage = '';
|
|
111
|
+
let tick = null;
|
|
112
|
+
|
|
113
|
+
const render = (msg) => {
|
|
114
|
+
if (!msg) return '';
|
|
115
|
+
if (!startTime) return msg;
|
|
116
|
+
return `${msg} ${kleur.dim(`[${formatElapsedSeconds(startTime)}]`)}`;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const stopTick = () => {
|
|
120
|
+
if (tick) { clearInterval(tick); tick = null; }
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
start(msg) {
|
|
125
|
+
startTime = Date.now();
|
|
126
|
+
currentMessage = msg || '';
|
|
127
|
+
s.start(render(currentMessage));
|
|
128
|
+
tick = setInterval(() => {
|
|
129
|
+
if (currentMessage) s.message(render(currentMessage));
|
|
130
|
+
}, 1000);
|
|
131
|
+
},
|
|
132
|
+
message(msg) {
|
|
133
|
+
currentMessage = msg || '';
|
|
134
|
+
s.message(render(currentMessage));
|
|
135
|
+
},
|
|
136
|
+
stop(msg, code) {
|
|
137
|
+
stopTick();
|
|
138
|
+
const finalMsg = msg != null ? render(msg) : render(currentMessage);
|
|
139
|
+
s.stop(finalMsg, code);
|
|
140
|
+
},
|
|
141
|
+
error(msg) {
|
|
142
|
+
stopTick();
|
|
143
|
+
s.error(msg != null ? render(msg) : render(currentMessage));
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Multi-step spinner where every step shows its own elapsed-time counter.
|
|
150
|
+
* Use for long sequential flows like Firebase project creation so the user
|
|
151
|
+
* can tell which steps are slow.
|
|
152
|
+
*/
|
|
153
|
+
function makeTimedStepper() {
|
|
154
|
+
let current = null;
|
|
155
|
+
let currentMsg = '';
|
|
156
|
+
return {
|
|
157
|
+
next(text) {
|
|
158
|
+
if (current) current.stop(currentMsg);
|
|
159
|
+
current = timedSpinner();
|
|
160
|
+
currentMsg = text;
|
|
161
|
+
current.start(text);
|
|
162
|
+
},
|
|
163
|
+
update(text) {
|
|
164
|
+
if (current) {
|
|
165
|
+
currentMsg = text;
|
|
166
|
+
current.message(text);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
succeed(text) {
|
|
170
|
+
if (current) {
|
|
171
|
+
current.stop(text || currentMsg);
|
|
172
|
+
current = null;
|
|
173
|
+
currentMsg = '';
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
fail(text) {
|
|
177
|
+
if (current) {
|
|
178
|
+
current.error(text || currentMsg);
|
|
179
|
+
current = null;
|
|
180
|
+
currentMsg = '';
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
warn(text) {
|
|
184
|
+
if (current) {
|
|
185
|
+
current.stop(`⚠ ${text || currentMsg}`);
|
|
186
|
+
current = null;
|
|
187
|
+
currentMsg = '';
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
stop() {
|
|
191
|
+
if (current) {
|
|
192
|
+
current.stop(currentMsg);
|
|
193
|
+
current = null;
|
|
194
|
+
currentMsg = '';
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
88
200
|
/**
|
|
89
201
|
* Multi-step spinner: each .next(text) succeeds the previous step
|
|
90
202
|
* with the previous message, then starts a new step with `text`.
|
|
@@ -166,7 +278,9 @@ module.exports = {
|
|
|
166
278
|
note,
|
|
167
279
|
cancel,
|
|
168
280
|
spinner,
|
|
281
|
+
timedSpinner,
|
|
169
282
|
makeStepper,
|
|
283
|
+
makeTimedStepper,
|
|
170
284
|
taskLog,
|
|
171
285
|
progress,
|
|
172
286
|
log,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kasy-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"kasy": "./bin/kasy.js"
|
|
@@ -51,7 +51,6 @@
|
|
|
51
51
|
"fs-extra": "^11.2.0",
|
|
52
52
|
"gradient-string": "^1.2.0",
|
|
53
53
|
"kleur": "^4.1.5",
|
|
54
|
-
"ora": "^8.0.1",
|
|
55
54
|
"prompts": "^2.4.2",
|
|
56
55
|
"yaml": "^2.4.2"
|
|
57
56
|
}
|
|
@@ -76,5 +76,14 @@ flutter {
|
|
|
76
76
|
dependencies {
|
|
77
77
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
|
78
78
|
implementation("com.google.android.gms:play-services-ads-identifier:18.1.0")
|
|
79
|
-
|
|
79
|
+
// strictly() blocks Gradle from upgrading via transitive resolution —
|
|
80
|
+
// the home_widget plugin declares `1.+` and would otherwise pick the
|
|
81
|
+
// latest alpha (1.3.0-alpha01 at time of writing), which requires
|
|
82
|
+
// compileSdk 37 + AGP 9.x and breaks the build.
|
|
83
|
+
implementation("androidx.glance:glance-appwidget") {
|
|
84
|
+
version {
|
|
85
|
+
strictly("1.1.1")
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
implementation("androidx.appcompat:appcompat:1.7.0")
|
|
80
89
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
17
17
|
|
|
18
18
|
<application
|
|
19
|
-
android:label="
|
|
19
|
+
android:label="Kasy App"
|
|
20
20
|
android:name="${applicationName}"
|
|
21
21
|
android:icon="@mipmap/ic_launcher"
|
|
22
22
|
android:localeConfig="@xml/locales_config">
|
package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
package com.aicrus.firebase.kit
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Bundle
|
|
5
|
+
import androidx.appcompat.app.AppCompatDelegate
|
|
3
6
|
import com.google.android.gms.ads.identifier.AdvertisingIdClient
|
|
4
7
|
import io.flutter.embedding.android.FlutterActivity
|
|
5
8
|
import io.flutter.embedding.engine.FlutterEngine
|
|
@@ -12,6 +15,27 @@ import kotlinx.coroutines.withContext
|
|
|
12
15
|
class MainActivity : FlutterActivity() {
|
|
13
16
|
private val CHANNEL = "kasy_kit/advertising_id"
|
|
14
17
|
|
|
18
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
19
|
+
applySavedThemeMode()
|
|
20
|
+
super.onCreate(savedInstanceState)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Forces the night mode to match the user's saved theme preference (read
|
|
24
|
+
// from `shared_preferences`) so the native splash drawable selection
|
|
25
|
+
// (drawable-night vs drawable) follows the in-app choice, not just the OS.
|
|
26
|
+
private fun applySavedThemeMode() {
|
|
27
|
+
val prefs = applicationContext.getSharedPreferences(
|
|
28
|
+
"FlutterSharedPreferences",
|
|
29
|
+
Context.MODE_PRIVATE,
|
|
30
|
+
)
|
|
31
|
+
val mode = when (prefs.getString("flutter.themeMode", null)) {
|
|
32
|
+
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
|
|
33
|
+
"light" -> AppCompatDelegate.MODE_NIGHT_NO
|
|
34
|
+
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
35
|
+
}
|
|
36
|
+
AppCompatDelegate.setDefaultNightMode(mode)
|
|
37
|
+
}
|
|
38
|
+
|
|
15
39
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
|
16
40
|
super.configureFlutterEngine(flutterEngine)
|
|
17
41
|
|
|
@@ -1,32 +1,50 @@
|
|
|
1
1
|
package com.aicrus.firebase.kit
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
4
5
|
import androidx.compose.runtime.Composable
|
|
5
6
|
import androidx.compose.ui.graphics.Color
|
|
6
7
|
import androidx.compose.ui.unit.dp
|
|
7
8
|
import androidx.compose.ui.unit.sp
|
|
8
9
|
import androidx.glance.GlanceId
|
|
9
10
|
import androidx.glance.GlanceModifier
|
|
11
|
+
import androidx.glance.Image
|
|
12
|
+
import androidx.glance.ImageProvider
|
|
13
|
+
import androidx.glance.LocalSize
|
|
14
|
+
import androidx.glance.action.clickable
|
|
10
15
|
import androidx.glance.appwidget.GlanceAppWidget
|
|
16
|
+
import androidx.glance.appwidget.SizeMode
|
|
17
|
+
import androidx.glance.appwidget.action.actionStartActivity
|
|
11
18
|
import androidx.glance.appwidget.provideContent
|
|
12
19
|
import androidx.glance.background
|
|
13
20
|
import androidx.glance.currentState
|
|
14
21
|
import androidx.glance.layout.Alignment
|
|
15
22
|
import androidx.glance.layout.Box
|
|
16
23
|
import androidx.glance.layout.Column
|
|
24
|
+
import androidx.glance.layout.ContentScale
|
|
25
|
+
import androidx.glance.layout.Row
|
|
17
26
|
import androidx.glance.layout.Spacer
|
|
18
27
|
import androidx.glance.layout.fillMaxSize
|
|
28
|
+
import androidx.glance.layout.fillMaxWidth
|
|
19
29
|
import androidx.glance.layout.padding
|
|
30
|
+
import androidx.glance.layout.size
|
|
20
31
|
import androidx.glance.state.GlanceStateDefinition
|
|
32
|
+
import androidx.glance.text.FontStyle
|
|
21
33
|
import androidx.glance.text.FontWeight
|
|
22
34
|
import androidx.glance.text.Text
|
|
23
35
|
import androidx.glance.text.TextStyle
|
|
24
36
|
import androidx.glance.unit.ColorProvider
|
|
25
37
|
import es.antonborri.home_widget.HomeWidgetGlanceState
|
|
26
38
|
import es.antonborri.home_widget.HomeWidgetGlanceStateDefinition
|
|
39
|
+
import java.util.Calendar
|
|
40
|
+
import java.util.Locale
|
|
27
41
|
|
|
28
42
|
class MyWidgetWidget : GlanceAppWidget() {
|
|
29
43
|
|
|
44
|
+
// Recompose when the user resizes the widget so the layout can adapt
|
|
45
|
+
// between small (no "+", no quote) and large (everything visible).
|
|
46
|
+
override val sizeMode: SizeMode = SizeMode.Exact
|
|
47
|
+
|
|
30
48
|
override val stateDefinition: GlanceStateDefinition<*>?
|
|
31
49
|
get() = HomeWidgetGlanceStateDefinition()
|
|
32
50
|
|
|
@@ -39,22 +57,49 @@ class MyWidgetWidget : GlanceAppWidget() {
|
|
|
39
57
|
@Composable
|
|
40
58
|
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
|
|
41
59
|
val prefs = currentState.preferences
|
|
42
|
-
val
|
|
43
|
-
val
|
|
60
|
+
val storedGreeting = prefs.getString("greeting", "") ?: ""
|
|
61
|
+
val storedTitle = prefs.getString("title", "") ?: ""
|
|
44
62
|
val planText = prefs.getString("planText", "") ?: ""
|
|
45
63
|
val isPro = prefs.getString("isPro", "false") == "true"
|
|
64
|
+
val quote = prefs.getString("quote", "") ?: ""
|
|
65
|
+
|
|
66
|
+
// Time/locale-based fallback used when Flutter has not pushed data yet —
|
|
67
|
+
// first install before the app opens. Keeps the widget from rendering
|
|
68
|
+
// blank in the gallery preview.
|
|
69
|
+
val defaults = defaultStrings()
|
|
70
|
+
val greeting = storedGreeting.ifEmpty { defaults.greeting }
|
|
71
|
+
val title = storedTitle.ifEmpty { defaults.hello }
|
|
72
|
+
|
|
73
|
+
val size = LocalSize.current
|
|
74
|
+
val isSmall = size.width < 200.dp
|
|
75
|
+
// Heuristic: tall enough to fit greeting + title + quote without crowding.
|
|
76
|
+
val isLarge = size.height >= 280.dp
|
|
46
77
|
|
|
47
|
-
val bgColor = Color(red = 0.08f, green = 0.03f, blue = 0.16f)
|
|
48
78
|
val white = Color.White
|
|
49
79
|
val whiteSubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.55f)
|
|
80
|
+
val whiteQuote = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.65f)
|
|
50
81
|
val gold = Color(red = 1f, green = 0.84f, blue = 0f)
|
|
51
|
-
val whiteVerySubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.
|
|
82
|
+
val whiteVerySubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.45f)
|
|
52
83
|
|
|
84
|
+
// The gradient lives in its own Image at the bottom of the stack rather
|
|
85
|
+
// than as `.background(ImageProvider(...))`, because in some Glance
|
|
86
|
+
// versions the latter ends up rendered ABOVE the content — the widget
|
|
87
|
+
// shows just the gradient, no text. The explicit Image+Column layering
|
|
88
|
+
// here is deterministic.
|
|
53
89
|
Box(
|
|
54
|
-
modifier = GlanceModifier
|
|
55
|
-
|
|
90
|
+
modifier = GlanceModifier
|
|
91
|
+
.fillMaxSize()
|
|
92
|
+
.clickable(actionStartActivity(launchAppIntent(context))),
|
|
56
93
|
) {
|
|
57
|
-
|
|
94
|
+
Image(
|
|
95
|
+
provider = ImageProvider(R.drawable.widget_gradient_inner),
|
|
96
|
+
contentDescription = null,
|
|
97
|
+
contentScale = ContentScale.FillBounds,
|
|
98
|
+
modifier = GlanceModifier.fillMaxSize(),
|
|
99
|
+
)
|
|
100
|
+
Column(
|
|
101
|
+
modifier = GlanceModifier.fillMaxSize().padding(16.dp),
|
|
102
|
+
) {
|
|
58
103
|
Text(
|
|
59
104
|
text = greeting,
|
|
60
105
|
style = TextStyle(
|
|
@@ -67,21 +112,125 @@ class MyWidgetWidget : GlanceAppWidget() {
|
|
|
67
112
|
text = title,
|
|
68
113
|
style = TextStyle(
|
|
69
114
|
color = ColorProvider(white),
|
|
70
|
-
fontSize =
|
|
115
|
+
fontSize = if (isSmall) 20.sp else 24.sp,
|
|
71
116
|
fontWeight = FontWeight.Bold,
|
|
72
117
|
),
|
|
73
118
|
modifier = GlanceModifier.padding(top = 4.dp),
|
|
74
119
|
)
|
|
120
|
+
if (isLarge && quote.isNotEmpty()) {
|
|
121
|
+
Text(
|
|
122
|
+
text = quote,
|
|
123
|
+
style = TextStyle(
|
|
124
|
+
color = ColorProvider(whiteQuote),
|
|
125
|
+
fontSize = 15.sp,
|
|
126
|
+
fontWeight = FontWeight.Normal,
|
|
127
|
+
fontStyle = FontStyle.Italic,
|
|
128
|
+
),
|
|
129
|
+
maxLines = 4,
|
|
130
|
+
modifier = GlanceModifier.padding(top = 12.dp),
|
|
131
|
+
)
|
|
132
|
+
}
|
|
75
133
|
Spacer(modifier = GlanceModifier.defaultWeight())
|
|
134
|
+
Row(
|
|
135
|
+
modifier = GlanceModifier.fillMaxWidth(),
|
|
136
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
137
|
+
) {
|
|
138
|
+
// Empty planText hides the pill (used in logged-out state).
|
|
139
|
+
if (planText.isNotEmpty()) {
|
|
140
|
+
PlanPill(
|
|
141
|
+
isPro = isPro,
|
|
142
|
+
planText = planText,
|
|
143
|
+
gold = gold,
|
|
144
|
+
whiteVerySubtle = whiteVerySubtle,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
// Small intentionally drops the "+" so the layout breathes —
|
|
148
|
+
// the pill sits flush left like the original design.
|
|
149
|
+
if (!isSmall) {
|
|
150
|
+
Spacer(modifier = GlanceModifier.defaultWeight())
|
|
151
|
+
Image(
|
|
152
|
+
provider = ImageProvider(R.drawable.widget_add_button),
|
|
153
|
+
contentDescription = "Add",
|
|
154
|
+
modifier = GlanceModifier
|
|
155
|
+
.size(34.dp)
|
|
156
|
+
.clickable(actionStartActivity(launchAppIntent(context))),
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@Composable
|
|
165
|
+
private fun PlanPill(
|
|
166
|
+
isPro: Boolean,
|
|
167
|
+
planText: String,
|
|
168
|
+
gold: Color,
|
|
169
|
+
whiteVerySubtle: Color,
|
|
170
|
+
) {
|
|
171
|
+
if (isPro) {
|
|
172
|
+
Box(
|
|
173
|
+
modifier = GlanceModifier
|
|
174
|
+
.background(ImageProvider(R.drawable.widget_pro_pill_bg))
|
|
175
|
+
.padding(horizontal = 10.dp, vertical = 5.dp),
|
|
176
|
+
) {
|
|
177
|
+
Text(
|
|
178
|
+
text = "⭐ $planText",
|
|
179
|
+
style = TextStyle(
|
|
180
|
+
color = ColorProvider(gold),
|
|
181
|
+
fontSize = 11.sp,
|
|
182
|
+
fontWeight = FontWeight.Bold,
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
Box(
|
|
188
|
+
modifier = GlanceModifier
|
|
189
|
+
.background(ImageProvider(R.drawable.widget_plan_pill_bg))
|
|
190
|
+
.padding(horizontal = 10.dp, vertical = 5.dp),
|
|
191
|
+
) {
|
|
76
192
|
Text(
|
|
77
|
-
text =
|
|
193
|
+
text = planText,
|
|
78
194
|
style = TextStyle(
|
|
79
|
-
color = ColorProvider(
|
|
195
|
+
color = ColorProvider(whiteVerySubtle),
|
|
80
196
|
fontSize = 11.sp,
|
|
81
|
-
fontWeight =
|
|
197
|
+
fontWeight = FontWeight.Medium,
|
|
82
198
|
),
|
|
83
199
|
)
|
|
84
200
|
}
|
|
85
201
|
}
|
|
86
202
|
}
|
|
203
|
+
|
|
204
|
+
private data class DefaultStrings(val greeting: String, val hello: String)
|
|
205
|
+
|
|
206
|
+
private fun defaultStrings(): DefaultStrings {
|
|
207
|
+
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
|
208
|
+
val lang = Locale.getDefault().language
|
|
209
|
+
val morning: String; val afternoon: String; val evening: String; val hello: String
|
|
210
|
+
when (lang) {
|
|
211
|
+
"pt" -> { morning = "Bom dia"; afternoon = "Boa tarde"; evening = "Boa noite"; hello = "Olá!" }
|
|
212
|
+
"es" -> { morning = "Buenos días"; afternoon = "Buenas tardes"; evening = "Buenas noches"; hello = "¡Hola!" }
|
|
213
|
+
else -> { morning = "Good morning"; afternoon = "Good afternoon"; evening = "Good evening"; hello = "Hi there!" }
|
|
214
|
+
}
|
|
215
|
+
val greeting = when {
|
|
216
|
+
hour < 12 -> morning
|
|
217
|
+
hour < 18 -> afternoon
|
|
218
|
+
else -> evening
|
|
219
|
+
}
|
|
220
|
+
return DefaultStrings(greeting, hello)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// Builds the same Intent the launcher would fire when the user taps the
|
|
224
|
+
/// app icon: ACTION_MAIN + CATEGORY_LAUNCHER. Without these flags, the
|
|
225
|
+
/// activity receives an empty Intent and go_router lands on the
|
|
226
|
+
/// errorBuilder (the "404 - Page not found" screen).
|
|
227
|
+
private fun launchAppIntent(context: Context): Intent {
|
|
228
|
+
return Intent(context, MainActivity::class.java).apply {
|
|
229
|
+
action = Intent.ACTION_MAIN
|
|
230
|
+
addCategory(Intent.CATEGORY_LAUNCHER)
|
|
231
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
232
|
+
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
|
233
|
+
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
234
|
+
}
|
|
235
|
+
}
|
|
87
236
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
android:width="34dp"
|
|
4
|
+
android:height="34dp"
|
|
5
|
+
android:viewportWidth="34"
|
|
6
|
+
android:viewportHeight="34">
|
|
7
|
+
<path
|
|
8
|
+
android:pathData="M17,0 A17,17 0 1,0 17,34 A17,17 0 1,0 17,0 Z"
|
|
9
|
+
android:fillColor="#2EFFFFFF"/>
|
|
10
|
+
<path
|
|
11
|
+
android:pathData="M17,9 L17,25 M9,17 L25,17"
|
|
12
|
+
android:strokeColor="#FFFFFFFF"
|
|
13
|
+
android:strokeWidth="2.5"
|
|
14
|
+
android:strokeLineCap="round"/>
|
|
15
|
+
</vector>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
|
3
|
+
<gradient
|
|
4
|
+
android:angle="315"
|
|
5
|
+
android:startColor="#FF140829"
|
|
6
|
+
android:endColor="#FF33176B"
|
|
7
|
+
android:type="linear" />
|
|
8
|
+
<corners android:radius="24dp" />
|
|
9
|
+
</shape>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Same gradient as widget_gradient_bg, but without rounded corners.
|
|
3
|
+
Used as the background of the Glance widget itself — the system
|
|
4
|
+
already clips the widget with the OS-provided corner radius, so
|
|
5
|
+
adding corners here would cause a visible double-radius edge. -->
|
|
6
|
+
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
|
7
|
+
<gradient
|
|
8
|
+
android:angle="315"
|
|
9
|
+
android:startColor="#FF140829"
|
|
10
|
+
android:endColor="#FF33176B"
|
|
11
|
+
android:type="linear" />
|
|
12
|
+
</shape>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Fallback preview shown in the widget gallery on launchers that don't
|
|
3
|
+
honor android:previewLayout (Android < 12, or some OEM launchers).
|
|
4
|
+
Layer list: rounded gradient + a hint star to suggest the PRO badge.
|
|
5
|
+
The actual widget overrides this once placed. -->
|
|
6
|
+
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
7
|
+
<item>
|
|
8
|
+
<shape android:shape="rectangle">
|
|
9
|
+
<gradient
|
|
10
|
+
android:angle="315"
|
|
11
|
+
android:startColor="#FF140829"
|
|
12
|
+
android:endColor="#FF33176B"
|
|
13
|
+
android:type="linear" />
|
|
14
|
+
<corners android:radius="24dp" />
|
|
15
|
+
</shape>
|
|
16
|
+
</item>
|
|
17
|
+
</layer-list>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Brief-flash layout shown for ~200ms while Glance composes the real
|
|
3
|
+
widget. Just the gradient — no text — so the user never sees the
|
|
4
|
+
gray default loading layout from the home_widget library. -->
|
|
5
|
+
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
6
|
+
android:layout_width="match_parent"
|
|
7
|
+
android:layout_height="match_parent"
|
|
8
|
+
android:background="@drawable/widget_gradient_bg" />
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Static preview shown in the Android widget gallery (Android 12+).
|
|
3
|
+
Mirrors the real widget layout so the user can see what they're
|
|
4
|
+
adding before they drop it on the home screen. The Glance widget
|
|
5
|
+
replaces this once placed. -->
|
|
6
|
+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
7
|
+
android:layout_width="match_parent"
|
|
8
|
+
android:layout_height="match_parent"
|
|
9
|
+
android:background="@drawable/widget_gradient_bg"
|
|
10
|
+
android:orientation="vertical"
|
|
11
|
+
android:padding="16dp">
|
|
12
|
+
|
|
13
|
+
<TextView
|
|
14
|
+
android:layout_width="wrap_content"
|
|
15
|
+
android:layout_height="wrap_content"
|
|
16
|
+
android:fontFamily="sans-serif-medium"
|
|
17
|
+
android:text="Boa noite"
|
|
18
|
+
android:textColor="#8CFFFFFF"
|
|
19
|
+
android:textSize="11sp" />
|
|
20
|
+
|
|
21
|
+
<TextView
|
|
22
|
+
android:layout_width="wrap_content"
|
|
23
|
+
android:layout_height="wrap_content"
|
|
24
|
+
android:layout_marginTop="4dp"
|
|
25
|
+
android:text="Olá!"
|
|
26
|
+
android:textColor="#FFFFFFFF"
|
|
27
|
+
android:textSize="24sp"
|
|
28
|
+
android:textStyle="bold" />
|
|
29
|
+
|
|
30
|
+
<Space
|
|
31
|
+
android:layout_width="match_parent"
|
|
32
|
+
android:layout_height="0dp"
|
|
33
|
+
android:layout_weight="1" />
|
|
34
|
+
|
|
35
|
+
<TextView
|
|
36
|
+
android:layout_width="wrap_content"
|
|
37
|
+
android:layout_height="wrap_content"
|
|
38
|
+
android:background="@drawable/widget_pro_pill_bg"
|
|
39
|
+
android:paddingHorizontal="10dp"
|
|
40
|
+
android:paddingVertical="5dp"
|
|
41
|
+
android:text="⭐ PRO"
|
|
42
|
+
android:textColor="#FFFFD700"
|
|
43
|
+
android:textSize="11sp"
|
|
44
|
+
android:textStyle="bold" />
|
|
45
|
+
|
|
46
|
+
</LinearLayout>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
2
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
-
android:initialLayout="@layout/
|
|
3
|
+
android:initialLayout="@layout/widget_loading"
|
|
4
|
+
android:previewLayout="@layout/widget_preview"
|
|
5
|
+
android:previewImage="@drawable/widget_preview_image"
|
|
4
6
|
android:minWidth="180dp"
|
|
5
7
|
android:minHeight="180dp"
|
|
8
|
+
android:minResizeWidth="120dp"
|
|
9
|
+
android:minResizeHeight="120dp"
|
|
10
|
+
android:maxResizeWidth="320dp"
|
|
11
|
+
android:maxResizeHeight="320dp"
|
|
6
12
|
android:resizeMode="horizontal|vertical"
|
|
7
|
-
android:updatePeriodMillis="
|
|
13
|
+
android:updatePeriodMillis="0"
|
|
8
14
|
android:description="@string/widget_my_widget_description"
|
|
9
15
|
android:targetCellWidth="2"
|
|
10
16
|
android:targetCellHeight="2"
|
|
11
|
-
|
|
17
|
+
android:widgetCategory="home_screen|keyguard" />
|
|
Binary file
|
|
Binary file
|