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