Skip to content

Commit 6461936

Browse files
committed
Merge remote-tracking branch 'origin/develop'
2 parents 9dcd75b + bf0a847 commit 6461936

File tree

6 files changed

+120
-23
lines changed

6 files changed

+120
-23
lines changed

docker/lfmerge/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
FROM ghcr.io/sillsdev/lfmerge:2.0.133
1+
FROM ghcr.io/sillsdev/lfmerge:2.0.134
22
# Do not add anything to this Dockerfile, it should stay empty

src/angular-app/bellows/shared/audio-recorder/audio-recorder.component.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { Project } from 'src/angular-app/bellows/shared/model/project.model';
22
import { Session, SessionService } from 'src/angular-app/bellows/core/session.service';
33
import { webmFixDuration } from "webm-fix-duration";
44
import * as angular from "angular";
5+
import { RecordingStateService } from 'src/angular-app/languageforge/lexicon/editor/recording-state.service';
6+
import { NoticeService } from '../../core/notice/notice.service';
57

68
export class AudioRecorderController implements angular.IController {
7-
static $inject = ["$interval", "$scope", "sessionService"];
9+
static $inject = ["$interval", "$scope", "sessionService", "recordingStateService", "silNoticeService"];
810

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

2427
constructor(
25-
private $interval: angular.IIntervalService,
26-
private $scope: angular.IScope,
27-
private sessionService: SessionService
28+
private readonly $interval: angular.IIntervalService,
29+
private readonly $scope: angular.IScope,
30+
private readonly sessionService: SessionService,
31+
private readonly recordingStateService: RecordingStateService,
32+
private readonly notice: NoticeService,
2833
) {}
2934

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

37-
private startRecording() {
42+
private startRecording(): boolean {
43+
if (!this.recordingStateService.startRecording()) {
44+
this.notice.push(this.notice.WARN, "Recording is already in progress", undefined, undefined, 4000);
45+
return false;
46+
}
47+
48+
this.hasUnresolvedRecording = true;
3849
this.recordingTime = "0:00";
3950
var codecSpecs: string;
4051
if(this.project.audioRecordingCodec === 'webm'){
@@ -109,6 +120,7 @@ export class AudioRecorderController implements angular.IController {
109120
console.error(err);
110121
}
111122
);
123+
return true;
112124
}
113125

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

126138
toggleRecording() {
127-
if (this.isRecording) this.stopRecording();
128-
else this.startRecording();
129-
this.isRecording = !this.isRecording;
139+
if (this.isRecording) {
140+
this.stopRecording();
141+
this.isRecording = false;
142+
} else {
143+
this.isRecording = this.startRecording();
144+
}
130145
}
131146

132147
close() {
133148
if (this.isRecording) {
134149
this.stopRecording();
135150
}
136151
this.callback(null);
152+
this.resolveRecording();
137153
}
138154

139155
saveAudio() {
140156
this.callback(this.blob);
157+
this.resolveRecording();
141158
}
142159

143160
recordingSupported() {
@@ -153,6 +170,14 @@ export class AudioRecorderController implements angular.IController {
153170
if (this.isRecording) {
154171
this.stopRecording();
155172
}
173+
this.resolveRecording();
174+
}
175+
176+
private resolveRecording() {
177+
if (this.hasUnresolvedRecording) {
178+
this.recordingStateService.resolveRecording();
179+
this.hasUnresolvedRecording = false;
180+
}
156181
}
157182
}
158183

src/angular-app/languageforge/lexicon/editor/editor.component.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { LexOptionList } from '../shared/model/option-list.model';
3434
import { FieldControl } from './field/field-control.model';
3535
import { OfflineCacheUtilsService } from '../../../bellows/core/offline/offline-cache-utils.service';
3636
import { IDeferred } from 'angular';
37+
import { RecordingStateService } from './recording-state.service';
3738

3839
class Show {
3940
more: () => void;
@@ -94,6 +95,7 @@ export class LexiconEditorController implements angular.IController {
9495
'lexRightsService',
9596
'lexSendReceive',
9697
'offlineCacheUtils',
98+
'recordingStateService',
9799
];
98100

99101
constructor(private readonly $filter: angular.IFilterService,
@@ -115,6 +117,7 @@ export class LexiconEditorController implements angular.IController {
115117
private readonly rightsService: LexiconRightsService,
116118
private readonly sendReceive: LexiconSendReceiveService,
117119
private readonly offlineCacheUtils: OfflineCacheUtilsService,
120+
private readonly recordingStateService: RecordingStateService,
118121
) { }
119122

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

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

375+
// We might be saving, because the user is navigating away from this entry,
376+
// in which case we don't want to lose the upload.
377+
await this.recordingStateService.uploading$();
378+
372379
if (this.hasUnsavedChanges() && this.lecRights.canEditEntry()) {
373380
this.cancelAutoSaveTimer();
374381
this.sendReceive.setStateUnsynced();
@@ -1125,7 +1132,15 @@ export class LexiconEditorController implements angular.IController {
11251132
return this.lecConfig.inputSystems[inputSystemTag].abbreviation;
11261133
}
11271134

1128-
private setCurrentEntry(entry: LexEntry = new LexEntry()): void {
1135+
private setCurrentEntry(entry: LexEntry = new LexEntry()): Promise<void> {
1136+
if (entry.id === this.currentEntry.id && this.recordingStateService.hasUnsavedChanges()) {
1137+
// If it's the same entry, then we're just making sure the UI is as up to date
1138+
// as possible. If the user is recording or has audio to save, then we can't touch
1139+
// the UI, because the audio will get lost.
1140+
// If it's a different entry, we assume the user is intentionally discarding the audio.
1141+
return;
1142+
}
1143+
11291144
// align custom fields into model
11301145
entry = this.alignCustomFieldsInData(entry);
11311146

src/angular-app/languageforge/lexicon/editor/editor.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {LexiconCoreModule} from '../core/lexicon-core.module';
1010
import {EditorCommentsModule} from './comment/comment.module';
1111
import {LexiconEditorComponent, LexiconEditorEntryController, LexiconEditorListController} from './editor.component';
1212
import {EditorFieldModule} from './field/field.module';
13+
import { RecordingStateService } from './recording-state.service';
1314

1415
export const LexiconEditorModule = angular
1516
.module('lexiconEditorModule', [
@@ -27,6 +28,7 @@ export const LexiconEditorModule = angular
2728
.component('lexiconEditor', LexiconEditorComponent)
2829
.controller('EditorListCtrl', LexiconEditorListController)
2930
.controller('EditorEntryCtrl', LexiconEditorEntryController)
31+
.service('recordingStateService', RecordingStateService)
3032
.config(['$stateProvider', ($stateProvider: angular.ui.IStateProvider) => {
3133

3234
// State machine from ui.router

src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {UploadFile, UploadResponse} from '../../../../bellows/shared/model/uploa
1111
import {LexiconProjectService} from '../../core/lexicon-project.service';
1212
import {Rights} from '../../core/lexicon-rights.service';
1313
import {LexiconUtilityService} from '../../core/lexicon-utility.service';
14+
import { RecordingStateService } from '../recording-state.service';
1415

1516
export class FieldAudioController implements angular.IController {
1617
dcFilename: string;
@@ -21,18 +22,27 @@ export class FieldAudioController implements angular.IController {
2122
showAudioUpload: boolean = false;
2223
showAudioRecorder: boolean = false;
2324

25+
private uploading$: angular.IDeferred<void>;
26+
2427
static $inject = ['$filter', '$state',
2528
'Upload', 'modalService',
2629
'silNoticeService', 'sessionService',
27-
'lexProjectService', '$scope'
30+
'lexProjectService', '$scope', '$q', 'recordingStateService'
2831
];
29-
constructor(private $filter: angular.IFilterService, private $state: angular.ui.IStateService,
30-
private Upload: any, private modalService: ModalService,
31-
private notice: NoticeService, private sessionService: SessionService,
32-
private lexProjectService: LexiconProjectService, private $scope: angular.IScope) {
33-
34-
this.$scope.$watch(() => this.dcFilename, () => this.showAudioRecorder = false);
35-
}
32+
constructor(
33+
private $filter: angular.IFilterService,
34+
private $state: angular.ui.IStateService,
35+
private Upload: angular.angularFileUpload.IUploadService,
36+
private modalService: ModalService,
37+
private notice: NoticeService,
38+
private sessionService: SessionService,
39+
private lexProjectService: LexiconProjectService,
40+
private $scope: angular.IScope,
41+
private $q: angular.IQService,
42+
private recordingStateService: RecordingStateService,
43+
) {
44+
this.$scope.$watch(() => this.dcFilename, () => this.showAudioRecorder = false);
45+
}
3646

3747
hasAudio(): boolean {
3848
if (this.dcFilename == null) {
@@ -101,14 +111,17 @@ export class FieldAudioController implements angular.IController {
101111
}
102112

103113
this.notice.setLoading('Uploading ' + file.name + '...');
104-
this.Upload.upload({
114+
this.uploading$ = this.$q.defer<void>();
115+
this.recordingStateService.startUploading(this.uploading$.promise);
116+
return this.Upload.upload<any>({
117+
method: 'POST',
105118
url: '/upload/audio',
106119
data: {
107120
file,
108121
previousFilename: this.dcFilename,
109122
recordedInBrowser: recordedInBrowser
110123
}
111-
}).then((response: UploadResponse) => {
124+
}).then((response) => {
112125
this.notice.cancelLoading();
113126
const isUploadSuccess = response.data.result;
114127
if (isUploadSuccess) {
@@ -143,7 +156,7 @@ export class FieldAudioController implements angular.IController {
143156

144157
(evt: ProgressEvent) => {
145158
this.notice.setPercentComplete(Math.floor(100.0 * evt.loaded / evt.total));
146-
});
159+
}).finally(() => this.uploading$.resolve());
147160
});
148161
}
149162

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

186+
$onDestroy() {
187+
this.uploading$?.resolve();
188+
}
173189
}
174190

175191
export const FieldAudioComponent: angular.IComponentOptions = {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as angular from 'angular';
2+
3+
export class RecordingStateService {
4+
static $inject: string[] = ['$q'];
5+
6+
private hasUnresolvedRecording = false;
7+
private uploads$: angular.IPromise<unknown>[] = [];
8+
9+
constructor(private readonly $q: angular.IQService) {
10+
}
11+
12+
startRecording(): boolean {
13+
if (this.hasUnresolvedRecording) {
14+
return false;
15+
} else {
16+
this.hasUnresolvedRecording = true;
17+
return true;
18+
}
19+
}
20+
21+
resolveRecording(): void {
22+
this.hasUnresolvedRecording = false;
23+
}
24+
25+
startUploading(upload: angular.IPromise<unknown>): void {
26+
this.uploads$.push(upload);
27+
upload.finally(() =>
28+
this.uploads$ = this.uploads$.filter(_upload => _upload !== upload)
29+
);
30+
}
31+
32+
uploading$(): angular.IPromise<unknown> {
33+
return this.$q.all(this.uploads$);
34+
}
35+
36+
hasUnsavedChanges(): boolean {
37+
return this.hasUnresolvedRecording || this.uploads$.length > 0;
38+
}
39+
}

0 commit comments

Comments
 (0)