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.2.3+19
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
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
collection: ^1.19.1
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
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();
}
},
),
);
}
}
/*
* 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
*/
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;
}
}
}
///
/// @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();
}
}
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-02
///
library;
class ConstValue {
//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';
}
library;
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.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/loading_screen.dart';
import 'package:lotteryslot/model.dart';
import 'package:lotteryslot/parse_locale_tag.dart';
import 'package:lotteryslot/setting_page.dart';
import 'package:lotteryslot/text_to_speech.dart';
import 'package:lotteryslot/theme_mode_number.dart';
import 'package:lotteryslot/theme_color.dart';
import 'package:lotteryslot/main.dart';
class MainHomePage extends StatefulWidget {
const MainHomePage({super.key});
@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;
@override
void initState() {
super.initState();
_initState();
}
Future<void> _initState() async {
_adManager = AdManager();
_audioPlay.machineSoundVolume = Model.machineSoundVolume;
_audioPlay.prizeSoundVolume = Model.prizeSoundVolume;
_audioPlay.playMachineStop();
await TextToSpeech.applyPreferences(Model.ttsVoiceId,Model.ttsVolume);
if (!kIsWeb && Platform.isAndroid) {
_audioPlay.playMachineStop();
}
if (mounted) {
setState(() {
_isReady = true;
});
}
}
@override
void dispose() {
_adManager.dispose();
_audioPlay.dispose();
TextToSpeech.stop();
super.dispose();
}
Future<void> _onClickSetting() async {
if (_busyFlag) {
return;
}
final updatedSettings = await Navigator.push<bool>(context,
MaterialPageRoute(builder: (_) => const SettingPage()),
);
if (updatedSettings == true) {
if (mounted) {
_audioPlay.machineSoundVolume = Model.machineSoundVolume;
_audioPlay.prizeSoundVolume = Model.prizeSoundVolume;
List<int> historyNumbers = Model.getHistoryNumbers();
historyNumbers = historyNumbers.reversed.toList();
_controllerDisplayHistoryString.text = historyNumbers.join(', ');
await TextToSpeech.applyPreferences(Model.ttsVoiceId,Model.ttsVolume);
//
final mainState = context.findAncestorStateOfType<MainAppState>();
if (mainState != null) { //MyAppStateに反映する
mainState
..themeMode = ThemeModeNumber.numberToThemeMode(Model.themeNumber)
..locale = parseLocaleTag(Model.languageCode)
..setState(() {});
setState(() {
_isFirst = true; //再度テーマ更新
});
}
}
}
}
@override
Widget build(BuildContext context) {
if (!_isReady) {
return const LoadingScreen();
}
if (_isFirst) {
_isFirst = false;
_themeColor = ThemeColor(themeNumber: Model.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: _onClickSetting,
),
),
],
),
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[Model.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 (Model.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 (Model.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 = Model.getHistoryNumbers().reversed.toList();
_controllerDisplayHistoryString.text = historyNumbers.join(', ');
final int nextNumber = await _nextNumber();
if (nextNumber == -1) {
setState(() {
_busyFlag = false;
});
return;
}
final ret = await Model.addHistoryText(nextNumber);
if (ret == false) {
return;
}
_digitSpeeds.shuffle();
_audioPlay.playMachineStart();
final int timeRemain = (10 - Model.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 (Model.ttsEnabled && Model.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 = Model.getCandidateNumbers();
if ((Model.getHistoryNumbers()).isNotEmpty) {
remains = remains
.where((int num) => !(Model.getHistoryNumbers()).contains(num))
.toList();
}
if (remains.isEmpty) {
return -1;
}
_controllerDisplayRemainString.text =
'Candidates:${Model.getCandidateNumbers().length} Results:${Model.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 Model.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(() {});
}
}
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,
),
),
);
}
}
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-09
///
library;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:lotteryslot/model.dart';
import 'package:lotteryslot/loading_screen.dart';
import 'package:lotteryslot/parse_locale_tag.dart';
import 'package:lotteryslot/theme_mode_number.dart';
import 'package:lotteryslot/home_page.dart';
import 'package:lotteryslot/l10n/app_localizations.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
));
MobileAds.instance.initialize();
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => MainAppState();
}
class MainAppState extends State<MainApp> {
ThemeMode themeMode = ThemeMode.light;
Locale? locale;
bool _isReady = false;
@override
void initState() {
super.initState();
_initState();
}
void _initState() async {
await Model.ensureReady();
themeMode = ThemeModeNumber.numberToThemeMode(Model.themeNumber);
locale = parseLocaleTag(Model.languageCode);
setState(() {
_isReady = true;
});
}
@override
Widget build(BuildContext context) {
if (!_isReady) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: LoadingScreen(),
),
),
);
}
const seed = Colors.purple;
return MaterialApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: locale,
themeMode: themeMode,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: seed),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: seed,
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const MainHomePage(),
);
}
}
import 'package:shared_preferences/shared_preferences.dart';
import 'package:lotteryslot/const_value.dart';
class Model {
Model._();
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 _prefTtsEnabled = 'ttsEnabled';
static const String _prefTtsVoiceId = 'ttsVoiceId';
static const String _prefTtsVolume = 'ttsVolume';
static const String _prefThemeNumber = 'themeNumber';
static const String _prefLanguageCode = 'languageCode';
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 = 0;
static String _languageCode = '';
static Future<void> ensureReady() async {
if (_ready) {
return;
}
final SharedPreferences prefs = await SharedPreferences.getInstance();
//
_candidateText = prefs.getString(_prefCandidateText) ?? ConstValue.candidateTextDefault;
_prizeText = prefs.getString(_prefPrizeText) ?? ConstValue.prizeTextDefault;
_historyText = prefs.getString(_prefHistoryText) ?? ConstValue.historyTextDefault;
_historyDrawFlag = prefs.getBool(_prefHistoryDrawFlag) ?? true;
_machineImageIndex = (prefs.getInt(_prefMachineImageIndex) ?? 0).clamp(0,3);
_machineSpeed = (prefs.getInt(_prefMachineSpeed) ?? 1).clamp(1,9);
_machineSoundVolume = (prefs.getDouble(_prefMachineSoundVolume) ?? 1.0).clamp(0.0,1.0);
_prizeSoundVolume = (prefs.getDouble(_prefPrizeSoundVolume) ?? 1.0).clamp(0.0,1.0);
_ttsEnabled = prefs.getBool(_prefTtsEnabled) ?? true;
_ttsVoiceId = prefs.getString(_prefTtsVoiceId) ?? '';
_ttsVolume = (prefs.getDouble(_prefTtsVolume) ?? 1.0).clamp(0.0,1.0);
_themeNumber = (prefs.getInt(_prefThemeNumber) ?? 0).clamp(0, 2);
_languageCode = prefs.getString(_prefLanguageCode) ?? '';
_ready = true;
}
//get------------------
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 String get languageCode => _languageCode;
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);
}
static Future<void> setCandidateText(String value) async {
value = _candidateFormat(value);
_candidateText = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefCandidateText, value);
}
static Future<void> setPrizeText(String value) async {
value = _prizeFormat(value);
_prizeText = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefPrizeText, value);
}
static Future<void> setHistoryText(String value) async {
value = _historyFormat(value);
_historyText = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefHistoryText, value);
}
static Future<void> setHistoryDrawFlag(bool value) async {
_historyDrawFlag = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefHistoryDrawFlag, value);
}
static Future<void> setMachineImageIndex(int value) async {
_machineImageIndex = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_prefMachineImageIndex, value);
}
static Future<void> setMachineSpeed(int value) async {
_machineSpeed = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_prefMachineSpeed, value);
}
static Future<void> setMachineSoundVolume(double value) async {
_machineSoundVolume = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_prefMachineSoundVolume, _machineSoundVolume);
}
static Future<void> setPrizeSoundVolume(double value) async {
_prizeSoundVolume = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_prefPrizeSoundVolume, _prizeSoundVolume);
}
static Future<bool> addHistoryText(int value) async {
List<int> numbers = getHistoryNumbers();
if (numbers.contains(value)) { //2重登録防止
return false;
}
numbers.add(value);
_historyText = numbers.map((int value) => value.toString()).join(',');
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefHistoryText, _historyText);
return true;
}
static Future<void> setTtsEnabled(bool value) async {
_ttsEnabled = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefTtsEnabled, value);
}
static Future<void> setTtsVoiceId(String value) async {
_ttsVoiceId = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefTtsVoiceId, value);
}
static Future<void> setTtsVolume(double value) async {
_ttsVolume = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_prefTtsVolume, value);
}
static Future<void> setThemeNumber(int value) async {
_themeNumber = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setInt(_prefThemeNumber, value);
}
static Future<void> setLanguageCode(String value) async {
_languageCode = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefLanguageCode, value);
}
//------------------
//'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;
}
}
import 'dart:ui';
Locale? parseLocaleTag(String tag) {
if (tag.isEmpty) {
return null;
}
final parts = tag.split('-');
final language = parts[0];
String? script, country;
if (parts.length >= 2) {
parts[1].length == 4 ? script = parts[1] : country = parts[1];
}
if (parts.length >= 3) {
parts[2].length == 4 ? script = parts[2] : country = parts[2];
}
return Locale.fromSubtags(
languageCode: language,
scriptCode: script,
countryCode: country,
);
}
///
/// @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/model.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;
late UmpConsentController _adUmp;
AdUmpState _adUmpState = AdUmpState.initial;
int _themeNumber = 0;
String _languageCode = '';
late ThemeColor _themeColor;
bool _isReady = false;
bool _isFirst = true;
//
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 = '';
@override
void initState() {
super.initState();
_initState();
}
void _initState() async {
_adManager = AdManager();
_themeNumber = Model.themeNumber;
_languageCode = Model.languageCode;
//
_adUmp = UmpConsentController();
_refreshConsentInfo();
//
_controllerCandidateText.text = Model.candidateText;
_controllerPrizeText.text = Model.prizeText;
_controllerHistoryText.text = Model.historyText;
_historyDrawFlag = Model.historyDrawFlag;
_machineImageIndex = Model.machineImageIndex;
_machineSpeedValue = Model.machineSpeed;
_machineSoundVolume = Model.machineSoundVolume;
_prizeSoundVolume = Model.prizeSoundVolume;
_ttsEnabled = Model.ttsEnabled;
_ttsVolume = Model.ttsVolume;
_ttsVoiceId = Model.ttsVoiceId;
//speech
await TextToSpeech.getInstance();
_ttsVoices = TextToSpeech.ttsVoices;
TextToSpeech.setVolume(_ttsVolume);
TextToSpeech.setTtsVoiceId(_ttsVoiceId);
//
setState((){
_isReady = true;
});
}
@override
void dispose() {
_adManager.dispose();
unawaited(TextToSpeech.stop());
super.dispose();
}
Future<void> _onApply() async {
if (_candidateInitialFlag) {
await Model.setCandidateText(ConstValue.candidateTextDefault);
} else {
await Model.setCandidateText(_controllerCandidateText.text);
}
if (_prizeInitialFlag) {
await Model.setPrizeText(ConstValue.prizeTextDefault);
} else {
await Model.setPrizeText(_controllerPrizeText.text);
}
if (_historyInitialFlag) {
await Model.setHistoryText(ConstValue.historyTextDefault);
} else {
await Model.setHistoryText(_controllerHistoryText.text);
}
await Model.setHistoryDrawFlag(_historyDrawFlag);
await Model.setMachineImageIndex(_machineImageIndex);
await Model.setMachineSpeed(_machineSpeedValue);
await Model.setMachineSoundVolume(_machineSoundVolume);
await Model.setPrizeSoundVolume(_prizeSoundVolume);
await Model.setTtsEnabled(_ttsEnabled);
await Model.setTtsVoiceId(_ttsVoiceId);
await Model.setTtsVolume(_ttsVolume);
await Model.setThemeNumber(_themeNumber);
await Model.setLanguageCode(_languageCode);
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),
_buildSpeechSettings(l),
_buildTheme(l),
_buildLanguage(l),
_buildCmp(l),
_buildUsage(l),
],
),
),
),
),
),
]),
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 _buildSpeechSettings(AppLocalizations l) {
if (_ttsVoices.isEmpty) {
return SizedBox.shrink();
}
return 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: 8, left: 16, right: 16),
child: Row(
children: [
Expanded(
child: Text(l.ttsEnabled),
),
Switch(
value: _ttsEnabled,
onChanged: (bool value) {
setState(() {
_ttsEnabled = value;
});
},
),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
children: [
Text(l.ttsVolume),
const Spacer(),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
children: <Widget>[
Text(_ttsVolume.toStringAsFixed(1)),
Expanded(
child: Slider(
value: _ttsVolume,
min: 0.0,
max: 1.0,
divisions: 10,
onChanged: _ttsEnabled
? (double value) {
setState(() {
_ttsVolume = double.parse(value.toStringAsFixed(1));
});
}
: null,
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
children: [
Text(l.ttsVoiceId),
const Spacer(),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
child: DropdownButtonFormField<String>(
dropdownColor: _themeColor.dropdownColor,
initialValue: () {
if (_ttsVoiceId.isNotEmpty && _ttsVoices.any((o) => o.id == _ttsVoiceId)) {
return _ttsVoiceId;
}
return _ttsVoices.first.id;
}(),
items: _ttsVoices
.map((o) => DropdownMenuItem<String>(value: o.id, child: Text(o.label)))
.toList(),
onChanged: (v) {
if (v == null) return;
setState(() => _ttsVoiceId = v);
},
),
),
],
)
);
}
Widget _buildTheme(AppLocalizations l) {
final TextTheme t = Theme.of(context).textTheme;
return Card(
margin: const EdgeInsets.only(left: 0, top: 12, right: 0, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: Text(
l.theme,
style: t.bodyMedium,
),
),
DropdownButton<int>(
value: _themeNumber,
items: [
DropdownMenuItem(value: 0, child: Text(l.systemSetting)),
DropdownMenuItem(value: 1, child: Text(l.lightTheme)),
DropdownMenuItem(value: 2, child: Text(l.darkTheme)),
],
onChanged: (value) {
if (value != null) {
setState(() {
_themeNumber = value;
});
}
},
),
],
),
),
);
}
Widget _buildLanguage(AppLocalizations l) {
final Map<String,String> languageNames = {
'en': 'English',
'bg': 'Bulgarian',
'cs': 'Čeština',
'da': 'Dansk',
'de': 'Deutsch',
'el': 'Ελληνικά',
'es': 'Español',
'et': 'Eesti',
'fi': 'Suomi',
'fr': 'Français',
'hu': 'Magyar',
'id': 'Indonesia',
'it': 'Italiano',
'ja': '日本語',
'ko': '한국어',
'lt': 'Lietuvių',
'lv': 'Latviešu',
'nb': 'Norsk Bokmål',
'nl': 'Nederlands',
'no': 'Norsk',
'pl': 'Polski',
'pt': 'Português',
'ro': 'Română',
'ru': 'Русский',
'sk': 'Slovenčina',
'sv': 'Svenska',
'th': 'ไทย',
'tr': 'Türkçe',
'uk': 'Українська',
'vi': 'Tiếng Việt',
'zh': '中文',
};
final TextTheme t = Theme.of(context).textTheme;
return Card(
margin: const EdgeInsets.only(left: 0, top: 12, right: 0, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Row(
children: [
Expanded(
child: Text(
l.language,
style: t.bodyMedium,
),
),
DropdownButton<String?>(
value: _languageCode,
items: [
DropdownMenuItem(value: '', child: Text('Default')),
...languageNames.entries.map((entry) => DropdownMenuItem<String?>(
value: entry.key,
child: Text(entry.value),
)),
],
onChanged: (String? value) {
setState(() {
_languageCode = value ?? '';
});
},
),
],
),
),
);
}
Widget _buildCmp(AppLocalizations l) {
final TextTheme t = Theme.of(context).textTheme;
final showButton = _adUmpState.privacyStatus == PrivacyOptionsRequirementStatus.required;
String statusLabel = l.cmpCheckingRegion;
IconData statusIcon = Icons.help_outline;
switch (_adUmpState.privacyStatus) {
case PrivacyOptionsRequirementStatus.required:
statusLabel = l.cmpRegionRequiresSettings;
statusIcon = Icons.privacy_tip_outlined;
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 Card(
margin: const EdgeInsets.only(left: 0, top: 12, right: 0, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l.cmpSettingsTitle,
style: t.bodyMedium,
),
const SizedBox(height: 8),
Text(
l.cmpConsentDescription,
style: t.bodySmall,
),
const SizedBox(height: 16),
Center(
child: Column(
children: [
Chip(
avatar: Icon(statusIcon, size: 18),
label: Text(statusLabel),
),
const SizedBox(height: 6),
Text(
'${l.cmpConsentStatusLabel} ${_adUmpState.consentStatus.localized(context)}',
style: t.bodySmall,
),
if (showButton) ...[
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _adUmpState.isChecking
? null
: _onTapPrivacyOptions,
icon: const Icon(Icons.settings),
label: Text(
_adUmpState.isChecking
? l.cmpConsentStatusChecking
: l.cmpOpenConsentSettings,
),
),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: _adUmpState.isChecking
? null
: _refreshConsentInfo,
icon: const Icon(Icons.refresh),
label: Text(l.cmpRefreshStatus),
),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: () async {
final messenger = ScaffoldMessenger.of(context);
final message = l.cmpResetStatusDone;
await ConsentInformation.instance.reset();
await _refreshConsentInfo();
if (!mounted) {
return;
}
messenger.showSnackBar(
SnackBar(content: Text(message)),
);
},
icon: const Icon(Icons.delete_sweep_outlined),
label: Text(l.cmpResetStatus),
),
],
],
),
),
],
),
),
);
}
Widget _buildUsage(AppLocalizations l) {
final TextTheme t = Theme.of(context).textTheme;
return SizedBox(
width: double.infinity,
child: Card(
margin: const EdgeInsets.only(left: 0, top: 12, right: 0, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l.usage1,
style: t.bodySmall,
),
const SizedBox(height: 12),
Text(
l.usage2,
style: t.bodySmall,
),
const SizedBox(height: 12),
Text(
l.usage3,
style: t.bodySmall,
),
const SizedBox(height: 12),
Text(
l.usage4,
style: t.bodySmall,
),
],
),
),
)
);
}
}
/*
void _initState() async {
await TextToSpeech.applyPreferences(Preferences.ttsVoiceId,Preferences.ttsVolume);
}
@override
void dispose() {
TextToSpeech.stop();
super.dispose();
}
void any() {
await TextToSpeech.speak(text);
}
void _onClickSetting() async {
final updatedSettings = await Navigator.push(
context,MaterialPageRoute(builder: (context) => SettingPage()),
);
if (updatedSettings != null) {
await TextToSpeech.applyPreferences(Preferences.ttsVoiceId,Preferences.ttsVolume);
}
}
*/
import 'package:flutter_tts/flutter_tts.dart';
import 'dart:io' show Platform;
import 'package:collection/collection.dart';
class TtsOption {
final String locale;
final String name;
const TtsOption(this.locale, this.name);
String get id => '$locale|$name';
String get label => '$locale $name';
}
class TextToSpeech {
static late FlutterTts _tts;
static final List<TtsOption> ttsVoices = [];
static String ttsVoiceId = '';
static TextToSpeech? _instance;
static bool _initialized = false;
TextToSpeech._internal();
static Future<TextToSpeech> getInstance() async {
_instance ??= TextToSpeech._internal();
if (!_initialized) {
await _instance!._initial();
_initialized = true;
}
return _instance!;
}
//声リスト作成
Future<void> _initial() async {
_tts = FlutterTts();
try {
List<dynamic>? vs;
for (int i = 0; i < 10; i++) {
vs = await _tts.getVoices;
if (vs != null) {
break;
}
await Future.delayed(Duration(seconds: 1));
}
if (vs is List) {
ttsVoices.clear();
for (final v in vs) {
if (v is Map && v['name'] is String && v['locale'] is String) {
ttsVoices.add(TtsOption(v['locale']!, v['name']!));
}
}
}
ttsVoices.sort((a, b) => a.label.compareTo(b.label));
ttsVoices.insert(0, TtsOption("Default", ""));
ttsVoiceId = ttsVoices.first.id;
await _tts.awaitSpeakCompletion(true);
} catch (_) {}
}
//ttsVoiceIdを登録
static Future<void> setTtsVoiceId(String newTtsVoiceId) async {
final exists = ttsVoices.any((o) => o.id == newTtsVoiceId);
if (exists) {
ttsVoiceId = newTtsVoiceId;
} else {
ttsVoiceId = ttsVoices.first.id;
}
await _setSpeechVoiceFromId();
}
//ttsVoiceIdの声を用意
static Future<void> _setSpeechVoiceFromId() async {
if (ttsVoices.isEmpty || ttsVoiceId.isEmpty) {
return;
}
final idx = ttsVoiceId.indexOf('|');
String selLocale = '';
String selName = ttsVoiceId;
if (idx >= 0) {
selLocale = ttsVoiceId.substring(0, idx);
selName = ttsVoiceId.substring(idx + 1);
}
TtsOption? match;
if (selLocale.isNotEmpty) {
match = ttsVoices.firstWhereOrNull(
(e) => e.name == selName && e.locale == selLocale,
);
}
match ??= ttsVoices.firstWhereOrNull((e) => e.name == selName);
if (match != null) {
final locale = match.locale;
final name = match.name;
try {
if (Platform.isAndroid) {
// Prefer Google TTS if available; ignore errors if not installed
try {
await _tts.setEngine('com.google.android.tts');
} catch (_) {}
if (locale.isNotEmpty) {
await _tts.setLanguage(locale);
}
await _tts.setVoice({'name': name, 'locale': locale});
} else if (Platform.isIOS) {
// On iOS, setting voice is sufficient; avoid setLanguage overriding the voice
await _tts.setVoice({'name': name, 'locale': locale});
} else {
// Fallback for other platforms
if (locale.isNotEmpty) {
await _tts.setLanguage(locale);
}
await _tts.setVoice({'name': name, 'locale': locale});
}
} catch (_) {}
}
}
//外部から呼び出し。インスタンス生成と設定を同時に行う。
static Future<void> applyPreferences(String ttsVoiceId, double ttsVolume) async {
await TextToSpeech.getInstance();
await TextToSpeech.setTtsVoiceId(ttsVoiceId);
await TextToSpeech.setVolume(ttsVolume);
}
//文字列を音声再生
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 (_) {}
}
}
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 get _isLight => _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;
}
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;
}
}
}