Skip to content

Commit

Permalink
Prevent UI updates when there is an unresolved recording in progress (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
myieye authored Jun 22, 2023
1 parent 006bd10 commit 8d0a15e
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { Project } from 'src/angular-app/bellows/shared/model/project.model';
import { Session, SessionService } from 'src/angular-app/bellows/core/session.service';
import { webmFixDuration } from "webm-fix-duration";
import * as angular from "angular";
import { RecordingStateService } from 'src/angular-app/languageforge/lexicon/editor/recording-state.service';
import { NoticeService } from '../../core/notice/notice.service';

export class AudioRecorderController implements angular.IController {
static $inject = ["$interval", "$scope", "sessionService"];
static $inject = ["$interval", "$scope", "sessionService", "recordingStateService", "silNoticeService"];

project: Project;
session: Session;
Expand All @@ -20,11 +22,14 @@ export class AudioRecorderController implements angular.IController {
callback: (blob: Blob) => void;
durationInMilliseconds: number;
interval: angular.IPromise<void>;
private hasUnresolvedRecording = false;

constructor(
private $interval: angular.IIntervalService,
private $scope: angular.IScope,
private sessionService: SessionService
private readonly $interval: angular.IIntervalService,
private readonly $scope: angular.IScope,
private readonly sessionService: SessionService,
private readonly recordingStateService: RecordingStateService,
private readonly notice: NoticeService,
) {}

$onInit(): void {
Expand All @@ -34,7 +39,13 @@ export class AudioRecorderController implements angular.IController {
});
}

private startRecording() {
private startRecording(): boolean {
if (!this.recordingStateService.startRecording()) {
this.notice.push(this.notice.WARN, "Recording is already in progress", undefined, undefined, 4000);
return false;
}

this.hasUnresolvedRecording = true;
this.recordingTime = "0:00";
var codecSpecs: string;
if(this.project.audioRecordingCodec === 'webm'){
Expand Down Expand Up @@ -109,6 +120,7 @@ export class AudioRecorderController implements angular.IController {
console.error(err);
}
);
return true;
}

private stopRecording() {
Expand All @@ -124,20 +136,25 @@ export class AudioRecorderController implements angular.IController {
}

toggleRecording() {
if (this.isRecording) this.stopRecording();
else this.startRecording();
this.isRecording = !this.isRecording;
if (this.isRecording) {
this.stopRecording();
this.isRecording = false;
} else {
this.isRecording = this.startRecording();
}
}

close() {
if (this.isRecording) {
this.stopRecording();
}
this.callback(null);
this.resolveRecording();
}

saveAudio() {
this.callback(this.blob);
this.resolveRecording();
}

recordingSupported() {
Expand All @@ -153,6 +170,14 @@ export class AudioRecorderController implements angular.IController {
if (this.isRecording) {
this.stopRecording();
}
this.resolveRecording();
}

private resolveRecording() {
if (this.hasUnresolvedRecording) {
this.recordingStateService.resolveRecording();
this.hasUnresolvedRecording = false;
}
}
}

Expand Down
21 changes: 18 additions & 3 deletions src/angular-app/languageforge/lexicon/editor/editor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { LexOptionList } from '../shared/model/option-list.model';
import { FieldControl } from './field/field-control.model';
import { OfflineCacheUtilsService } from '../../../bellows/core/offline/offline-cache-utils.service';
import { IDeferred } from 'angular';
import { RecordingStateService } from './recording-state.service';

class Show {
more: () => void;
Expand Down Expand Up @@ -94,6 +95,7 @@ export class LexiconEditorController implements angular.IController {
'lexRightsService',
'lexSendReceive',
'offlineCacheUtils',
'recordingStateService',
];

constructor(private readonly $filter: angular.IFilterService,
Expand All @@ -115,6 +117,7 @@ export class LexiconEditorController implements angular.IController {
private readonly rightsService: LexiconRightsService,
private readonly sendReceive: LexiconSendReceiveService,
private readonly offlineCacheUtils: OfflineCacheUtilsService,
private readonly recordingStateService: RecordingStateService,
) { }

$onInit(): void {
Expand Down Expand Up @@ -232,8 +235,8 @@ export class LexiconEditorController implements angular.IController {
this.$state.go('importExport');
}

returnToList(): void {
this.saveCurrentEntry();
async returnToList(): Promise<void> {
await this.saveCurrentEntry();
this.setCurrentEntry();
this.hideRightPanelWithoutAnimation();
this.$state.go('editor.list', {
Expand Down Expand Up @@ -369,6 +372,10 @@ export class LexiconEditorController implements angular.IController {
// entry and is NOT going to a different entry (as is the case with editing another entry.
let newEntryTempId: string;

// We might be saving, because the user is navigating away from this entry,
// in which case we don't want to lose the upload.
await this.recordingStateService.uploading$();

if (this.hasUnsavedChanges() && this.lecRights.canEditEntry()) {
this.cancelAutoSaveTimer();
this.sendReceive.setStateUnsynced();
Expand Down Expand Up @@ -1125,7 +1132,15 @@ export class LexiconEditorController implements angular.IController {
return this.lecConfig.inputSystems[inputSystemTag].abbreviation;
}

private setCurrentEntry(entry: LexEntry = new LexEntry()): void {
private setCurrentEntry(entry: LexEntry = new LexEntry()): Promise<void> {
if (entry.id === this.currentEntry.id && this.recordingStateService.hasUnsavedChanges()) {
// If it's the same entry, then we're just making sure the UI is as up to date
// as possible. If the user is recording or has audio to save, then we can't touch
// the UI, because the audio will get lost.
// If it's a different entry, we assume the user is intentionally discarding the audio.
return;
}

// align custom fields into model
entry = this.alignCustomFieldsInData(entry);

Expand Down
2 changes: 2 additions & 0 deletions src/angular-app/languageforge/lexicon/editor/editor.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {LexiconCoreModule} from '../core/lexicon-core.module';
import {EditorCommentsModule} from './comment/comment.module';
import {LexiconEditorComponent, LexiconEditorEntryController, LexiconEditorListController} from './editor.component';
import {EditorFieldModule} from './field/field.module';
import { RecordingStateService } from './recording-state.service';

export const LexiconEditorModule = angular
.module('lexiconEditorModule', [
Expand All @@ -27,6 +28,7 @@ export const LexiconEditorModule = angular
.component('lexiconEditor', LexiconEditorComponent)
.controller('EditorListCtrl', LexiconEditorListController)
.controller('EditorEntryCtrl', LexiconEditorEntryController)
.service('recordingStateService', RecordingStateService)
.config(['$stateProvider', ($stateProvider: angular.ui.IStateProvider) => {

// State machine from ui.router
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {UploadFile, UploadResponse} from '../../../../bellows/shared/model/uploa
import {LexiconProjectService} from '../../core/lexicon-project.service';
import {Rights} from '../../core/lexicon-rights.service';
import {LexiconUtilityService} from '../../core/lexicon-utility.service';
import { RecordingStateService } from '../recording-state.service';

export class FieldAudioController implements angular.IController {
dcFilename: string;
Expand All @@ -21,18 +22,27 @@ export class FieldAudioController implements angular.IController {
showAudioUpload: boolean = false;
showAudioRecorder: boolean = false;

private uploading$: angular.IDeferred<void>;

static $inject = ['$filter', '$state',
'Upload', 'modalService',
'silNoticeService', 'sessionService',
'lexProjectService', '$scope'
'lexProjectService', '$scope', '$q', 'recordingStateService'
];
constructor(private $filter: angular.IFilterService, private $state: angular.ui.IStateService,
private Upload: any, private modalService: ModalService,
private notice: NoticeService, private sessionService: SessionService,
private lexProjectService: LexiconProjectService, private $scope: angular.IScope) {

this.$scope.$watch(() => this.dcFilename, () => this.showAudioRecorder = false);
}
constructor(
private $filter: angular.IFilterService,
private $state: angular.ui.IStateService,
private Upload: angular.angularFileUpload.IUploadService,
private modalService: ModalService,
private notice: NoticeService,
private sessionService: SessionService,
private lexProjectService: LexiconProjectService,
private $scope: angular.IScope,
private $q: angular.IQService,
private recordingStateService: RecordingStateService,
) {
this.$scope.$watch(() => this.dcFilename, () => this.showAudioRecorder = false);
}

hasAudio(): boolean {
if (this.dcFilename == null) {
Expand Down Expand Up @@ -101,14 +111,17 @@ export class FieldAudioController implements angular.IController {
}

this.notice.setLoading('Uploading ' + file.name + '...');
this.Upload.upload({
this.uploading$ = this.$q.defer<void>();
this.recordingStateService.startUploading(this.uploading$.promise);
return this.Upload.upload<any>({
method: 'POST',
url: '/upload/audio',
data: {
file,
previousFilename: this.dcFilename,
recordedInBrowser: recordedInBrowser
}
}).then((response: UploadResponse) => {
}).then((response) => {
this.notice.cancelLoading();
const isUploadSuccess = response.data.result;
if (isUploadSuccess) {
Expand Down Expand Up @@ -143,7 +156,7 @@ export class FieldAudioController implements angular.IController {

(evt: ProgressEvent) => {
this.notice.setPercentComplete(Math.floor(100.0 * evt.loaded / evt.total));
});
}).finally(() => this.uploading$.resolve());
});
}

Expand All @@ -170,6 +183,9 @@ export class FieldAudioController implements angular.IController {
return filename.substr(filename.indexOf('_') + 1);
}

$onDestroy() {
this.uploading$?.resolve();
}
}

export const FieldAudioComponent: angular.IComponentOptions = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as angular from 'angular';

export class RecordingStateService {
static $inject: string[] = ['$q'];

private hasUnresolvedRecording = false;
private uploads$: angular.IPromise<unknown>[] = [];

constructor(private readonly $q: angular.IQService) {
}

startRecording(): boolean {
if (this.hasUnresolvedRecording) {
return false;
} else {
this.hasUnresolvedRecording = true;
return true;
}
}

resolveRecording(): void {
this.hasUnresolvedRecording = false;
}

startUploading(upload: angular.IPromise<unknown>): void {
this.uploads$.push(upload);
upload.finally(() =>
this.uploads$ = this.uploads$.filter(_upload => _upload !== upload)
);
}

uploading$(): angular.IPromise<unknown> {
return this.$q.all(this.uploads$);
}

hasUnsavedChanges(): boolean {
return this.hasUnresolvedRecording || this.uploads$.length > 0;
}
}

0 comments on commit 8d0a15e

Please sign in to comment.