ソースコード source code

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

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

下記コードの最終ビルド日: 2023-11-24

pubspec.yaml

name: luckybox
description: "Lucky box"
# 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.1+13

environment:
  sdk: '>=3.3.0-143.0.dev <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

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.6
  package_info_plus: ^5.0.1
  shared_preferences: ^2.0.17
  flutter_localizations:    #多言語ライブラリの本体    # .arbファイルを更新したら flutter gen-l10n
    sdk: flutter
  intl: ^0.18.1     #多言語やフォーマッタなどの関連ライブラリ
  google_mobile_ads: ^3.1.0
  just_audio: ^0.9.35

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_launcher_icons: ^0.13.1    #flutter pub run flutter_launcher_icons
  flutter_native_splash: ^2.3.6     #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: ^3.0.1

# 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: '#ffcc00'
  image: 'assets/image/splash.png'
  color_dark: '#ffcc00'
  image_dark: 'assets/image/splash.png'
  fullscreen: true
  android_12:
    icon_background_color: '#ffcc00'
    image: 'assets/image/splash.png'
    icon_background_color_dark: '#ffcc00'
    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/image/box/
    - assets/image/ticket/
    - assets/sound/

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

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # 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/custom-fonts/#from-packages

lib/ad_mob.dart

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

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

class AdMob {
  late BannerAd _adMobBanner;
  bool _isAdMob = false;
  AdMob() { //constructor
    String adBannerUnitId = '';
    if (!kIsWeb && Platform.isAndroid) {
      //adBannerUnitId = 'ca-app-pub-3940256099942544/6300978111';  //test
      adBannerUnitId = 'ca-app-pub-0000000000000000/0000000000';
      _isAdMob = true;
    } else if (!kIsWeb && Platform.isIOS) {
      //adBannerUnitId = 'ca-app-pub-3940256099942544/6300978111';  //test
      adBannerUnitId = 'ca-app-pub-0000000000000000/0000000000';
      _isAdMob = true;
    }
    if (_isAdMob) {
      _adMobBanner = BannerAd(
        adUnitId: adBannerUnitId,
        size: AdSize.banner,
        request: const AdRequest(),
        listener: const BannerAdListener(),
      );
    }
  }
  void load() {
    if (_isAdMob) {
      _adMobBanner.load();
    }
  }
  void dispose() {
    if (_isAdMob) {
      _adMobBanner.dispose();
    }
  }
  Widget getAdBannerWidget() {
    if (_isAdMob) {
      return Container(
        alignment: Alignment.center,
        width: _adMobBanner.size.width.toDouble(),
        height: _adMobBanner.size.height.toDouble(),
        child: AdWidget(ad: _adMobBanner),
      );
    } else {
      return Container();
    }
  }
  double getAdBannerHeight() {
    if (_isAdMob) {
      return _adMobBanner.size.height.toDouble();
    } else {
      return 150;  //for web
    }
  }
}

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:luckybox/const_value.dart';

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

  double _soundVolume01 = 0.0;
  double _soundVolume02 = 0.0;

  //constructor
  AudioPlay() {
    constructor();
  }
  void constructor() async {
    for (int i = 0; i < _player01.length; i++) {
      await _player01[i].setVolume(0);
      await _player01[i].setAsset(ConstValue.audioReady);
    }
    for (int i = 0; i < _player02.length; i++) {
      await _player02[i].setVolume(0);
      await _player02[i].setAsset(ConstValue.audioAction);
    }
    playZero();
  }
  void dispose() {
    for (int i = 0; i < _player01.length; i++) {
      _player01[i].dispose();
    }
    for (int i = 0; i < _player02.length; i++) {
      _player02[i].dispose();
    }
  }
  //getter
  double get soundVolume01 {
    return _soundVolume01;
  }
  double get soundVolume02 {
    return _soundVolume02;
  }
  //setter
  set soundVolume01(double vol) {
    _soundVolume01 = vol;
  }
  set soundVolume02(double vol) {
    _soundVolume02 = vol;
  }
  //最初に音が鳴らないのを回避する方法
  void playZero() async {
    AudioPlayer ap = AudioPlayer();
    await ap.setAsset(ConstValue.audioZero);
    await ap.load();
    await ap.play();
  }
  //
  void play01() async {
    if (_soundVolume01 == 0) {
      return;
    }
    _player01Ptr += 1;
    if (_player01Ptr >= _player01.length) {
      _player01Ptr = 0;
    }
    await _player01[_player01Ptr].setVolume(_soundVolume01);
    await _player01[_player01Ptr].pause();
    await _player01[_player01Ptr].seek(const Duration(milliseconds: 100));
    await _player01[_player01Ptr].play();
  }
  //
  void play02() async {
    if (_soundVolume02 == 0) {
      return;
    }
    _player02Ptr += 1;
    if (_player02Ptr >= _player02.length) {
      _player02Ptr = 0;
    }
    await _player02[_player02Ptr].setVolume(_soundVolume02);
    await _player02[_player02Ptr].pause();
    await _player02[_player02Ptr].seek(Duration.zero);
    await _player02[_player02Ptr].play();
  }
}

lib/const_value.dart

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

import 'package:flutter/material.dart';

class ConstValue {
  //pref
  static const String prefLanguageCode = 'languageCode';
  static const String prefPrizeText = 'prizeText';
  static const String prefCountdownTime = 'countdownTime';
  static const String prefSoundReadyVolume = 'soundReadyVolume';
  static const String prefSoundStartVolume = 'soundStartVolume';
  //
  static const String prizeTextDefault = '1:car\n3:bicycle\n5:bag\n100:lose';
  //color
  static const Color colorHeader = Color.fromRGBO(255,0,0,0.1);
  static const Color colorHeaderOn = Color.fromRGBO(255,255,255,0.3);
  static const Color colorNote = Color.fromRGBO(255,0,0,0.4);
  static const Color colorBack = Color.fromRGBO(255,204,0,1);
  static const Color colorSettingHeader = Color.fromRGBO(255,204,0,1);
  static const Color colorUiActiveColor = Color.fromRGBO(255,204,0,1);
  static const Color colorUiInactiveColor = Colors.black26;
  //sound
  static const String audioZero = 'assets/sound/zero.wav';    //無音1秒
  static const String audioReady = 'assets/sound/switch.wav';
  static const String audioAction = 'assets/sound/slide.wav';
  //image
  static const String imageBack = 'assets/image/back.png';
  static const List<String> imageBoxes = [
    'assets/image/box/box001.webp',
    'assets/image/box/box002.webp',
    'assets/image/box/box003.webp',
    'assets/image/box/box004.webp',
    'assets/image/box/box005.webp',
    'assets/image/box/box006.webp',
    'assets/image/box/box007.webp',
    'assets/image/box/box008.webp',
    'assets/image/box/box009.webp',
    'assets/image/box/box010.webp',
    'assets/image/box/box011.webp',
    'assets/image/box/box012.webp',
    'assets/image/box/box013.webp',
    'assets/image/box/box014.webp',
    'assets/image/box/box015.webp',
    'assets/image/box/box016.webp',
    'assets/image/box/box017.webp',
    'assets/image/box/box018.webp',
    'assets/image/box/box019.webp',
    'assets/image/box/box020.webp',
    'assets/image/box/box021.webp',
    'assets/image/box/box022.webp',
    'assets/image/box/box023.webp',
    'assets/image/box/box024.webp',
    'assets/image/box/box025.webp',
    'assets/image/box/box026.webp',
    'assets/image/box/box027.webp',
    'assets/image/box/box028.webp',
    'assets/image/box/box029.webp',
    'assets/image/box/box030.webp',
    'assets/image/box/box031.webp',
    'assets/image/box/box032.webp',
    'assets/image/box/box033.webp',
    'assets/image/box/box034.webp',
    'assets/image/box/box035.webp',
    'assets/image/box/box036.webp',
    'assets/image/box/box037.webp',
    'assets/image/box/box038.webp',
    'assets/image/box/box039.webp',
    'assets/image/box/box040.webp',
    'assets/image/box/box041.webp',
    'assets/image/box/box042.webp',
    'assets/image/box/box043.webp',
    'assets/image/box/box044.webp',
    'assets/image/box/box045.webp',
    'assets/image/box/box046.webp',
    'assets/image/box/box047.webp',
    'assets/image/box/box048.webp',
    'assets/image/box/box049.webp',
    'assets/image/box/box050.webp',
    'assets/image/box/box051.webp',
    'assets/image/box/box052.webp',
    'assets/image/box/box053.webp',
    'assets/image/box/box054.webp',
    'assets/image/box/box055.webp',
    'assets/image/box/box056.webp',
    'assets/image/box/box057.webp',
    'assets/image/box/box058.webp',
    'assets/image/box/box059.webp',
    'assets/image/box/box060.webp',
    'assets/image/box/box061.webp',
    'assets/image/box/box062.webp',
    'assets/image/box/box063.webp',
    'assets/image/box/box064.webp',
    'assets/image/box/box065.webp',
    'assets/image/box/box066.webp',
    'assets/image/box/box067.webp',
    'assets/image/box/box068.webp',
    'assets/image/box/box069.webp',
    'assets/image/box/box070.webp',
    'assets/image/box/box071.webp',
    'assets/image/box/box072.webp',
    'assets/image/box/box073.webp',
    'assets/image/box/box074.webp',
    'assets/image/box/box075.webp',
    'assets/image/box/box076.webp',
    'assets/image/box/box077.webp',
    'assets/image/box/box078.webp',
    'assets/image/box/box079.webp',
    'assets/image/box/box080.webp',
    'assets/image/box/box081.webp',
    'assets/image/box/box082.webp',
    'assets/image/box/box083.webp',
    'assets/image/box/box084.webp',
    'assets/image/box/box085.webp',
    'assets/image/box/box086.webp',
    'assets/image/box/box087.webp',
    'assets/image/box/box088.webp',
    'assets/image/box/box089.webp',
    'assets/image/box/box090.webp',
    'assets/image/box/box091.webp',
    'assets/image/box/box092.webp',
    'assets/image/box/box093.webp',
    'assets/image/box/box094.webp',
    'assets/image/box/box095.webp',
    'assets/image/box/box096.webp',
    'assets/image/box/box097.webp',
    'assets/image/box/box098.webp',
    'assets/image/box/box099.webp',
    'assets/image/box/box100.webp',
    'assets/image/box/box101.webp',
    'assets/image/box/box102.webp',
    'assets/image/box/box103.webp',
    'assets/image/box/box104.webp',
    'assets/image/box/box105.webp',
    'assets/image/box/box106.webp',
    'assets/image/box/box107.webp',
    'assets/image/box/box108.webp',
    'assets/image/box/box109.webp',
    'assets/image/box/box110.webp',
    'assets/image/box/box111.webp',
    'assets/image/box/box112.webp',
    'assets/image/box/box113.webp',
    'assets/image/box/box114.webp',
    'assets/image/box/box115.webp',
    'assets/image/box/box116.webp',
    'assets/image/box/box117.webp',
    'assets/image/box/box118.webp',
    'assets/image/box/box119.webp',
    'assets/image/box/box120.webp',
  ];
  static const List<String> imageTickets = [
    /*
    'assets/image/ticket/ticket070.webp',
    'assets/image/ticket/ticket071.webp',
    'assets/image/ticket/ticket072.webp',
    'assets/image/ticket/ticket073.webp',
    'assets/image/ticket/ticket074.webp',
    'assets/image/ticket/ticket075.webp',
    'assets/image/ticket/ticket076.webp',
    'assets/image/ticket/ticket077.webp',
    'assets/image/ticket/ticket078.webp',
    'assets/image/ticket/ticket079.webp',
     */
    'assets/image/ticket/ticket080.webp',
    'assets/image/ticket/ticket081.webp',
    'assets/image/ticket/ticket082.webp',
    'assets/image/ticket/ticket083.webp',
    'assets/image/ticket/ticket084.webp',
    'assets/image/ticket/ticket085.webp',
    'assets/image/ticket/ticket086.webp',
    'assets/image/ticket/ticket087.webp',
    'assets/image/ticket/ticket088.webp',
    'assets/image/ticket/ticket089.webp',
    /*
    'assets/image/ticket/ticket090.webp',
    'assets/image/ticket/ticket091.webp',
    'assets/image/ticket/ticket092.webp',
    'assets/image/ticket/ticket093.webp',
    'assets/image/ticket/ticket094.webp',
    'assets/image/ticket/ticket095.webp',
    'assets/image/ticket/ticket096.webp',
    'assets/image/ticket/ticket097.webp',
    'assets/image/ticket/ticket098.webp',
    'assets/image/ticket/ticket099.webp',
    'assets/image/ticket/ticket100.webp',
    'assets/image/ticket/ticket101.webp',
    'assets/image/ticket/ticket102.webp',
    'assets/image/ticket/ticket103.webp',
    'assets/image/ticket/ticket104.webp',
    'assets/image/ticket/ticket105.webp',
    'assets/image/ticket/ticket106.webp',
    'assets/image/ticket/ticket107.webp',
    'assets/image/ticket/ticket108.webp',
    'assets/image/ticket/ticket109.webp',
    'assets/image/ticket/ticket110.webp',
    'assets/image/ticket/ticket111.webp',
    'assets/image/ticket/ticket112.webp',
    'assets/image/ticket/ticket113.webp',
    'assets/image/ticket/ticket114.webp',
    'assets/image/ticket/ticket115.webp',
    'assets/image/ticket/ticket116.webp',

     */
  ];
  static const List<String> imageNumbers = [
    'assets/image/number_null.webp',
    'assets/image/number1.webp',
    'assets/image/number2.webp',
    'assets/image/number3.webp',
    'assets/image/number4.webp',
    'assets/image/number5.webp',
    'assets/image/number6.webp',
    'assets/image/number7.webp',
    'assets/image/number8.webp',
    'assets/image/number9.webp',
  ];
  //string
  static const Map<String,String> languageCode = {
    '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': '中文',
  };

}

lib/empty.dart

lib/language_state.dart

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

import 'package:luckybox/preferences.dart';

class LanguageState {

  static String _languageCode = 'en';

  //言語コードを記録
  static Future<void> setLanguageCode(String str) async {
    _languageCode = str;
    await Preferences.setLanguageCode(_languageCode);
  }
  //言語コードを返す
  static Future<String> getLanguageCode() async {
    _languageCode = await Preferences.getLanguageCode() ?? 'en';
    return _languageCode;
  }

}

lib/main.dart

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

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart' if (dart.library.html) 'empty.dart';
import 'package:flutter/foundation.dart' show kIsWeb;

//自身で作成したclassを読み込む
import 'package:luckybox/const_value.dart';
import 'package:luckybox/language_state.dart';
import 'package:luckybox/version_state.dart';
import 'package:luckybox/setting.dart';
import 'package:luckybox/ad_mob.dart';
import 'package:luckybox/page_state.dart';
import 'package:luckybox/preferences.dart';
import 'package:luckybox/audio_play.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatefulWidget {    //statefulに変更して言語変更に対応
  const MainApp({super.key});
  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  Locale localeLanguage = const Locale('en');
  @override
  Widget build(BuildContext context) {
    if (kIsWeb == false) {
      MobileAds.instance.initialize();
    }
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      localizationsDelegates: AppLocalizations.localizationsDelegates,   //多言語化
      supportedLocales: AppLocalizations.supportedLocales,  //自動で言語リストを生成
      locale: localeLanguage,
      home: const MainHomePage(),
    );
  }
}

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

class _MainHomePageState extends State<MainHomePage> with SingleTickerProviderStateMixin {
  final GlobalKey _aspectRatioKey = GlobalKey();  //_textAreaのサイズ取得用
  final AdMob _adMob = AdMob(); //広告表示
  final AudioPlay _audioPlay = AudioPlay(); //効果音
  bool _busyFlag = false; //動作中
  int _tickNumber = 0;  //0~119で画像と結果表示を切り替え
  String _ticketText = ''; //結果 e.g. 'はずれ'
  double _ticketTextSize = 15.0;  //結果の文字サイズ。_tickNumberで変化
  late Timer _timer;  //カウントダウンと画像切り替えで使用
  //countdown
  int _countdownSubtraction = 0;  //_countdownTimeが代入されて実際にカウントダウンされる
  String _imageCountdownNumber = ConstValue.imageNumbers[0];  //カウントダウン画像
  double _countdownScale = 3;   //カウントダウン画像の拡大率
  double _countdownOpacity = 0; //カウントダウン画像の非透明度
  int _timerCount = 30; //Timerで処理される1秒間の数
  //

  //アプリのバージョン取得
  void _getVersion() async {
    PackageInfo packageInfo = await PackageInfo.fromPlatform();
    setState(() {
      VersionState.versionSave(packageInfo.version);
    });
  }
  //言語準備
  void _getCurrentLocale() async {
    Locale locale = Locale(await LanguageState.getLanguageCode());
    if (mounted) {  //Widgetが存在する。Widgetが存在しない時の実行によるエラーを回避する為。
      context.findAncestorStateOfType<_MainAppState>()!
        ..localeLanguage = locale
        ..setState(() {});
    }
  }
  //ページ起動開始時に一度だけ呼ばれる
  @override
  void initState() {
    super.initState();
    _getVersion();
    _getCurrentLocale();
    LanguageState.getLanguageCode();
    _adMob.load();
    _audioPlay.playZero();
    (() async {
      await Preferences.initial();
      _audioPlay.soundVolume01 = Preferences.soundReadyVolume;
      _audioPlay.soundVolume02 = Preferences.soundStartVolume;
      setState(() {});
    })();
  }
  //ページ終了時に一度だけ呼ばれる
  @override
  void dispose() {
    PageState.setCurrentPage('');
    _adMob.dispose();
    _timer.cancel();
    super.dispose();
  }
  //画面全体
  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: _decoration(),
      child: GestureDetector(
        onTap: () {
          _onClickStart();
        },
        child: Scaffold(
          backgroundColor: Colors.transparent,
          appBar: AppBar(
            backgroundColor: _busyFlag ? ConstValue.colorHeaderOn : ConstValue.colorHeader,
            //タイトル表示
            title: const Text('Lucky Box',
              style: TextStyle(
                color: Colors.white,
                fontSize: 15.0,
              )
            ),
            //設定ボタン
            actions: <Widget>[
              Opacity(
                opacity: _busyFlag ? 0.1 : 1,
                child: TextButton(
                  onPressed: () async {
                    if (_busyFlag) {
                      return;
                    }
                    bool? ret = await Navigator.of(context).push(
                      MaterialPageRoute<bool>(builder:(context) => const SettingPage()),
                    );
                    //awaitで呼び出しているので、settingから戻ったら以下が実行される。
                    if (ret!) { //設定で適用だった場合
                      _getCurrentLocale();
                      _audioPlay.soundVolume01 = Preferences.soundReadyVolume;
                      _audioPlay.soundVolume02 = Preferences.soundStartVolume;
                      setState(() {});
                    }
                  },
                  child: Text(
                    AppLocalizations.of(context)!.setting,
                    style: const TextStyle(
                      color: Colors.white,
                    )
                  )
                )
              )
            ]
          ),
          body: SafeArea(
            child: Column(children:[
              Expanded(
                child: Stack(children:[
                  Center(
                    child: _boxArea(),
                  ),
                  Center(
                    child: _preTextArea(),
                  ),
                  Center(
                    child: _textArea(),
                  ),
                  Center(
                    child: _ticketArea(),
                  ),
                  Center(
                    child: Opacity(
                      opacity: _countdownOpacity,
                      child: Transform.scale(
                        scale: _countdownScale,
                        child: Image.asset(
                          _imageCountdownNumber,
                        ),
                      )
                    )
                  ),
                  SizedBox(
                    width: double.infinity,
                    child: Text(AppLocalizations.of(context)!.start,
                      textAlign: TextAlign.center,
                      style: const TextStyle(
                        color: ConstValue.colorNote,
                      )
                    )
                  )
                ])
              ),
              //広告
              Padding(
                padding: const EdgeInsets.only(top: 10, left: 0, right: 0, bottom: 0),
                child: SizedBox(
                  width: double.infinity,
                  child: _adMob.getAdBannerWidget(),
                )
              )
            ])
          )
        )
      )
    );
  }
  Decoration _decoration() {
    return const BoxDecoration(
      //colorは起動時に真黒な画面にならないようにする為
      color: ConstValue.colorSettingHeader,
      image: DecorationImage(
        image: AssetImage(ConstValue.imageBack),
        fit: BoxFit.cover,
      ),
    );
  }
  //カウントダウンタイマー
  void _timerStart() {
    _timer = Timer.periodic(const Duration(milliseconds: (1000 ~/ 30)), (timer) {
      setState(() {
        _countdown();
      });
    });
  }
  //START
  void _onClickStart() {
    if (_busyFlag) {
      return;
    }
    _busyFlag = true;
    _countdownSubtraction = Preferences.countdownTime;
    if (_countdownSubtraction == 0) { //カウントダウンしない場合
      _tickAction();
    } else {
      _audioPlay.soundVolume01 = Preferences.soundReadyVolume;
      _audioPlay.play01();
      _tickNumber = 0;
      _ticketTextSize = 0;
      _timerStart();
    }
  }
  //0~119まで変化。画像と結果表示を切り替え
  void _tickAction() {
    _audioPlay.soundVolume02 = Preferences.soundStartVolume;
    _audioPlay.play02();
    _tickNumber = 0;
    _ticketText = Preferences.nextPrizeText();
    _timer = Timer.periodic(const Duration(milliseconds: (1000 ~/ 30)), (timer) {
      setState(() {
        _tickNumber += 1;
        if (_tickNumber < 80) {
          _ticketTextSize = 0;
        } else {
          _ticketTextSize = (_tickNumber - 80) / 2 + 5.0;
        }
        if (_tickNumber >= 119) {
          _timer.cancel();
          _busyFlag = false;
        }
      });
    });
  }
  //Timerで定期実行
  void _countdown() {
    //カウントダウン終了時
    if (_countdownSubtraction == 0) {
      return;
    }
    //数字画像を切り替え
    if (_timerCount == 30) {
      _imageCountdownNumber = ConstValue.imageNumbers[_countdownSubtraction];
    }
    _timerCount -= 1;
    if (_timerCount <= 0) {
      _timerCount = 30;
      _countdownSubtraction -= 1;
      if (_countdownSubtraction == 0) {
        _imageCountdownNumber = ConstValue.imageNumbers[0];
        _timer.cancel();
        _tickAction();
      }
    }
    _countdownScale = 1 + (0.1 * (_timerCount / 30));
    if (_timerCount >= 20) {
      _countdownOpacity = (30 - _timerCount) / 10;
    } else if (_timerCount <= 5) {
      _countdownOpacity = _timerCount / 5;
    } else {
      _countdownOpacity = 1;
    }
  }
  //box画像
  Widget _boxArea() {
    List<Widget> widgets = [];
    for (int i = 0; i < ConstValue.imageBoxes.length; i++) {
      widgets.add(
        Opacity(
          opacity: _tickNumber == i ? 1 : 0,
          child: Image.asset(ConstValue.imageBoxes[i]),
        ),
      );
    }
    return Stack(children:widgets);
  }
  //ticket上側を重ねて表示
  Widget _ticketArea() {
    List<Widget> widgets = [];
    for (int i = 0; i < ConstValue.imageTickets.length; i++) {
      widgets.add(
        Opacity(
          opacity: _tickNumber == (i + 80) ? 1 : 0,
          child: Image.asset(ConstValue.imageTickets[i]),
        ),
      );
    }
    return Stack(children:widgets);
  }
  //_textAreaのサイズ取得用
  Widget _preTextArea() {
    return AspectRatio(
      key: _aspectRatioKey,
      aspectRatio: 1,
    );
  }
  //結果文字表示
  Widget _textArea() {
    late Size size;
    try {
      RenderBox renderBox = _aspectRatioKey.currentContext?.findRenderObject() as RenderBox;
      size = renderBox.size;
    } catch (_) {
      return Container();
    }
    return AspectRatio(
        aspectRatio: 1,
        child: Container(
            alignment: Alignment.center,
            padding: EdgeInsets.fromLTRB(0, size.width * 0.25, size.width * 0.19, 0),
            child: Text(_ticketText,
                style: TextStyle(
                  fontSize: _ticketTextSize,
                )
            )
        )
    );
  }
}

lib/page_state.dart

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

//現在のページを記録。initStateでタイミングが合わない時にbuild内で一度だけ実行させるために使用。
class PageState {

  static String _currentPage = '';

  static void setCurrentPage(String str) {
    _currentPage = str;
  }

  static String getCurrentPage() {
    return _currentPage;
  }

}

lib/preferences.dart

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

import 'package:shared_preferences/shared_preferences.dart';

import 'package:luckybox/const_value.dart';

//デバイスに情報を保存
class Preferences {

  static bool ready = false;
  //この値は常に最新にしておく
  static String _languageCode = '';
  static String _prizeText = '';
  static List<Map<String,dynamic>> _prizeList = [];
  static int _countdownTime = 0;
  static double _soundReadyVolume = 0.5;
  static double _soundStartVolume = 0.5;

  static String get languageCode {
    return _languageCode;
  }
  static String get prizeText {
    return _prizeText;
  }
  static List<Map<String,dynamic>> get prizeList {
    return _prizeList;
  }
  static int get countdownTime {
    return _countdownTime;
  }
  static double get soundReadyVolume {
    return _soundReadyVolume;
  }
  static double get soundStartVolume {
    return _soundStartVolume;
  }

  static Future<void> initial() async {
    _languageCode = await getLanguageCode();
    _prizeText = await getPrizeText();
    _prizeList = await getPrizeList();
    if (_prizeText == '') {
      await setPrizeText(''); //空文字で初期値をセット
    }
    _countdownTime = await getCountdownTime();
    _soundReadyVolume = await getSoundReadyVolume();
    _soundStartVolume = await getSoundStartVolume();
    ready = true;
  }

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

  //言語コード
  static Future<void> setLanguageCode(String str) async {
    _languageCode = str;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString(ConstValue.prefLanguageCode, str);
  }
  static Future<String> getLanguageCode() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final String str = prefs.getString(ConstValue.prefLanguageCode) ?? 'en';
    return str;
  }

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

  //賞
  static Future<void> setPrizeText(String str) async {
    if (str == '') {
      str = ConstValue.prizeTextDefault;
    }
    _prizeText = _prizeFormat(str);
    _prizeList = _makePrizeList(_prizeText);
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString(ConstValue.prefPrizeText, _prizeText);
  }
  static Future<void> setPrizeTextDefault() async {
    setPrizeText(ConstValue.prizeTextDefault);
  }
  static Future<String> getPrizeText() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final String str = prefs.getString(ConstValue.prefPrizeText) ?? '';
    return str;
  }
  //賞をListで返す
  static Future<List<Map<String,dynamic>>> getPrizeList() async {
    return _makePrizeList(await getPrizeText());
  }
  //賞をListにする
  static List<Map<String,dynamic>> _makePrizeList(String str) {
    List<Map<String,dynamic>> mapList = [];
    if (str == '') {
      return mapList;
    }
    final List<String> lines = str.replaceAll('\r','').split('\n');
    for (int i = 0; i < lines.length; i++) {
      final List<String> ary = lines[i].split(':');
      final int number = _parseStrToNumber(ary[0]);
      final Map<String,dynamic> mapOne = {'number':number,'prize':ary[1]};
      mapList.add(mapOne);
    }
    return mapList;
  }
  //賞を1個取り出す
  static String nextPrizeText() {
    final List<int> candidates = [];
    int row = 0;
    for (Map<String,dynamic> prize in _prizeList) {
      for (int i = 0; i < prize['number']; i++) {
        candidates.add(row);
      }
      row += 1;
    }
    if (candidates.isEmpty) {
      return '* END *';
    }
    candidates.shuffle();
    final int choice = candidates.first;
    String result = '';
    for (int i = 0; i < _prizeList.length; i++) {
      if (i == choice) {
        _prizeList[i]['number'] -= 1;
        result = _prizeList[i]['prize'];
        break;
      }
    }
    List<String> prizeStringList = [];
    for (Map<String,dynamic> prize in _prizeList) {
      prizeStringList.add('${prize['number']}:${prize['prize']}');
    }
    setPrizeText(prizeStringList.join('\n'));
    return result;
  }

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

  //カウントダウン時間
  static Future<void> setCountdownTime(int num) async {
    _countdownTime = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setInt(ConstValue.prefCountdownTime, num);
  }
  static Future<int> getCountdownTime() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final int num = prefs.getInt(ConstValue.prefCountdownTime) ?? 0;
    return num;
  }

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

  //効果音音量
  static Future<void> setSoundReadyVolume(double num) async {
    _soundReadyVolume = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setDouble(ConstValue.prefSoundReadyVolume, num);
  }
  static Future<double> getSoundReadyVolume() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final double num = prefs.getDouble(ConstValue.prefSoundReadyVolume) ?? 0.5;
    return num;
  }

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

  //効果音音量
  static Future<void> setSoundStartVolume(double num) async {
    _soundStartVolume = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setDouble(ConstValue.prefSoundStartVolume, num);
  }
  static Future<double> getSoundStartVolume() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final double num = prefs.getDouble(ConstValue.prefSoundStartVolume) ?? 0.5;
    return num;
  }

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

  //文字列を数値に変換
  static int _parseStrToNumber(String numString) {
    if (_isStringToIntParsable(numString)) {
      return int.parse(numString);
    }
    return 0;
  }
  //String を int に変換できるか
  static bool _isStringToIntParsable(String str) {
    try {
      int.parse(str);
      return true;
    } catch (e) {
      return false;
    }
  }
  //賞を整える。ユーザーの入力なので適宜調整する
  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]'), '');
      prizes.add('${ary[0]}:${ary[1]}');
    }
    return prizes.join('\n');
  }

}

lib/setting.dart

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

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

import 'package:luckybox/const_value.dart';
import 'package:luckybox/preferences.dart';
import 'package:luckybox/language_state.dart';
import 'package:luckybox/version_state.dart';
import 'package:luckybox/ad_mob.dart';
import 'package:luckybox/page_state.dart';

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

  @override
  State<SettingPage> createState() => _SettingPageState();
}

class _SettingPageState extends State<SettingPage> {
  final AdMob _adMob = AdMob(); //広告
  //これら変数はUIへの表示や入力の為に一時的に使用される。
  String _languageKey = ''; //言語コード 'en'
  String _languageValue = '';
  final TextEditingController _controllerPrizeText = TextEditingController();
  bool _prizeInitialFlag = false;
  int _countdownTime = 0;
  double _soundReadyVolume = 0.5;
  double _soundStartVolume = 0.5;

  //ページ起動時に一度だけ実行される
  @override
  void initState() {
    super.initState();
    _adMob.load();
  }
  //ページ終了時に一度だけ実行される
  @override
  void dispose() {
    PageState.setCurrentPage('');
    _adMob.dispose();
    _controllerPrizeText.dispose();
    super.dispose();
  }
  //ページ描画
  @override
  Widget build(BuildContext context) {
    //このページが開いたときに一度だけ実行される処理を記述。initStateではタイミングが合わない為。
    if (PageState.getCurrentPage() != 'setting') {
      PageState.setCurrentPage('setting');
      (() async {
        _languageKey = await LanguageState.getLanguageCode();
        _languageValue = ConstValue.languageCode[_languageKey] ?? '';
        await Preferences.initial();
        _controllerPrizeText.text = Preferences.prizeText;
        _countdownTime = Preferences.countdownTime;
        _soundReadyVolume = Preferences.soundReadyVolume;
        _soundStartVolume = Preferences.soundStartVolume;
        setState((){});
      })();
    }
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        elevation: 0,
        //設定キャンセルボタン
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: () {
            Navigator.of(context).pop(false); //falseを返す
          },
        ),
        title: Text(AppLocalizations.of(context)!.setting),
        foregroundColor: const Color.fromRGBO(255,255,255,1),
        backgroundColor: ConstValue.colorSettingHeader,
        actions: [
          //設定OKボタン
          IconButton(
            icon: const Icon(Icons.check),
            onPressed: () async {
              await LanguageState.setLanguageCode(_languageKey);
              if (_prizeInitialFlag) {
                await Preferences.setPrizeTextDefault();
              } else {
                await Preferences.setPrizeText(_controllerPrizeText.text);
              }
              await Preferences.setCountdownTime(_countdownTime);
              await Preferences.setSoundReadyVolume(_soundReadyVolume);
              await Preferences.setSoundStartVolume(_soundStartVolume);
              if (!mounted) {
                return;
              }
              Navigator.of(context).pop(true);  //trueを返す
            },
          ),
        ],
      ),
      body: Column(children:[
        Expanded(
          child: GestureDetector(
            onTap: () => FocusScope.of(context).unfocus(),  //背景タップでキーボードを仕舞う
            child: SingleChildScrollView(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(children: [
                  Padding(
                    padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 0),
                    child: Row(children:<Widget>[
                      Expanded(
                        child: Text(AppLocalizations.of(context)!.prize,style: const TextStyle(fontSize: 16)),
                      ),
                      Text(AppLocalizations.of(context)!.initial),
                      Switch(
                        value: _prizeInitialFlag,
                        onChanged: (bool value) {
                          setState(() {
                            _prizeInitialFlag = value;
                          });
                        },
                        activeColor: Colors.red,
                        inactiveThumbColor: ConstValue.colorUiInactiveColor,
                      ),
                    ]),
                  ),
                  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(),
                      ),
                    ),
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 18, left: 16, right: 16, bottom: 0),
                    child: Row(children: [
                      Text(AppLocalizations.of(context)!.countdownTime,style: const TextStyle(fontSize: 16)),
                      const Spacer(),
                    ])
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 6),
                    child: Row(children: <Widget>[
                      Text(_countdownTime.toString()),
                      Expanded(
                        child: Slider(
                          value: _countdownTime.toDouble(),
                          min: 0,
                          max: 9,
                          divisions: 9,
                          onChanged: (double value) {
                            setState(() {
                              _countdownTime = value.toInt();
                            });
                          },
                          activeColor: ConstValue.colorUiActiveColor,
                          inactiveColor: ConstValue.colorUiInactiveColor,
                        )
                      )
                    ])
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 18, left: 16, right: 16, bottom: 0),
                    child: Row(children: [
                      Text(AppLocalizations.of(context)!.soundReadyVolume,style: const TextStyle(fontSize: 16)),
                      const Spacer(),
                    ])
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 6),
                    child: Row(children: <Widget>[
                      Text(_soundReadyVolume.toString()),
                      Expanded(
                        child: Slider(
                          value: _soundReadyVolume,
                          min: 0.0,
                          max: 1.0,
                          divisions: 10,
                          onChanged: (double value) {
                            setState(() {
                              _soundReadyVolume = value;
                            });
                          },
                          activeColor: ConstValue.colorUiActiveColor,
                          inactiveColor: ConstValue.colorUiInactiveColor,
                        )
                      )
                    ])
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 18, left: 16, right: 16, bottom: 0),
                    child: Row(children: [
                      Text(AppLocalizations.of(context)!.soundStartVolume,style: const TextStyle(fontSize: 16)),
                      const Spacer(),
                    ])
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 6),
                    child: Row(children: <Widget>[
                      Text(_soundStartVolume.toString()),
                      Expanded(
                        child: Slider(
                          value: _soundStartVolume,
                          min: 0.0,
                          max: 1.0,
                          divisions: 10,
                          onChanged: (double value) {
                            setState(() {
                              _soundStartVolume = value;
                            });
                          },
                          activeColor: ConstValue.colorUiActiveColor,
                          inactiveColor: ConstValue.colorUiInactiveColor,
                        )
                      )
                    ])
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 18, left: 0, right: 0, bottom: 0),
                    child: Row(children:[
                      const SizedBox(width:16),
                      Text(AppLocalizations.of(context)!.language,
                        style: const TextStyle(
                          fontSize: 16,
                        )
                      ),
                      const Spacer(),
                    ])
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 12, left: 0, right: 0, bottom: 18),
                    child: Table(
                      children: <TableRow>[
                        TableRow(children: <Widget>[
                          _languageTableCell(0),
                          _languageTableCell(1),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(2),
                          _languageTableCell(3),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(4),
                          _languageTableCell(5),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(6),
                          _languageTableCell(7),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(8),
                          _languageTableCell(9),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(10),
                          _languageTableCell(11),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(12),
                          _languageTableCell(13),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(14),
                          _languageTableCell(15),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(16),
                          _languageTableCell(17),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(18),
                          _languageTableCell(19),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(20),
                          _languageTableCell(21),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(22),
                          _languageTableCell(23),
                        ]),
                      ]
                    ),
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 24, left: 0, right: 0, bottom: 24),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children:[
                        Text(AppLocalizations.of(context)!.usage1),
                        const SizedBox(height:15),
                        Text(AppLocalizations.of(context)!.usage2),
                        const SizedBox(height:15),
                        Text(AppLocalizations.of(context)!.usage3),
                        const SizedBox(height:15),
                        Text(AppLocalizations.of(context)!.usage4),
                      ]
                    ),
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 24, left: 0, right: 0, bottom: 36),
                    child: SizedBox(
                      child: Text('version  ${VersionState.versionLoad()}',
                        style: const TextStyle(
                          fontSize: 10,
                        ),
                      ),
                    ),
                  ),
                ]),
              ),
            ),
          ),
        ),
        Padding(
          padding: const EdgeInsets.only(top: 10, left: 0, right: 0, bottom: 0),
          child: SizedBox(
            width: double.infinity,
            child: _adMob.getAdBannerWidget(),
          ),
        ),
      ]),
    );
  }
  //UIの仕切り用ボーダーライン
  Widget _border() {
    return Container(
      decoration: BoxDecoration(
        border: Border(
          top: BorderSide(
            color: Colors.grey.shade300,
            width: 1,
          ),
        ),
      ),
    );
  }
  //言語一覧表示
  TableCell _languageTableCell(int index) {
    return TableCell(
      child: RadioListTile(
        visualDensity: const VisualDensity(horizontal: VisualDensity.minimumDensity,vertical: VisualDensity.minimumDensity),
        contentPadding: EdgeInsets.zero,
        title: Text(ConstValue.languageCode.values.elementAt(index)),
        value: ConstValue.languageCode.values.elementAt(index),
        groupValue: _languageValue,
        onChanged: (String? value) {
          setState(() {
            _languageValue = value ?? '';
            _languageKey = ConstValue.languageCode.keys.elementAt(index);
          });
        },
        activeColor: ConstValue.colorUiActiveColor,
      ),
    );
  }

}

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;
  }

}

lib/l10n/app_bg.arb

{
	"@@locale":"bg",
	"@locale": {
		"description": "ブルガリア"
	},
	"setting": "Настройка",
	"start": "Започнете с докосване на екрана",
	"prize": "награда",
	"initial": "Първоначална стойност",
	"countdownTime": "Време за обратно броене",
	"soundReadyVolume": "Сила на звука на звуковия ефект: Звук за обратно броене",
	"soundStartVolume": "Сила на звука на звуковия ефект: Стартирайте звука",
	"language": "език",
	"usage1": "Теглене на томбола.",
	"usage2": "Тегленето на томбола намалява броя на партидите в кутията. Наградите се въвеждат съгласно следните правила",
	"usage3": "Количество:Име\nКоличество:Име\nКоличество:Име",
	"usage4": "напр.\n1:кола\n3:велосипед\n5:чанта\n100:губи",

	"dummy": "dummy"
}

lib/l10n/app_cs.arb

{
	"@@locale":"cs",
	"@locale": {
		"description": "チェコ"
	},
	"setting": "Nastavení",
	"start": "Začněte klepnutím na obrazovku",
	"prize": "Cena",
	"initial": "Počáteční hodnota",
	"countdownTime": "Čas odpočítávání",
	"soundReadyVolume": "Hlasitost zvukového efektu: Zvuk odpočítávání",
	"soundStartVolume": "Hlasitost zvukového efektu: Spustit zvuk",
	"language": "Jazyk",
	"usage1": "Losování tomboly.",
	"usage2": "Losováním tomboly se snižuje počet losů v krabici. Ceny se vkládají podle následujících pravidel",
	"usage3": "Množství:Název\nMnožství:Název\nMnožství:Název",
	"usage4": "např.\n1:auto\n3:jízdní kolo\n5:sáček\n100:prohrát",

	"dummy": "dummy"
}

lib/l10n/app_da.arb

{
	"@@locale":"da",
	"@locale": {
		"description": "デンマーク"
	},
	"setting": "Indstilling",
	"start": "Start med at trykke på skærmen",
	"prize": "Præmie",
	"initial": "Startværdi",
	"countdownTime": "Nedtællingstid",
	"soundReadyVolume": "Lydeffekt lydstyrke: Nedtællingslyd",
	"soundStartVolume": "Lydeffekt lydstyrke: Start lyd",
	"language": "Sprog",
	"usage1": "Lodtrækning.",
	"usage2": "Udtrækning af en lodtrækning reducerer antallet af lod i kassen. Præmier tildeles efter følgende regler",
	"usage3": "Mængde:Navn\nMængde:Navn\nMængde:Navn",
	"usage4": "f.eks.\n1:bil\n3:cykel\n5:taske\n100:tab",

	"dummy": "dummy"
}

lib/l10n/app_de.arb

{
	"@@locale":"de",
	"@locale": {
		"description": "ドイツ"
	},
	"setting": "Einstellung",
	"start": "Tippen Sie zunächst auf den Bildschirm",
	"prize": "Preis",
	"initial": "Ursprünglicher Wert",
	"countdownTime": "Countdown-Zeit",
	"soundReadyVolume": "Lautstärke des Soundeffekts: Countdown-Sound",
	"soundStartVolume": "Lautstärke des Soundeffekts: Ton starten",
	"language": "Sprache",
	"usage1": "Verlosung.",
	"usage2": "Bei einer Verlosung verringert sich die Anzahl der Lose in der Box. Die Gewinne werden nach den folgenden Regeln vergeben",
	"usage3": "Menge:Name\nMenge:Name\nMenge:Name",
	"usage4": "z.B.\n1:Auto\n3:Fahrrad\n5:Tasche\n100:verlieren",

	"dummy": "dummy"
}

lib/l10n/app_el.arb

{
	"@@locale":"el",
	"@locale": {
		"description": "ギリシャ"
	},
	"setting": "Σύνθεση",
	"start": "Ξεκινήστε πατώντας στην οθόνη",
	"prize": "Βραβείο",
	"initial": "Αρχική τιμή",
	"countdownTime": "Χρόνος αντίστροφης μέτρησης",
	"soundReadyVolume": "Ένταση ήχου εφέ: Ήχος αντίστροφης μέτρησης",
	"soundStartVolume": "Ένταση ήχου εφέ: Έναρξη ήχου",
	"language": "Γλώσσα",
	"usage1": "Κλήρωση κλήρωσης.",
	"usage2": "Η κλήρωση μιας κλήρωσης μειώνει τον αριθμό των παρτίδων στο κουτί. Τα βραβεία εισάγονται σύμφωνα με τους παρακάτω κανόνες",
	"usage3": "Ποσότητα:Όνομα\nΠοσότητα:Όνομα\nΠοσότητα:Όνομα",
	"usage4": "π.χ.\n1:αυτοκίνητο\n3:ποδήλατο\n5:τσάντα\n100:χάνω",

	"dummy": "dummy"
}

lib/l10n/app_en.arb

{
	"@@locale":"en",
	"@locale": {
		"description": "英語"
	},
	"setting": "Setting",
	"start": "Start by tapping on the screen",
	"prize": "Prize",
	"initial":"Initial value",
	"countdownTime": "Countdown time",
	"soundReadyVolume": "Sound effect volume: Countdown sound",
	"soundStartVolume": "Sound effect volume: Start sound",
	"language": "Language",
	"usage1": "Raffle draw.",
	"usage2": "Drawing a raffle reduces the number of lots in the box. Prizes are entered according to the following rules",
	"usage3": "Quantity:Name\nQuantity:Name\nQuantity:Name",
	"usage4": "e.g.\n1:car\n3:bicycle\n5:bag\n100:lose",

	"dummy": "dummy"
}

lib/l10n/app_es.arb

{
	"@@locale":"es",
	"@locale": {
		"description": "スペイン"
	},
	"setting": "Configuración",
	"start": "Comience tocando la pantalla",
	"prize": "Premio",
	"initial": "Valor inicial",
	"countdownTime": "tiempo de cuenta regresiva",
	"soundReadyVolume": "Volumen del efecto de sonido: sonido de cuenta regresiva",
	"soundStartVolume": "Volumen del efecto de sonido: sonido de inicio",
	"language": "Idioma",
	"usage1": "Sorteo de la rifa.",
	"usage2": "Sortear reduce el número de lotes en la caja. Los premios se ingresan de acuerdo con las siguientes reglas.",
	"usage3": "Cantidad:Nombre\nCantidad:Nombre\nCantidad:Nombre",
	"usage4": "p.ej.\n1:coche\n3:bicicleta\n5:bolsa\n100:perder",

	"dummy": "dummy"
}

lib/l10n/app_et.arb

{
	"@@locale":"et",
	"@locale": {
		"description": "エストニア"
	},
	"setting": "Seadistamine",
	"start": "Alustage ekraani puudutamisest",
	"prize": "Auhind",
	"initial": "Algne väärtus",
	"countdownTime": "Loendusaeg",
	"soundReadyVolume": "Heliefekti helitugevus: tagasilugemise heli",
	"soundStartVolume": "Heliefekti helitugevus: käivitage heli",
	"language": "Keel",
	"usage1": "Loosimine.",
	"usage2": "Loosimine vähendab kastis olevate loosimiste arvu. Auhinnad antakse välja järgmiste reeglite järgi",
	"usage3": "Kogus:Nimi\nKogus:Nimi\nKogus:Nimi",
	"usage4": "nt.\n1:auto\n3:jalgratas\n5:kott\n100:kaotada",

	"dummy": "dummy"
}

lib/l10n/app_fi.arb

{
	"@@locale":"fi",
	"@locale": {
		"description": "フィンランド"
	},
	"setting": "Asetus",
	"start": "Aloita napauttamalla näyttöä",
	"prize": "Palkinto",
	"initial": "Alkuarvo",
	"countdownTime": "Lähtölaskenta aika",
	"soundReadyVolume": "Äänitehosteen äänenvoimakkuus: Ajastinääni",
	"soundStartVolume": "Äänitehosteen voimakkuus: Aloita ääni",
	"language": "Kieli",
	"usage1": "Arvonta.",
	"usage2": "Arpajaiset vähentävät laatikossa olevien arpien määrää. Palkinnot arvotaan seuraavien sääntöjen mukaisesti",
	"usage3": "Määrä:Nimi\nMäärä:Nimi\nMäärä:Nimi",
	"usage4": "esim.\n1:auto\n3:polkupyörä\n5:laukku\n100:häviä",

	"dummy": "dummy"
}

lib/l10n/app_fr.arb

{
	"@@locale":"fr",
	"@locale": {
		"description": "フランス"
	},
	"setting": "Paramètre",
	"start": "Commencez par appuyer sur l'écran",
	"prize": "Prix",
	"initial": "Valeur initiale",
	"countdownTime": "Temps de compte à rebours",
	"soundReadyVolume": "Volume de l'effet sonore: son du compte à rebours",
	"soundStartVolume": "Volume de l'effet sonore: Démarrer le son",
	"language": "Langue",
	"usage1": "Tirage au sort.",
	"usage2": "Le tirage au sort réduit le nombre de lots dans la boîte. Les prix sont inscrits selon les règles suivantes",
	"usage3": "Quantité:Nom\nQuantité:Nom\nQuantité:Nom",
	"usage4": "par exemple.\n1:voiture\n3:vélo\n5:sac\n100:perdre",

	"dummy": "dummy"
}

lib/l10n/app_hu.arb

{
	"@@locale":"hu",
	"@locale": {
		"description": "ハンガリー"
	},
	"setting": "Beállítás",
	"start": "Kezdje a képernyő megérintésével",
	"prize": "Díj",
	"initial": "Kezdő érték",
	"countdownTime": "Visszaszámlálási idő",
	"soundReadyVolume": "Hangeffektus hangereje: Visszaszámláló hang",
	"soundStartVolume": "Hangeffektus hangereje: Hang indítása",
	"language": "Nyelv",
	"usage1": "Tombola sorsolás.",
	"usage2": "A tombolasorsolás csökkenti a dobozban lévő tételek számát. A nyeremények az alábbi szabályok szerint kerülnek beadásra",
	"usage3": "Mennyiség:Név\nMennyiség:Név\nMennyiség:Név",
	"usage4": "például.\n1:autó\n3:kerékpár\n5:táska\n100:veszít",

	"dummy": "dummy"
}

lib/l10n/app_it.arb

{
	"@@locale":"it",
	"@locale": {
		"description": "イタリア"
	},
	"setting": "Collocamento",
	"start": "Inizia toccando lo schermo",
	"prize": "Premio",
	"initial": "Valore iniziale",
	"countdownTime": "Tempo del conto alla rovescia",
	"soundReadyVolume": "Volume dell'effetto sonoro: suono del conto alla rovescia",
	"soundStartVolume": "Volume dell'effetto sonoro: avvia il suono",
	"language": "Lingua",
	"usage1": "Estrazione della lotteria.",
	"usage2": "L'estrazione di una lotteria riduce il numero di lotti nella scatola. I premi vengono inseriti secondo le seguenti regole",
	"usage3": "Quantità:nome\nQuantità:nome\nQuantità:nome",
	"usage4": "per esempio.\n1:auto\n3:bicicletta\n5:borsa\n100:perdere",


	"dummy": "dummy"
}

lib/l10n/app_ja.arb

{
	"@@locale":"ja",
	"@locale": {
		"description": "日本"
	},
	"setting": "設定",
	"start": "画面内をタップでスタート",
	"prize": "賞",
	"initial":"初期値",
	"countdownTime": "カウントダウン時間",
	"soundReadyVolume": "音量:開始音",
	"soundStartVolume": "音量:抽選音",
	"language": "言語",
	"usage1": "くじ引きです。",
	"usage2": "くじを引くと箱の中のくじが減ります。賞は以下のルールで入力します。",
	"usage3": "個数:名称\n個数:名称\n個数:名称",
	"usage4": "例)\n1:自動車\n3:自転車\n5:バッグ\n100:はずれ",

	"dummy": "dummy"
}

lib/l10n/app_lt.arb

{
	"@@locale":"lt",
	"@locale": {
		"description": "リトアニア"
	},
	"setting": "Nustatymas",
	"start": "Pradėkite bakstelėdami ekraną",
	"prize": "Prizas",
	"initial": "Pradinė vertė",
	"countdownTime": "Atgalinės atskaitos laikas",
	"soundReadyVolume": "Garso efekto garsumas: Atgalinės atskaitos garsas",
	"soundStartVolume": "Garso efekto garsumas: Pradėti garsą",
	"language": "Kalba",
	"usage1": "Loterija.",
	"usage2": "Loterijos traukimas sumažina lotų skaičių dėžutėje. Prizai įvedami pagal šias taisykles",
	"usage3": "Kiekis:Pavadinimas\nKiekis:Pavadinimas\nKiekis:Pavadinimas",
	"usage4": "pvz.\n1:automobilis\n3:dviratis\n5:maišelis\n100:pralaimėti",

	"dummy": "dummy"
}

lib/l10n/app_lv.arb

{
	"@@locale":"lv",
	"@locale": {
		"description": "ラトビア"
	},
	"setting": "Iestatījums",
	"start": "Sāciet, pieskaroties ekrānam",
	"prize": "Balva",
	"initial": "Sākotnējā vērtība",
	"countdownTime": "Atpakaļskaitīšanas laiks",
	"soundReadyVolume": "Skaņas efekta skaļums: Atpakaļskaitīšanas skaņa",
	"soundStartVolume": "Skaņas efekta skaļums: Sākt skaņu",
	"language": "Valoda",
	"usage1": "Izloze.",
	"usage2": "Izloze samazina ložu skaitu kastē. Balvas tiek piešķirtas saskaņā ar šādiem noteikumiem",
	"usage3": "Daudzums:Vārds\nDaudzums:Vārds\nDaudzums:Vārds",
	"usage4": "piem.\n1:automašīna\n3:velosipēds\n5:maiss\n100:zaudēt",

	"dummy": "dummy"
}

lib/l10n/app_nl.arb

{
	"@@locale":"nl",
	"@locale": {
		"description": "オランダ"
	},
	"setting": "Instelling",
	"start": "Begin door op het scherm te tikken",
	"prize": "Prijs",
	"initial": "Beginwaarde",
	"countdownTime": "Afteltijd",
	"soundReadyVolume": "Volume geluidseffect: aftelgeluid",
	"soundStartVolume": "Volume geluidseffect: Start geluid",
	"language": "Taal",
	"usage1": "Loting loterij.",
	"usage2": "Door een loterij te trekken, wordt het aantal loten in de doos verminderd. Prijzen worden toegekend volgens de volgende regels",
	"usage3": "Hoeveelheid:Naam\nHoeveelheid:Naam\nHoeveelheid:Naam",
	"usage4": "bijv.\n1:auto\n3:fiets\n5:zak\n100:verliezen",

	"dummy": "dummy"
}

lib/l10n/app_pl.arb

{
	"@@locale":"pl",
	"@locale": {
		"description": "ポーランド"
	},
	"setting": "Ustawienie",
	"start": "Zacznij od dotknięcia ekranu",
	"prize": "Nagroda",
	"initial": "Wartość początkowa",
	"countdownTime": "Czas odliczania",
	"soundReadyVolume": "Głośność efektu dźwiękowego: Dźwięk odliczania",
	"soundStartVolume": "Głośność efektu dźwiękowego: Włącz dźwięk",
	"language": "Język",
	"usage1": "Losowanie loterii.",
	"usage2": "Losowanie loterii zmniejsza liczbę losów w pudełku. Nagrody przyznawane są według poniższych zasad",
	"usage3": "Ilość:nazwa\nIlość:nazwa\nIlość:nazwa",
	"usage4": "np.\n1:samochód\n3:rower\n5:torba\n100:przegraj",

	"dummy": "dummy"
}

lib/l10n/app_pt.arb

{
	"@@locale":"pt",
	"@locale": {
		"description": "ポルトガル"
	},
	"setting": "Contexto",
	"start": "Comece tocando na tela",
	"prize": "Prêmio",
	"initial": "Valor inicial",
	"countdownTime": "Tempo de contagem regressiva",
	"soundReadyVolume": "Volume do efeito sonoro: Som de contagem regressiva",
	"soundStartVolume": "Volume do efeito sonoro: som inicial",
	"language": "Linguagem",
	"usage1": "Sorteio de rifa.",
	"usage2": "O sorteio reduz o número de lotes na caixa. Os prêmios são inscritos de acordo com as seguintes regras",
	"usage3": "Quantidade:Nome\nQuantidade:Nome\nQuantidade:Nome",
	"usage4": "por exemplo.\n1:carro\n3:bicicleta\n5:bolsa\n100:perder",

	"dummy": "dummy"
}

lib/l10n/app_ro.arb

{
	"@@locale":"ro",
	"@locale": {
		"description": "ルーマニア"
	},
	"setting": "Setare",
	"start": "Începeți prin atingerea ecranului",
	"prize": "Premiu",
	"initial": "Valoarea initiala",
	"countdownTime": "Timp de numărătoare inversă",
	"soundReadyVolume": "Volumul efectului sonor: sunet de numărătoare inversă",
	"soundStartVolume": "Volumul efectului sonor: porniți sunetul",
	"language": "Limba",
	"usage1": "Extragere la tombolă.",
	"usage2": "Extragerea unei tombole reduce numărul de loturi din cutie. Premiile se înscriu conform următoarelor reguli",
	"usage3": "Cantitate:Nume\nCantitate:Nume\nCantitate:Nume",
	"usage4": "de exemplu.\n1:masina\n3:bicicletă\n5:geanta\n100:pierde",

	"dummy": "dummy"
}

lib/l10n/app_ru.arb

{
	"@@locale":"ru",
	"@locale": {
		"description": "ロシア"
	},
	"setting": "Параметр",
	"start": "Начните с нажатия на экран",
	"prize": "Приз",
	"initial": "Начальное значение",
	"countdownTime": "Время обратного отсчета",
	"soundReadyVolume": "Громкость звукового эффекта: звук обратного отсчета",
	"soundStartVolume": "Громкость звукового эффекта: Звук запуска",
	"language": "Язык",
	"usage1": "Розыгрыш лотереи.",
	"usage2": "Розыгрыш лотереи уменьшает количество лотов в коробке. Призы разыгрываются по следующим правилам",
	"usage3": "Количество:Имя\nКоличество:Имя\nКоличество:Имя",
	"usage4": "например\n1:машина\n3:велосипед\n5:сумка\n100:проиграть",

	"dummy": "dummy"
}

lib/l10n/app_sk.arb

{
	"@@locale":"sk",
	"@locale": {
		"description": "スロバキア"
	},
	"setting": "Nastavenie",
	"start": "Začnite ťuknutím na obrazovku",
	"prize": "cena",
	"initial": "Pôvodná hodnota",
	"countdownTime": "Čas odpočítavania",
	"soundReadyVolume": "Hlasitosť zvukového efektu: Zvuk odpočítavania",
	"soundStartVolume": "Hlasitosť zvukového efektu: Spustenie zvuku",
	"language": "Jazyk",
	"usage1": "Žrebovanie tomboly.",
	"usage2": "Žrebovanie tomboly znižuje počet žrebov v krabici. Ceny sa odovzdávajú podľa nasledujúcich pravidiel",
	"usage3": "Množstvo:Názov\nMnožstvo:Názov\nMnožstvo:Názov",
	"usage4": "napr.\n1:auto\n3:bicykel\n5:vrecko\n100:prehrať",

	"dummy": "dummy"
}

lib/l10n/app_sv.arb

{
	"@@locale":"sv",
	"@locale": {
		"description": "スウェーデン"
	},
	"setting": "Miljö",
	"start": "Börja med att trycka på skärmen",
	"prize": "Pris",
	"initial": "Ursprungligt värde",
	"countdownTime": "Nedräkningstid",
	"soundReadyVolume": "Ljudeffektvolym: Ljud för nedräkning",
	"soundStartVolume": "Ljudeffektvolym: Startljud",
	"language": "Språk",
	"usage1": "Lottning.",
	"usage2": "Att dra en utlottning minskar antalet lotter i lådan. Priser anmäls enligt följande regler",
	"usage3": "Antal:Namn\nAntal:Namn\nAntal:Namn",
	"usage4": "t.ex.\n1:bil\n3:cykel\n5:väska\n100:förlora",


	"dummy": "dummy"
}

lib/l10n/app_th.arb

{
	"@@locale":"th",
	"@locale": {
		"description": "タイ"
	},
	"setting": "การตั้งค่า",
	"start": "เริ่มต้นด้วยการแตะบนหน้าจอ",
	"prize": "รางวัล",
	"initial": "ค่าเริ่มต้น",
	"countdownTime": "เวลานับถอยหลัง",
	"soundReadyVolume": "ระดับเอฟเฟกต์เสียง: เสียงนับถอยหลัง",
	"soundStartVolume": "ระดับเสียงเอฟเฟกต์: เริ่มเสียง",
	"language": "ภาษา",
	"usage1": "การจับสลาก.",
	"usage2": "การออกสลากจะลดจำนวนลอตในกล่อง รางวัลจะถูกป้อนตามกฎต่อไปนี้",
	"usage3": "ปริมาณ:ชื่อ\nปริมาณ:ชื่อ\nปริมาณ:ชื่อ",
	"usage4": "เช่น.\n1:รถ\n3:จักรยาน\n5:กระเป๋า\n100:แพ้",

	"dummy": "dummy"
}

lib/l10n/app_zh.arb

{
	"@@locale":"zh",
	"@locale": {
		"description": "中国"
	},
	"setting": "环境",
	"start": "首先点击屏幕开始",
	"prize": "奖",
	"initial": "初始值",
	"countdownTime": "倒计时时间",
	"soundReadyVolume": "音效音量: 倒计时声音",
	"soundStartVolume": "音效音量:开始声音",
	"language": "语言",
	"usage1": "抽奖。",
	"usage2": "抽奖会减少盒子中的手数。 奖品按照以下规则报名",
	"usage3": "数量:名称\n数量:名称\n数量:名称",
	"usage4": "例如\n1:汽车\n3:自行车\n5:袋子\n100:输",

	"dummy": "dummy"
}