diff --git a/web/slide_puzzle/asset/fonts/plaster/OFL.txt b/web/slide_puzzle/asset/fonts/plaster/OFL.txt index f074fc5df..e0d637407 100755 --- a/web/slide_puzzle/asset/fonts/plaster/OFL.txt +++ b/web/slide_puzzle/asset/fonts/plaster/OFL.txt @@ -1,94 +1,94 @@ -Copyright (c) 2011 by Sorkin Type Co (www.sorkintype.com), -with Reserved Font Name "Plaster". - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright (c) 2011 by Sorkin Type Co (www.sorkintype.com), +with Reserved Font Name "Plaster". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/web/slide_puzzle/assets/preview.png b/web/slide_puzzle/assets/preview.png deleted file mode 100644 index 0f55f737a..000000000 Binary files a/web/slide_puzzle/assets/preview.png and /dev/null differ diff --git a/web/slide_puzzle/lib/main.dart b/web/slide_puzzle/lib/main.dart index dc9b8aa08..673cc7cf9 100644 --- a/web/slide_puzzle/lib/main.dart +++ b/web/slide_puzzle/lib/main.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'src/core/puzzle_animator.dart'; +import 'src/flutter.dart'; import 'src/puzzle_home_state.dart'; void main() => runApp(PuzzleApp()); @@ -21,7 +21,7 @@ class PuzzleApp extends StatelessWidget { class _PuzzleHome extends StatefulWidget { final int _rows, _columns; - const _PuzzleHome(this._rows, this._columns, {Key key}) : super(key: key); + const _PuzzleHome(this._rows, this._columns); @override PuzzleHomeState createState() => diff --git a/web/slide_puzzle/lib/src/app_state.dart b/web/slide_puzzle/lib/src/app_state.dart index efef589ed..f9860773c 100644 --- a/web/slide_puzzle/lib/src/app_state.dart +++ b/web/slide_puzzle/lib/src/app_state.dart @@ -1,29 +1,9 @@ -import 'core/puzzle_animator.dart'; -import 'package:flutter/material.dart'; -import 'shared_theme.dart'; +import 'package:flutter/foundation.dart'; -abstract class AppState { - TabController get tabController; - - Animation get shuffleOffsetAnimation; +import 'core/puzzle_proxy.dart'; +abstract class AppState { PuzzleProxy get puzzle; - bool get autoPlay; - - void setAutoPlay(bool newValue); - - AnimationNotifier get animationNotifier; - - Iterable get themeData; - - SharedTheme get currentTheme; - - set currentTheme(SharedTheme theme); -} - -abstract class AnimationNotifier implements Listenable { - void animate(); - - void dispose(); + Listenable get animationNotifier; } diff --git a/web/slide_puzzle/lib/src/core/puzzle.dart b/web/slide_puzzle/lib/src/core/puzzle.dart index 4419dc9e6..d758fba3a 100644 --- a/web/slide_puzzle/lib/src/core/puzzle.dart +++ b/web/slide_puzzle/lib/src/core/puzzle.dart @@ -123,8 +123,7 @@ abstract class Puzzle { value += delta * delta; } } - value *= incorrectTiles; - return value; + return value * incorrectTiles; } Puzzle clickRandom({bool vertical}) { @@ -137,8 +136,8 @@ abstract class Puzzle { List clickableValues({bool vertical}) { final open = openPosition(); - final doRow = (vertical == null || vertical == false); - final doColumn = (vertical == null || vertical); + final doRow = vertical == null || vertical == false; + final doColumn = vertical == null || vertical; final values = Uint8List((doRow ? (width - 1) : 0) + (doColumn ? (height - 1) : 0)); diff --git a/web/slide_puzzle/lib/src/core/puzzle_animator.dart b/web/slide_puzzle/lib/src/core/puzzle_animator.dart index 3646e4d6e..9197c1e3f 100644 --- a/web/slide_puzzle/lib/src/core/puzzle_animator.dart +++ b/web/slide_puzzle/lib/src/core/puzzle_animator.dart @@ -3,32 +3,7 @@ import 'dart:math' show Point, Random; import 'body.dart'; import 'puzzle.dart'; - -enum PuzzleEvent { click, reset, noop } - -abstract class PuzzleProxy { - int get width; - - int get height; - - int get length; - - bool get solved; - - void reset(); - - void clickOrShake(int tileValue); - - int get tileCount; - - int get clickCount; - - int get incorrectTiles; - - Point location(int index); - - bool isCorrectPosition(int value); -} +import 'puzzle_proxy.dart'; class PuzzleAnimator implements PuzzleProxy { final _rnd = Random(); @@ -57,13 +32,10 @@ class PuzzleAnimator implements PuzzleProxy { @override int get tileCount => _puzzle.tileCount; - @override int get incorrectTiles => _puzzle.incorrectTiles; - @override int get clickCount => _clickCount; - @override void reset() => _resetCore(); Stream get onEvent => _controller.stream; @@ -93,7 +65,7 @@ class PuzzleAnimator implements PuzzleProxy { _puzzle = _puzzle.clickRandom(vertical: _nextRandomVertical); _nextRandomVertical = !_nextRandomVertical; _clickCount++; - _controller.add(PuzzleEvent.click); + _controller.add(PuzzleEvent.random); } @override @@ -165,7 +137,7 @@ class PuzzleAnimator implements PuzzleProxy { final delta = _puzzle.openPosition() - _puzzle.coordinatesOf(tileValue); deltaDouble = Point(delta.x.toDouble(), delta.y.toDouble()); } - deltaDouble *= (0.5 / deltaDouble.magnitude); + deltaDouble *= 0.5 / deltaDouble.magnitude; _locations[tileValue].kick(deltaDouble); } diff --git a/web/slide_puzzle/lib/src/frame_nanny.dart b/web/slide_puzzle/lib/src/frame_nanny.dart deleted file mode 100644 index 8c58e7de0..000000000 --- a/web/slide_puzzle/lib/src/frame_nanny.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'dart:collection'; - -class FrameNanny { - static const _bufferSize = 200; - static const _maxFrameDuration = Duration(milliseconds: 34); - final _buffer = ListQueue(_bufferSize); - final _watch = Stopwatch(); - - Duration tick(Duration source) { - _watch.start(); - _buffer.add(source); - - while (_buffer.length > _bufferSize) { - _buffer.removeFirst(); - } - - if (source > _maxFrameDuration) { - source = _maxFrameDuration; - } - - if (_watch.elapsed > const Duration(seconds: 2)) { - var goodCount = 0; - var sum = const Duration(); - Duration best, worst; - - for (var e in _buffer) { - sum += e; - if (e <= _maxFrameDuration) { - goodCount++; - } - - if (best == null || e < best) { - best = e; - } - - if (worst == null || e > worst) { - worst = e; - } - } - - _watch.reset(); - print([ - '**Nanny**', - '${(100 * goodCount / _buffer.length).toStringAsFixed(1)}%', - '<= ${_maxFrameDuration.inMilliseconds}ms', - 'best:', - best?.inMilliseconds, - 'avg:', - _safeDivide(sum, _buffer.length), - 'worst', - worst?.inMilliseconds - ].join(' ')); - } - return source; - } -} - -Object _safeDivide(Duration source, int divisor) { - if (divisor == 0) { - return double.nan; - } - return (source ~/ divisor).inMilliseconds; -} diff --git a/web/slide_puzzle/lib/src/puzzle_flow_delegate.dart b/web/slide_puzzle/lib/src/puzzle_flow_delegate.dart index 348dbd6e2..7f19d1960 100644 --- a/web/slide_puzzle/lib/src/puzzle_flow_delegate.dart +++ b/web/slide_puzzle/lib/src/puzzle_flow_delegate.dart @@ -1,5 +1,5 @@ -import 'core/puzzle_animator.dart'; -import 'package:flutter/material.dart'; +import 'core/puzzle_proxy.dart'; +import 'flutter.dart'; class PuzzleFlowDelegate extends FlowDelegate { final Size _tileSize; diff --git a/web/slide_puzzle/lib/src/puzzle_home_state.dart b/web/slide_puzzle/lib/src/puzzle_home_state.dart index 8b69c73c8..6310d07f4 100644 --- a/web/slide_puzzle/lib/src/puzzle_home_state.dart +++ b/web/slide_puzzle/lib/src/puzzle_home_state.dart @@ -1,101 +1,80 @@ import 'dart:async'; -import 'dart:math' as math; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; +import 'package:provider/provider.dart'; import 'app_state.dart'; import 'core/puzzle_animator.dart'; -import 'frame_nanny.dart'; +import 'core/puzzle_proxy.dart'; +import 'flutter.dart'; +import 'puzzle_controls.dart'; +import 'puzzle_flow_delegate.dart'; import 'shared_theme.dart'; -import 'theme_plaster.dart'; -import 'theme_seattle.dart'; -import 'theme_simple.dart'; +import 'themes.dart'; +import 'value_tab_controller.dart'; -class PuzzleHomeState extends State - with TickerProviderStateMixin - implements AppState { - TabController _tabController; - AnimationController _controller; - Animation _shuffleOffsetAnimation; +class _PuzzleControls extends ChangeNotifier implements PuzzleControls { + final PuzzleHomeState _parent; - @override - Animation get shuffleOffsetAnimation => _shuffleOffsetAnimation; + _PuzzleControls(this._parent); @override - final PuzzleAnimator puzzle; + bool get autoPlay => _parent._autoPlay; + + void _notify() => notifyListeners(); @override - final animationNotifier = _AnimationNotifier(); + void Function(bool newValue) get setAutoPlayFunction { + if (_parent.puzzle.solved) { + return null; + } + return _parent._setAutoPlay; + } @override - TabController get tabController => _tabController; + int get clickCount => _parent.puzzle.clickCount; - final _nanny = FrameNanny(); + @override + int get incorrectTiles => _parent.puzzle.incorrectTiles; - SharedTheme _currentTheme; + @override + void reset() => _parent.puzzle.reset(); +} +class PuzzleHomeState extends State + with SingleTickerProviderStateMixin, AppState { @override - SharedTheme get currentTheme => _currentTheme; + final PuzzleAnimator puzzle; @override - set currentTheme(SharedTheme theme) { - setState(() { - _currentTheme = theme; - }); - } + final _AnimationNotifier animationNotifier = _AnimationNotifier(); Duration _tickerTimeSinceLastEvent = Duration.zero; Ticker _ticker; Duration _lastElapsed; - StreamSubscription sub; + StreamSubscription _puzzleEventSubscription; - @override - bool autoPlay = false; + bool _autoPlay = false; + _PuzzleControls _autoPlayListenable; PuzzleHomeState(this.puzzle) { - sub = puzzle.onEvent.listen(_onPuzzleEvent); - - _themeDataCache = List.unmodifiable([ - ThemeSimple(this), - ThemeSeattle(this), - ThemePlaster(this), - ]); - - _currentTheme = themeData.first; + _puzzleEventSubscription = puzzle.onEvent.listen(_onPuzzleEvent); } @override void initState() { super.initState(); + _autoPlayListenable = _PuzzleControls(this); _ticker ??= createTicker(_onTick); _ensureTicking(); - - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 200), - ); - - _shuffleOffsetAnimation = _controller.drive(const _Shake()); - _tabController = TabController(vsync: this, length: _themeDataCache.length); - - _tabController.addListener(() { - currentTheme = _themeDataCache[_tabController.index]; - }); } - List _themeDataCache; - - @override - Iterable get themeData => _themeDataCache; - - @override - void setAutoPlay(bool newValue) { - if (newValue != autoPlay) { + void _setAutoPlay(bool newValue) { + if (newValue != _autoPlay) { setState(() { // Only allow enabling autoPlay if the puzzle is not solved - autoPlay = newValue && !puzzle.solved; - if (autoPlay) { + _autoPlayListenable._notify(); + _autoPlay = newValue && !puzzle.solved; + if (_autoPlay) { _ensureTicking(); } }); @@ -103,26 +82,46 @@ class PuzzleHomeState extends State } @override - Widget build(BuildContext context) => _currentTheme.build(context); + Widget build(BuildContext context) => MultiProvider( + providers: [ + Provider.value(value: this), + ListenableProvider.value( + listenable: _autoPlayListenable, + ) + ], + child: Material( + child: Stack( + children: [ + const SizedBox.expand( + child: FittedBox( + fit: BoxFit.cover, + child: Image( + image: AssetImage('asset/seattle.jpg'), + ), + ), + ), + const LayoutBuilder(builder: _doBuild), + ], + ), + ), + ); @override void dispose() { animationNotifier.dispose(); - _tabController.dispose(); - _controller?.dispose(); _ticker?.dispose(); - sub.cancel(); + _autoPlayListenable?.dispose(); + _puzzleEventSubscription.cancel(); super.dispose(); } void _onPuzzleEvent(PuzzleEvent e) { + _autoPlayListenable._notify(); + if (e != PuzzleEvent.random) { + _setAutoPlay(false); + } _tickerTimeSinceLastEvent = Duration.zero; _ensureTicking(); - if (e == PuzzleEvent.noop) { - assert(e == PuzzleEvent.noop); - _controller.reset(); - _controller.forward(); - } setState(() { // noop }); @@ -148,40 +147,135 @@ class PuzzleHomeState extends State } _tickerTimeSinceLastEvent += delta; - puzzle.update(_nanny.tick(delta)); + puzzle.update(delta > _maxFrameDuration ? _maxFrameDuration : delta); if (!puzzle.stable) { animationNotifier.animate(); } else { - if (!autoPlay) { + if (!_autoPlay) { _ticker.stop(); _lastElapsed = null; } } - if (autoPlay && + if (_autoPlay && _tickerTimeSinceLastEvent > const Duration(milliseconds: 200)) { puzzle.playRandom(); if (puzzle.solved) { - setAutoPlay(false); + _setAutoPlay(false); } } } } -class _Shake extends Animatable { - const _Shake(); - - @override - Offset transform(double t) => Offset(0.01 * math.sin(t * math.pi * 3), 0); -} - -class _AnimationNotifier extends ChangeNotifier implements AnimationNotifier { - _AnimationNotifier(); - - @override +class _AnimationNotifier extends ChangeNotifier { void animate() { notifyListeners(); } } + +const _maxFrameDuration = Duration(milliseconds: 34); + +Widget _updateConstraints( + BoxConstraints constraints, Widget Function(bool small) builder) { + const _smallWidth = 580; + + final constraintWidth = + constraints.hasBoundedWidth ? constraints.maxWidth : 1000.0; + + final constraintHeight = + constraints.hasBoundedHeight ? constraints.maxHeight : 1000.0; + + return builder(constraintWidth < _smallWidth || constraintHeight < 690); +} + +Widget _doBuild(BuildContext _, BoxConstraints constraints) => + _updateConstraints(constraints, _doBuildCore); + +Widget _doBuildCore(bool small) => ValueTabController( + values: themes, + child: Consumer( + builder: (_, theme, __) => AnimatedContainer( + duration: puzzleAnimationDuration, + color: theme.puzzleThemeBackground, + child: Center( + child: theme.styledWrapper( + small, + SizedBox( + width: 580, + child: Consumer( + builder: (context, appState, _) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.black26, + width: 1, + ), + ), + ), + margin: const EdgeInsets.symmetric(horizontal: 20), + child: TabBar( + controller: ValueTabController.of(context), + labelPadding: const EdgeInsets.fromLTRB(0, 20, 0, 12), + labelColor: theme.puzzleAccentColor, + indicatorColor: theme.puzzleAccentColor, + indicatorWeight: 1.5, + unselectedLabelColor: Colors.black.withOpacity(0.6), + tabs: themes + .map((st) => Text( + st.name.toUpperCase(), + style: const TextStyle( + letterSpacing: 0.5, + ), + )) + .toList(), + ), + ), + Flexible( + child: Container( + padding: const EdgeInsets.all(10), + child: Flow( + delegate: PuzzleFlowDelegate( + small ? const Size(90, 90) : const Size(140, 140), + appState.puzzle, + appState.animationNotifier, + ), + children: List.generate( + appState.puzzle.length, + (i) => theme.tileButtonCore( + i, appState.puzzle, small), + ), + ), + ), + ), + Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: Colors.black26, width: 1), + ), + ), + padding: const EdgeInsets.only( + left: 10, + bottom: 6, + top: 2, + right: 10, + ), + child: Consumer( + builder: (_, controls, __) => + Row(children: theme.bottomControls(controls)), + ), + ) + ], + ), + ), + ), + ), + ), + ), + ), + ); diff --git a/web/slide_puzzle/lib/src/shared_theme.dart b/web/slide_puzzle/lib/src/shared_theme.dart index 3240ce4ab..34536d46f 100644 --- a/web/slide_puzzle/lib/src/shared_theme.dart +++ b/web/slide_puzzle/lib/src/shared_theme.dart @@ -1,30 +1,26 @@ -import 'package:flutter/material.dart'; - -import 'app_state.dart'; -import 'core/puzzle_animator.dart'; -import 'puzzle_flow_delegate.dart'; +import 'core/puzzle_proxy.dart'; +import 'flutter.dart'; +import 'puzzle_controls.dart'; import 'widgets/material_interior_alt.dart'; -abstract class SharedTheme { - SharedTheme(this._appState); - - final AppState _appState; +final puzzleAnimationDuration = kThemeAnimationDuration * 3; - PuzzleProxy get puzzle => _appState.puzzle; +abstract class SharedTheme { + const SharedTheme(); String get name; Color get puzzleThemeBackground; - RoundedRectangleBorder get puzzleBorder; + RoundedRectangleBorder puzzleBorder(bool small); Color get puzzleBackgroundColor; Color get puzzleAccentColor; - EdgeInsetsGeometry get tilePadding => const EdgeInsets.all(6); + EdgeInsetsGeometry tilePadding(PuzzleProxy puzzle) => const EdgeInsets.all(6); - Widget tileButton(int i); + Widget tileButton(int i, PuzzleProxy puzzle, bool small); Ink createInk( Widget child, { @@ -40,159 +36,57 @@ abstract class SharedTheme { ); Widget createButton( + PuzzleProxy puzzle, + bool small, int tileValue, Widget content, { Color color, RoundedRectangleBorder shape, }) => AnimatedContainer( - duration: _puzzleAnimationDuration, - padding: tilePadding, + duration: puzzleAnimationDuration, + padding: tilePadding(puzzle), child: RaisedButton( elevation: 4, clipBehavior: Clip.hardEdge, - animationDuration: _puzzleAnimationDuration, - onPressed: () => _tilePress(tileValue), - shape: shape ?? puzzleBorder, + animationDuration: puzzleAnimationDuration, + onPressed: () => puzzle.clickOrShake(tileValue), + shape: shape ?? puzzleBorder(small), padding: const EdgeInsets.symmetric(), child: content, color: color, ), ); - Widget build(BuildContext context) => Material( - child: Stack( - children: [ - const SizedBox.expand( - child: FittedBox( - fit: BoxFit.cover, - child: Image( - image: AssetImage('asset/seattle.jpg'), - ), - ), - ), - AnimatedContainer( - duration: _puzzleAnimationDuration, - color: puzzleThemeBackground, - child: Center( - child: _styledWrapper( - SizedBox( - width: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: Colors.black26, - width: 1, - ), - ), - ), - margin: const EdgeInsets.symmetric(horizontal: 20), - child: TabBar( - controller: _appState.tabController, - labelPadding: const EdgeInsets.fromLTRB(0, 20, 0, 12), - labelColor: puzzleAccentColor, - indicatorColor: puzzleAccentColor, - indicatorWeight: 1.5, - unselectedLabelColor: Colors.black.withOpacity(0.6), - tabs: _appState.themeData - .map((st) => Text( - st.name.toUpperCase(), - style: const TextStyle( - letterSpacing: 0.5, - ), - )) - .toList(), - ), - ), - Container( - constraints: const BoxConstraints.tightForFinite(), - padding: const EdgeInsets.all(10), - child: Flow( - delegate: PuzzleFlowDelegate( - _tileSize, - puzzle, - _appState.animationNotifier, - ), - children: List.generate( - puzzle.length, - _tileButton, - ), - ), - ), - Container( - decoration: const BoxDecoration( - border: Border( - top: BorderSide(color: Colors.black26, width: 1), - ), - ), - padding: const EdgeInsets.only( - left: 10, - bottom: 6, - top: 2, - right: 10, - ), - child: Row(children: _bottomControls(context)), - ) - ], - ), - ), - ), - ), - ) - ], - )); - - Duration get _puzzleAnimationDuration => kThemeAnimationDuration * 3; - // Thought about using AnimatedContainer here, but it causes some weird // resizing behavior - Widget _styledWrapper(Widget child) => MaterialInterior( - duration: _puzzleAnimationDuration, - shape: puzzleBorder, + Widget styledWrapper(bool small, Widget child) => MaterialInterior( + duration: puzzleAnimationDuration, + shape: puzzleBorder(small), color: puzzleBackgroundColor, child: child, ); - Size get _tileSize => const Size(140.0, 140.0); - - void Function(bool newValue) get _setAutoPlay { - if (puzzle.solved) { - return null; - } - return _appState.setAutoPlay; - } - - void _tilePress(int tileValue) { - _appState.setAutoPlay(false); - _appState.puzzle.clickOrShake(tileValue); - } - TextStyle get _infoStyle => TextStyle( color: puzzleAccentColor, fontWeight: FontWeight.bold, ); - List _bottomControls(BuildContext context) => [ + List bottomControls(PuzzleControls controls) => [ IconButton( - onPressed: puzzle.reset, + onPressed: controls.reset, icon: Icon(Icons.refresh, color: puzzleAccentColor), - //Icons.refresh, ), Checkbox( - value: _appState.autoPlay, - onChanged: _setAutoPlay, + value: controls.autoPlay, + onChanged: controls.setAutoPlayFunction, activeColor: puzzleAccentColor, ), Expanded( child: Container(), ), Text( - puzzle.clickCount.toString(), + controls.clickCount.toString(), textAlign: TextAlign.right, style: _infoStyle, ), @@ -200,7 +94,7 @@ abstract class SharedTheme { SizedBox( width: 28, child: Text( - puzzle.incorrectTiles.toString(), + controls.incorrectTiles.toString(), textAlign: TextAlign.right, style: _infoStyle, ), @@ -208,11 +102,11 @@ abstract class SharedTheme { const Text(' Tiles left ') ]; - Widget _tileButton(int i) { + Widget tileButtonCore(int i, PuzzleProxy puzzle, bool small) { if (i == puzzle.tileCount && !puzzle.solved) { return const Center(); } - return tileButton(i); + return tileButton(i, puzzle, small); } } diff --git a/web/slide_puzzle/lib/src/theme_plaster.dart b/web/slide_puzzle/lib/src/theme_plaster.dart index e80b6fe02..d676856a6 100644 --- a/web/slide_puzzle/lib/src/theme_plaster.dart +++ b/web/slide_puzzle/lib/src/theme_plaster.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; - -import 'app_state.dart'; +import 'core/puzzle_proxy.dart'; +import 'flutter.dart'; import 'shared_theme.dart'; const _yellowIsh = Color.fromARGB(255, 248, 244, 233); @@ -11,7 +10,7 @@ class ThemePlaster extends SharedTheme { @override String get name => 'Plaster'; - ThemePlaster(AppState baseTheme) : super(baseTheme); + const ThemePlaster(); @override Color get puzzleThemeBackground => _chocolate; @@ -23,18 +22,18 @@ class ThemePlaster extends SharedTheme { Color get puzzleAccentColor => _orangeIsh; @override - RoundedRectangleBorder get puzzleBorder => const RoundedRectangleBorder( - side: BorderSide( + RoundedRectangleBorder puzzleBorder(bool small) => RoundedRectangleBorder( + side: const BorderSide( color: Color.fromARGB(255, 103, 103, 105), width: 8, ), borderRadius: BorderRadius.all( - Radius.circular(18), + Radius.circular(small ? 10 : 18), ), ); @override - Widget tileButton(int i) { + Widget tileButton(int i, PuzzleProxy puzzle, bool small) { final correctColumn = i % puzzle.width; final correctRow = i ~/ puzzle.width; @@ -42,10 +41,10 @@ class ThemePlaster extends SharedTheme { if (i == puzzle.tileCount) { assert(puzzle.solved); - return const Center( + return Center( child: Icon( Icons.thumb_up, - size: 72, + size: small ? 50 : 72, color: _orangeIsh, ), ); @@ -56,11 +55,13 @@ class ThemePlaster extends SharedTheme { style: TextStyle( color: primary ? _yellowIsh : _chocolate, fontFamily: 'Plaster', - fontSize: 77, + fontSize: small ? 40 : 77, ), ); return createButton( + puzzle, + small, i, content, color: primary ? _orangeIsh : _yellowIsh, diff --git a/web/slide_puzzle/lib/src/theme_seattle.dart b/web/slide_puzzle/lib/src/theme_seattle.dart index 2a9505d9d..15f75afb8 100644 --- a/web/slide_puzzle/lib/src/theme_seattle.dart +++ b/web/slide_puzzle/lib/src/theme_seattle.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; - -import 'app_state.dart'; +import 'core/puzzle_proxy.dart'; +import 'flutter.dart'; import 'shared_theme.dart'; import 'widgets/decoration_image_plus.dart'; @@ -8,7 +7,7 @@ class ThemeSeattle extends SharedTheme { @override String get name => 'Seattle'; - ThemeSeattle(AppState proxy) : super(proxy); + const ThemeSeattle(); @override Color get puzzleThemeBackground => const Color.fromARGB(153, 90, 135, 170); @@ -20,18 +19,19 @@ class ThemeSeattle extends SharedTheme { Color get puzzleAccentColor => const Color(0xff000579f); @override - RoundedRectangleBorder get puzzleBorder => const RoundedRectangleBorder( + RoundedRectangleBorder puzzleBorder(bool small) => + const RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(1), ), ); @override - EdgeInsetsGeometry get tilePadding => + EdgeInsetsGeometry tilePadding(PuzzleProxy puzzle) => puzzle.solved ? const EdgeInsets.all(1) : const EdgeInsets.all(4); @override - Widget tileButton(int i) { + Widget tileButton(int i, PuzzleProxy puzzle, bool small) { if (i == puzzle.tileCount && !puzzle.solved) { assert(puzzle.solved); } @@ -58,14 +58,14 @@ class ThemeSeattle extends SharedTheme { style: TextStyle( fontWeight: FontWeight.normal, color: correctPosition ? Colors.white : Colors.black, - fontSize: 42, + fontSize: small ? 25 : 42, ), ), ), image: decorationImage, - padding: const EdgeInsets.all(32), + padding: EdgeInsets.all(small ? 20 : 32), ); - return createButton(i, content); + return createButton(puzzle, small, i, content); } } diff --git a/web/slide_puzzle/lib/src/theme_simple.dart b/web/slide_puzzle/lib/src/theme_simple.dart index c962da38f..fa68d1c38 100644 --- a/web/slide_puzzle/lib/src/theme_simple.dart +++ b/web/slide_puzzle/lib/src/theme_simple.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; - -import 'app_state.dart'; +import 'core/puzzle_proxy.dart'; +import 'flutter.dart'; import 'shared_theme.dart'; const _accentBlue = Color(0xff000579e); @@ -9,7 +8,7 @@ class ThemeSimple extends SharedTheme { @override String get name => 'Simple'; - ThemeSimple(AppState proxy) : super(proxy); + const ThemeSimple(); @override Color get puzzleThemeBackground => Colors.white; @@ -21,7 +20,8 @@ class ThemeSimple extends SharedTheme { Color get puzzleAccentColor => _accentBlue; @override - RoundedRectangleBorder get puzzleBorder => const RoundedRectangleBorder( + RoundedRectangleBorder puzzleBorder(bool small) => + const RoundedRectangleBorder( side: BorderSide(color: Colors.black26, width: 1), borderRadius: BorderRadius.all( Radius.circular(4), @@ -29,7 +29,7 @@ class ThemeSimple extends SharedTheme { ); @override - Widget tileButton(int i) { + Widget tileButton(int i, PuzzleProxy puzzle, bool small) { if (i == puzzle.tileCount) { assert(puzzle.solved); return const Center( @@ -50,13 +50,15 @@ class ThemeSimple extends SharedTheme { style: TextStyle( color: Colors.white, fontWeight: correctPosition ? FontWeight.bold : FontWeight.normal, - fontSize: 49, + fontSize: small ? 30 : 49, ), ), ), ); return createButton( + puzzle, + small, i, content, color: const Color.fromARGB(255, 13, 87, 155), diff --git a/web/slide_puzzle/lib/src/widgets/decoration_image_plus.dart b/web/slide_puzzle/lib/src/widgets/decoration_image_plus.dart index f48803c75..4ee6a6057 100644 --- a/web/slide_puzzle/lib/src/widgets/decoration_image_plus.dart +++ b/web/slide_puzzle/lib/src/widgets/decoration_image_plus.dart @@ -1,8 +1,9 @@ // ignore_for_file: omit_local_variable_types, annotate_overrides +import 'dart:developer' as developer; import 'dart:ui' as ui show Image; -import 'package:flutter/material.dart'; +import '../flutter.dart'; // A model on top of DecorationImage that supports slicing up the source image // efficiently to draw it as tiles in the puzzle game @@ -143,14 +144,22 @@ class DecorationImagePlus implements DecorationImage { if (colorFilter != null) properties.add('$colorFilter'); if (fit != null && !(fit == BoxFit.fill && centerSlice != null) && - !(fit == BoxFit.scaleDown && centerSlice == null)) + !(fit == BoxFit.scaleDown && centerSlice == null)) { properties.add('$fit'); + } properties.add('$alignment'); if (centerSlice != null) properties.add('centerSlice: $centerSlice'); if (repeat != ImageRepeat.noRepeat) properties.add('$repeat'); if (matchTextDirection) properties.add('match text direction'); return '$runtimeType(${properties.join(", ")})'; } + + @override + ImageErrorListener get onError => (error, stackTrace) { + developer.log('Failed to load image.\n' + '$error\n' + '$stackTrace', name: 'slide_puzzle.decoration_image_plus'); + }; } /// The painter for a [DecorationImagePlus]. @@ -166,16 +175,13 @@ class DecorationImagePlus implements DecorationImage { /// longer needed. class DecorationImagePainterPlus implements DecorationImagePainter { DecorationImagePainterPlus._(this._details, this._onChanged) - : assert(_details != null) { - _imageStreamListener = ImageStreamListener(_imageListener); - } + : assert(_details != null); final DecorationImagePlus _details; final VoidCallback _onChanged; ImageStream _imageStream; ImageInfo _image; - ImageStreamListener _imageStreamListener; /// Draw the image onto the given canvas. /// @@ -217,8 +223,10 @@ class DecorationImagePainterPlus implements DecorationImagePainter { final ImageStream newImageStream = _details.image.resolve(configuration); if (newImageStream.key != _imageStream?.key) { - _imageStream?.removeListener(_imageStreamListener); - _imageStream = newImageStream..addListener(_imageStreamListener); + final listener = ImageStreamListener(_imageListener); + _imageStream?.removeListener(listener); + _imageStream = newImageStream; + _imageStream.addListener(listener); } if (_image == null) return; @@ -257,7 +265,7 @@ class DecorationImagePainterPlus implements DecorationImagePainter { /// After this method has been called, the object is no longer usable. @mustCallSuper void dispose() { - _imageStream?.removeListener(_imageStreamListener); + _imageStream?.removeListener(ImageStreamListener(_imageListener)); } @override diff --git a/web/slide_puzzle/lib/src/widgets/material_interior_alt.dart b/web/slide_puzzle/lib/src/widgets/material_interior_alt.dart index 9673cebae..9cef4c52e 100644 --- a/web/slide_puzzle/lib/src/widgets/material_interior_alt.dart +++ b/web/slide_puzzle/lib/src/widgets/material_interior_alt.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import '../flutter.dart'; // Copied from // https://github.com/flutter/flutter/blob/f5b02e3c05ed1ab31e890add84fb56e35de2d392/packages/flutter/lib/src/material/material.dart#L593-L715 diff --git a/web/slide_puzzle/pubspec.lock b/web/slide_puzzle/pubspec.lock index fa7e49419..f7f6cf21d 100644 --- a/web/slide_puzzle/pubspec.lock +++ b/web/slide_puzzle/pubspec.lock @@ -1,62 +1,55 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.3" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.39.4" + version: "0.36.4" archive: dependency: transitive description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.13" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.5.2" + version: "1.6.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.4.1" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "2.0.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.3" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.11" + version: "1.14.12" convert: dependency: transitive description: @@ -70,14 +63,14 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "0.13.8" + version: "0.13.9" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.4" csslib: dependency: transitive description: @@ -95,6 +88,13 @@ packages: description: flutter source: sdk version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.19" glob: dependency: transitive description: @@ -115,14 +115,14 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.0+4" + version: "0.12.0+2" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.1.0" http_parser: dependency: transitive description: @@ -136,7 +136,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.12" io: dependency: transitive description: @@ -151,6 +151,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.1+1" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.19" logging: dependency: transitive description: @@ -213,7 +220,14 @@ packages: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.9.1" + version: "1.9.3" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" path: dependency: transitive description: @@ -227,7 +241,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.8.0+1" petitparser: dependency: transitive description: @@ -242,20 +256,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.0" + provider: + dependency: "direct dev" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1+1" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.3" + version: "1.4.2" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.3" shelf: dependency: transitive description: @@ -269,7 +290,7 @@ packages: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "1.0.4" shelf_static: dependency: transitive description: @@ -302,14 +323,14 @@ packages: name: source_maps url: "https://pub.dartlang.org" source: hosted - version: "0.10.9" + version: "0.10.8" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.5" + version: "1.7.0" stack_trace: dependency: transitive description: @@ -344,7 +365,7 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.14.1" + version: "1.14.2" test_api: dependency: transitive description: @@ -358,7 +379,7 @@ packages: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.2" + version: "0.3.3" typed_data: dependency: transitive description: @@ -379,14 +400,14 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "3.0.0+1" + version: "4.0.1" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+14" + version: "0.9.7+12" web_socket_channel: dependency: transitive description: @@ -407,7 +428,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "3.5.0" + version: "3.6.1" yaml: dependency: transitive description: diff --git a/web/slide_puzzle/pubspec.yaml b/web/slide_puzzle/pubspec.yaml index e782f2387..e28692e1b 100644 --- a/web/slide_puzzle/pubspec.yaml +++ b/web/slide_puzzle/pubspec.yaml @@ -13,13 +13,13 @@ dev_dependencies: flutter_test: sdk: flutter pedantic: ^1.3.0 + provider: ^2.0.0 test: ^1.3.4 flutter: uses-material-design: true assets: - asset/ - - preview.png fonts: - family: Plaster