diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png
new file mode 100644
index 0000000..e924165
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png differ
diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..2057608
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png
new file mode 100644
index 0000000..e6dfe77
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..5ec3941
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..dec4ff7
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..65e5c7a
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..5ccf6ef
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..8e5fd94
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..e0c3d69
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..32c077b
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..d047760
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index db77bb4..146c252 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 17987b7..f04fc2a 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 09d4391..14d40b8 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index d5f1c8d..0a91643 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 4d6372e..ca59693 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/assets/icon/icon.png b/assets/icon/icon.png
new file mode 100644
index 0000000..9516762
Binary files /dev/null and b/assets/icon/icon.png differ
diff --git a/assets/icon/icon_background.png b/assets/icon/icon_background.png
new file mode 100644
index 0000000..cdddaae
Binary files /dev/null and b/assets/icon/icon_background.png differ
diff --git a/assets/icon/icon_foreground.png b/assets/icon/icon_foreground.png
new file mode 100644
index 0000000..7b192e0
Binary files /dev/null and b/assets/icon/icon_foreground.png differ
diff --git a/docs/screenshots/sim_app_icon.png b/docs/screenshots/sim_app_icon.png
new file mode 100644
index 0000000..16a18f5
Binary files /dev/null and b/docs/screenshots/sim_app_icon.png differ
diff --git a/docs/store/feature_graphic.png b/docs/store/feature_graphic.png
new file mode 100644
index 0000000..b8902a1
Binary files /dev/null and b/docs/store/feature_graphic.png differ
diff --git a/flutter_launcher_icons.yaml b/flutter_launcher_icons.yaml
new file mode 100644
index 0000000..ce7eb9a
--- /dev/null
+++ b/flutter_launcher_icons.yaml
@@ -0,0 +1,8 @@
+flutter_launcher_icons:
+ android: true
+ ios: true
+ image_path: "assets/icon/icon.png"
+ remove_alpha_ios: true
+ min_sdk_android: 21
+ adaptive_icon_background: "assets/icon/icon_background.png"
+ adaptive_icon_foreground: "assets/icon/icon_foreground.png"
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 6207be1..872c4b9 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -562,7 +562,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -619,7 +619,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
index d36b1fa..d0d98aa 100644
--- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,122 +1 @@
-{
- "images" : [
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "83.5x83.5",
- "idiom" : "ipad",
- "filename" : "Icon-App-83.5x83.5@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "1024x1024",
- "idiom" : "ios-marketing",
- "filename" : "Icon-App-1024x1024@1x.png",
- "scale" : "1x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
+{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
\ No newline at end of file
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index dc9ada4..14866f4 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 7353c41..e6c69d3 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index 797d452..96a194f 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index 6ed2d93..7507a52 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index 4cd7b00..9755b97 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index fe73094..1a87347 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index 321773c..5ba2561 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index 797d452..96a194f 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index 502f463..9b0e6e6 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index 0ec3034..16a18f5 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png
new file mode 100644
index 0000000..8b65247
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png
new file mode 100644
index 0000000..026bb03
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
new file mode 100644
index 0000000..117e1f5
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
new file mode 100644
index 0000000..60e484c
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index 0ec3034..16a18f5 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index e9f5fea..91497c4 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
new file mode 100644
index 0000000..6adeed7
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
new file mode 100644
index 0000000..6d76528
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 84ac32a..92381b6 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index 8953cba..68e7e39 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 0467bf1..befafea 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/lib/data/save_repository.dart b/lib/data/save_repository.dart
index 0629725..2844337 100644
--- a/lib/data/save_repository.dart
+++ b/lib/data/save_repository.dart
@@ -42,6 +42,9 @@ class SaveRepository {
_adsRemoved =
(json['flags'] as Map?)?['adsRemoved'] as bool? ??
false;
+ _soundEnabled =
+ (json['flags'] as Map?)?['soundEnabled'] as bool? ??
+ true;
}
}
@@ -56,11 +59,13 @@ class SaveRepository {
bool _tutorialDone = false;
int _endlessBest = 0;
bool _adsRemoved = false;
+ bool _soundEnabled = true;
StreakState get streak => _streak;
bool get tutorialDone => _tutorialDone;
int get endlessBest => _endlessBest;
bool get adsRemoved => _adsRemoved;
+ bool get soundEnabled => _soundEnabled;
Future markTutorialDone() {
_tutorialDone = true;
@@ -72,6 +77,11 @@ class SaveRepository {
return _flush();
}
+ Future setSoundEnabled(bool value) {
+ _soundEnabled = value;
+ return _flush();
+ }
+
Future recordEndlessScore(int score) {
if (score > _endlessBest) _endlessBest = score;
return _flush();
@@ -140,7 +150,11 @@ class SaveRepository {
'best': _streak.best,
'lastYmd': _streak.lastYmd,
},
- 'flags': {'tutorialDone': _tutorialDone, 'adsRemoved': _adsRemoved},
+ 'flags': {
+ 'tutorialDone': _tutorialDone,
+ 'adsRemoved': _adsRemoved,
+ 'soundEnabled': _soundEnabled,
+ },
'endless': {'best': _endlessBest},
}),
);
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 8c2659d..eb00f48 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -59,5 +59,6 @@
"removeAdsDescription": "Removes banners and full-screen ads. Reward ads stay available.",
"restorePurchases": "Restore purchases",
"adsRemovedThanks": "Ads removed — thank you!",
- "purchaseUnavailable": "Purchases are unavailable right now."
+ "purchaseUnavailable": "Purchases are unavailable right now.",
+ "soundAndVibration": "Sound & vibration"
}
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
index a4f2705..47be69c 100644
--- a/lib/l10n/app_ko.arb
+++ b/lib/l10n/app_ko.arb
@@ -31,5 +31,6 @@
"removeAdsDescription": "배너와 전면 광고를 제거합니다. 보상형 광고는 계속 사용할 수 있습니다.",
"restorePurchases": "구매 복원",
"adsRemovedThanks": "광고가 제거되었습니다 — 감사합니다!",
- "purchaseUnavailable": "지금은 구매를 사용할 수 없습니다."
+ "purchaseUnavailable": "지금은 구매를 사용할 수 없습니다.",
+ "soundAndVibration": "소리 및 진동"
}
diff --git a/lib/state/providers.dart b/lib/state/providers.dart
index 02e35d2..4ec04d4 100644
--- a/lib/state/providers.dart
+++ b/lib/state/providers.dart
@@ -11,6 +11,7 @@ import '../services/consent_service.dart';
import '../services/iap_service.dart';
import 'ads_notifier.dart';
import 'endless_best_notifier.dart';
+import 'sound_notifier.dart';
import 'game_session_notifier.dart';
import 'progress_notifier.dart';
import 'season_flow_notifier.dart';
@@ -22,8 +23,12 @@ final gameSessionProvider =
GameSessionNotifier.new,
);
+final soundEnabledProvider =
+ NotifierProvider(SoundEnabledNotifier.new);
+
final audioServiceProvider = Provider((ref) {
- final service = AudioService();
+ final service = AudioService(enabled: ref.read(soundEnabledProvider));
+ ref.listen(soundEnabledProvider, (_, next) => service.enabled = next);
ref.onDispose(service.dispose);
return service;
});
diff --git a/lib/state/sound_notifier.dart b/lib/state/sound_notifier.dart
new file mode 100644
index 0000000..482b039
--- /dev/null
+++ b/lib/state/sound_notifier.dart
@@ -0,0 +1,17 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'providers.dart';
+
+/// SFX + gameplay haptics on/off, seeded from the save repository.
+class SoundEnabledNotifier extends Notifier {
+ @override
+ bool build() => ref.read(saveRepositoryProvider).soundEnabled;
+
+ Future toggle() => set(!state);
+
+ Future set(bool value) async {
+ if (state == value) return;
+ await ref.read(saveRepositoryProvider).setSoundEnabled(value);
+ state = value;
+ }
+}
diff --git a/lib/ui/branding/app_icon_painter.dart b/lib/ui/branding/app_icon_painter.dart
new file mode 100644
index 0000000..ce8fdbd
--- /dev/null
+++ b/lib/ui/branding/app_icon_painter.dart
@@ -0,0 +1,43 @@
+// lib/ui/branding/app_icon_painter.dart
+import 'package:flutter/material.dart';
+
+import '../widgets/tile_painter.dart';
+
+/// Draws the Block Seasons brand mark: deep-navy field + a 2×2 grid of glossy
+/// brand-color blocks. Shared by the launcher-icon and feature-graphic
+/// generators so the brand stays identical everywhere.
+class AppIconMark {
+ static const navy = [Color(0xFF101736), Color(0xFF192555), Color(0xFF2C3168)];
+ static const pink = Color(0xFFFF7EB3);
+ static const yellow = Color(0xFFFFD166);
+ static const cyan = Color(0xFF6FCDF5);
+ static const green = Color(0xFF7EDB9C);
+
+ /// Fills [rect] with the navy gradient.
+ static void paintBackground(Canvas canvas, Rect rect) {
+ final paint = Paint()
+ ..shader = const LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: navy,
+ ).createShader(rect);
+ canvas.drawRect(rect, paint);
+ }
+
+ /// Paints the 2×2 glossy blocks centered in a square of side [size], the
+ /// block group occupying [groupFraction] of the side.
+ static void paintBlocks(Canvas canvas, double size,
+ {double groupFraction = 0.6}) {
+ final group = size * groupFraction;
+ final gap = size * 0.05;
+ final block = (group - gap) / 2;
+ final m = (size - group) / 2;
+ final far = m + block + gap;
+ void tile(double x, double y, Color c) => paintGlossyTile(
+ canvas, Rect.fromLTWH(x, y, block, block), c, radiusFactor: 0.24);
+ tile(m, m, pink);
+ tile(far, m, yellow);
+ tile(m, far, cyan);
+ tile(far, far, green);
+ }
+}
diff --git a/lib/ui/branding/feature_graphic_painter.dart b/lib/ui/branding/feature_graphic_painter.dart
new file mode 100644
index 0000000..f90cc22
--- /dev/null
+++ b/lib/ui/branding/feature_graphic_painter.dart
@@ -0,0 +1,42 @@
+// lib/ui/branding/feature_graphic_painter.dart
+import 'package:flutter/material.dart';
+
+import 'app_icon_painter.dart';
+
+/// Paints the Play feature graphic (1024×500): navy field, the brand blocks on
+/// the left, wordmark + tagline on the right.
+class FeatureGraphic {
+ static void paint(Canvas canvas, Size size) {
+ final rect = Offset.zero & size;
+ AppIconMark.paintBackground(canvas, rect);
+
+ // Blocks on the left, vertically centered.
+ canvas.save();
+ final blockArea = size.height * 0.74;
+ canvas.translate(size.height * 0.16, (size.height - blockArea) / 2);
+ AppIconMark.paintBlocks(canvas, blockArea, groupFraction: 0.92);
+ canvas.restore();
+
+ // Text column begins just right of the blocks; kept within the 1024 width.
+ final textLeft = size.height * 0.94;
+ void text(String s, double dy, double fontSize, Color c) {
+ final tp = TextPainter(
+ text: TextSpan(
+ text: s,
+ style: TextStyle(
+ color: c,
+ fontSize: fontSize,
+ fontFamily: 'TitanOne',
+ letterSpacing: 0.5,
+ ),
+ ),
+ textDirection: TextDirection.ltr,
+ )..layout(maxWidth: size.width - textLeft - size.height * 0.06);
+ tp.paint(canvas, Offset(textLeft, dy));
+ }
+
+ text('Block Seasons', size.height * 0.33, 56, Colors.white);
+ text('A new season of blocks,\nevery few weeks.', size.height * 0.56, 22,
+ const Color(0xFFB9C4E6));
+ }
+}
diff --git a/lib/ui/screens/game_screen.dart b/lib/ui/screens/game_screen.dart
index 90cf52b..dfa157f 100644
--- a/lib/ui/screens/game_screen.dart
+++ b/lib/ui/screens/game_screen.dart
@@ -120,16 +120,17 @@ class _GameScreenState extends ConsumerState
final audio = ref.read(audioServiceProvider);
if (prev?.fxTick != next.fxTick && next.lastPlacement != null) {
final placement = next.lastPlacement!;
+ final hapticsOn = ref.read(soundEnabledProvider);
if (placement.linesCleared > 0) {
audio.play(placement.comboStreak >= 2 ? Sfx.combo : Sfx.clear);
- HapticFeedback.mediumImpact();
+ if (hapticsOn) HapticFeedback.mediumImpact();
if (placement.comboStreak >= 4) {
- HapticFeedback.heavyImpact();
+ if (hapticsOn) HapticFeedback.heavyImpact();
_shake.forward(from: 0);
}
} else {
audio.play(Sfx.place);
- HapticFeedback.lightImpact();
+ if (hapticsOn) HapticFeedback.lightImpact();
}
ref.read(tutorialProvider.notifier).onPlaced();
if (placement.linesCleared > 0) {
diff --git a/lib/ui/screens/home_screen.dart b/lib/ui/screens/home_screen.dart
index 2891e60..6c87d53 100644
--- a/lib/ui/screens/home_screen.dart
+++ b/lib/ui/screens/home_screen.dart
@@ -6,6 +6,8 @@ import '../../game/models/stage.dart';
import '../../l10n/gen/app_localizations.dart';
import '../../state/providers.dart';
import '../widgets/banner_ad_slot.dart';
+import '../widgets/fade_route.dart';
+import '../widgets/pressable_scale.dart';
import '../widgets/season_background.dart';
import 'game_screen.dart';
import 'season_map_screen.dart';
@@ -67,43 +69,45 @@ class HomeScreen extends ConsumerWidget {
),
],
const SizedBox(height: 44),
- FilledButton(
- style: FilledButton.styleFrom(
- padding: const EdgeInsets.symmetric(
- horizontal: 56, vertical: 18),
- textStyle: Theme.of(context).textTheme.titleLarge,
+ PressableScale(
+ child: FilledButton(
+ style: FilledButton.styleFrom(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 56, vertical: 18),
+ textStyle: Theme.of(context).textTheme.titleLarge,
+ ),
+ onPressed: () {
+ if (!(ModalRoute.of(context)?.isCurrent ?? true)) return;
+ Navigator.of(context).push(
+ fadeRoute(const SeasonMapScreen()),
+ );
+ },
+ child: Text(l10n.adventure),
),
- onPressed: () {
- if (!(ModalRoute.of(context)?.isCurrent ?? true)) return;
- Navigator.of(context).push(
- MaterialPageRoute(
- builder: (_) => const SeasonMapScreen()),
- );
- },
- child: Text(l10n.adventure),
),
const SizedBox(height: 14),
- OutlinedButton(
- style: OutlinedButton.styleFrom(
- padding: const EdgeInsets.symmetric(
- horizontal: 40, vertical: 14),
- textStyle: Theme.of(context).textTheme.titleMedium,
+ PressableScale(
+ child: OutlinedButton(
+ style: OutlinedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 40, vertical: 14),
+ textStyle: Theme.of(context).textTheme.titleMedium,
+ ),
+ onPressed: () {
+ if (!(ModalRoute.of(context)?.isCurrent ?? true)) return;
+ ref.read(seasonFlowProvider.notifier).clear();
+ ref.read(analyticsProvider).endlessStart();
+ ref.read(gameSessionProvider.notifier).startStage(
+ StageConfig.endless(
+ seed: DateTime.now().millisecondsSinceEpoch,
+ ),
+ );
+ Navigator.of(context).push(
+ fadeRoute(const GameScreen()),
+ );
+ },
+ child: Text(l10n.classic),
),
- onPressed: () {
- if (!(ModalRoute.of(context)?.isCurrent ?? true)) return;
- ref.read(seasonFlowProvider.notifier).clear();
- ref.read(analyticsProvider).endlessStart();
- ref.read(gameSessionProvider.notifier).startStage(
- StageConfig.endless(
- seed: DateTime.now().millisecondsSinceEpoch,
- ),
- );
- Navigator.of(context).push(
- MaterialPageRoute(
- builder: (_) => const GameScreen()),
- );
- },
- child: Text(l10n.classic),
),
if (best > 0) ...[
const SizedBox(height: 10),
@@ -127,7 +131,7 @@ class HomeScreen extends ConsumerWidget {
child: IconButton(
icon: const Icon(Icons.settings, color: Colors.white70),
onPressed: () => Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => const SettingsScreen()),
+ fadeRoute(const SettingsScreen()),
),
),
),
diff --git a/lib/ui/screens/season_map_screen.dart b/lib/ui/screens/season_map_screen.dart
index 8088cba..daed0cc 100644
--- a/lib/ui/screens/season_map_screen.dart
+++ b/lib/ui/screens/season_map_screen.dart
@@ -5,7 +5,9 @@ import '../../game/models/season.dart';
import '../../state/providers.dart';
import '../theme/palette.dart';
import '../widgets/banner_ad_slot.dart';
+import '../widgets/fade_route.dart';
import '../widgets/map_layout.dart';
+import '../widgets/pressable_scale.dart';
import '../widgets/season_background.dart';
import '../widgets/tile_painter.dart';
import 'game_screen.dart';
@@ -193,79 +195,81 @@ class _JourneyMapState extends ConsumerState<_JourneyMap> {
key: Key('stage_node_$i'),
left: center.dx - size / 2,
top: center.dy - size / 2,
- child: GestureDetector(
- onTap: !isUnlocked
- ? null
- : () {
- ref
- .read(seasonFlowProvider.notifier)
- .startSeasonStage(widget.pack, i);
- Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => const GameScreen()),
- );
- },
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Container(
- width: size,
- height: size,
- alignment: Alignment.center,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- gradient: isUnlocked
- ? LinearGradient(
- begin: Alignment.topLeft,
- end: Alignment.bottomRight,
- colors: isCurrent
- ? [
- lighten(colors.accent, 0.25),
- colors.accent,
- darken(colors.accent, 0.2),
- ]
- : [
- const Color(0xFFFFE9A8),
- const Color(0xFFFFD166),
- const Color(0xFFE0AC3B),
- ],
- )
- : null,
- color: isUnlocked ? null : GamePalette.lockedNode,
- boxShadow: isCurrent
- ? [
- BoxShadow(
- color: colors.accent.withValues(alpha: 0.7),
- blurRadius: 22,
+ child: PressableScale(
+ child: GestureDetector(
+ onTap: !isUnlocked
+ ? null
+ : () {
+ ref
+ .read(seasonFlowProvider.notifier)
+ .startSeasonStage(widget.pack, i);
+ Navigator.of(context).push(
+ fadeRoute(const GameScreen()),
+ );
+ },
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: size,
+ height: size,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: isUnlocked
+ ? LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: isCurrent
+ ? [
+ lighten(colors.accent, 0.25),
+ colors.accent,
+ darken(colors.accent, 0.2),
+ ]
+ : [
+ const Color(0xFFFFE9A8),
+ const Color(0xFFFFD166),
+ const Color(0xFFE0AC3B),
+ ],
+ )
+ : null,
+ color: isUnlocked ? null : GamePalette.lockedNode,
+ boxShadow: isCurrent
+ ? [
+ BoxShadow(
+ color: colors.accent.withValues(alpha: 0.7),
+ blurRadius: 22,
+ ),
+ ]
+ : null,
+ ),
+ child: isUnlocked
+ ? Text(
+ '${i + 1}',
+ style: TextStyle(
+ fontSize: isCurrent ? 22 : 17,
+ fontWeight: FontWeight.w900,
+ color: isCurrent
+ ? Colors.white
+ : const Color(0xFF5A4200),
),
- ]
- : null,
+ )
+ : const Icon(Icons.lock, color: Colors.white24, size: 20),
),
- child: isUnlocked
- ? Text(
- '${i + 1}',
- style: TextStyle(
- fontSize: isCurrent ? 22 : 17,
- fontWeight: FontWeight.w900,
- color: isCurrent
- ? Colors.white
- : const Color(0xFF5A4200),
+ if (isUnlocked && !isCurrent)
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ for (var s = 0; s < 3; s++)
+ Icon(
+ Icons.star,
+ size: 13,
+ color: s < stars ? Colors.amber : Colors.white24,
),
- )
- : const Icon(Icons.lock, color: Colors.white24, size: 20),
- ),
- if (isUnlocked && !isCurrent)
- Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- for (var s = 0; s < 3; s++)
- Icon(
- Icons.star,
- size: 13,
- color: s < stars ? Colors.amber : Colors.white24,
- ),
- ],
- ),
- ],
+ ],
+ ),
+ ],
+ ),
),
),
);
diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart
index e4269af..cff3545 100644
--- a/lib/ui/screens/settings_screen.dart
+++ b/lib/ui/screens/settings_screen.dart
@@ -2,8 +2,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../game/models/season.dart';
import '../../l10n/gen/app_localizations.dart';
import '../../state/providers.dart';
+import '../widgets/season_background.dart';
class SettingsScreen extends ConsumerWidget {
const SettingsScreen({super.key});
@@ -12,6 +14,7 @@ class SettingsScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final adsRemoved = ref.watch(adsRemovedProvider);
+ final soundOn = ref.watch(soundEnabledProvider);
final iap = ref.read(iapServiceProvider);
ref.listen(adsRemovedProvider, (prev, next) {
@@ -22,37 +25,54 @@ class SettingsScreen extends ConsumerWidget {
}
});
- return Scaffold(
- appBar: AppBar(title: Text(l10n.settings)),
- body: ListView(
- padding: const EdgeInsets.all(16),
- children: [
- ListTile(
- title: Text(l10n.removeAds),
- subtitle: Text(l10n.removeAdsDescription),
- trailing: adsRemoved
- ? const Icon(Icons.check_circle, color: Colors.green)
- : Text(iap.product?.price ?? ''),
- onTap: adsRemoved
- ? null
- : () async {
- if (!iap.available || iap.product == null) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(l10n.purchaseUnavailable)),
- );
- return;
- }
- await iap.buyRemoveAds();
- },
+ return Stack(
+ fit: StackFit.expand,
+ children: [
+ const SeasonBackground(theme: SeasonTheme.fallback),
+ Scaffold(
+ backgroundColor: Colors.transparent,
+ appBar: AppBar(
+ backgroundColor: Colors.transparent,
+ title: Text(l10n.settings),
),
- const Divider(),
- ListTile(
- leading: const Icon(Icons.restore),
- title: Text(l10n.restorePurchases),
- onTap: () => iap.restorePurchases(),
+ body: ListView(
+ padding: const EdgeInsets.all(16),
+ children: [
+ SwitchListTile(
+ title: Text(l10n.soundAndVibration),
+ value: soundOn,
+ onChanged: (v) =>
+ ref.read(soundEnabledProvider.notifier).set(v),
+ ),
+ const Divider(),
+ ListTile(
+ title: Text(l10n.removeAds),
+ subtitle: Text(l10n.removeAdsDescription),
+ trailing: adsRemoved
+ ? const Icon(Icons.check_circle, color: Colors.green)
+ : Text(iap.product?.price ?? ''),
+ onTap: adsRemoved
+ ? null
+ : () async {
+ if (!iap.available || iap.product == null) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(l10n.purchaseUnavailable)),
+ );
+ return;
+ }
+ await iap.buyRemoveAds();
+ },
+ ),
+ const Divider(),
+ ListTile(
+ leading: const Icon(Icons.restore),
+ title: Text(l10n.restorePurchases),
+ onTap: () => iap.restorePurchases(),
+ ),
+ ],
),
- ],
- ),
+ ),
+ ],
);
}
}
diff --git a/lib/ui/widgets/fade_route.dart b/lib/ui/widgets/fade_route.dart
new file mode 100644
index 0000000..ba77b24
--- /dev/null
+++ b/lib/ui/widgets/fade_route.dart
@@ -0,0 +1,22 @@
+// lib/ui/widgets/fade_route.dart
+import 'package:flutter/material.dart';
+
+/// A gentle fade(+slight scale) page transition for in-app navigation.
+Route fadeRoute(Widget page) {
+ return PageRouteBuilder(
+ transitionDuration: const Duration(milliseconds: 320),
+ reverseTransitionDuration: const Duration(milliseconds: 240),
+ pageBuilder: (_, _, _) => page,
+ transitionsBuilder: (_, animation, _, child) {
+ final curved =
+ CurvedAnimation(parent: animation, curve: Curves.easeOutCubic);
+ return FadeTransition(
+ opacity: curved,
+ child: ScaleTransition(
+ scale: Tween(begin: 0.98, end: 1.0).animate(curved),
+ child: child,
+ ),
+ );
+ },
+ );
+}
diff --git a/lib/ui/widgets/pressable_scale.dart b/lib/ui/widgets/pressable_scale.dart
new file mode 100644
index 0000000..e7944cd
--- /dev/null
+++ b/lib/ui/widgets/pressable_scale.dart
@@ -0,0 +1,37 @@
+// lib/ui/widgets/pressable_scale.dart
+import 'package:flutter/material.dart';
+
+/// Wraps a tappable child with a quick scale-down on press for tactile feel.
+/// If [onTap] is provided it handles the tap; otherwise the child's own
+/// gesture/button handles it and this only adds the visual squish.
+class PressableScale extends StatefulWidget {
+ const PressableScale({super.key, required this.child, this.onTap});
+
+ final Widget child;
+ final VoidCallback? onTap;
+
+ @override
+ State createState() => _PressableScaleState();
+}
+
+class _PressableScaleState extends State {
+ bool _down = false;
+ void _set(bool v) => setState(() => _down = v);
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTapDown: (_) => _set(true),
+ onTapUp: (_) => _set(false),
+ onTapCancel: () => _set(false),
+ onTap: widget.onTap,
+ child: AnimatedScale(
+ scale: _down ? 0.94 : 1.0,
+ duration: const Duration(milliseconds: 90),
+ curve: Curves.easeOut,
+ child: widget.child,
+ ),
+ );
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
index afa215b..16d90e8 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -33,6 +33,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.7"
+ archive:
+ dependency: transitive
+ description:
+ name: archive
+ sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.9"
args:
dependency: transitive
description:
@@ -121,6 +129,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.4"
cli_config:
dependency: transitive
description:
@@ -129,6 +145,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.0"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.2"
clock:
dependency: transitive
description:
@@ -262,6 +286,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_launcher_icons:
+ dependency: "direct dev"
+ description:
+ name: flutter_launcher_icons
+ sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@@ -341,6 +373,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
+ image:
+ dependency: transitive
+ description:
+ name: image
+ sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.8.0"
in_app_purchase:
dependency: "direct main"
description:
@@ -549,6 +589,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.2"
platform:
dependency: transitive
description:
@@ -573,6 +621,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.2"
+ posix:
+ dependency: transitive
+ description:
+ name: posix
+ sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.5.0"
pub_semver:
dependency: transitive
description:
@@ -890,6 +946,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.6.1"
yaml:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index c3ad09a..04e906d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -58,6 +58,7 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
+ flutter_launcher_icons: ^0.14.4
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
diff --git a/test/data/save_repository_sound_test.dart b/test/data/save_repository_sound_test.dart
new file mode 100644
index 0000000..122d329
--- /dev/null
+++ b/test/data/save_repository_sound_test.dart
@@ -0,0 +1,24 @@
+import 'package:block_seasons/data/save_repository.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+void main() {
+ setUp(() => SharedPreferences.setMockInitialValues({}));
+
+ test('soundEnabled defaults true and persists across reopen', () async {
+ final repo = await SaveRepository.open();
+ expect(repo.soundEnabled, isTrue);
+ await repo.setSoundEnabled(false);
+ expect(repo.soundEnabled, isFalse);
+ final reopened = await SaveRepository.open();
+ expect(reopened.soundEnabled, isFalse);
+ });
+
+ test('legacy save without the sound flag reads as true', () async {
+ SharedPreferences.setMockInitialValues({
+ 'save_v1': '{"saveVersion":1,"progress":{},"flags":{"tutorialDone":true}}',
+ });
+ final repo = await SaveRepository.open();
+ expect(repo.soundEnabled, isTrue);
+ });
+}
diff --git a/test/state/sound_notifier_test.dart b/test/state/sound_notifier_test.dart
new file mode 100644
index 0000000..d44499d
--- /dev/null
+++ b/test/state/sound_notifier_test.dart
@@ -0,0 +1,21 @@
+import 'package:block_seasons/data/save_repository.dart';
+import 'package:block_seasons/state/providers.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+void main() {
+ test('reads persisted sound flag and toggles + persists', () async {
+ SharedPreferences.setMockInitialValues({});
+ final repo = await SaveRepository.open();
+ final c = ProviderContainer(
+ overrides: [saveRepositoryProvider.overrideWithValue(repo)],
+ );
+ addTearDown(c.dispose);
+
+ expect(c.read(soundEnabledProvider), isTrue);
+ await c.read(soundEnabledProvider.notifier).toggle();
+ expect(c.read(soundEnabledProvider), isFalse);
+ expect(repo.soundEnabled, isFalse);
+ });
+}
diff --git a/test/tool/generate_brand_assets_test.dart b/test/tool/generate_brand_assets_test.dart
new file mode 100644
index 0000000..75cf07b
--- /dev/null
+++ b/test/tool/generate_brand_assets_test.dart
@@ -0,0 +1,70 @@
+// test/tool/generate_brand_assets_test.dart
+import 'dart:io';
+import 'dart:ui';
+
+import 'package:block_seasons/ui/branding/app_icon_painter.dart';
+import 'package:block_seasons/ui/branding/feature_graphic_painter.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+Future _writePng(String path, int size, void Function(Canvas) draw) async {
+ final recorder = PictureRecorder();
+ final canvas = Canvas(recorder);
+ draw(canvas);
+ final picture = recorder.endRecording();
+ final image = await picture.toImage(size, size);
+ final bytes = await image.toByteData(format: ImageByteFormat.png);
+ File(path).parent.createSync(recursive: true);
+ File(path).writeAsBytesSync(bytes!.buffer.asUint8List());
+}
+
+void main() {
+ testWidgets('generate launcher icon PNGs', (tester) async {
+ const s = 1024;
+ final full = Rect.fromLTWH(0, 0, s.toDouble(), s.toDouble());
+
+ await tester.runAsync(() async {
+ // Master (iOS + fallback): opaque navy + blocks at 60%.
+ await _writePng('assets/icon/icon.png', s, (c) {
+ AppIconMark.paintBackground(c, full);
+ AppIconMark.paintBlocks(c, s.toDouble(), groupFraction: 0.6);
+ });
+ // Adaptive background: navy only.
+ await _writePng('assets/icon/icon_background.png', s, (c) {
+ AppIconMark.paintBackground(c, full);
+ });
+ // Adaptive foreground: blocks only (transparent), 52% for the safe zone.
+ await _writePng('assets/icon/icon_foreground.png', s, (c) {
+ AppIconMark.paintBlocks(c, s.toDouble(), groupFraction: 0.52);
+ });
+ });
+
+ for (final f in ['icon.png', 'icon_background.png', 'icon_foreground.png']) {
+ expect(File('assets/icon/$f').existsSync(), isTrue, reason: f);
+ }
+ });
+
+ testWidgets('generate feature graphic', (tester) async {
+ await tester.runAsync(() async {
+ // Load the real display font (OFL Titan One) so the wordmark renders as
+ // glyphs — the flutter_test default font draws every char as a box.
+ final fontBytes =
+ File('tool/fonts/TitanOne-Regular.ttf').readAsBytesSync();
+ final loader = FontLoader('TitanOne')
+ ..addFont(Future.value(ByteData.view(fontBytes.buffer)));
+ await loader.load();
+
+ const w = 1024, h = 500;
+ final recorder = PictureRecorder();
+ final canvas = Canvas(recorder);
+ FeatureGraphic.paint(canvas, const Size(1024, 500));
+ final picture = recorder.endRecording();
+ final image = await picture.toImage(w, h);
+ final bytes = await image.toByteData(format: ImageByteFormat.png);
+ File('docs/store/feature_graphic.png').parent.createSync(recursive: true);
+ File('docs/store/feature_graphic.png')
+ .writeAsBytesSync(bytes!.buffer.asUint8List());
+ });
+ expect(File('docs/store/feature_graphic.png').existsSync(), isTrue);
+ });
+}
diff --git a/test/ui/pressable_scale_test.dart b/test/ui/pressable_scale_test.dart
new file mode 100644
index 0000000..07a7b23
--- /dev/null
+++ b/test/ui/pressable_scale_test.dart
@@ -0,0 +1,43 @@
+import 'package:block_seasons/ui/widgets/pressable_scale.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets('inner button onPressed still fires when wrapped', (tester) async {
+ var taps = 0;
+ await tester.pumpWidget(MaterialApp(
+ home: Scaffold(
+ body: Center(
+ child: PressableScale(
+ child: FilledButton(
+ onPressed: () => taps++,
+ child: const Text('Play'),
+ ),
+ ),
+ ),
+ ),
+ ));
+
+ await tester.tap(find.text('Play'));
+ await tester.pump();
+ expect(taps, 1, reason: 'PressableScale must not swallow the inner tap');
+ });
+
+ testWidgets('own onTap fires when no inner handler', (tester) async {
+ var taps = 0;
+ await tester.pumpWidget(MaterialApp(
+ home: Scaffold(
+ body: Center(
+ child: PressableScale(
+ onTap: () => taps++,
+ child: const SizedBox(width: 100, height: 50),
+ ),
+ ),
+ ),
+ ));
+
+ await tester.tap(find.byType(PressableScale));
+ await tester.pump();
+ expect(taps, 1);
+ });
+}
diff --git a/tool/fonts/OFL.txt b/tool/fonts/OFL.txt
new file mode 100644
index 0000000..4885975
--- /dev/null
+++ b/tool/fonts/OFL.txt
@@ -0,0 +1,94 @@
+Copyright (c) 2011, Rodrigo Fuenzalida (www.rfuenzalida.com|hello@rfuenzalida.com),
+with Reserved Font Name Titan.
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/tool/fonts/TitanOne-Regular.ttf b/tool/fonts/TitanOne-Regular.ttf
new file mode 100644
index 0000000..aabd6ed
Binary files /dev/null and b/tool/fonts/TitanOne-Regular.ttf differ