diff --git a/Nuvolari/CLAUDE.md b/Nuvolari/CLAUDE.md index 4a6ec8b..93498b9 100644 --- a/Nuvolari/CLAUDE.md +++ b/Nuvolari/CLAUDE.md @@ -77,6 +77,9 @@ grounds that there are no ads today. generated by tool/generate_places.py. Offline search, no geocoding service. The source DBF is UTF-8 despite appearances and the geometry is UTM 32N — see docs/data-sources.md before regenerating. +- Launcher icon: a white cloud on the seed blue #1F6FB2, drawn by tool/generate_icon.py + and expanded by flutter_launcher_icons. Read the "Launcher icon" section of + docs/architecture.md before regenerating: the tool corrupts an Xcode build setting. ## Verification loop (run after each milestone) - `dart format .` ; `flutter analyze` ; `flutter test` ; `flutter build appbundle` diff --git a/Nuvolari/app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/Nuvolari/app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..b61e8e3 Binary files /dev/null and b/Nuvolari/app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/Nuvolari/app/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png b/Nuvolari/app/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..b61e8e3 Binary files /dev/null and b/Nuvolari/app/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png differ diff --git a/Nuvolari/app/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/Nuvolari/app/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..708d88d Binary files /dev/null and b/Nuvolari/app/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/Nuvolari/app/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png b/Nuvolari/app/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..708d88d Binary files /dev/null and b/Nuvolari/app/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png differ diff --git a/Nuvolari/app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/Nuvolari/app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..6340a97 Binary files /dev/null and b/Nuvolari/app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/Nuvolari/app/android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png b/Nuvolari/app/android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..6340a97 Binary files /dev/null and b/Nuvolari/app/android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png differ diff --git a/Nuvolari/app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/Nuvolari/app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..cd37043 Binary files /dev/null and b/Nuvolari/app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/Nuvolari/app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png b/Nuvolari/app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..cd37043 Binary files /dev/null and b/Nuvolari/app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png differ diff --git a/Nuvolari/app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/Nuvolari/app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..cf1ccef Binary files /dev/null and b/Nuvolari/app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/Nuvolari/app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png b/Nuvolari/app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..cf1ccef Binary files /dev/null and b/Nuvolari/app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png differ diff --git a/Nuvolari/app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Nuvolari/app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..c61e081 --- /dev/null +++ b/Nuvolari/app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/Nuvolari/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Nuvolari/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..bc21ce1 100644 Binary files a/Nuvolari/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/Nuvolari/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/Nuvolari/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Nuvolari/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..f618914 100644 Binary files a/Nuvolari/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/Nuvolari/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/Nuvolari/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Nuvolari/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..f1c16ca 100644 Binary files a/Nuvolari/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/Nuvolari/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/Nuvolari/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Nuvolari/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..2218ff3 100644 Binary files a/Nuvolari/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/Nuvolari/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/Nuvolari/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Nuvolari/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..391d050 100644 Binary files a/Nuvolari/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/Nuvolari/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/Nuvolari/app/android/app/src/main/res/values/colors.xml b/Nuvolari/app/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..66c4cb7 --- /dev/null +++ b/Nuvolari/app/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #1F6FB2 + \ No newline at end of file diff --git a/Nuvolari/app/assets/icon/icon.png b/Nuvolari/app/assets/icon/icon.png new file mode 100644 index 0000000..284e4db Binary files /dev/null and b/Nuvolari/app/assets/icon/icon.png differ diff --git a/Nuvolari/app/assets/icon/icon_foreground.png b/Nuvolari/app/assets/icon/icon_foreground.png new file mode 100644 index 0000000..c371298 Binary files /dev/null and b/Nuvolari/app/assets/icon/icon_foreground.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fa..b252554 100644 --- a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1,158 @@ { - "images" : [ + "images": [ { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "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": "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@1x.png", + "scale": "1x" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "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": "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@2x.png", + "scale": "2x" }, { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" + "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": "57x57", + "idiom": "iphone", + "filename": "Icon-App-57x57@1x.png", + "scale": "1x" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" + "size": "57x57", + "idiom": "iphone", + "filename": "Icon-App-57x57@2x.png", + "scale": "2x" }, { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" + "size": "60x60", + "idiom": "iphone", + "filename": "Icon-App-60x60@2x.png", + "scale": "2x" }, { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "size": "60x60", + "idiom": "iphone", + "filename": "Icon-App-60x60@3x.png", + "scale": "3x" }, { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "size": "20x20", + "idiom": "ipad", + "filename": "Icon-App-20x20@1x.png", + "scale": "1x" }, { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "size": "20x20", + "idiom": "ipad", + "filename": "Icon-App-20x20@2x.png", + "scale": "2x" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" + "size": "29x29", + "idiom": "ipad", + "filename": "Icon-App-29x29@1x.png", + "scale": "1x" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "size": "29x29", + "idiom": "ipad", + "filename": "Icon-App-29x29@2x.png", + "scale": "2x" }, { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" + "size": "40x40", + "idiom": "ipad", + "filename": "Icon-App-40x40@1x.png", + "scale": "1x" }, { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" + "size": "40x40", + "idiom": "ipad", + "filename": "Icon-App-40x40@2x.png", + "scale": "2x" }, { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" + "size": "50x50", + "idiom": "ipad", + "filename": "Icon-App-50x50@1x.png", + "scale": "1x" }, { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@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" + "info": { + "version": 1, + "author": "xcode" } } diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index dc9ada4..137ad25 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 7353c41..793fbc9 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 797d452..c435131 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 6ed2d93..947bedb 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cd7b00..8f84104 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index fe73094..e1811dc 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 321773c..03957c1 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 797d452..c435131 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 502f463..b9a58d7 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 0ec3034..b0ff144 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..9114596 Binary files /dev/null and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..a1de92c Binary files /dev/null and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..7558f22 Binary files /dev/null and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..c0fbc32 Binary files /dev/null and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 0ec3034..b0ff144 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index e9f5fea..e5be355 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..6d615b8 Binary files /dev/null and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..e3ea470 Binary files /dev/null and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 84ac32a..17d650f 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 8953cba..ffffb90 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 0467bf1..06482f9 100644 Binary files a/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/Nuvolari/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/Nuvolari/app/pubspec.lock b/Nuvolari/app/pubspec.lock index e8d6561..c2c36f4 100644 --- a/Nuvolari/app/pubspec.lock +++ b/Nuvolari/app/pubspec.lock @@ -41,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -134,6 +150,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: @@ -269,6 +293,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.3" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" leak_tracker: dependency: transitive description: @@ -698,6 +730,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea + url: "https://pub.dev" + source: hosted + version: "3.1.4" sdks: dart: ">=3.13.3 <4.0.0" flutter: ">=3.44.0" diff --git a/Nuvolari/app/pubspec.yaml b/Nuvolari/app/pubspec.yaml index 47ad936..3d19515 100644 --- a/Nuvolari/app/pubspec.yaml +++ b/Nuvolari/app/pubspec.yaml @@ -33,6 +33,11 @@ dev_dependencies: sdk: flutter flutter_lints: ^6.0.0 + # Fans the two generated sources out to every Android density, the adaptive + # icon and the iOS set. Build-time only: it writes files into android/ and + # ios/, which are committed, so nothing here ships in the app. + flutter_launcher_icons: ^0.14.4 + flutter: uses-material-design: true @@ -45,3 +50,33 @@ flutter: # the app runnable offline with no credentials. - assets/mock/ - assets/mock/frames/ + +# Launcher icon, generated from the sources that tool/generate_icon.py draws. +# Re-run after changing them: +# +# python ../tool/generate_icon.py +# dart run flutter_launcher_icons +flutter_launcher_icons: + android: true + ios: true + image_path: assets/icon/icon.png + + # Adaptive icon: the launcher masks the foreground to its own shape, so the + # cloud is drawn inside the safe zone and the app blue fills whatever the mask + # leaves. Kept identical to ColorScheme.fromSeed in lib/main.dart. + adaptive_icon_background: "#1F6FB2" + adaptive_icon_foreground: assets/icon/icon_foreground.png + + # The generator already frames the cloud inside the 72dp safe zone. Left at + # its default of 16, this would shrink the layer again and the icon would read + # noticeably smaller than every other one on the home screen. + adaptive_icon_foreground_inset: 0 + + # Android 13 themed icons tint the alpha of this layer with the user's + # wallpaper palette. The foreground is a flat white silhouette, so its alpha + # is already exactly the cloud. + adaptive_icon_monochrome: assets/icon/icon_foreground.png + + # iOS rejects an alpha channel in app icons. The source is opaque anyway; + # this drops the channel itself. + remove_alpha_ios: true diff --git a/Nuvolari/docs/architecture.md b/Nuvolari/docs/architecture.md index 2901497..f46a67b 100644 --- a/Nuvolari/docs/architecture.md +++ b/Nuvolari/docs/architecture.md @@ -33,7 +33,7 @@ Nuvolari/ ├─ app/ Flutter application (Dart package "nuvolari") ├─ backend/ Python worker ├─ docs/ this documentation -└─ tool/ verification scripts +└─ tool/ verification and asset-generation scripts ``` ## App layers @@ -131,3 +131,38 @@ subscribing to FCM topics named after geographic cells — the subscription happ device, so the backend holds no user records at all. With no advertising and no forecast provider, nothing about the user leaves the device except the area the map is looking at, which is inherent to any hosted base map. + +## Launcher icon + +The icon is a white cloud on the app's seed blue `#1F6FB2`, so the launcher and the +interface are recognisably the same product. It is drawn, not hand-edited: +`tool/generate_icon.py` renders it from signed distance fields — a union of three +circles and one rounded box — and writes two 1024px sources into `app/assets/icon/`. +Those sources are not listed in the pubspec `assets:` block: they feed the build and are +never bundled into the app. + +`flutter_launcher_icons` fans them out to the Android densities, the adaptive icon, the +Android 13 monochrome layer and the iOS set, all of which are committed. + +To change it: + +``` +python tool/generate_icon.py +cd app && dart run flutter_launcher_icons +``` + +Two things to know before doing that. + +**The generator owns the framing.** An adaptive layer is 108dp of which only the central +72dp survives the launcher's mask, so `ADAPTIVE_WIDTH` is measured against that safe zone +and `adaptive_icon_foreground_inset` is set to `0`. Left at its default of 16 the tool +would inset the layer a second time and the icon would read visibly smaller than every +other one on the home screen. + +**flutter_launcher_icons 0.14.4 corrupts an unrelated Xcode setting.** `ios.dart:340` +rewrites the value of any line containing `ASSETCATALOG`, so it writes `AppIcon` into +`ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS`, which is a boolean. +Check `git diff app/ios/Runner.xcodeproj/project.pbxproj` after running it and restore +those lines to `YES`. It also minifies `AppIcon.appiconset/Contents.json` onto one line; +re-indenting it keeps the file reviewable. + diff --git a/Nuvolari/tool/generate_icon.py b/Nuvolari/tool/generate_icon.py new file mode 100644 index 0000000..230a9df --- /dev/null +++ b/Nuvolari/tool/generate_icon.py @@ -0,0 +1,225 @@ +"""Draw the Nuvolari launcher icon: a stylised cloud. + +Produces two 1024x1024 sources, which `flutter_launcher_icons` then fans out to +every Android density, the Android adaptive icon and the iOS set: + + app/assets/icon/icon.png white cloud on the app blue, full bleed + app/assets/icon/icon_foreground.png the cloud alone, transparent + +The foreground is drawn smaller on purpose. An Android adaptive icon layer is +108dp of which only the central 72dp is guaranteed to survive the launcher's +mask, so a cloud drawn edge to edge would lose its outer puffs to a circle. +This file owns that framing: flutter_launcher_icons is configured with +`adaptive_icon_foreground_inset: 0` so it does not shrink the layer a second +time on top of what is drawn here. + +Shapes are rendered from signed distance fields rather than by supersampling: a +cloud is a union of circles and one rounded box, the distance to that union is +exact, and turning distance into coverage gives clean antialiasing at one +sample per pixel. That keeps this to the standard library — no Pillow, no +cairo, nothing for anyone to install. + +Usage: + python tool/generate_icon.py +""" + +from __future__ import annotations + +import math +import pathlib +import struct +import zlib + +SIZE = 1024 + +OUT_DIR = pathlib.Path(__file__).resolve().parent.parent / "app" / "assets" / "icon" + +# The app's seed colour, so the icon and the interface are recognisably the same +# product. Keep these in step with ColorScheme.fromSeed in lib/main.dart. +BACKGROUND = (0x1F, 0x6F, 0xB2) +CLOUD = (0xFF, 0xFF, 0xFF) + +# How much of the canvas width the cloud spans. +# +# The full icon can run closer to the edges: iOS and the legacy Android icon are +# square and unmasked. +FULL_WIDTH = 0.72 + +# The adaptive layer is measured against the 72dp the mask always keeps, not +# against the 108dp canvas, so the two icons read at the same size on a home +# screen. 0.72 of that safe zone leaves the widest points of the cloud at 0.82 +# of the circle's radius — clear of a circular mask, and of the rounder masks +# some launchers use. +SAFE_ZONE = 72 / 108 +ADAPTIVE_WIDTH = 0.72 * SAFE_ZONE + + +# --- the cloud --------------------------------------------------------------- +# +# Defined in its own arbitrary coordinate space. The bounding box is computed +# from these numbers rather than written alongside them, so the shape can be +# tuned without the framing silently going wrong — which is exactly what +# happened when the two were maintained separately. + +# (centre x, centre y, half width, half height, corner radius) +BODY = (0.500, 0.560, 0.255, 0.075, 0.075) + +# (centre x, centre y, radius) +# +# The trailing puff rests on the base line: its lowest point is exactly the +# bottom of the body, so the outline runs from the arc into the flat base +# without turning back on itself. Left higher, the union re-enters and leaves a +# visible notch at the bottom left. +PUFFS = ( + (0.415, 0.470, 0.140), # main puff, left of centre and largest + (0.585, 0.495, 0.110), # shoulder puff, higher and smaller + (0.290, BODY[1] + BODY[3] - 0.088, 0.088), # trailing puff, on the base +) + + +def cloud_bounds() -> tuple[float, float, float, float]: + """Axis-aligned bounds of the whole cloud: (min x, min y, max x, max y).""" + cx, cy, hw, hh, _ = BODY + min_x, min_y = cx - hw, cy - hh + max_x, max_y = cx + hw, cy + hh + + for px, py, r in PUFFS: + min_x = min(min_x, px - r) + min_y = min(min_y, py - r) + max_x = max(max_x, px + r) + max_y = max(max_y, py + r) + + return min_x, min_y, max_x, max_y + + +# --- signed distance fields -------------------------------------------------- + + +def circle_sdf(x: float, y: float, cx: float, cy: float, r: float) -> float: + return math.hypot(x - cx, y - cy) - r + + +def rounded_box_sdf( + x: float, + y: float, + cx: float, + cy: float, + half_w: float, + half_h: float, + radius: float, +) -> float: + dx = abs(x - cx) - (half_w - radius) + dy = abs(y - cy) - (half_h - radius) + outside = math.hypot(max(dx, 0.0), max(dy, 0.0)) + inside = min(max(dx, dy), 0.0) + return outside + inside - radius + + +def cloud_sdf(x: float, y: float) -> float: + """Distance to the cloud: the union of the body and the puffs. + + The asymmetry is deliberate — a cloud built from equal circles reads as a + flower. + """ + distance = rounded_box_sdf(x, y, *BODY) + for puff in PUFFS: + distance = min(distance, circle_sdf(x, y, *puff)) + return distance + + +# --- rendering --------------------------------------------------------------- + + +def coverage(distance_px: float) -> float: + """Turns a signed distance in pixels into pixel coverage. + + A half-pixel band across the boundary is all the antialiasing a shape this + smooth needs. + """ + return min(max(0.5 - distance_px, 0.0), 1.0) + + +def render(size: int, width_fraction: float, opaque_background: bool) -> list[bytes]: + """Renders the icon, returning one RGBA bytes row per scanline. + + [width_fraction] is how much of the canvas width the cloud spans. The cloud + is centred on its own bounding box, so changing the shape above cannot leave + it drifting off-centre. + """ + min_x, min_y, max_x, max_y = cloud_bounds() + cloud_width = max_x - min_x + cloud_cx = (min_x + max_x) / 2 + cloud_cy = (min_y + max_y) / 2 + + # Canvas pixel -> cloud space. + pixels_per_unit = size * width_fraction / cloud_width + rows: list[bytes] = [] + + for py in range(size): + row = bytearray() + sy = (py + 0.5 - size / 2) / pixels_per_unit + cloud_cy + + for px in range(size): + sx = (px + 0.5 - size / 2) / pixels_per_unit + cloud_cx + + # Distance in cloud units, converted to pixels so the antialiasing + # band stays one pixel wide at any output size. + alpha = coverage(cloud_sdf(sx, sy) * pixels_per_unit) + + if opaque_background: + r = round(BACKGROUND[0] + (CLOUD[0] - BACKGROUND[0]) * alpha) + g = round(BACKGROUND[1] + (CLOUD[1] - BACKGROUND[1]) * alpha) + b = round(BACKGROUND[2] + (CLOUD[2] - BACKGROUND[2]) * alpha) + row += bytes((r, g, b, 255)) + else: + row += bytes((CLOUD[0], CLOUD[1], CLOUD[2], round(alpha * 255))) + + rows.append(bytes(row)) + + return rows + + +def write_png(path: pathlib.Path, size: int, rows: list[bytes]) -> None: + def chunk(kind: bytes, data: bytes) -> bytes: + return ( + struct.pack(">I", len(data)) + + kind + + data + + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF) + ) + + raw = b"".join(b"\x00" + row for row in rows) # filter type 0 per scanline + png = ( + b"\x89PNG\r\n\x1a\n" + + chunk(b"IHDR", struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)) + + chunk(b"IDAT", zlib.compress(raw, 9)) + + chunk(b"IEND", b"") + ) + path.write_bytes(png) + + +def main() -> int: + OUT_DIR.mkdir(parents=True, exist_ok=True) + + full = OUT_DIR / "icon.png" + write_png(full, SIZE, render(SIZE, FULL_WIDTH, opaque_background=True)) + print(f"{full.name}: {full.stat().st_size / 1024:.0f} KB") + + foreground = OUT_DIR / "icon_foreground.png" + write_png( + foreground, + SIZE, + render(SIZE, ADAPTIVE_WIDTH, opaque_background=False), + ) + print(f"{foreground.name}: {foreground.stat().st_size / 1024:.0f} KB") + + print( + "\nbackground #%02X%02X%02X cloud #%02X%02X%02X" + % (*BACKGROUND, *CLOUD) + ) + print("now run: flutter pub run flutter_launcher_icons") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())