croupier: Drag And Drop Arrange Players

Now allows players to drag and drop themselves (and others) into the
appropriate slots.

You can also move people that are in the way.

This isn't very good when more than 1 person ends up in the same slot;
however, data consistency is maintained in Syncbase.

Note: We always had this race condition, so this isn't a new problem.
This CL alleviates the problem since you can just reposition yourself
when such a thing happens.

Change-Id: I4937c3f6e6f9b8e7a55b567dd02928fcfdf5f12a
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index 770c7b7..de92aa7 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -150,11 +150,21 @@
     double size = style.Size.settingsSize;
     config.croupier.players_found.forEach((int userID, int playerNumber) {
       if (!needsArrangement || playerNumber == null) {
+        // Note: Even if cs is null, a placeholder will be shown instead.
         CroupierSettings cs = config.croupier.settings_everyone[userID];
-        // cs could be null if this settings data hasn't synced yet.
-        // If so, a placeholder is shown instead.
-        profileWidgets.add(new CroupierProfileComponent(
-            settings: cs, height: size, width: size));
+        bool isMe = config.croupier.settings.userID == userID;
+        Widget cpc = new Container(
+            decoration: isMe ? style.Box.liveNow : null,
+            child: new CroupierProfileComponent(
+                settings: cs, height: size, width: size));
+
+        // If the player profiles can be arranged, they should be draggable too.
+        if (needsArrangement) {
+          profileWidgets.add(new Draggable<CroupierSettings>(
+              child: cpc, feedback: cpc, data: cs));
+        } else {
+          profileWidgets.add(cpc);
+        }
       }
     });
 
@@ -187,7 +197,8 @@
     if (gad.needsArrangement) {
       // Games that need arrangement can show their game arrange component.
       allWidgets.add(component_game.createGameArrangeComponent(config.croupier,
-          width: ui.window.size.width, height: ui.window.size.height / 2));
+          width: ui.window.size.width * 0.90,
+          height: ui.window.size.height * 0.50));
     }
 
     // Allow games that can start with these players to begin.
@@ -214,7 +225,7 @@
                     backgroundColor: startCb != null
                         ? style.theme.accentColor
                         : Colors.grey[300]),
-                padding: new EdgeDims.all(10.0),
+                padding: style.Spacing.smallPadding,
                 child: new FlatButton(
                     child: new Text("Start Game", style: style.Text.hugeStyle),
                     onPressed: startCb))
diff --git a/lib/components/game.dart b/lib/components/game.dart
index c798c71..32d9085 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -13,6 +13,7 @@
 
 import '../logic/card.dart' as logic_card;
 import '../logic/croupier.dart' show Croupier;
+import '../logic/croupier_settings.dart' show CroupierSettings;
 import '../logic/game/game.dart' show Game, GameType;
 import '../logic/hearts/hearts.dart' show HeartsGame, HeartsPhase, HeartsType;
 import '../logic/solitaire/solitaire.dart' show SolitaireGame, SolitairePhase;
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index b8ab205..ec5cc00 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -777,8 +777,6 @@
   HeartsArrangeComponent(Croupier croupier, {double width, double height})
       : super(croupier, width: width, height: height);
 
-  bool get hasSat => croupier.game.playerNumber != null;
-
   Widget build(BuildContext context) {
     int numAtTable = croupier.players_found.values
         .where((int playerNumber) => playerNumber == 4)
@@ -792,22 +790,22 @@
           new Flexible(
               flex: 1,
               child: new Row(
-                  [_buildEmptySlot(), _buildSlot("Player", 2), _buildEmptySlot()],
+                  [_buildEmptySlot(), _buildSlot("social/person_outline", 2), _buildEmptySlot()],
                   justifyContent: FlexJustifyContent.spaceAround,
                   alignItems: FlexAlignItems.stretch)),
           new Flexible(
               flex: 1,
               child: new Row([
-                _buildSlot("Player", 1),
-                _buildSlot("Table: ${numAtTable}", 4),
-                _buildSlot("Player", 3)
+                _buildSlot("social/person_outline", 1),
+                _buildSlot("hardware/tablet", 4, extra: "x${numAtTable}"),
+                _buildSlot("social/person_outline", 3)
               ],
                   justifyContent: FlexJustifyContent.spaceAround,
                   alignItems: FlexAlignItems.stretch)),
           new Flexible(
               flex: 1,
               child: new Row(
-                  [_buildEmptySlot(), _buildSlot("Player", 0), _buildEmptySlot()],
+                  [_buildEmptySlot(), _buildSlot("social/person_outline", 0), _buildEmptySlot()],
                   justifyContent: FlexJustifyContent.spaceAround,
                   alignItems: FlexAlignItems.stretch))
         ],
@@ -819,30 +817,38 @@
     return new Flexible(flex: 1, child: new Text(""));
   }
 
-  Widget _buildSlot(String name, int index) {
-    NoArgCb onTap = () {
-      croupier.settings_manager.setPlayerNumber(croupier.game.gameID, index);
-    };
-    Widget slotWidget = new Text(name, style: style.Text.hugeStyle);
+  Widget _buildSlot(String name, int index, {String extra: ""}) {
+    Widget slotWidget = new Row([
+      new Icon(size: IconSize.s48, icon: name),
+      new Text(extra, style: style.Text.largeStyle)
+    ],
+        alignItems: FlexAlignItems.center,
+        justifyContent: FlexJustifyContent.center);
 
-    bool seatTaken =
-        index >= 0 && index < 4 && croupier.players_found.containsValue(index);
+    bool isMe = croupier.game.playerNumber == index;
+    bool isPlayerIndex = index >= 0 && index < 4;
+    bool isTableIndex = index == 4;
+    bool seatTaken = (isPlayerIndex || (isTableIndex && isMe)) &&
+        croupier.players_found.containsValue(index);
     if (seatTaken) {
-      onTap = null;
-      slotWidget = new CroupierProfileComponent(
-          settings: croupier.settingsFromPlayerNumber(index));
-    } else if (hasSat) {
-      onTap = null;
+      // Note: If more than 1 person is in the seat, it may no longer show you.
+      CroupierSettings cs = croupier.settingsFromPlayerNumber(index);
+      CroupierProfileComponent cpc = new CroupierProfileComponent(settings: cs);
+      slotWidget =
+          new Draggable<CroupierSettings>(child: cpc, feedback: cpc, data: cs);
     }
 
-    return new Flexible(
-        flex: 1,
-        child: new GestureDetector(
-            child: new Card(
-                color: croupier.game.playerNumber == index
-                    ? style.theme.accentColor
-                    : null,
-                child: slotWidget),
-            onTap: onTap));
+    Widget dragTarget = new DragTarget<CroupierSettings>(
+        builder: (BuildContext context, List<CroupierSettings> data, _) {
+      return new Container(
+          constraints: const BoxConstraints.expand(),
+          decoration: isMe ? style.Box.liveBackground : style.Box.border,
+          child: slotWidget);
+    }, onAccept: (CroupierSettings cs) {
+      croupier.settings_manager
+          .setPlayerNumber(croupier.game.gameID, cs.userID, index);
+    }, onWillAccept: (_) => true);
+
+    return new Flexible(flex: 1, child: dragTarget);
   }
 }
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
index b6096b5..c4fcf5b 100644
--- a/lib/logic/croupier.dart
+++ b/lib/logic/croupier.dart
@@ -192,9 +192,8 @@
         _advertiseFuture = settings_manager
             .createGameSyncgroup(gameTypeToString(gt), game.gameID)
             .then((GameStartData gsd) {
-          if (!game.gameArrangeData.needsArrangement) {
-            settings_manager.setPlayerNumber(gsd.gameID, 0);
-          }
+          // The game creator should always sit as player 0, at least initially.
+          settings_manager.setPlayerNumber(gsd.gameID, settings.userID, 0);
           // Only the game chooser should be advertising the game.
           return settings_manager.advertiseSettings(gsd);
         }); // don't wait for this future.
@@ -228,7 +227,7 @@
         players_found[gsd.ownerID] = null;
         settings_manager.joinGameSyncgroup(sgName, gsd.gameID).then((_) {
           if (!game.gameArrangeData.needsArrangement) {
-            settings_manager.setPlayerNumber(gsd.gameID, 0);
+            settings_manager.setPlayerNumber(gsd.gameID, settings.userID, 0);
           }
         });
 
diff --git a/lib/src/mocks/settings_manager.dart b/lib/src/mocks/settings_manager.dart
index e0b1daa..b767029 100644
--- a/lib/src/mocks/settings_manager.dart
+++ b/lib/src/mocks/settings_manager.dart
@@ -70,7 +70,7 @@
     return new Future(() => null);
   }
 
-  Future setPlayerNumber(int gameID, int playerNumber) async {
+  Future setPlayerNumber(int gameID, int userID, int playerNumber) async {
     return new Future(() => null);
   }
 
diff --git a/lib/src/syncbase/settings_manager.dart b/lib/src/syncbase/settings_manager.dart
index ef40e49..b6c8e31 100644
--- a/lib/src/syncbase/settings_manager.dart
+++ b/lib/src/syncbase/settings_manager.dart
@@ -197,13 +197,12 @@
         .put(UTF8.encode(await _mySettingsSyncgroupName()));
   }
 
-  Future setPlayerNumber(int gameID, int playerNumber) async {
+  Future setPlayerNumber(int gameID, int userID, int playerNumber) async {
     sc.SyncbaseDatabase db = await _cc.createDatabase();
     sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
 
-    int id = await _getUserID();
     await gameTable
-        .row(util.playerNumberKeyFromData(gameID, id))
+        .row(util.playerNumberKeyFromData(gameID, userID))
         .put(UTF8.encode("${playerNumber}"));
   }
 
diff --git a/manifest.yaml b/manifest.yaml
index 67dd5b0..9a936d1 100644
--- a/manifest.yaml
+++ b/manifest.yaml
@@ -5,9 +5,11 @@
   - name: action/settings
   - name: av/play_arrow
   - name: action/swap_vert
+  - name: hardware/tablet
   - name: navigation/arrow_back
   - name: navigation/arrow_forward
   - name: navigation/menu
+  - name: social/person_outline
 assets:
   - images/default/classic/down/c10.png
   - images/default/classic/down/c1.png