kasy-cli 1.4.1 → 1.5.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/lib/commands/new.js +6 -11
- package/lib/commands/update.js +31 -18
- package/lib/scaffold/CHANGELOG.json +5 -5
- package/lib/scaffold/features/widget/lib/core/home_widgets/home_widget_mywidget_service.dart +61 -20
- package/lib/utils/i18n.js +6 -3
- package/package.json +1 -1
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +48 -12
- package/templates/firebase/home_widget_config.json +9 -5
- package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +69 -41
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +60 -19
package/lib/commands/new.js
CHANGED
|
@@ -192,22 +192,17 @@ async function promptOrganizationIfNeeded(tr, onCancel) {
|
|
|
192
192
|
function printBanner(tr) {
|
|
193
193
|
const bar = kleur.gray('─────────────────────────────────────────────────');
|
|
194
194
|
const logo = [
|
|
195
|
-
'
|
|
196
|
-
'
|
|
197
|
-
'
|
|
198
|
-
' ██╔═██╗ ██╔══██║╚════██║ ╚██╔╝ ',
|
|
199
|
-
' ██║ ██╗██║ ██║███████║ ██║ ',
|
|
200
|
-
' ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ ',
|
|
195
|
+
' ╦╔═ ╔═╗ ╔═╗ ╦ ╦',
|
|
196
|
+
' ╠╩╗ ╠═╣ ╚═╗ ╚╦╝',
|
|
197
|
+
' ╩ ╩ ╩ ╩ ╚═╝ ╩ ',
|
|
201
198
|
]
|
|
202
199
|
.map((line) => gradient(['#a78bfa', '#60a5fa'])(line))
|
|
203
200
|
.join('\n');
|
|
204
|
-
|
|
205
|
-
console.log(`\n${bar}`);
|
|
201
|
+
console.log(`\n${bar}\n`);
|
|
206
202
|
console.log(logo);
|
|
207
203
|
console.log('');
|
|
208
|
-
console.log(
|
|
209
|
-
console.log(
|
|
210
|
-
console.log(`${bar}\n`);
|
|
204
|
+
console.log(` ${kleur.dim(tr('new.subtitle2'))}`);
|
|
205
|
+
console.log(`\n${bar}\n`);
|
|
211
206
|
}
|
|
212
207
|
|
|
213
208
|
function printPrerequisites(tr, backend, firebaseSetupMode = 'existing', checkResults = []) {
|
package/lib/commands/update.js
CHANGED
|
@@ -355,33 +355,46 @@ async function runUpdate(module, options = {}) {
|
|
|
355
355
|
const fromLabel = projectVersion || '?';
|
|
356
356
|
console.log(`\n${t('update.status', { from: fromLabel, to: currentVersion })}\n`);
|
|
357
357
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// Changelog per module
|
|
363
|
-
if (Object.keys(changes).length > 0) {
|
|
358
|
+
// Modules/components with actual changelog entries — show what improved + the command
|
|
359
|
+
const modulesWithChanges = Object.keys(changes);
|
|
360
|
+
if (modulesWithChanges.length > 0) {
|
|
364
361
|
console.log(kleur.bold(t('update.changesTitle')));
|
|
365
|
-
for (const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
console.log(kleur.dim(
|
|
362
|
+
for (const mod of modulesWithChanges) {
|
|
363
|
+
for (const { description } of changes[mod]) {
|
|
364
|
+
console.log(` ${kleur.cyan('✦')} ${kleur.bold(mod)} ${kleur.dim('→')} ${kleur.cyan(`kasy update ${mod}`)}`);
|
|
365
|
+
console.log(` ${kleur.dim(description)}`);
|
|
369
366
|
}
|
|
370
367
|
}
|
|
371
368
|
console.log('');
|
|
372
369
|
}
|
|
373
370
|
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
371
|
+
// Modules without changelog entries that can still be re-applied (advanced / recovery)
|
|
372
|
+
const modulesWithoutChanges = patchableModules.filter((m) => !modulesWithChanges.includes(m));
|
|
373
|
+
const hasComponentChanges = modulesWithChanges.includes(COMPONENTS_UPDATE_TARGET);
|
|
374
|
+
|
|
375
|
+
if (modulesWithChanges.length === 0 && !hasComponentChanges) {
|
|
376
|
+
// Nothing new — show everything available
|
|
377
|
+
if (patchableModules.length > 0) {
|
|
378
|
+
console.log(t('update.howToUpdate'));
|
|
379
|
+
for (const m of patchableModules) {
|
|
380
|
+
console.log(kleur.cyan(` kasy update ${m}`));
|
|
381
|
+
}
|
|
382
|
+
console.log('');
|
|
383
|
+
}
|
|
384
|
+
console.log(t('update.howToUpdateComponents'));
|
|
385
|
+
console.log(kleur.cyan(` kasy update ${COMPONENTS_UPDATE_TARGET}`));
|
|
386
|
+
console.log('');
|
|
387
|
+
} else if (modulesWithoutChanges.length > 0) {
|
|
388
|
+
// Some modules have no new changes — show as secondary info
|
|
389
|
+
console.log(kleur.dim(t('update.reapplyTitle')));
|
|
390
|
+
for (const m of modulesWithoutChanges) {
|
|
391
|
+
console.log(kleur.dim(` kasy update ${m}`));
|
|
392
|
+
}
|
|
393
|
+
if (!hasComponentChanges) {
|
|
394
|
+
console.log(kleur.dim(` kasy update ${COMPONENTS_UPDATE_TARGET}`));
|
|
379
395
|
}
|
|
380
396
|
console.log('');
|
|
381
397
|
}
|
|
382
|
-
console.log(t('update.howToUpdateComponents'));
|
|
383
|
-
console.log(kleur.cyan(` kasy update ${COMPONENTS_UPDATE_TARGET}`));
|
|
384
|
-
console.log('');
|
|
385
398
|
}
|
|
386
399
|
|
|
387
400
|
module.exports = { runUpdate };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"1.
|
|
2
|
+
"1.5.0": {
|
|
3
3
|
"modules": {
|
|
4
|
-
"
|
|
5
|
-
"pt": "
|
|
6
|
-
"en": "
|
|
7
|
-
"es": "
|
|
4
|
+
"widget": {
|
|
5
|
+
"pt": "Widget redesenhado: saudação por horário no idioma do usuário (pt/en/es), nome real e status do plano",
|
|
6
|
+
"en": "Redesigned widget: time-of-day greeting in the user's language (pt/en/es), real name and plan status",
|
|
7
|
+
"es": "Widget rediseñado: saludo según el horario en el idioma del usuario (pt/en/es), nombre real y estado del plan"
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
}
|
package/lib/scaffold/features/widget/lib/core/home_widgets/home_widget_mywidget_service.dart
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
|
|
1
3
|
import 'package:home_widget/home_widget.dart';
|
|
2
|
-
import 'package:
|
|
4
|
+
import 'package:kasy_kit/core/data/models/user.dart';
|
|
3
5
|
import 'package:kasy_kit/core/home_widgets/home_widget_service.dart';
|
|
4
6
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
7
|
+
import 'package:logger/logger.dart';
|
|
5
8
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
6
9
|
|
|
7
10
|
part 'home_widget_mywidget_service.g.dart';
|
|
@@ -18,40 +21,78 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
18
21
|
@override
|
|
19
22
|
Future<void> update() {
|
|
20
23
|
Logger().i('🔄 Updating MyWidget Home Widget');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
final
|
|
24
|
+
final user = ref.read(userStateNotifierProvider).user;
|
|
25
|
+
|
|
26
|
+
final name = switch (user) {
|
|
27
|
+
AuthenticatedUserData(:final name)
|
|
28
|
+
when name != null && name.isNotEmpty =>
|
|
29
|
+
name.split(' ').first,
|
|
30
|
+
_ => 'there',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
final isPro = switch (user) {
|
|
34
|
+
AuthenticatedUserData(:final subscription) ||
|
|
35
|
+
AnonymousUserData(:final subscription) =>
|
|
36
|
+
subscription?.isActive ?? false,
|
|
37
|
+
_ => false,
|
|
38
|
+
};
|
|
24
39
|
|
|
25
|
-
return updateWidget({
|
|
40
|
+
return updateWidget({
|
|
41
|
+
'greeting': _greeting(),
|
|
42
|
+
'name': name,
|
|
43
|
+
'isPro': isPro.toString(),
|
|
44
|
+
});
|
|
26
45
|
}
|
|
27
46
|
|
|
28
|
-
/// Update widget data
|
|
29
|
-
///
|
|
30
|
-
/// This will save data that the widget can read
|
|
31
47
|
Future<void> updateWidget(Map<String, String> data) async {
|
|
32
|
-
await HomeWidget.saveWidgetData<String>(
|
|
48
|
+
await HomeWidget.saveWidgetData<String>(
|
|
49
|
+
'greeting', data['greeting'] ?? 'Good morning');
|
|
50
|
+
await HomeWidget.saveWidgetData<String>('name', data['name'] ?? 'there');
|
|
51
|
+
await HomeWidget.saveWidgetData<String>(
|
|
52
|
+
'isPro', data['isPro'] ?? 'false');
|
|
33
53
|
|
|
34
|
-
await HomeWidget.saveWidgetData<String>('counter', data['counter'] ?? '0');
|
|
35
|
-
|
|
36
|
-
// Trigger widget update
|
|
37
54
|
await HomeWidget.updateWidget(
|
|
38
55
|
name: _androidWidgetName,
|
|
39
56
|
iOSName: _iosWidgetName,
|
|
40
57
|
);
|
|
41
58
|
}
|
|
42
59
|
|
|
43
|
-
/// Get current widget data
|
|
44
60
|
Future<Map<String, dynamic>> getWidgetData() async {
|
|
45
61
|
return {
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
defaultValue: '
|
|
62
|
+
'greeting': await HomeWidget.getWidgetData<String>(
|
|
63
|
+
'greeting',
|
|
64
|
+
defaultValue: 'Good morning',
|
|
49
65
|
),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'
|
|
53
|
-
defaultValue: '0',
|
|
66
|
+
'name': await HomeWidget.getWidgetData<String>(
|
|
67
|
+
'name',
|
|
68
|
+
defaultValue: 'there',
|
|
54
69
|
),
|
|
70
|
+
'isPro': await HomeWidget.getWidgetData<String>(
|
|
71
|
+
'isPro',
|
|
72
|
+
defaultValue: 'false',
|
|
73
|
+
),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Returns a time-of-day greeting in the device language (pt / es / en).
|
|
78
|
+
// Uses Platform.localeName so it works in background isolates without
|
|
79
|
+
// requiring the Flutter locale system to be initialized.
|
|
80
|
+
static String _greeting() {
|
|
81
|
+
final lang = Platform.localeName.split(RegExp(r'[_\-]')).first.toLowerCase();
|
|
82
|
+
final hour = DateTime.now().hour;
|
|
83
|
+
|
|
84
|
+
return switch (lang) {
|
|
85
|
+
'pt' => hour < 12 ? 'Bom dia' : hour < 18 ? 'Boa tarde' : 'Boa noite',
|
|
86
|
+
'es' => hour < 12
|
|
87
|
+
? 'Buenos días'
|
|
88
|
+
: hour < 18
|
|
89
|
+
? 'Buenas tardes'
|
|
90
|
+
: 'Buenas noches',
|
|
91
|
+
_ => hour < 12
|
|
92
|
+
? 'Good morning'
|
|
93
|
+
: hour < 18
|
|
94
|
+
? 'Good afternoon'
|
|
95
|
+
: 'Good evening',
|
|
55
96
|
};
|
|
56
97
|
}
|
|
57
98
|
}
|
package/lib/utils/i18n.js
CHANGED
|
@@ -690,7 +690,8 @@ const MESSAGES = {
|
|
|
690
690
|
'update.alreadyUpToDate': 'Project is already up to date (v{version}).',
|
|
691
691
|
'update.status': 'Project: v{from} → CLI: v{to}',
|
|
692
692
|
'update.noVersion': 'Project was generated without version tracking. All features can be updated.',
|
|
693
|
-
'update.changesTitle': '
|
|
693
|
+
'update.changesTitle': 'Updates available:',
|
|
694
|
+
'update.reapplyTitle': 'No new changes — can re-apply if needed:',
|
|
694
695
|
'update.howToUpdate': 'To update a feature:',
|
|
695
696
|
'update.howToUpdateComponents': 'To update base components:',
|
|
696
697
|
'update.warn.commit': 'This will overwrite files from feature "{module}". Make sure you have committed your changes first.',
|
|
@@ -1400,7 +1401,8 @@ const MESSAGES = {
|
|
|
1400
1401
|
'update.alreadyUpToDate': 'Projeto ja esta atualizado (v{version}).',
|
|
1401
1402
|
'update.status': 'Projeto: v{from} → CLI: v{to}',
|
|
1402
1403
|
'update.noVersion': 'Projeto foi gerado sem rastreamento de versao. Todas as features podem ser atualizadas.',
|
|
1403
|
-
'update.changesTitle': '
|
|
1404
|
+
'update.changesTitle': 'Atualizacoes disponiveis:',
|
|
1405
|
+
'update.reapplyTitle': 'Sem mudancas novas — pode reaplicar se precisar:',
|
|
1404
1406
|
'update.howToUpdate': 'Para atualizar uma feature:',
|
|
1405
1407
|
'update.howToUpdateComponents': 'Para atualizar componentes base:',
|
|
1406
1408
|
'update.warn.commit': 'Isso vai sobrescrever os arquivos da feature "{module}". Faca commit de tudo antes de continuar.',
|
|
@@ -2110,7 +2112,8 @@ const MESSAGES = {
|
|
|
2110
2112
|
'update.alreadyUpToDate': 'El proyecto ya esta actualizado (v{version}).',
|
|
2111
2113
|
'update.status': 'Proyecto: v{from} → CLI: v{to}',
|
|
2112
2114
|
'update.noVersion': 'El proyecto fue generado sin seguimiento de version. Todas las features pueden actualizarse.',
|
|
2113
|
-
'update.changesTitle': '
|
|
2115
|
+
'update.changesTitle': 'Actualizaciones disponibles:',
|
|
2116
|
+
'update.reapplyTitle': 'Sin cambios nuevos — puedes reaplicar si es necesario:',
|
|
2114
2117
|
'update.howToUpdate': 'Para actualizar una feature:',
|
|
2115
2118
|
'update.howToUpdateComponents': 'Para actualizar componentes base:',
|
|
2116
2119
|
'update.warn.commit': 'Esto sobreescribira los archivos de la feature "{module}". Asegurate de haber hecho commit antes.',
|
package/package.json
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
package com.aicrus.firebase.kit
|
|
2
2
|
|
|
3
|
+
import android.content.Context
|
|
3
4
|
import androidx.compose.runtime.Composable
|
|
4
5
|
import androidx.compose.ui.graphics.Color
|
|
5
6
|
import androidx.compose.ui.unit.dp
|
|
7
|
+
import androidx.compose.ui.unit.sp
|
|
6
8
|
import androidx.glance.GlanceId
|
|
7
9
|
import androidx.glance.GlanceModifier
|
|
8
10
|
import androidx.glance.appwidget.GlanceAppWidget
|
|
9
11
|
import androidx.glance.appwidget.provideContent
|
|
10
12
|
import androidx.glance.background
|
|
13
|
+
import androidx.glance.currentState
|
|
14
|
+
import androidx.glance.layout.Alignment
|
|
11
15
|
import androidx.glance.layout.Box
|
|
12
16
|
import androidx.glance.layout.Column
|
|
17
|
+
import androidx.glance.layout.Spacer
|
|
18
|
+
import androidx.glance.layout.fillMaxSize
|
|
13
19
|
import androidx.glance.layout.padding
|
|
14
20
|
import androidx.glance.state.GlanceStateDefinition
|
|
21
|
+
import androidx.glance.text.FontWeight
|
|
15
22
|
import androidx.glance.text.Text
|
|
23
|
+
import androidx.glance.text.TextStyle
|
|
24
|
+
import androidx.glance.unit.ColorProvider
|
|
16
25
|
import es.antonborri.home_widget.HomeWidgetGlanceState
|
|
17
26
|
import es.antonborri.home_widget.HomeWidgetGlanceStateDefinition
|
|
18
|
-
import android.content.Context
|
|
19
|
-
import androidx.glance.currentState
|
|
20
27
|
|
|
21
28
|
class MyWidgetWidget : GlanceAppWidget() {
|
|
22
29
|
|
|
@@ -32,18 +39,47 @@ class MyWidgetWidget : GlanceAppWidget() {
|
|
|
32
39
|
@Composable
|
|
33
40
|
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
|
|
34
41
|
val prefs = currentState.preferences
|
|
42
|
+
val greeting = prefs.getString("greeting", "Good morning") ?: "Good morning"
|
|
43
|
+
val name = prefs.getString("name", "there") ?: "there"
|
|
44
|
+
val isPro = prefs.getString("isPro", "false") == "true"
|
|
35
45
|
|
|
36
|
-
val
|
|
37
|
-
|
|
38
|
-
val
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Column {
|
|
42
|
-
|
|
43
|
-
Text(text = title ?: "--", modifier = GlanceModifier.padding(bottom = 4.dp))
|
|
44
|
-
|
|
45
|
-
Text(text = counter ?: "--")
|
|
46
|
+
val bgColor = Color(red = 0.08f, green = 0.03f, blue = 0.16f)
|
|
47
|
+
val white = Color.White
|
|
48
|
+
val whiteSubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.55f)
|
|
49
|
+
val gold = Color(red = 1f, green = 0.84f, blue = 0f)
|
|
50
|
+
val whiteVerySubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.40f)
|
|
46
51
|
|
|
52
|
+
Box(
|
|
53
|
+
modifier = GlanceModifier.fillMaxSize().background(bgColor).padding(16.dp),
|
|
54
|
+
contentAlignment = Alignment.TopStart,
|
|
55
|
+
) {
|
|
56
|
+
Column(modifier = GlanceModifier.fillMaxSize()) {
|
|
57
|
+
Text(
|
|
58
|
+
text = greeting,
|
|
59
|
+
style = TextStyle(
|
|
60
|
+
color = ColorProvider(whiteSubtle),
|
|
61
|
+
fontSize = 11.sp,
|
|
62
|
+
fontWeight = FontWeight.Medium,
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
Text(
|
|
66
|
+
text = "Hi, $name!",
|
|
67
|
+
style = TextStyle(
|
|
68
|
+
color = ColorProvider(white),
|
|
69
|
+
fontSize = 22.sp,
|
|
70
|
+
fontWeight = FontWeight.Bold,
|
|
71
|
+
),
|
|
72
|
+
modifier = GlanceModifier.padding(top = 4.dp),
|
|
73
|
+
)
|
|
74
|
+
Spacer(modifier = GlanceModifier.defaultWeight())
|
|
75
|
+
Text(
|
|
76
|
+
text = if (isPro) "⭐ PRO" else "Free plan",
|
|
77
|
+
style = TextStyle(
|
|
78
|
+
color = ColorProvider(if (isPro) gold else whiteVerySubtle),
|
|
79
|
+
fontSize = 11.sp,
|
|
80
|
+
fontWeight = if (isPro) FontWeight.Bold else FontWeight.Medium,
|
|
81
|
+
),
|
|
82
|
+
)
|
|
47
83
|
}
|
|
48
84
|
}
|
|
49
85
|
}
|
|
@@ -4,13 +4,17 @@
|
|
|
4
4
|
"name": "MyWidget",
|
|
5
5
|
"description": "Sample home widget generated by kasy",
|
|
6
6
|
"metadata": {
|
|
7
|
-
"
|
|
7
|
+
"greeting": {
|
|
8
8
|
"type": "string",
|
|
9
|
-
"defaultValue": "
|
|
9
|
+
"defaultValue": "Good morning"
|
|
10
10
|
},
|
|
11
|
-
"
|
|
12
|
-
"type": "
|
|
13
|
-
"defaultValue": "
|
|
11
|
+
"name": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"defaultValue": "there"
|
|
14
|
+
},
|
|
15
|
+
"isPro": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"defaultValue": "false"
|
|
14
18
|
}
|
|
15
19
|
},
|
|
16
20
|
"isLockScreenWidget": false,
|
|
@@ -3,63 +3,78 @@ import SwiftUI
|
|
|
3
3
|
|
|
4
4
|
struct MyWidgetProvider: TimelineProvider {
|
|
5
5
|
func placeholder(in context: Context) -> MyWidgetEntry {
|
|
6
|
-
MyWidgetEntry(
|
|
7
|
-
date: Date(),
|
|
8
|
-
title: "Hello",
|
|
9
|
-
counter: "0"
|
|
10
|
-
)
|
|
6
|
+
MyWidgetEntry(date: Date(), greeting: "Good morning", name: "there", isPro: false)
|
|
11
7
|
}
|
|
12
8
|
|
|
13
|
-
func getSnapshot(in context: Context, completion: @escaping (MyWidgetEntry) ->
|
|
9
|
+
func getSnapshot(in context: Context, completion: @escaping (MyWidgetEntry) -> Void) {
|
|
14
10
|
let prefs = UserDefaults(suiteName: "group.com.aicrus.firebase.kit")
|
|
15
|
-
|
|
16
|
-
let title = prefs?.string(forKey: "title") ?? "Hello"
|
|
17
|
-
|
|
18
|
-
let counter = prefs?.string(forKey: "counter") ?? "0"
|
|
19
|
-
|
|
20
|
-
let entry = MyWidgetEntry(
|
|
11
|
+
completion(MyWidgetEntry(
|
|
21
12
|
date: Date(),
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
greeting: prefs?.string(forKey: "greeting") ?? "Good morning",
|
|
14
|
+
name: prefs?.string(forKey: "name") ?? "there",
|
|
15
|
+
isPro: prefs?.string(forKey: "isPro") == "true"
|
|
16
|
+
))
|
|
26
17
|
}
|
|
27
18
|
|
|
28
|
-
func getTimeline(in context: Context, completion: @escaping (Timeline<MyWidgetEntry>) ->
|
|
29
|
-
getSnapshot(in: context) {
|
|
30
|
-
|
|
31
|
-
completion(timeline)
|
|
19
|
+
func getTimeline(in context: Context, completion: @escaping (Timeline<MyWidgetEntry>) -> Void) {
|
|
20
|
+
getSnapshot(in: context) { entry in
|
|
21
|
+
completion(Timeline(entries: [entry], policy: .atEnd))
|
|
32
22
|
}
|
|
33
23
|
}
|
|
34
24
|
}
|
|
35
25
|
|
|
36
26
|
struct MyWidgetEntry: TimelineEntry {
|
|
37
27
|
let date: Date
|
|
38
|
-
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
let counter: String
|
|
42
|
-
|
|
28
|
+
let greeting: String
|
|
29
|
+
let name: String
|
|
30
|
+
let isPro: Bool
|
|
43
31
|
}
|
|
44
32
|
|
|
45
33
|
struct MyWidgetWidgetView: View {
|
|
46
34
|
var entry: MyWidgetProvider.Entry
|
|
35
|
+
@Environment(\.widgetFamily) var family
|
|
36
|
+
|
|
37
|
+
private var titleSize: CGFloat {
|
|
38
|
+
switch family {
|
|
39
|
+
case .systemSmall: return 24
|
|
40
|
+
case .systemMedium: return 28
|
|
41
|
+
default: return 34
|
|
42
|
+
}
|
|
43
|
+
}
|
|
47
44
|
|
|
48
45
|
var body: some View {
|
|
49
|
-
VStack(alignment: .leading, spacing:
|
|
50
|
-
Text(entry.
|
|
51
|
-
.font(.system(size:
|
|
52
|
-
.
|
|
53
|
-
.
|
|
54
|
-
.lineLimit(2)
|
|
46
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
47
|
+
Text(entry.greeting)
|
|
48
|
+
.font(.system(size: 11, weight: .medium, design: .rounded))
|
|
49
|
+
.foregroundStyle(.white.opacity(0.55))
|
|
50
|
+
.lineLimit(1)
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
.font(.system(size: 24, weight: .bold, design: .rounded))
|
|
58
|
-
.foregroundColor(.primary)
|
|
52
|
+
Spacer().frame(height: 6)
|
|
59
53
|
|
|
54
|
+
Text("Hi, \(entry.name)!")
|
|
55
|
+
.font(.system(size: titleSize, weight: .bold, design: .rounded))
|
|
56
|
+
.foregroundStyle(.white)
|
|
57
|
+
.lineLimit(2)
|
|
58
|
+
.minimumScaleFactor(0.75)
|
|
59
|
+
|
|
60
|
+
Spacer()
|
|
61
|
+
|
|
62
|
+
if entry.isPro {
|
|
63
|
+
Label("PRO", systemImage: "star.fill")
|
|
64
|
+
.font(.system(size: 10, weight: .bold, design: .rounded))
|
|
65
|
+
.foregroundStyle(Color(red: 1.0, green: 0.84, blue: 0.0))
|
|
66
|
+
.padding(.horizontal, 8)
|
|
67
|
+
.padding(.vertical, 4)
|
|
68
|
+
.background(Color(red: 1.0, green: 0.84, blue: 0.0).opacity(0.18))
|
|
69
|
+
.clipShape(Capsule())
|
|
70
|
+
} else {
|
|
71
|
+
Text("Free plan")
|
|
72
|
+
.font(.system(size: 10, weight: .medium, design: .rounded))
|
|
73
|
+
.foregroundStyle(.white.opacity(0.4))
|
|
74
|
+
}
|
|
60
75
|
}
|
|
61
|
-
.padding()
|
|
62
76
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
|
77
|
+
.padding()
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
|
|
@@ -69,18 +84,31 @@ struct MyWidgetWidget: Widget {
|
|
|
69
84
|
var body: some WidgetConfiguration {
|
|
70
85
|
StaticConfiguration(kind: kind, provider: MyWidgetProvider()) { entry in
|
|
71
86
|
MyWidgetWidgetView(entry: entry)
|
|
72
|
-
.containerBackground(
|
|
87
|
+
.containerBackground(for: .widget) {
|
|
88
|
+
LinearGradient(
|
|
89
|
+
gradient: Gradient(colors: [
|
|
90
|
+
Color(red: 0.08, green: 0.03, blue: 0.16),
|
|
91
|
+
Color(red: 0.20, green: 0.09, blue: 0.42),
|
|
92
|
+
]),
|
|
93
|
+
startPoint: .topLeading,
|
|
94
|
+
endPoint: .bottomTrailing
|
|
95
|
+
)
|
|
96
|
+
}
|
|
73
97
|
}
|
|
74
98
|
.configurationDisplayName("MyWidget")
|
|
75
99
|
.description("Sample home widget generated by kasy")
|
|
76
|
-
.supportedFamilies([
|
|
77
|
-
.systemSmall,.systemMedium,.systemLarge, // test 2
|
|
78
|
-
])
|
|
100
|
+
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
|
|
79
101
|
}
|
|
80
102
|
}
|
|
81
103
|
|
|
82
|
-
#Preview("
|
|
104
|
+
#Preview("Small", as: .systemSmall) {
|
|
105
|
+
MyWidgetWidget()
|
|
106
|
+
} timeline: {
|
|
107
|
+
MyWidgetEntry(date: .now, greeting: "Good morning", name: "Paulo", isPro: true)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#Preview("Medium", as: .systemMedium) {
|
|
83
111
|
MyWidgetWidget()
|
|
84
112
|
} timeline: {
|
|
85
|
-
MyWidgetEntry(date: .now,
|
|
113
|
+
MyWidgetEntry(date: .now, greeting: "Good afternoon", name: "Paulo", isPro: false)
|
|
86
114
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
|
|
1
3
|
import 'package:home_widget/home_widget.dart';
|
|
4
|
+
import 'package:kasy_kit/core/data/models/user.dart';
|
|
2
5
|
import 'package:kasy_kit/core/home_widgets/home_widget_service.dart';
|
|
3
6
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
4
7
|
import 'package:logger/logger.dart';
|
|
@@ -18,40 +21,78 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
18
21
|
@override
|
|
19
22
|
Future<void> update() {
|
|
20
23
|
Logger().i('🔄 Updating MyWidget Home Widget');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
final
|
|
24
|
+
final user = ref.read(userStateNotifierProvider).user;
|
|
25
|
+
|
|
26
|
+
final name = switch (user) {
|
|
27
|
+
AuthenticatedUserData(:final name)
|
|
28
|
+
when name != null && name.isNotEmpty =>
|
|
29
|
+
name.split(' ').first,
|
|
30
|
+
_ => 'there',
|
|
31
|
+
};
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
final isPro = switch (user) {
|
|
34
|
+
AuthenticatedUserData(:final subscription) ||
|
|
35
|
+
AnonymousUserData(:final subscription) =>
|
|
36
|
+
subscription?.isActive ?? false,
|
|
37
|
+
_ => false,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return updateWidget({
|
|
41
|
+
'greeting': _greeting(),
|
|
42
|
+
'name': name,
|
|
43
|
+
'isPro': isPro.toString(),
|
|
44
|
+
});
|
|
26
45
|
}
|
|
27
46
|
|
|
28
|
-
/// Update widget data
|
|
29
|
-
///
|
|
30
|
-
/// This will save data that the widget can read
|
|
31
47
|
Future<void> updateWidget(Map<String, String> data) async {
|
|
32
|
-
await HomeWidget.saveWidgetData<String>(
|
|
33
|
-
|
|
34
|
-
await HomeWidget.saveWidgetData<String>('
|
|
48
|
+
await HomeWidget.saveWidgetData<String>(
|
|
49
|
+
'greeting', data['greeting'] ?? 'Good morning');
|
|
50
|
+
await HomeWidget.saveWidgetData<String>('name', data['name'] ?? 'there');
|
|
51
|
+
await HomeWidget.saveWidgetData<String>(
|
|
52
|
+
'isPro', data['isPro'] ?? 'false');
|
|
35
53
|
|
|
36
|
-
// Trigger widget update
|
|
37
54
|
await HomeWidget.updateWidget(
|
|
38
55
|
name: _androidWidgetName,
|
|
39
56
|
iOSName: _iosWidgetName,
|
|
40
57
|
);
|
|
41
58
|
}
|
|
42
59
|
|
|
43
|
-
/// Get current widget data
|
|
44
60
|
Future<Map<String, dynamic>> getWidgetData() async {
|
|
45
61
|
return {
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
defaultValue: '
|
|
62
|
+
'greeting': await HomeWidget.getWidgetData<String>(
|
|
63
|
+
'greeting',
|
|
64
|
+
defaultValue: 'Good morning',
|
|
49
65
|
),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'
|
|
53
|
-
|
|
66
|
+
'name': await HomeWidget.getWidgetData<String>(
|
|
67
|
+
'name',
|
|
68
|
+
defaultValue: 'there',
|
|
69
|
+
),
|
|
70
|
+
'isPro': await HomeWidget.getWidgetData<String>(
|
|
71
|
+
'isPro',
|
|
72
|
+
defaultValue: 'false',
|
|
54
73
|
),
|
|
55
74
|
};
|
|
56
75
|
}
|
|
76
|
+
|
|
77
|
+
// Returns a time-of-day greeting in the device language (pt / es / en).
|
|
78
|
+
// Uses Platform.localeName so it works in background isolates without
|
|
79
|
+
// requiring the Flutter locale system to be initialized.
|
|
80
|
+
static String _greeting() {
|
|
81
|
+
final lang = Platform.localeName.split(RegExp(r'[_\-]')).first.toLowerCase();
|
|
82
|
+
final hour = DateTime.now().hour;
|
|
83
|
+
|
|
84
|
+
return switch (lang) {
|
|
85
|
+
'pt' => hour < 12 ? 'Bom dia' : hour < 18 ? 'Boa tarde' : 'Boa noite',
|
|
86
|
+
'es' => hour < 12
|
|
87
|
+
? 'Buenos días'
|
|
88
|
+
: hour < 18
|
|
89
|
+
? 'Buenas tardes'
|
|
90
|
+
: 'Buenas noches',
|
|
91
|
+
_ => hour < 12
|
|
92
|
+
? 'Good morning'
|
|
93
|
+
: hour < 18
|
|
94
|
+
? 'Good afternoon'
|
|
95
|
+
: 'Good evening',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
57
98
|
}
|