mirror of
https://github.com/flutter/samples.git
synced 2025-11-10 14:58:34 +00:00
Flutter 3.29 beta (#2571)
This commit is contained in:
@@ -36,12 +36,15 @@ class AdsController {
|
||||
void preloadAd() {
|
||||
// TODO: When ready, change this to the Ad Unit IDs provided by AdMob.
|
||||
// The current values are AdMob's sample IDs.
|
||||
final adUnitId = defaultTargetPlatform == TargetPlatform.android
|
||||
? 'ca-app-pub-3940256099942544/6300978111'
|
||||
// iOS
|
||||
: 'ca-app-pub-3940256099942544/2934735716';
|
||||
_preloadedAd =
|
||||
PreloadedBannerAd(size: AdSize.mediumRectangle, adUnitId: adUnitId);
|
||||
final adUnitId =
|
||||
defaultTargetPlatform == TargetPlatform.android
|
||||
? 'ca-app-pub-3940256099942544/6300978111'
|
||||
// iOS
|
||||
: 'ca-app-pub-3940256099942544/2934735716';
|
||||
_preloadedAd = PreloadedBannerAd(
|
||||
size: AdSize.mediumRectangle,
|
||||
adUnitId: adUnitId,
|
||||
);
|
||||
|
||||
// Wait a bit so that calling at start of a new screen doesn't have
|
||||
// adverse effects on performance.
|
||||
|
||||
@@ -53,8 +53,11 @@ class _BannerAdWidgetState extends State<BannerAdWidget> {
|
||||
if (_currentOrientation == orientation &&
|
||||
_bannerAd != null &&
|
||||
_adLoadingState == _LoadingState.loaded) {
|
||||
_log.info(() => 'We have everything we need. Showing the ad '
|
||||
'${_bannerAd.hashCode} now.');
|
||||
_log.info(
|
||||
() =>
|
||||
'We have everything we need. Showing the ad '
|
||||
'${_bannerAd.hashCode} now.',
|
||||
);
|
||||
return SizedBox(
|
||||
width: _bannerAd!.size.width.toDouble(),
|
||||
height: _bannerAd!.size.height.toDouble(),
|
||||
@@ -123,7 +126,8 @@ class _BannerAdWidgetState extends State<BannerAdWidget> {
|
||||
if (useAnchoredAdaptiveSize) {
|
||||
final AnchoredAdaptiveBannerAdSize? adaptiveSize =
|
||||
await AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(
|
||||
MediaQuery.of(context).size.width.truncate());
|
||||
MediaQuery.of(context).size.width.truncate(),
|
||||
);
|
||||
|
||||
if (adaptiveSize == null) {
|
||||
_log.warning('Unable to get height of anchored banner.');
|
||||
@@ -137,16 +141,19 @@ class _BannerAdWidgetState extends State<BannerAdWidget> {
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
assert(Platform.isAndroid || Platform.isIOS,
|
||||
'AdMob currently does not support ${Platform.operatingSystem}');
|
||||
assert(
|
||||
Platform.isAndroid || Platform.isIOS,
|
||||
'AdMob currently does not support ${Platform.operatingSystem}',
|
||||
);
|
||||
_bannerAd = BannerAd(
|
||||
// This is a test ad unit ID from
|
||||
// https://developers.google.com/admob/android/test-ads. When ready,
|
||||
// you replace this with your own, production ad unit ID,
|
||||
// created in https://apps.admob.com/.
|
||||
adUnitId: Theme.of(context).platform == TargetPlatform.android
|
||||
? 'ca-app-pub-3940256099942544/6300978111'
|
||||
: 'ca-app-pub-3940256099942544/2934735716',
|
||||
adUnitId:
|
||||
Theme.of(context).platform == TargetPlatform.android
|
||||
? 'ca-app-pub-3940256099942544/6300978111'
|
||||
: 'ca-app-pub-3940256099942544/2934735716',
|
||||
size: size,
|
||||
request: const AdRequest(),
|
||||
listener: BannerAdListener(
|
||||
|
||||
@@ -31,8 +31,10 @@ class PreloadedBannerAd {
|
||||
Future<BannerAd> get ready => _adCompleter.future;
|
||||
|
||||
Future<void> load() {
|
||||
assert(Platform.isAndroid || Platform.isIOS,
|
||||
'AdMob currently does not support ${Platform.operatingSystem}');
|
||||
assert(
|
||||
Platform.isAndroid || Platform.isIOS,
|
||||
'AdMob currently does not support ${Platform.operatingSystem}',
|
||||
);
|
||||
|
||||
_bannerAd = BannerAd(
|
||||
// This is a test ad unit ID from
|
||||
|
||||
@@ -19,8 +19,9 @@ class _AppLifecycleObserverState extends State<AppLifecycleObserver>
|
||||
with WidgetsBindingObserver {
|
||||
static final _log = Logger('AppLifecycleObserver');
|
||||
|
||||
final ValueNotifier<AppLifecycleState> lifecycleListenable =
|
||||
ValueNotifier(AppLifecycleState.inactive);
|
||||
final ValueNotifier<AppLifecycleState> lifecycleListenable = ValueNotifier(
|
||||
AppLifecycleState.inactive,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -43,12 +43,13 @@ class AudioController {
|
||||
/// Background music does not count into the [polyphony] limit. Music will
|
||||
/// never be overridden by sound effects because that would be silly.
|
||||
AudioController({int polyphony = 2})
|
||||
: assert(polyphony >= 1),
|
||||
_musicPlayer = AudioPlayer(playerId: 'musicPlayer'),
|
||||
_sfxPlayers = Iterable.generate(
|
||||
polyphony, (i) => AudioPlayer(playerId: 'sfxPlayer#$i'))
|
||||
.toList(growable: false),
|
||||
_playlist = Queue.of(List<Song>.of(songs)..shuffle()) {
|
||||
: assert(polyphony >= 1),
|
||||
_musicPlayer = AudioPlayer(playerId: 'musicPlayer'),
|
||||
_sfxPlayers = Iterable.generate(
|
||||
polyphony,
|
||||
(i) => AudioPlayer(playerId: 'sfxPlayer#$i'),
|
||||
).toList(growable: false),
|
||||
_playlist = Queue.of(List<Song>.of(songs)..shuffle()) {
|
||||
_musicPlayer.onPlayerComplete.listen(_changeSong);
|
||||
}
|
||||
|
||||
@@ -56,7 +57,8 @@ class AudioController {
|
||||
/// and therefore do things like stopping playback when the game
|
||||
/// goes into the background.
|
||||
void attachLifecycleNotifier(
|
||||
ValueNotifier<AppLifecycleState> lifecycleNotifier) {
|
||||
ValueNotifier<AppLifecycleState> lifecycleNotifier,
|
||||
) {
|
||||
_lifecycleNotifier?.removeListener(_handleAppLifecycle);
|
||||
|
||||
lifecycleNotifier.addListener(_handleAppLifecycle);
|
||||
@@ -108,10 +110,12 @@ class AudioController {
|
||||
// This assumes there is only a limited number of sound effects in the game.
|
||||
// If there are hundreds of long sound effect files, it's better
|
||||
// to be more selective when preloading.
|
||||
await AudioCache.instance.loadAll(SfxType.values
|
||||
.expand(soundTypeToFilename)
|
||||
.map((path) => 'sfx/$path')
|
||||
.toList());
|
||||
await AudioCache.instance.loadAll(
|
||||
SfxType.values
|
||||
.expand(soundTypeToFilename)
|
||||
.map((path) => 'sfx/$path')
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Plays a single sound effect, defined by [type].
|
||||
@@ -127,8 +131,9 @@ class AudioController {
|
||||
}
|
||||
final soundsOn = _settings?.soundsOn.value ?? false;
|
||||
if (!soundsOn) {
|
||||
_log.info(() =>
|
||||
'Ignoring playing sound ($type) because sounds are turned off.');
|
||||
_log.info(
|
||||
() => 'Ignoring playing sound ($type) because sounds are turned off.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,8 +143,10 @@ class AudioController {
|
||||
_log.info(() => '- Chosen filename: $filename');
|
||||
|
||||
final currentPlayer = _sfxPlayers[_currentSfxPlayer];
|
||||
currentPlayer.play(AssetSource('sfx/$filename'),
|
||||
volume: soundTypeToVolume(type));
|
||||
currentPlayer.play(
|
||||
AssetSource('sfx/$filename'),
|
||||
volume: soundTypeToVolume(type),
|
||||
);
|
||||
_currentSfxPlayer = (_currentSfxPlayer + 1) % _sfxPlayers.length;
|
||||
}
|
||||
|
||||
@@ -209,17 +216,23 @@ class AudioController {
|
||||
await _playFirstSongInPlaylist();
|
||||
}
|
||||
case PlayerState.stopped:
|
||||
_log.info("resumeMusic() called when music is stopped. "
|
||||
"This probably means we haven't yet started the music. "
|
||||
"For example, the game was started with sound off.");
|
||||
_log.info(
|
||||
"resumeMusic() called when music is stopped. "
|
||||
"This probably means we haven't yet started the music. "
|
||||
"For example, the game was started with sound off.",
|
||||
);
|
||||
await _playFirstSongInPlaylist();
|
||||
case PlayerState.playing:
|
||||
_log.warning('resumeMusic() called when music is playing. '
|
||||
'Nothing to do.');
|
||||
_log.warning(
|
||||
'resumeMusic() called when music is playing. '
|
||||
'Nothing to do.',
|
||||
);
|
||||
case PlayerState.completed:
|
||||
_log.warning('resumeMusic() called when music is completed. '
|
||||
"Music should never be 'completed' as it's either not playing "
|
||||
"or looping forever.");
|
||||
_log.warning(
|
||||
'resumeMusic() called when music is completed. '
|
||||
"Music should never be 'completed' as it's either not playing "
|
||||
"or looping forever.",
|
||||
);
|
||||
await _playFirstSongInPlaylist();
|
||||
default:
|
||||
_log.warning('Unhandled PlayerState: ${_musicPlayer.state}');
|
||||
|
||||
@@ -5,11 +5,7 @@
|
||||
List<String> soundTypeToFilename(SfxType type) {
|
||||
switch (type) {
|
||||
case SfxType.huhsh:
|
||||
return const [
|
||||
'hash1.mp3',
|
||||
'hash2.mp3',
|
||||
'hash3.mp3',
|
||||
];
|
||||
return const ['hash1.mp3', 'hash2.mp3', 'hash3.mp3'];
|
||||
case SfxType.wssh:
|
||||
return const [
|
||||
'wssh1.mp3',
|
||||
@@ -22,27 +18,13 @@ List<String> soundTypeToFilename(SfxType type) {
|
||||
'kss1.mp3',
|
||||
];
|
||||
case SfxType.buttonTap:
|
||||
return const [
|
||||
'k1.mp3',
|
||||
'k2.mp3',
|
||||
'p1.mp3',
|
||||
'p2.mp3',
|
||||
];
|
||||
return const ['k1.mp3', 'k2.mp3', 'p1.mp3', 'p2.mp3'];
|
||||
case SfxType.congrats:
|
||||
return const [
|
||||
'yay1.mp3',
|
||||
'wehee1.mp3',
|
||||
'oo1.mp3',
|
||||
];
|
||||
return const ['yay1.mp3', 'wehee1.mp3', 'oo1.mp3'];
|
||||
case SfxType.erase:
|
||||
return const [
|
||||
'fwfwfwfwfw1.mp3',
|
||||
'fwfwfwfw1.mp3',
|
||||
];
|
||||
return const ['fwfwfwfwfw1.mp3', 'fwfwfwfw1.mp3'];
|
||||
case SfxType.swishSwish:
|
||||
return const [
|
||||
'swishswish1.mp3',
|
||||
];
|
||||
return const ['swishswish1.mp3'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,11 +43,4 @@ double soundTypeToVolume(SfxType type) {
|
||||
}
|
||||
}
|
||||
|
||||
enum SfxType {
|
||||
huhsh,
|
||||
wssh,
|
||||
buttonTap,
|
||||
congrats,
|
||||
erase,
|
||||
swishSwish,
|
||||
}
|
||||
enum SfxType { huhsh, wssh, buttonTap, congrats, erase, swishSwish }
|
||||
|
||||
@@ -28,8 +28,10 @@ class GamesServicesController {
|
||||
///
|
||||
/// Does nothing when the game isn't signed into the underlying
|
||||
/// games service.
|
||||
Future<void> awardAchievement(
|
||||
{required String iOS, required String android}) async {
|
||||
Future<void> awardAchievement({
|
||||
required String iOS,
|
||||
required String android,
|
||||
}) async {
|
||||
if (!await signedIn) {
|
||||
_log.warning('Trying to award achievement when not logged in.');
|
||||
return;
|
||||
@@ -37,10 +39,7 @@ class GamesServicesController {
|
||||
|
||||
try {
|
||||
await gs.GamesServices.unlock(
|
||||
achievement: gs.Achievement(
|
||||
androidID: android,
|
||||
iOSID: iOS,
|
||||
),
|
||||
achievement: gs.Achievement(androidID: android, iOSID: iOS),
|
||||
);
|
||||
} catch (e) {
|
||||
_log.severe('Cannot award achievement: $e');
|
||||
|
||||
@@ -37,9 +37,12 @@ class Score {
|
||||
buf.write('$minutes');
|
||||
}
|
||||
buf.write(':');
|
||||
buf.write((duration.inSeconds % Duration.secondsPerMinute)
|
||||
.toString()
|
||||
.padLeft(2, '0'));
|
||||
buf.write(
|
||||
(duration.inSeconds % Duration.secondsPerMinute).toString().padLeft(
|
||||
2,
|
||||
'0',
|
||||
),
|
||||
);
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -47,12 +47,15 @@ class InAppPurchaseController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
_log.info('Querying the store with queryProductDetails()');
|
||||
final response = await inAppPurchaseInstance
|
||||
.queryProductDetails({AdRemovalPurchase.productId});
|
||||
final response = await inAppPurchaseInstance.queryProductDetails({
|
||||
AdRemovalPurchase.productId,
|
||||
});
|
||||
|
||||
if (response.error != null) {
|
||||
_reportError('There was an error when making the purchase: '
|
||||
'${response.error}');
|
||||
_reportError(
|
||||
'There was an error when making the purchase: '
|
||||
'${response.error}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,8 +64,10 @@ class InAppPurchaseController extends ChangeNotifier {
|
||||
'Products in response: '
|
||||
'${response.productDetails.map((e) => '${e.id}: ${e.title}, ').join()}',
|
||||
);
|
||||
_reportError('There was an error when making the purchase: '
|
||||
'product ${AdRemovalPurchase.productId} does not exist?');
|
||||
_reportError(
|
||||
'There was an error when making the purchase: '
|
||||
'product ${AdRemovalPurchase.productId} does not exist?',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final productDetails = response.productDetails.single;
|
||||
@@ -71,14 +76,16 @@ class InAppPurchaseController extends ChangeNotifier {
|
||||
final purchaseParam = PurchaseParam(productDetails: productDetails);
|
||||
try {
|
||||
final success = await inAppPurchaseInstance.buyNonConsumable(
|
||||
purchaseParam: purchaseParam);
|
||||
purchaseParam: purchaseParam,
|
||||
);
|
||||
_log.info('buyNonConsumable() request was sent with success: $success');
|
||||
// The result of the purchase will be reported in the purchaseStream,
|
||||
// which is handled in [_listenToPurchaseUpdated].
|
||||
} catch (e) {
|
||||
_log.severe(
|
||||
'Problem with calling inAppPurchaseInstance.buyNonConsumable(): '
|
||||
'$e');
|
||||
'Problem with calling inAppPurchaseInstance.buyNonConsumable(): '
|
||||
'$e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,29 +114,38 @@ class InAppPurchaseController extends ChangeNotifier {
|
||||
/// Subscribes to the [inAppPurchaseInstance.purchaseStream].
|
||||
void subscribe() {
|
||||
_subscription?.cancel();
|
||||
_subscription =
|
||||
inAppPurchaseInstance.purchaseStream.listen((purchaseDetailsList) {
|
||||
_listenToPurchaseUpdated(purchaseDetailsList);
|
||||
}, onDone: () {
|
||||
_subscription?.cancel();
|
||||
}, onError: (dynamic error) {
|
||||
_log.severe('Error occurred on the purchaseStream: $error');
|
||||
});
|
||||
_subscription = inAppPurchaseInstance.purchaseStream.listen(
|
||||
(purchaseDetailsList) {
|
||||
_listenToPurchaseUpdated(purchaseDetailsList);
|
||||
},
|
||||
onDone: () {
|
||||
_subscription?.cancel();
|
||||
},
|
||||
onError: (dynamic error) {
|
||||
_log.severe('Error occurred on the purchaseStream: $error');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _listenToPurchaseUpdated(
|
||||
List<PurchaseDetails> purchaseDetailsList) async {
|
||||
List<PurchaseDetails> purchaseDetailsList,
|
||||
) async {
|
||||
for (final purchaseDetails in purchaseDetailsList) {
|
||||
_log.info(() => 'New PurchaseDetails instance received: '
|
||||
'productID=${purchaseDetails.productID}, '
|
||||
'status=${purchaseDetails.status}, '
|
||||
'purchaseID=${purchaseDetails.purchaseID}, '
|
||||
'error=${purchaseDetails.error}, '
|
||||
'pendingCompletePurchase=${purchaseDetails.pendingCompletePurchase}');
|
||||
_log.info(
|
||||
() =>
|
||||
'New PurchaseDetails instance received: '
|
||||
'productID=${purchaseDetails.productID}, '
|
||||
'status=${purchaseDetails.status}, '
|
||||
'purchaseID=${purchaseDetails.purchaseID}, '
|
||||
'error=${purchaseDetails.error}, '
|
||||
'pendingCompletePurchase=${purchaseDetails.pendingCompletePurchase}',
|
||||
);
|
||||
|
||||
if (purchaseDetails.productID != AdRemovalPurchase.productId) {
|
||||
_log.severe("The handling of the product with id "
|
||||
"'${purchaseDetails.productID}' is not implemented.");
|
||||
_log.severe(
|
||||
"The handling of the product with id "
|
||||
"'${purchaseDetails.productID}' is not implemented.",
|
||||
);
|
||||
_adRemoval = const AdRemovalPurchase.notStarted();
|
||||
notifyListeners();
|
||||
continue;
|
||||
@@ -151,7 +167,8 @@ class InAppPurchaseController extends ChangeNotifier {
|
||||
} else {
|
||||
_log.severe('Purchase verification failed: $purchaseDetails');
|
||||
_adRemoval = AdRemovalPurchase.error(
|
||||
StateError('Purchase could not be verified'));
|
||||
StateError('Purchase could not be verified'),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
case PurchaseStatus.error:
|
||||
|
||||
@@ -31,8 +31,10 @@ class LevelSelectionScreen extends StatelessWidget {
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Select level',
|
||||
style:
|
||||
TextStyle(fontFamily: 'Permanent Marker', fontSize: 30),
|
||||
style: TextStyle(
|
||||
fontFamily: 'Permanent Marker',
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -42,18 +44,20 @@ class LevelSelectionScreen extends StatelessWidget {
|
||||
children: [
|
||||
for (final level in gameLevels)
|
||||
ListTile(
|
||||
enabled: playerProgress.highestLevelReached >=
|
||||
enabled:
|
||||
playerProgress.highestLevelReached >=
|
||||
level.number - 1,
|
||||
onTap: () {
|
||||
final audioController = context.read<AudioController>();
|
||||
audioController.playSfx(SfxType.buttonTap);
|
||||
|
||||
GoRouter.of(context)
|
||||
.go('/play/session/${level.number}');
|
||||
GoRouter.of(
|
||||
context,
|
||||
).go('/play/session/${level.number}');
|
||||
},
|
||||
leading: Text(level.number.toString()),
|
||||
title: Text('Level #${level.number}'),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -12,10 +12,7 @@ const gameLevels = [
|
||||
// You get this string when you configure an achievement in Play Console.
|
||||
achievementIdAndroid: 'NhkIwB69ejkMAOOLDb',
|
||||
),
|
||||
GameLevel(
|
||||
number: 2,
|
||||
difficulty: 42,
|
||||
),
|
||||
GameLevel(number: 2, difficulty: 42),
|
||||
GameLevel(
|
||||
number: 3,
|
||||
difficulty: 100,
|
||||
@@ -42,8 +39,9 @@ class GameLevel {
|
||||
this.achievementIdIOS,
|
||||
this.achievementIdAndroid,
|
||||
}) : assert(
|
||||
(achievementIdAndroid != null && achievementIdIOS != null) ||
|
||||
(achievementIdAndroid == null && achievementIdIOS == null),
|
||||
'Either both iOS and Android achievement ID must be provided, '
|
||||
'or none');
|
||||
(achievementIdAndroid != null && achievementIdIOS != null) ||
|
||||
(achievementIdAndroid == null && achievementIdIOS == null),
|
||||
'Either both iOS and Android achievement ID must be provided, '
|
||||
'or none',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,10 +48,9 @@ class _PlaySessionScreenState extends State<PlaySessionScreen> {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => LevelState(
|
||||
goal: widget.level.difficulty,
|
||||
onWin: _playerWon,
|
||||
),
|
||||
create:
|
||||
(context) =>
|
||||
LevelState(goal: widget.level.difficulty, onWin: _playerWon),
|
||||
),
|
||||
],
|
||||
child: IgnorePointer(
|
||||
@@ -76,17 +75,22 @@ class _PlaySessionScreenState extends State<PlaySessionScreen> {
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text('Drag the slider to ${widget.level.difficulty}%'
|
||||
' or above!'),
|
||||
Text(
|
||||
'Drag the slider to ${widget.level.difficulty}%'
|
||||
' or above!',
|
||||
),
|
||||
Consumer<LevelState>(
|
||||
builder: (context, levelState, child) => Slider(
|
||||
label: 'Level Progress',
|
||||
autofocus: true,
|
||||
value: levelState.progress / 100,
|
||||
onChanged: (value) =>
|
||||
levelState.setProgress((value * 100).round()),
|
||||
onChangeEnd: (value) => levelState.evaluate(),
|
||||
),
|
||||
builder:
|
||||
(context, levelState, child) => Slider(
|
||||
label: 'Level Progress',
|
||||
autofocus: true,
|
||||
value: levelState.progress / 100,
|
||||
onChanged:
|
||||
(value) => levelState.setProgress(
|
||||
(value * 100).round(),
|
||||
),
|
||||
onChangeEnd: (value) => levelState.evaluate(),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
@@ -106,9 +110,7 @@ class _PlaySessionScreenState extends State<PlaySessionScreen> {
|
||||
child: Visibility(
|
||||
visible: _duringCelebration,
|
||||
child: IgnorePointer(
|
||||
child: Confetti(
|
||||
isStopped: !_duringCelebration,
|
||||
),
|
||||
child: Confetti(isStopped: !_duringCelebration),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -9,9 +9,11 @@ import 'settings.dart';
|
||||
|
||||
void showCustomNameDialog(BuildContext context) {
|
||||
showGeneralDialog(
|
||||
context: context,
|
||||
pageBuilder: (context, animation, secondaryAnimation) =>
|
||||
CustomNameDialog(animation: animation));
|
||||
context: context,
|
||||
pageBuilder:
|
||||
(context, animation, secondaryAnimation) =>
|
||||
CustomNameDialog(animation: animation),
|
||||
);
|
||||
}
|
||||
|
||||
class CustomNameDialog extends StatefulWidget {
|
||||
|
||||
@@ -23,7 +23,7 @@ class SettingsController {
|
||||
|
||||
/// Creates a new instance of [SettingsController] backed by [persistence].
|
||||
SettingsController({required SettingsPersistence persistence})
|
||||
: _persistence = persistence;
|
||||
: _persistence = persistence;
|
||||
|
||||
/// Asynchronously loads values from the injected persistence store.
|
||||
Future<void> loadStateFromPersistence() async {
|
||||
|
||||
@@ -39,52 +39,49 @@ class SettingsScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
_gap,
|
||||
const _NameChangeLine(
|
||||
'Name',
|
||||
),
|
||||
const _NameChangeLine('Name'),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: settings.soundsOn,
|
||||
builder: (context, soundsOn, child) => _SettingsLine(
|
||||
'Sound FX',
|
||||
Icon(soundsOn ? Icons.graphic_eq : Icons.volume_off),
|
||||
onSelected: () => settings.toggleSoundsOn(),
|
||||
),
|
||||
builder:
|
||||
(context, soundsOn, child) => _SettingsLine(
|
||||
'Sound FX',
|
||||
Icon(soundsOn ? Icons.graphic_eq : Icons.volume_off),
|
||||
onSelected: () => settings.toggleSoundsOn(),
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: settings.musicOn,
|
||||
builder: (context, musicOn, child) => _SettingsLine(
|
||||
'Music',
|
||||
Icon(musicOn ? Icons.music_note : Icons.music_off),
|
||||
onSelected: () => settings.toggleMusicOn(),
|
||||
),
|
||||
builder:
|
||||
(context, musicOn, child) => _SettingsLine(
|
||||
'Music',
|
||||
Icon(musicOn ? Icons.music_note : Icons.music_off),
|
||||
onSelected: () => settings.toggleMusicOn(),
|
||||
),
|
||||
),
|
||||
Consumer<InAppPurchaseController?>(
|
||||
builder: (context, inAppPurchase, child) {
|
||||
if (inAppPurchase == null) {
|
||||
// In-app purchases are not supported yet.
|
||||
// Go to lib/main.dart and uncomment the lines that create
|
||||
// the InAppPurchaseController.
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
builder: (context, inAppPurchase, child) {
|
||||
if (inAppPurchase == null) {
|
||||
// In-app purchases are not supported yet.
|
||||
// Go to lib/main.dart and uncomment the lines that create
|
||||
// the InAppPurchaseController.
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget icon;
|
||||
VoidCallback? callback;
|
||||
if (inAppPurchase.adRemoval.active) {
|
||||
icon = const Icon(Icons.check);
|
||||
} else if (inAppPurchase.adRemoval.pending) {
|
||||
icon = const CircularProgressIndicator();
|
||||
} else {
|
||||
icon = const Icon(Icons.ad_units);
|
||||
callback = () {
|
||||
inAppPurchase.buy();
|
||||
};
|
||||
}
|
||||
return _SettingsLine(
|
||||
'Remove ads',
|
||||
icon,
|
||||
onSelected: callback,
|
||||
);
|
||||
}),
|
||||
Widget icon;
|
||||
VoidCallback? callback;
|
||||
if (inAppPurchase.adRemoval.active) {
|
||||
icon = const Icon(Icons.check);
|
||||
} else if (inAppPurchase.adRemoval.pending) {
|
||||
icon = const CircularProgressIndicator();
|
||||
} else {
|
||||
icon = const Icon(Icons.ad_units);
|
||||
callback = () {
|
||||
inAppPurchase.buy();
|
||||
};
|
||||
}
|
||||
return _SettingsLine('Remove ads', icon, onSelected: callback);
|
||||
},
|
||||
),
|
||||
_SettingsLine(
|
||||
'Reset progress',
|
||||
const Icon(Icons.delete),
|
||||
@@ -94,7 +91,8 @@ class SettingsScreen extends StatelessWidget {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Player progress has been reset.')),
|
||||
content: Text('Player progress has been reset.'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -129,21 +127,24 @@ class _NameChangeLine extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Permanent Marker',
|
||||
fontSize: 30,
|
||||
)),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Permanent Marker',
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: settings.playerName,
|
||||
builder: (context, name, child) => Text(
|
||||
'‘$name’',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Permanent Marker',
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
builder:
|
||||
(context, name, child) => Text(
|
||||
'‘$name’',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Permanent Marker',
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -52,21 +52,21 @@ class ConfettiPainter extends CustomPainter {
|
||||
|
||||
final UnmodifiableListView<Color> colors;
|
||||
|
||||
ConfettiPainter(
|
||||
{required Listenable animation, required Iterable<Color> colors})
|
||||
: colors = UnmodifiableListView(colors),
|
||||
super(repaint: animation);
|
||||
ConfettiPainter({
|
||||
required Listenable animation,
|
||||
required Iterable<Color> colors,
|
||||
}) : colors = UnmodifiableListView(colors),
|
||||
super(repaint: animation);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (_size == null) {
|
||||
// First time we have a size.
|
||||
_snippings = List.generate(
|
||||
snippingsCount,
|
||||
(i) => _PaperSnipping(
|
||||
frontColor: colors[i % colors.length],
|
||||
bounds: size,
|
||||
));
|
||||
snippingsCount,
|
||||
(i) =>
|
||||
_PaperSnipping(frontColor: colors[i % colors.length], bounds: size),
|
||||
);
|
||||
}
|
||||
|
||||
final didResize = _size != null && _size != size;
|
||||
@@ -97,10 +97,7 @@ class _ConfettiState extends State<Confetti>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
painter: ConfettiPainter(
|
||||
colors: widget.colors,
|
||||
animation: _controller,
|
||||
),
|
||||
painter: ConfettiPainter(colors: widget.colors, animation: _controller),
|
||||
willChange: true,
|
||||
child: const SizedBox.expand(),
|
||||
);
|
||||
@@ -181,10 +178,8 @@ class _PaperSnipping {
|
||||
|
||||
final paint = Paint()..style = PaintingStyle.fill;
|
||||
|
||||
_PaperSnipping({
|
||||
required this.frontColor,
|
||||
required Size bounds,
|
||||
}) : _bounds = bounds;
|
||||
_PaperSnipping({required this.frontColor, required Size bounds})
|
||||
: _bounds = bounds;
|
||||
|
||||
void draw(Canvas canvas) {
|
||||
if (cosA > 0) {
|
||||
@@ -193,16 +188,17 @@ class _PaperSnipping {
|
||||
paint.color = backColor;
|
||||
}
|
||||
|
||||
final path = Path()
|
||||
..addPolygon(
|
||||
List.generate(
|
||||
final path =
|
||||
Path()..addPolygon(
|
||||
List.generate(
|
||||
4,
|
||||
(index) => Offset(
|
||||
position.x + corners[index].x * size,
|
||||
position.y + corners[index].y * size * cosA,
|
||||
)),
|
||||
true,
|
||||
);
|
||||
position.x + corners[index].x * size,
|
||||
position.y + corners[index].y * size * cosA,
|
||||
),
|
||||
),
|
||||
true,
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,7 @@ CustomTransitionPage<T> buildMyTransition<T>({
|
||||
return CustomTransitionPage<T>(
|
||||
child: child,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return _MyReveal(
|
||||
animation: animation,
|
||||
color: color,
|
||||
child: child,
|
||||
);
|
||||
return _MyReveal(animation: animation, color: color, child: child);
|
||||
},
|
||||
key: key,
|
||||
name: name,
|
||||
@@ -90,9 +86,7 @@ class _MyRevealState extends State<_MyReveal> {
|
||||
reverseCurve: Curves.easeOutCubic,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
color: widget.color,
|
||||
),
|
||||
child: Container(color: widget.color),
|
||||
),
|
||||
AnimatedOpacity(
|
||||
opacity: _finished ? 1 : 0,
|
||||
|
||||
@@ -46,10 +46,7 @@ class ResponsiveScreen extends StatelessWidget {
|
||||
children: [
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: topMessageArea,
|
||||
),
|
||||
child: Padding(padding: padding, child: topMessageArea),
|
||||
),
|
||||
Expanded(
|
||||
flex: (mainAreaProminence * 100).round(),
|
||||
@@ -63,10 +60,7 @@ class ResponsiveScreen extends StatelessWidget {
|
||||
SafeArea(
|
||||
top: false,
|
||||
maintainBottomViewPadding: true,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: rectangularMenuArea,
|
||||
),
|
||||
child: Padding(padding: padding, child: rectangularMenuArea),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -94,10 +88,7 @@ class ResponsiveScreen extends StatelessWidget {
|
||||
bottom: false,
|
||||
left: false,
|
||||
maintainBottomViewPadding: true,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: topMessageArea,
|
||||
),
|
||||
child: Padding(padding: padding, child: topMessageArea),
|
||||
),
|
||||
Expanded(
|
||||
child: SafeArea(
|
||||
@@ -112,7 +103,7 @@ class ResponsiveScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,11 +8,10 @@ import 'package:flutter/material.dart';
|
||||
/// with global key [scaffoldMessengerKey] is anywhere in the widget tree.
|
||||
void showSnackBar(String message) {
|
||||
final messenger = scaffoldMessengerKey.currentState;
|
||||
messenger?.showSnackBar(
|
||||
SnackBar(content: Text(message)),
|
||||
);
|
||||
messenger?.showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
|
||||
/// Use this when creating [MaterialApp] if you want [showSnackBar] to work.
|
||||
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
|
||||
GlobalKey(debugLabel: 'scaffoldMessengerKey');
|
||||
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey(
|
||||
debugLabel: 'scaffoldMessengerKey',
|
||||
);
|
||||
|
||||
@@ -16,10 +16,7 @@ import '../style/responsive_screen.dart';
|
||||
class WinGameScreen extends StatelessWidget {
|
||||
final Score score;
|
||||
|
||||
const WinGameScreen({
|
||||
super.key,
|
||||
required this.score,
|
||||
});
|
||||
const WinGameScreen({super.key, required this.score});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -37,11 +34,7 @@ class WinGameScreen extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
if (adsControllerAvailable && !adsRemoved) ...[
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: BannerAdWidget(),
|
||||
),
|
||||
),
|
||||
const Expanded(child: Center(child: BannerAdWidget())),
|
||||
],
|
||||
gap,
|
||||
const Center(
|
||||
@@ -56,7 +49,9 @@ class WinGameScreen extends StatelessWidget {
|
||||
'Score: ${score.score}\n'
|
||||
'Time: ${score.formattedTime}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Permanent Marker', fontSize: 20),
|
||||
fontFamily: 'Permanent Marker',
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user