kasy-cli 1.13.0 → 1.15.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 +140 -12
- 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 +28 -7
- package/lib/commands/new.js +8 -20
- package/lib/commands/remove.js +1 -1
- package/lib/commands/reset.js +385 -0
- package/lib/commands/run.js +24 -17
- package/lib/commands/splash.js +14 -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 +115 -16
- package/lib/utils/checks.js +45 -107
- package/lib/utils/debug.js +75 -0
- package/lib/utils/flutter-run.js +173 -0
- package/lib/utils/friendly-error.js +91 -0
- package/lib/utils/i18n/messages-en.js +970 -0
- package/lib/utils/i18n/messages-es.js +968 -0
- package/lib/utils/i18n/messages-pt.js +968 -0
- package/lib/utils/i18n.js +21 -2483
- package/lib/utils/mobile-identity.js +35 -0
- package/lib/utils/png-padding.js +120 -0
- package/lib/utils/ui.js +114 -0
- package/package.json +8 -4
- 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 +161 -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/drawable-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -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 +53 -0
- package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -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/assets/images/icon_android.png +0 -0
- package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
- package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
- package/templates/firebase/assets/images/splash_logo_light_android12.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/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
- package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
- package/templates/firebase/lib/components/kasy_button.dart +8 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +431 -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 +73 -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_components_page.dart +1 -1
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
- 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 +10 -3
- 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 +9 -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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { readBundleId } = require('./apple-release');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads the Android applicationId (package name) of the project.
|
|
7
|
+
* Source of truth is build.gradle(.kts) — what gets baked into the APK and
|
|
8
|
+
* what the device sees. kit_setup.json can drift when the user renames the
|
|
9
|
+
* package manually after `kasy new`, so it's only used as a fallback when
|
|
10
|
+
* the Android project hasn't been generated yet.
|
|
11
|
+
*/
|
|
12
|
+
async function readPackageName(projectDir) {
|
|
13
|
+
const candidates = [
|
|
14
|
+
path.join(projectDir, 'android', 'app', 'build.gradle.kts'),
|
|
15
|
+
path.join(projectDir, 'android', 'app', 'build.gradle'),
|
|
16
|
+
];
|
|
17
|
+
for (const file of candidates) {
|
|
18
|
+
if (!(await fs.pathExists(file))) continue;
|
|
19
|
+
const content = await fs.readFile(file, 'utf8');
|
|
20
|
+
const m = content.match(/applicationId\s*=?\s*["']([^"']+)["']/);
|
|
21
|
+
if (m) return m[1];
|
|
22
|
+
}
|
|
23
|
+
const kitSetupPath = path.join(projectDir, 'kit_setup.json');
|
|
24
|
+
if (await fs.pathExists(kitSetupPath)) {
|
|
25
|
+
try {
|
|
26
|
+
const kit = await fs.readJson(kitSetupPath);
|
|
27
|
+
if (kit.bundleId) return kit.bundleId;
|
|
28
|
+
} catch {
|
|
29
|
+
// ignore
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { readBundleId, readPackageName };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const fsp = require('node:fs/promises');
|
|
2
|
+
const { PNG } = require('pngjs');
|
|
3
|
+
|
|
4
|
+
const ANDROID12_SAFE_RATIO = 0.5;
|
|
5
|
+
|
|
6
|
+
async function readPng(filePath) {
|
|
7
|
+
const buffer = await fsp.readFile(filePath);
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
new PNG().parse(buffer, (err, data) => {
|
|
10
|
+
if (err) reject(err);
|
|
11
|
+
else resolve(data);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resizeBilinear(src, dstW, dstH) {
|
|
17
|
+
const dst = new PNG({ width: dstW, height: dstH });
|
|
18
|
+
const srcW = src.width;
|
|
19
|
+
const srcH = src.height;
|
|
20
|
+
const xRatio = srcW > 1 ? (srcW - 1) / dstW : 0;
|
|
21
|
+
const yRatio = srcH > 1 ? (srcH - 1) / dstH : 0;
|
|
22
|
+
|
|
23
|
+
for (let y = 0; y < dstH; y++) {
|
|
24
|
+
const sy = (y + 0.5) * yRatio;
|
|
25
|
+
const y0 = Math.floor(sy);
|
|
26
|
+
const y1 = Math.min(y0 + 1, srcH - 1);
|
|
27
|
+
const wy = sy - y0;
|
|
28
|
+
|
|
29
|
+
for (let x = 0; x < dstW; x++) {
|
|
30
|
+
const sx = (x + 0.5) * xRatio;
|
|
31
|
+
const x0 = Math.floor(sx);
|
|
32
|
+
const x1 = Math.min(x0 + 1, srcW - 1);
|
|
33
|
+
const wx = sx - x0;
|
|
34
|
+
|
|
35
|
+
const i00 = (y0 * srcW + x0) * 4;
|
|
36
|
+
const i10 = (y0 * srcW + x1) * 4;
|
|
37
|
+
const i01 = (y1 * srcW + x0) * 4;
|
|
38
|
+
const i11 = (y1 * srcW + x1) * 4;
|
|
39
|
+
|
|
40
|
+
const dstIdx = (y * dstW + x) * 4;
|
|
41
|
+
for (let c = 0; c < 4; c++) {
|
|
42
|
+
const top = src.data[i00 + c] * (1 - wx) + src.data[i10 + c] * wx;
|
|
43
|
+
const bot = src.data[i01 + c] * (1 - wx) + src.data[i11 + c] * wx;
|
|
44
|
+
dst.data[dstIdx + c] = Math.round(top * (1 - wy) + bot * wy);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return dst;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function compositeOnTransparentSquare(logo, canvasSize) {
|
|
52
|
+
const canvas = new PNG({ width: canvasSize, height: canvasSize });
|
|
53
|
+
canvas.data.fill(0);
|
|
54
|
+
|
|
55
|
+
const offsetX = Math.floor((canvasSize - logo.width) / 2);
|
|
56
|
+
const offsetY = Math.floor((canvasSize - logo.height) / 2);
|
|
57
|
+
|
|
58
|
+
for (let y = 0; y < logo.height; y++) {
|
|
59
|
+
for (let x = 0; x < logo.width; x++) {
|
|
60
|
+
const srcIdx = (y * logo.width + x) * 4;
|
|
61
|
+
const dstIdx = ((y + offsetY) * canvasSize + (x + offsetX)) * 4;
|
|
62
|
+
canvas.data[dstIdx] = logo.data[srcIdx];
|
|
63
|
+
canvas.data[dstIdx + 1] = logo.data[srcIdx + 1];
|
|
64
|
+
canvas.data[dstIdx + 2] = logo.data[srcIdx + 2];
|
|
65
|
+
canvas.data[dstIdx + 3] = logo.data[srcIdx + 3];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return canvas;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function writePng(png, filePath) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const chunks = [];
|
|
74
|
+
png.pack()
|
|
75
|
+
.on('data', (chunk) => chunks.push(chunk))
|
|
76
|
+
.on('end', async () => {
|
|
77
|
+
try {
|
|
78
|
+
await fsp.writeFile(filePath, Buffer.concat(chunks));
|
|
79
|
+
resolve();
|
|
80
|
+
} catch (e) {
|
|
81
|
+
reject(e);
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.on('error', reject);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read a PNG and write a new one of the same dimensions, with the source
|
|
90
|
+
* logo scaled down to fit inside the Android 12+ splash safe area (centered,
|
|
91
|
+
* transparent padding around it). This is what `windowSplashScreenAnimatedIcon`
|
|
92
|
+
* needs so the OS-applied circular mask doesn't clip the logo edges.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} srcPath
|
|
95
|
+
* @param {string} dstPath
|
|
96
|
+
* @param {number} safeRatio fraction of the canvas the logo should occupy (default 0.6)
|
|
97
|
+
*/
|
|
98
|
+
async function writeAndroid12Variant(srcPath, dstPath, safeRatio = ANDROID12_SAFE_RATIO) {
|
|
99
|
+
const src = await readPng(srcPath);
|
|
100
|
+
const canvasSize = Math.max(src.width, src.height);
|
|
101
|
+
const safeSide = Math.round(canvasSize * safeRatio);
|
|
102
|
+
|
|
103
|
+
const aspect = src.width / src.height;
|
|
104
|
+
let logoW;
|
|
105
|
+
let logoH;
|
|
106
|
+
if (aspect >= 1) {
|
|
107
|
+
logoW = safeSide;
|
|
108
|
+
logoH = Math.round(safeSide / aspect);
|
|
109
|
+
} else {
|
|
110
|
+
logoH = safeSide;
|
|
111
|
+
logoW = Math.round(safeSide * aspect);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const resized = resizeBilinear(src, logoW, logoH);
|
|
115
|
+
const composited = compositeOnTransparentSquare(resized, canvasSize);
|
|
116
|
+
await writePng(composited, dstPath);
|
|
117
|
+
return { canvasSize, logoW, logoH };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { writeAndroid12Variant, ANDROID12_SAFE_RATIO };
|
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.15.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"
|
|
@@ -39,10 +39,12 @@
|
|
|
39
39
|
"validate": "node ./bin/kasy.js validate --analyze-only",
|
|
40
40
|
"extract:patch": "node ./scripts/extract_patch.js",
|
|
41
41
|
"check:firebase": "node ./scripts/check-firebase-template.js",
|
|
42
|
+
"test": "for f in test/*.test.js; do node \"$f\" || exit 1; done",
|
|
42
43
|
"test:google-ios": "node ./test/google-ios-url-scheme.test.js",
|
|
43
44
|
"test:apple-release": "node ./test/apple-release.test.js",
|
|
44
45
|
"test:localize-docs": "node ./test/localize-release-docs.test.js",
|
|
45
|
-
"test:i18n-accents": "node ./test/i18n-accents.test.js"
|
|
46
|
+
"test:i18n-accents": "node ./test/i18n-accents.test.js",
|
|
47
|
+
"lint": "eslint bin lib scripts"
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
48
50
|
"@clack/prompts": "^1.4.0",
|
|
@@ -51,8 +53,10 @@
|
|
|
51
53
|
"fs-extra": "^11.2.0",
|
|
52
54
|
"gradient-string": "^1.2.0",
|
|
53
55
|
"kleur": "^4.1.5",
|
|
54
|
-
"
|
|
55
|
-
"prompts": "^2.4.2",
|
|
56
|
+
"pngjs": "^7.0.0",
|
|
56
57
|
"yaml": "^2.4.2"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"eslint": "^9.39.4"
|
|
57
61
|
}
|
|
58
62
|
}
|
|
@@ -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,126 @@ 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 exact Intent the system launcher fires when the user taps
|
|
224
|
+
/// the app icon. We must NOT add extra flags here — getLaunchIntentForPackage
|
|
225
|
+
/// already returns `FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_IF_NEEDED`,
|
|
226
|
+
/// which is the same combo the launcher uses. Adding `CLEAR_TOP` destroys
|
|
227
|
+
/// go_router's navigation stack on warm starts and lands the user on the
|
|
228
|
+
/// errorBuilder ("404 - Page not found").
|
|
229
|
+
private fun launchAppIntent(context: Context): Intent {
|
|
230
|
+
return context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
231
|
+
?: Intent(context, MainActivity::class.java).apply {
|
|
232
|
+
action = Intent.ACTION_MAIN
|
|
233
|
+
addCategory(Intent.CATEGORY_LAUNCHER)
|
|
234
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
235
|
+
}
|
|
236
|
+
}
|
|
87
237
|
}
|
|
@@ -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>
|