Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize cards feature #18

Merged
merged 13 commits into from Mar 22, 2020
1 change: 1 addition & 0 deletions .github/workflows/screenshots_and_web.yml
Expand Up @@ -18,6 +18,7 @@ jobs:
with:
channel: 'beta'
- run: flutter pub get
- run: ./tools/run_test.sh
- name: Install Android SDK
uses: malinskiy/action-android/install-sdk@release/0.0.5
- name: Phone screenshots
Expand Down
3 changes: 3 additions & 0 deletions lib/app.dart
@@ -1,5 +1,6 @@
import 'package:flutter/widgets.dart';
import 'package:scrum_poker/screens/card_screen.dart';
import 'package:scrum_poker/screens/edit_screen.dart';
import 'package:scrum_poker/screens/home_screen.dart';

class MyApp extends StatelessWidget {
Expand All @@ -15,6 +16,8 @@ class MyApp extends StatelessWidget {
return HomeScreen();
case '/card':
return CardScreen();
case '/edit':
return EditScreen();
default:
return null;
}
Expand Down
5 changes: 0 additions & 5 deletions lib/screens/card_screen.dart
@@ -1,13 +1,8 @@
import 'package:flutter/widgets.dart';
import 'package:scrum_poker/widgets/card.dart';
import 'package:scrum_poker/widgets/screen.dart';
import 'package:wakelock/wakelock.dart';

class CardScreen extends StatelessWidget {
CardScreen() {
Wakelock.enable();
}

@override
Widget build(BuildContext context) {
final String arg = ModalRoute.of(context).settings.arguments;
Expand Down
187 changes: 187 additions & 0 deletions lib/screens/edit_screen.dart
@@ -0,0 +1,187 @@
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:scrum_poker/state/theme.dart';
import 'package:scrum_poker/widgets/screen.dart';

class EditScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() => _EditScreenState();
}

class _EditScreenState extends State<EditScreen> {
TextEditingController _controller;

@override
void initState() {
_controller = TextEditingController();
super.initState();
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
_controller.text = ModalRoute.of(context).settings.arguments;
}

@override
Widget build(BuildContext context) {
return Screen(
child: Consumer<ThemeModel>(builder: (context, theme, _) {
Color labelTextColor;
Color valueTextColor;
Color valueBgColor;
Color btnBgColor;
Color btnTextColor;
Color cursorColor;

if (theme.isDarkMode) {
labelTextColor = const Color(0xFFFFFFFF);
valueTextColor = const Color(0xFFFFFFFF);
valueBgColor = const Color(0xFF1D1D1D);
btnBgColor = const Color(0xFF1D1D1D);
btnTextColor = const Color(0xFFFFFFFF);
cursorColor = const Color(0xFFFFFFFF);
} else {
labelTextColor = const Color(0xFF1D1D1D);
valueTextColor = const Color(0xFF1D1D1D);
valueBgColor = const Color(0xFFFFFFFF);
btnBgColor = const Color(0xFFFFFFFF);
btnTextColor = const Color(0xFF1D1D1D);
cursorColor = const Color(0xFF1D1D1D);
}

var _labelTextStyle = TextStyle(
color: labelTextColor,
fontFamily: "Alata",
fontSize: 20,
);

var _valueTextStyle = TextStyle(
color: valueTextColor,
fontFamily: "Alata",
fontSize: 60,
);

var _valueBoxDecoration = BoxDecoration(
color: valueBgColor,
borderRadius: const BorderRadius.all(
const Radius.circular(10),
),
);

return Column(
children: [
Text(
"Previous value:",
style: _labelTextStyle,
),
Container(
child: Text(
_controller.text,
textAlign: TextAlign.center,
style: _valueTextStyle,
),
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.only(
left: 4,
right: 4,
top: 4,
bottom: 20,
),
decoration: _valueBoxDecoration,
width: double.infinity,
),
Text(
"New value:",
style: _labelTextStyle,
),
Container(
child: EditableText(
controller: _controller,
focusNode: FocusNode(),
style: _valueTextStyle,
textAlign: TextAlign.center,
cursorColor: cursorColor,
backgroundCursorColor: cursorColor,
autofocus: true,
autocorrect: false,
enableSuggestions: false,
maxLines: 1,
),
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.only(
left: 4,
right: 4,
top: 4,
bottom: 20,
),
decoration: _valueBoxDecoration,
),
_createButtons(btnTextColor, btnBgColor),
],
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
);
}),
);
}

Widget _createButtons(Color textColor, Color bgColor) {
return Row(
children: [
Expanded(
child: GestureDetector(
key: Key("btn_save"),
child: _createButton("Save", textColor, bgColor),
onTap: () => _doAction(EditAction.update),
),
),
Expanded(
child: GestureDetector(
child: _createButton("+ Save as a new", textColor, bgColor),
onTap: () => _doAction(EditAction.add),
),
),
Expanded(
child: GestureDetector(
child: _createButton("- Delete", textColor, bgColor),
onTap: () => _doAction(EditAction.delete),
),
),
],
);
}

Widget _createButton(String text, Color textColor, Color bgColor) {
return Container(
child: Text(
text,
style: TextStyle(
color: textColor,
fontFamily: "Alata",
),
textAlign: TextAlign.center,
),
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: bgColor,
borderRadius: const BorderRadius.all(
const Radius.circular(10),
),
),
);
}

void _doAction(EditAction action) {
Navigator.pop(context, MapEntry(action, _controller.text));
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

enum EditAction { add, update, delete }
118 changes: 94 additions & 24 deletions lib/screens/home_screen.dart
@@ -1,53 +1,123 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:scrum_poker/screens/edit_screen.dart';
import 'package:scrum_poker/state/theme.dart';
import 'package:scrum_poker/util/list_apis.dart';
import 'package:scrum_poker/widgets/card.dart';
import 'package:scrum_poker/widgets/screen.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wakelock/wakelock.dart';

const VALUES = [
["0", "1", "2"],
["3", "5", "8"],
["13", "21", "∞"]
];
const KEY_ITEMS = "items";

class HomeScreen extends StatelessWidget {
Widget _createRow(BuildContext context, List<String> rowValues) {
class HomeScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
Future<List<String>> _items;

@override
void initState() {
super.initState();

_items = SharedPreferences.getInstance().then((prefs) {
var savedItems = prefs.getStringList(KEY_ITEMS);
if (savedItems == null || savedItems.isEmpty) {
return ["0", "1", "2", "3", "5", "8", "13", "21", "∞"];
} else {
return savedItems;
}
});
}

@override
Widget build(BuildContext context) {
return Screen(
child: GestureDetector(
onPanEnd: (details) {
Provider.of<ThemeModel>(context).toggleDarkMode();
},
child: FutureBuilder<List<String>>(
future: _items,
builder: (context, snapshot) {
return Column(
children: ListUtil.chunk(snapshot.data, 3)
.asMap()
.entries
.map((entry) => _createRow(context, entry.key, entry.value))
.toList(),
);
},
),
),
);
}

Widget _createRow(BuildContext context, index, List<String> rowValues) {
return Expanded(
child: Row(
children: rowValues
.map((String value) => _createItem(context, value))
.asMap()
.entries
.map((entry) =>
_createItem(context, 3 * index + entry.key, entry.value))
.toList(),
),
);
}

Widget _createItem(BuildContext context, String value) {
Widget _createItem(BuildContext context, int index, String value) {
return Expanded(
child: GestureDetector(
child: PokerCard(value: value),
onTap: () {
if (!kIsWeb) {
Wakelock.enable();
}

Navigator.pushNamed(context, "/card", arguments: value).then((_) {
Wakelock.disable();
if (!kIsWeb) {
Wakelock.disable();
}
});
},
),
);
}
onLongPress: () async {
var result =
await Navigator.pushNamed(context, "/edit", arguments: value);

@override
Widget build(BuildContext context) {
return Screen(
child: GestureDetector(
onPanEnd: (details) {
Provider.of<ThemeModel>(context).toggleDarkMode();
if (result != null && result is MapEntry<EditAction, String>) {
_saveAndUpdate(result.key, index, result.value);
}
},
child: Column(
children: VALUES
.map((List<String> rowValues) => _createRow(context, rowValues))
.toList(),
),
),
);
}

void _saveAndUpdate(EditAction action, int index, String newValue) async {
var prefs = await SharedPreferences.getInstance();
var currentItems = await _items;

switch (action) {
case EditAction.add:
currentItems.insert(index + 1, newValue);
break;
case EditAction.update:
currentItems[index] = newValue;
break;
case EditAction.delete:
if (currentItems.length > 1) {
currentItems.removeAt(index);
}
break;
}

setState(() {
_items = prefs.setStringList(KEY_ITEMS, currentItems).then((_) {
return currentItems;
});
});
}
}
17 changes: 17 additions & 0 deletions lib/util/list_apis.dart
@@ -0,0 +1,17 @@
class ListUtil {
static List<List<T>> chunk<T>(List<T> list, int number) {
assert(number > 0);

List<List<T>> result = [];

for (var i = 0; i < list.length; i += number) {
var endIndex = i + number;
if (endIndex > list.length) {
endIndex = list.length;
}
result.add(list.sublist(i, endIndex));
}

return result;
}
}