Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent UI updates when there is an unresolved recording in progress #1763

Merged
merged 2 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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$();
myieye marked this conversation as resolved.
Show resolved Hide resolved

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;
}
}