| // 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. |
| |
| part of syncbase_store; |
| |
| class _AppActions extends AppActions { |
| _AppState _state; |
| Function _emitChange; |
| _AppActions(this._state, this._emitChange); |
| |
| ////////////////////////////////////// |
| // Decks |
| |
| Future addDeck(model.Deck deck) async { |
| log.info("Adding deck ${deck.name}..."); |
| sb.SyncbaseTable tb = _getDecksTable(); |
| await tb.put(deck.key, UTF8.encode(deck.toJson())); |
| log.info("Deck ${deck.name} added."); |
| } |
| |
| Future removeDeck(String deckKey) async { |
| sb.SyncbaseTable tb = _getDecksTable(); |
| tb.deleteRange(new sb.RowRange.prefix(deckKey)); |
| } |
| |
| Future setSlides(String deckKey, List<model.Slide> slides) async { |
| sb.SyncbaseTable tb = _getDecksTable(); |
| |
| slides.forEach((slide) async { |
| // TODO(aghassemi): Use batching. |
| await tb.put( |
| keyutil.getSlideKey(deckKey, slide.num), UTF8.encode(slide.toJson())); |
| }); |
| } |
| |
| Future setCurrSlideNum(String deckId, int slideNum) async { |
| var deckState = _state._getOrCreateDeckState(deckId); |
| if (slideNum < 0 || slideNum >= deckState.slides.length) { |
| throw new ArgumentError.value(slideNum, |
| "Slide number out of range. Only ${deckState.slides.length} slides exist."); |
| } |
| |
| // TODO(aghassemi): Have a session table to persist UI state such as |
| // local slide number within a deck, whether user is navigating a presentation |
| // on their own, and similar UI state that is desirable to be persisted. |
| deckState._currSlideNum = slideNum; |
| |
| // Is slide number change happening within a presentation? |
| if (deckState.presentation != null) { |
| // Is the current user driving the presentation? |
| if (deckState.presentation.isDriving(_state.user)) { |
| // Update the common slide number for the presentation. |
| sb.SyncbaseTable tb = _getPresentationsTable(); |
| await tb.put( |
| keyutil.getPresentationCurrSlideNumKey( |
| deckId, deckState.presentation.key), |
| UTF8.encode(slideNum.toString())); |
| } else { |
| // User is not driving the presentation so they are navigating on their own. |
| deckState.presentation._isFollowingPresentation = false; |
| } |
| } |
| _emitChange(); |
| } |
| |
| Future loadDeckFromSdCard() { |
| return new Loader.singleton().loadDeck(); |
| } |
| |
| ////////////////////////////////////// |
| // Presentation |
| |
| Future<model.PresentationAdvertisement> startPresentation( |
| String deckId) async { |
| if (!_state._decks.containsKey(deckId)) { |
| throw new ArgumentError.value(deckId, 'Deck no longer exists.'); |
| } |
| |
| // Stop the existing presentation, if any. |
| if (_state._advertisedPresentation != null) { |
| await stopPresentation(_state._advertisedPresentation.key); |
| } |
| |
| model.Deck deck = _state._getOrCreateDeckState(deckId)._deck; |
| String presentationId = uuidutil.createUuid(); |
| String syncgroupName = _getSyncgroupName(_state.settings, presentationId); |
| String thumbnailSyncgroupName = |
| _getSyncgroupName(_state.settings, deck.thumbnail.key); |
| |
| var presentation = new model.PresentationAdvertisement( |
| presentationId, deck, syncgroupName, thumbnailSyncgroupName); |
| |
| // Syncgroup for deck and presentation data, including blobs. |
| await sb.createSyncgroup(_state.settings.mounttable, syncgroupName, [ |
| sb.SyncbaseClient.syncgroupPrefix(decksTableName, deckId), |
| sb.SyncbaseClient.syncgroupPrefix(presentationsTableName, |
| keyutil.getPresentationPrefix(deckId, presentationId)), |
| sb.SyncbaseClient.syncgroupPrefix(blobsTableName, deckId) |
| ]); |
| |
| // TODO(aghassemi): Use a simple RPC instead of a syncgroup to get the thumbnail. |
| // See https://github.com/vanadium/syncslides/issues/17 |
| // Syncgroup for deck thumbnail. |
| await sb.createSyncgroup( |
| _state.settings.mounttable, thumbnailSyncgroupName, [ |
| sb.SyncbaseClient.syncgroupPrefix(blobsTableName, deck.thumbnail.key) |
| ]); |
| |
| await discovery.advertise(presentation); |
| _state._advertisedPresentation = presentation; |
| |
| // Set the presentation state for the deck. |
| _DeckState deckState = _state._getOrCreateDeckState(deckId); |
| _PresentationState presentationstate = |
| deckState._getOrCreatePresentationState(presentation.key); |
| presentationstate._isOwner = true; |
| |
| setDefaultsAndJoin() async { |
| // Set the current slide number to 0. |
| sb.SyncbaseTable tb = _getPresentationsTable(); |
| await tb.put( |
| keyutil.getPresentationCurrSlideNumKey(deckId, presentation.key), |
| UTF8.encode('0')); |
| |
| // Set the current user as the driver. |
| await _setPresentationDriver(deckId, presentation.key, _state.user); |
| |
| // Also join the presentation. |
| await joinPresentation(presentation); |
| } |
| |
| try { |
| // Wait for join. If it fails, remove the presentation state from the deck. |
| await setDefaultsAndJoin(); |
| } catch (e) { |
| deckState._isPresenting = false; |
| throw e; |
| } |
| |
| return presentation; |
| } |
| |
| Future joinPresentation(model.PresentationAdvertisement presentation) async { |
| String deckId = presentation.deck.key; |
| |
| // Set the presentation state for the deck. |
| _DeckState deckState = _state._getOrCreateDeckState(deckId); |
| deckState._getOrCreatePresentationState(presentation.key); |
| |
| deckState._isPresenting = true; |
| |
| // Wait until at least the current slide number, driver and the slide for current slide number is synced. |
| join() async { |
| bool isMyOwnPresentation = |
| _state._advertisedPresentation?.key == presentation.key; |
| if (!isMyOwnPresentation) { |
| await sb.joinSyncgroup(presentation.syncgroupName); |
| } |
| |
| Completer completer = new Completer(); |
| new Timer.periodic(new Duration(milliseconds: 30), (Timer timer) { |
| if (_state._decks.containsKey(deckId) && |
| _state._decks[deckId].deck != null && |
| _state._decks[deckId].presentation != null && |
| _state._decks[deckId].slides.length > |
| _state._decks[deckId].presentation.currSlideNum && |
| _state._decks[deckId].presentation.driver != null && |
| !completer.isCompleted) { |
| timer.cancel(); |
| completer.complete(); |
| } |
| }); |
| await completer.future.timeout(new Duration(seconds: 20)); |
| } |
| |
| try { |
| // Wait for join. If it fails, remove the presentation state from the deck. |
| await join(); |
| } catch (e) { |
| deckState._isPresenting = false; |
| throw e; |
| } |
| |
| log.info('Joined presentation ${presentation.key}'); |
| } |
| |
| Future stopPresentation(String presentationId) async { |
| await discovery.stopAdvertising(presentationId); |
| _state._advertisedPresentation = null; |
| _state._decks.values.forEach((_DeckState deck) { |
| if (deck.presentation != null && |
| deck.presentation.key == presentationId) { |
| deck._isPresenting = false; |
| } |
| }); |
| _emitChange(); |
| log.info('Presentation $presentationId stopped'); |
| } |
| |
| Future followPresentation(String deckId) async { |
| var deckState = _state._getOrCreateDeckState(deckId); |
| |
| if (deckState.presentation == null) { |
| throw new ArgumentError.value(deckId, 'Deck is not being presented.'); |
| } |
| |
| // Set the current slide number to the presentation's current slide number. |
| await setCurrSlideNum(deckId, deckState.presentation.currSlideNum); |
| |
| deckState.presentation._isFollowingPresentation = true; |
| } |
| |
| Future askQuestion(String deckId, int slideNum, String questionText) async { |
| var deckState = _state._getOrCreateDeckState(deckId); |
| |
| if (deckState.presentation == null) { |
| throw new ArgumentError.value(deckId, |
| 'Cannot ask a question because deck is not part of a presentation'); |
| } |
| |
| sb.SyncbaseTable tb = _getPresentationsTable(); |
| String questionId = uuidutil.createUuid(); |
| |
| model.Question question = new model.Question( |
| questionId, questionText, slideNum, _state.user, new DateTime.now()); |
| |
| var key = keyutil.getPresentationQuestionKey( |
| deckId, deckState.presentation.key, questionId); |
| |
| await tb.put(key, UTF8.encode(question.toJson())); |
| } |
| |
| Future setDriver(String deckId, model.User driver) async { |
| var deckState = _state._getOrCreateDeckState(deckId); |
| |
| if (deckState.presentation == null) { |
| throw new ArgumentError.value(deckId, |
| 'Cannot set the driver because deck is not part of a presentation'); |
| } |
| await _setPresentationDriver(deckId, deckState.presentation.key, driver); |
| } |
| |
| ////////////////////////////////////// |
| // Blobs |
| |
| Future putBlob(String key, List<int> bytes) async { |
| sb.SyncbaseTable tb = _getBlobsTable(); |
| await tb.put(key, bytes); |
| } |
| |
| Future<List<int>> getBlob(String key) async { |
| sb.SyncbaseTable tb = _getBlobsTable(); |
| return tb.get(key); |
| } |
| } |
| |
| ////////////////////////////////////// |
| // Utilities |
| |
| Future _setPresentationDriver( |
| String deckId, String presentationId, model.User driver) async { |
| sb.SyncbaseTable tb = _getPresentationsTable(); |
| await tb.put(keyutil.getPresentationDriverKey(deckId, presentationId), |
| UTF8.encode(driver.toJson())); |
| } |
| |
| String _getSyncgroupName(model.Settings settings, String uuid) { |
| return '${settings.mounttable}/${settings.deviceId}/%%sync/$uuid'; |
| } |
| |
| sb.SyncbaseTable _getDecksTable() { |
| return sb.database.table(decksTableName); |
| } |
| |
| sb.SyncbaseTable _getPresentationsTable() { |
| return sb.database.table(presentationsTableName); |
| } |
| |
| sb.SyncbaseTable _getBlobsTable() { |
| return sb.database.table(blobsTableName); |
| } |