blob: fb3cb649fe4004f6f9873615926b1e7befc8919b [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import '../stores/store.dart';
import '../styles/common.dart' as style;
import '../utils/image_provider.dart' as image_provider;
import 'askquestion.dart';
import 'questionlist.dart';
import 'slideshow_fullscreen.dart';
import 'syncslides_page.dart';
final GlobalKey _scaffoldKey = new GlobalKey();
final Logger log = new Logger('components/slideshow');
class SlideshowPage extends SyncSlidesPage {
final String _deckId;
SlideshowPage(this._deckId);
@override
Widget build(BuildContext context, AppState appState, AppActions appActions) {
if (!appState.decks.containsKey(_deckId)) {
// TODO(aghassemi): Proper error page with navigation back to main view.
return new Text('Deck no longer exists.');
}
return new Scaffold(
key: _scaffoldKey,
body: new Material(
child:
new SlideShow(appActions, appState, appState.decks[_deckId])));
}
}
class SlideShow extends StatelessWidget {
AppActions _appActions;
AppState _appState;
DeckState _deckState;
int _currSlideNum;
SlideShow(this._appActions, this._appState, this._deckState);
@override
Widget build(BuildContext context) {
if (_deckState.slides.length == 0) {
// TODO(aghassemi): Proper error page with navigation back to main view.
return new Text('No slide to show.');
}
if (_deckState.presentation != null &&
_deckState.presentation.isFollowingPresentation) {
_currSlideNum = _deckState.presentation.currSlideNum;
} else {
_currSlideNum = _deckState.currSlideNum;
}
if (_currSlideNum >= _deckState.slides.length) {
// TODO(aghassemi): Can this ever happen?
// -What if slide number set by another peer is synced before the actual slides?
// -What if we have navigated to a particular slide on our own and peer deletes that slide?
// I think without careful batching and consuming watch events as batches, this could happen
// maybe for a split second until rest of data syncs up.
// UI needs to be bullet-roof, a flicker in the UI is better than an exception and crash.
log.shout(
'Current slide number $_currSlideNum is greater than the number of slides ${_deckState.slides.length}.');
// TODO(aghassemi): Proper error page with navigation back to main view.
return new Text('Slide does not exist.');
}
if (MediaQuery.of(context).orientation == Orientation.landscape) {
return _buildLandscapeLayout(context);
} else {
return _buildPortraitLayout(context);
}
}
Widget _buildPortraitLayout(BuildContext context) {
var image = new Flexible(child: _buildImage(context), flex: 5);
var actions = new Flexible(child: _buildActions(context), flex: 0);
var notes = new Flexible(child: _buildNotes(), flex: 3);
var nav =
new Flexible(child: new Row(children: _buildThumbnailNavs()), flex: 3);
var items = [image, actions, notes, nav];
var footer = _buildFooter();
if (footer != null) {
items.add(footer);
}
var layout = new Column(
children: items, crossAxisAlignment: CrossAxisAlignment.stretch);
return layout;
}
Widget _buildLandscapeLayout(BuildContext context) {
var notes = new Flexible(child: _buildNotes(), flex: 5);
var nav = new Flexible(
child: new Column(children: _buildThumbnailNavs()), flex: 8);
var image = new Flexible(child: _buildImage(context), flex: 11);
var actions = new Flexible(child: _buildActions(context), flex: 0);
var notesAndNavColumn = new Flexible(
child: new Column(
children: [notes, nav],
crossAxisAlignment: CrossAxisAlignment.stretch),
flex: 4);
var imageAndActionsColumn = new Flexible(
child: new Column(
children: [image, actions],
crossAxisAlignment: CrossAxisAlignment.stretch),
flex: 16);
var layout = new Row(
children: [notesAndNavColumn, imageAndActionsColumn],
crossAxisAlignment: CrossAxisAlignment.stretch);
var footer = _buildFooter();
if (footer != null) {
layout = new Column(
children: [new Flexible(child: layout, flex: 8), footer],
crossAxisAlignment: CrossAxisAlignment.stretch);
}
return layout;
}
List<Widget> _buildThumbnailNavs() {
return <Widget>[
_buildThumbnailNav(_currSlideNum - 1, 'Previous'),
_buildThumbnailNav(_currSlideNum + 1, 'Next')
];
}
Widget _buildImage(BuildContext context) {
var provider = image_provider.getSlideImage(
_deckState.deck.key, _deckState.slides[_currSlideNum]);
var image = new AsyncImage(provider: provider, fit: ImageFit.scaleDown);
// If not driving the presentation, tapping the image navigates to fullscreen mode.
if (_deckState.presentation == null ||
!_deckState.presentation.isDriving(_appState.user)) {
image = new InkWell(
child: image,
onTap: () {
Navigator.push(
context,
new MaterialPageRoute(builder: (context) =>
new SlideshowFullscreenPage(_deckState.deck.key)));
});
}
var counter = _buildBubbleOverlay(
'${_currSlideNum + 1} of ${_deckState.slides.length}', 0.5, 0.98);
image = new Stack(children: [image, counter]);
return new ClipRect(child: image);
}
Widget _buildNotes() {
var notes =
new Text('Notes (only you see these)', style: style.Text.subtitleStyle);
var container = new Container(
child: notes,
padding: style.Spacing.normalPadding,
decoration: new BoxDecoration(border: new Border(
bottom: new BorderSide(color: style.theme.dividerColor))));
return container;
}
Widget _buildThumbnailNav(int slideNum, String label) {
var container;
if (slideNum >= 0 && slideNum < _deckState.slides.length) {
var thumbnail = new AsyncImage(
provider: image_provider.getSlideImage(
_deckState.deck.key, _deckState.slides[slideNum]),
height: style.Size.thumbnailNavHeight,
fit: ImageFit.scaleDown);
container = new InkWell(
child: thumbnail,
onTap: () {
_appActions.setCurrSlideNum(_deckState.deck.key, slideNum);
});
} else {
// Empty grey placeholder.
container = new Container(
decoration: new BoxDecoration(backgroundColor: Colors.grey[100]));
}
var nextPreviousBubble = _buildBubbleOverlay(label, 0.5, 0.05);
container = new Stack(children: [container, nextPreviousBubble]);
container = new ClipRect(child: container);
return new Flexible(child: container, flex: 1);
}
Widget _buildActions(BuildContext context) {
// It collects a list of action widgets for the action bar and fabs.
// Left contains items that are in-line on the left side of the UI.
// Right contains the FABs that hover over the right side of the UI.
List<Widget> left = [];
List<Widget> right = [];
_buildActionsPrev(left, right);
_buildActionsSlidelist(left, right, context);
_buildActionsQuestion(left, right, context);
_buildActionsNext(left, right);
_buildActionsFollowPresentation(left, right);
return new AppBar(
leading: new Row(children: _buildActionsAddMargin(left)),
actions: right);
}
void _buildActionsPrev(List<Widget> left, List<Widget> right) {
if (_currSlideNum == 0) {
return;
}
var prev = new InkWell(
child: new Icon(icon: Icons.arrow_back),
onTap: () {
_appActions.setCurrSlideNum(_deckState.deck.key, _currSlideNum - 1);
});
left.add(prev);
}
void _buildActionsSlidelist(
List<Widget> left, List<Widget> right, BuildContext context) {
var slideList = new InkWell(
child: new Icon(icon: Icons.layers),
onTap: () {
Navigator.pop(context);
});
left.add(slideList);
}
void _buildActionsQuestion(
List<Widget> left, List<Widget> right, BuildContext context) {
if (_deckState.presentation == null) {
return;
}
// Presentation over is taken to a list of questions view.
if (_deckState.presentation.isOwner) {
var numQuestions = new FloatingActionButton(child: new Text(
_deckState.presentation.questions.length.toString(),
style: style.theme.primaryTextTheme.title));
// TODO(aghassemi): Find a better way. Scaling down a FAB and
// using transform to position it does not seem to be the best approach.
final Matrix4 moveUp = new Matrix4.identity().translate(-95.0, 25.0);
final Matrix4 scaleDown = new Matrix4.identity().scale(0.3);
numQuestions = new Transform(child: numQuestions, transform: moveUp);
numQuestions = new Transform(child: numQuestions, transform: scaleDown);
var questions = new InkWell(
child: new Icon(icon: Icons.help),
onTap: () {
Navigator.push(
context,
new MaterialPageRoute(builder: (context) =>
new QuestionListPage(_deckState.deck.key)));
});
left.add(questions);
left.add(numQuestions);
} else {
// Audience is taken to ask a question view.
var route = new MaterialPageRoute(builder: (context) =>
new AskQuestionPage(_deckState.deck.key, _currSlideNum));
var askQuestion = new InkWell(
child: new Icon(icon: Icons.help),
onTap: () {
Navigator.push(context, route);
});
left.add(askQuestion);
}
}
final Matrix4 moveUpFabTransform =
new Matrix4.identity().translate(0.0, -27.5);
void _buildActionsNext(List<Widget> left, List<Widget> right) {
if (_currSlideNum >= (_deckState.slides.length - 1)) {
return;
}
var nextOnTap = () {
_appActions.setCurrSlideNum(_deckState.deck.key, _currSlideNum + 1);
};
// If driving the presentation, show a bigger FAB next button on the right side,
// otherwise a regular next button on the left side.
if (_deckState.presentation != null &&
_deckState.presentation.isDriving(_appState.user)) {
var next = new FloatingActionButton(
child: new Icon(icon: Icons.arrow_forward), onPressed: nextOnTap);
var container =
new Container(child: next, margin: style.Spacing.fabMargin);
next = new Transform(transform: moveUpFabTransform, child: container);
right.add(next);
} else {
var next = new InkWell(
child: new Icon(icon: Icons.arrow_forward), onTap: nextOnTap);
left.add(next);
}
}
void _buildActionsFollowPresentation(List<Widget> left, List<Widget> right) {
if (_deckState.presentation == null ||
_deckState.presentation.isFollowingPresentation) {
return;
}
var syncNav = new FloatingActionButton(
child: new Icon(icon: Icons.sync),
onPressed: () async {
_appActions.followPresentation(_deckState.deck.key);
});
syncNav =
new Container(child: syncNav, margin: style.Spacing.actionsMargin);
syncNav = new Transform(transform: moveUpFabTransform, child: syncNav);
right.add(syncNav);
}
Widget _buildFooter() {
if (_deckState.presentation == null) {
return null;
}
// Owner and not driving?
if (_deckState.presentation.isOwner &&
!_deckState.presentation.isDriving(_appState.user)) {
SnackBarAction resume = new SnackBarAction(
label: 'RESUME',
onPressed: () {
_appActions.setDriver(_deckState.deck.key, _appState.user);
});
return _buildSnackbarFooter('You have handed off control.',
action: resume);
}
// Driving but not the owner?
if (!_deckState.presentation.isOwner &&
_deckState.presentation.isDriving(_appState.user)) {
return _buildSnackbarFooter('You are now driving the presentation.');
}
return null;
}
List<Widget> _buildActionsAddMargin(List<Widget> actions) {
return actions
.map(
(w) => new Container(child: w, margin: style.Spacing.actionsMargin))
.toList();
}
Widget _buildBubbleOverlay(String text, double xOffset, double yOffset) {
return new Align(
child: new Container(
child: new DefaultTextStyle(
child: new Text(text), style: Typography.white.body1),
decoration: new BoxDecoration(
borderRadius: 50.0, // Make the bubble round.
backgroundColor:
style.Box.bubbleOverlayBackground), // Transparent gray.
padding: new EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0)),
alignment: new FractionalOffset(xOffset, yOffset));
}
Widget _buildSnackbarFooter(String lable, {SnackBarAction action}) {
var text = new Text(lable);
text = new DefaultTextStyle(style: Typography.white.subhead, child: text);
List<Widget> children = <Widget>[
new Flexible(child: new Container(
margin: style.Spacing.footerVerticalMargin,
child: new DefaultTextStyle(
style: Typography.white.subhead, child: text)))
];
if (action != null) {
children.add(action);
}
var clipper = new ClipRect(child: new Material(
elevation: 6,
color: style.Box.footerBackground,
child: new Container(
margin: style.Spacing.footerHorizontalMargin,
child: new DefaultTextStyle(
style: new TextStyle(color: style.theme.accentColor),
child: new Row(children: children)))));
return new Flexible(child: clipper, flex: 1);
}
}