syncslides: Ability to delete a deck and some polish / bug fixes

Closes: https://github.com/vanadium/syncslides/issues/21
Change-Id: I9a08c09240abd552c2d42f273fbcc06627fcc104
diff --git a/dart/Makefile b/dart/Makefile
index 670dcc3..9893527 100644
--- a/dart/Makefile
+++ b/dart/Makefile
@@ -61,7 +61,7 @@
 endef
 
 .PHONY: install
-install: build deploy
+install: build
 	$(call GENERATE_SHORTCUT_FILE,$(GS_BUCKET_URL),$(SYNCBASE_ARGS))
 	adb -s $(DEVICE_ID) push -p shortcut_commands $(MOJO_SHELL_CMD_PATH)
 	adb -s $(DEVICE_ID) shell chmod 555 $(MOJO_SHELL_CMD_PATH)
@@ -75,7 +75,7 @@
 # TODO(aghassemi): Is there a way to remove the shortcut via adb?
 
 .PHONY: deploy
-deploy: packages
+deploy: build
 	gsutil cp $(APP_FLX_FILE) $(GS_BUCKET_PATH)
 	gsutil cp $(SYNCBASE_MOJO_DIR)/syncbase_server.mojo $(GS_BUCKET_PATH)
 	gsutil cp $(DISCOVERY_MOJO_DIR)/discovery.mojo $(GS_BUCKET_PATH)
@@ -108,7 +108,7 @@
 
 .PHONY: clean
 clean:
-	rm -f app.flx snapshot_blob.bin
+	rm -f app.flx snapshot_blob.bin shortcut_commands
 	rm -rf packages
 	adb -s $(DEVICE_ID) shell run-as org.chromium.mojo.shell rm $(SETTINGS_FILE) settings_commands
 
diff --git a/dart/flutter.yaml b/dart/flutter.yaml
index 05af4c9..f7279fd 100644
--- a/dart/flutter.yaml
+++ b/dart/flutter.yaml
@@ -1,6 +1,7 @@
 name: syncslides
 material-design-icons:
   - name: action/account_circle
+  - name: action/delete
   - name: action/perm_device_information
   - name: av/loop
   - name: av/play_arrow
diff --git a/dart/lib/components/deckgrid.dart b/dart/lib/components/deckgrid.dart
index 0874c53..fc7b71a 100644
--- a/dart/lib/components/deckgrid.dart
+++ b/dart/lib/components/deckgrid.dart
@@ -23,15 +23,18 @@
   }
 
   Widget build(BuildContext context, AppState appState, AppActions appActions) {
-    // Local decks.
-    List<model.Deck> decks = appState.decks.values
-        .where((DeckState d) => d.deck != null && d.presentation == null)
-        .map((DeckState d) => d.deck);
-
     // Advertised decks.
     List<model.PresentationAdvertisement> presentations =
         appState.presentationAdvertisements.values;
 
+    // Local decks that are not presented or advertised.
+    List<model.Deck> decks = appState.decks.values
+        .where((DeckState d) => d.deck != null &&
+            d.presentation == null &&
+            !presentations.any((model.PresentationAdvertisement p) =>
+                p.deck.key == d.deck.key))
+        .map((DeckState d) => d.deck);
+
     Widget title = new Text('SyncSlides');
     Widget drawer = new IconButton(icon: "navigation/menu", onPressed: () {
       showDrawer(
@@ -73,19 +76,17 @@
     List<Widget> presentationBoxes = _presentations
         .map((presentation) => _buildPresentationBox(context, presentation));
     var allBoxes = new List.from(presentationBoxes)..addAll(deckBoxes);
-    var grid = new Grid(allBoxes, maxChildExtent: style.Size.thumbnailWidth);
+    var grid = new Grid(allBoxes, maxChildExtent: style.Size.gridbox);
     return new ScrollableViewport(child: grid);
   }
 
   Widget _buildDeckBox(BuildContext context, model.Deck deckData) {
-    var thumbnail =
-        new AsyncImage(provider: imageProvider.getDeckThumbnailImage(deckData));
-    // TODO(aghassemi): Add "Opened on" data.
-    var subtitleWidget =
-        new Text("Opened on Sep 12, 2015", style: style.Text.subtitleStyle);
-    subtitleWidget = stopWrapping(subtitleWidget);
-    var footer = _buildBoxFooter(deckData.name, subtitleWidget);
-    var box = _buildCard(deckData.key, [thumbnail, footer], () {
+    var thumbnail = new AsyncImage(
+        provider: imageProvider.getDeckThumbnailImage(deckData),
+        fit: ImageFit.scaleDown);
+
+    var footer = _buildBoxFooter(deckData.name);
+    var box = _buildCard(deckData.key, thumbnail, footer, () {
       Navigator.push(
           context,
           new MaterialPageRoute(
@@ -98,16 +99,17 @@
   Widget _buildPresentationBox(
       BuildContext context, model.PresentationAdvertisement presentationData) {
     var thumbnail = new AsyncImage(
-        provider: imageProvider.getDeckThumbnailImage(presentationData.deck));
+        provider: imageProvider.getDeckThumbnailImage(presentationData.deck),
+        fit: ImageFit.scaleDown);
     var liveBox = new Row([
       new Container(
           child: new Text("LIVE NOW", style: style.Text.liveNow),
           decoration: style.Box.liveNow,
-          margin: style.Spacing.normalMargin,
           padding: style.Spacing.extraSmallPadding)
     ]);
-    var footer = _buildBoxFooter(presentationData.deck.name, liveBox);
-    var box = _buildCard(presentationData.key, [thumbnail, footer], () async {
+
+    var footer = _buildBoxFooter(presentationData.deck.name, subtitle: liveBox);
+    var box = _buildCard(presentationData.key, thumbnail, footer, () async {
       toast.info(
           _scaffoldKey, 'Joining presentation ${presentationData.deck.name}...',
           duration: toast.Durations.permanent);
@@ -136,18 +138,32 @@
     return box;
   }
 
-  Widget _buildBoxFooter(String title, Widget subtitle) {
-    var titleWidget = new Text(title, style: style.Text.titleStyle);
-    titleWidget = stopWrapping(titleWidget);
+  Widget _buildBoxFooter(String title, {Widget subtitle}) {
+    var titleChildren = [new Text(title, style: style.Text.titleStyle)];
+    if (subtitle != null) {
+      titleChildren.add(subtitle);
+    }
 
-    var titleAndSubtitle = new BlockBody([titleWidget, subtitle]);
-    return new Container(
-        child: titleAndSubtitle, padding: style.Spacing.normalPadding);
+    var titleContainer = new Container(
+        child: new BlockBody(titleChildren),
+        padding: style.Spacing.normalPadding);
+
+    titleContainer = stopWrapping(titleContainer);
+
+    return titleContainer;
   }
 
-  Widget _buildCard(String key, List<Widget> children, Function onTap) {
+  Widget _buildCard(String key, Widget image, Widget footer, Function onTap) {
+    image = new Flexible(child: image, flex: 1);
+    footer = new Container(
+        child: footer,
+        constraints: new BoxConstraints.tight(
+            new Size.fromHeight(style.Size.boxFooterHeight)));
+    footer = new Flexible(child: footer, flex: 0);
     var content = new Container(
-        child: new Card(child: new BlockBody(children)),
+        child: new Card(
+            child: new Column([image, footer],
+                alignItems: FlexAlignItems.stretch)),
         margin: style.Spacing.normalMargin);
 
     return new InkWell(key: new Key(key), child: content, onTap: onTap);
diff --git a/dart/lib/components/slidelist.dart b/dart/lib/components/slidelist.dart
index 86d0ac6..dfcf945 100644
--- a/dart/lib/components/slidelist.dart
+++ b/dart/lib/components/slidelist.dart
@@ -26,17 +26,36 @@
     }
     var deckState = appState.decks[_deckId];
     var slides = deckState.slides;
+    var toolbarActions = [];
+    var deleteAction = _buildDelete(context, appState, appActions);
+    if (deleteAction != null) {
+      toolbarActions.add(deleteAction);
+    }
     return new Scaffold(
         key: _scaffoldKey,
         toolBar: new ToolBar(
             left: new IconButton(
                 icon: 'navigation/arrow_back',
                 onPressed: () => Navigator.pop(context)),
-            center: new Text(deckState.deck.name)),
+            center: new Text(deckState.deck.name),
+            right: toolbarActions),
         floatingActionButton: _buildPresentFab(context, appState, appActions),
         body: new Material(child: new SlideList(_deckId, slides, appActions)));
   }
 
+  _buildDelete(BuildContext context, AppState appState, AppActions appActions) {
+    var deckState = appState.decks[_deckId];
+    if (deckState.presentation != null) {
+      // Can't delete while in a presentation.
+      return null;
+    }
+
+    return new IconButton(icon: 'action/delete', onPressed: () async {
+      await appActions.removeDeck(deckState.deck.key);
+      Navigator.of(context).pop();
+    });
+  }
+
   _buildPresentFab(
       BuildContext context, AppState appState, AppActions appActions) {
     var deckState = appState.decks[_deckId];
@@ -92,7 +111,8 @@
     {Function onTap}) {
   var thumbnail = new AsyncImage(
       provider: imageProvider.getSlideImage(deckId, slideData),
-      fit: ImageFit.scaleDown);
+      fit: ImageFit.cover,
+      width: style.Size.slideListThumbnailWidth);
 
   thumbnail = new Flexible(child: new Container(child: thumbnail), flex: 0);
 
diff --git a/dart/lib/components/slideshow.dart b/dart/lib/components/slideshow.dart
index fee3b05..a3f3aee 100644
--- a/dart/lib/components/slideshow.dart
+++ b/dart/lib/components/slideshow.dart
@@ -80,7 +80,7 @@
 
   Widget _buildPortraitLayout(BuildContext context) {
     var image = new Flexible(child: _buildImage(context), flex: 5);
-    var actions = new Flexible(child: _buildActions(context), flex: 1);
+    var actions = new Flexible(child: _buildActions(context), flex: 0);
     var notes = new Flexible(child: _buildNotes(), flex: 3);
     var nav = new Flexible(child: new Row(_buildThumbnailNavs()), flex: 3);
 
@@ -101,7 +101,7 @@
     var nav = new Flexible(child: new Column(_buildThumbnailNavs()), flex: 8);
 
     var image = new Flexible(child: _buildImage(context), flex: 11);
-    var actions = new Flexible(child: _buildActions(context), flex: 2);
+    var actions = new Flexible(child: _buildActions(context), flex: 0);
 
     var notesAndNavColumn = new Flexible(
         child: new Column([notes, nav], alignItems: FlexAlignItems.stretch),
diff --git a/dart/lib/components/utils/stop_wrapping.dart b/dart/lib/components/utils/stop_wrapping.dart
index 7c4bd17..7b89e5d 100644
--- a/dart/lib/components/utils/stop_wrapping.dart
+++ b/dart/lib/components/utils/stop_wrapping.dart
@@ -4,7 +4,7 @@
 
 import 'package:flutter/material.dart';
 
-Widget stopWrapping(Text child) {
+Widget stopWrapping(Widget child) {
   // TODO(aghassemi): There is no equivalent of CSS's white-space: nowrap,
   // overflow: hidden or text-overflow: ellipsis in Flutter yet.
   // This workaround simulates white-space: nowrap and overflow: hidden.
diff --git a/dart/lib/main.dart b/dart/lib/main.dart
index cb32ae3..039f701 100644
--- a/dart/lib/main.dart
+++ b/dart/lib/main.dart
@@ -17,7 +17,7 @@
   _initLogging();
   _initBackButtonHandler();
 
-  // TODO(aghassemi): Splash screen while store is initializing.
+  // TODO(aghassemi): Display splash screen while store is initializing.
   store.init().then((_) => runApp(new MaterialApp(
       theme: style.theme,
       title: 'SyncSlides',
diff --git a/dart/lib/stores/syncbase/actions.dart b/dart/lib/stores/syncbase/actions.dart
index 86b1b7d..2a3f2fe 100644
--- a/dart/lib/stores/syncbase/actions.dart
+++ b/dart/lib/stores/syncbase/actions.dart
@@ -100,7 +100,8 @@
     // Syncgroup for deck and presentation data, including blobs.
     await sb.createSyncgroup(_state.settings.mounttable, syncgroupName, [
       sb.SyncbaseClient.syncgroupPrefix(decksTableName, deckId),
-      sb.SyncbaseClient.syncgroupPrefix(presentationsTableName, deckId),
+      sb.SyncbaseClient.syncgroupPrefix(presentationsTableName,
+          keyutil.getPresentationPrefix(deckId, presentationId)),
       sb.SyncbaseClient.syncgroupPrefix(blobsTableName, deckId)
     ]);
 
diff --git a/dart/lib/stores/syncbase/store.dart b/dart/lib/stores/syncbase/store.dart
index 64e705f..5f27c96 100644
--- a/dart/lib/stores/syncbase/store.dart
+++ b/dart/lib/stores/syncbase/store.dart
@@ -159,7 +159,7 @@
       _state._getOrCreateDeckState(deckId)._deck =
           new model.Deck.fromJson(deckId, UTF8.decode(value));
     } else if (changeType == sb.WatchChangeTypes.delete) {
-      _state.decks.remove(deckId);
+      _state._decks.remove(deckId);
     }
   }
 
@@ -199,7 +199,6 @@
 
   _onPresentationDriverChange(int changeType, String rowKey, List<int> value) {
     String deckId = keyutil.presentationDriverKeyToDeckId(rowKey);
-
     _DeckState deckState = _state._getOrCreateDeckState(deckId);
     _PresentationState presentationState = deckState.presentation;
     if (presentationState == null) {
diff --git a/dart/lib/stores/utils/key.dart b/dart/lib/stores/utils/key.dart
index 367dee9..3c0f154 100644
--- a/dart/lib/stores/utils/key.dart
+++ b/dart/lib/stores/utils/key.dart
@@ -42,6 +42,11 @@
   return deckId + '/';
 }
 
+// Constructs a key prefix for a presentation.
+String getPresentationPrefix(String deckId, String presentationId) {
+  return '$deckId/$presentationId';
+}
+
 // Returns true if a key is for a deck.
 bool isDeckKey(String key) {
   return !key.contains('/');
diff --git a/dart/lib/styles/common.dart b/dart/lib/styles/common.dart
index 01bd5ff..71668a0 100644
--- a/dart/lib/styles/common.dart
+++ b/dart/lib/styles/common.dart
@@ -16,10 +16,12 @@
 }
 
 class Size {
-  static const double thumbnailWidth = 250.0;
+  static const double gridbox = 250.0;
+  static const double boxFooterHeight = 55.0;
   static const double listHeight = 120.0;
   static const double thumbnailNavHeight = 250.0;
   static const double questionListThumbnailWidth = 100.0;
+  static const double slideListThumbnailWidth = 200.0;
 }
 
 class Spacing {