name: roulettewheeleurope
description: "roulettewheeleurope"
publish_to: 'none'
version: 2.13.1+39
environment:
sdk: ^3.11.5
dependencies: #flutter pub upgrade --major-versions
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.20.2 #flutter gen-l10n
shared_preferences: ^2.5.3
flutter_tts: ^4.2.3
google_mobile_ads: ^8.0.0
audioplayers: ^6.0.0
cupertino_icons: ^1.0.8
wakelock_plus: ^1.4.0
in_app_review: ^2.0.11
app_settings: ^7.0.0
dev_dependencies:
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.14.4 #flutter pub run flutter_launcher_icons
flutter_native_splash: ^2.4.0 #flutter pub run flutter_native_splash:create
flutter_icons:
android: "launcher_icon"
ios: true
image_path: "assets/icon/icon.png"
adaptive_icon_background: "assets/icon/icon_back.png"
adaptive_icon_foreground: "assets/icon/icon_fore.png"
flutter_native_splash:
color: '#9da9f5'
image: 'assets/image/splash.png'
color_dark: '#9da9f5'
image_dark: 'assets/image/splash.png'
fullscreen: true
android_12:
icon_background_color: '#9da9f5'
image: 'assets/image/splash.png'
icon_background_color_dark: '#9da9f5'
image_dark: 'assets/image/splash.png'
flutter:
generate: true
uses-material-design: true
config:
enable-swift-package-manager: true
assets:
- assets/icon/
- assets/image/
- assets/sound/
/// Copyright© ao-system, Inc.
import 'package:flutter/cupertino.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:roulettewheeleurope/ad_manager.dart';
class AdBannerWidget extends StatefulWidget {
final AdManager adManager;
const AdBannerWidget({super.key, required this.adManager});
@override
State<AdBannerWidget> createState() => _AdBannerWidgetState();
}
class _AdBannerWidgetState extends State<AdBannerWidget> {
int _lastBannerWidthDp = 0;
bool _isAdLoaded = false;
bool _isLoading = false;
@override
Widget build(BuildContext context) {
return SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
final int width = constraints.maxWidth.isFinite ? constraints.maxWidth.truncate() : MediaQuery.of(context).size.width.truncate();
final bannerAd = widget.adManager.bannerAd;
if (width > 0) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
final bannerAd = widget.adManager.bannerAd;
final bool widthChanged = _lastBannerWidthDp != width;
final bool sizeMismatch = bannerAd == null || bannerAd.size.width != width;
if ((widthChanged || !_isAdLoaded || sizeMismatch) && !_isLoading) {
_lastBannerWidthDp = width;
setState(() { _isAdLoaded = false; _isLoading = true; });
widget.adManager.loadAdaptiveBannerAd(width, () {
if (mounted) {
setState(() { _isAdLoaded = true; _isLoading = false; });
}
});
}
}
});
}
if (_isAdLoaded && bannerAd != null) {
return 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();
}
},
),
);
}
}
/// Copyright© ao-system, Inc.
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';
import 'package:flutter/widgets.dart';
import 'package:roulettewheeleurope/_secrets.dart';
class AdManager {
static String get _adUnitId => Platform.isIOS ? Secrets.adUnitIdIos : Secrets.adUnitIdAndroid;
BannerAd? _bannerAd;
int _lastWidthPx = 0;
VoidCallback? _onLoadedCb;
Timer? _retryTimer;
int _retryAttempt = 0;
BannerAd? get bannerAd => _bannerAd;
/// アプリ起動時の設定
/// UMP(同意管理)を導入したため、手動のNPA設定は不要になった。
static Future<void> initForNPA() async {
if (kIsWeb) {
return;
}
// UMP SDK が保存した同意情報を MobileAds SDK が自動で読み取るため、
// ここで RequestConfiguration を使って NPA を強制する必要はない。
await MobileAds.instance.updateRequestConfiguration(
RequestConfiguration(
tagForChildDirectedTreatment: TagForChildDirectedTreatment.unspecified,
testDeviceIds: Secrets.umpConsentTestDeviceIds, //テストデバイスID:広告の誤クリック防止
),
);
}
Future<void> loadAdaptiveBannerAd(int widthPx, VoidCallback onAdLoaded) async {
if (kIsWeb) {
return;
}
_onLoadedCb = onAdLoaded;
_lastWidthPx = widthPx;
_retryAttempt = 0;
_retryTimer?.cancel();
_retryTimer = null;
_startLoad(widthPx);
}
static AdRequest getAdRequest() {
// ユーザーの同意状態(TCF信号)は、SDKによって自動的に付与される。
// 手動で npa: 1 を送ると、UMPでのユーザーの選択と競合する可能性があるため、空で返す。
// AdRequest(nonPersonalizedAds: true);にはしない
return const AdRequest();
}
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;
_bannerAd = BannerAd(
adUnitId: _adUnitId,
request: getAdRequest(),
size: size,
listener: BannerAdListener(
onAdLoaded: (ad) {
_retryTimer?.cancel();
_retryTimer = null;
_retryAttempt = 0;
final cb = _onLoadedCb;
if (cb != null) {
cb();
}
},
onAdFailedToLoad: (ad, err) {
ad.dispose();
_scheduleRetry();
},
),
)..load();
}
void _scheduleRetry() {
if (kIsWeb) return;
_retryTimer?.cancel();
_retryTimer = null;
_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();
_retryTimer = null;
}
}
/// Copyright© ao-system, Inc.
import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:flutter/widgets.dart';
import 'package:roulettewheeleurope/l10n/app_localizations.dart';
import 'package:roulettewheeleurope/_secrets.dart';
/// UMP状態格納用
class AdUmpState {
final PrivacyOptionsRequirementStatus privacyStatus;
final ConsentStatus consentStatus;
final bool privacyOptionsRequired;
final bool isChecking;
const AdUmpState({
required this.privacyStatus,
required this.consentStatus,
required this.privacyOptionsRequired,
required this.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,
);
}
//UMPコントローラ
class AdUmpConsentController {
//デバッグ用:同意フォームの表示テスト:EEA地域を強制する(本番ではfalseにすること)
final bool forceEeaForDebug = false;
//デバッグ用:同意フォームの表示テスト:EEA地域を強制するテストデバイスID
static final List<String> _testDeviceIds = Secrets.umpConsentTestDeviceIds;
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 {
//同意フォームが必要なら表示する
ConsentForm.loadAndShowConsentFormIfRequired((formError) async {
final s = await ConsentInformation.instance.getPrivacyOptionsRequirementStatus();
final c = await ConsentInformation.instance.getConsentStatus();
completer.complete(
state.copyWith(
privacyStatus: s,
consentStatus: c,
privacyOptionsRequired: s == PrivacyOptionsRequirementStatus.required,
isChecking: false,
),
);
});
},
(FormError e) {
completer.complete(state.copyWith(isChecking: false));
},
);
return await completer.future;
} catch (_) {
return state.copyWith(isChecking: false);
}
}
//プライバシーオプションフォームを表示
Future<FormError?> showPrivacyOptions() async {
if (kIsWeb) return null;
final completer = Completer<FormError?>();
ConsentForm.showPrivacyOptionsForm((FormError? e) {
completer.complete(e);
});
return completer.future;
}
}
class AdUmpService {
final AdUmpConsentController _adUmpConsentController = AdUmpConsentController();
Future<AdUmpState> updateConsentInfo(AdUmpState current) async {
return await _adUmpConsentController.updateConsentInfo(current: current);
}
Future<void> requestConsentInfoUpdate(ConsentRequestParameters params) async {
final completer = Completer<void>();
ConsentInformation.instance.requestConsentInfoUpdate(
params,
() => completer.complete(),
(FormError error) => completer.completeError(error),
);
return completer.future;
}
Future<FormError?> showPrivacyOptions() async {
return await _adUmpConsentController.showPrivacyOptions();
}
}
extension ConsentStatusL10n on ConsentStatus {
String localized(BuildContext context) {
final l = AppLocalizations.of(context)!;
switch (this) {
case ConsentStatus.obtained:
return l.cmpConsentStatusObtained;
case ConsentStatus.required:
return l.cmpConsentStatusRequired;
case ConsentStatus.notRequired:
return l.cmpConsentStatusNotRequired;
case ConsentStatus.unknown:
return l.cmpConsentStatusUnknown;
}
}
}
/// Copyright© ao-system, Inc.
import 'package:flutter/services.dart';
///App Tracking Transparency サービス
///iOS 14以降で広告トラッキングの許可をリクエストする
class AttService {
//チャンネル名をiOS側と一致させる
static const _channel = MethodChannel('aosystem.att');
static final AttService _instance = AttService._internal();
factory AttService() => _instance;
AttService._internal();
///トラッキング許可をリクエスト
///戻り値: AttStatus (enum)
Future<AttStatus> requestTracking() async {
try {
//iOS側からInt値を受け取る
final result = await _channel.invokeMethod<int>('requestTracking');
//enumに変換して返却
return parseAttStatus(result);
} on MissingPluginException catch (_) {
//ハンドラ未登録(iOS以外/旧バイナリ等)はアプリ全体を落とさない
return AttStatus.unknown;
} on PlatformException catch (_) {
return AttStatus.unknown;
}
}
///現在のトラッキング許可状態を取得
///戻り値:AttStatus(enum)
Future<AttStatus> getTrackingStatus() async {
try {
//iOS側からInt値を受け取る
final result = await _channel.invokeMethod<int>('getTrackingStatus');
//enumに変換して返却
return parseAttStatus(result);
} on MissingPluginException catch (_) {
return AttStatus.unknown;
} on PlatformException catch (_) {
return AttStatus.unknown;
}
}
///トラッキングが許可されているか
Future<bool> isTrackingAuthorized() async {
final status = await getTrackingStatus();
return status == AttStatus.authorized;
}
}
//一緒に利用する enum とヘルパー関数
enum AttStatus {
notDetermined, // 0
restricted, // 1
denied, // 2
authorized, // 3
unknown, // 4
}
AttStatus parseAttStatus(int? value) {
switch (value) {
case 0:
return AttStatus.notDetermined;
case 1:
return AttStatus.restricted;
case 2:
return AttStatus.denied;
case 3:
return AttStatus.authorized;
default:
return AttStatus.unknown;
}
}
/// Copyright© ao-system, Inc.
import 'package:flutter/material.dart';
// Maps roulette result codes to Colors.
// Codes: 'g' (green), 'k' (black), 'r' (red)
Color colorFromCode(String code) {
switch (code) {
case 'g':
return const Color(0xFF00BB00);
case 'k':
return const Color(0xFF222222);
case 'r':
return const Color(0xFFD00000);
default:
return Colors.transparent;
}
}
/// Copyright© ao-system, Inc.
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:roulettewheeleurope/theme_color.dart';
import 'package:roulettewheeleurope/model.dart';
import 'package:roulettewheeleurope/text_to_speech.dart';
import 'package:roulettewheeleurope/l10n/app_localizations.dart';
import 'package:roulettewheeleurope/setting_page.dart';
import 'package:roulettewheeleurope/wheel_view.dart';
import 'package:roulettewheeleurope/ad_banner_widget.dart';
import 'package:roulettewheeleurope/color_utils.dart';
import 'package:roulettewheeleurope/main.dart';
import 'package:roulettewheeleurope/service_status.dart';
import 'package:roulettewheeleurope/loading_screen.dart';
class MainHomePage extends StatefulWidget {
const MainHomePage({super.key});
@override
State<MainHomePage> createState() => _MainHomePageState();
}
class _MainHomePageState extends State<MainHomePage> with WidgetsBindingObserver {
late ThemeColor _themeColor;
final AudioPlayer _audio = AudioPlayer();
//
static const double _ballSizeRatio = 0.04; // 4% of wheel diameter
static const double _ballDistanceStart = 0.89;
static const double _ballDistanceEnd = 0.535;
//
static const List<_RouletteSlot> _slots = [
_RouletteSlot("0", "g"),
_RouletteSlot("32", "r"),
_RouletteSlot("15", "k"),
_RouletteSlot("19", "r"),
_RouletteSlot("4", "k"),
_RouletteSlot("21", "r"),
_RouletteSlot("2", "k"),
_RouletteSlot("25", "r"),
_RouletteSlot("17", "k"),
_RouletteSlot("34", "r"),
_RouletteSlot("6", "k"),
_RouletteSlot("27", "r"),
_RouletteSlot("13", "k"),
_RouletteSlot("36", "r"),
_RouletteSlot("11", "k"),
_RouletteSlot("30", "r"),
_RouletteSlot("8", "k"),
_RouletteSlot("23", "r"),
_RouletteSlot("10", "k"),
_RouletteSlot("5", "r"),
_RouletteSlot("24", "k"),
_RouletteSlot("16", "r"),
_RouletteSlot("33", "k"),
_RouletteSlot("1", "r"),
_RouletteSlot("20", "k"),
_RouletteSlot("14", "r"),
_RouletteSlot("31", "k"),
_RouletteSlot("9", "r"),
_RouletteSlot("22", "k"),
_RouletteSlot("18", "r"),
_RouletteSlot("29", "k"),
_RouletteSlot("7", "r"),
_RouletteSlot("28", "k"),
_RouletteSlot("12", "r"),
_RouletteSlot("35", "k"),
_RouletteSlot("3", "r"),
_RouletteSlot("26", "k"),
];
// UI
bool _startUiVisible = true;
bool _settingUiVisible = true;
// UI mirror state for Flutter-native wheel
double _uiWheelAngle = 0;
double _uiBallLeft = 0;
double _uiBallTop = 0;
double _uiBallSize = 0;
bool _uiBallVisible = false;
double _uiAlphaThree = 0;
double _uiAlphaTwo = 0;
double _uiAlphaOne = 0;
double _uiAlphaNoMoreBets = 0;
double _uiAlphaResult = 0;
String _uiResultText = '';
String _uiResultColor = '';
final List<_HistoryItem> _history = <_HistoryItem>[];
// Wheel logic
double _baseSize = 0; // logical size of square wheel area (in px)
double _wheelAngle = 360;
double _ballAngle = 0;
double _wheelAngleStart = 0;
bool _ballRotateFlag = false;
int _ballTick = 0;
double _adjustAngle = 0;
double _ballDistanceRatio = _ballDistanceStart;
bool _busy = false;
Timer? _timer;
bool _isReady = false;
bool _ttsAvailable = ServiceStatus.ttsEnabled;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initState();
}
void _initState() async {
_themeColor = ThemeColor(themeNumber: Model.themeNumber, context: context);
await TextToSpeech.applyPreferences(Model.ttsVoiceId,Model.ttsVolume);
await _applyTtsPreferencesWithFallback();
_initAudio();
_startTicker();
_scheduleServiceIssueMessage();
setState(() {
_isReady = true;
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
WakelockPlus.disable();
_audio.dispose();
_timer?.cancel();
unawaited(TextToSpeech.stop());
super.dispose();
}
//need with WidgetsBindingObserver
//WidgetsBinding.instance.addObserver(this);
//WidgetsBinding.instance.removeObserver(this);
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
_wakelock();
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
WakelockPlus.disable();
break;
}
}
void _wakelock() {
if (Model.wakelockEnabled) {
WakelockPlus.enable();
} else {
WakelockPlus.disable();
}
}
Future<void> _applyTtsPreferencesWithFallback() async {
if (!ServiceStatus.ttsEnabled) {
_updateTtsAvailability(false);
return;
}
try {
await TextToSpeech.applyPreferences(
Model.ttsVoiceId,
Model.ttsVolume,
).timeout(const Duration(seconds: 5));
_updateTtsAvailability(true);
} on TimeoutException catch (error) {
//debugPrint('TextToSpeech initialization timed out: $error');
ServiceStatus.record(
ServiceType.tts,
'T001', //'Text-to-speech has been disabled (initialization timed out).',
);
_updateTtsAvailability(false);
} catch (error, stackTrace) {
//debugPrint('TextToSpeech initialization failed: $error');
//debugPrint('$stackTrace');
ServiceStatus.record(
ServiceType.tts,
'T002', //'Text-to-speech has been disabled (TTS engine unavailable).',
);
_updateTtsAvailability(false);
}
}
void _updateTtsAvailability(bool value) {
if (_ttsAvailable == value) {
return;
}
if (!mounted) {
_ttsAvailable = value;
return;
}
setState(() {
_ttsAvailable = value;
});
}
void _scheduleServiceIssueMessage() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
unawaited(ServiceStatus.showIssuesMessage(context));
});
}
Future<void> _initAudio() async {
await _audio.setReleaseMode(ReleaseMode.stop);
await _audio.setVolume(Model.ttsVolume);
try {
await _audio.setSource(AssetSource('sound/kachi.wav')); //assets/は不要
} catch (_) {}
}
void _startTicker() {
_timer = Timer.periodic(const Duration(milliseconds: 25), (_) {
// wheel rotation
_wheelAngle -= 0.5;
if (_wheelAngle < 0) {
_wheelAngle = 359.5;
}
_setWheelRotation(_wheelAngle);
if (_ballRotateFlag) {
_adjustAngle += 0.5;
_ballAngle += 5;
if (_ballAngle >= 360) {
_ballAngle = 0;
}
if (_ballTick > 0) {
_ballTick -= 1;
if (_ballTick == 500) {
if (Model.countdown) {
_showOverlay(three: 0.8);
if (Model.ttsEnabled && Model.ttsVolume > 0.0) {
_speak("3");
}
}
}
if (_ballTick == 450) {
if (Model.countdown) {
_showOverlay(three: 0.0, two: 0.8);
if (Model.ttsEnabled && Model.ttsVolume > 0.0) {
_speak("2");
}
}
}
if (_ballTick == 400) {
if (Model.countdown) {
_showOverlay(two: 0.0, one: 0.8);
if (Model.ttsEnabled && Model.ttsVolume > 0.0) {
_speak("1");
}
}
}
if (_ballTick == 350) {
_showOverlay(one: 0.0);
if (Model.showNoMoreBets) {
_showOverlay(noMoreBets: 0.8);
}
if (Model.sayNoMoreBets) {
_speak(AppLocalizations.of(context)?.noMoreBets ?? 'no more bets');
}
}
if (_ballTick == 250) {
_showOverlay(noMoreBets: 0.0);
_ballTick -= (math.Random().nextDouble() * 100).toInt();
}
if (_ballTick < 5) {
_ballDistanceRatio = (_ballDistanceStart + _ballDistanceEnd) / 2;
}
if (_ballTick < 1) {
_ballDistanceRatio = _ballDistanceEnd;
}
if (_ballTick <= 0) {
_ballRotateFlag = false;
_setStartUiVisible(true);
_resultNumber();
_busy = false;
_playPocketSound();
if (Model.readOutResult) {
Future.delayed(const Duration(milliseconds: 800), () {
_speak(_speakTextForNumber(_uiResultText));
});
}
}
}
}
_ballPosition();
});
}
void _speak(String text) {
if (!_ttsAvailable) {
return;
}
if (Model.ttsEnabled && Model.ttsVolume > 0.0) {
TextToSpeech.speak(text);
}
}
void _onStart() {
if (_busy) {
return;
}
_busy = true;
_setStartUiVisible(false);
_showOverlay(resultAlpha: 0.0);
_ballAngle = 0;
_setBallVisible(true);
_ballDistanceRatio = _ballDistanceStart;
_wheelAngleStart = _wheelAngle;
_ballTick = (10 - Model.shortTime) * 100 + 260; // 1260..360
_adjustAngle = 0;
_ballRotateFlag = true;
}
void _resultNumber() {
double angle = _ballAngle;
angle = ((angle - _wheelAngleStart + _adjustAngle) / (360 / 37)).toInt() * (360 / 37) + (180 / 37);
angle = 180 - angle + 0.5;
angle += 3600;
angle %= 360;
int num = 37 - (angle / (360 / 37)).toInt();
num %= 37;
final slot = _slots[num];
final resultNumber = slot.number;
final resultColor = slot.color;
_setResult(number: resultNumber, color: resultColor);
if (Model.showResult) {
_showOverlay(resultAlpha: 1.0);
}
_addHistory(resultNumber, resultColor);
}
String _speakTextForNumber(String number) {
if (number == '00') {
return 'double zero';
}
return number;
}
void _addHistory(String number, String color) {
setState(() {
_history.insert(0, _HistoryItem(number, color));
if (_history.length > 20) {
_history.removeLast();
}
});
}
Future<void> _playPocketSound() async {
if (Model.soundVolume == 0.0) {
return;
}
try {
await _audio.setVolume(Model.soundVolume);
// Restart from beginning without reloading the asset
await _audio.seek(Duration.zero);
unawaited(_audio.resume());
} catch (_) {
// Fallback: try a direct play if preloading failed
try {
await _audio.stop();
await _audio.play(AssetSource('sound/kachi.wav')); //assets/は不要
} catch (_) {}
}
}
void _ballPosition() {
if (_baseSize <= 0) {
return;
}
final ballSize = _baseSize * _ballSizeRatio;
_setBallSize(ballSize.toInt());
double angle = _ballAngle;
if (!_ballRotateFlag) {
angle = ((angle - _wheelAngleStart + _adjustAngle) / (360 / 37)).toInt() * (360 / 37) + (180 / 37);
angle += _wheelAngle;
}
double x = (-math.sin(angle * (math.pi / 180)) * (_baseSize / 2));
double y = (math.cos(angle * (math.pi / 180)) * (_baseSize / 2));
x *= _ballDistanceRatio;
y *= _ballDistanceRatio;
x += _baseSize / 2.0 * 0.89;
y += _baseSize / 2.0 * 0.89;
x += ballSize * 0.9;
y += ballSize * 0.9;
_setBallPosition(x.toInt(), y.toInt());
}
// Channel helpers
Future<void> _setWheelRotation(double angle) async {
setState(() {
_uiWheelAngle = angle;
});
}
Future<void> _setBallPosition(int x, int y) async {
setState(() {
_uiBallLeft = x.toDouble();
_uiBallTop = y.toDouble();
});
}
Future<void> _setBallSize(int sizePx) async {
setState(() {
_uiBallSize = sizePx.toDouble();
});
}
Future<void> _setBallVisible(bool visible) async {
setState(() {
_uiBallVisible = visible;
});
}
Future<void> _showOverlay({double? three, double? two, double? one, double? noMoreBets, double? resultAlpha}) async {
setState(() {
if (three != null) { _uiAlphaThree = three; }
if (two != null) { _uiAlphaTwo = two; }
if (one != null) { _uiAlphaOne = one; }
if (noMoreBets != null) { _uiAlphaNoMoreBets = noMoreBets; }
if (resultAlpha != null) { _uiAlphaResult = resultAlpha; }
});
}
Future<void> _setResult({required String number, required String color}) async {
setState(() {
_uiResultText = number;
_uiResultColor = color;
});
}
void _setStartUiVisible(bool on) {
setState(() {
_startUiVisible = on;
_settingUiVisible = on;
});
}
Future<void> _openSetting() async {
final updated = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (_) => const SettingPage()),
);
if (mounted && updated == true) {
MainApp.of(context).rebuildApp();
_themeColor = ThemeColor(themeNumber: Model.themeNumber, context: context);
await _applyTtsPreferencesWithFallback();
_scheduleServiceIssueMessage();
_wakelock();
}
if (mounted) {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
if (!_isReady) {
return LoadingScreen();
}
final l = AppLocalizations.of(context)!;
return Scaffold(
backgroundColor: _themeColor.mainBackColor,
body: Stack(children:[
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [_themeColor.mainBackColor, _themeColor.mainBack2Color, _themeColor.mainBack2Color, _themeColor.mainBackColor],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
image: DecorationImage(
image: AssetImage('assets/image/tile.png'),
repeat: ImageRepeat.repeat,
opacity: 0.1,
),
),
),
SafeArea(
child: Column(
children: [
Row(children: [
const Spacer(),
Padding(
padding: const EdgeInsets.only(right: 10),
child: IconButton(
onPressed: _settingUiVisible ? _openSetting : null,
tooltip: l.setting,
icon: Icon(Icons.settings, color: Colors.white.withValues(alpha: _settingUiVisible ? 0.85 : 0)),
),
),
]),
const SizedBox(height: 5),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
final paddingH = 10.0;
final width = constraints.maxWidth - paddingH * 2;
_baseSize = width;
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: Column(
children: [
AspectRatio(
aspectRatio: 1,
child: Center(
child: WheelFlutterView(
size: width,
wheelAngleDeg: _uiWheelAngle,
ballVisible: _uiBallVisible,
ballLeft: _uiBallLeft,
ballTop: _uiBallTop,
ballSize: _uiBallSize,
alphaThree: _uiAlphaThree,
alphaTwo: _uiAlphaTwo,
alphaOne: _uiAlphaOne,
alphaNoMoreBets: _uiAlphaNoMoreBets,
alphaResult: _uiAlphaResult,
resultText: _uiResultText,
resultColor: _uiResultColor,
),
),
),
const SizedBox(height: 20),
Stack(
children: [
Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: 60,
height: 200,
child: _buildHistoryList(),
),
),
Align(
alignment: Alignment.center,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: _startUiVisible ? 1.0 : 0.0,
child: Opacity(
opacity: _busy ? 0.4 : 1.0,
child: ElevatedButton(
onPressed: _busy ? null : _onStart,
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
fixedSize: const Size(180, 180),
backgroundColor: _themeColor.mainButtonBackColor,
elevation: 0,
),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(l.start,
textAlign: TextAlign.center,
style: TextStyle(color: _themeColor.mainButtonForeColor, fontSize: 28),
),
),
),
),
),
),
)
],
),
const SizedBox(height: 200),
],
),
),
);
},
),
),
],
)
),
]),
bottomNavigationBar: MainApp.of(context).adManager != null
? AdBannerWidget(adManager: MainApp.of(context).adManager!)
: null,
);
}
Widget _buildHistoryList() {
if (_history.isEmpty) {
return SizedBox.shrink();
}
if (Model.showHistory == false) {
return SizedBox.shrink();
}
return ListView.separated(
reverse: false,
itemCount: _history.length,
separatorBuilder: (_, __) => const SizedBox(height: 1),
itemBuilder: (context, index) {
final item = _history[index];
return Container(
height: 19,
decoration: BoxDecoration(
color: colorFromCode(item.color),
borderRadius: BorderRadius.circular(20),
),
alignment: Alignment.center,
child: Text(
item.number,
style: const TextStyle(color: Colors.white, fontSize: 16),
),
);
},
);
}
}
class _HistoryItem {
final String number;
final String color;
_HistoryItem(this.number, this.color);
}
class _RouletteSlot {
final String number;
final String color;
const _RouletteSlot(this.number, this.color);
}
/// Copyright© ao-system, Inc.
import 'dart:math';
import 'package:flutter/material.dart';
class LoadingScreen extends StatefulWidget {
const LoadingScreen({super.key});
@override
State<LoadingScreen> createState() => _LoadingScreenState();
}
class _LoadingScreenState extends State<LoadingScreen> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
@override
void initState() {
super.initState();
final randomStart = Random().nextDouble();
_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 6),
value: randomStart,
)..repeat();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Color _rainbowColor(double value) {
final hue = _animationController.value * 360;
return HSVColor.fromAHSV(1, hue, 1, value).toColor();
}
@override
Widget build(BuildContext context) {
final barHeight = MediaQuery.of(context).size.height * 0.4;
return AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
final foreColor = _rainbowColor(1.0);
final backColor = _rainbowColor(0.08);
return Scaffold(
backgroundColor: backColor,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: barHeight,
child: RotatedBox(
quarterTurns: -1,
child: LinearProgressIndicator(
minHeight: 1,
valueColor: AlwaysStoppedAnimation(foreColor),
backgroundColor: Colors.transparent,
),
),
),
const SizedBox(height: 5),
Text(
'LOADING',
style: TextStyle(
color: foreColor,
fontSize: 18,
letterSpacing: 16,
),
),
const SizedBox(height: 5),
SizedBox(
height: barHeight,
child: RotatedBox(
quarterTurns: 1,
child: LinearProgressIndicator(
minHeight: 1,
valueColor: AlwaysStoppedAnimation(foreColor),
backgroundColor: Colors.transparent,
),
),
),
],
),
),
);
},
);
}
}
/// Copyright© ao-system, Inc.
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:roulettewheeleurope/l10n/app_localizations.dart';
import 'package:roulettewheeleurope/home_page.dart';
import 'package:roulettewheeleurope/model.dart';
import 'package:roulettewheeleurope/service_status.dart';
import 'package:roulettewheeleurope/theme_mode_number.dart';
import 'package:roulettewheeleurope/parse_locale_tag.dart';
import 'package:roulettewheeleurope/loading_screen.dart';
import 'package:roulettewheeleurope/ad_ump_status.dart';
import 'package:roulettewheeleurope/att_service.dart';
import 'package:roulettewheeleurope/ad_manager.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
//UI設定
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
statusBarColor: Colors.transparent,
systemNavigationBarContrastEnforced: false,
systemStatusBarContrastEnforced: false,
),
);
try {
await MobileAds.instance.initialize().timeout(const Duration(seconds: 5));
} on TimeoutException catch (error) {
//debugPrint('AdMob initialization timed out: $error');
ServiceStatus.record(
ServiceType.ads,
'A001', //'Ad display has been disabled (AdMob initialization timed out).',
);
} catch (error, stackTrace) {
//debugPrint('AdMob initialization failed: $error');
//debugPrint('$stackTrace');
ServiceStatus.record(
ServiceType.ads,
'A002', //'Ad display has been disabled (failed to initialize AdMob).',
);
}
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
static MainAppState of(BuildContext context) {
return context.findAncestorStateOfType<MainAppState>()!;
}
@override
State<MainApp> createState() => MainAppState();
}
class MainAppState extends State<MainApp> {
late final AdManager? adManager;
ThemeMode _themeMode = ThemeMode.system;
Locale? _locale;
bool _hasError = false;
bool _isReady = false;
@override
void initState() {
super.initState();
_initState();
}
void _initState() async {
try {
//ad
if (ServiceStatus.adsEnabled) {
adManager = AdManager();
} else {
adManager = null;
}
//アプリの基本データ
await Model.ensureReady();
//ATT
//iOSは「アプリがactive/resumed状態」でないとrequestTrackingがダイアログを出さず即座にnotDeterminedを返すため、ライフサイクルがresumedになるまで待つ。
//(iOSは「設定→トラッキング」でトグルを変えるとアプリプロセスをkillして再起動するので、起動時にgetTrackingStatusを読めば常に最新の値が手に入る)
if (!kIsWeb && Platform.isIOS) {
if (await _waitForResumed()) {
final attService = AttService();
//未決定(初回起動)のときだけダイアログ表示。既に決定済みならスキップ。
if (await attService.getTrackingStatus() == AttStatus.notDetermined) {
await attService.requestTracking();
}
}
}
//UMP(ATTの後)
final adUmpConsentController = AdUmpConsentController();
await adUmpConsentController.updateConsentInfo();
//Mobile Ads SDK(同意確定後)
await MobileAds.instance.initialize();
//自前の広告設定
await AdManager.initForNPA();
//UI更新
if (mounted) {
setState(() {
_themeMode = ThemeModeNumber.numberToThemeMode(Model.themeNumber);
_locale = parseLocaleTag(Model.languageCode);
_isReady = true;
});
}
} catch (e) {
if (mounted) {
setState(() {
_hasError = true;
});
}
}
}
@override
void dispose() {
adManager?.dispose();
super.dispose();
}
//アプリがactive/resumed状態になるまで待つ。すでにresumedならすぐにtrueを返す。タイムアウト時はfalse。
Future<bool> _waitForResumed({
Duration timeout = const Duration(seconds: 5),
}) async {
final binding = WidgetsBinding.instance;
if (binding.lifecycleState == AppLifecycleState.resumed) {
return true;
}
final completer = Completer<bool>();
late final AppLifecycleListener listener;
listener = AppLifecycleListener(
onStateChange: (state) {
if (state == AppLifecycleState.resumed && !completer.isCompleted) {
completer.complete(true);
}
},
);
try {
return await completer.future.timeout(timeout, onTimeout: () => false);
} finally {
listener.dispose();
}
}
void rebuildApp() {
setState(() {
_themeMode = ThemeModeNumber.numberToThemeMode(Model.themeNumber);
_locale = parseLocaleTag(Model.languageCode);
});
}
Color _getRainbowAccentColor(int hue) {
return HSVColor.fromAHSV(1.0, hue.toDouble(), 1.0, 1.0).toColor();
}
ThemeData _createTheme(Brightness brightness, Color seed) {
final colorScheme = ColorScheme.fromSeed(seedColor: seed, brightness: brightness);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
appBarTheme: const AppBarTheme(backgroundColor: Colors.transparent),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
sliderTheme: SliderThemeData(
showValueIndicator: ShowValueIndicator.onDrag,
valueIndicatorTextStyle: TextStyle(
color: brightness == Brightness.light ? Colors.white : Colors.black,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
side: BorderSide(color: colorScheme.primary),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
);
}
@override
Widget build(BuildContext context) {
if (_hasError) {
return _buildErrorMessage();
}
final seed = _getRainbowAccentColor(Model.schemeColor);
return MaterialApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: _locale,
themeMode: _themeMode,
theme: _createTheme(Brightness.light, seed),
darkTheme: _createTheme(Brightness.dark, seed),
home: _isReady ? const MainHomePage() : const Scaffold(body: LoadingScreen()),
);
}
Widget _buildErrorMessage() {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Padding(
padding: EdgeInsets.all(24.0),
child: Text(
'Initialization failed. Please restart the app.',
textAlign: TextAlign.center,
),
),
),
),
);
}
}
/// Copyright© ao-system, Inc.
import 'dart:ui' as ui;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:roulettewheeleurope/l10n/app_localizations.dart';
class Model {
Model._();
static const String _prefShortTime = "shortTime";
static const String _prefShowNoMoreBets = "showNoMoreBets";
static const String _prefSayNoMoreBets = "sayNoMoreBets";
static const String _prefShowResult = "showResult";
static const String _prefReadOutResult = "readOutResult";
static const String _prefShowHistory = "showHistory";
static const String _prefCountdown = "countdown";
static const String _prefSoundVolume = "soundVolume";
static const String _prefTtsEnabled = "ttsEnabled";
static const String _prefTtsVolume = "ttsVolume";
static const String _prefTtsVoiceId = "ttsVoiceId";
static const String _prefWakelockEnabled = 'wakelockEnabled';
static const String _prefSchemeColor = 'schemeColor';
static const String _prefThemeNumber = "themeNumber";
static const String _prefLanguageCode = "languageCode";
static bool _ready = false;
static int _shortTime = 0;
static bool _showNoMoreBets = true;
static bool _sayNoMoreBets = true;
static bool _showResult = true;
static bool _readOutResult = true;
static bool _showHistory = true;
static bool _countdown = false;
static double _soundVolume = 0.5;
static bool _ttsEnabled = true;
static double _ttsVolume = 1.0;
static String _ttsVoiceId = '';
static bool _wakelockEnabled = false;
static int _schemeColor = 120;
static int _themeNumber = 0;
static String _languageCode = '';
static int get shortTime => _shortTime;
static bool get showNoMoreBets => _showNoMoreBets;
static bool get sayNoMoreBets => _sayNoMoreBets;
static bool get showResult => _showResult;
static bool get readOutResult => _readOutResult;
static bool get showHistory => _showHistory;
static bool get countdown => _countdown;
static double get soundVolume => _soundVolume;
static bool get ttsEnabled => _ttsEnabled;
static double get ttsVolume => _ttsVolume;
static String get ttsVoiceId => _ttsVoiceId;
static bool get wakelockEnabled => _wakelockEnabled;
static int get schemeColor => _schemeColor;
static int get themeNumber => _themeNumber;
static String get languageCode => _languageCode;
static Future<void> ensureReady() async {
if (_ready) {
return;
}
final prefs = await SharedPreferences.getInstance();
//
_shortTime = (prefs.getInt(_prefShortTime) ?? 0).clamp(0, 10);
_showNoMoreBets = prefs.getBool(_prefShowNoMoreBets) ?? true;
_sayNoMoreBets = prefs.getBool(_prefSayNoMoreBets) ?? true;
_showResult = prefs.getBool(_prefShowResult) ?? true;
_readOutResult = prefs.getBool(_prefReadOutResult) ?? true;
_showHistory = prefs.getBool(_prefShowHistory) ?? true;
_countdown = prefs.getBool(_prefCountdown) ?? false;
_soundVolume = (prefs.getDouble(_prefSoundVolume) ?? 0.5).clamp(0.0, 1.0);
_ttsEnabled = prefs.getBool(_prefTtsEnabled) ?? true;
_ttsVolume = (prefs.getDouble(_prefTtsVolume) ?? 1.0).clamp(0.0, 1.0);
_ttsVoiceId = prefs.getString(_prefTtsVoiceId) ?? '';
_wakelockEnabled = prefs.getBool(_prefWakelockEnabled) ?? false;
_schemeColor = (prefs.getInt(_prefSchemeColor) ?? 120).clamp(0, 360);
_themeNumber = (prefs.getInt(_prefThemeNumber) ?? 0).clamp(0, 2);
_languageCode = prefs.getString(_prefLanguageCode) ?? ui.PlatformDispatcher.instance.locale.languageCode;
_languageCode = _resolveLanguageCode(_languageCode);
_ready = true;
}
static String _resolveLanguageCode(String code) {
final supported = AppLocalizations.supportedLocales;
if (supported.any((l) => l.languageCode == code)) {
return code;
} else {
return '';
}
}
static Future<void> setShortTime(int value) async {
_shortTime = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setInt(_prefShortTime, value);
}
static Future<void> setSayNoMoreBets(bool value) async {
_sayNoMoreBets = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefSayNoMoreBets, value);
}
static Future<void> setShowNoMoreBets(bool value) async {
_showNoMoreBets = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefShowNoMoreBets, value);
}
static Future<void> setShowResult(bool value) async {
_showResult = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefShowResult, value);
}
static Future<void> setReadOutResult(bool value) async {
_readOutResult = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefReadOutResult, value);
}
static Future<void> setShowHistory(bool value) async {
_showHistory = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefShowHistory, value);
}
static Future<void> setCountdown(bool value) async {
_countdown = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefCountdown, value);
}
static Future<void> setSoundVolume(double value) async {
_soundVolume = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_prefSoundVolume, value);
}
static Future<void> setTtsEnabled(bool value) async {
_ttsEnabled = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefTtsEnabled, value);
}
static Future<void> setTtsVolume(double value) async {
_ttsVolume = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_prefTtsVolume, value);
}
static Future<void> setTtsVoiceId(String value) async {
_ttsVoiceId = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefTtsVoiceId, value);
}
static Future<void> setWakelockEnabled(bool value) async {
_wakelockEnabled = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefWakelockEnabled, value);
}
static Future<void> setSchemeColor(int value) async {
_schemeColor = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setInt(_prefSchemeColor, 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);
}
}
/// Copyright© ao-system, Inc.
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,
);
}
/// Copyright© ao-system, Inc.
import 'package:flutter/material.dart';
import 'package:roulettewheeleurope/l10n/app_localizations.dart';
enum ServiceType { ads, tts }
class _ServiceIssue {
final ServiceType type;
final String detail;
const _ServiceIssue(this.type, this.detail);
}
class ServiceStatus {
ServiceStatus._();
static final Map<ServiceType, _ServiceIssue> _issues = <ServiceType, _ServiceIssue>{};
static bool _dialogVisible = false;
static void record(ServiceType type, String detail) {
_issues[type] = _ServiceIssue(type, detail);
}
static bool get adsEnabled => !_issues.containsKey(ServiceType.ads);
static bool get ttsEnabled => !_issues.containsKey(ServiceType.tts);
static bool get hasIssues => _issues.isNotEmpty;
static String buildIssueMessage(AppLocalizations l) {
if (!hasIssues) {
return '';
}
final buffer = StringBuffer(l.serviceLimitDialogMessage);
for (final issue in _issues.values) {
if (issue.detail.isEmpty) {
continue;
}
buffer.writeln();
buffer.write('- ${issue.detail}');
}
return buffer.toString();
}
static Future<void> showIssuesMessage(BuildContext context) async {
if (!hasIssues || _dialogVisible) {
return;
}
final l = AppLocalizations.of(context)!;
final message = buildIssueMessage(l);
if (message.isEmpty) {
return;
}
_dialogVisible = true;
try {
await showDialog<void>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Text(l.serviceLimitDialogTitle),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(l.serviceLimitDialogConfirm),
),
],
);
},
);
} finally {
_dialogVisible = false;
}
}
}
/// Copyright© ao-system, Inc.
import 'package:flutter/material.dart';
import 'package:roulettewheeleurope/theme_color.dart';
import 'package:roulettewheeleurope/model.dart';
/// 設定画面専用のカスタムCardウィジェット
class SettingCard extends StatelessWidget {
final Widget child;
final ShapeBorder shape;
final EdgeInsetsGeometry margin;
const SettingCard({
super.key,
required this.child,
this.margin = const EdgeInsets.only(left: 0, top: 12, right: 0, bottom: 0),
}) : shape = const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
);
const SettingCard.top({
super.key,
required this.child,
this.margin = const EdgeInsets.only(left: 0, top: 12, right: 0, bottom: 0),
}) : shape = const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
bottomLeft: Radius.circular(0),
bottomRight: Radius.circular(0),
),
);
const SettingCard.flat({
super.key,
required this.child,
this.margin = const EdgeInsets.only(left: 0, top: 2, right: 0, bottom: 0),
}) : shape = const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(0),
topRight: Radius.circular(0),
bottomLeft: Radius.circular(0),
bottomRight: Radius.circular(0),
),
);
const SettingCard.bottom({
super.key,
required this.child,
this.margin = const EdgeInsets.only(left: 0, top: 2, right: 0, bottom: 0),
}) : shape = const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(0),
topRight: Radius.circular(0),
bottomLeft: Radius.circular(12),
bottomRight: Radius.circular(12),
),
);
@override
Widget build(BuildContext context) {
final themeColor = ThemeColor(
themeNumber: Model.themeNumber,
context: context,
);
return SizedBox(
width: double.infinity,
child: Card(
elevation: 0,
margin: margin,
surfaceTintColor: Colors.transparent,
shadowColor: Colors.transparent,
color: themeColor.cardColor,
shape: shape,
child: child,
),
);
}
}
/// Copyright© ao-system, Inc.
import "dart:async";
import "dart:io";
import "package:app_settings/app_settings.dart";
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:in_app_review/in_app_review.dart';
import "package:roulettewheeleurope/setting_card.dart";
import 'package:roulettewheeleurope/l10n/app_localizations.dart';
import 'package:roulettewheeleurope/model.dart';
import 'package:roulettewheeleurope/text_to_speech.dart';
import 'package:roulettewheeleurope/ad_banner_widget.dart';
import 'package:roulettewheeleurope/ad_ump_status.dart';
import 'package:roulettewheeleurope/loading_screen.dart';
import 'package:roulettewheeleurope/_secrets.dart';
import 'package:roulettewheeleurope/main.dart';
import 'package:roulettewheeleurope/att_service.dart';
class SettingPage extends StatefulWidget {
const SettingPage({super.key});
@override
State<SettingPage> createState() => _SettingPageState();
}
class _SettingPageState extends State<SettingPage> {
AdUmpState _adUmpState = AdUmpState.initial;
late final AdUmpService _adUmpService;
final _inAppReview = InAppReview.instance;
bool _wakelockEnabled = true;
int _schemeColor = 0;
Color _accentColor = Colors.red;
int _themeNumber = 0;
String _languageCode = '';
bool _isReady = false;
//
int _shortTime = 0;
bool _showNoMoreBets = true;
bool _sayNoMoreBets = true;
bool _showResult = true;
bool _readOutResult = true;
bool _showHistory = true;
bool _countdown = false;
double _soundVolume = 0.5;
bool _ttsEnabled = true;
String _ttsVoiceId = '';
double _ttsVolume = 1.0;
@override
void initState() {
super.initState();
_initState();
}
void _initState() async {
//ump
_adUmpService = AdUmpService();
await _refreshConsentInfo();
//model
_shortTime = Model.shortTime;
_showNoMoreBets = Model.showNoMoreBets;
_sayNoMoreBets = Model.sayNoMoreBets;
_showResult = Model.showResult;
_readOutResult = Model.readOutResult;
_showHistory = Model.showHistory;
_countdown = Model.countdown;
_soundVolume = Model.soundVolume;
_ttsEnabled = Model.ttsEnabled;
_ttsVoiceId = Model.ttsVoiceId;
_ttsVolume = Model.ttsVolume;
_wakelockEnabled = Model.wakelockEnabled;
_schemeColor = Model.schemeColor;
_accentColor = _getRainbowAccentColor(_schemeColor);
_themeNumber = Model.themeNumber;
_languageCode = Model.languageCode;
//speech
await TextToSpeech.applyPreferences(Model.ttsVoiceId,Model.ttsVolume);
setState(() {
_isReady = true;
});
}
@override
void dispose() {
unawaited(TextToSpeech.stop());
super.dispose();
}
Future<void> _refreshConsentInfo() async {
final AdUmpState newState = await _adUmpService.updateConsentInfo(_adUmpState);
if (mounted) {
setState(() { _adUmpState = newState; });
}
}
Color _getRainbowAccentColor(int hue) {
return HSVColor.fromAHSV(1.0, hue.toDouble(), 1.0, 1.0).toColor();
}
void _onApply() async {
FocusScope.of(context).unfocus();
await Model.setShortTime(_shortTime);
await Model.setShowNoMoreBets(_showNoMoreBets);
await Model.setSayNoMoreBets(_sayNoMoreBets);
await Model.setShowResult(_showResult);
await Model.setReadOutResult(_readOutResult);
await Model.setShowHistory(_showHistory);
await Model.setCountdown(_countdown);
await Model.setSoundVolume(_soundVolume);
await Model.setTtsEnabled(_ttsEnabled);
await Model.setTtsVolume(_ttsVolume);
await Model.setTtsVoiceId(_ttsVoiceId);
await Model.setWakelockEnabled(_wakelockEnabled);
await Model.setSchemeColor(_schemeColor);
await Model.setThemeNumber(_themeNumber);
await Model.setLanguageCode(_languageCode);
if (!mounted) {
return;
}
Navigator.of(context).pop(true);
}
@override
Widget build(BuildContext context) {
if (!_isReady) {
return LoadingScreen();
}
final l = AppLocalizations.of(context)!;
final TextTheme t = Theme.of(context).textTheme;
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: AppBar(
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.close),
tooltip: l.cancel,
onPressed: () => Navigator.of(context).pop(),
),
title: null,
actions: [
Padding(
padding: const EdgeInsets.only(right: 10),
child: IconButton(
onPressed: _onApply,
tooltip: l.apply,
icon: const Icon(Icons.check),
),
),
],
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildShortTime(l, t),
_buildShowNoMoreBets(l, t),
_buildSayNoMoreBets(l, t),
_buildShowResult(l, t),
_buildReadOutResult(l, t),
_buildShowHistory(l, t),
_buildCountdown(l, t),
_buildSoundVolume(l, t),
_buildSpeechSettings(l, t),
_buildWakelockEnabled(l, t),
_buildSchemeColor(l, t),
_buildTheme(l, t),
_buildLanguage(l, t),
_buildReview(l, t),
_buildCmp(l, t),
_buildAtt(l, t),
],
),
),
),
],
),
),
bottomNavigationBar: MainApp.of(context).adManager != null
? AdBannerWidget(adManager: MainApp.of(context).adManager!)
: null,
);
}
Widget _buildShortTime(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l.shortTime, style: t.bodyMedium),
subtitle: Row(
children: [
Text(_shortTime.toStringAsFixed(0)),
const SizedBox(width: 12),
Expanded(
child: Slider(
value: _shortTime.toDouble(),
min: 0,
max: 10,
divisions: 10,
label: _shortTime.toStringAsFixed(0),
onChanged: (value) {
setState(() {
_shortTime = value.toInt();
});
},
),
),
],
),
),
);
}
Widget _buildShowNoMoreBets(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l.showNoMoreBets, style: t.bodyMedium),
trailing: Switch(
value: _showNoMoreBets,
onChanged: (value) {
setState(() {
_showNoMoreBets = value;
});
},
),
),
);
}
Widget _buildSayNoMoreBets(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l.sayNoMoreBets, style: t.bodyMedium),
trailing: Switch(
value: _sayNoMoreBets,
onChanged: (value) {
setState(() {
_sayNoMoreBets = value;
});
},
),
),
);
}
Widget _buildShowResult(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l.showResult, style: t.bodyMedium),
trailing: Switch(
value: _showResult,
onChanged: (value) {
setState(() {
_showResult = value;
});
},
),
),
);
}
Widget _buildReadOutResult(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l.readOutResult, style: t.bodyMedium),
trailing: Switch(
value: _readOutResult,
onChanged: (value) {
setState(() {
_readOutResult = value;
});
},
),
),
);
}
Widget _buildShowHistory(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l.showHistory, style: t.bodyMedium),
trailing: Switch(
value: _showHistory,
onChanged: (value) {
setState(() {
_showHistory = value;
});
},
),
),
);
}
Widget _buildCountdown(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l.countdown, style: t.bodyMedium),
trailing: Switch(
value: _countdown,
onChanged: (value) {
setState(() {
_countdown = value;
});
},
),
),
);
}
Widget _buildSoundVolume(AppLocalizations l, TextTheme t) {
return SettingCard(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Text(l.soundVolume),
const Spacer(),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: <Widget>[
Text(_soundVolume.toStringAsFixed(1)),
Expanded(
child: Slider(
value: _soundVolume,
min: 0.0,
max: 1.0,
divisions: 10,
label: _soundVolume.toStringAsFixed(1),
onChanged: (double value) {
setState(() {
_soundVolume = double.parse(value.toStringAsFixed(1));
});
}
),
),
],
),
),
],
)
);
}
Widget _buildSpeechSettings(AppLocalizations l, TextTheme t) {
if (TextToSpeech.ttsVoices.isEmpty) {
return SizedBox.shrink();
}
return Column(
children: [
SettingCard.top(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: Text(l.ttsEnabled),
),
Switch(
value: _ttsEnabled,
onChanged: (bool value) {
setState(() {
_ttsEnabled = value;
});
},
),
],
),
)
),
SettingCard.flat(
child: Column(
children:[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
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,
label: _ttsVolume.toStringAsFixed(1),
onChanged: _ttsEnabled
? (double value) {
setState(() {
_ttsVolume = double.parse(value.toStringAsFixed(1));
});
}
: null,
),
),
],
),
),
]
)
),
SettingCard.bottom(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: null,
subtitle: DropdownButtonFormField<String>(
initialValue: () {
if (_ttsVoiceId.isNotEmpty && TextToSpeech.ttsVoices.any((o) => o.id == _ttsVoiceId)) {
return _ttsVoiceId;
}
return TextToSpeech.ttsVoices.first.id;
}(),
items: TextToSpeech.buildGroupedItems(),
onChanged: (v) {
if (v == null) {
return;
}
setState(() => _ttsVoiceId = v);
},
),
),
)
]
);
}
Widget _buildWakelockEnabled(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l.wakelockEnabled, style: t.bodyMedium),
trailing: Switch(
value: _wakelockEnabled,
onChanged: (value) {
setState(() {
_wakelockEnabled = value;
});
},
),
),
);
}
Widget _buildSchemeColor(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l.colorScheme, style: t.bodyMedium),
subtitle: Row(
children: [
Text(_schemeColor.toStringAsFixed(0)),
const SizedBox(width: 12),
Expanded(
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: _accentColor,
inactiveTrackColor: _accentColor.withValues(alpha: 0.3),
thumbColor: _accentColor,
overlayColor: _accentColor.withValues(alpha: 0.2),
valueIndicatorColor: _accentColor,
),
child: Slider(
value: _schemeColor.toDouble(),
min: 0,
max: 360,
divisions: 360,
label: _schemeColor.toString(),
onChanged: (value) {
setState(() {
_schemeColor = value.toInt();
_accentColor = _getRainbowAccentColor(_schemeColor);
});
},
),
),
),
],
),
),
);
}
Widget _buildTheme(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
minVerticalPadding: 0,
title: Text(l.theme, style: t.bodyMedium),
trailing: 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, TextTheme t) {
final Map<String,String> languageNames = {
'af': 'af: Afrikaans',
'ar': 'ar: العربية',
'bg': 'bg: Български',
'bn': 'bn: বাংলা',
'bs': 'bs: Bosanski',
'ca': 'ca: Català',
'cs': 'cs: Čeština',
'da': 'da: Dansk',
'de': 'de: Deutsch',
'el': 'el: Ελληνικά',
'en': 'en: English',
'es': 'es: Español',
'et': 'et: Eesti',
'fa': 'fa: فارسی',
'fi': 'fi: Suomi',
'fil': 'fil: Filipino',
'fr': 'fr: Français',
'gu': 'gu: ગુજરાતી',
'he': 'he: עברית',
'hi': 'hi: हिन्दी',
'hr': 'hr: Hrvatski',
'hu': 'hu: Magyar',
'id': 'id: Bahasa Indonesia',
'it': 'it: Italiano',
'ja': 'ja: 日本語',
//'jv': 'jv: Basa Jawa', //flutterのサポート外
'km': 'km: ខ្មែរ',
'kn': 'kn: ಕನ್ನಡ',
'ko': 'ko: 한국어',
'lt': 'lt: Lietuvių',
'lv': 'lv: Latviešu',
'ml': 'ml: മലയാളം',
'mr': 'mr: मराठी',
'ms': 'ms: Bahasa Melayu',
'my': 'my: မြန်မာ',
'ne': 'ne: नेपाली',
'nl': 'nl: Nederlands',
'or': 'or: ଓଡ଼ିଆ',
'pa': 'pa: ਪੰਜਾਬੀ',
'pl': 'pl: Polski',
'pt': 'pt: Português',
'ro': 'ro: Română',
'ru': 'ru: Русский',
'si': 'si: සිංහල',
'sk': 'sk: Slovenčina',
'sr': 'sr: Српски',
'sv': 'sv: Svenska',
'sw': 'sw: Kiswahili',
'ta': 'ta: தமிழ்',
'te': 'te: తెలుగు',
'th': 'th: ไทย',
'tl': 'tl: Tagalog',
'tr': 'tr: Türkçe',
'uk': 'uk: Українська',
'ur': 'ur: اردو',
'uz': 'uz: Oʻzbekcha',
'vi': 'vi: Tiếng Việt',
'zh': 'zh: 中文',
'zu': 'zu: isiZulu',
};
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
minVerticalPadding: 0,
title: Text(l.language, style: t.bodyMedium),
trailing: 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 _buildReview(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l.reviewApp, style: t.bodyMedium),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 8),
OutlinedButton.icon(
icon: const Icon(Icons.open_in_new, size: 16),
label: Text(l.reviewStore, style: t.bodySmall),
onPressed: () async {
await _inAppReview.openStoreListing(
appStoreId: Secrets.appStoreId,
);
},
),
],
),
),
);
}
Widget _buildCmp(AppLocalizations l, TextTheme t) {
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 SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l.cmpSettingsTitle, style: t.bodyMedium),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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 (_adUmpState.consentStatus == ConsentStatus.obtained) ...[
const SizedBox(height: 6),
Text(l.cmpConsentStatusObtainedNote, style: t.bodySmall),
],
if (showButton) ...[
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _adUmpState.isChecking
? null
: () async {
try {
await _adUmpService.showPrivacyOptions();
} catch (e) {
//debugPrint('Privacy options error ignored: $e');
}
await _refreshConsentInfo();
},
icon: const Icon(Icons.settings),
label: Text(
_adUmpState.isChecking
? l.cmpConsentStatusChecking
: l.cmpOpenConsentSettings,
),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: _adUmpState.isChecking ? null : _refreshConsentInfo,
icon: const Icon(Icons.refresh),
label: Text(l.cmpRefreshStatus),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: () async {
final messenger = ScaffoldMessenger.of(context);
final message = l.cmpResetStatusDone;
await ConsentInformation.instance.reset();
if (!mounted) {
return;
}
setState(() {
_adUmpState = _adUmpState.copyWith(
consentStatus: ConsentStatus.unknown,
);
});
messenger.showSnackBar(SnackBar(content: Text(message)));
},
icon: const Icon(Icons.delete_sweep_outlined),
label: Text(l.cmpResetStatus),
),
],
],
),
),
],
),
),
);
}
Widget _buildAtt(AppLocalizations l, TextTheme t) {
if (kIsWeb || !Platform.isIOS) {
return const SizedBox.shrink();
}
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l.attSettingsTitle, style: t.bodyMedium),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Text(l.attDescription, style: t.bodySmall),
const SizedBox(height: 8),
FutureBuilder<AttStatus>(
future: AttService().getTrackingStatus(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Column(
children: [
Chip(
avatar: const Icon(Icons.hourglass_empty),
label: Text(l.attStatusChecking),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: null,
icon: const Icon(Icons.open_in_new),
label: Text(l.attOpenSettings),
),
],
),
);
}
final status = snapshot.data ?? AttStatus.unknown;
final label = status.name;
return Center(
child: Column(
children: [
Chip(
avatar: const Icon(Icons.track_changes),
label: Text('${l.attStatusLabel} $label'),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: () => AppSettings.openAppSettings(),
icon: const Icon(Icons.open_in_new, size: 16),
label: Text(l.attOpenSettings, style: t.bodySmall),
),
],
),
);
},
),
],
),
),
);
}
}
/// Copyright© ao-system, Inc.
/*
--- home_page.dart ---
import 'package:example/text_to_speech.dart';
class _MainHomePageState extends State<MainHomePage> {
Future<void> _initState() async {
await TextToSpeech.applyPreferences(Model.ttsVoiceId,Model.ttsVolume);
}
@override
void dispose() {
unawaited(TextToSpeech.stop());
}
void _ttsResult(String text) async {
if (Model.ttsEnabled && Model.ttsVolume > 0.0) {
await TextToSpeech.speak(text);
}
}
Future<void> _openSetting() async {
await TextToSpeech.applyPreferences(Model.ttsVoiceId,Model.ttsVolume);
}
}
*/
/*
--- setting_page.dart ---
import 'package:example/text_to_speech.dart';
class _SettingPageState extends State<SettingPage> {
bool _ttsEnabled = true;
String _ttsVoiceId = '';
double _ttsVolume = 1.0;
Future<void> _initialize() async {
//model
_ttsEnabled = Model.ttsEnabled;
_ttsVolume = Model.ttsVolume;
_ttsVoiceId = Model.ttsVoiceId;
//speech
await TextToSpeech.applyPreferences(Model.ttsVoiceId,Model.ttsVolume);
}
@override
void dispose() {
unawaited(TextToSpeech.stop());
}
Future<void> _onApply() async {
await Model.setTtsEnabled(_ttsEnabled);
await Model.setTtsVoiceId(_ttsVoiceId);
await Model.setTtsVolume(_ttsVolume);
}
Widget _buildSpeechSettings(AppLocalizations l, TextTheme t) {
if (TextToSpeech.ttsVoices.isEmpty) {
return SizedBox.shrink();
}
return Column(children:[
SettingCard.bottom(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: null,
subtitle: DropdownButtonFormField<String>(
initialValue: () {
if (_ttsVoiceId.isNotEmpty && TextToSpeech.ttsVoices.any((o) => o.id == _ttsVoiceId)) {
return _ttsVoiceId;
}
return TextToSpeech.ttsVoices.first.id;
}(),
items: TextToSpeech.buildGroupedItems(),
onChanged: (v) {
if (v == null) {
return;
}
setState(() => _ttsVoiceId = v);
},
),
),
)
]
}
}
*/
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart';
class TtsOption {
final String locale;
final String name;
const TtsOption(this.locale, this.name);
String get id => '$locale|$name';
String get label => '$locale $name';
}
class TextToSpeech {
static final FlutterTts _tts = FlutterTts();
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) {
_initialized = true;
await _instance!._initial();
}
return _instance!;
}
// 初期化:音声リスト作成
Future<void> _initial() async {
try {
final vs = await _tts.getVoices;
ttsVoices.clear();
if (vs is List) {
for (final v in vs) {
if (v is Map && v['name'] is String && v['locale'] is String) {
ttsVoices.add(TtsOption(v['locale']!, v['name']!));
}
}
}
// Default を定義(表示用)
const defaultVoice = TtsOption("Default", "");
if (ttsVoices.isEmpty) {
// 取得できなかった → Default のみ
ttsVoices.add(defaultVoice);
} else {
// Default を先頭に固定
ttsVoices.removeWhere((v) => v.id == defaultVoice.id);
ttsVoices.sort((a, b) => a.label.compareTo(b.label));
ttsVoices.insert(0, defaultVoice);
}
// voiceId が存在しない場合は Default
if (!ttsVoices.any((o) => o.id == ttsVoiceId)) {
ttsVoiceId = ttsVoices.first.id;
}
await _setSpeechVoiceFromId();
} catch (_) {}
}
// locale ごとにグループ化
static Map<String, List<TtsOption>> _groupedVoices() {
final Map<String, List<TtsOption>> grouped = {};
for (final v in ttsVoices) {
grouped.putIfAbsent(v.locale, () => []);
grouped[v.locale]!.add(v);
}
return grouped;
}
//DropdownMenuItemを生成(Dividerで区切る)
static List<DropdownMenuItem<String>> buildGroupedItems() {
final grouped = _groupedVoicesDefaultFirst();
final List<DropdownMenuItem<String>> items = [];
bool first = true;
grouped.forEach((locale, voices) {
// 最初のグループの前には Divider を入れない
if (!first) {
items.add(
const DropdownMenuItem<String>(
enabled: false,
value: null,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 1),
child: Divider(height: 1, thickness: 1),
),
),
);
}
first = false;
// グループ内の選択可能な項目
for (final v in voices) {
items.add(
DropdownMenuItem<String>(
value: v.id,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(v.label),
),
),
);
}
});
return items;
}
static Map<String, List<TtsOption>> _groupedVoicesSorted() {
final grouped = _groupedVoices();
// locale をアルファベット順にソート
final sortedKeys = grouped.keys.toList()..sort();
final Map<String, List<TtsOption>> sorted = {};
for (final key in sortedKeys) {
sorted[key] = grouped[key]!;
}
return sorted;
}
static Map<String, List<TtsOption>> _groupedVoicesDefaultFirst() {
final grouped = _groupedVoices();
// Default が存在しなければ通常のソート
if (!grouped.containsKey("Default")) {
return _groupedVoicesSorted();
}
// Default 以外の locale をソート
final otherKeys = grouped.keys.where((k) => k != "Default").toList()..sort();
final Map<String, List<TtsOption>> sorted = {};
// Default を最上段に
sorted["Default"] = grouped["Default"]!;
// 残りをアルファベット順に追加
for (final key in otherKeys) {
sorted[key] = grouped[key]!;
}
return sorted;
}
// ttsVoiceId を登録
static Future<void> _setTtsVoiceId(String newTtsVoiceId) async {
await _getInstance();
final exists = ttsVoices.any((o) => o.id == newTtsVoiceId);
ttsVoiceId = exists ? newTtsVoiceId : ttsVoices.first.id;
await _setSpeechVoiceFromId();
}
// 選択された voiceId を TTS に反映
static Future<void> _setSpeechVoiceFromId() async {
if (ttsVoices.isEmpty || ttsVoiceId.isEmpty) {
return;
}
final idx = ttsVoiceId.indexOf('|');
final locale = idx >= 0 ? ttsVoiceId.substring(0, idx) : '';
final name = idx >= 0 ? ttsVoiceId.substring(idx + 1) : ttsVoiceId;
// Default の場合 → デバイスのデフォルトロケールを使う
if (locale == "Default") {
final deviceLocale = PlatformDispatcher.instance.locale.toString(); //e.g. ja_JP
final ttsLocale = deviceLocale.replaceAll('_', '-'); //e.g. ja-JP
try {
await _tts.setLanguage(ttsLocale);
} catch (_) {}
return;
}
// 通常の処理
try {
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> _setVolume(double volume) async {
try {
await _tts.setVolume(volume);
} catch (_) {}
}
// 音声再生
static Future<void> speak(String text) async {
try {
await _getInstance();
await _tts.speak(text);
} catch (_) {}
}
// 停止
static Future<void> stop() async {
try {
await _tts.stop();
} 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 (_) {}
}
}
/// Copyright© ao-system, Inc.
import 'package:flutter/material.dart';
import 'package:roulettewheeleurope/model.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;
Color _getRainbowAccentColor(int hue, double saturation, double value) {
return HSVColor.fromAHSV(1.0, hue.toDouble(), saturation, value).toColor();
}
//main page
Color get mainBackColor => _isLight ? _getRainbowAccentColor(Model.schemeColor,1,0.4) : _getRainbowAccentColor(Model.schemeColor,1,0.1);
Color get mainBack2Color => _isLight ? _getRainbowAccentColor(Model.schemeColor,1,0.8) : _getRainbowAccentColor(Model.schemeColor,1,0.4);
Color get mainButtonBackColor => _isLight ? Color.fromRGBO(255,255,255,0.2) : Color.fromRGBO(0,0,0,0.3);
Color get mainButtonForeColor => _isLight ? Color.fromRGBO(255,255,255,1) : Color.fromRGBO(255,255,255,0.8);
//setting page
Color get backColor => _isLight ? Colors.grey[300]! : 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;
Color get borderColor => _isLight ? Colors.grey[300]! : Colors.grey[700]!;
Color get inputFillColor => _isLight ? Colors.grey[50]! : Colors.grey[900]!;
}
/// Copyright© ao-system, Inc.
import 'package:flutter/material.dart';
class ThemeModeNumber {
static ThemeMode numberToThemeMode(int value) {
switch (value) {
case 1:
return ThemeMode.light;
case 2:
return ThemeMode.dark;
default:
return ThemeMode.system;
}
}
}
/// Copyright© ao-system, Inc.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:roulettewheeleurope/color_utils.dart';
class WheelFlutterView extends StatelessWidget {
final double size;
final double wheelAngleDeg;
final bool ballVisible;
final double ballLeft;
final double ballTop;
final double ballSize;
final double alphaThree;
final double alphaTwo;
final double alphaOne;
final double alphaNoMoreBets;
final double alphaResult;
final String resultText;
final String resultColor; // 'g'|'k'|'r'
const WheelFlutterView({
super.key,
required this.size,
required this.wheelAngleDeg,
required this.ballVisible,
required this.ballLeft,
required this.ballTop,
required this.ballSize,
required this.alphaThree,
required this.alphaTwo,
required this.alphaOne,
required this.alphaNoMoreBets,
required this.alphaResult,
required this.resultText,
required this.resultColor,
});
@override
Widget build(BuildContext context) {
final wheelAngleRad = wheelAngleDeg * (math.pi / 180.0);
return SizedBox(
width: size,
height: size,
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
// Base
Positioned.fill(
child: Image.asset(
'assets/image/roulettewheel_base.png',
fit: BoxFit.contain,
filterQuality: FilterQuality.low,
),
),
// Top (rotating)
Positioned.fill(
child: Transform.rotate(
angle: wheelAngleRad,
child: Image.asset(
'assets/image/roulettewheel_top.png',
fit: BoxFit.contain,
filterQuality: FilterQuality.low,
),
),
),
// Ball
Positioned(
left: ballLeft,
top: ballTop,
child: Opacity(
opacity: ballVisible ? 1.0 : 0.0,
child: Image.asset(
'assets/image/ball.png',
width: ballSize,
height: ballSize,
fit: BoxFit.contain,
filterQuality: FilterQuality.low,
),
),
),
// Overlays 3,2,1, No more bets (full-size)
Positioned.fill(
child: IgnorePointer(
ignoring: true,
child: AnimatedOpacity(
opacity: alphaThree.clamp(0.0, 1.0),
duration: const Duration(milliseconds: 300),
child: Image.asset('assets/image/three.png', fit: BoxFit.contain),
),
),
),
Positioned.fill(
child: IgnorePointer(
ignoring: true,
child: AnimatedOpacity(
opacity: alphaTwo.clamp(0.0, 1.0),
duration: const Duration(milliseconds: 300),
child: Image.asset('assets/image/two.png', fit: BoxFit.contain),
),
),
),
Positioned.fill(
child: IgnorePointer(
ignoring: true,
child: AnimatedOpacity(
opacity: alphaOne.clamp(0.0, 1.0),
duration: const Duration(milliseconds: 300),
child: Image.asset('assets/image/one.png', fit: BoxFit.contain),
),
),
),
Positioned.fill(
child: IgnorePointer(
ignoring: true,
child: AnimatedOpacity(
opacity: alphaNoMoreBets.clamp(0.0, 1.0),
duration: const Duration(milliseconds: 300),
child: Image.asset('assets/image/nomorebets.png', fit: BoxFit.contain),
),
),
),
// Result text
Positioned.fill(
child: IgnorePointer(
ignoring: true,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: alphaResult.clamp(0.0, 1.0),
child: Align(
alignment: Alignment.topLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 3),
decoration: BoxDecoration(
color: colorFromCode(resultColor),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.white, width: 1),
),
child: Text(
resultText,
style: const TextStyle(
color: Colors.white,
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
],
),
);
}
}