name: gearcombination
description: "GearCombination"
publish_to: 'none'
version: 2.7.0+29
environment:
sdk: ^3.11.5
dependencies: #flutter pub upgrade --major-versions
flutter:
sdk: flutter
flutter_localizations: #flutter gen-l10n
sdk: flutter
intl: ^0.20.2
cupertino_icons: ^1.0.8
package_info_plus: ^10.1.0
shared_preferences: ^2.0.17
google_mobile_ads: ^8.0.0
just_audio: ^0.10.4
flutter_svg: ^2.0.9
wakelock_plus: ^1.4.0
in_app_review: ^2.0.11
app_tracking_transparency: ^2.0.4
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.3.5 #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: '#2A01AD'
image: 'assets/image/splash.png'
color_dark: '#2A01AD'
image_dark: 'assets/image/splash.png'
fullscreen: true
android_12:
icon_background_color: '#2A01AD'
image: 'assets/image/splash.png'
icon_background_color_dark: '#2A01AD'
image_dark: 'assets/image/splash.png'
flutter:
generate: true
uses-material-design: true
assets:
- assets/image/
- assets/sound/
import 'package:flutter/cupertino.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:gearcombination/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();
}
},
),
);
}
}
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:gearcombination/_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();
_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();
_retryAttempt = 0;
final cb = _onLoadedCb;
if (cb != null) {
cb();
}
},
onAdFailedToLoad: (ad, err) {
ad.dispose();
_scheduleRetry();
},
),
)..load();
}
void _scheduleRetry() {
if (kIsWeb) return;
_retryTimer?.cancel();
_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();
}
}
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:gearcombination/l10n/app_localizations.dart';
import 'package:gearcombination/_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;
}
}
}
import 'package:just_audio/just_audio.dart';
import 'package:gearcombination/const_value.dart';
class AudioPlay {
//音を重ねて連続再生できるようにインスタンスを用意しておき、順繰りに使う。
static final List<AudioPlayer> _playerJoin = [
AudioPlayer(),
AudioPlayer(),
AudioPlayer(),
];
static final List<AudioPlayer> _playerSlide = [
AudioPlayer(),
AudioPlayer(),
AudioPlayer(),
];
int _playerJoinPtr = 0;
int _playerSlidePtr = 0;
double _soundVolume = 0.0;
//constructor
AudioPlay() {
constructor();
}
void constructor() async {
for (int i = 0; i < _playerJoin.length; i++) {
await _playerJoin[i].setVolume(0);
await _playerJoin[i].setAsset(ConstValue.audioJoin);
}
for (int i = 0; i < _playerSlide.length; i++) {
await _playerSlide[i].setVolume(0);
await _playerSlide[i].setAsset(ConstValue.audioSlide);
}
playZero();
}
void dispose() {
for (int i = 0; i < _playerJoin.length; i++) {
_playerJoin[i].dispose();
}
for (int i = 0; i < _playerSlide.length; i++) {
_playerSlide[i].dispose();
}
}
//getter
double get soundVolume => _soundVolume;
//setter
set soundVolume(double vol) {
_soundVolume = vol;
}
//最初に音が鳴らないのを回避する方法
void playZero() async {
AudioPlayer ap = AudioPlayer();
await ap.setAsset(ConstValue.audioZero);
await ap.load();
await ap.play();
}
//
void playJoin() async {
if (_soundVolume == 0) {
return;
}
_playerJoinPtr += 1;
if (_playerJoinPtr >= _playerJoin.length) {
_playerJoinPtr = 0;
}
await _playerJoin[_playerJoinPtr].setVolume(_soundVolume * 0.7);
await _playerJoin[_playerJoinPtr].pause();
await _playerJoin[_playerJoinPtr].seek(Duration.zero);
await _playerJoin[_playerJoinPtr].play();
}
void playSlide() async {
if (_soundVolume == 0) {
return;
}
_playerSlidePtr += 1;
if (_playerSlidePtr >= _playerSlide.length) {
_playerSlidePtr = 0;
}
await _playerSlide[_playerSlidePtr].setVolume(_soundVolume);
await _playerSlide[_playerSlidePtr].pause();
await _playerSlide[_playerSlidePtr].seek(Duration.zero);
await _playerSlide[_playerSlidePtr].play();
}
}
class ConstValue {
ConstValue._();
//image
static const String imageBack1 = 'assets/image/back1.svg';
static const String imageSpace1 = 'assets/image/space1.webp';
static const List<String> imageBackGrounds = [
'assets/image/bg1.webp', //0 dummy
'assets/image/bg1.webp', //1
'assets/image/bg2.webp', //2
'assets/image/bg3.webp',
'assets/image/bg4.webp',
'assets/image/bg5.webp',
'assets/image/bg6.webp',
'assets/image/bg7.webp',
'assets/image/bg8.webp',
'assets/image/bg9.webp',
'assets/image/bg10.webp',
];
//sound
static const String audioZero = 'assets/sound/zero.wav'; //効果音1個
static const String audioSlide = 'assets/sound/slide.mp3';
static const String audioJoin = 'assets/sound/set.wav';
}
enum CurrentState {
normal,
flash,
clear,
}
class Game {
//main.dartとstage.dartとのデータ受け渡しで使用
//現在のクエスト番号
int currentQuestNumber = 0;
}
import 'package:flutter/cupertino.dart';
class GearOne {
String name; //ギア名
String src; //画像名
double width; //幅
Widget image; //生成したWidget imageを保持。使いまわすため
int teeth1; //歯の数外側
int teeth2; //歯の数内側
Widget widget; //生成したWidgetを保持。
double degrees; //回転 0..180..360
double ratio; //回転率。停止時は0
double left; //配置位置
double top; //配置位置
int stack; //重ね順(z-indexの役目)
//constructor
GearOne(this.name, this.src, this.width, this.image, this.teeth1, this.teeth2, this.widget, this.degrees, this.ratio, this.left, this.top, this.stack);
GearOne clone() {
return GearOne(name, src, width, image, teeth1, teeth2, widget, degrees, ratio, left, top, stack);
}
int compareStack(GearOne other) {
return stack.compareTo(other.stack);
}
}
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:gearcombination/model.dart';
import 'package:gearcombination/gear_one.dart';
import 'package:gearcombination/audio_play.dart';
import 'package:gearcombination/current_state.dart';
import 'package:gearcombination/theme_color.dart';
class Gears {
//各ギア
final List<GearOne> _gears = [
GearOne('16', 'assets/image/g16.svg', 145, Container(), 16, 0, Container(), 0, 0, 0,0, 24),
GearOne('18', 'assets/image/g18.svg', 161, Container(), 18, 0, Container(), 0, 0, 0,0, 23),
GearOne('20', 'assets/image/g20.svg', 177, Container(), 20, 0, Container(), 0, 0, 0,0, 22),
GearOne('22', 'assets/image/g22.svg', 193, Container(), 22, 0, Container(), 0, 0, 0,0, 21),
GearOne('24', 'assets/image/g24.svg', 209, Container(), 24, 0, Container(), 0, 0, 0,0, 20),
GearOne('26', 'assets/image/g26.svg', 225, Container(), 26, 0, Container(), 0, 0, 0,0, 19),
GearOne('28', 'assets/image/g28.svg', 241, Container(), 28, 0, Container(), 0, 0, 0,0, 18),
GearOne('30', 'assets/image/g30.svg', 257, Container(), 30, 0, Container(), 0, 0, 0,0, 17),
GearOne('32', 'assets/image/g32.svg', 273, Container(), 32, 0, Container(), 0, 0, 0,0, 16),
GearOne('32b','assets/image/g32_16.svg', 273, Container(), 32, 16, Container(), 0, 0, 0,0, 15),
GearOne('34', 'assets/image/g34.svg', 289, Container(), 34, 0, Container(), 0, 0, 0,0, 14),
GearOne('36', 'assets/image/g36.svg', 305, Container(), 36, 0, Container(), 0, 0, 0,0, 13),
GearOne('36b','assets/image/g36_18.svg', 305, Container(), 36, 18, Container(), 0, 0, 0,0, 12),
GearOne('38', 'assets/image/g38.svg', 321, Container(), 38, 0, Container(), 0, 0, 0,0, 11),
GearOne('40', 'assets/image/g40.svg', 337, Container(), 40, 0, Container(), 0, 0, 0,0, 10),
GearOne('40b','assets/image/g40_20.svg', 337, Container(), 40, 20, Container(), 0, 0, 0,0, 9),
GearOne('42', 'assets/image/g42.svg', 352, Container(), 42, 0, Container(), 0, 0, 0,0, 8),
GearOne('42b','assets/image/g42_21.svg', 352, Container(), 42, 21, Container(), 0, 0, 0,0, 7),
GearOne('44', 'assets/image/g44.svg', 368, Container(), 44, 0, Container(), 0, 0, 0,0, 6),
GearOne('44b','assets/image/g44_22.svg', 368, Container(), 44, 22, Container(), 0, 0, 0,0, 5),
GearOne('46', 'assets/image/g46.svg', 385, Container(), 46, 0, Container(), 0, 0, 0,0, 4),
GearOne('46b','assets/image/g46_23.svg', 385, Container(), 46, 23, Container(), 0, 0, 0,0, 3),
GearOne('48', 'assets/image/g48.svg', 401, Container(), 48, 0, Container(), 0, 0, 0,0, 2),
GearOne('48b','assets/image/g48_16.svg', 401, Container(), 48, 16, Container(), 0, 0, 0,0, 1),
GearOne('50', 'assets/image/g50.svg', 417, Container(), 50, 0, Container(), 0, 0, 0,0, 0),
];
//クエストで左上に配置されるギア
final Map<int,int> _questionGearSelect = {
0:0,
1:0,
2:0,
3:0,
4:6,
5:6,
6:6,
7:6,
8:10,
9:10,
10:10,
11:10,
12:10,
13:10,
14:16,
15:22,
16:22,
17:22,
18:22,
19:22,
20:22,
21:22,
22:13,
23:13,
24:13,
25:13,
26:13,
27:13,
28:13,
29:13,
30:13,
31:13,
32:13,
33:13,
34:13,
35:13,
36:13,
37:13,
38:13,
39:13,
40:13,
41:0,
42:0,
43:0,
44:0,
45:0,
46:0,
47:0,
48:0,
49:0,
50:0,
51:0,
52:0,
};
//回答
final Map<int,double> _answers = {
5:-1,
6:1,
7:2,
8:0.5,
9:-0.25,
10:0.25,
11:-5.1,
12:17,
13:-17,
14:1.5,
15:0.4,
16:-0.4,
17:36,
18:-36,
19:-3,
20:7.2,
21:-6,
22:-0.151,
23:-0.14,
24:-28.5,
25:19,
26:57,
27:15.2,
28:114,
29:-19,
30:11.4,
31:22.8,
32:14.25,
33:9.5,
34:-15.2,
35:6,
36:-3,
37:-5.7,
38:28.5,
39:3,
40:456,
41:-0.4,
42:-0.32,
43:-0.5,
44:0.4,
45:0.32,
46:0.5,
47:-0.8,
48:0.8,
49:1.6,
50:-1.6,
51:-3.2,
52:3.2,
};
final double _adjustTeethRatioBase1 = 2.87; //歯の数から噛み合う位置を求める
final double _adjustTeethRatioBase2 = 3.44; //歯の数から噛み合う位置を求める
double _adjustTeethRatio = 4.45; //歯の数から噛み合う位置を求める
double _devicePixelRatio = 1.0; //MediaQuery.of(context).devicePixelRatio
double _stageRatio = 1.0; //横幅を900とした場合の比率
double _magnificationRate = 1.0; //stage全体の拡大率
late AudioPlay _audioPlay;
late GearOne _rootGear; //左上に配置されるギア
GearOne? _lastGear = null; //ギア接続の最後
int _lastRotationGearCount = 0; //直前のギア接続数
bool _connectedFlash = false; //ギア接続でフラッシュする場合はtrue
double _fineTuningGearEngagement = 0.0; //ギア噛み合わせ微調整
CurrentState _headerColorMode = CurrentState.normal;
int _currentQuestNumber = 0; //クエスト番号
bool _isReady = false; //全ての状態が整ったらtrue
late ThemeColor _themeColor;
//
bool get isReady => _isReady;
Future<void> initial(int currentQuestNumber, BuildContext context) async {
_themeColor = ThemeColor(themeNumber: Model.themeNumber, context: context);
_currentQuestNumber = currentQuestNumber;
_audioPlay = AudioPlay(); //音再生用
_audioPlay.soundVolume = 0.0;
_audioPlay.playZero(); //音が鳴らないから鳴らしておく
_connectedFlash = Model.connectedFlash;
_fineTuningGearEngagement = Model.fineTuningGearEngagement;
//ルートギア用意
_rootGear = _gears[_questionGearSelect[_currentQuestNumber] ?? 0].clone();
_rootGear.name = 'root';
_rootGear.ratio = 1;
_rootGear.stack = 0;
_rootGear.image = _rootGearImage();
_rootGear.widget = _rootGearWidget();
//各ギア用意
for (final GearOne gear in _gears) {
gear.image = _gearImage(gear);
gear.widget = _gearWidget(gear);
}
//準備完了
_isReady = true;
}
void setSoundVolumeZero() {
_audioPlay.soundVolume = 0;
}
void readSoundVolume() {
_audioPlay.soundVolume = Model.soundVolume;
}
Widget getRootWidget() {
return _rootGear.widget;
}
double getRootWidth() {
return _rootGear.width;
}
Widget getWidget(int index) {
return _gears[index].widget;
}
double getWidth(int index) {
return _gears[index].width;
}
int getGearLength() {
return _gears.length;
}
List<GearOne> getGears() {
return _gears;
}
//MediaQuery.of(context).devicePixelRatio
set devicePixelRatio(double num) {
_devicePixelRatio = num;
//スマートフォンとタブレットでギアのかみ合わせ位置がずれるため、その調整
_adjustTeethRatio = _devicePixelRatio * _adjustTeethRatioBase1 - _adjustTeethRatioBase2 + _fineTuningGearEngagement;
}
//横幅を900とした場合の比率
set stageRatio(double num) {
_stageRatio = num;
_updateGears();
}
//stage全体の拡大率
set magnificationRate(double num) {
_magnificationRate = num;
_updateGears();
}
//ギア再描画
void _updateGears() {
_rootGear.image = _rootGearImage();
_rootGear.widget = _rootGearWidget();
for (final GearOne gear in _gears) {
gear.image = _gearImage(gear);
gear.widget = _gearWidget(gear);
}
}
void rootGearPosition(double left, double top) {
_rootGear.left = left;
_rootGear.top = top;
_rootGear.widget = _rootGearWidget();
}
void gearPosition(int index, double left, double top) {
_gears[index].left = left;
_gears[index].top = top;
_gears[index].widget = _gearWidget(_gears[index]);
}
//stage.dartから1秒間に30回呼ばれる
void tick() {
if (_isReady == false) {
return;
}
//root
_rootGear.degrees += _rootGear.ratio;
_rootGear.degrees %= 360;
_rootGear.widget = _rootGearWidget();
//gears
for (final GearOne gear in _gears) {
if (gear.ratio != 0) {
gear.degrees += gear.ratio;
gear.degrees %= 360;
gear.widget = _gearWidget(gear);
}
}
}
//ルートギア画像
Widget _rootGearImage() {
return SvgPicture.asset(_rootGear.src,
width: _rootGear.width * _stageRatio * _magnificationRate,
height: _rootGear.width * _stageRatio * _magnificationRate,
);
}
//各ギア画像
Widget _gearImage(GearOne gearOne) {
return SvgPicture.asset(gearOne.src,
width: gearOne.width * _stageRatio * _magnificationRate,
height: gearOne.width * _stageRatio * _magnificationRate,
);
}
//ルートギアWidget
Widget _rootGearWidget() {
return Positioned(
left: _rootGear.left * _stageRatio,
top: _rootGear.top * _stageRatio,
child: Transform.rotate(
angle: _rootGear.degrees * (3.141592653589793 / 180),
child: _rootGear.image,
)
);
}
//各ギアWidget
Widget _gearWidget(GearOne gearOne) {
return Positioned(
left: gearOne.left * _stageRatio,
top: gearOne.top * _stageRatio,
child: GestureDetector(
onPanUpdate: (panUpdateDetails) {
Offset position = panUpdateDetails.delta;
gearOne.left += position.dx / _stageRatio;
gearOne.top += position.dy / _stageRatio;
_updatePosition(gearOne.name);
},
onPanEnd: (panUpdateDetails) {
_checkGearTouch(gearOne.name);
},
child: Transform.rotate(
angle: gearOne.degrees * (3.141592653589793 / 180),
child: gearOne.image,
)
)
);
}
//ギアのドラッグ更新
void _updatePosition(String name) {
final int index = _gears.indexWhere((gear) => gear.name == name);
_gears[index].widget = _gearWidget(_gears[index]);
_checkGearTouch('');
_findTip();
}
//ギアの接触を調べて回転速度をセット
void _checkGearTouch(String gearName) {
//全て回転を止める
for (final GearOne gearOne in _gears) {
gearOne.ratio = 0;
}
//ルートギアと接触しているか調査
for (final GearOne gear1 in _gears) {
final int touch = _checkGearTouch2(_rootGear,gear1);
if (touch == 1) {
gear1.ratio = _rootGear.ratio * _rootGear.teeth1 / gear1.teeth1 * -1;
if (gear1.name == gearName) {
_adsorption(_rootGear, gear1);
}
} else if (touch == 3) {
gear1.ratio = _rootGear.ratio * _rootGear.teeth1 / gear1.teeth2 * -1;
if (gear1.name == gearName) {
_adsorption(_rootGear, gear1);
}
}
}
//その他ギアと接触しているか調査
for (int i = 0; i < _gears.length; i++) {
bool joinFlag = false;
for (final GearOne gear1 in _gears) {
for (final GearOne gear2 in _gears) {
if (gear1.name != gear2.name) {
final int touch = _checkGearTouch2(gear1,gear2);
if (gear1.ratio != 0 && gear2.ratio == 0) {
if (touch == 1) {
gear2.ratio = gear1.ratio * gear1.teeth1 / gear2.teeth1 * -1;
if (gear2.name == gearName) {
_adsorption(gear1, gear2);
}
joinFlag = true;
} else if (touch == 2) {
gear2.ratio = gear1.ratio * gear1.teeth2 / gear2.teeth1 * -1;
if (gear2.name == gearName && gear2.stack < gear1.stack) {
final int tmp = gear2.stack;
gear2.stack = gear1.stack;
gear1.stack = tmp;
_adsorption(gear1, gear2);
}
joinFlag = true;
} else if (touch == 3) {
gear2.ratio = gear1.ratio * gear1.teeth1 / gear2.teeth2 * -1;
if (gear2.name == gearName && gear1.stack < gear2.stack) {
final int tmp = gear2.stack;
gear2.stack = gear1.stack;
gear1.stack = tmp;
_adsorption(gear1, gear2);
}
joinFlag = true;
}
}
}
}
}
if (joinFlag == false) {
break;
}
}
}
//ギアとギアの接触を調べる
int _checkGearTouch2(GearOne gear1, GearOne gear2) {
double rad1 = gear1.teeth1 * _adjustTeethRatio * _stageRatio * _magnificationRate;
double center1x = gear1.left / 2 + rad1;
double center1y = gear1.top / 2 + rad1;
double rad2 = gear2.teeth1 * _adjustTeethRatio * _stageRatio * _magnificationRate;
double center2x = gear2.left / 2 + rad2;
double center2y = gear2.top / 2 + rad2;
double distance = sqrt(pow(center1x - center2x,2) + pow(center1y - center2y,2)); //ギア同士の距離
double judge = rad1 + rad2; //ギア2個の半径計
if (distance < judge + 3 && distance > judge - 3) {
return 1; //接触
}
//
if (gear1.teeth2 > 0) {
double rad1b = gear1.teeth2 * _adjustTeethRatio * _stageRatio * _magnificationRate;
double judge = rad1b + rad2; //ギア2個の半径計
if (distance < judge + 3 && distance > judge - 3) {
return 2; //ギア1の内側に接触
}
}
if (gear2.teeth2 > 0) {
double rad2b = gear2.teeth2 * _adjustTeethRatio * _stageRatio * _magnificationRate;
double judge = rad1 + rad2b; //ギア2個の半径計
if (distance < judge + 3 && distance > judge - 3) {
return 3; //ギア2の内側に接触
}
}
return 0; //非接触
}
//ギアの吸着。位置をずらしていき、一番距離が短いものを採用
void _adsorption(GearOne gear1, GearOne gear2) {
double rad1 = gear1.teeth1 * _adjustTeethRatio * _stageRatio * _magnificationRate;
double rad2 = gear2.teeth1 * _adjustTeethRatio * _stageRatio * _magnificationRate;
double judge = rad1 + rad2; //ギア2個の半径計
double center1x = gear1.left / 2 + rad1;
double center1y = gear1.top / 2 + rad1;
int answerX = 0; //最小の移動距離が記録される
int answerY = 0; //最小の移動距離が記録される
double minimum = 999;
for (int y in [-5,-4,-3,-2,-1,0,1,2,3,4,5]) {
for (int x in [-5,-4,-3,-2,-1,0,1,2,3,4,5]) {
double center2x = (gear2.left + x) / 2 + rad2;
double center2y = (gear2.top + y) / 2 + rad2;
double distance = sqrt(pow(center1x - center2x, 2) + pow(center1y - center2y, 2));
if (distance - judge < minimum) {
if ((distance - judge).abs() <= 3) {
minimum = distance - judge;
answerX = x;
answerY = y;
}
}
}
}
gear2.left += answerX;
gear2.top += answerY;
}
//末端を調べる
void _findTip() {
List<GearOne> connection = []; //末端までの接続
//ルートギアと接触しているギアを求める
GearOne? gear1;
for (GearOne gear in _gears) {
final int touch = _checkGearTouch2(_rootGear,gear);
if (touch != 0) {
gear1 = gear;
break;
}
}
_lastGear = gear1;
if (_lastGear != null) {
connection.add(gear1!);
List<GearOne> used = [];
used.add(gear1);
for (int j = 0; j < _gears.length; j++) {
bool findFlag = false;
for (GearOne gear1 in _gears) {
if (used.contains(gear1) == false) {
final int touch = _checkGearTouch2(_lastGear!,gear1);
if (touch != 0) {
_lastGear = gear1;
used.add(gear1);
connection.add(gear1);
findFlag = true;
break;
}
}
}
if (findFlag == false) {
break;
}
}
}
_rotateGearCount();
if (_headerColorMode != CurrentState.clear) {
final bool ret = _answerCheck(connection);
if (ret) {
_headerColorMode = CurrentState.clear; //success
Model.addQuestProgressSuccess(_currentQuestNumber);
}
}
}
//回転しているギアの数で音を出す
void _rotateGearCount() {
int count = 0;
for (final GearOne gear1 in _gears) {
if (gear1.ratio != 0) {
count += 1;
}
}
if (_lastRotationGearCount < count) {
_lastRotationGearCount = count;
_audioPlay.playJoin();
if (_connectedFlash) {
if (_headerColorMode != CurrentState.clear) {
(() async {
_headerColorMode = CurrentState.flash;
await Future.delayed(const Duration(milliseconds: 200));
if (_headerColorMode != CurrentState.clear) {
_headerColorMode = CurrentState.normal;
}
})();
}
}
} else if (_lastRotationGearCount > count) {
_lastRotationGearCount = count;
_audioPlay.playSlide();
}
}
bool _answerCheck(List<GearOne> connection) {
const double allowable = 0.002; //計算誤差に対する許容範囲プラス方向とマイナス方向
//各クエスト番号で正解していたらtrueを返す。
if (_currentQuestNumber == 0) {
return true;
} else if (_currentQuestNumber == 1) {
if (connection.length == 1) {
return true;
}
} else if (_currentQuestNumber == 2) {
if (connection.length == 2) {
return true;
}
} else if (_currentQuestNumber == 3) {
if (connection.length == 3) {
return true;
}
} else if (_currentQuestNumber == 4) {
if (connection.isNotEmpty) {
if (connection.last.teeth1 == 28) {
return true;
}
}
} else if (_currentQuestNumber >= 5) {
if (connection.isNotEmpty) {
final double r = connection.last.ratio;
final double reference = _answers[_currentQuestNumber] ?? 0.0;
if (r >= (reference - allowable) && r <= (reference + allowable)) {
return true;
}
}
}
return false;
}
//ルートギア下に配置される文字
Widget rootGearText() {
return Positioned(
left: 10 * _stageRatio,
top: _rootGear.width / 2 * _stageRatio * _magnificationRate,
child: const Text('10 rpm',
style: TextStyle(
color: Colors.white,
fontSize: 15.0,
)
)
);
}
//最終接続ギアの下に配置される文字
Widget lastGearText() {
if (_lastGear == null) {
return Container();
}
return Positioned(
left: (_lastGear!.left + (_lastGear!.width / 4 * _magnificationRate)) * _stageRatio,
top: (_lastGear!.top + _lastGear!.width * _magnificationRate) * _stageRatio,
child: Text('${(_lastGear!.ratio * 1000).roundToDouble() / 100} rpm',
style: const TextStyle(
color: Colors.red,
fontSize: 15.0,
)
)
);
}
//ヘッダカラー
Color headerColor() {
if (_headerColorMode == CurrentState.flash) { //フラッシュ時
return _themeColor.stageHeaderFlash;
} else if (_headerColorMode == CurrentState.clear) { //クエストクリア時
return _themeColor.stageHeaderClear;
} else { //通常時
return _themeColor.stageHeaderNormal;
}
}
CurrentState get headerColorMode => _headerColorMode;
}
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:gearcombination/const_value.dart';
import 'package:gearcombination/quest_progress.dart';
import 'package:gearcombination/game.dart';
import 'package:gearcombination/setting_page.dart';
import 'package:gearcombination/stage_page.dart';
import 'package:gearcombination/ad_manager.dart';
import 'package:gearcombination/ad_banner_widget.dart';
import 'package:gearcombination/model.dart';
import 'package:gearcombination/loading_screen.dart';
import 'package:gearcombination/theme_color.dart';
import 'package:gearcombination/main.dart';
import 'package:gearcombination/l10n/app_localizations.dart';
class MainHomePage extends StatefulWidget {
const MainHomePage({super.key});
@override
State<MainHomePage> createState() => _MainHomePageState();
}
class _MainHomePageState extends State<MainHomePage> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
late AdManager _adManager;
final Game _game = Game(); //ゲームのデータを一時的に保持するなどの役目
//
late ThemeColor _themeColor;
bool _isReady = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initState();
}
void _initState() async {
_adManager = AdManager();
_wakelock();
setState(() {
_isReady = true;
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_adManager.dispose();
WakelockPlus.disable();
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;
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_themeColor = ThemeColor(themeNumber: Model.themeNumber, context: context);
}
void _wakelock() {
if (Model.wakelockEnabled) {
WakelockPlus.enable();
} else {
WakelockPlus.disable();
}
}
Future<void> _openSetting() async {
final updated = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (_) => const SettingPage()),
);
if (mounted && updated == true) {
MainApp.of(context).rebuildApp();
_wakelock();
}
if (mounted) {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
if (!_isReady) {
return Scaffold(body: LoadingScreen());
}
final l = AppLocalizations.of(context)!;
return Scaffold(
backgroundColor: _themeColor.mainBackColor,
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Colors.transparent,
title: Text(l.appTitle,
style: TextStyle(color: _themeColor.mainForeColor, fontSize: 15.0),
),
centerTitle: false,
actions: [
IconButton(
icon: const Icon(Icons.settings),
color: _themeColor.mainForeColor,
onPressed: _openSetting,
),
const SizedBox(width: 8),
],
),
body: Stack(
children: [
SizedBox.expand(
child: SvgPicture.asset(
ConstValue.imageBack1,
fit: BoxFit.cover,
)
),
SafeArea(
child: SingleChildScrollView(
child: _stageArea(),
),
),
],
),
bottomNavigationBar: AdBannerWidget(adManager: _adManager),
);
}
Widget _stageArea() {
return Padding(
padding: const EdgeInsets.only(top: 10, left: 10, right: 10, bottom: 45),
child: Column(
children: [
Row(children: [_questButton(0)]),
const SizedBox(height: 8),
Row(
children: [
_questButton(1),
const SizedBox(width: 8),
_questButton(2),
const SizedBox(width: 8),
_questButton(3),
const SizedBox(width: 8),
_questButton(4),
],
),
const SizedBox(height: 8),
Row(
children: [
_questButton(5),
const SizedBox(width: 8),
_questButton(6),
const SizedBox(width: 8),
_questButton(7),
const SizedBox(width: 8),
_questButton(8),
],
),
const SizedBox(height: 8),
Row(
children: [
_questButton(9),
const SizedBox(width: 8),
_questButton(10),
const SizedBox(width: 8),
_questButton(11),
const SizedBox(width: 8),
_questButton(12),
],
),
const SizedBox(height: 8),
Row(
children: [
_questButton(13),
const SizedBox(width: 8),
_questButton(14),
const SizedBox(width: 8),
_questButton(15),
const SizedBox(width: 8),
_questButton(16),
],
),
const SizedBox(height: 8),
Row(
children: [
_questButton(17),
const SizedBox(width: 8),
_questButton(18),
const SizedBox(width: 8),
_questButton(19),
const SizedBox(width: 8),
_questButton(20),
],
),
const SizedBox(height: 8),
Row(
children: [
_questButton(21),
const SizedBox(width: 8),
_questButton(22),
const SizedBox(width: 8),
_questButton(23),
const SizedBox(width: 8),
_questButton(24),
],
),
const SizedBox(height: 8),
Row(
children: [
_questButton(25),
const SizedBox(width: 8),
_questButton(26),
const SizedBox(width: 8),
_questButton(27),
const SizedBox(width: 8),
_questButton(28),
],
),
const SizedBox(height: 8),
Row(
children: [
_questButton(29),
const SizedBox(width: 8),
_questButton(30),
const SizedBox(width: 8),
_questButton(31),
const SizedBox(width: 8),
_questButton(32),
],
),
const SizedBox(height: 8),
Row(
children: [
_questButton(33),
const SizedBox(width: 8),
_questButton(34),
const SizedBox(width: 8),
_questButton(35),
const SizedBox(width: 8),
_questButton(36),
],
),
const SizedBox(height: 8),
Row(
children: [
_questButton(37),
const SizedBox(width: 8),
_questButton(38),
const SizedBox(width: 8),
_questButton(39),
const SizedBox(width: 8),
_questButton(40),
],
),
const SizedBox(height: 8),
Row(
children: [
_questButton(41),
const SizedBox(width: 8),
_questButton(42),
const SizedBox(width: 8),
_questButton(43),
const SizedBox(width: 8),
_questButton(44),
],
),
const SizedBox(height: 8),
Row(
children: [
_questButton(45),
const SizedBox(width: 8),
_questButton(46),
const SizedBox(width: 8),
_questButton(47),
const SizedBox(width: 8),
_questButton(48),
],
),
const SizedBox(height: 8),
Row(
children: [
_questButton(49),
const SizedBox(width: 8),
_questButton(50),
const SizedBox(width: 8),
_questButton(51),
const SizedBox(width: 8),
_questButton(52),
],
),
],
),
);
}
//クエストの各ボタン
Widget _questButton(int number) {
//保存されているクエスト進行具合を取得
Set<QuestProgress> questProgress = Model.questProgress;
//進行具合にnumberが含まれていればそれを取り出す。無ければQuestProgress(0, false)を用意
QuestProgress? questP = questProgress.firstWhere(
(quest) => quest.questNumber == number,
orElse: () => QuestProgress(0, false),
);
//クリア済みかセット
final bool clearFlag = questP.clearFlag;
//
return Expanded(
child: Center(
child: GestureDetector(
onTap: () async {
_game.currentQuestNumber = number;
await Navigator.of(context).push(
MaterialPageRoute<bool>(
builder: (context) => StagePage(game: _game),
),
);
setState(() {});
},
child: Container(
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 255, 255, 0.5),
borderRadius: BorderRadius.circular(100),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.check,
color: clearFlag ? Colors.redAccent : Colors.grey,
),
const SizedBox(width: 8),
Text(
'Q$number',
style: const TextStyle(color: Colors.black, fontSize: 22.0),
),
],
),
),
),
),
);
}
}
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,
),
),
),
],
),
),
);
},
);
}
}
import 'dart:io';
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
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:gearcombination/l10n/app_localizations.dart';
import 'package:gearcombination/home_page.dart';
import 'package:gearcombination/model.dart';
import 'package:gearcombination/theme_mode_number.dart';
import 'package:gearcombination/parse_locale_tag.dart';
import 'package:gearcombination/loading_screen.dart';
import 'package:gearcombination/ad_ump_status.dart';
import 'package:gearcombination/ad_manager.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
//ATTを最優先で呼ぶ(広告SDKより前)
if (!kIsWeb && Platform.isIOS) {
final status = await AppTrackingTransparency.trackingAuthorizationStatus;
if (status == TrackingStatus.notDetermined) {
await Future.delayed(const Duration(milliseconds: 300));
await AppTrackingTransparency.requestTrackingAuthorization();
}
}
//UI設定
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
statusBarColor: Colors.transparent,
systemNavigationBarContrastEnforced: false,
systemStatusBarContrastEnforced: false,
),
);
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> {
ThemeMode _themeMode = ThemeMode.system;
Locale? _locale;
bool _hasError = false;
bool _isReady = false;
@override
void initState() {
super.initState();
_initState();
}
void _initState() async {
try {
//アプリの基本データ
await Model.ensureReady();
//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;
});
}
}
}
void rebuildApp() {
setState(() {
_themeMode = ThemeModeNumber.numberToThemeMode(Model.themeNumber);
_locale = parseLocaleTag(Model.languageCode);
});
}
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();
}
Color seed = Colors.blue;
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,
),
),
),
),
);
}
}
import 'dart:convert';
import 'dart:ui' as ui;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:gearcombination/quest_progress.dart';
import 'package:gearcombination/l10n/app_localizations.dart';
class Model {
Model._();
static const String _prefMagnificationRate = 'magnificationRate';
static const String _prefSoundVolume = 'soundVolume';
static const String _prefConnectedFlash = 'connectedFlash';
static const String _prefFineTuningGearEngagement = 'fineTuningGearEngagement';
static const String _prefBackgroundImageNumber = 'backgroundImageNumber';
static const String _prefWakelockEnabled = 'wakelockEnabled';
static const String _prefThemeNumber = 'themeNumber';
static const String _prefLanguageCode = 'languageCode';
static const String _prefQuestProgress = 'questProgress';
static bool _ready = false;
static double _magnificationRate = 1.0;
static double _soundVolume = 1.0;
static bool _connectedFlash = true;
static double _fineTuningGearEngagement = 0.0;
static int _backgroundImageNumber = 1;
static bool _wakelockEnabled = false;
static int _themeNumber = 0;
static String _languageCode = '';
static Set<QuestProgress> _questProgress = {};
static double get magnificationRate => _magnificationRate;
static double get soundVolume => _soundVolume;
static bool get connectedFlash => _connectedFlash;
static double get fineTuningGearEngagement => _fineTuningGearEngagement;
static int get backgroundImageNumber => _backgroundImageNumber;
static bool get wakelockEnabled => _wakelockEnabled;
static int get themeNumber => _themeNumber;
static String get languageCode => _languageCode;
static Set<QuestProgress> get questProgress => _questProgress;
static Future<void> ensureReady() async {
if (_ready) {
return;
}
final SharedPreferences prefs = await SharedPreferences.getInstance();
//
_magnificationRate = (prefs.getDouble(_prefMagnificationRate) ?? 1.0).clamp(0.3,3.0);
_soundVolume = (prefs.getDouble(_prefSoundVolume) ?? 1.0).clamp(0.0,1.0);
_connectedFlash = prefs.getBool(_prefConnectedFlash) ?? true;
_fineTuningGearEngagement = (prefs.getDouble(_prefFineTuningGearEngagement) ?? 0.0).clamp(-5.0,5.0);
_backgroundImageNumber = (prefs.getInt(_prefBackgroundImageNumber) ?? 1).clamp(0,10);
_wakelockEnabled = prefs.getBool(_prefWakelockEnabled) ?? false;
_themeNumber = (prefs.getInt(_prefThemeNumber) ?? 0).clamp(0, 2);
_languageCode = prefs.getString(_prefLanguageCode) ?? ui.PlatformDispatcher.instance.locale.languageCode;
_languageCode = _resolveLanguageCode(_languageCode);
_questProgress = _decodeQuestProgress(prefs.getString(_prefQuestProgress));
_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> setMagnificationRate(double value) async {
_magnificationRate = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_prefMagnificationRate, value);
}
static Future<void> setSoundVolume(double value) async {
_soundVolume = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_prefSoundVolume, value);
}
static Future<void> setConnectedFlash(bool value) async {
_connectedFlash = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefConnectedFlash, value);
}
static Future<void> setFineTuningGearEngagement(double value) async {
_fineTuningGearEngagement = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_prefFineTuningGearEngagement, value);
}
static Future<void> setBackgroundImageNumber(int value) async {
_backgroundImageNumber = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setInt(_prefBackgroundImageNumber, value);
}
static Future<void> setWakelockEnabled(bool value) async {
_wakelockEnabled = value;
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefWakelockEnabled, 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);
}
//----------------------------
//quest progress
static Future<void> setQuestProgress(Set<QuestProgress> questProgress) async {
_questProgress = questProgress;
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String json = jsonEncode(
questProgress.map((quest) => quest.toJson()).toList(),
);
await prefs.setString(_prefQuestProgress, json);
}
static Future<Set<QuestProgress>> getQuestProgress() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
_questProgress = _decodeQuestProgress(prefs.getString(_prefQuestProgress));
return _questProgress;
}
static Future<void> addQuestProgressSuccess(int currentQuestNumber) async {
final Set<QuestProgress> questProgress = {
...await getQuestProgress(),
};
QuestProgress val = QuestProgress(currentQuestNumber, true);
if (!questProgress.contains(val)) {
questProgress.add(val);
await setQuestProgress(questProgress);
}
}
static Set<QuestProgress> _decodeQuestProgress(String? json) {
if (json == null || json.isEmpty) {
return {};
}
final List<dynamic> decodedList = jsonDecode(json) as List<dynamic>;
return decodedList
.map((item) => QuestProgress(
item['questNumber'] as int,
item['clearFlag'] as bool,
))
.toSet();
}
//----------------------------
}
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,
);
}
class QuestProgress {
int questNumber;
bool clearFlag;
//constructor
QuestProgress(this.questNumber, this.clearFlag);
Map<String, dynamic> toJson() {
return {
'questNumber': questNumber,
'clearFlag': clearFlag,
};
}
}
import 'package:flutter/material.dart';
import 'package:gearcombination/theme_color.dart';
import 'package:gearcombination/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,
),
);
}
}
import 'dart:async';
import "dart:io";
import "package:app_settings/app_settings.dart";
import "package:app_tracking_transparency/app_tracking_transparency.dart";
import 'package:flutter/material.dart';
import "package:flutter/foundation.dart";
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:in_app_review/in_app_review.dart';
import "package:gearcombination/setting_card.dart";
import 'package:gearcombination/l10n/app_localizations.dart';
import 'package:gearcombination/model.dart';
import 'package:gearcombination/ad_manager.dart';
import 'package:gearcombination/ad_banner_widget.dart';
import 'package:gearcombination/loading_screen.dart';
import 'package:gearcombination/theme_color.dart';
import 'package:gearcombination/ad_ump_status.dart';
import 'package:gearcombination/_secrets.dart';
class SettingPage extends StatefulWidget {
const SettingPage({super.key});
@override
State<SettingPage> createState() => _SettingPageState();
}
class _SettingPageState extends State<SettingPage> {
late AdManager _adManager;
AdUmpState _adUmpState = AdUmpState.initial;
late final AdUmpService _adUmpService;
bool _wakelockEnabled = true;
int _themeNumber = 0;
String _languageCode = '';
late ThemeColor _themeColor;
final _inAppReview = InAppReview.instance;
bool _isReady = false;
//
double _magnificationRate = 1.0;
double _soundVolume = 0.0;
bool _connectedFlash = true;
double _fineTuningGearEngagement = 0.0;
int _backgroundImageNumber = 0;
@override
void initState() {
super.initState();
_initState();
}
void _initState() async {
//ump
_adUmpService = AdUmpService();
_refreshConsentInfo();
//
_adManager = AdManager();
//
_magnificationRate = Model.magnificationRate;
_soundVolume = Model.soundVolume;
_connectedFlash = Model.connectedFlash;
_fineTuningGearEngagement = Model.fineTuningGearEngagement;
_backgroundImageNumber = Model.backgroundImageNumber;
_wakelockEnabled = Model.wakelockEnabled;
_themeNumber = Model.themeNumber;
_languageCode = Model.languageCode;
if (mounted) {
setState(() {
_isReady = true;
});
}
}
@override
void dispose() {
_adManager.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_themeColor = ThemeColor(themeNumber: Model.themeNumber, context: context);
}
Future<void> _refreshConsentInfo() async {
final AdUmpState newState = await _adUmpService.updateConsentInfo(_adUmpState);
if (mounted) {
setState(() { _adUmpState = newState; });
}
}
Future<void> _onApply() async {
await Model.setMagnificationRate(_magnificationRate);
await Model.setSoundVolume(_soundVolume);
await Model.setConnectedFlash(_connectedFlash);
await Model.setFineTuningGearEngagement(_fineTuningGearEngagement);
await Model.setBackgroundImageNumber(_backgroundImageNumber);
await Model.setWakelockEnabled(_wakelockEnabled);
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: _themeColor.backColor,
appBar: AppBar(
foregroundColor: _themeColor.appBarForegroundColor,
backgroundColor: Colors.transparent,
title: Text(AppLocalizations.of(context)!.setting),
centerTitle: true,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(false),
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 10),
child:IconButton(
icon: const Icon(Icons.check),
onPressed: _onApply,
)
),
],
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 0, bottom: 100),
child: Column(
children: [
_buildMagnificationRate(l, t),
_buildSoundVolume(l, t),
_buildFlash(l, t),
_buildFileTune(l, t),
_buildBackgroundImage(l, t),
_buildWakelockEnabled(l, t),
_buildTheme(l, t),
_buildLanguage(l, t),
_buildReview(l, t),
_buildCmp(l, t),
_buildAtt(l, t),
_buildUsage(l, t),
],
),
),
),
),
),
],
)
),
bottomNavigationBar: AdBannerWidget(adManager: _adManager),
);
}
Widget _buildMagnificationRate(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(
l.magnificationRate,
style: t.bodyMedium,
),
subtitle: Row(
children: [
Text(_magnificationRate.toStringAsFixed(1)),
Expanded(
child: Slider(
value: _magnificationRate,
min: 0.3,
max: 3.0,
divisions: 27,
label: _magnificationRate.toStringAsFixed(1),
onChanged: (double value) {
setState(() {
_magnificationRate = value;
});
},
),
),
],
),
),
);
}
Widget _buildSoundVolume(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(
l.soundVolume,
style: t.bodyMedium,
),
subtitle: Row(
children: [
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 = value;
});
},
),
),
],
),
),
);
}
Widget _buildFlash(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(
l.connectedFlash,
style: t.bodyMedium,
),
trailing: Switch(
value: _connectedFlash,
onChanged: (bool value) {
setState(() {
_connectedFlash = value;
});
},
),
),
);
}
Widget _buildFileTune(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(
l.fineTuningGearEngagement,
style: t.bodyMedium,
),
subtitle: Row(
children: [
Text(_fineTuningGearEngagement.toStringAsFixed(1)),
Expanded(
child: Slider(
value: _fineTuningGearEngagement,
min: -5.0,
max: 5.0,
divisions: 100,
label: _fineTuningGearEngagement.toStringAsFixed(1),
onChanged: (double value) {
setState(() {
_fineTuningGearEngagement = value;
});
},
),
),
],
),
),
);
}
Widget _buildBackgroundImage(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(
l.backgroundImageNumber,
style: t.bodyMedium,
),
subtitle: Row(
children: [
Text(_backgroundImageNumber.toString().padLeft(2, '0')),
Expanded(
child: Slider(
value: _backgroundImageNumber.toDouble(),
min: 0,
max: 10,
divisions: 10,
label: _backgroundImageNumber.toString(),
onChanged: (double value) {
setState(() {
_backgroundImageNumber = value.toInt();
});
},
),
),
],
),
),
);
}
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 _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<TrackingStatus>(
future: AppTrackingTransparency.trackingAuthorizationStatus,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return 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;
final label = status != null
? status.toString().split('.').last
: l.attStatusUnknown;
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),
),
],
),
);
},
),
],
),
),
);
}
Widget _buildUsage(AppLocalizations l, TextTheme t) {
return SettingCard(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l.usage1, style: t.bodySmall),
const SizedBox(height: 16),
Text(l.usage2, style: t.bodySmall),
const SizedBox(height: 16),
Text(l.usage3, style: t.bodySmall),
],
),
),
);
}
}
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:gearcombination/l10n/app_localizations.dart';
import 'package:gearcombination/const_value.dart';
import 'package:gearcombination/model.dart';
import 'package:gearcombination/game.dart';
import 'package:gearcombination/ad_manager.dart';
import 'package:gearcombination/ad_banner_widget.dart';
import 'package:gearcombination/gears.dart';
import 'package:gearcombination/gear_one.dart';
import 'package:gearcombination/current_state.dart';
import 'package:gearcombination/loading_screen.dart';
import 'package:gearcombination/theme_color.dart';
class StagePage extends StatefulWidget {
//メインページでは SettingPage(game: _game) と渡している。
//受け取った game は widget.game でアクセスできる。
final Game game;
const StagePage({super.key, required this.game});
@override
State<StagePage> createState() => _StagePageState();
}
class _StagePageState extends State<StagePage> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
late AdManager _adManager;
final Gears _gears = Gears();
late Timer _timer;
double _devicePixelRatio = 0;
double _screenWidth = 0;
double _screenHeight = 0;
double _stageWidth = 0;
double _stageRatio = 1;
double _bgImageSize = 0; //背景画像サイズ
double _bgImageAngle = 0; //背景画像回転角度
int _backgroundImageNumber = 0;
final List<String> _questQuestion = [];
String _title = '';
late ThemeColor _themeColor;
bool _isReady = false;
bool _isFirst = true;
late AnimationController _excellentController;
late Animation<double> _excellentScale;
bool _showExcellent = false;
bool _excellentDismissible = false;
bool _excellentShown = false;
Timer? _excellentDismissTimer;
Timer? _excellentFadeOutTimer;
double _excellentOpacity = 1.0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_excellentController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
);
_excellentScale = Tween<double>(begin: 0, end: 0.8).animate(
CurvedAnimation(parent: _excellentController, curve: Curves.easeOut),
);
_initState();
}
void _initState() async {
_adManager = AdManager();
_wakelock();
await _gears.initial(widget.game.currentQuestNumber, context);
_gears.magnificationRate = Model.magnificationRate;
_gears.readSoundVolume();
_backgroundImageNumber = Model.backgroundImageNumber;
_timer = Timer.periodic(const Duration(milliseconds: (1000 ~/ 30)), (timer) {
final bool triggerClear = !_excellentShown &&
_gears.headerColorMode == CurrentState.clear;
if (triggerClear) {
_excellentShown = true;
_excellentController.forward(from: 0);
_excellentDismissTimer?.cancel();
_excellentDismissTimer = Timer(const Duration(seconds: 2), () {
if (!mounted || !_showExcellent) {
return;
}
setState(() {
_excellentDismissible = true;
_excellentOpacity = 1.0;
});
});
}
setState(() {
_gears.tick();
_bgImageAngle -= 0.002;
if (_bgImageAngle < -314159265) {
_bgImageAngle = 0;
}
if (triggerClear) {
_showExcellent = true;
_excellentDismissible = false;
_excellentOpacity = 1.0;
_excellentFadeOutTimer?.cancel();
}
});
});
setState(() {
_isReady = true;
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
WakelockPlus.disable();
_gears.setSoundVolumeZero();
_adManager.dispose();
_timer.cancel();
_excellentDismissTimer?.cancel();
_excellentFadeOutTimer?.cancel();
_excellentController.dispose();
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;
}
}
//別のページから戻ったときに実行される
@override
void didUpdateWidget(StagePage oldWidget) {
super.didUpdateWidget(oldWidget);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_themeColor = ThemeColor(themeNumber: Model.themeNumber, context: context);
//クエスト出題をすべて取得
_questQuestion.add(AppLocalizations.of(context)!.quest0);
_questQuestion.add(AppLocalizations.of(context)!.quest1);
_questQuestion.add(AppLocalizations.of(context)!.quest2);
_questQuestion.add(AppLocalizations.of(context)!.quest3);
_questQuestion.add(AppLocalizations.of(context)!.quest4);
_questQuestion.add(AppLocalizations.of(context)!.quest5);
_questQuestion.add(AppLocalizations.of(context)!.quest6);
_questQuestion.add(AppLocalizations.of(context)!.quest7);
_questQuestion.add(AppLocalizations.of(context)!.quest8);
_questQuestion.add(AppLocalizations.of(context)!.quest9);
_questQuestion.add(AppLocalizations.of(context)!.quest10);
_questQuestion.add(AppLocalizations.of(context)!.quest11);
_questQuestion.add(AppLocalizations.of(context)!.quest12);
_questQuestion.add(AppLocalizations.of(context)!.quest13);
_questQuestion.add(AppLocalizations.of(context)!.quest14);
_questQuestion.add(AppLocalizations.of(context)!.quest15);
_questQuestion.add(AppLocalizations.of(context)!.quest16);
_questQuestion.add(AppLocalizations.of(context)!.quest17);
_questQuestion.add(AppLocalizations.of(context)!.quest18);
_questQuestion.add(AppLocalizations.of(context)!.quest19);
_questQuestion.add(AppLocalizations.of(context)!.quest20);
_questQuestion.add(AppLocalizations.of(context)!.quest21);
_questQuestion.add(AppLocalizations.of(context)!.quest22);
_questQuestion.add(AppLocalizations.of(context)!.quest23);
_questQuestion.add(AppLocalizations.of(context)!.quest24);
_questQuestion.add(AppLocalizations.of(context)!.quest25);
_questQuestion.add(AppLocalizations.of(context)!.quest26);
_questQuestion.add(AppLocalizations.of(context)!.quest27);
_questQuestion.add(AppLocalizations.of(context)!.quest28);
_questQuestion.add(AppLocalizations.of(context)!.quest29);
_questQuestion.add(AppLocalizations.of(context)!.quest30);
_questQuestion.add(AppLocalizations.of(context)!.quest31);
_questQuestion.add(AppLocalizations.of(context)!.quest32);
_questQuestion.add(AppLocalizations.of(context)!.quest33);
_questQuestion.add(AppLocalizations.of(context)!.quest34);
_questQuestion.add(AppLocalizations.of(context)!.quest35);
_questQuestion.add(AppLocalizations.of(context)!.quest36);
_questQuestion.add(AppLocalizations.of(context)!.quest37);
_questQuestion.add(AppLocalizations.of(context)!.quest38);
_questQuestion.add(AppLocalizations.of(context)!.quest39);
_questQuestion.add(AppLocalizations.of(context)!.quest40);
_questQuestion.add(AppLocalizations.of(context)!.quest41);
_questQuestion.add(AppLocalizations.of(context)!.quest42);
_questQuestion.add(AppLocalizations.of(context)!.quest43);
_questQuestion.add(AppLocalizations.of(context)!.quest44);
_questQuestion.add(AppLocalizations.of(context)!.quest45);
_questQuestion.add(AppLocalizations.of(context)!.quest46);
_questQuestion.add(AppLocalizations.of(context)!.quest47);
_questQuestion.add(AppLocalizations.of(context)!.quest48);
_questQuestion.add(AppLocalizations.of(context)!.quest49);
_questQuestion.add(AppLocalizations.of(context)!.quest50);
_questQuestion.add(AppLocalizations.of(context)!.quest51);
_questQuestion.add(AppLocalizations.of(context)!.quest52);
}
void _wakelock() {
if (Model.wakelockEnabled) {
WakelockPlus.enable();
} else {
WakelockPlus.disable();
}
}
@override
Widget build(BuildContext context) {
if (_isReady == false || _gears.isReady == false) {
return LoadingScreen();
}
if (_isFirst) {
_isFirst = false;
_devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
_gears.devicePixelRatio = _devicePixelRatio;
_screenWidth = MediaQuery.of(context).size.width;
_screenHeight = MediaQuery.of(context).size.height;
_bgImageSize = max(_screenWidth,_screenHeight);
_stageWidth = _screenWidth;
_stageRatio = _stageWidth / 900;
_gears.stageRatio = _stageRatio;
_gears.rootGearPosition(0,- _gears.getRootWidth() / 2 * Model.magnificationRate);
double h = 0;
for (int i = _gears.getGearLength() - 1; i >= 0; i--) {
_gears.gearPosition(i,-(_gears.getWidth(i) / 2) + (_stageWidth / _stageRatio) - (_gears.getWidth(0) / 7),h);
h += _gears.getWidth(0) / 4; //最初のギアを基準値にしている。深い意味はない
}
try {
_title = _questQuestion[widget.game.currentQuestNumber];
} catch (_) {}
_gears.readSoundVolume();
}
return Container(
decoration: _decoration(),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
centerTitle: true,
elevation: 0,
//戻るボタン
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop(false);
},
),
title: Text(_title,
softWrap: true,
overflow: TextOverflow.visible,
maxLines: 2,
style: const TextStyle(
color: Colors.white,
fontSize: 16.0,
)
),
foregroundColor: const Color.fromRGBO(255,255,255,1),
backgroundColor: _gears.headerColor(),
),
body: SafeArea(
child: Stack(children:[
_background(),
Column(children:[
Expanded(
child: Stack(children:_stageGears())
),
]),
if (_showExcellent) _excellentOverlay(),
])
),
bottomNavigationBar: AdBannerWidget(adManager: _adManager),
)
);
}
Decoration _decoration() {
if (_backgroundImageNumber == 0) {
return const BoxDecoration(
color: Colors.white,
);
} else {
return const BoxDecoration(
image: DecorationImage(
image: AssetImage(ConstValue.imageSpace1),
fit: BoxFit.cover,
),
);
}
}
Widget _background() {
if (_backgroundImageNumber == 0) {
return Container(
color: _themeColor.stageBackColor,
);
} else {
return Transform.rotate(
angle: _bgImageAngle,
child: Transform.scale(
scale: 2.6,
child: Image.asset(
ConstValue.imageBackGrounds[_backgroundImageNumber],
width: _bgImageSize,
height: _bgImageSize,
),
),
);
}
}
//ステージ上に配置される要素
List<Widget> _stageGears() {
//Widgetの配列を返す
List<Widget> widgets = [];
//各ギアを取得
List<GearOne> gearAll = _gears.getGears();
//ギアの重なり順に並び変える
gearAll.sort((a, b) => a.compareStack(b));
//各ギア
for (final GearOne gear1 in gearAll) {
widgets.add(gear1.widget);
}
//ルートギア
widgets.add(_gears.getRootWidget());
//ルートギア下のテキスト
widgets.add(_gears.rootGearText());
//最終接続ギア下のテキスト
widgets.add(_gears.lastGearText());
//
return widgets;
}
Widget _excellentOverlay() {
return Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_excellentDismissible == false) {
return;
}
_excellentDismissible = false;
_excellentFadeOutTimer?.cancel();
_excellentFadeOutTimer = null;
_excellentDismissTimer?.cancel();
_excellentDismissTimer = null;
_runExcellentFadeOut();
},
child: Center(
child: AnimatedBuilder(
animation: _excellentScale,
builder: (context, child) {
final double width = MediaQuery.of(context).size.width * _excellentScale.value;
if (width <= 0) {
return const SizedBox.shrink();
}
return SizedBox(
width: width,
height: width,
child: Opacity(
opacity: _excellentOpacity,
child: child,
),
);
},
child: SvgPicture.asset(
'assets/image/excellent.svg',
fit: BoxFit.contain,
),
),
),
),
);
}
void _runExcellentFadeOut() {
const fadeDuration = Duration(milliseconds: 500);
const tick = Duration(milliseconds: 16);
var elapsed = Duration.zero;
_excellentFadeOutTimer = Timer.periodic(tick, (timer) {
if (!mounted) {
timer.cancel();
_excellentFadeOutTimer = null;
return;
}
elapsed += tick;
final t = (elapsed.inMilliseconds / fadeDuration.inMilliseconds).clamp(0.0, 1.0);
setState(() {
_excellentOpacity = 1.0 - t;
});
if (elapsed >= fadeDuration) {
timer.cancel();
_excellentFadeOutTimer = null;
setState(() {
_showExcellent = false;
_excellentOpacity = 1.0;
});
}
});
}
}
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(0, 10, 117, 1.0) : Color.fromRGBO(0, 6, 55, 1.0);
Color get mainForeColor => _isLight ? Color.fromRGBO(255, 255, 255, 0.8) : Color.fromRGBO(255, 255, 255, 0.8);
//stage page
Color get stageBackColor => _isLight ? Color.fromRGBO(50,50,50, 1) : Color.fromRGBO(50,50,50, 1);
Color get stageHeaderNormal => _isLight ? Color.fromRGBO(0,0,0, 0.8) : Color.fromRGBO(0,0,0, 0.8);
Color get stageHeaderFlash => _isLight ? Color.fromRGBO(0,0,0, 0.1) : Color.fromRGBO(0,0,0, 0.1);
Color get stageHeaderClear => _isLight ? Color.fromRGBO(50, 0, 255, 0.7) : Color.fromRGBO(50,0,255, 0.7);
//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;
Color get borderColor => _isLight ? Colors.grey[300]! : Colors.grey[700]!;
Color get inputFillColor => _isLight ? Colors.grey[50]! : Colors.grey[900]!;
}
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;
}
}
}