diff --git a/package-lock.json b/package-lock.json index 3ba15042c..ec9e787be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "capture-lite", "version": "0.47.0", "dependencies": { "@angular/animations": "^12.2.4", @@ -39,6 +40,7 @@ "immutable": "^4.0.0-rc.14", "lodash-es": "^4.17.21", "material-design-icons-iconfont": "^6.1.0", + "ngx-long-press2": "^2.0.0", "ngx-pinch-zoom": "^2.6.0", "process": "^0.11.10", "rxjs": "^6.6.7", @@ -14522,6 +14524,21 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, + "node_modules/ngx-long-press2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ngx-long-press2/-/ngx-long-press2-2.0.0.tgz", + "integrity": "sha512-vXYaoy4izaCWpgD1Z1t+rHnp+XW3MvLRr2raagtQcQW9m+2OWeS3+jTa13jnr6nqq6mSEHy1MtY+mRuZS7pXww==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10.13" + }, + "peerDependencies": { + "@angular/common": "^10.0.14", + "@angular/core": "^10.0.14" + } + }, "node_modules/ngx-pinch-zoom": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/ngx-pinch-zoom/-/ngx-pinch-zoom-2.6.0.tgz", @@ -36108,6 +36125,14 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, + "ngx-long-press2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ngx-long-press2/-/ngx-long-press2-2.0.0.tgz", + "integrity": "sha512-vXYaoy4izaCWpgD1Z1t+rHnp+XW3MvLRr2raagtQcQW9m+2OWeS3+jTa13jnr6nqq6mSEHy1MtY+mRuZS7pXww==", + "requires": { + "tslib": "^2.0.0" + } + }, "ngx-pinch-zoom": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/ngx-pinch-zoom/-/ngx-pinch-zoom-2.6.0.tgz", diff --git a/package.json b/package.json index c2be805d6..0493a4887 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "immutable": "^4.0.0-rc.14", "lodash-es": "^4.17.21", "material-design-icons-iconfont": "^6.1.0", + "ngx-long-press2": "^2.0.0", "ngx-pinch-zoom": "^2.6.0", "process": "^0.11.10", "rxjs": "^6.6.7", diff --git a/src/app/features/home/home.page.ts b/src/app/features/home/home.page.ts index 2659d6577..3ca44418c 100644 --- a/src/app/features/home/home.page.ts +++ b/src/app/features/home/home.page.ts @@ -5,7 +5,7 @@ import { Plugins } from '@capacitor/core'; import { ActionSheetController, AlertController } from '@ionic/angular'; import { TranslocoService } from '@ngneat/transloco'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { defer, EMPTY, iif, of } from 'rxjs'; +import { combineLatest, defer, EMPTY, iif, of } from 'rxjs'; import { catchError, concatMap, @@ -154,40 +154,42 @@ export class HomePage { } private presentCaptureActions$() { - return this.translocoService - .selectTranslateObject({ + return combineLatest([ + this.translocoService.selectTranslateObject({ takePicture: null, recordVideo: null, + }), + this.goProBluetoothService.connectedDevice$, + ]).pipe( + first(), + concatMap(([translations, connectedDevice]) => { + const [takePicture, recordVideo] = translations; + + return new Promise(resolve => { + const buttons = [ + { + text: takePicture, + handler: () => resolve(this.cameraService.takePhoto()), + }, + { + text: recordVideo, + handler: () => resolve(this.recordVideo()), + }, + ]; + + if (connectedDevice) { + buttons.push({ + text: 'Capture from GoPro', + handler: () => resolve(this.caputureFromGoPro()), + }); + } + + return this.actionSheetController + .create({ buttons }) + .then(sheet => sheet.present()); + }); }) - .pipe( - first(), - concatMap(async ([takePicture, recordVideo]) => { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async resolve => { - const buttons = [ - { - text: takePicture, - handler: () => resolve(this.cameraService.takePhoto()), - }, - { - text: recordVideo, - handler: () => resolve(this.recordVideo()), - }, - ]; - - if (await this.goProBluetoothService.getConnectedDevice()) { - buttons.push({ - text: 'Capture from GoPro', - handler: () => resolve(this.caputureFromGoPro()), - }); - } - - return this.actionSheetController - .create({ buttons }) - .then(sheet => sheet.present()); - }); - }) - ); + ); } private async recordVideo() { diff --git a/src/app/features/settings/go-pro/go-pro-media-item-detail-on-camera/go-pro-media-item-detail-on-camera.component.html b/src/app/features/settings/go-pro/go-pro-media-item-detail-on-camera/go-pro-media-item-detail-on-camera.component.html index 4409d7245..307d5dd93 100644 --- a/src/app/features/settings/go-pro/go-pro-media-item-detail-on-camera/go-pro-media-item-detail-on-camera.component.html +++ b/src/app/features/settings/go-pro/go-pro-media-item-detail-on-camera/go-pro-media-item-detail-on-camera.component.html @@ -25,8 +25,12 @@ Your browser does not support the video tag. -
- +
+ Upload to capture directly from camera @@ -184,7 +188,7 @@ - + { + this.route.queryParams.subscribe(_ => { const state = this.router.getCurrentNavigation()?.extras.state; if (state) { this.mediaFile = state.goProMediaFile; @@ -46,31 +43,7 @@ export class GoProMediaItemDetailOnCameraComponent implements OnInit { } ngOnInit() { - this.mediaType = GoProMediaService.getFileType(this.mediaFile?.url); - } - - async downloadFileFromGoProCamera() { - if (!this.mediaFile) { - return; - } - - const fileName = GoProMediaService.extractFileNameFromUrl( - this.mediaFile.url - ); - - const loading = await this.loadingController.create({ - message: 'Please wait... Download in progress', - }); - await loading.present(); - - try { - await this.goProMediaService.downloadFromGoProCamera(this.mediaFile); - this.presentToast(`${fileName} downloaded ✅`); - } catch (error) { - this.presentToast(`Failed to download ${fileName} ❌`); - } - - await loading.dismiss(); + this.mediaType = getFileType(this.mediaFile?.url); } async uploadToCapture() { diff --git a/src/app/features/settings/go-pro/go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component.html b/src/app/features/settings/go-pro/go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component.html deleted file mode 100644 index ebab1e732..000000000 --- a/src/app/features/settings/go-pro/go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - Detail On Device - - - - - - - -
- - - Upload to capture - -
diff --git a/src/app/features/settings/go-pro/go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component.scss b/src/app/features/settings/go-pro/go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component.scss deleted file mode 100644 index bf53a3806..000000000 --- a/src/app/features/settings/go-pro/go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component.scss +++ /dev/null @@ -1,13 +0,0 @@ -:host { - display: block; - object-fit: contain; - animation: loading 2.5s ease-in-out alternate infinite; -} - -img, -video { - display: block; - width: 100%; - height: 50%; - background-color: black; -} diff --git a/src/app/features/settings/go-pro/go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component.spec.ts b/src/app/features/settings/go-pro/go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component.spec.ts deleted file mode 100644 index a4617a428..000000000 --- a/src/app/features/settings/go-pro/go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { SharedTestingModule } from '../../../../shared/shared-testing.module'; -import { GoProMediaItemDetailOnDeviceComponent } from './go-pro-media-item-detail-on-device.component'; - -describe('GoProMediaItemDetailOnDeviceComponent', () => { - let component: GoProMediaItemDetailOnDeviceComponent; - let fixture: ComponentFixture; - - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [GoProMediaItemDetailOnDeviceComponent], - imports: [SharedTestingModule], - }).compileComponents(); - - fixture = TestBed.createComponent(GoProMediaItemDetailOnDeviceComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }) - ); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/features/settings/go-pro/go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component.ts b/src/app/features/settings/go-pro/go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component.ts deleted file mode 100644 index 44f672256..000000000 --- a/src/app/features/settings/go-pro/go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Location } from '@angular/common'; -import { Component, Inject, OnInit } from '@angular/core'; -import { SafeUrl } from '@angular/platform-browser'; -import { ActivatedRoute, Router } from '@angular/router'; -import { NetworkPlugin } from '@capacitor/core'; -import { AlertController, ToastController } from '@ionic/angular'; -import { NETOWRK_PLUGIN } from '../../../../shared/capacitor-plugins/capacitor-plugins.module'; -import { GoProFileOnDevice } from '../go-pro-media-file'; -import { GoProMediaService } from '../services/go-pro-media.service'; -import { GoProWifiService } from '../services/go-pro-wifi.service'; - -@Component({ - selector: 'app-go-pro-media-item-detail-on-device', - templateUrl: './go-pro-media-item-detail-on-device.component.html', - styleUrls: ['./go-pro-media-item-detail-on-device.component.scss'], -}) -export class GoProMediaItemDetailOnDeviceComponent implements OnInit { - goProFileOnDevice?: GoProFileOnDevice; - - url?: string | SafeUrl; - - constructor( - private readonly router: Router, - private readonly location: Location, - private readonly route: ActivatedRoute, - public goProMediaService: GoProMediaService, - public goProWiFiService: GoProWifiService, - @Inject(NETOWRK_PLUGIN) - private readonly networkPlugin: NetworkPlugin, - public alertController: AlertController, - public toastController: ToastController - ) { - this.route.queryParams.subscribe(() => { - const state = this.router.getCurrentNavigation()?.extras.state; - if (state) { - this.goProFileOnDevice = state.goProFileOnDevice; - } - }); - } - - async ngOnInit() { - if (this.goProFileOnDevice) { - this.url = await this.goProMediaService.getFileSrcFromDevice( - this.goProFileOnDevice.url - ); - } - } - - goBack() { - this.location.back(); - } - - async uploadToCapture() { - const newtorkStatus = await this.networkPlugin.getStatus(); - - if (newtorkStatus.connectionType == 'wifi') { - const connectedToGoProWiFi = - await GoProWifiService.isConnectedToGoProWifi(); - - if (!connectedToGoProWiFi) { - this.startUploadToCapture(); - return; - } - } - - if (newtorkStatus.connectionType == 'cellular') { - const allowed = await this.allowUploadWithMobileInternet(); - if (allowed) { - this.startUploadToCapture(); - } - } - } - - private async startUploadToCapture() { - try { - await this.presentToast(`✅ Upload added to the queue, see Home Page`); - await this.goProMediaService.uploadToCaptureFromDevice( - this.goProFileOnDevice - ); - } catch (error) { - await this.presentToast(`❌ Failed to upload`); - } - } - - async presentToast(message: string) { - const toast = await this.toastController.create({ - message, - duration: 1700, - }); - toast.present(); - } - - async allowUploadWithMobileInternet() { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async resolve => { - const alert = await this.alertController.create({ - header: 'Warning!', - message: - 'You are using mobile data to upload file!' + - 'Do you want to continue?', - buttons: [ - { - text: 'Cancel', - role: 'cancel', - handler: () => { - resolve(false); - }, - }, - { - text: 'Okay', - handler: () => { - resolve(true); - }, - }, - ], - }); - await alert.present(); - }); - } -} diff --git a/src/app/features/settings/go-pro/go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component.html b/src/app/features/settings/go-pro/go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component.html index bb1d52a9c..3ebbb473d 100644 --- a/src/app/features/settings/go-pro/go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component.html +++ b/src/app/features/settings/go-pro/go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component.html @@ -6,10 +6,12 @@ broken_image + +
diff --git a/src/app/features/settings/go-pro/go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component.scss b/src/app/features/settings/go-pro/go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component.scss index 981f308c1..2c3502542 100644 --- a/src/app/features/settings/go-pro/go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component.scss +++ b/src/app/features/settings/go-pro/go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component.scss @@ -32,3 +32,13 @@ img { object-fit: cover; object-position: center; } + +.selected { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + border: 3px solid #564dfc; + border-radius: 4px; +} diff --git a/src/app/features/settings/go-pro/go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component.ts b/src/app/features/settings/go-pro/go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component.ts index 49a1ee18f..fb5fd7a70 100644 --- a/src/app/features/settings/go-pro/go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component.ts +++ b/src/app/features/settings/go-pro/go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component.ts @@ -11,6 +11,7 @@ import { GoProMediaService } from '../services/go-pro-media.service'; export class GoProMediaListItemOnCameraComponent { mediaType: 'unknown' | 'video' | 'image' = 'unknown'; @Input() mediaFile: GoProFile | undefined; + @Input() selected = false; constructor( private readonly router: Router, diff --git a/src/app/features/settings/go-pro/go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component.html b/src/app/features/settings/go-pro/go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component.html deleted file mode 100644 index dc11d8fb2..000000000 --- a/src/app/features/settings/go-pro/go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - broken_image - diff --git a/src/app/features/settings/go-pro/go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component.scss b/src/app/features/settings/go-pro/go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component.scss deleted file mode 100644 index 981f308c1..000000000 --- a/src/app/features/settings/go-pro/go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component.scss +++ /dev/null @@ -1,34 +0,0 @@ -:host { - background-color: lightgray; -} - -ion-icon { - color: white; - z-index: 10; - position: absolute; - opacity: 0.3; -} - -ion-icon.is-video { - top: 8px; - right: 8px; - font-size: 16px; -} - -.is-thumbnail-missing { - color: white; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 48px; - height: 48px; - font-size: 48px; -} - -img { - width: 100%; - height: 100%; - object-fit: cover; - object-position: center; -} diff --git a/src/app/features/settings/go-pro/go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component.spec.ts b/src/app/features/settings/go-pro/go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component.spec.ts deleted file mode 100644 index decc8f9c2..000000000 --- a/src/app/features/settings/go-pro/go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { SharedTestingModule } from '../../../../shared/shared-testing.module'; -import { GoProMediaListItemOnDeviceComponent } from './go-pro-media-list-item-on-device.component'; - -describe('GoProMediaListItemOnDeviceComponent', () => { - let component: GoProMediaListItemOnDeviceComponent; - let fixture: ComponentFixture; - - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [GoProMediaListItemOnDeviceComponent], - imports: [SharedTestingModule], - }).compileComponents(); - - fixture = TestBed.createComponent(GoProMediaListItemOnDeviceComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }) - ); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/features/settings/go-pro/go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component.ts b/src/app/features/settings/go-pro/go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component.ts deleted file mode 100644 index b3d52235c..000000000 --- a/src/app/features/settings/go-pro/go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { SafeUrl } from '@angular/platform-browser'; -import { Router } from '@angular/router'; -import { GoProFileOnDevice } from '../go-pro-media-file'; -import { GoProMediaService } from '../services/go-pro-media.service'; - -@Component({ - selector: 'app-go-pro-media-list-item-on-device', - templateUrl: './go-pro-media-list-item-on-device.component.html', - styleUrls: ['./go-pro-media-list-item-on-device.component.scss'], -}) -export class GoProMediaListItemOnDeviceComponent implements OnInit { - thumbnailSrc?: string | SafeUrl; - @Input() goProFileOnDevice!: GoProFileOnDevice | undefined; - - constructor( - private readonly router: Router, - private readonly goProMediaService: GoProMediaService - ) {} - - async ngOnInit() { - if (this.goProFileOnDevice !== undefined) { - this.thumbnailSrc = await this.goProMediaService.getFileSrcFromDevice( - this.goProFileOnDevice.thumbnailUrl - ); - } - } - - showDetails() { - this.router.navigate( - ['/settings', 'go-pro', 'media-item-detail-on-device'], - { - state: { goProFileOnDevice: this.goProFileOnDevice }, - } - ); - } -} diff --git a/src/app/features/settings/go-pro/go-pro-media-list-on-camera/go-pro-media-list-on-camera.component.html b/src/app/features/settings/go-pro/go-pro-media-list-on-camera/go-pro-media-list-on-camera.component.html index fdb5e8e02..5c4814321 100644 --- a/src/app/features/settings/go-pro/go-pro-media-list-on-camera/go-pro-media-list-on-camera.component.html +++ b/src/app/features/settings/go-pro/go-pro-media-list-on-camera/go-pro-media-list-on-camera.component.html @@ -1,35 +1,77 @@ - - GoPro Media on Camera + + + GoPro Media on Camera + + + + + + {{ selectedGoProFiles.length }} file(s) selected + + - +

Connected WiFi: {{ connectedWifiSSID }}

+ Connect to go pro WiFi (enable first) + + Upload to capture + + -

+

Make sure you connected to GoPro WiFi first

{{ fetchingFilesError }} +
+ Hint: click refresh button at top right.

Fetching files...

+ + + Stay on this screen with the app open to ensure your downloads complete! + +
+ + + +
Fetching files... class="go-pro-item" > + [selected]="isItemInSelectedList(mediaFile)" + > +
diff --git a/src/app/features/settings/go-pro/go-pro-media-list-on-camera/go-pro-media-list-on-camera.component.scss b/src/app/features/settings/go-pro/go-pro-media-list-on-camera/go-pro-media-list-on-camera.component.scss index fb813eda1..3adae70f7 100644 --- a/src/app/features/settings/go-pro/go-pro-media-list-on-camera/go-pro-media-list-on-camera.component.scss +++ b/src/app/features/settings/go-pro/go-pro-media-list-on-camera/go-pro-media-list-on-camera.component.scss @@ -11,3 +11,30 @@ app-go-pro-media-list-item-on-camera { overflow: hidden; border-radius: 4px; } + +.wifi-state-container { + padding: 0 16px 8px; + display: flex; + flex-direction: column; +} + +.connect-to-go-pro-wifi-btn { + color: white; + + --box-shadow: 0; +} + +.multi-select-mode-btn { + color: white; + align-self: flex-end; + + --box-shadow: 0; +} + +.upload-in-progress-info { + padding-top: 8px; +} + +.should-connect-to-wifi-text { + padding: 16px; +} diff --git a/src/app/features/settings/go-pro/go-pro-media-list-on-camera/go-pro-media-list-on-camera.component.ts b/src/app/features/settings/go-pro/go-pro-media-list-on-camera/go-pro-media-list-on-camera.component.ts index 59f6d641b..13eacd525 100644 --- a/src/app/features/settings/go-pro/go-pro-media-list-on-camera/go-pro-media-list-on-camera.component.ts +++ b/src/app/features/settings/go-pro/go-pro-media-list-on-camera/go-pro-media-list-on-camera.component.ts @@ -1,6 +1,11 @@ import { Location } from '@angular/common'; import { Component, OnInit } from '@angular/core'; -import { ToastController } from '@ionic/angular'; +import { Router } from '@angular/router'; +import { + AlertController, + NavController, + ToastController, +} from '@ionic/angular'; import { GoProFile } from '../go-pro-media-file'; import { GoProBluetoothService } from '../services/go-pro-bluetooth.service'; import { GoProMediaService } from '../services/go-pro-media.service'; @@ -21,9 +26,20 @@ export class GoProMediaListOnCameraComponent implements OnInit { connectedWifiSSID: string | null = null; isConnectedToGoProWifi: boolean | undefined; + isScrollingContent = false; + + multiSelectMode = false; + selectedGoProFiles: GoProFile[] = []; + + filesToUpload: GoProFile[] = []; + uploadInProgress = false; + constructor( private readonly location: Location, private readonly goProMediaService: GoProMediaService, + private readonly router: Router, + private readonly navCtrl: NavController, + private readonly alertCtrl: AlertController, private readonly goProBluetoothService: GoProBluetoothService, private readonly goProWifiService: GoProWifiService, public toastController: ToastController @@ -34,9 +50,9 @@ export class GoProMediaListOnCameraComponent implements OnInit { } async checkWiFiConnection() { - this.connectedWifiSSID = await GoProWifiService.getConnectedWifiSSID(); + this.connectedWifiSSID = await this.goProWifiService.getConnectedWifiSSID(); this.isConnectedToGoProWifi = - await GoProWifiService.isConnectedToGoProWifi(); + await this.goProWifiService.isConnectedToGoProWifi(); if (this.isConnectedToGoProWifi) { this.fetchFilesFromGoProWiFi(); @@ -76,4 +92,105 @@ export class GoProMediaListOnCameraComponent implements OnInit { goBack() { this.location.back(); } + + onItemClick(item: GoProFile) { + if (!this.multiSelectMode) { + this.router.navigate( + ['/settings', 'go-pro', 'media-item-detail-on-camera'], + { state: { goProMediaFile: item } } + ); + } else if (!this.isItemInSelectedList(item)) { + this.selectedGoProFiles.push(item); + } else { + this.selectedGoProFiles = this.selectedGoProFiles.filter( + i => i.url !== item.url + ); + } + } + + async uploadSelectedFiles() { + for (const selectedFile of this.selectedGoProFiles) { + if (this.isItemInUploadList(selectedFile) === false) { + this.filesToUpload.push(selectedFile); + } + } + + this.exitMultiSelectMode(); + + if (!this.uploadInProgress) { + this.uploadInProgress = true; + + while (this.filesToUpload.length > 0) { + const fileToUpload = this.filesToUpload.shift(); + + const uploadResult = + await this.goProMediaService.uploadToCaptureFromGoProCamera( + fileToUpload + ); + + if (uploadResult.isDownloaded && uploadResult.isCaptured === false) { + this.fileWasUploadedBefore(fileToUpload); + } + } + + this.uploadInProgress = false; + this.navigateToHomeScreen(); + } + } + + async fileWasUploadedBefore(fileToUpload: GoProFile | undefined) { + if (!fileToUpload) return; + + const alert = await this.alertCtrl.create({ + cssClass: 'go-pro-alert-message-with', + header: 'File Upload Error!', + subHeader: 'File Previously Uploaded.', + message: ``, + buttons: ['OK'], + }); + + await alert.present(); + + await alert.onDidDismiss(); + } + + private async navigateToHomeScreen() { + // this.navCtrl.navigateRoot('/'); + await this.navCtrl.pop(); + await this.navCtrl.pop(); + await this.navCtrl.pop(); + } + + ionScrollStart() { + this.isScrollingContent = true; + } + + ionScrollEnd() { + this.isScrollingContent = false; + } + + enterMultiSelectMode(firstSelectedItem?: GoProFile) { + if (this.multiSelectMode) return; + if (this.isScrollingContent) return; + if (firstSelectedItem) this.selectedGoProFiles.push(firstSelectedItem); + this.multiSelectMode = true; + } + + exitMultiSelectMode() { + this.multiSelectMode = false; + this.selectedGoProFiles = []; + } + + isItemInSelectedList(item: GoProFile) { + return this.selectedGoProFiles.find(i => i.url === item.url) !== undefined; + } + + isItemInUploadList(item: GoProFile) { + return this.filesToUpload.find(i => i.url === item.url) !== undefined; + } + + onUploadCancel() { + this.uploadInProgress = false; + this.filesToUpload = []; + } } diff --git a/src/app/features/settings/go-pro/go-pro-media-list-on-device/go-pro-media-list-on-device.component.html b/src/app/features/settings/go-pro/go-pro-media-list-on-device/go-pro-media-list-on-device.component.html deleted file mode 100644 index c9b5a1b2d..000000000 --- a/src/app/features/settings/go-pro/go-pro-media-list-on-device/go-pro-media-list-on-device.component.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - Media List On Device - - - -
- - - - - - -
-
diff --git a/src/app/features/settings/go-pro/go-pro-media-list-on-device/go-pro-media-list-on-device.component.scss b/src/app/features/settings/go-pro/go-pro-media-list-on-device/go-pro-media-list-on-device.component.scss deleted file mode 100644 index b50c1ce70..000000000 --- a/src/app/features/settings/go-pro/go-pro-media-list-on-device/go-pro-media-list-on-device.component.scss +++ /dev/null @@ -1,13 +0,0 @@ -.grid-container { - padding-left: 16px; - padding-right: 16px; - margin-top: 24px; - margin-bottom: 48px; -} - -app-go-pro-media-list-item-on-device { - width: 100%; - height: 100%; - overflow: hidden; - border-radius: 4px; -} diff --git a/src/app/features/settings/go-pro/go-pro-media-list-on-device/go-pro-media-list-on-device.component.spec.ts b/src/app/features/settings/go-pro/go-pro-media-list-on-device/go-pro-media-list-on-device.component.spec.ts deleted file mode 100644 index 291e42c7d..000000000 --- a/src/app/features/settings/go-pro/go-pro-media-list-on-device/go-pro-media-list-on-device.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { SharedTestingModule } from '../../../../shared/shared-testing.module'; -import { GoProMediaListOnDeviceComponent } from './go-pro-media-list-on-device.component'; - -describe('GoProMediaListOnDeviceComponent', () => { - let component: GoProMediaListOnDeviceComponent; - let fixture: ComponentFixture; - - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [GoProMediaListOnDeviceComponent], - imports: [SharedTestingModule], - }).compileComponents(); - - fixture = TestBed.createComponent(GoProMediaListOnDeviceComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }) - ); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/features/settings/go-pro/go-pro-media-list-on-device/go-pro-media-list-on-device.component.ts b/src/app/features/settings/go-pro/go-pro-media-list-on-device/go-pro-media-list-on-device.component.ts deleted file mode 100644 index 736e4c5ab..000000000 --- a/src/app/features/settings/go-pro/go-pro-media-list-on-device/go-pro-media-list-on-device.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { GoProFileOnDevice } from '../go-pro-media-file'; -import { GoProMediaService } from '../services/go-pro-media.service'; - -@Component({ - selector: 'app-go-pro-media-list-on-device', - templateUrl: './go-pro-media-list-on-device.component.html', - styleUrls: ['./go-pro-media-list-on-device.component.scss'], -}) -export class GoProMediaListOnDeviceComponent implements OnInit { - goProFilesOnDevice: GoProFileOnDevice[] = []; - - constructor(public goProMediaService: GoProMediaService) {} - - async ngOnInit() { - this.goProFilesOnDevice = - await this.goProMediaService.loadFilesFromStorage(); - } -} diff --git a/src/app/features/settings/go-pro/go-pro-media-loading-bar/go-pro-media-loading-bar.component.html b/src/app/features/settings/go-pro/go-pro-media-loading-bar/go-pro-media-loading-bar.component.html new file mode 100644 index 000000000..e81cbef0d --- /dev/null +++ b/src/app/features/settings/go-pro/go-pro-media-loading-bar/go-pro-media-loading-bar.component.html @@ -0,0 +1,9 @@ + + {{ loadingText }} + + + + + + cancel + diff --git a/src/app/features/settings/go-pro/go-pro-media-loading-bar/go-pro-media-loading-bar.component.scss b/src/app/features/settings/go-pro/go-pro-media-loading-bar/go-pro-media-loading-bar.component.scss new file mode 100644 index 000000000..f3f87e5ce --- /dev/null +++ b/src/app/features/settings/go-pro/go-pro-media-loading-bar/go-pro-media-loading-bar.component.scss @@ -0,0 +1,30 @@ +.thin-upload-bar { + min-height: 32px; + height: 32px; + justify-content: stretch; + transition: all 1s; + + .text { + flex-grow: 1; + font-size: 0.7em; + margin-left: 10px; + margin-right: 10px; + } + + .spacer-text { + flex-grow: 0; + } + + mat-icon { + margin-left: 10px; + margin-right: 10px; + } +} + +#thin-upload-bar { + height: 32px; +} + +.spacer { + flex: 1 1 auto; +} diff --git a/src/app/features/settings/go-pro/go-pro-media-loading-bar/go-pro-media-loading-bar.component.spec.ts b/src/app/features/settings/go-pro/go-pro-media-loading-bar/go-pro-media-loading-bar.component.spec.ts new file mode 100644 index 000000000..c40393dac --- /dev/null +++ b/src/app/features/settings/go-pro/go-pro-media-loading-bar/go-pro-media-loading-bar.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; + +import { GoProMediaLoadingBarComponent } from './go-pro-media-loading-bar.component'; + +describe('GoProMediaLoadingBarComponent', () => { + let component: GoProMediaLoadingBarComponent; + let fixture: ComponentFixture; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [GoProMediaLoadingBarComponent], + imports: [IonicModule.forRoot()], + }).compileComponents(); + + fixture = TestBed.createComponent(GoProMediaLoadingBarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }) + ); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/go-pro/go-pro-media-loading-bar/go-pro-media-loading-bar.component.ts b/src/app/features/settings/go-pro/go-pro-media-loading-bar/go-pro-media-loading-bar.component.ts new file mode 100644 index 000000000..a975cefad --- /dev/null +++ b/src/app/features/settings/go-pro/go-pro-media-loading-bar/go-pro-media-loading-bar.component.ts @@ -0,0 +1,15 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-go-pro-media-loading-bar', + templateUrl: './go-pro-media-loading-bar.component.html', + styleUrls: ['./go-pro-media-loading-bar.component.scss'], +}) +export class GoProMediaLoadingBarComponent { + @Input() loadingText = ''; + @Output() uploadCancel = new EventEmitter(); + + cancel() { + this.uploadCancel.emit(); + } +} diff --git a/src/app/features/settings/go-pro/go-pro-routing.module.ts b/src/app/features/settings/go-pro/go-pro-routing.module.ts index 7eaa8d2bf..04bc09385 100644 --- a/src/app/features/settings/go-pro/go-pro-routing.module.ts +++ b/src/app/features/settings/go-pro/go-pro-routing.module.ts @@ -1,9 +1,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { GoProMediaItemDetailOnCameraComponent } from './go-pro-media-item-detail-on-camera/go-pro-media-item-detail-on-camera.component'; -import { GoProMediaItemDetailOnDeviceComponent } from './go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component'; import { GoProMediaListOnCameraComponent } from './go-pro-media-list-on-camera/go-pro-media-list-on-camera.component'; -import { GoProMediaListOnDeviceComponent } from './go-pro-media-list-on-device/go-pro-media-list-on-device.component'; import { GoProPage } from './go-pro.page'; const routes: Routes = [ @@ -19,15 +17,6 @@ const routes: Routes = [ path: 'media-item-detail-on-camera', component: GoProMediaItemDetailOnCameraComponent, }, - - { - path: 'media-list-on-device', - component: GoProMediaListOnDeviceComponent, - }, - { - path: 'media-item-detail-on-device', - component: GoProMediaItemDetailOnDeviceComponent, - }, ]; @NgModule({ diff --git a/src/app/features/settings/go-pro/go-pro.module.ts b/src/app/features/settings/go-pro/go-pro.module.ts index 1c55fa79f..8b8e739e6 100644 --- a/src/app/features/settings/go-pro/go-pro.module.ts +++ b/src/app/features/settings/go-pro/go-pro.module.ts @@ -1,26 +1,21 @@ import { NgModule } from '@angular/core'; +import { NgxLongPress2Module } from 'ngx-long-press2'; import { SharedModule } from '../../../shared/shared.module'; import { GoProMediaItemDetailOnCameraComponent } from './go-pro-media-item-detail-on-camera/go-pro-media-item-detail-on-camera.component'; -import { GoProMediaItemDetailOnDeviceComponent } from './go-pro-media-item-detail-on-device/go-pro-media-item-detail-on-device.component'; import { GoProMediaListItemOnCameraComponent } from './go-pro-media-list-item-on-camera/go-pro-media-list-item-on-camera.component'; -import { GoProMediaListItemOnDeviceComponent } from './go-pro-media-list-item-on-device/go-pro-media-list-item-on-device.component'; import { GoProMediaListOnCameraComponent } from './go-pro-media-list-on-camera/go-pro-media-list-on-camera.component'; -import { GoProMediaListOnDeviceComponent } from './go-pro-media-list-on-device/go-pro-media-list-on-device.component'; +import { GoProMediaLoadingBarComponent } from './go-pro-media-loading-bar/go-pro-media-loading-bar.component'; import { GoProPageRoutingModule } from './go-pro-routing.module'; import { GoProPage } from './go-pro.page'; @NgModule({ - imports: [SharedModule, GoProPageRoutingModule], + imports: [SharedModule, GoProPageRoutingModule, NgxLongPress2Module], declarations: [ GoProPage, - GoProMediaListOnCameraComponent, GoProMediaListItemOnCameraComponent, GoProMediaItemDetailOnCameraComponent, - - GoProMediaListOnDeviceComponent, - GoProMediaListItemOnDeviceComponent, - GoProMediaItemDetailOnDeviceComponent, + GoProMediaLoadingBarComponent, ], }) export class GoProPageModule {} diff --git a/src/app/features/settings/go-pro/go-pro.page.html b/src/app/features/settings/go-pro/go-pro.page.html index 2368cf754..f31910d0c 100644 --- a/src/app/features/settings/go-pro/go-pro.page.html +++ b/src/app/features/settings/go-pro/go-pro.page.html @@ -11,15 +11,16 @@ expand="block" [disabled]="bluetoothIsScanning" (click)="scanForBluetoothDevices()" + class="scan-for-bluetooth-devices-btn" > - {{ bluetoothIsScanning ? 'Scaning' : 'Scan for bluetooth devices' }} + {{ bluetoothIsScanning ? 'Scanning' : 'Scan for bluetooth devices' }}

{{ scanResult.device.name }}

-

{{ scanResult.device.deviceId }}

+

{{ scanResult.device.deviceId }}

{{ scanResult.device.name }} " (click)="connectToBluetoothDevice(scanResult)" slot="end" + class="connect-to-bluetooth-device-btn" > Connect
-
+
- -

NOT CONNECTED TO ANY GO PRO CAMERA ⚠️

-
- - +

CONNECTED TO: @@ -62,12 +63,11 @@

- + Show files on go pro - send
-
+
diff --git a/src/app/features/settings/go-pro/go-pro.page.scss b/src/app/features/settings/go-pro/go-pro.page.scss index e69de29bb..704fc4680 100644 --- a/src/app/features/settings/go-pro/go-pro.page.scss +++ b/src/app/features/settings/go-pro/go-pro.page.scss @@ -0,0 +1,28 @@ +.connected-bluetooth-device-container { + margin-top: 40px; +} + +.scan-for-bluetooth-devices-btn { + color: white; + margin: 0 16px; + + --box-shadow: 0; +} + +.connect-to-bluetooth-device-btn { + color: white; + + --box-shadow: 0; +} + +.device-id-text { + font-size: 8px; +} + +.spacer { + height: 24px; +} + +.spacer-bottom { + height: 300px; +} diff --git a/src/app/features/settings/go-pro/go-pro.page.ts b/src/app/features/settings/go-pro/go-pro.page.ts index 535e8a821..7706e8dd4 100644 --- a/src/app/features/settings/go-pro/go-pro.page.ts +++ b/src/app/features/settings/go-pro/go-pro.page.ts @@ -3,9 +3,8 @@ import { Router } from '@angular/router'; import { ScanResult } from '@capacitor-community/bluetooth-le'; import { WifiPlugin } from '@capacitor-community/wifi'; import { Plugins } from '@capacitor/core'; -import { ToastController } from '@ionic/angular'; +import { LoadingController, ToastController } from '@ionic/angular'; import { GoProBluetoothService } from './services/go-pro-bluetooth.service'; -import { GoProMediaService } from './services/go-pro-media.service'; const Wifi: WifiPlugin = Plugins.Wifi as WifiPlugin; @@ -30,30 +29,13 @@ export class GoProPage implements OnInit { constructor( public toastController: ToastController, - private readonly goProMediaService: GoProMediaService, + private readonly loadingController: LoadingController, private readonly router: Router, private readonly goProBluetoothService: GoProBluetoothService ) {} ngOnInit() { this.restoreBluetoothConnection(); - - // this.router.navigate( - // ['/settings', 'go-pro', 'media-item-detail-on-camera'], - // { - // state: { - // goProMediaFile: { - // name: 'string;', - // url: 'https://images.pexels.com/photos/9683060/pexels-photo-9683060.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260', - // thumbnailUrl: - // 'https://images.pexels.com/photos/9683060/pexels-photo-9683060.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260', - // type: 'image', - // size: 1024, - // storageKey: 'string; // TODO: remove this field', - // }, - // }, - // } - // ); } async restoreBluetoothConnection() { @@ -75,16 +57,26 @@ export class GoProPage implements OnInit { this.bluetoothIsScanning = false; } catch (error) { this.bluetoothScanResults = []; + } finally { this.bluetoothIsScanning = false; } } async connectToBluetoothDevice(scanResult: ScanResult) { + const loading = await this.loadingController.create({ + message: `Connecting to ${scanResult.device.name}`, + }); + try { + await loading.present(); await this.goProBluetoothService.connectToBluetoothDevice(scanResult); + await this.goProBluetoothService.pairDevice(); + this.bluetoothConnectedDevice = scanResult; + await loading.dismiss(); this.presentToast(`🅱 Connected to ${scanResult.device.name}`); } catch (error) { + await loading.dismiss(); this.presentToast(JSON.stringify(error)); } } diff --git a/src/app/features/settings/go-pro/services/go-pro-bluetooth.service.spec.ts b/src/app/features/settings/go-pro/services/go-pro-bluetooth.service.spec.ts index f5a9ff2e5..987106fd1 100644 --- a/src/app/features/settings/go-pro/services/go-pro-bluetooth.service.spec.ts +++ b/src/app/features/settings/go-pro/services/go-pro-bluetooth.service.spec.ts @@ -1,12 +1,14 @@ import { TestBed } from '@angular/core/testing'; - +import { SharedTestingModule } from '../../../../shared/shared-testing.module'; import { GoProBluetoothService } from './go-pro-bluetooth.service'; describe('GoProBluetoothService', () => { let service: GoProBluetoothService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + imports: [SharedTestingModule], + }); service = TestBed.inject(GoProBluetoothService); }); diff --git a/src/app/features/settings/go-pro/services/go-pro-bluetooth.service.ts b/src/app/features/settings/go-pro/services/go-pro-bluetooth.service.ts index 77f5d4d2f..8294fe2bc 100644 --- a/src/app/features/settings/go-pro/services/go-pro-bluetooth.service.ts +++ b/src/app/features/settings/go-pro/services/go-pro-bluetooth.service.ts @@ -6,10 +6,10 @@ import { ScanResult, } from '@capacitor-community/bluetooth-le'; import { Wifi } from '@capacitor-community/wifi'; -import { Plugins } from '@capacitor/core'; import { isPlatform } from '@ionic/core'; - -const { Storage } = Plugins; +import { isEqual } from 'lodash-es'; +import { BehaviorSubject } from 'rxjs'; +import { PreferenceManager } from '../../../../shared/preference-manager/preference-manager.service'; interface GoProWiFiCreds { wifiPASS: string; @@ -55,15 +55,35 @@ export class GoProBluetoothService { // eslint-disable-next-line @typescript-eslint/no-magic-numbers private readonly enableGoProWiFiCommand = [0x03, 0x17, 0x01, 0x01]; - constructor() { - BleClient.initialize().catch(err => { + private hasInitialized = false; + + readonly id = 'GoProBluetoothService'; + + private readonly preferences = this.preferenceManager.getPreferences(this.id); + + readonly connectedDevice$ = new BehaviorSubject( + undefined + ); + + constructor(private readonly preferenceManager: PreferenceManager) {} + + private async initialize() { + if (this.hasInitialized) { + return; + } + + try { + await BleClient.initialize(); + this.hasInitialized = true; + } catch (err: any) { if ( err instanceof Error && err.message === 'Web Bluetooth API not available in this browser.' - ) + ) { return; + } throw new Error(err.message); - }); + } } async scanForBluetoothDevices(): Promise { @@ -74,32 +94,30 @@ export class GoProBluetoothService { const bluetoothScanResults: ScanResult[] = []; - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { - try { - await BleClient.initialize(); - - await BleClient.requestLEScan( - { services: [this.goProControlAndQueryServiceUUID] }, - foundDevice => bluetoothScanResults.push(foundDevice) - ); - - const stopScanAfterMilliSeconds = 2000; - setTimeout(async () => { - await BleClient.stopLEScan(); - resolve(bluetoothScanResults); - }, stopScanAfterMilliSeconds); - } catch (error) { - reject(error); - } + await this.initialize(); + + BleClient.requestLEScan( + { services: [this.goProControlAndQueryServiceUUID] }, + (foundDevice: any) => bluetoothScanResults.push(foundDevice) + ); + + await new Promise(resolve => { + const stopScanAfterMilliSeconds = 2000; + setTimeout(resolve, stopScanAfterMilliSeconds); }); + + await BleClient.stopLEScan(); + + return bluetoothScanResults; } async connectToBluetoothDevice(scanResult: ScanResult) { - await BleClient.connect(scanResult.device.deviceId, () => { + await this.initialize(); + await BleClient.disconnect(scanResult.device.deviceId); + await BleClient.connect(scanResult.device.deviceId, _ => { this.onDisconnectedFromBluetoothDevice(scanResult); }); - this.saveConnectedDeviceToStorage(scanResult); + await this.saveConnectedDeviceToStorage(scanResult); } onDisconnectedFromBluetoothDevice(scanResult: ScanResult) { @@ -111,24 +129,32 @@ export class GoProBluetoothService { this.removeConnectedDeviceFromStorage(scanResult); } - async getConnectedDeviceFromStorage(): Promise { - const result = await Storage.get({ - key: this.GO_PRO_BLUETOOTH_STORAGE_KEY, - }); - if (result.value) { - return JSON.parse(result.value) as ScanResult; + private async getConnectedDeviceFromStorage(): Promise< + ScanResult | undefined + > { + const res = await this.preferences.getString( + PrefKeys.LAST_CONNECTED_BLUETOOTH_DEVICE + ); + if (res !== '') { + return JSON.parse(res) as ScanResult; } } async saveConnectedDeviceToStorage(scanResult: ScanResult) { - await Storage.set({ - key: this.GO_PRO_BLUETOOTH_STORAGE_KEY, - value: JSON.stringify(scanResult), - }); + await this.preferences.setString( + PrefKeys.LAST_CONNECTED_BLUETOOTH_DEVICE, + JSON.stringify(scanResult) + ); + this.connectedDevice$.next(scanResult); } - async removeConnectedDeviceFromStorage(_: ScanResult) { - await Storage.remove({ key: this.GO_PRO_BLUETOOTH_STORAGE_KEY }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async removeConnectedDeviceFromStorage(scanResult: ScanResult) { + await this.preferences.setString( + PrefKeys.LAST_CONNECTED_BLUETOOTH_DEVICE, + '' + ); + this.connectedDevice$.next(undefined); } async getConnectedDevice(): Promise { @@ -157,10 +183,14 @@ export class GoProBluetoothService { } async sendBluetoothWriteCommand(command: number[]) { + await this.initialize(); await this.checkBluetoothDeviceConnection(); const connectedDevice = await this.getConnectedDeviceFromStorage(); + + if (!connectedDevice) return; + await BleClient.write( - connectedDevice!.device.deviceId, + connectedDevice.device.deviceId, this.goProControlAndQueryServiceUUID, this.goProCommandReqCharacteristicsUUID, numbersToDataView(command) @@ -168,10 +198,10 @@ export class GoProBluetoothService { } async sendBluetoothReadCommand(command: number[]) { + await this.initialize(); await this.checkBluetoothDeviceConnection(); - // TODO: find better solution for comparing 2 arrays with numbers - if (JSON.stringify(command) === JSON.stringify(this.shutdownCommand)) { + if (isEqual(command, this.shutdownCommand)) { this.getGoProWiFiCreds(); } @@ -206,22 +236,27 @@ export class GoProBluetoothService { } async connectToGoProWiFi() { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { - await this.sendBluetoothWriteCommand(this.enableGoProWiFiCommand); - - const wifiCreds = await this.getGoProWiFiCreds(); - - try { - const result = await Wifi.connect({ - ssid: wifiCreds.wifiSSID, - password: wifiCreds.wifiPASS, - }); + await this.sendBluetoothWriteCommand(this.enableGoProWiFiCommand); - resolve(result); - } catch (error) { - reject(error); - } + const wifiCreds = await this.getGoProWiFiCreds(); + await Wifi.connect({ + ssid: wifiCreds.wifiSSID, + password: wifiCreds.wifiPASS, }); } + + /** Trigger pairing between device and GoPro. + * + * Because '@capacitor-community/bluetooth-le' have no such command pair device. However we can + * send any bluetooth read command to trigger pairing if connected for the first time. + * For example: it can be get wifi credentials command + */ + async pairDevice(): Promise { + await this.initialize(); + await this.getGoProWiFiCreds(); + } +} + +const enum PrefKeys { + LAST_CONNECTED_BLUETOOTH_DEVICE = 'GO_PRO_LAST_CONNECTED_BLUETOOTH_DEVICE', } diff --git a/src/app/features/settings/go-pro/services/go-pro-media.service.ts b/src/app/features/settings/go-pro/services/go-pro-media.service.ts index 76048b21f..72226eb99 100644 --- a/src/app/features/settings/go-pro/services/go-pro-media.service.ts +++ b/src/app/features/settings/go-pro/services/go-pro-media.service.ts @@ -1,8 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; -import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { DomSanitizer } from '@angular/platform-browser'; import '@capacitor-community/http'; -import { HttpDownloadFileResult } from '@capacitor-community/http'; import { Capacitor, FilesystemDirectory, @@ -10,11 +9,16 @@ import { Plugins, } from '@capacitor/core'; import { isPlatform } from '@ionic/core'; +import { + detectFileTypeFromUrl, + extractFileNameFromGoProUrl, + urlIsImage, +} from '../../../../../utils/url'; import { FILESYSTEM_PLUGIN } from '../../../../shared/capacitor-plugins/capacitor-plugins.module'; import { CaptureService } from '../../../../shared/capture/capture.service'; import { blobToBase64 } from '../../../../utils/encoding/encoding'; -import { GoProFile, GoProFileOnDevice } from '../go-pro-media-file'; -const { Http, Storage } = Plugins; +import { GoProFile } from '../go-pro-media-file'; +const { Http } = Plugins; @Injectable({ providedIn: 'root', @@ -38,201 +42,70 @@ export class GoProMediaService { private readonly httpClient: HttpClient ) {} - static getFileType(url?: string): 'unknown' | 'video' | 'image' { - if (url === undefined) { - return 'unknown'; - } - if (url.toLowerCase().includes('.mp4')) { - return 'video'; - } - if ( - url.toLowerCase().includes('.jpg') || - url.toLowerCase().includes('.jpeg') - ) { - return 'image'; - } - return 'unknown'; - } - - static extractFileNameFromUrl(url: string): string { - // return extract filename with extension from url - // example 001.jpg, 002.mp4 - return url.split('?')[0].split('/').pop()!; - } - - static extractFileExtensionFormUrl(filaName: string): string { - return filaName.split('?')[0].split('.').pop()!; - } - - static extractFileNameFromGoProUrl(url: string): string { - // example of GoPro urls - // _________url: http://10.5.5.9:8080/videos/DCIM/100GOPRO/GH010168.MP4 - // thumbnailUrl: http://10.5.5.9:8080/gopro/media/thumbnail?path=100GOPRO/GH010168.MP4 - return url.split('/').pop()!; - } - - static extractFileExtensionFromGoProUrl(url: string): string { - return url.split('.').pop()!; - } - - static urlIsImage(url: string): boolean { - const url_lowercase = url.toLocaleLowerCase(); - return url_lowercase.includes('.jpeg') || url_lowercase.includes('.jpg'); - } - - static urlIsVideo(url: string): boolean { - const url_lowercase = url.toLowerCase(); - return url_lowercase.includes('.mp4'); - } - - static detectFileTypeFromUrl(url: string): 'image' | 'video' | 'unknown' { - if (GoProMediaService.urlIsImage(url)) { - return 'image'; - } - if (GoProMediaService.urlIsVideo(url)) { - return 'video'; - } - return 'unknown'; - } - - private async saveFilesToStorage(files: GoProFileOnDevice[]) { - await Storage.set({ - key: this.GO_PRO_FILES_ON_DEVICE_STORAGE_KEY, - value: JSON.stringify(files), - }); - } - - async clearStorage() { - await this.saveFilesToStorage([]); - } - getThumbnailUrlFrom(url: string): string { const fileName = url.split('/').pop(); const thumbnailUrl = `${this.goproBaseUrl}/gopro/media/thumbnail?path=100GOPRO/${fileName}`; return thumbnailUrl; } - async uploadToCaptureFromGoProCamera(mediaFile: GoProFile | undefined) { - if (!mediaFile) return; - - const fileName = GoProMediaService.extractFileNameFromGoProUrl( - mediaFile.url - ); - - // const option = 'oldWay'; - // if (option === 'oldWay') { - await Http.downloadFile({ - url: mediaFile.url!, - filePath: fileName, - fileDirectory: this.directory, - method: 'GET', - }); - - const readResult = await this.filesystemPlugin.getUri({ - directory: this.directory, - // path: `${this.rootDir}/${goProFileOnDevice.name}`, - path: `${fileName}`, // Because when saving we forget to add rootDir - }); - - const url = Capacitor.convertFileSrc(readResult.uri); - - const blob = await this.httpClient - .get(url, { responseType: 'blob' }) - .toPromise(); - - const base64 = await blobToBase64(blob); - - const mimeType = GoProMediaService.urlIsImage(mediaFile.url) - ? 'image/jpeg' - : 'video/mp4'; - - await this.captureService.capture({ base64, mimeType }); - - // delete temp downloaded file - await this.filesystemPlugin.deleteFile({ - directory: this.directory, - // path: `${this.rootDir}/${goProFileOnDevice.name}`, - path: `${fileName}`, // Because when saving we forget to add rootDir - }); - // } else { - // const url = mediaFile.url; - - // const blob = await Http.request({ - // method: 'GET', - // url: url, - // headers: { responseType: 'blob' }, - // }); - - // const base64 = await blobToBase64(blob); - - // const mimeType = this.urlIsImage(url) ? 'image/jpeg' : 'video/mp4'; - - // await this.captureService.capture({ base64, mimeType }); - // } - } - - async getFileSrcFromDevice(filePath: string): Promise { - const fileName = filePath.split('/').pop(); + async uploadToCaptureFromGoProCamera( + mediaFile: GoProFile | undefined + ): Promise<{ + isDownloaded: boolean; + isCaptured: boolean; + }> { + if (!mediaFile) return { isDownloaded: false, isCaptured: false }; - const result = await this.filesystemPlugin.getUri({ - directory: this.directory, - // path: `${this.rootDir}/${fileName}`, - path: `${fileName}`, // Because when saving we forget to add rootDir - }); + const fileName = extractFileNameFromGoProUrl(mediaFile.url); - const uri = result.uri; + let isDownloaded = false; + let isCaptured = false; - const url = Capacitor.convertFileSrc(uri); + try { + await Http.downloadFile({ + url: mediaFile.url, + filePath: fileName, + fileDirectory: this.directory, + method: 'GET', + }); - return this.sanitizer.bypassSecurityTrustUrl(url); - } - - async uploadToCaptureFromDevice(goProFileOnDevice?: GoProFileOnDevice) { - if (!goProFileOnDevice) return; - - // const option = 'oldWay'; - // if (option !== 'oldWay') { - // const readResult = await this.filesystemPlugin.readFile({ - // directory: this.directory, - // // path: `${this.rootDir}/${goProFileOnDevice.name}`, - // path: `${goProFileOnDevice.name}`, // Because when saving we forget to add rootDir - // }); + const readResult = await this.filesystemPlugin.getUri({ + directory: this.directory, + path: fileName, + }); - // const base64 = readResult.data; + const url = Capacitor.convertFileSrc(readResult.uri); - // const mimeType = this.urlIsImage(goProFileOnDevice.url) - // ? 'image/jpeg' - // : 'video/mp4'; - - // await this.captureService.capture({ base64, mimeType }); - // } else { - const result = await this.filesystemPlugin.getUri({ - directory: this.directory, - // path: `${this.rootDir}/${goProFileOnDevice.name}`, - path: `${goProFileOnDevice.name}`, // Because when saving we forget to add rootDir - }); + const blob = await this.httpClient + .get(url, { responseType: 'blob' }) + .toPromise(); - const url = Capacitor.convertFileSrc(result.uri); + const base64 = await blobToBase64(blob); - const blob = await this.httpClient - .get(url, { responseType: 'blob' }) - .toPromise(); + const mimeType = urlIsImage(mediaFile.url) ? 'image/jpeg' : 'video/mp4'; + isDownloaded = true; - const base64 = await blobToBase64(blob); + await this.captureService.capture({ base64, mimeType }); + isCaptured = true; - const mimeType = GoProMediaService.urlIsImage(goProFileOnDevice.url) - ? 'image/jpeg' - : 'video/mp4'; + // delete temp downloaded file + await this.filesystemPlugin.deleteFile({ + directory: this.directory, + path: fileName, + }); + } catch (error: any) { + const printIndentation = 2; + // eslint-disable-next-line no-console + console.warn(`'😭 ${JSON.stringify(error, null, printIndentation)}`); + } - await this.captureService.capture({ base64, mimeType }); - // } + return { isDownloaded, isCaptured }; } async getFilesFromGoPro(): Promise { const url = this.goproBaseUrl + '/gopro/media/list'; const params = {}; const headers = {}; - const response = await Http.request({ method: 'GET', url, @@ -241,9 +114,7 @@ export class GoProMediaService { }); const data = response.data; - const files = (data.media[0].fs as any[]).reverse(); - const fileNames: string[] = files.map(e => e.n); return fileNames @@ -256,64 +127,8 @@ export class GoProMediaService { url, storageKey: undefined, thumbnailUrl: this.getThumbnailUrlFrom(url), - name: GoProMediaService.extractFileNameFromGoProUrl(url), - type: GoProMediaService.detectFileTypeFromUrl(url), + name: extractFileNameFromGoProUrl(url), + type: detectFileTypeFromUrl(url), }; } - - async loadFilesFromStorage() { - const result = await Storage.get({ - key: this.GO_PRO_FILES_ON_DEVICE_STORAGE_KEY, - }); - const filesOnDevice: GoProFileOnDevice[] = JSON.parse(result.value ?? '[]'); - return filesOnDevice; - } - - async addFileToStorage(fileToAdd: GoProFileOnDevice) { - const filesOnDevice = await this.loadFilesFromStorage(); - filesOnDevice.unshift(fileToAdd); - await this.saveFilesToStorage(filesOnDevice); - } - - async downloadFromGoProCamera(mediaFile?: GoProFile) { - if (!mediaFile) { - return; - } - const fileName = GoProMediaService.extractFileNameFromGoProUrl( - mediaFile.url - ); - // const fileExtension = GoProMediaService.extractFileExtensionFromGoProUrl( - // mediaFile.url - // ); - const fileType = GoProMediaService.detectFileTypeFromUrl(mediaFile.url); - - const thumbName = GoProMediaService.extractFileNameFromGoProUrl( - mediaFile.thumbnailUrl! - ); - const thumbNameFull = 'thumbnail_' + thumbName + '.jpeg'; - - const fileResponse: HttpDownloadFileResult = await Http.downloadFile({ - url: mediaFile.url!, - filePath: fileName, - fileDirectory: this.directory, - method: 'GET', - }); - - const thumbResponse: HttpDownloadFileResult = await Http.downloadFile({ - url: mediaFile.thumbnailUrl!, - filePath: thumbNameFull, - fileDirectory: this.directory, - method: 'GET', - }); - - const goProFileOnDevice: GoProFileOnDevice = { - name: fileName, - url: fileResponse.path!, - thumbnailUrl: thumbResponse.path!, - size: 1, // TODO: find out size of file - type: fileType, - }; - - await this.addFileToStorage(goProFileOnDevice); - } } diff --git a/src/app/features/settings/go-pro/services/go-pro-wifi.service.spec.ts b/src/app/features/settings/go-pro/services/go-pro-wifi.service.spec.ts index 48e474fe9..3ecb15698 100644 --- a/src/app/features/settings/go-pro/services/go-pro-wifi.service.spec.ts +++ b/src/app/features/settings/go-pro/services/go-pro-wifi.service.spec.ts @@ -1,12 +1,14 @@ import { TestBed } from '@angular/core/testing'; - +import { SharedTestingModule } from '../../../../shared/shared-testing.module'; import { GoProWifiService } from './go-pro-wifi.service'; describe('GoProWifiService', () => { let service: GoProWifiService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + imports: [SharedTestingModule], + }); service = TestBed.inject(GoProWifiService); }); diff --git a/src/app/features/settings/go-pro/services/go-pro-wifi.service.ts b/src/app/features/settings/go-pro/services/go-pro-wifi.service.ts index d3f094133..1c240d334 100644 --- a/src/app/features/settings/go-pro/services/go-pro-wifi.service.ts +++ b/src/app/features/settings/go-pro/services/go-pro-wifi.service.ts @@ -2,29 +2,32 @@ import { Injectable } from '@angular/core'; import { WifiPlugin } from '@capacitor-community/wifi'; import { Plugins } from '@capacitor/core'; import { Platform } from '@ionic/angular'; +import { PreferenceManager } from '../../../../shared/preference-manager/preference-manager.service'; import { GoProBluetoothService } from './go-pro-bluetooth.service'; const Wifi: WifiPlugin = Plugins.Wifi as WifiPlugin; -const { Storage } = Plugins; - @Injectable({ providedIn: 'root', }) export class GoProWifiService { - private readonly GO_PRO_TUTORIAL_MOBILE_DATA_ONLY_APPS_STORAGE_KEY = - 'GO_PRO_TUTORIAL_MOBILE_DATA_ONLY_APPS_STORAGE_KEY'; + readonly id = 'GoProWifiService'; + + private readonly preferences = this.preferenceManager.getPreferences(this.id); constructor( + private readonly preferenceManager: PreferenceManager, private readonly goProBluetoothService: GoProBluetoothService, public platform: Platform ) {} - static async isConnectedToGoProWifi() { + // eslint-disable-next-line class-methods-use-this + async isConnectedToGoProWifi(): Promise { const result = await Wifi.getSSID(); return result.ssid?.startsWith('GP') ?? false; } - static async getConnectedWifiSSID() { + // eslint-disable-next-line class-methods-use-this + async getConnectedWifiSSID() { const result = await Wifi.getSSID(); return result.ssid; } @@ -37,27 +40,28 @@ export class GoProWifiService { password: creds.wifiPASS, }); - return result.ssid!; + return result.ssid ?? ''; } async showTutorialForMobileDataOnlyApps() { if (this.platform.is('android') === false) return false; - const result = await Storage.get({ - key: this.GO_PRO_TUTORIAL_MOBILE_DATA_ONLY_APPS_STORAGE_KEY, - }); - - if (result.value) { - return JSON.parse(result.value) as boolean; - } + const result = await this.preferences.getBoolean( + PrefKeys.SHOW_MOBILE_DATA_TUTORIAL_ON_IOS, + true + ); - return false; + return result; } async dontShowAgainTutorialForMobileDataOnlyApps() { - await Storage.set({ - key: this.GO_PRO_TUTORIAL_MOBILE_DATA_ONLY_APPS_STORAGE_KEY, - value: JSON.stringify(true), - }); + await this.preferences.setBoolean( + PrefKeys.SHOW_MOBILE_DATA_TUTORIAL_ON_IOS, + false + ); } } + +const enum PrefKeys { + SHOW_MOBILE_DATA_TUTORIAL_ON_IOS = 'GO_PRO_SHOW_MOBILE_DATA_TUTORIAL_ON_IOS', +} diff --git a/src/global.scss b/src/global.scss index fbfacc324..5c3127c22 100644 --- a/src/global.scss +++ b/src/global.scss @@ -127,3 +127,12 @@ body.dark .mat-card { body.dark .mat-tab-group { background-color: var(--ion-background-color); } + +.go-pro-alert-message-with img { + width: 100%; + height: 150px; + border-radius: 4px; + overflow: hidden; + object-fit: cover; + align-self: center; +} diff --git a/src/utils/url.ts b/src/utils/url.ts new file mode 100644 index 000000000..861b3f36d --- /dev/null +++ b/src/utils/url.ts @@ -0,0 +1,49 @@ +export function getFileType(url?: string): 'unknown' | 'video' | 'image' { + if (url === undefined) { + return 'unknown'; + } + if (url.toLowerCase().includes('.mp4')) { + return 'video'; + } + if ( + url.toLowerCase().includes('.jpg') || + url.toLowerCase().includes('.jpeg') + ) { + return 'image'; + } + return 'unknown'; +} + +/** + * @param url - Exmaple urls from GoPro + * * http://10.5.5.9:8080/videos/DCIM/100GOPRO/GH010168.MP4 + * * http://10.5.5.9:8080/gopro/media/thumbnail?path=100GOPRO/GH010168.MP4 + * + * @returns fileName from url - For example: GH010168.MP4 + */ +export function extractFileNameFromGoProUrl(url: string): string { + return url.split('/').pop() ?? ''; +} + +export function urlIsImage(url: string): boolean { + return ( + url.toLocaleLowerCase().includes('.jpeg') || + url.toLocaleLowerCase().includes('.jpg') + ); +} + +export function urlIsVideo(url: string): boolean { + return url.toLowerCase().includes('.mp4'); +} + +export function detectFileTypeFromUrl( + url: string +): 'image' | 'video' | 'unknown' { + if (urlIsImage(url)) { + return 'image'; + } + if (urlIsVideo(url)) { + return 'video'; + } + return 'unknown'; +}