ソースコード source code

下記アプリの主要なソースコードを公開しています。アプリ開発の参考になれば幸いです。

画像等が別途必要ですので下記情報のみでアプリが完成するものではありません。 アプリは少しずつ機能拡張していますのでストア公開されているアプリと内容が異なる場合があります。 コードはコピーして自由にお使いいただけます。ただし著作権は放棄しておりませんので全部の再掲載はご遠慮ください。部分的に再掲載したり、改変して再掲載するのは構いません。 自身のアプリ作成の参考として個人使用・商用問わず自由にお使いいただけます。 コード記述のお手本を示すものではありません。ミニアプリですので変数名などさほど気遣いしていない部分も有りますし間違いも有るかと思いますので参考程度にお考え下さい。 他の賢者の皆様が公開されているコードを参考にした箇所も含まれます。Flutter開発の熟練者が書いたコードではありません。 エンジニア向け技術情報共有サービスではありませんので説明は省いています。 GitHubなどへの公開は予定しておりません。

下記コードの最終ビルド日: 2025-09-17

pubspec.yaml

name: roulettewheel
description: "RouletteWheel"
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 2.0.3+23

environment:
  sdk: ">=3.3.0 <4.0.0"

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.20.2               #flutter gen-l10n
  shared_preferences: ^2.5.3
  flutter_tts: ^4.2.3
  google_mobile_ads: ^6.0.0
  audioplayers: ^6.0.0

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_launcher_icons: ^0.14.4    #flutter pub run flutter_launcher_icons
  flutter_native_splash: ^2.4.0     #flutter pub run flutter_native_splash:create

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^6.0.0

flutter_icons:
  android: "launcher_icon"
  ios: true
  image_path: "assets/icon/icon.png"
  adaptive_icon_background: "assets/icon/icon_back.png"
  adaptive_icon_foreground: "assets/icon/icon_fore.png"

flutter_native_splash:
  color: '#9da9f5'
  image: 'assets/image/splash.png'
  color_dark: '#9da9f5'
  image_dark: 'assets/image/splash.png'
  fullscreen: true
  android_12:
    icon_background_color: '#9da9f5'
    image: 'assets/image/splash.png'
    icon_background_color_dark: '#9da9f5'
    image_dark: 'assets/image/splash.png'


# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:
  generate: true

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  assets:
    - assets/icon/
    - assets/image/
    - assets/sound/

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/to/resolution-aware-images

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/to/asset-from-package

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/to/font-from-package

lib/ad_banner_widget.dart

import 'package:flutter/cupertino.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';

import 'package:roulettewheeleurope/ad_manager.dart';

class AdBannerWidget extends StatefulWidget {
  final AdManager adManager;
  const AdBannerWidget({super.key, required this.adManager});
  @override
  State<AdBannerWidget> createState() => _AdBannerWidgetState();
}

class _AdBannerWidgetState extends State<AdBannerWidget> {
  int _lastBannerWidthDp = 0;
  bool _isAdLoaded = false;
  bool _isLoading = false;
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: LayoutBuilder(
        builder: (context, constraints) {
          final int width = constraints.maxWidth.isFinite ? constraints.maxWidth.truncate() : MediaQuery.of(context).size.width.truncate();
          final bannerAd = widget.adManager.bannerAd;
          if (width > 0) {
            WidgetsBinding.instance.addPostFrameCallback((_) {
              if (mounted) {
                final bannerAd = widget.adManager.bannerAd;
                final bool widthChanged = _lastBannerWidthDp != width;
                final bool sizeMismatch = bannerAd == null || bannerAd.size.width != width;
                if ((widthChanged || !_isAdLoaded || sizeMismatch) && !_isLoading) {
                  _lastBannerWidthDp = width;
                  setState(() { _isAdLoaded = false; _isLoading = true; });
                  widget.adManager.loadAdaptiveBannerAd(width, () {
                    if (mounted) {
                      setState(() { _isAdLoaded = true; _isLoading = false; });
                    }
                  });
                }
              }
            });
          }
          if (_isAdLoaded && bannerAd != null) {
            return Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                SizedBox(
                  width: bannerAd.size.width.toDouble(),
                  height: bannerAd.size.height.toDouble(),
                  child: AdWidget(ad: bannerAd),
                ),
              ],
            );
          } else {
            return const SizedBox.shrink();
          }
        },
      ),
    );
  }
}

lib/ad_manager.dart

import 'dart:async';
import 'dart:io' show Platform;
import 'dart:ui';
import 'package:google_mobile_ads/google_mobile_ads.dart';

class AdManager {
  // Test IDs
  // static const String _androidAdUnitId = "ca-app-pub-3940256099942544/6300978111";
  // static const String _iosAdUnitId     = "ca-app-pub-3940256099942544/2934735716";

  // Production IDs
  static const String _androidAdUnitId = "ca-app-pub-0/0";
  static const String _iosAdUnitId     = "ca-app-pub-0/0";

  static String get _adUnitId => Platform.isIOS ? _iosAdUnitId : _androidAdUnitId;

  BannerAd? _bannerAd;
  int _lastWidthPx = 0;
  VoidCallback? _onLoadedCb;
  Timer? _retryTimer;
  int _retryAttempt = 0;

  BannerAd? get bannerAd => _bannerAd;

  Future<void> loadAdaptiveBannerAd(int widthPx, VoidCallback onAdLoaded) async {
    _onLoadedCb = onAdLoaded;
    _lastWidthPx = widthPx;
    _retryAttempt = 0;
    _retryTimer?.cancel();
    _startLoad(widthPx);
  }

  Future<void> _startLoad(int widthPx) async {
    _bannerAd?.dispose();

    AnchoredAdaptiveBannerAdSize? adaptiveSize;
    try {
      adaptiveSize = await AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(widthPx);
    } catch (_) {
      adaptiveSize = null;
    }
    final AdSize size = adaptiveSize ?? AdSize.fullBanner;

    _bannerAd = BannerAd(
      adUnitId: _adUnitId,
      request: const AdRequest(),
      size: size,
      listener: BannerAdListener(
        onAdLoaded: (ad) {
          _retryTimer?.cancel();
          _retryAttempt = 0;
          final cb = _onLoadedCb;
          if (cb != null) {
            cb();
          }
        },
        onAdFailedToLoad: (ad, err) {
          ad.dispose();
          _scheduleRetry();
        },
      ),
    )..load();
  }

  void _scheduleRetry() {
    _retryTimer?.cancel();
    // Exponential backoff: 3s, 6s, 12s, max 30s
    _retryAttempt = (_retryAttempt + 1).clamp(1, 5);
    final seconds = _retryAttempt >= 4 ? 30 : (3 << (_retryAttempt - 1));
    _retryTimer = Timer(Duration(seconds: seconds), () {
      _startLoad(_lastWidthPx > 0 ? _lastWidthPx : 320);
    });
  }

  void dispose() {
    _bannerAd?.dispose();
    _retryTimer?.cancel();
  }

}

lib/color_utils.dart

import 'package:flutter/material.dart';

// Maps roulette result codes to Colors.
// Codes: 'g' (green), 'k' (black), 'r' (red)
Color colorFromCode(String code) {
  switch (code) {
    case 'g':
      return const Color(0xFF00BB00);
    case 'k':
      return const Color(0xFF222222);
    case 'r':
      return const Color(0xFFD00000);
    default:
      return Colors.transparent;
  }
}

lib/const_value.dart

class ConstValue {
  static const String settings = "settings";
  static const String speechNumber = "speechNumber"; // 0 or 1
  static const String shortNumber = "shortNumber"; // 0..9
  static const String themeNumber = "themeNumber"; // 0 light, 1 dark
  static const String localeLanguage = "localeLanguage"; // "en", "ja", ... or ""
  static const String countdownNumber = "countdownNumber"; // 0 off, 1 on
  static const String soundVolume = "soundVolume"; // 0..10
  static const String speechVolume = "speechVolume"; // 0..10
  static const String voiceId = "voiceId"; // "<locale>|<name>"
}

lib/main.dart

import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'l10n/app_localizations.dart';

import 'package:roulettewheel/roulette_home.dart';


Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await MobileAds.instance.initialize();
  runApp(const RouletteWheelApp());
}

class RouletteWheelApp extends StatefulWidget {
  const RouletteWheelApp({super.key});
  @override
  State<RouletteWheelApp> createState() => _RouletteWheelAppState();
}

class _RouletteWheelAppState extends State<RouletteWheelApp> {
  ThemeMode _themeMode = ThemeMode.light;
  Locale? _locale;
  void updateThemeAndLocale({required int themeNumber, required String localeLanguage}) {
    setState(() {
      // 0: system, 1: light, 2: dark
      switch (themeNumber) {
        case 2:
          _themeMode = ThemeMode.dark;
          break;
        case 1:
          _themeMode = ThemeMode.light;
          break;
        default:
          _themeMode = ThemeMode.system;
      }
      _locale = _localeFromTag(localeLanguage);
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Roulette Wheel',
      themeMode: _themeMode,
      theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true),
      darkTheme: ThemeData.dark(useMaterial3: true),
      locale: _locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: RouletteHome(onUpdateApp: updateThemeAndLocale),
    );
  }
}

Locale? _localeFromTag(String tag) {
  if (tag.isEmpty) {
    return null;
  }
  final parts = tag.split('-');
  final lang = parts.isNotEmpty ? parts[0] : 'en';
  String? script;
  String? country;
  if (parts.length >= 2) {
    final p1 = parts[1];
    if (p1.length == 4) {
      script = p1;
    } else {
      country = p1;
    }
  }
  if (parts.length >= 3) {
    final p2 = parts[2];
    if (p2.length == 4) {
      script = p2;
    } else {
      country = p2;
    }
  }
  return Locale.fromSubtags(languageCode: lang, scriptCode: script, countryCode: country);
}

lib/roulette_home.dart

import 'dart:async';
import 'dart:io' show Platform;
import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'l10n/app_localizations.dart';

import 'package:roulettewheel/const_value.dart';
import 'package:roulettewheel/setting_page.dart';
import 'package:roulettewheel/wheel_view.dart';
import 'package:roulettewheel/ad_manager.dart';
import 'package:roulettewheel/ad_banner_widget.dart';
import 'package:roulettewheel/color_utils.dart';
import 'package:audioplayers/audioplayers.dart';


class RouletteHome extends StatefulWidget {
  final void Function({required int themeNumber, required String localeLanguage}) onUpdateApp;
  const RouletteHome({super.key, required this.onUpdateApp});
  @override
  State<RouletteHome> createState() => _RouletteHomeState();
}

class _RouletteHomeState extends State<RouletteHome> {
  late AdManager _adManager;

  // UI
  bool _startUiVisible = true;
  bool _settingUiVisible = true;
  // UI mirror state for Flutter-native wheel
  double _uiWheelAngle = 0;
  double _uiBallLeft = 0;
  double _uiBallTop = 0;
  double _uiBallSize = 0;
  bool _uiBallVisible = false;
  double _uiAlphaThree = 0;
  double _uiAlphaTwo = 0;
  double _uiAlphaOne = 0;
  double _uiAlphaNoMoreBets = 0;
  double _uiAlphaResult = 0;
  String _uiResultText = '';
  String _uiResultColor = '';
  final List<_HistoryItem> _history = <_HistoryItem>[];
  // TTS
  final FlutterTts _tts = FlutterTts();
  List<Map> _voices = [];
  // SFX
  final AudioPlayer _audio = AudioPlayer();

  // Prefs / settings
  int speechNumber = 1;
  int shortNumber = 0; // 0..9
  int themeNumber = 0; // 0 light, 1 dark
  String localeLanguage = ""; // empty = system
  int countdownNumber = 1; // 1 on, 0 off
  int speechVolume = 10; // 0..10
  int soundVolume = 10; // 0..10
  String voiceId = ""; // "<locale>|<name>"

  // Wheel logic
  double baseSize = 0; // logical size of square wheel area (in px)
  double wheelAngle = 360;
  double ballAngle = 0;
  double wheelAngleStart = 0;
  bool ballRotateFlag = false;
  int ballTick = 0;
  double adjustAngle = 0;
  double ballDistanceRatio = _ballDistanceStart;
  bool busy = false;
  Timer? _timer;

  static const double _ballSizeRatio = 0.04; // 4% of wheel diameter
  static const double _ballDistanceStart = 0.89;
  static const double _ballDistanceEnd = 0.535;

  static const List<_RouletteSlot> _slots = [
    _RouletteSlot("0", "g"),
    _RouletteSlot("28", "k"),
    _RouletteSlot("9", "r"),
    _RouletteSlot("26", "k"),
    _RouletteSlot("30", "r"),
    _RouletteSlot("11", "k"),
    _RouletteSlot("7", "r"),
    _RouletteSlot("20", "k"),
    _RouletteSlot("32", "r"),
    _RouletteSlot("17", "k"),
    _RouletteSlot("5", "r"),
    _RouletteSlot("22", "k"),
    _RouletteSlot("34", "r"),
    _RouletteSlot("15", "k"),
    _RouletteSlot("3", "r"),
    _RouletteSlot("24", "k"),
    _RouletteSlot("36", "r"),
    _RouletteSlot("13", "k"),
    _RouletteSlot("1", "r"),
    _RouletteSlot("00", "g"),
    _RouletteSlot("27", "r"),
    _RouletteSlot("10", "k"),
    _RouletteSlot("25", "r"),
    _RouletteSlot("29", "k"),
    _RouletteSlot("12", "r"),
    _RouletteSlot("8", "k"),
    _RouletteSlot("19", "r"),
    _RouletteSlot("31", "k"),
    _RouletteSlot("18", "r"),
    _RouletteSlot("6", "k"),
    _RouletteSlot("21", "r"),
    _RouletteSlot("33", "k"),
    _RouletteSlot("16", "r"),
    _RouletteSlot("4", "k"),
    _RouletteSlot("23", "r"),
    _RouletteSlot("35", "k"),
    _RouletteSlot("14", "r"),
    _RouletteSlot("2", "k"),
  ];

  @override
  void initState() {
    super.initState();
    _adManager = AdManager();
    _loadPrefs();
    _initTts();
    _initAudio();
    _startTicker();
  }

  @override
  void dispose() {
    _adManager.dispose();
    _audio.dispose();
    _timer?.cancel();
    super.dispose();
  }

  Future<void> _initAudio() async {
    await _audio.setReleaseMode(ReleaseMode.stop);
    await _audio.setVolume((soundVolume.clamp(0, 10)) / 10.0);
    // Preload source once to avoid per-play asset resolution/IO
    try {
      await _audio.setSource(AssetSource('sound/kachi.wav'));  //assets/は不要
    } catch (_) {}
  }

  Future<void> _initTts() async {
    try {
      final v = await _tts.getVoices; // returns List<dynamic>
      if (v is List) {
        _voices = v.cast<Map>();
        setState(() {});
      }
    } catch (_) {}
    unawaited(_setSpeechVoiceFromId());
    try {
      await _tts.setVolume((speechVolume.clamp(0, 10)) / 10.0);
    } catch (_) {}
  }

  Future<void> _loadPrefs() async {
    final pref = await SharedPreferences.getInstance();
    speechNumber = pref.getInt(ConstValue.speechNumber) ?? 1;
    voiceId = pref.getString(ConstValue.voiceId) ?? '';
    shortNumber = pref.getInt(ConstValue.shortNumber) ?? 0;
    themeNumber = pref.getInt(ConstValue.themeNumber) ?? 0;
    localeLanguage = pref.getString(ConstValue.localeLanguage) ?? "";
    countdownNumber = pref.getInt(ConstValue.countdownNumber) ?? 1;
    speechVolume = pref.getInt(ConstValue.speechVolume) ?? 10;
    soundVolume = pref.getInt(ConstValue.soundVolume) ?? 10;
    widget.onUpdateApp(themeNumber: themeNumber, localeLanguage: localeLanguage);
    setState(() {});
    try {
      await _tts.setVolume((speechVolume.clamp(0, 10)) / 10.0);
      await _audio.setVolume((soundVolume.clamp(0, 10)) / 10.0);
    } catch (_) {}
  }

  Future<void> _saveSpeechNumber() async {
    final pref = await SharedPreferences.getInstance();
    await pref.setInt(ConstValue.speechNumber, speechNumber);
  }

  Future<void> _saveVoiceId() async {
    final pref = await SharedPreferences.getInstance();
    await pref.setString(ConstValue.voiceId, voiceId);
  }

  Future<void> _saveShortNumber() async {
    final pref = await SharedPreferences.getInstance();
    await pref.setInt(ConstValue.shortNumber, shortNumber);
  }

  Future<void> _saveThemeNumber() async {
    final pref = await SharedPreferences.getInstance();
    await pref.setInt(ConstValue.themeNumber, themeNumber);
  }

  Future<void> _saveLocaleLanguage() async {
    final pref = await SharedPreferences.getInstance();
    await pref.setString(ConstValue.localeLanguage, localeLanguage);
  }

  Future<void> _saveCountdownNumber() async {
    final pref = await SharedPreferences.getInstance();
    await pref.setInt(ConstValue.countdownNumber, countdownNumber);
  }

  Future<void> _saveSoundVolume() async {
    final pref = await SharedPreferences.getInstance();
    await pref.setInt(ConstValue.soundVolume, soundVolume);
  }

  Future<void> _saveSpeechVolume() async {
    final pref = await SharedPreferences.getInstance();
    await pref.setInt(ConstValue.speechVolume, speechVolume);
  }

  Future<void> _setSpeechVoiceFromId() async {
    if (_voices.isEmpty || voiceId.isEmpty) return;
    final idx = voiceId.indexOf('|');
    String selLocale = '';
    String selName = voiceId;
    if (idx >= 0) {
      selLocale = voiceId.substring(0, idx);
      selName = voiceId.substring(idx + 1);
    }
    Map? match;
    if (selLocale.isNotEmpty) {
      match = _voices.cast<Map?>().firstWhere(
        (e) => (e?['name']?.toString() ?? '') == selName && (e?['locale']?.toString() ?? '') == selLocale,
        orElse: () => null,
      );
    }
    match ??= _voices.cast<Map?>().firstWhere(
      (e) => (e?['name']?.toString() ?? '') == selName,
      orElse: () => null,
    );
    if (match != null) {
      final locale = (match['locale']?.toString() ?? selLocale);
      final name = (match['name']?.toString() ?? selName);
      try {
        if (Platform.isAndroid) {
          // Prefer Google TTS if available; ignore errors if not installed
          try { await _tts.setEngine('com.google.android.tts'); } catch (_) {}
          if (locale.isNotEmpty) { await _tts.setLanguage(locale); }
          await _tts.setVoice({'name': name, 'locale': locale});
        } else if (Platform.isIOS) {
          // On iOS, setting voice is sufficient; avoid setLanguage overriding the voice
          await _tts.setVoice({'name': name, 'locale': locale});
        } else {
          // Fallback for other platforms
          if (locale.isNotEmpty) { await _tts.setLanguage(locale); }
          await _tts.setVoice({'name': name, 'locale': locale});
        }
      } catch (_) {}
    }
  }

  void _startTicker() {
    _timer = Timer.periodic(const Duration(milliseconds: 25), (_) {
      // wheel rotation
      wheelAngle -= 0.5;
      if (wheelAngle < 0) {
        wheelAngle = 359.5;
      }
      _setWheelRotation(wheelAngle);

      if (ballRotateFlag) {
        adjustAngle += 0.5;
        ballAngle += 5;
        if (ballAngle >= 360) {
          ballAngle = 0;
        }

        if (ballTick > 0) {
          ballTick -= 1;
          if (ballTick == 500) {
            if (countdownNumber == 1) {
              _showOverlay(three: 0.8);
              if (speechNumber == 1) {
                _speak("3");
              }
            }
          }
          if (ballTick == 450) {
            if (countdownNumber == 1) {
              _showOverlay(three: 0.0, two: 0.8);
              if (speechNumber == 1) {
                _speak("2");
              }
            }
          }
          if (ballTick == 400) {
            if (countdownNumber == 1) {
              _showOverlay(two: 0.0, one: 0.8);
              if (speechNumber == 1) {
                _speak("1");
              }
            }
          }
          if (ballTick == 350) {
            _showOverlay(one: 0.0, noMoreBets: 0.8);
            _speak(_localizedNoMoreBets());
          }
          if (ballTick == 250) {
            _showOverlay(noMoreBets: 0.0);
            ballTick -= (math.Random().nextDouble() * 100).toInt();
          }
          if (ballTick < 5) {
            ballDistanceRatio = (_ballDistanceStart + _ballDistanceEnd) / 2;
          }
          if (ballTick < 1) {
            ballDistanceRatio = _ballDistanceEnd;
          }
          if (ballTick <= 0) {
            ballRotateFlag = false;
            _setStartUiVisible(true);
            _resultNumber();
            busy = false;
            _playPocketSound();
            Future.delayed(const Duration(milliseconds: 800), () {
              _speak(_speakTextForNumber(_uiResultText));
            });
          }
        }
      }
      _ballPosition();
    });
  }

  String _localizedNoMoreBets() => AppLocalizations.of(context)?.noMoreBets ?? 'no more bets';

  void _speak(String text) {
    if (speechNumber == 1) {
      _tts.speak(text);
    }
  }

  void _onStart() {
    if (busy) {
      return;
    }
    busy = true;
    _setStartUiVisible(false);
    _showOverlay(resultAlpha: 0.0);
    ballAngle = 0;
    _setBallVisible(true);
    ballDistanceRatio = _ballDistanceStart;
    wheelAngleStart = wheelAngle;
    ballTick = (10 - shortNumber) * 100 + 260; // 1260..360
    adjustAngle = 0;
    ballRotateFlag = true;
  }

  void _resultNumber() {
    double angle = ballAngle;
    angle = ((angle - wheelAngleStart + adjustAngle) / (360 / 38)).toInt() * (360 / 38) + (180 / 38);
    angle = 180 - angle;
    angle += 3600;
    angle %= 360;
    int num = 38 - (angle / (360 / 38)).toInt() - 1;
    num %= 38;
    final slot = _slots[num];
    final resultNumber = slot.number;
    final resultColor = slot.color;
    _setResult(number: resultNumber, color: resultColor);
    _showOverlay(resultAlpha: 1.0);
    _addHistory(resultNumber, resultColor);
  }

  String _speakTextForNumber(String number) {
    if (number == '00') {
      return 'double zero';
    }
    return number;
  }

  void _addHistory(String number, String color) {
    setState(() {
      _history.insert(0, _HistoryItem(number, color));
      if (_history.length > 20) {
        _history.removeLast();
      }
    });
  }

  Future<void> _playPocketSound() async {
    final vol = soundVolume.clamp(0, 10) / 10.0;
    if (vol <= 0) return;
    try {
      await _audio.setVolume(vol);
      // Restart from beginning without reloading the asset
      await _audio.seek(Duration.zero);
      unawaited(_audio.resume());
    } catch (_) {
      // Fallback: try a direct play if preloading failed
      try {
        await _audio.stop();
        await _audio.play(AssetSource('sound/kachi.wav'));  //assets/は不要
      } catch (_) {}
    }
  }

  Widget _buildHistoryList() {
    if (_history.isEmpty) {
      return SizedBox.shrink();
    }
    return ListView.separated(
      reverse: false,
      itemCount: _history.length,
      separatorBuilder: (_, __) => const SizedBox(height: 1),
      itemBuilder: (context, index) {
        final item = _history[index];
        return Container(
          height: 19,
          decoration: BoxDecoration(
            color: colorFromCode(item.color),
            borderRadius: BorderRadius.circular(20),
          ),
          alignment: Alignment.center,
          child: Text(
            item.number,
            style: const TextStyle(color: Colors.white, fontSize: 16),
          ),
        );
      },
    );
  }

  void _ballPosition() {
    if (baseSize <= 0) {
      return;
    }
    final ballSize = baseSize * _ballSizeRatio;
    _setBallSize(ballSize.toInt());
    double angle = ballAngle;
    if (!ballRotateFlag) {
      angle = ((angle - wheelAngleStart + adjustAngle) / (360 / 38)).toInt() * (360 / 38) + (180 / 38);
      angle += wheelAngle;
    }
    double x = (-math.sin(angle * (math.pi / 180)) * (baseSize / 2));
    double y = (math.cos(angle * (math.pi / 180)) * (baseSize / 2));
    x *= ballDistanceRatio;
    y *= ballDistanceRatio;
    x += baseSize / 2.0 * 0.89;
    y += baseSize / 2.0 * 0.89;
    x += ballSize * 0.9;
    y += ballSize * 0.9;
    _setBallPosition(x.toInt(), y.toInt());
  }

  // Channel helpers
  Future<void> _setWheelRotation(double angle) async {
    setState(() {
      _uiWheelAngle = angle;
    });
  }

  Future<void> _setBallPosition(int x, int y) async {
    setState(() {
      _uiBallLeft = x.toDouble();
      _uiBallTop = y.toDouble();
    });
  }

  Future<void> _setBallSize(int sizePx) async {
    setState(() {
      _uiBallSize = sizePx.toDouble();
    });
  }

  Future<void> _setBallVisible(bool visible) async {
    setState(() {
      _uiBallVisible = visible;
    });
  }

  Future<void> _showOverlay({double? three, double? two, double? one, double? noMoreBets, double? resultAlpha}) async {
    setState(() {
      if (three != null) { _uiAlphaThree = three; }
      if (two != null) { _uiAlphaTwo = two; }
      if (one != null) { _uiAlphaOne = one; }
      if (noMoreBets != null) { _uiAlphaNoMoreBets = noMoreBets; }
      if (resultAlpha != null) { _uiAlphaResult = resultAlpha; }
    });
  }

  Future<void> _setResult({required String number, required String color}) async {
    setState(() {
      _uiResultText = number;
      _uiResultColor = color;
    });
  }

  void _setStartUiVisible(bool on) {
    setState(() {
      _startUiVisible = on;
      _settingUiVisible = on;
    });
  }

  @override
  Widget build(BuildContext context) {
    final bgColor = (themeNumber == 2) ? Color.fromARGB(255, 20,40,30) : Color.fromARGB(255, 50,130,60);
    return Scaffold(
      backgroundColor: bgColor,
      body: SafeArea(
        child: Column(
          children: [
            Row(children: [
              const Spacer(),
              Padding(
                padding: const EdgeInsets.only(right: 10),
                child: IconButton(
                  onPressed: _settingUiVisible ? _onTapSetting : null,
                  tooltip: AppLocalizations.of(context)!.setting,
                  icon: Icon(Icons.settings, color: Colors.white.withValues(alpha: _settingUiVisible ? 0.85 : 0)),
                ),
              ),
            ]),
            const SizedBox(height: 5),
            Expanded(
              child: LayoutBuilder(
                builder: (context, constraints) {
                  final paddingH = 10.0;
                  final width = constraints.maxWidth - paddingH * 2;
                  baseSize = width;
                  return SingleChildScrollView(
                    child: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
                      child: Column(
                        children: [
                          AspectRatio(
                            aspectRatio: 1,
                            child: Center(
                              child: WheelFlutterView(
                                size: width,
                                wheelAngleDeg: _uiWheelAngle,
                                ballVisible: _uiBallVisible,
                                ballLeft: _uiBallLeft,
                                ballTop: _uiBallTop,
                                ballSize: _uiBallSize,
                                alphaThree: _uiAlphaThree,
                                alphaTwo: _uiAlphaTwo,
                                alphaOne: _uiAlphaOne,
                                alphaNoMoreBets: _uiAlphaNoMoreBets,
                                alphaResult: _uiAlphaResult,
                                resultText: _uiResultText,
                                resultColor: _uiResultColor,
                              ),
                            ),
                          ),
                          const SizedBox(height: 20),
                          Stack(
                            children: [
                              Align(
                                alignment: Alignment.centerLeft,
                                child: SizedBox(
                                  width: 60,
                                  height: 200,
                                  child: _buildHistoryList(),
                                ),
                              ),
                              Align(
                                alignment: Alignment.center,
                                child: AnimatedOpacity(
                                  duration: const Duration(milliseconds: 300),
                                  opacity: _startUiVisible ? 1.0 : 0.0,
                                  child: Opacity(
                                    opacity: busy ? 0.4 : 1.0,
                                    child: ElevatedButton(
                                      onPressed: busy ? null : _onStart,
                                      style: ElevatedButton.styleFrom(
                                        shape: const CircleBorder(),
                                        fixedSize: const Size(180, 180),
                                        backgroundColor: Colors.white.withValues(alpha: 0.25),
                                        foregroundColor: Colors.white,
                                        elevation: 0,
                                      ),
                                      child: FittedBox(
                                        fit: BoxFit.scaleDown,
                                        child: Padding(
                                          padding: const EdgeInsets.all(8.0),
                                          child: Text(
                                            AppLocalizations.of(context)!.start,
                                            textAlign: TextAlign.center,
                                            style: const TextStyle(color: Colors.white, fontSize: 28),
                                          ),
                                        ),
                                      ),
                                    ),
                                  ),
                                ),
                              )
                            ],
                          ),
                          const SizedBox(height: 200),
                        ],
                      ),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
      bottomNavigationBar: AdBannerWidget(adManager: _adManager),
    );
  }

  Future<void> _onTapSetting() async {
    if (busy) {
      return;
    }
    final result = await Navigator.of(context).push(
      MaterialPageRoute(
        builder: (_) => SettingPage(
          speechNumber: speechNumber,
          shortNumber: shortNumber,
          themeNumber: themeNumber,
          localeLanguage: localeLanguage,
          countdownNumber: countdownNumber,
          speechVolume: speechVolume,
          soundVolume: soundVolume,
          voiceId: voiceId,
        ),
      ),
    );
    if (result is Map) {
      final lastSpeechNumber = speechNumber;
      speechNumber = (result[ConstValue.speechNumber] as int?) ?? 1;
      if (lastSpeechNumber != speechNumber) {
        await _saveSpeechNumber();
      }

      final lastVoiceId = voiceId;
      voiceId = (result[ConstValue.voiceId] as String?) ?? voiceId;
      if (lastVoiceId != voiceId) {
        await _saveVoiceId();
        await _setSpeechVoiceFromId();
      }

      final lastShortNumber = shortNumber;
      shortNumber = (result[ConstValue.shortNumber] as int?) ?? shortNumber;
      if (lastShortNumber != shortNumber) {
        await _saveShortNumber();
      }

      final lastTheme = themeNumber;
      themeNumber = (result[ConstValue.themeNumber] as int?) ?? themeNumber;
      if (lastTheme != themeNumber) {
        await _saveThemeNumber();
      }

      final lastLocale = localeLanguage;
      localeLanguage = (result[ConstValue.localeLanguage] as String?) ?? localeLanguage;
      if (lastLocale != localeLanguage) {
        await _saveLocaleLanguage();
      }

      final lastCountdown = countdownNumber;
      countdownNumber = (result[ConstValue.countdownNumber] as int?) ?? countdownNumber;
      if (lastCountdown != countdownNumber) {
        await _saveCountdownNumber();
      }

      final lastSoundVolume = soundVolume;
      soundVolume = (result[ConstValue.soundVolume] as int?) ?? soundVolume;
      if (lastSoundVolume != soundVolume) {
        await _saveSoundVolume();
        try {
          await _audio.setVolume((soundVolume.clamp(0, 10)) / 10.0);
        } catch (_) {}
      }

      final lastSpeechVolume = speechVolume;
      speechVolume = (result[ConstValue.speechVolume] as int?) ?? speechVolume;
      if (lastSpeechVolume != speechVolume) {
        await _saveSpeechVolume();
        try {
          await _tts.setVolume((speechVolume.clamp(0, 10)) / 10.0);
        } catch (_) {}
      }

      widget.onUpdateApp(themeNumber: themeNumber, localeLanguage: localeLanguage);
      setState(() {});
    }
  }
}

class _HistoryItem {
  final String number;
  final String color;
  _HistoryItem(this.number, this.color);
}

class _RouletteSlot {
  final String number;
  final String color;
  const _RouletteSlot(this.number, this.color);
}

lib/setting_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'l10n/app_localizations.dart';

import 'package:roulettewheel/const_value.dart';
import 'package:roulettewheel/ad_manager.dart';
import 'package:roulettewheel/ad_banner_widget.dart';

class SettingPage extends StatefulWidget {
  final int speechNumber;
  final int shortNumber; // 0..9
  final int themeNumber; // 0 system, 1 light, 2 dark
  final String localeLanguage; // BCP-47 tag or ""
  final int countdownNumber; // 0 off, 1 on
  final int speechVolume; // 0..10
  final int soundVolume; // 0..10
  final String voiceId; // "<locale>|<name>"
  const SettingPage({
    super.key,
    required this.speechNumber,
    required this.shortNumber,
    required this.themeNumber,
    required this.localeLanguage,
    required this.countdownNumber,
    required this.speechVolume,
    required this.soundVolume,
    required this.voiceId,
  });
  @override
  State<SettingPage> createState() => _SettingPageState();
}

class _SettingPageState extends State<SettingPage> {
  late AdManager _adManager;
  late bool _speechOn;
  late int _shortNumber;
  late int _themeNumber; // 0 system, 1 light, 2 dark
  late String _languageCode; // BCP-47 tag or empty for system
  late bool _countdownOn;
  late int _speechVolume; // 0..10
  late int _soundVolume; // 0..10
  String _voiceId = '';
  List<_VoiceOption> _voices = [];
  final Map<String, String> _languageOptions = const {
    'en': 'English',
    'bg': 'Bulgarian',
    'cs': 'Čeština',
    'da': 'Dansk',
    'de': 'Deutsch',
    'el': 'Ελληνικά',
    'es': 'Español',
    'et': 'Eesti',
    'fi': 'Suomi',
    'fr': 'Français',
    'hu': 'Magyar',
    'id': 'Indonesia',
    'it': 'Italiano',
    'ja': '日本語',
    'ko': '한국어',
    'lt': 'Lietuvių',
    'lv': 'Latviešu',
    'nl': 'Nederlands',
    'no': 'Norsk',
    'pl': 'Polski',
    'pt': 'Português',
    'ro': 'Română',
    'ru': 'Русский',
    'sk': 'Slovenčina',
    'sv': 'Svenska',
    'th': 'ไทย',
    'tr': 'Türkçe',
    'uk': 'Українська',
    'vi': 'Tiếng Việt',
    'zh': '中文',
  };

  @override
  void initState() {
    super.initState();
    _adManager = AdManager();
    _speechOn = widget.speechNumber != 0;
    _shortNumber = widget.shortNumber;
    _themeNumber = {0, 1, 2}.contains(widget.themeNumber) ? widget.themeNumber : 0;
    _languageCode = widget.localeLanguage;
    _countdownOn = (widget.countdownNumber != 0);
    _speechVolume = widget.speechVolume;
    _soundVolume = widget.soundVolume;
    _voiceId = widget.voiceId;
    _loadVoices();
  }

  @override
  void dispose() {
    _adManager.dispose();
    super.dispose();
  }

  Future<void> _loadVoices() async {
    final tts = FlutterTts();
    try {
      final vs = await tts.getVoices;
      final List<_VoiceOption> voiceOptions = [];
      if (vs is List) {
        for (final v in vs) {
          if (v is Map && v['name'] is String && v['locale'] is String) {
            voiceOptions.add(_VoiceOption(v['locale'] as String, v['name'] as String));
          }
        }
      }
      voiceOptions.sort((a, b) => a.label.compareTo(b.label));
      setState(() {
        _voices = voiceOptions;
        // Ensure voiceId is valid; fallback to first
        if (_voices.isNotEmpty) {
          final exists = _voices.any((o) => o.id == _voiceId);
          if (!exists) {
            _voiceId = _voices.first.id;
          }
        }
      });
    } catch (_) {}
  }

  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context)!;
    return Scaffold(
      backgroundColor: Theme.of(context).colorScheme.surface,
      appBar: AppBar(
        automaticallyImplyLeading: false,
        leading: IconButton(
          icon: const Icon(Icons.close),
          tooltip: l.cancel,
          onPressed: () => Navigator.of(context).pop(),
        ),
        title: null,
        actions: [
          Padding(
            padding: const EdgeInsets.only(right: 10),
            child: IconButton(
              onPressed: _onApply,
              tooltip: l.apply,
              icon: const Icon(Icons.check),
            ),
          ),
        ],
      ),
      body: SafeArea(
        child: Column(
          children: [
            Expanded(
              child: SingleChildScrollView(
                padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const SizedBox(height: 10),
                    _buildShortNumber(),
                    const Divider(height: 40, thickness: 1),
                    _buildCountdownSwitch(),
                    const Divider(height: 40, thickness: 1),
                    _buildSpeechSwitch(),
                    _buildSpeechVolume(),
                    _buildSpeechLanguage(),
                    const Divider(height: 40, thickness: 1),
                    _buildSoundVolume(),
                    const Divider(height: 40, thickness: 1),
                    _buildLanguage(),
                    const Divider(height: 40, thickness: 1),
                    _buildTheme(),
                    const Divider(height: 40, thickness: 1),
                    const SizedBox(height: 120),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
      bottomNavigationBar: AdBannerWidget(adManager: _adManager),
    );
  }

  void _onApply() {
    Navigator.of(context).pop({
      ConstValue.speechNumber: _speechOn ? 1 : 0,
      ConstValue.voiceId: _voiceId,
      ConstValue.shortNumber: _shortNumber,
      ConstValue.themeNumber: _themeNumber,
      ConstValue.localeLanguage: _languageCode,
      ConstValue.countdownNumber: _countdownOn ? 1 : 0,
      ConstValue.speechVolume: _speechVolume,
      ConstValue.soundVolume: _soundVolume,
    });
  }

  Widget _buildShortNumber() {
    final l = AppLocalizations.of(context)!;
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Row(children: [
        Expanded(child: Text(l.shortNumber,style: Theme.of(context).textTheme.bodyLarge)),
        SizedBox(
          child: Slider(
            min: 0,
            max: 10,
            divisions: 10,
            value: _shortNumber.toDouble(),
            label: _shortNumber.toString(),
            onChanged: (v) => setState(() => _shortNumber = v.round()),
          ),
        ),
        Text(
          _shortNumber.toInt().toString().padLeft(2, '0'),
          style: Theme.of(context).textTheme.titleMedium,
        )
      ])
    );
  }

  Widget _buildCountdownSwitch() {
    final l = AppLocalizations.of(context)!;
    return SwitchListTile(
      value: _countdownOn,
      onChanged: (v) => setState(() => _countdownOn = v),
      title: Text(l.countdown),
    );
  }

  Widget _buildSpeechSwitch() {
    final l = AppLocalizations.of(context)!;
    return SwitchListTile(
      value: _speechOn,
      onChanged: (v) => setState(() => _speechOn = v),
      title: Text(l.speechNumber),
    );
  }

  Widget _buildSpeechVolume() {
    final l = AppLocalizations.of(context)!;
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Row(children: [
        Expanded(child: Text(l.speechVolume, style: Theme.of(context).textTheme.bodyLarge)),
        SizedBox(
          child: Slider(
            min: 0,
            max: 10,
            divisions: 10,
            value: _speechVolume.toDouble(),
            label: _speechVolume.toString(),
            onChanged: (v) => setState(() => _speechVolume = v.round()),
          ),
        ),
        Text(
          _speechVolume.toInt().toString().padLeft(2, '0'),
          style: Theme.of(context).textTheme.titleMedium,
        )
      ])
    );
  }

  Widget _buildSpeechLanguage() {
    if (_voices.isEmpty) {
      return SizedBox.shrink();
    }
    return Padding(
      padding: const EdgeInsets.only(left: 16,right: 8),
      child: DropdownButtonFormField<String>(
        initialValue: () {
          if (_voiceId.isNotEmpty && _voices.any((o) => o.id == _voiceId)) {
            return _voiceId;
          }
          return _voices.first.id;
        }(),
        items: _voices
            .map((o) => DropdownMenuItem<String>(value: o.id, child: Text(o.label)))
            .toList(),
        onChanged: (v) {
          if (v == null) return;
          setState(() => _voiceId = v);
        },
      ),
    );
  }

  Widget _buildSoundVolume() {
    final l = AppLocalizations.of(context)!;
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Row(children: [
        Expanded(child: Text(l.soundVolume, style: Theme.of(context).textTheme.bodyLarge)),
        SizedBox(
          child: Slider(
            min: 0,
            max: 10,
            divisions: 10,
            value: _soundVolume.toDouble(),
            label: _soundVolume.toString(),
            onChanged: (v) => setState(() => _soundVolume = v.round()),
          ),
        ),
        Text(
          _soundVolume.toInt().toString().padLeft(2, '0'),
          style: Theme.of(context).textTheme.titleMedium)
      ])
    );
  }

  Widget _buildLanguage() {
    final l = AppLocalizations.of(context)!;
    return ListTile(
      contentPadding: const EdgeInsets.only(left: 16, right: 10),
      title: Text(l.language),
      trailing: DropdownButton<String?>(
        value: _languageCode.isEmpty ? null : _languageCode,
        hint: Text(l.systemDefault),
        items: [
          DropdownMenuItem<String?>(value: null, child: Text(l.systemDefault)),
          ..._languageOptions.entries.map((e) => DropdownMenuItem<String?>(value: e.key, child: Text(e.value))),
        ],
        onChanged: (value) => setState(() => _languageCode = value ?? ''),
      ),
    );
  }

  Widget _buildTheme() {
    final l = AppLocalizations.of(context)!;
    return ListTile(
      contentPadding: const EdgeInsets.only(left: 16, right: 10),
      title: Text(l.theme),
      trailing: DropdownButton<int>(
        value: _themeNumber,
        items: [
          DropdownMenuItem(value: 0, child: Text(l.systemDefault)),
          DropdownMenuItem(value: 1, child: Text(l.lightTheme)),
          DropdownMenuItem(value: 2, child: Text(l.darkTheme)),
        ],
        onChanged: (v) => setState(() => _themeNumber = v ?? 0),
      ),
    );
  }

}

class _VoiceOption {
  final String locale;
  final String name;
  const _VoiceOption(this.locale, this.name);
  String get id => '$locale|$name';
  String get label => '$locale $name';
}

lib/wheel_view.dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:roulettewheel/color_utils.dart';

class WheelFlutterView extends StatelessWidget {
  final double size;
  final double wheelAngleDeg;
  final bool ballVisible;
  final double ballLeft;
  final double ballTop;
  final double ballSize;
  final double alphaThree;
  final double alphaTwo;
  final double alphaOne;
  final double alphaNoMoreBets;
  final double alphaResult;
  final String resultText;
  final String resultColor; // 'g'|'k'|'r'

  const WheelFlutterView({
    super.key,
    required this.size,
    required this.wheelAngleDeg,
    required this.ballVisible,
    required this.ballLeft,
    required this.ballTop,
    required this.ballSize,
    required this.alphaThree,
    required this.alphaTwo,
    required this.alphaOne,
    required this.alphaNoMoreBets,
    required this.alphaResult,
    required this.resultText,
    required this.resultColor,
  });

  @override
  Widget build(BuildContext context) {
    final wheelAngleRad = wheelAngleDeg * (math.pi / 180.0);
    return SizedBox(
      width: size,
      height: size,
      child: Stack(
        clipBehavior: Clip.hardEdge,
        children: [
          // Base
          Positioned.fill(
            child: Image.asset(
              'assets/image/roulettewheel_base.png',
              fit: BoxFit.contain,
              filterQuality: FilterQuality.low,
            ),
          ),
          // Top (rotating)
          Positioned.fill(
            child: Transform.rotate(
              angle: wheelAngleRad,
              child: Image.asset(
                'assets/image/roulettewheel_top.png',
                fit: BoxFit.contain,
                filterQuality: FilterQuality.low,
              ),
            ),
          ),
          // Ball
          Positioned(
            left: ballLeft,
            top: ballTop,
            child: Opacity(
              opacity: ballVisible ? 1.0 : 0.0,
              child: Image.asset(
                'assets/image/ball.png',
                width: ballSize,
                height: ballSize,
                fit: BoxFit.contain,
                filterQuality: FilterQuality.low,
              ),
            ),
          ),
          // Overlays 3,2,1, No more bets (full-size)
          Positioned.fill(
            child: IgnorePointer(
              ignoring: true,
              child: AnimatedOpacity(
                opacity: alphaThree.clamp(0.0, 1.0),
                duration: const Duration(milliseconds: 300),
                child: Image.asset('assets/image/three.png', fit: BoxFit.contain),
              ),
            ),
          ),
          Positioned.fill(
            child: IgnorePointer(
              ignoring: true,
              child: AnimatedOpacity(
                opacity: alphaTwo.clamp(0.0, 1.0),
                duration: const Duration(milliseconds: 300),
                child: Image.asset('assets/image/two.png', fit: BoxFit.contain),
              ),
            ),
          ),
          Positioned.fill(
            child: IgnorePointer(
              ignoring: true,
              child: AnimatedOpacity(
                opacity: alphaOne.clamp(0.0, 1.0),
                duration: const Duration(milliseconds: 300),
                child: Image.asset('assets/image/one.png', fit: BoxFit.contain),
              ),
            ),
          ),
          Positioned.fill(
            child: IgnorePointer(
              ignoring: true,
              child: AnimatedOpacity(
                opacity: alphaNoMoreBets.clamp(0.0, 1.0),
                duration: const Duration(milliseconds: 300),
                child: Image.asset('assets/image/nomorebets.png', fit: BoxFit.contain),
              ),
            ),
          ),
          // Result text
          Positioned.fill(
            child: IgnorePointer(
              ignoring: true,
              child: AnimatedOpacity(
                duration: const Duration(milliseconds: 500),
                opacity: alphaResult.clamp(0.0, 1.0),
                child: Align(
                  alignment: Alignment.topLeft,
                  child: Container(
                    margin: const EdgeInsets.only(bottom: 8),
                    padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 3),
                    decoration: BoxDecoration(
                      color: colorFromCode(resultColor),
                      borderRadius: BorderRadius.circular(30),
                      border: Border.all(color: Colors.white, width: 1),
                    ),
                    child: Text(
                      resultText,
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 48,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

}