ソースコード source code

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

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

下記コードの最終ビルド日: 2025-10-12

pubspec.yaml

name: lotteryslot
description: "LotterySlot"
# 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: 1.1.5+15

environment:
  sdk: ^3.9.2

# 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

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.8
  package_info_plus: ^9.0.0
  shared_preferences: ^2.2.2
  flutter_localizations:    # flutter gen-l10n
    sdk: flutter
  intl: ^0.20.2
  flutter_tts: ^4.2.3
  google_mobile_ads: ^6.0.0
  just_audio: ^0.10.4

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_launcher_icons: ^0.14.4    #flutter pub run flutter_launcher_icons
  flutter_native_splash: ^2.3.5     #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

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

flutter_launcher_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: '#2f050d'
  image: 'assets/image/splash.png'
  color_dark: '#2f050d'
  image_dark: 'assets/image/splash.png'
  fullscreen: true
  android_12:
    icon_background_color: '#2f050d'
    image: 'assets/image/splash.png'
    icon_background_color_dark: '#2f050d'
    image_dark: 'assets/image/splash.png'

# 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/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:lotteryslot/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 Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                SizedBox(height: 10),
                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

/*
 *	mainへの記述
 *	void main() async {
 *		WidgetsFlutterBinding.ensureInitialized();
 *		if (!kIsWeb) {
 *			//AdMob初期化
 *			MobileAds.instance.initialize();
 *			//NPAポリシーの集中設定(将来拡張もここで) 現時点は使用していないので記述しなくても良い
 *			await AdManager.initForNPA();
 *		}
 *		runApp(const MyApp());
 *	}
 */

import 'dart:async';
import 'dart:io' show Platform;
import 'dart:ui';

import 'package:flutter/foundation.dart' show kIsWeb;
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;

  //(任意)アプリ起動時などに呼ぶ。将来のCMP/NPA関連設定を集中管理。
  static Future<void> initForNPA() async {
    if (kIsWeb) {
      return;
    }
    //ここでグローバルなRequestConfigurationを設定しておく(必要に応じて拡張)
    await MobileAds.instance.updateRequestConfiguration(
      RequestConfiguration(
        //例:最大コンテンツレーティング等を付けたい場合はここに追加
        //maxAdContentRating: MaxAdContentRating.g,	//例
        //tagForChildDirectedTreatment: TagForChildDirectedTreatment.unspecified,
        //tagForUnderAgeOfConsent: TagForUnderAgeOfConsent.unspecified,
      ),
    );
  }

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

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

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

    //常にNPAで配信(CMP対応)
    const adRequest = AdRequest(
      nonPersonalizedAds: true,	//NPA Non-Personalized Ads(非パーソナライズ広告)指定
    );

    _bannerAd = BannerAd(
      adUnitId: _adUnitId,
      request: 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() {
    if (kIsWeb) {
      return;
    }
    _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();
  }
}

/*
広告配信について
本アプリでは、Google AdMob を利用して広告を表示しています。
当アプリの広告はすべて「非パーソナライズ広告(NPA)」として配信しており、ユーザーの行動履歴や個人情報をもとにしたパーソナライズは一切行っていません。
Google AdMob によって、広告の表示のために以下の情報が利用される場合があります:
- 端末情報(例:OSの種類、画面サイズなど)
- おおまかな位置情報(国・地域レベル)
これらの情報は、パーソナライズを目的としたトラッキングやプロファイリングには使用されません。
詳しくは、Google のプライバシーポリシーをご覧ください:
https://policies.google.com/privacy


Advertising
This app uses Google AdMob to display advertisements.
All ads in this app are served as non-personalized ads (NPA).
This means that we do not use personal data or user behavior information to personalize the ads you see.
Google AdMob may use certain information in order to display ads properly, such as:
- Device information (e.g., OS type, screen size)
- Approximate location information (country/region level)
This information is not used for tracking or profiling for advertising purposes.
For more details, please refer to Google Privacy Policy:
https://policies.google.com/privacy
*/

lib/ad_ump_status.dart

import 'dart:async';

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/widgets.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';

import 'package:lotteryslot/l10n/app_localizations.dart';

class AdUmpState {
  const AdUmpState({
    required this.privacyStatus,
    required this.consentStatus,
    required this.privacyOptionsRequired,
    required this.isChecking,
  });

  final PrivacyOptionsRequirementStatus privacyStatus;
  final ConsentStatus consentStatus;
  final bool privacyOptionsRequired;
  final bool isChecking;

  AdUmpState copyWith({
    PrivacyOptionsRequirementStatus? privacyStatus,
    ConsentStatus? consentStatus,
    bool? privacyOptionsRequired,
    bool? isChecking,
  }) {
    return AdUmpState(
      privacyStatus: privacyStatus ?? this.privacyStatus,
      consentStatus: consentStatus ?? this.consentStatus,
      privacyOptionsRequired:
          privacyOptionsRequired ?? this.privacyOptionsRequired,
      isChecking: isChecking ?? this.isChecking,
    );
  }

  static const initial = AdUmpState(
    privacyStatus: PrivacyOptionsRequirementStatus.unknown,
    consentStatus: ConsentStatus.unknown,
    privacyOptionsRequired: false,
    isChecking: false,
  );
}

class UmpConsentController {
  UmpConsentController({this.forceEeaForDebug = false});

  final bool forceEeaForDebug;

  static const List<String> _testDeviceIds = <String>[
    '608970392F100B87D62A1174996C952C',
  ];

  ConsentRequestParameters _buildParams() {
    if (forceEeaForDebug && _testDeviceIds.isNotEmpty) {
      return ConsentRequestParameters(
        consentDebugSettings: ConsentDebugSettings(
          debugGeography: DebugGeography.debugGeographyEea,
          testIdentifiers: _testDeviceIds,
        ),
      );
    }
    return ConsentRequestParameters();
  }

  Future<AdUmpState> updateConsentInfo({AdUmpState current = AdUmpState.initial}) async {
    if (kIsWeb) {
      return current;
    }
    var state = current.copyWith(isChecking: true);
    try {
      final params = _buildParams();
      final completer = Completer<AdUmpState>();
      ConsentInformation.instance.requestConsentInfoUpdate(
        params,
        () async {
          final requirement =
              await ConsentInformation.instance.getPrivacyOptionsRequirementStatus();
          final consent = await ConsentInformation.instance.getConsentStatus();
          completer.complete(
            state.copyWith(
              privacyStatus: requirement,
              consentStatus: consent,
              privacyOptionsRequired:
                  requirement == PrivacyOptionsRequirementStatus.required,
              isChecking: false,
            ),
          );
        },
        (FormError error) {
          completer.complete(
            state.copyWith(
              privacyStatus: PrivacyOptionsRequirementStatus.unknown,
              consentStatus: ConsentStatus.unknown,
              privacyOptionsRequired: false,
              isChecking: false,
            ),
          );
        },
      );
      state = await completer.future;
      return state;
    } catch (_) {
      return state.copyWith(isChecking: false);
    }
  }

  Future<FormError?> showPrivacyOptions() async {
    if (kIsWeb) {
      return null;
    }
    final completer = Completer<FormError?>();
    ConsentForm.showPrivacyOptionsForm((FormError? error) {
      completer.complete(error);
    });
    return completer.future;
  }
}

extension ConsentStatusL10n on ConsentStatus {
  String localized(BuildContext context) {
    final localization = AppLocalizations.of(context)!;
    switch (this) {
      case ConsentStatus.obtained:
        return localization.cmpConsentStatusObtained;
      case ConsentStatus.required:
        return localization.cmpConsentStatusRequired;
      case ConsentStatus.notRequired:
        return localization.cmpConsentStatusNotRequired;
      case ConsentStatus.unknown:
        return localization.cmpConsentStatusUnknown;
    }
  }
}

lib/audio_play.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-01-27
///
library;

import 'package:just_audio/just_audio.dart';

import 'package:lotteryslot/const_value.dart';

class AudioPlay {
  //音を重ねて連続再生できるようにインスタンスを用意しておき、順繰りに使う。
  static final List<AudioPlayer> _playerMachineStart = [
    AudioPlayer(),
    AudioPlayer(),
  ];
  static final List<AudioPlayer> _playerMachineStop = [
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
  ];
  static final List<AudioPlayer> _playerPrize = [
    AudioPlayer(),
    AudioPlayer(),
  ];
  int _playerMachineStartPtr = 0;
  int _playerMachineStopPtr = 0;
  int _playerPrizePtr = 0;

  double _machineSoundVolume = 1.0;
  double _prizeSoundVolume = 1.0;

  //constructor
  AudioPlay() {
    _initial();
  }
  void _initial() async {
    for (int i = 0; i < _playerMachineStart.length; i++) {
      await _playerMachineStart[i].setVolume(0);
      await _playerMachineStart[i].setAsset(ConstValue.audioMachineStart);
    }
    for (int i = 0; i < _playerMachineStop.length; i++) {
      await _playerMachineStop[i].setVolume(0);
      await _playerMachineStop[i].setAsset(ConstValue.audioMachineStop);
    }
    for (int i = 0; i < _playerPrize.length; i++) {
      await _playerPrize[i].setVolume(0);
      await _playerPrize[i].setAsset(ConstValue.audioPrize);
    }
  }
  void dispose() {
    for (int i = 0; i < _playerMachineStart.length; i++) {
      _playerMachineStart[i].dispose();
    }
    for (int i = 0; i < _playerMachineStop.length; i++) {
      _playerMachineStop[i].dispose();
    }
    for (int i = 0; i < _playerPrize.length; i++) {
      _playerPrize[i].dispose();
    }
  }
  //setter
  set machineSoundVolume(double vol) {
    _machineSoundVolume = vol;
  }
  set prizeSoundVolume(double vol) {
    _prizeSoundVolume = vol;
  }
  //
  void playMachineStart() async {
    _playerMachineStartPtr += 1;
    if (_playerMachineStartPtr >= _playerMachineStart.length) {
      _playerMachineStartPtr = 0;
    }
    await _playerMachineStart[_playerMachineStartPtr].setVolume(_machineSoundVolume);
    await _playerMachineStart[_playerMachineStartPtr].pause();
    await _playerMachineStart[_playerMachineStartPtr].seek(Duration.zero);
    await _playerMachineStart[_playerMachineStartPtr].play();
  }
  void playMachineStop() async {
    _playerMachineStopPtr += 1;
    if (_playerMachineStopPtr >= _playerMachineStop.length) {
      _playerMachineStopPtr = 0;
    }
    await _playerMachineStop[_playerMachineStopPtr].setVolume(_machineSoundVolume);
    await _playerMachineStop[_playerMachineStopPtr].pause();
    await _playerMachineStop[_playerMachineStopPtr].seek(Duration.zero);
    await _playerMachineStop[_playerMachineStopPtr].play();
  }
  void playPrize() async {
    _playerPrizePtr += 1;
    if (_playerPrizePtr >= _playerPrize.length) {
      _playerPrizePtr = 0;
    }
    await _playerPrize[_playerPrizePtr].setVolume(_prizeSoundVolume);
    await _playerPrize[_playerPrizePtr].pause();
    await _playerPrize[_playerPrizePtr].seek(Duration.zero);
    await _playerPrize[_playerPrizePtr].play();
  }
}

lib/const_value.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-02
///
library;

class ConstValue {
  //pref
  static const String prefLanguageCode = 'languageCode';
  static const String prefCandidateText = 'candidateTexts';
  static const String prefPrizeText = 'prizeTexts';
  static const String prefHistoryText = 'historyTexts';
  static const String prefHistoryDrawFlag = 'historyDrawFlag';
  static const String prefMachineImageIndex = 'machineImageIndex';
  static const String prefMachineSpeed = 'machineSpeed';
  static const String prefMachineSoundVolume = 'machineSoundVolume';
  static const String prefPrizeSoundVolume = 'prizeSoundVolume';
  static const String prefSpeakSoundVolume = 'speakSoundVolume';
  static const String prefTtsEnabled = 'ttsEnabled';
  static const String prefTtsVoiceId = 'ttsVoiceId';
  static const String prefTtsVolume = 'ttsVolume';
  static const String prefThemeNumber = 'themeNumber';
  static const int minThemeNumber = 0;
  static const int maxThemeNumber = 2;
  static const int defaultThemeNumber = 0;
  //default
  static const String candidateTextDefault = '1-100';
  static const String prizeTextDefault = '1:Space travel\n2:Round-the-world trip\n3:Luxury sports car\n4-10:Coffee ticket\n11,22,33,44,55,66,77,88,99:Smartphone\n100:Laptop computer';
  static const String historyTextDefault = '';
  //image
  static const List<String> machineImages = [
    'assets/image/machine02.webp',
    'assets/image/machine03.webp',
    'assets/image/machine04.webp',
    'assets/image/machine01.webp',
  ];
  static const String imageMachineOver = 'assets/image/machine_over.webp';
  static const String imageReel = 'assets/image/reel.webp';
  //sound
  static const String audioMachineStart = 'assets/sound/reel_start.wav';
  static const String audioMachineStop = 'assets/sound/reel_stop.wav';
  static const String audioPrize = 'assets/sound/bell.wav';
}

lib/home_page.dart

library;

import 'dart:async';
import 'dart:io';
import 'dart:math';

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';

import 'package:lotteryslot/ad_banner_widget.dart';
import 'package:lotteryslot/ad_manager.dart';
import 'package:lotteryslot/audio_play.dart';
import 'package:lotteryslot/const_value.dart';
import 'package:lotteryslot/l10n/app_localizations.dart';
import 'package:lotteryslot/language_state.dart';
import 'package:lotteryslot/loading_screen.dart';
import 'package:lotteryslot/preferences.dart';
import 'package:lotteryslot/setting_page.dart';
import 'package:lotteryslot/text_to_speech.dart';
import 'package:lotteryslot/theme_mode_number.dart';
import 'package:lotteryslot/version_state.dart';
import 'package:lotteryslot/theme_color.dart';

class MainHomePage extends StatefulWidget {
  const MainHomePage({
    super.key,
    required this.onLocaleChanged,
    required this.onThemeModeChanged,
  });
  final ValueChanged<Locale?> onLocaleChanged;
  final ValueChanged<ThemeMode> onThemeModeChanged;
  @override
  State<MainHomePage> createState() => _MainHomePageState();
}

class _MainHomePageState extends State<MainHomePage> {
  late AdManager _adManager;
  final AudioPlay _audioPlay = AudioPlay();
  late ThemeColor _themeColor;
  bool _isReady = false;
  bool _isFirst = true;
  bool _busyFlag = false;
  final List<double> _digitPositions = <double>[
    -1.0,
    -0.82,
    -0.64,
    -0.46,
    -0.28,
    -0.1,
    0.08,
    0.26,
    0.44,
    0.61,
    0.81,
    0.99,
  ];
  final List<double> _digitAlignment = <double>[-1.0, -1.0, -1.0, -1.0, -1.0];
  final List<double> _digitSpeeds = <double>[0.010, 0.011, 0.012, 0.013, 0.014];
  final double _digitSpeedMax = 0.015;
  final TextEditingController _controllerDisplayPrizeString = TextEditingController();
  final TextEditingController _controllerDisplayRemainString = TextEditingController();
  final TextEditingController _controllerDisplayHistoryString = TextEditingController();
  double _displayPrizeStringOpacity = 0.0;

  void _getVersion() async {
    final packageInfo = await PackageInfo.fromPlatform();
    if (!mounted) {
      return;
    }
    setState(() {
      VersionState.versionSave(packageInfo.version);
    });
  }

  void _getCurrentLocale() async {
    final code = await LanguageState.getLanguageCode();
    if (!mounted) {
      return;
    }
    final locale = parseLocaleTag(code);
    widget.onLocaleChanged(locale);
  }

  Future<void> _applyThemeMode() async {
    final themeMode = ThemeModeNumber.numberToThemeMode(Preferences.themeNumber);
    widget.onThemeModeChanged(themeMode);
  }

  @override
  void initState() {
    super.initState();
    _initState();
  }

  Future<void> _initState() async {
    await Preferences.ensureReady();
    await LanguageState.ensureInitialized();
    _getVersion();
    _getCurrentLocale();
    _applyThemeMode();
    _adManager = AdManager();
    _audioPlay.machineSoundVolume = Preferences.machineSoundVolume;
    _audioPlay.prizeSoundVolume = Preferences.prizeSoundVolume;
    _audioPlay.playMachineStop();
    await TextToSpeech.getInstance();
    TextToSpeech.setTtsVoiceId(Preferences.ttsVoiceId);
    await TextToSpeech.setVolume(Preferences.ttsVolume);
    await TextToSpeech.setSpeechVoiceFromId();
    if (!kIsWeb && Platform.isAndroid) {
      _audioPlay.playMachineStop();
    }
    if (mounted) {
      setState(() {
        _isReady = true;
      });
    }
  }

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

  @override
  Widget build(BuildContext context) {
    if (!_isReady) {
      return const LoadingScreen();
    }
    if (_isFirst) {
      _isFirst = false;
      _themeColor = ThemeColor(themeNumber: Preferences.themeNumber, context: context);
    }
    final l = AppLocalizations.of(context)!;
    return Scaffold(
      appBar: AppBar(
        toolbarHeight: 40.0,
        backgroundColor: _themeColor.mainBackColor,
        foregroundColor: _themeColor.mainStartForeColor,
        actions: <Widget>[
          Opacity(
            opacity: _busyFlag ? 0.3 : 1,
            child: IconButton(
              tooltip: l.setting,
              icon: Icon(Icons.settings, color: _themeColor.mainButtonColor),
              onPressed: () async {
                if (_busyFlag) {
                  return;
                }
                final bool? ret = await Navigator.of(context).push(
                  MaterialPageRoute<bool>(
                    builder: (context) => SettingPage(),
                  ),
                );
                if (ret == true) {
                  _getCurrentLocale();
                  _applyThemeMode();
                  _audioPlay.machineSoundVolume = Preferences.machineSoundVolume;
                  _audioPlay.prizeSoundVolume = Preferences.prizeSoundVolume;
                  await TextToSpeech.setVolume(Preferences.ttsVolume);
                  TextToSpeech.setTtsVoiceId(Preferences.ttsVoiceId);
                  await TextToSpeech.setSpeechVoiceFromId();
                  List<int> historyNumbers = Preferences.getHistoryNumbers();
                  historyNumbers = historyNumbers.reversed.toList();
                  _controllerDisplayHistoryString.text = historyNumbers.join(', ');
                  setState(() {
                    _themeColor = ThemeColor(
                      themeNumber: Preferences.themeNumber,
                      context: context,
                    );
                  });
                }
              },
            ),
          ),
        ],
      ),
      body: SafeArea(
        child: Container(
          color: _themeColor.mainBackColor,
          child: Column(
            children: [
              Expanded(
                child: SingleChildScrollView(
                  child: Column(
                    children: <Widget>[
                      _stage(l),
                      _remainArea(),
                      _historyArea(),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
      bottomNavigationBar: AdBannerWidget(adManager: _adManager),
    );
  }

  Widget _digit(BoxConstraints constraints, double left, int column) {
    return Positioned(
      left: constraints.maxHeight * left,
      top: constraints.maxHeight * 0.314,
      child: ClipRect(
        child: Align(
          alignment: Alignment(0, _digitAlignment[column]),
          widthFactor: 1.0,
          heightFactor: 0.080,
          child: SizedBox(
            width: constraints.maxHeight * 0.08,
            child: Image.asset(ConstValue.imageReel),
          ),
        ),
      ),
    );
  }

  Widget _stage(AppLocalizations l) {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 4.0),
      decoration: BoxDecoration(
        color: Colors.transparent,
        borderRadius: BorderRadius.circular(30.0),
      ),
      clipBehavior: Clip.antiAlias,
      child: AspectRatio(
        aspectRatio: 1 / 1,
        child: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            return Stack(
              children: <Widget>[
                Image.asset(ConstValue.machineImages[Preferences.machineImageIndex]),
                _digit(constraints, 0.179, 4),
                _digit(constraints, 0.301, 3),
                _digit(constraints, 0.431, 2),
                _digit(constraints, 0.558, 1),
                _digit(constraints, 0.681, 0),
                Image.asset(ConstValue.imageMachineOver),
                _prizeArea(),
                Positioned(
                  right: 6,
                  bottom: 6,
                  child: ElevatedButton(
                    onPressed: _busyFlag ? null : _lottery,
                    style: ElevatedButton.styleFrom(
                      backgroundColor: _themeColor.mainStartBackColor,
                      foregroundColor: _themeColor.mainStartForeColor,
                      padding: const EdgeInsets.symmetric(
                        horizontal: 32,
                        vertical: 10,
                      ),
                    ),
                    child: Text(l.start,style: const TextStyle(fontSize: 18.0)),
                  ),
                ),
              ],
            );
          },
        ),
      )
    );
  }

  Widget _prizeArea() {
    return AnimatedOpacity(
      opacity: _displayPrizeStringOpacity,
      duration: const Duration(milliseconds: 500),
      child: _controllerDisplayPrizeString.text.isEmpty
        ? const SizedBox.shrink()
        : Container(
          margin: const EdgeInsets.all(6.0),
          decoration: BoxDecoration(
            color: Colors.yellowAccent,
            borderRadius: BorderRadius.circular(50.0),
          ),
          padding: const EdgeInsets.all(5.0),
          child: SizedBox(
            width: double.infinity,
            child: Text(
              _controllerDisplayPrizeString.text,
              maxLines: null,
              textAlign: TextAlign.center,
              style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold, fontSize: 22.0),
            )
          )
        )
    );
  }

  Widget _remainArea() {
    if (Preferences.historyDrawFlag == false) {
      return Container();
    }
    return TextField(
      controller: _controllerDisplayRemainString,
      maxLines: null,
      readOnly: true,
      style: TextStyle(color: _themeColor.mainCandidateForeColor),
      decoration: const InputDecoration(
        contentPadding: EdgeInsets.only(top: 0, left: 10, right: 10, bottom: 0),
        border: InputBorder.none,
      ),
    );
  }

  Widget _historyArea() {
    if (Preferences.historyDrawFlag == false) {
      return Container();
    }
    return TextField(
      controller: _controllerDisplayHistoryString,
      maxLines: null,
      readOnly: true,
      style: TextStyle(color: _themeColor.mainHistoryForeColor, fontSize: 26),
      decoration: const InputDecoration(
        contentPadding: EdgeInsets.only(top: 0, left: 10, right: 10, bottom: 10),
        border: InputBorder.none,
      ),
    );
  }

  void _lottery() async {
    if (_busyFlag) {
      return;
    }
    setState(() {
      _busyFlag = true;
    });
    _controllerDisplayPrizeString.text = '';
    _displayPrizeStringOpacity = 0.0;
    final List<int> historyNumbers = Preferences.getHistoryNumbers().reversed.toList();
    _controllerDisplayHistoryString.text = historyNumbers.join(', ');
    final int nextNumber = await _nextNumber();
    if (nextNumber == -1) {
      setState(() {
        _busyFlag = false;
      });
      return;
    }
    final ret = await Preferences.addHistoryText(nextNumber);
    if (ret == false) {
      return;
    }
    _digitSpeeds.shuffle();
    _audioPlay.playMachineStart();
    final int timeRemain = (10 - Preferences.machineSpeed) * 40;
    _lotteryRecursion(nextNumber, timeRemain, 4);
  }

  void _lotteryRecursion(int nextNumber, int timeRemain, int stopColumn) {
    for (int column = stopColumn; column >= 0; column--) {
      setState(() {
        _digitAlignment[column] += _digitSpeeds[column];
        if (_digitAlignment[column] >= _digitPositions[10]) {
          _digitAlignment[column] = _digitPositions[0];
        }
      });
    }
    timeRemain -= 1;
    if (timeRemain <= 0) {
      final double position =
          _digitPositions[(nextNumber / pow(10, stopColumn)).floor() % 10];
      if ((_digitAlignment[stopColumn] - position).abs() < _digitSpeedMax) {
        _digitAlignment[stopColumn] += _digitSpeedMax;
        _audioPlay.playMachineStop();
        stopColumn -= 1;
      }
    }
    if (stopColumn >= 0) {
      Timer(const Duration(milliseconds: 10), () {
        _lotteryRecursion(nextNumber, timeRemain, stopColumn);
      });
    } else {
      setState(() async {
        await Future.delayed(const Duration(milliseconds: 500));
        if (Preferences.ttsEnabled && Preferences.ttsVolume > 0.0) {
          TextToSpeech.speak(nextNumber.toString());
        }
        _prizeDraw(nextNumber);
        setState(() {
          _controllerDisplayHistoryString.text =
              '$nextNumber\n${_controllerDisplayHistoryString.text}';
          _busyFlag = false;
        });
      });
    }
  }

  Future<int> _nextNumber() async {
    List<int> remains = Preferences.getCandidateNumbers();
    if ((Preferences.getHistoryNumbers()).isNotEmpty) {
      remains = remains
          .where((int num) => !(Preferences.getHistoryNumbers()).contains(num))
          .toList();
    }
    if (remains.isEmpty) {
      return -1;
    }
    _controllerDisplayRemainString.text =
        'Candidates:${Preferences.getCandidateNumbers().length} Results:${Preferences.getHistoryNumbers().length + 1} Remaining:${remains.length - 1}';
    final int nextNumber = remains[Random().nextInt(remains.length)];
    return nextNumber;
  }

  void _prizeDraw(int nextNumber) async {
    for (final Map<String, dynamic> mapListOne in Preferences.getPrizeList()) {
      for (int j = 0; j < mapListOne['numbers'].length; j++) {
        if (mapListOne['numbers'][j] == nextNumber) {
          _controllerDisplayPrizeString.text = mapListOne['prize'];
          _displayPrizeStringOpacity = 1.0;
          await Future.delayed(const Duration(milliseconds: 1200));
          _audioPlay.playPrize();
          setState(() {});
          return;
        }
      }
    }
    _controllerDisplayPrizeString.text = '';
    _displayPrizeStringOpacity = 0.0;
    setState(() {});
  }

}

lib/language_state.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'package:lotteryslot/const_value.dart';

class LanguageCatalog {
  const LanguageCatalog._();

  static const String prefLanguageCode = ConstValue.prefLanguageCode;

  static const Map<String, String> names = {
    'en': 'English',
    'bg': 'български език',
    'cs': 'Čeština',
    'da': 'dansk',
    'de': 'Deutsch',
    'el': 'Ελληνικά',
    'es': 'Español',
    'et': 'eesti keel',
    'fi': 'Suomen kieli',
    'fr': 'Français',
    'hu': 'magyar nyelv',
    'it': 'Italiano',
    'ja': '日本語',
    'lt': 'lietuvių kalba',
    'lv': 'Latviešu',
    'nl': 'Nederlands',
    'pl': 'Polski',
    'pt': 'Português',
    'ro': 'limba română',
    'ru': 'русский',
    'sk': 'Slovenčina',
    'sv': 'svenska',
    'th': 'ภาษาไทย',
    'zh': '中文',
  };

  static List<String> get supportedCodes => names.keys.toList(growable: false);

  static List<Locale> buildSupportedLocales() {
    return supportedCodes.map((code) => Locale(code)).toList(growable: false);
  }

  static String labelFor(String? code) {
    if (code == null || code.isEmpty) {
      return 'Default';
    }
    return names[code] ?? code;
  }
}

abstract class LanguageStorage {
  const LanguageStorage();

  Future<void> saveLanguageCode(String code);

  Future<String> loadLanguageCode();
}

class SharedPreferencesLanguageStorage extends LanguageStorage {
  const SharedPreferencesLanguageStorage({
    this.prefLanguageCode = LanguageCatalog.prefLanguageCode,
  });

  final String prefLanguageCode;

  Future<SharedPreferences> _ensurePrefs() async {
    return SharedPreferences.getInstance();
  }

  @override
  Future<void> saveLanguageCode(String code) async {
    final prefs = await _ensurePrefs();
    await prefs.setString(prefLanguageCode, code);
  }

  @override
  Future<String> loadLanguageCode() async {
    final prefs = await _ensurePrefs();
    return prefs.getString(prefLanguageCode) ?? '';
  }
}

class LanguageState {
  LanguageState._();

  static LanguageStorage _storage = const SharedPreferencesLanguageStorage();
  static String _languageCode = '';
  static bool _initialized = false;
  static Completer<void>? _initializing;

  static String get currentCode => _languageCode;

  static void configure({LanguageStorage? storage, String? initialCode}) {
    if (storage != null) {
      _storage = storage;
    }
    if (initialCode != null) {
      _languageCode = initialCode;
      _initialized = true;
    }
  }

  static Future<void> ensureInitialized() async {
    if (_initialized) {
      return;
    }
    final completer = _initializing;
    if (completer != null) {
      await completer.future;
      return;
    }
    final newCompleter = Completer<void>();
    _initializing = newCompleter;
    try {
      _languageCode = await _storage.loadLanguageCode();
      _initialized = true;
      newCompleter.complete();
    } catch (error, stackTrace) {
      if (!newCompleter.isCompleted) {
        newCompleter.completeError(error, stackTrace);
      }
      rethrow;
    } finally {
      _initializing = null;
    }
  }

  static Future<void> setLanguageCode(String? code) async {
    final value = code?.trim() ?? '';
    _languageCode = value;
    _initialized = true;
    await _storage.saveLanguageCode(value);
  }

  static Future<String> getLanguageCode() async {
    await ensureInitialized();
    return _languageCode;
  }
}

Locale? parseLocaleTag(String tag) {
  if (tag.isEmpty) {
    return null;
  }
  final parts = tag.split('-');
  final language = parts.isNotEmpty ? parts[0] : tag;
  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: language,
    scriptCode: script,
    countryCode: country,
  );
}

lib/loading_screen.dart

import 'package:flutter/material.dart';

class LoadingScreen extends StatelessWidget {
  const LoadingScreen({super.key});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.brown[800],
      body: Center(
        child: CircularProgressIndicator(
          valueColor: AlwaysStoppedAnimation<Color>(Colors.brown[300]!),
          backgroundColor: Colors.white,
        ),
      ),
    );
  }
}

lib/main.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-09
///
library;

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';

import 'package:lotteryslot/home_page.dart';
import 'package:lotteryslot/l10n/app_localizations.dart';
import 'package:lotteryslot/language_state.dart';
import 'package:lotteryslot/preferences.dart';
import 'package:lotteryslot/theme_mode_number.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Preferences.ensureReady();
  await LanguageState.ensureInitialized();
  runApp(const MainApp());
}

class MainApp extends StatefulWidget {
  const MainApp({super.key});

  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  Locale? localeLanguage = parseLocaleTag(LanguageState.currentCode);
  ThemeMode themeMode = ThemeMode.system;

  @override
  void initState() {
    super.initState();
    if (!kIsWeb) {
      MobileAds.instance.initialize();
    }
    themeMode = ThemeModeNumber.numberToThemeMode(Preferences.themeNumber);
  }

  @override
  Widget build(BuildContext context) {
    const seed = Color(0xFFAD2A00);
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      locale: localeLanguage,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: seed),
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: seed,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      themeMode: themeMode,
      home: MainHomePage(
        onLocaleChanged: (locale) {
          setState(() {
            localeLanguage = locale;
          });
        },
        onThemeModeChanged: (mode) {
          setState(() {
            themeMode = mode;
          });
        },
      ),
    );
  }
}

lib/preferences.dart

import 'package:shared_preferences/shared_preferences.dart';

import 'package:lotteryslot/const_value.dart';

class Preferences {
  Preferences._();

  static bool _ready = false;
  static String _candidateText = ConstValue.candidateTextDefault;
  static String _prizeText = ConstValue.prizeTextDefault;
  static String _historyText = ConstValue.historyTextDefault;
  static bool _historyDrawFlag = true;
  static int _machineImageIndex = 0;
  static int _machineSpeed = 1;
  static double _machineSoundVolume = 1.0;
  static double _prizeSoundVolume = 1.0;
  static bool _ttsEnabled = true;
  static double _ttsVolume = 1.0;
  static String _ttsVoiceId = '';
  static int _themeNumber = ConstValue.defaultThemeNumber;

  static Future<void> ensureReady() async {
    if (_ready) {
      return;
    }
    final prefs = await SharedPreferences.getInstance();
    _ttsEnabled = prefs.getBool(ConstValue.prefTtsEnabled) ?? true;
    _ttsVoiceId = prefs.getString(ConstValue.prefTtsVoiceId) ?? '';
    _ttsVolume = (prefs.getDouble(ConstValue.prefTtsVolume) ?? 1.0).clamp(0.0,1.0);
    _candidateText = prefs.getString(ConstValue.prefCandidateText) ?? ConstValue.candidateTextDefault;
    _prizeText = prefs.getString(ConstValue.prefPrizeText) ?? ConstValue.prizeTextDefault;
    _historyText = prefs.getString(ConstValue.prefHistoryText) ?? ConstValue.historyTextDefault;
    _historyDrawFlag = prefs.getBool(ConstValue.prefHistoryDrawFlag) ?? true;
    _machineImageIndex = (prefs.getInt(ConstValue.prefMachineImageIndex) ?? 0).clamp(0,3);
    _machineSpeed = (prefs.getInt(ConstValue.prefMachineSpeed) ?? 1).clamp(1,9);
    _themeNumber = (prefs.getInt(ConstValue.prefThemeNumber) ?? 0).clamp(0, 2);
    _machineSoundVolume = (prefs.getDouble(ConstValue.prefMachineSoundVolume) ?? 1.0).clamp(0.0,1.0);
    _prizeSoundVolume = (prefs.getDouble(ConstValue.prefPrizeSoundVolume) ?? 1.0).clamp(0.0,1.0);
    _ready = true;
  }

  //get------------------

  static bool get ready => _ready;
  static String get candidateText => _candidateText;
  static String get prizeText => _prizeText;
  static String get historyText => _historyText;
  static bool get historyDrawFlag => _historyDrawFlag;
  static int get machineImageIndex => _machineImageIndex;
  static int get machineSpeed => _machineSpeed;
  static double get machineSoundVolume => _machineSoundVolume;
  static double get prizeSoundVolume => _prizeSoundVolume;
  static bool get ttsEnabled => _ttsEnabled;
  static double get ttsVolume => _ttsVolume;
  static String get ttsVoiceId => _ttsVoiceId;
  static int get themeNumber => _themeNumber;

  static List<int> getCandidateNumbers() {
    return _parseStrToNumbers(_candidateText);
  }

  static List<Map<String,dynamic>> getPrizeList() {
    List<Map<String,dynamic>> mapList = [];
    final List<String> lines = _prizeText.replaceAll('\r','').split('\n');
    for (int i = 0; i < lines.length; i++) {
      final List<String> ary = lines[i].split(':');
      final List<int> numbers = _parseStrToNumbers(ary[0]);
      final Map<String,dynamic> mapOne = {'numbers':numbers,'prize':ary[1]};
      mapList.add(mapOne);
    }
    return mapList;
  }

  static List<int> getHistoryNumbers() {
    return _parseStrToNumbers(_historyText);
  }

  //set------------------

  static Future<void> setTtsEnabled(bool value) async {
    _ttsEnabled = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(ConstValue.prefTtsEnabled, value);
  }

  static Future<void> setTtsVoiceId(String value) async {
    _ttsVoiceId = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(ConstValue.prefTtsVoiceId, value);
  }

  static Future<void> setTtsVolume(double value) async {
    _ttsVolume = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setDouble(ConstValue.prefTtsVolume, value);
  }

  static Future<void> setCandidateText(String value) async {
    value = _candidateFormat(value);
    _candidateText = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(ConstValue.prefCandidateText, value);
  }

  static Future<void> setPrizeText(String value) async {
    value = _prizeFormat(value);
    _prizeText = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(ConstValue.prefPrizeText, value);
  }

  static Future<void> setHistoryText(String value) async {
    value = _historyFormat(value);
    _historyText = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(ConstValue.prefHistoryText, value);
  }

  static Future<void> setHistoryDrawFlag(bool value) async {
    _historyDrawFlag = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(ConstValue.prefHistoryDrawFlag, value);
  }

  static Future<void> setMachineImageIndex(int value) async {
    _machineImageIndex = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setInt(ConstValue.prefMachineImageIndex, value);
  }

  static Future<void> setMachineSpeed(int value) async {
    _machineSpeed = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setInt(ConstValue.prefMachineSpeed, value);
  }

  static Future<void> setThemeNumber(int value) async {
    _themeNumber = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setInt(ConstValue.prefThemeNumber, _themeNumber);
  }

  static Future<void> setMachineSoundVolume(double value) async {
    _machineSoundVolume = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setDouble(ConstValue.prefMachineSoundVolume, _machineSoundVolume);
  }

  static Future<void> setPrizeSoundVolume(double value) async {
    _prizeSoundVolume = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setDouble(ConstValue.prefPrizeSoundVolume, _prizeSoundVolume);
  }

  static Future<bool> addHistoryText(int num) async {
    List<int> numbers = getHistoryNumbers();
    if (numbers.contains(num)) { //2重登録防止
      return false;
    }
    numbers.add(num);
    _historyText = numbers.map((int num) => num.toString()).join(',');
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(ConstValue.prefHistoryText, _historyText);
    return true;
  }

  //------------------

  //'1-10,12,15,17,20-50' などの文字列を数値配列に変換
  static List<int> _parseStrToNumbers(String numString) {
    final List<String> numStrings = numString.split(',');
    final List<int> numbers = <int>[];
    for (final String str in numStrings) {
      if (str.contains('-')) {
        final List<String> ary = str.split('-');
        if (_isStringToIntParsable(ary[0]) && _isStringToIntParsable(ary[1])) {
          for (int i = int.parse(ary[0]); i <= int.parse(ary[1]); i++) {
            numbers.add(i);
          }
        }
      } else {
        if (_isStringToIntParsable(str)) {
          numbers.add(int.parse(str));
        }
      }
    }
    return Set<int>.from(numbers).toList();
  }

  //String を int に変換できるか
  static bool _isStringToIntParsable(String str) {
    return int.tryParse(str) != null;
  }

  //選択肢を整える。ユーザーの入力なので適宜調整する
  static String _candidateFormat(String str) {
    str = str.replaceAll('0','0');
    str = str.replaceAll('1','1');
    str = str.replaceAll('2','2');
    str = str.replaceAll('3','3');
    str = str.replaceAll('4','4');
    str = str.replaceAll('5','5');
    str = str.replaceAll('6','6');
    str = str.replaceAll('7','7');
    str = str.replaceAll('8','8');
    str = str.replaceAll('9','9');
    str = str.replaceAll('、',',');
    str = str.replaceAll(',',',');
    str = str.replaceAll('ー','-');
    str = str.replaceAll('―','-');
    str = str.replaceAll(RegExp(r'[^0-9,-]'), '');
    str = str.replaceAll(RegExp(r',+'), ',');
    str = str.replaceAll(RegExp(r'\-+'), '-');
    return str;
  }

  //賞を整える。ユーザーの入力なので適宜調整する
  static String _prizeFormat(String str) {
    final List<String> lines = str.replaceAll('\r','').split('\n');
    List<String> prizes = [];
    for (String str in lines) {
      str = str.replaceAll(':',':');
      if (str.contains(':') == false) {
        continue;
      }
      List<String> ary = str.split(':');
      ary[0] = ary[0].replaceAll('0','0');
      ary[0] = ary[0].replaceAll('1','1');
      ary[0] = ary[0].replaceAll('2','2');
      ary[0] = ary[0].replaceAll('3','3');
      ary[0] = ary[0].replaceAll('4','4');
      ary[0] = ary[0].replaceAll('5','5');
      ary[0] = ary[0].replaceAll('6','6');
      ary[0] = ary[0].replaceAll('7','7');
      ary[0] = ary[0].replaceAll('8','8');
      ary[0] = ary[0].replaceAll('9','9');
      ary[0] = ary[0].replaceAll('、',',');
      ary[0] = ary[0].replaceAll(',',',');
      ary[0] = ary[0].replaceAll('ー','-');
      ary[0] = ary[0].replaceAll('―','-');
      ary[0] = ary[0].replaceAll(RegExp(r'[^0-9,-]'), '');
      ary[0] = ary[0].replaceAll(RegExp(r',+'), ',');
      ary[0] = ary[0].replaceAll(RegExp(r'\-+'), '-');
      prizes.add('${ary[0]}:${ary[1]}');
    }
    return prizes.join('\n');
  }

  //抽選結果を整える。ユーザーの入力なので適宜調整する
  static String _historyFormat(String str) {
    str = str.replaceAll('0','0');
    str = str.replaceAll('1','1');
    str = str.replaceAll('2','2');
    str = str.replaceAll('3','3');
    str = str.replaceAll('4','4');
    str = str.replaceAll('5','5');
    str = str.replaceAll('6','6');
    str = str.replaceAll('7','7');
    str = str.replaceAll('8','8');
    str = str.replaceAll('9','9');
    str = str.replaceAll('、',',');
    str = str.replaceAll(',',',');
    str = str.replaceAll(RegExp(r'[^0-9,]'), '');
    str = str.replaceAll(RegExp(r',+'), ',');
    return str;
  }

}

lib/setting_page.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-05
///
library;

import 'dart:async';

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

import 'package:lotteryslot/l10n/app_localizations.dart';
import 'package:lotteryslot/const_value.dart';
import 'package:lotteryslot/language_state.dart';
import 'package:lotteryslot/preferences.dart';
import 'package:lotteryslot/version_state.dart';
import 'package:lotteryslot/text_to_speech.dart';
import 'package:lotteryslot/ad_manager.dart';
import 'package:lotteryslot/ad_banner_widget.dart';
import 'package:lotteryslot/ad_ump_status.dart';
import 'package:lotteryslot/theme_color.dart';
import 'package:lotteryslot/loading_screen.dart';

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

class _SettingPageState extends State<SettingPage> {
  late AdManager _adManager;
  String? _languageKey; //null when system default
  final TextEditingController _controllerCandidateText = TextEditingController();
  final TextEditingController _controllerPrizeText = TextEditingController();
  final TextEditingController _controllerHistoryText = TextEditingController();
  bool _candidateInitialFlag = false;
  bool _prizeInitialFlag = false;
  bool _historyInitialFlag = false;
  bool _historyDrawFlag = true;
  int _machineImageIndex = 0;
  int _machineSpeedValue = 1;
  double _machineSoundVolume = 1.0;
  double _prizeSoundVolume = 1.0;
  late List<TtsOption> _ttsVoices;
  bool _ttsEnabled = true;
  double _ttsVolume = 1.0;
  String _ttsVoiceId = '';
  int _themeNumber = ConstValue.defaultThemeNumber;
  late ThemeColor _themeColor;
  bool _isReady = false;
  bool _isFirst = true;
  //AdUmpState
  late final UmpConsentController _adUmp;
  AdUmpState _adUmpState = AdUmpState.initial;

  @override
  void initState() {
    super.initState();
    _initState();
  }

  void _initState() async {
    _adManager = AdManager();
    final String languageCode = await LanguageState.getLanguageCode();
    _languageKey = languageCode.isEmpty ? null : languageCode;
    //
    await Preferences.ensureReady();
    await LanguageState.ensureInitialized();
    //
    _controllerCandidateText.text = Preferences.candidateText;
    _controllerPrizeText.text = Preferences.prizeText;
    _controllerHistoryText.text = Preferences.historyText;
    _historyDrawFlag = Preferences.historyDrawFlag;
    _machineImageIndex = Preferences.machineImageIndex;
    _machineSpeedValue = Preferences.machineSpeed;
    _machineSoundVolume = Preferences.machineSoundVolume;
    _prizeSoundVolume = Preferences.prizeSoundVolume;
    _ttsEnabled = Preferences.ttsEnabled;
    _ttsVolume = Preferences.ttsVolume;
    _ttsVoiceId = Preferences.ttsVoiceId;
    _themeNumber = Preferences.themeNumber;
    //speech
    await TextToSpeech.getInstance();
    _ttsVoices = TextToSpeech.ttsVoices;
    TextToSpeech.setVolume(_ttsVolume);
    TextToSpeech.setTtsVoiceId(_ttsVoiceId);
    _ttsVoiceId = TextToSpeech.ttsVoiceId; // ensure dropdown value matches available voice
    //
    _adUmp = UmpConsentController();
    _refreshConsentInfo();
    setState((){
      _isReady = true;
    });
  }

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

  Future<void> _onApply() async {
    await LanguageState.setLanguageCode(_languageKey);
    await Preferences.setTtsEnabled(_ttsEnabled);
    await Preferences.setTtsVoiceId(_ttsVoiceId);
    await Preferences.setTtsVolume(_ttsVolume);
    if (_candidateInitialFlag) {
      await Preferences.setCandidateText(ConstValue.candidateTextDefault);
    } else {
      await Preferences.setCandidateText(_controllerCandidateText.text);
    }
    if (_prizeInitialFlag) {
      await Preferences.setPrizeText(ConstValue.prizeTextDefault);
    } else {
      await Preferences.setPrizeText(_controllerPrizeText.text);
    }
    if (_historyInitialFlag) {
      await Preferences.setHistoryText(ConstValue.historyTextDefault);
    } else {
      await Preferences.setHistoryText(_controllerHistoryText.text);
    }
    await Preferences.setHistoryDrawFlag(_historyDrawFlag);
    await Preferences.setMachineImageIndex(_machineImageIndex);
    await Preferences.setMachineSpeed(_machineSpeedValue);
    await Preferences.setMachineSoundVolume(_machineSoundVolume);
    await Preferences.setPrizeSoundVolume(_prizeSoundVolume);
    await Preferences.setTtsVolume(_ttsVolume);
    await Preferences.setThemeNumber(_themeNumber);
    if (!mounted) {
      return;
    }
    Navigator.of(context).pop(true);
  }

  Future<void> _refreshConsentInfo() async {
    _adUmpState = await _adUmp.updateConsentInfo(current: _adUmpState);
    if (mounted) {
      setState(() {});
    }
  }

  Future<void> _onTapPrivacyOptions() async {
    final err = await _adUmp.showPrivacyOptions();
    await _refreshConsentInfo();
    if (err != null && mounted) {
      final l = AppLocalizations.of(context)!;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('${l.cmpErrorOpeningSettings} ${err.message}')),
      );
    }
  }

  //ページ描画
  @override
  Widget build(BuildContext context) {
    if (!_isReady) {
      return const LoadingScreen();
    }
    if (_isFirst) {
      _isFirst = false;
      _themeColor = ThemeColor(themeNumber: _themeNumber, context: context);
    }
    final l = AppLocalizations.of(context)!;
    return Scaffold(
      backgroundColor: _themeColor.backColor,
      appBar: AppBar(
        centerTitle: true,
        elevation: 0,
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: () {
            Navigator.of(context).pop(false);
          },
        ),
        title: Text(l.setting),
        foregroundColor: _themeColor.appBarForegroundColor,
        backgroundColor: Colors.transparent,
        actions: [
          IconButton(
            icon: const Icon(Icons.check),
            onPressed: _onApply,
          ),
        ],
      ),
      body: Column(children:[
        Expanded(
          child: GestureDetector(
            onTap: () => FocusScope.of(context).unfocus(),  //背景タップでキーボードを仕舞う
            child: SingleChildScrollView(
              child: Padding(
                padding: const EdgeInsets.only(left: 4, right: 4, top: 4, bottom: 100),
                child: Column(
                  children: [
                    _buildCandidate(l),
                    _buildPrize(l),
                    _buildHistory(l),
                    _buildMachineImage(l),
                    _buildSpeed(l),
                    _buildMachineVolume(l),
                    _buildSoundVolume(l),
                    _buildSpeak(l),
                    _buildTheme(l),
                    _buildLanguage(l),
                    _buildCmpSection(l),
                    _buildUsage(l),
                    _buildVersion(),
                  ],
                ),
              ),
            ),
          ),
        ),
      ]),
      bottomNavigationBar: AdBannerWidget(adManager: _adManager),
    );
  }
  Widget _buildCandidate(AppLocalizations l) {
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Column(children: [
          Padding(
            padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 0),
            child: Row(children:<Widget>[
              Expanded(
                child: Text(l.candidate,style: const TextStyle(fontSize: 16)),
              ),
              Text(l.initial),
              Switch(
                value: _candidateInitialFlag,
                onChanged: (bool value) {
                  setState(() {
                    _candidateInitialFlag = value;
                  });
                },
              ),
            ]),
          ),
          Padding(
            padding: const EdgeInsets.only(top: 1, left: 16, right: 16, bottom: 16),
            child: TextField(
              controller: _controllerCandidateText,
              maxLines: null, //nullで複数行のテキストエリア
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
              ),
            ),
          )
        ])
      )
    );
  }

  Widget _buildPrize(AppLocalizations l) {
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Column(children: [
          Padding(
            padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 0),
            child: Row(children:<Widget>[
              Expanded(
                child: Text(l.prize,style: const TextStyle(fontSize: 16)),
              ),
              Text(l.initial),
              Switch(
                value: _prizeInitialFlag,
                onChanged: (bool value) {
                  setState(() {
                    _prizeInitialFlag = value;
                  });
                },
              ),
            ]),
          ),
          Padding(
            padding: const EdgeInsets.only(top: 1, left: 16, right: 16, bottom: 16),
            child: TextField(
              controller: _controllerPrizeText,
              maxLines: null, //nullで複数行のテキストエリア
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
              ),
            ),
          ),
        ])
      )
    );
  }

  Widget _buildHistory(AppLocalizations l) {
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Column(children: [
          Padding(
            padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 0),
            child: Row(children:<Widget>[
              Expanded(
                child: Text(l.history,style: const TextStyle(fontSize: 16)),
              ),
              Text(l.erase),
              Switch(
                value: _historyInitialFlag,
                onChanged: (bool value) {
                  setState(() {
                    _historyInitialFlag = value;
                  });
                },
              ),
            ]),
          ),
          Padding(
            padding: const EdgeInsets.only(top: 1, left: 16, right: 16, bottom: 0),
            child: TextField(
              controller: _controllerHistoryText,
              maxLines: null, //nullで複数行のテキストエリア
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 6),
            child: Row(children:<Widget>[
              Expanded(
                child: Text(l.historyMainDraw,style: const TextStyle(fontSize: 16)),
              ),
              Switch(
                value: _historyDrawFlag,
                onChanged: (bool value) {
                  setState(() {
                    _historyDrawFlag = value;
                  });
                },
              ),
            ]),
          ),
        ])
      )
    );
  }

  Widget _buildSpeed(AppLocalizations l) {
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Column(children: [
          Padding(
            padding: const EdgeInsets.only(top: 12, left: 16, right: 16, bottom: 0),
            child: Row(children: [
              Text(l.machineSpeed,style: const TextStyle(fontSize: 16)),
              const Spacer(),
            ]),
          ),
          Row(
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.only(top: 0, left: 16, right: 0, bottom: 0),
                child: Text(_machineSpeedValue.toString())
              ),
              Expanded(
                child: Slider(
                  value: _machineSpeedValue.toDouble(),
                  min: 1,
                  max: 9,
                  divisions: 9,
                  onChanged: (double value) {
                    setState(() {
                      _machineSpeedValue = value.toInt();
                    });
                  },
                ),
              ),
            ],
          ),
        ])
      )
    );
  }

  Widget _buildMachineImage(AppLocalizations l) {
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Column(children: [
          Padding(
            padding: const EdgeInsets.only(top: 12, left: 16, right: 16, bottom: 0),
            child: Row(children: [
              Text(l.machineImageIndex,style: const TextStyle(fontSize: 16)),
              const Spacer(),
            ]),
          ),
          Row(
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.only(top: 0, left: 16, right: 0, bottom: 0),
                child: Text(_machineImageIndex.toString())
              ),
              Expanded(
                child: Slider(
                  value: _machineImageIndex.toDouble(),
                  min: 0,
                  max: ConstValue.machineImages.length - 1,
                  divisions: ConstValue.machineImages.length - 1,
                  onChanged: (double value) {
                    setState(() {
                      _machineImageIndex = value.toInt();
                    });
                  },
                ),
              ),
            ],
          ),
        ])
      )
    );
  }

  Widget _buildMachineVolume(AppLocalizations l) {
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Column(children: [
          Padding(
            padding: const EdgeInsets.only(top: 12, left: 16, right: 16, bottom: 0),
            child: Row(children: [
              Text(l.machineSoundVolume, style: const TextStyle(fontSize: 16)),
              const Spacer(),
            ]),
          ),
          Row(children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(top: 0, left: 16, right: 0, bottom: 0),
              child: Text(_machineSoundVolume.toString())
            ),
            Expanded(
              child: Slider(
                value: _machineSoundVolume,
                min: 0.0,
                max: 1.0,
                divisions: 10,
                onChanged: (double value) {
                  setState(() {
                    _machineSoundVolume = value;
                  });
                },
              ),
            ),
          ])
        ])
      )
    );
  }

  Widget _buildSoundVolume(AppLocalizations l) {
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Column(children: [
          Padding(
            padding: const EdgeInsets.only(top: 12, left: 16, right: 16, bottom: 0),
            child: Row(children: [
              Text(l.prizeSoundVolume,style: const TextStyle(fontSize: 16)),
              const Spacer(),
            ]),
          ),
          Row(
            children: <Widget>[
              Padding(
                  padding: const EdgeInsets.only(top: 0, left: 16, right: 0, bottom: 0),
                  child: Text(_prizeSoundVolume.toString())
              ),
              Expanded(
                child: Slider(
                  value: _prizeSoundVolume,
                  min: 0.0,
                  max: 1.0,
                  divisions: 10,
                  onChanged: (double value) {
                    setState(() {
                      _prizeSoundVolume = value;
                    });
                  },
                ),
              ),
            ],
          ),
        ])
      )
    );
  }

  Widget _buildSpeak(AppLocalizations l) {
    final disabledColor = Theme.of(context).disabledColor;
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Padding(
          padding: const EdgeInsets.only(top: 12, left: 16, right: 16, bottom: 16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Text(l.speakResult, style: const TextStyle(fontSize: 16)),
                  const Spacer(),
                  Switch(
                    value: _ttsEnabled,
                    onChanged: (bool value) {
                      setState(() {
                        _ttsEnabled = value;
                      });
                      if (!value) {
                        TextToSpeech.stop();
                      }
                    },
                  ),
                ],
              ),
              const SizedBox(height: 12),
              Row(
                children: [
                  Text(
                    l.speakSoundVolume,
                    style: TextStyle(
                      fontSize: 14,
                      color: _ttsEnabled ? null : disabledColor,
                    ),
                  ),
                  const SizedBox(width: 8),
                  Text(
                    _ttsVolume.toStringAsFixed(1),
                    style: TextStyle(color: _ttsEnabled ? null : disabledColor),
                  ),
                ],
              ),
              Slider(
                value: _ttsVolume,
                min: 0.0,
                max: 1.0,
                divisions: 10,
                onChanged: _ttsEnabled
                  ? (double value) {
                    setState(() {
                      _ttsVolume = value;
                    });
                    TextToSpeech.setVolume(value);
                  }
                  : null,
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  Text(
                    l.voice,
                    style: TextStyle(
                      fontSize: 16,
                      color: _ttsEnabled ? null : disabledColor,
                    ),
                  ),
                  const Spacer(),
                  DropdownButton<String?>(
                    value: _ttsVoiceId,
                    onChanged: _ttsEnabled
                        ? (String? value) async {
                            if (value == null) {
                              return;
                            }
                            setState(() {
                              _ttsVoiceId = value;
                            });
                            TextToSpeech.setTtsVoiceId(value);
                            await TextToSpeech.setSpeechVoiceFromId();
                          }
                        : null,
                    items: _ttsVoices
                        .map(
                          (option) => DropdownMenuItem<String?>(
                            value: option.id,
                            child: Text(option.label),
                          ),
                        )
                        .toList(),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildTheme(AppLocalizations l) {
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Padding(
          padding: const EdgeInsets.only(top: 12, left: 16, right: 16, bottom: 12),
          child: Row(children: <Widget>[
            Text(l.theme, style: const TextStyle(fontSize: 16)),
            const Spacer(),
            DropdownButton<int>(
              value: _themeNumber,
              dropdownColor: _themeColor.dropdownColor,
              onChanged: (int? value) {
                if (value == null) {
                  return;
                }
                setState(() {
                  _themeNumber = value;
                });
              },
              items: <DropdownMenuItem<int>>[
                DropdownMenuItem(value: 0, child: Text(l.systemDefault)),
                DropdownMenuItem(value: 1, child: Text(l.lightTheme)),
                DropdownMenuItem(value: 2, child: Text(l.darkTheme)),
              ],
            ),
          ]),
        ),
      ),
    );
  }

  Widget _buildLanguage(AppLocalizations l) {
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Padding(
          padding: const EdgeInsets.only(top: 12, left: 16, right: 16, bottom: 12),
          child: Row(children: <Widget>[
            Text(l.language, style: const TextStyle(fontSize: 16)),
            const Spacer(),
            DropdownButton<String?>(
              value: _languageKey,
              dropdownColor: _themeColor.dropdownColor,
              onChanged: (String? value) {
                setState(() {
                  _languageKey = value;
                });
              },
              items: <DropdownMenuItem<String?>>[
                const DropdownMenuItem<String?>(
                  value: null,
                  child: Text('Default'),
                ),
                ...LanguageCatalog.names.entries.map(
                  (entry) => DropdownMenuItem<String?>(
                    value: entry.key,
                    child: Text(entry.value),
                  ),
                ),
              ],
            ),
          ]),
        ),
      ),
    );
  }

  Widget _buildCmpSection(AppLocalizations l) {
    String statusLabel = l.cmpCheckingRegion;
    IconData statusIcon = Icons.help_outline;
    final showButtons =
        _adUmpState.privacyStatus == PrivacyOptionsRequirementStatus.required;
    switch (_adUmpState.privacyStatus) {
      case PrivacyOptionsRequirementStatus.required:
        statusLabel = l.cmpRegionRequiresSettings;
        statusIcon = Icons.privacy_tip;
        break;
      case PrivacyOptionsRequirementStatus.notRequired:
        statusLabel = l.cmpRegionNoSettingsRequired;
        statusIcon = Icons.check_circle_outline;
        break;
      case PrivacyOptionsRequirementStatus.unknown:
        statusLabel = l.cmpRegionCheckFailed;
        statusIcon = Icons.error_outline;
        break;
    }
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Padding(
          padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 22),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                l.cmpSettingsTitle,
                style: Theme.of(context).textTheme.bodyMedium,
              ),
              const SizedBox(height: 8),
              Text(
                l.cmpConsentDescription,
                style: Theme.of(context).textTheme.bodySmall,
              ),
              const SizedBox(height: 8),
              Center(
                child: Column(
                  children: [
                    Chip(
                      avatar: Icon(statusIcon, size: 18),
                      label: Text(statusLabel),
                      side: BorderSide.none,
                    ),
                    const SizedBox(height: 4),
                    Text(
                      '${l.cmpConsentStatusLabel} ${_adUmpState.consentStatus.localized(context)}',
                      style: Theme.of(context).textTheme.bodySmall,
                    ),
                    if (showButtons)
                      Column(
                        children: [
                          const SizedBox(height: 16),
                          ElevatedButton.icon(
                            onPressed: _adUmpState.isChecking ? null : _onTapPrivacyOptions,
                            icon: const Icon(Icons.settings),
                            label: Text(
                              _adUmpState.isChecking
                                  ? l.cmpConsentStatusChecking
                                  : l.cmpOpenConsentSettings,
                            ),
                            style: ElevatedButton.styleFrom(
                              elevation: 0,
                              side: const BorderSide(width: 1),
                            ),
                          ),
                          const SizedBox(height: 16),
                          OutlinedButton.icon(
                            onPressed: _adUmpState.isChecking ? null : _refreshConsentInfo,
                            icon: const Icon(Icons.refresh),
                            label: Text(l.cmpRefreshStatus),
                          ),
                          const SizedBox(height: 16),
                          OutlinedButton.icon(
                            onPressed: _adUmpState.isChecking
                              ? null
                              : () async {
                                await ConsentInformation.instance.reset();
                                await _refreshConsentInfo();
                                if (mounted) {
                                  ScaffoldMessenger.of(context).showSnackBar(
                                    SnackBar(content: Text(l.cmpResetStatusDone)),
                                  );
                                }
                              },
                            icon: const Icon(Icons.restore),
                            label: Text(l.cmpResetStatus),
                          ),
                        ],
                      ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildUsage(AppLocalizations l) {
    final noteStyle = Theme.of(context).textTheme.bodySmall;
    return SizedBox(
        width: double.infinity,
        child: Card(
          margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
          color: _themeColor.cardColor,
          elevation: 0,
          shadowColor: Colors.transparent,
          surfaceTintColor: Colors.transparent,
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(l.usage1, style: noteStyle),
                const SizedBox(height: 8),
                Text(l.usage2, style: noteStyle),
                const SizedBox(height: 16),
                Text(l.usage3, style: noteStyle),
                const SizedBox(height: 16),
                Text(l.usage4, style: noteStyle),
              ],
            ),
          ),
        )
    );
  }

  Widget _buildVersion() {
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 16),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Padding(
          padding: const EdgeInsets.symmetric(vertical: 16),
          child: Center(
            child: Text(
              'version  ${VersionState.versionLoad()}',
              style: const TextStyle(fontSize: 10),
            ),
          ),
        ),
      )
    );
  }

}

lib/text_to_speech.dart

import 'dart:io' show Platform;

import 'package:flutter_tts/flutter_tts.dart';

class TtsOption {
  final String locale;
  final String name;
  const TtsOption(this.locale, this.name);

  String get id => '$locale|$name';
  String get label => name.isEmpty ? locale : '$locale $name';
}

class TextToSpeech {
  TextToSpeech._();

  static final FlutterTts _tts = FlutterTts();
  static final List<TtsOption> ttsVoices = <TtsOption>[];
  static String ttsVoiceId = '';
  static bool _initialized = false;

  static Future<void> getInstance() async {
    if (_initialized) {
      return;
    }
    await _initial();
    _initialized = true;
  }

  static Future<void> _initial() async {
    try {
      List<dynamic>? rawVoices;
      for (int i = 0; i < 10; i++) {
        rawVoices = await _tts.getVoices;
        if (rawVoices != null) {
          break;
        }
        await Future.delayed(const Duration(seconds: 1));
      }
      ttsVoices.clear();
      if (rawVoices is List) {
        for (final voice in rawVoices) {
          if (voice is Map &&
              voice['locale'] is String &&
              voice['name'] is String) {
            final locale = (voice['locale'] as String).trim();
            final name = (voice['name'] as String).trim();
            ttsVoices.add(TtsOption(locale, name));
          }
        }
      }
      ttsVoices.sort((a, b) => a.label.compareTo(b.label));
      ttsVoices.insert(0, const TtsOption('Default', ''));
      if (ttsVoices.isNotEmpty) {
        ttsVoiceId = ttsVoices.first.id;
      }
      await _tts.awaitSpeakCompletion(true);
    } catch (_) {}
  }

  static void setTtsVoiceId(String newId) {
    if (newId.isEmpty) {
      ttsVoiceId = ttsVoices.isNotEmpty ? ttsVoices.first.id : '';
      return;
    }
    final match = _findVoice((option) => option.id == newId);
    ttsVoiceId = (match ?? (ttsVoices.isNotEmpty ? ttsVoices.first : null))?.id ?? '';
  }

  static Future<void> setSpeechVoiceFromId() async {
    if (ttsVoices.isEmpty || ttsVoiceId.isEmpty) {
      return;
    }
    final option = _voiceFromId(ttsVoiceId) ??
        (ttsVoices.isNotEmpty ? ttsVoices.first : null);
    if (option == null) {
      return;
    }
    if (option.locale == 'Default') {
      return;
    }
    try {
      final locale = option.locale;
      final name = option.name;
      if (Platform.isAndroid) {
        try {
          await _tts.setEngine('com.google.android.tts');
        } catch (_) {}
        if (locale.isNotEmpty && locale != 'Default') {
          await _tts.setLanguage(locale);
        }
        await _tts.setVoice({'name': name, 'locale': locale});
      } else if (Platform.isIOS) {
        await _tts.setVoice({'name': name, 'locale': locale});
      } else {
        if (locale.isNotEmpty && locale != 'Default') {
          await _tts.setLanguage(locale);
        }
        await _tts.setVoice({'name': name, 'locale': locale});
      }
    } catch (_) {}
  }

  static Future<void> speak(String text) async {
    try {
      await _tts.stop();
      await _tts.speak(text);
    } catch (_) {}
  }

  static Future<void> stop() async {
    try {
      await _tts.stop();
    } catch (_) {}
  }

  static Future<void> setVolume(double volume) async {
    try {
      await _tts.setVolume(volume);
    } catch (_) {}
  }

  static Future<void> setPitch(double pitch) async {
    try {
      await _tts.setPitch(pitch);
    } catch (_) {}
  }

  static Future<void> setSpeechRate(double speechRate) async {
    try {
      await _tts.setSpeechRate(speechRate);
    } catch (_) {}
  }

  static TtsOption? _voiceFromId(String id) {
    final index = id.indexOf('|');
    if (index < 0) {
      return _findVoice((option) => option.id == id);
    }
    final locale = id.substring(0, index);
    final name = id.substring(index + 1);
    final exact = _findVoice(
      (option) => option.locale == locale && option.name == name,
    );
    return exact ?? _findVoice((option) => option.name == name);
  }

  static TtsOption? _findVoice(bool Function(TtsOption) predicate) {
    for (final option in ttsVoices) {
      if (predicate(option)) {
        return option;
      }
    }
    return null;
  }
}

lib/theme_color.dart

import 'package:flutter/material.dart';

class ThemeColor {
  final int? themeNumber;
  final BuildContext context;

  ThemeColor({this.themeNumber, required this.context});

  Brightness get _effectiveBrightness {
    switch (themeNumber) {
      case 1:
        return Brightness.light;
      case 2:
        return Brightness.dark;
      default:
        return Theme.of(context).brightness;
    }
  }

  bool _isLight() {
    return _effectiveBrightness == Brightness.light;
  }

  //main page
  Color get mainBackColor => _isLight()
      ? Color.fromRGBO(255,248,246, 1.0) : Color.fromRGBO(0, 0, 0, 1.0);
  Color get mainButtonColor => _isLight()
      ? Color.fromRGBO(0, 0, 0, 0.5) : Color.fromRGBO(255,255,255,0.5);
  Color get mainStartBackColor => _isLight()
      ? Color.fromRGBO(255,255,255,0.3) : Color.fromRGBO(255,255,255,0.3);
  Color get mainStartForeColor => _isLight()
      ? Color.fromRGBO(0,0,0,0.6) : Color.fromRGBO(255,255,255,0.8);
  Color get mainCandidateForeColor => _isLight()
      ? Colors.orange[800]! : Colors.orange;
  Color get mainHistoryForeColor => _isLight()
      ? Color.fromRGBO(0,0,0,0.9) : Color.fromRGBO(255,255,255,0.9);
  //setting page
  Color get backColor => _isLight() ? Colors.grey[200]! : Colors.grey[900]!;
  Color get cardColor => _isLight() ? Colors.white : Colors.grey[800]!;
  Color get appBarForegroundColor => _isLight() ? Colors.grey[700]! : Colors.white70;
  Color get dropdownColor => cardColor;

}

lib/theme_mode_number.dart

import 'package:flutter/material.dart';

class ThemeModeNumber {
  ThemeModeNumber._();

  static ThemeMode numberToThemeMode(int value) {
    switch (value) {
      case 1:
        return ThemeMode.light;
      case 2:
        return ThemeMode.dark;
      default:
        return ThemeMode.system;
    }
  }
}

lib/version_state.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-01-27
///
library;

class VersionState {

  static String _version = '';

  //バージョンを記録
  static void versionSave(String str) {
    _version = str;
  }
  //バージョンを返す
  static String versionLoad() {
    return _version;
  }

}