From 719d83a2b5616969f2263ad18ae4159c9277edbd Mon Sep 17 00:00:00 2001 From: Paul Ochieng Levi Date: Sat, 29 Jun 2024 22:13:56 +0300 Subject: [PATCH 01/17] chore: update Android Gradle plugin to version 8.5.0 --- apps/picsa-apps/extension-app-native/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/picsa-apps/extension-app-native/android/build.gradle b/apps/picsa-apps/extension-app-native/android/build.gradle index 40735778..76f1d892 100644 --- a/apps/picsa-apps/extension-app-native/android/build.gradle +++ b/apps/picsa-apps/extension-app-native/android/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.4' + classpath 'com.android.tools.build:gradle:8.5.0' classpath 'com.google.gms:google-services:4.4.0' // NOTE: Do not place your application dependencies here; they belong From 43e82c0d651ef945afd261c78ed28b5bde041cc0 Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Thu, 18 Jul 2024 22:56:08 +0300 Subject: [PATCH 02/17] refactor: Move photo-input service to shared library --- .../photo-input/photo-input.component.ts | 63 ------------------- .../photo-input/photo-input.component.html | 0 .../photo-input/photo-input.component.scss | 0 .../photo-input/photo-input.component.spec.ts | 0 .../photo-input/photo-input.service.ts | 3 +- .../src/features/photo-input/schema/index.ts | 12 ++++ .../features/photo-input/schema/schema_v0.ts | 47 ++++++++++++++ 7 files changed, 61 insertions(+), 64 deletions(-) delete mode 100644 apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.component.ts rename {apps/picsa-tools/farmer-activity/src/app/components => libs/shared/src/features}/photo-input/photo-input.component.html (100%) rename {apps/picsa-tools/farmer-activity/src/app/components => libs/shared/src/features}/photo-input/photo-input.component.scss (100%) rename {apps/picsa-tools/farmer-activity/src/app/components => libs/shared/src/features}/photo-input/photo-input.component.spec.ts (100%) rename {apps/picsa-tools/farmer-activity/src/app/components => libs/shared/src/features}/photo-input/photo-input.service.ts (85%) create mode 100644 libs/shared/src/features/photo-input/schema/index.ts create mode 100644 libs/shared/src/features/photo-input/schema/schema_v0.ts diff --git a/apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.component.ts b/apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.component.ts deleted file mode 100644 index 009c66c9..00000000 --- a/apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.component.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { Component, ElementRef, ViewChild } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; -import { Capacitor } from '@capacitor/core'; -import { PicsaTranslateModule } from '@picsa/shared/modules'; - -import { PhotoService } from './photo-input.service'; - -interface Photo { - webPath?: string; -} - -@Component({ - selector: 'picsa-photo-input', - templateUrl: './photo-input.component.html', - styleUrls: ['./photo-input.component.scss'], - standalone: true, - imports: [CommonModule, MatButtonModule, MatIconModule, PicsaTranslateModule], -}) -export class PicsaPhotoInputComponent { - @ViewChild('fileInput') fileInput: ElementRef; - photos: Photo[] = []; - - public isWebPlatform = Capacitor.getPlatform() === 'web'; - - constructor(private photoService: PhotoService) {} - - async takeOrChoosePicture() { - const platform = Capacitor.getPlatform(); - const source = platform === 'web' ? CameraSource.Photos : CameraSource.Prompt; - - const image = await Camera.getPhoto({ - quality: 90, - allowEditing: false, - resultType: CameraResultType.Uri, - source: source, - }); - - this.photos.push({ - webPath: image.webPath, - }); - } - - processWebPicture(event: any) { - const file = event.target.files[0]; - const reader = new FileReader(); - reader.onload = (e) => { - const photo = { webPath: e.target?.result as string }; - this.photos.push(photo); - }; - reader.readAsDataURL(file); - } - - removePhoto(index: number) { - this.photos.splice(index, 1); - } - - removeAllPhotos() { - this.photos = []; - } -} diff --git a/apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.component.html b/libs/shared/src/features/photo-input/photo-input.component.html similarity index 100% rename from apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.component.html rename to libs/shared/src/features/photo-input/photo-input.component.html diff --git a/apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.component.scss b/libs/shared/src/features/photo-input/photo-input.component.scss similarity index 100% rename from apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.component.scss rename to libs/shared/src/features/photo-input/photo-input.component.scss diff --git a/apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.component.spec.ts b/libs/shared/src/features/photo-input/photo-input.component.spec.ts similarity index 100% rename from apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.component.spec.ts rename to libs/shared/src/features/photo-input/photo-input.component.spec.ts diff --git a/apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.service.ts b/libs/shared/src/features/photo-input/photo-input.service.ts similarity index 85% rename from apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.service.ts rename to libs/shared/src/features/photo-input/photo-input.service.ts index be22c07b..54e4f62e 100644 --- a/apps/picsa-tools/farmer-activity/src/app/components/photo-input/photo-input.service.ts +++ b/libs/shared/src/features/photo-input/photo-input.service.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { Injectable } from '@angular/core'; @Injectable({ @@ -13,7 +14,7 @@ export class PhotoService { } // This method will save the photo to the database. - savePhoto(photo: any) { + savePhoto() { return; } diff --git a/libs/shared/src/features/photo-input/schema/index.ts b/libs/shared/src/features/photo-input/schema/index.ts new file mode 100644 index 00000000..0a9fbb8b --- /dev/null +++ b/libs/shared/src/features/photo-input/schema/index.ts @@ -0,0 +1,12 @@ +import * as schema from './schema_v0'; + +// Re-export schema to provide latest version without need to refactor additional code + +export const COLLECTION = schema.PHOTO_COLLECTION_V0; +export type IPhotoEntry = schema.IPhotoEntry_V0; +export const SCHEMA = schema.PHOTO_SCHEMA_V0; + +// Ensure blank templates always recreated from scratch +export const ENTRY_TEMPLATE = schema.PHOTO_ENTRY_TEMPLATE_V0; + +export const COLLECTION_NAME = 'photo_storage'; diff --git a/libs/shared/src/features/photo-input/schema/schema_v0.ts b/libs/shared/src/features/photo-input/schema/schema_v0.ts new file mode 100644 index 00000000..3d987f08 --- /dev/null +++ b/libs/shared/src/features/photo-input/schema/schema_v0.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { RxJsonSchema } from 'rxdb'; + +import type { IPicsaCollectionCreator } from '../../../services/core/db_v2/models/index'; + +const PHOTO_SCHEMA_VERSION = 0; + +export interface IPhotoEntry_V0 { + id: string; + photoData: string; // This could be a URL or base64 string + timestamp: number; // Unix timestamp + custom_meta?: any; // Optional field for any additional data +} + +export const PHOTO_ENTRY_TEMPLATE_V0: (id: string, photoData: string) => IPhotoEntry_V0 = (id, photoData) => ({ + id, + photoData, + timestamp: Date.now(), + custom_meta: {}, +}); + +export const PHOTO_SCHEMA_V0: RxJsonSchema = { + title: 'photo storage schema', + version: PHOTO_SCHEMA_VERSION, + type: 'object', + primaryKey: 'id', + properties: { + id: { + type: 'string', + }, + photoData: { + type: 'string', + }, + timestamp: { + type: 'number', + }, + custom_meta: { + type: 'object', + }, + }, + required: ['id', 'photoData', 'timestamp'], +}; + +export const PHOTO_COLLECTION_V0: IPicsaCollectionCreator = { + schema: PHOTO_SCHEMA_V0, + isUserCollection: true, +}; From 1040c411b11169428b5ab9d41ac3068d3790dcaa Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Thu, 18 Jul 2024 22:58:53 +0300 Subject: [PATCH 03/17] chore: Update Picsa components module to include PicsaPhotoInputModule --- .../src/app/components/components.module.ts | 7 +-- libs/shared/src/features/index.ts | 1 + .../photo-input/photo-input.component.ts | 58 +++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 libs/shared/src/features/photo-input/photo-input.component.ts diff --git a/apps/picsa-tools/farmer-activity/src/app/components/components.module.ts b/apps/picsa-tools/farmer-activity/src/app/components/components.module.ts index d71931c5..d5b272c5 100644 --- a/apps/picsa-tools/farmer-activity/src/app/components/components.module.ts +++ b/apps/picsa-tools/farmer-activity/src/app/components/components.module.ts @@ -4,12 +4,11 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; // Shared modules import { PicsaCommonComponentsModule } from '@picsa/components'; -import { PicsaVideoPlayerModule } from '@picsa/shared/features'; +import { PicsaPhotoInputModule, PicsaVideoPlayerModule } from '@picsa/shared/features'; import { PicsaTranslateModule } from '@picsa/shared/modules'; // Local components import { FarmerActivityMaterialModule } from './material.module'; -import { PicsaPhotoInputComponent } from './photo-input/photo-input.component'; const Components = []; @@ -18,7 +17,7 @@ const Components = []; CommonModule, FormsModule, PicsaCommonComponentsModule, - PicsaPhotoInputComponent, + PicsaPhotoInputModule, PicsaTranslateModule, PicsaVideoPlayerModule, ReactiveFormsModule, @@ -28,7 +27,7 @@ const Components = []; exports: [ FarmerActivityMaterialModule, PicsaCommonComponentsModule, - PicsaPhotoInputComponent, + PicsaPhotoInputModule, PicsaVideoPlayerModule, ...Components, ], diff --git a/libs/shared/src/features/index.ts b/libs/shared/src/features/index.ts index 84e09c6a..b75676e2 100644 --- a/libs/shared/src/features/index.ts +++ b/libs/shared/src/features/index.ts @@ -4,4 +4,5 @@ export * from './data-table'; export * from './dialog'; export * from './drawing'; export * from './pdf-viewer'; +export * from './photo-input'; export * from './video-player'; diff --git a/libs/shared/src/features/photo-input/photo-input.component.ts b/libs/shared/src/features/photo-input/photo-input.component.ts new file mode 100644 index 00000000..91baa193 --- /dev/null +++ b/libs/shared/src/features/photo-input/photo-input.component.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import { Capacitor } from '@capacitor/core'; + +import { PhotoService } from './photo-input.service'; + +interface Photo { + webPath?: string; +} + +@Component({ + selector: 'picsa-photo-input', + templateUrl: './photo-input.component.html', + styleUrls: ['./photo-input.component.scss'], +}) +export class PicsaPhotoInputComponent { + @ViewChild('fileInput') fileInput: ElementRef; + photos: Photo[] = []; + + public isWebPlatform = Capacitor.getPlatform() === 'web'; + + constructor(private photoService: PhotoService) {} + + async takeOrChoosePicture() { + const platform = Capacitor.getPlatform(); + const source = platform === 'web' ? CameraSource.Photos : CameraSource.Prompt; + + const image = await Camera.getPhoto({ + quality: 90, + allowEditing: false, + resultType: CameraResultType.Uri, + source: source, + }); + + this.photos.push({ + webPath: image.webPath, + }); + } + + processWebPicture(event: any) { + const file = event.target.files[0]; + const reader = new FileReader(); + reader.onload = (e) => { + const photo = { webPath: e.target?.result as string }; + this.photos.push(photo); + }; + reader.readAsDataURL(file); + } + + removePhoto(index: number) { + this.photos.splice(index, 1); + } + + removeAllPhotos() { + this.photos = []; + } +} From fd405e0ee666506483a614875de9f2397c3ac884 Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Thu, 18 Jul 2024 23:10:28 +0300 Subject: [PATCH 04/17] chore: Add PicsaPhotoInputModule to Picsa components module --- libs/shared/src/features/photo-input/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 libs/shared/src/features/photo-input/index.ts diff --git a/libs/shared/src/features/photo-input/index.ts b/libs/shared/src/features/photo-input/index.ts new file mode 100644 index 00000000..2a910808 --- /dev/null +++ b/libs/shared/src/features/photo-input/index.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; + +import { PicsaTranslateModule } from '../../modules'; +import { PicsaPhotoInputComponent } from './photo-input.component'; + +@NgModule({ + imports: [CommonModule, MatButtonModule, MatIconModule, PicsaTranslateModule], + exports: [PicsaPhotoInputComponent], + declarations: [PicsaPhotoInputComponent], + providers: [], +}) +export class PicsaPhotoInputModule {} From 697c2ffc3b6f310b73dfebc7ff71fcea34722741 Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Wed, 24 Jul 2024 18:53:19 +0300 Subject: [PATCH 05/17] feat: Add photo-list component and route for displaying photos --- .../src/app/app-routing.module.ts | 5 +++++ .../photo-list/photo-list.component.html | 1 + .../photo-list/photo-list.component.scss | 0 .../photo-list/photo-list.component.spec.ts | 22 +++++++++++++++++++ .../photo-list/photo-list.component.ts | 11 ++++++++++ 5 files changed, 39 insertions(+) create mode 100644 libs/shared/src/features/photo-list/photo-list.component.html create mode 100644 libs/shared/src/features/photo-list/photo-list.component.scss create mode 100644 libs/shared/src/features/photo-list/photo-list.component.spec.ts create mode 100644 libs/shared/src/features/photo-list/photo-list.component.ts diff --git a/apps/picsa-apps/extension-app/src/app/app-routing.module.ts b/apps/picsa-apps/extension-app/src/app/app-routing.module.ts index aace9ad5..356996cb 100644 --- a/apps/picsa-apps/extension-app/src/app/app-routing.module.ts +++ b/apps/picsa-apps/extension-app/src/app/app-routing.module.ts @@ -72,6 +72,11 @@ const routes: Routes = [ loadChildren: () => import('@picsa/seasonal-calendar/src/app/app.module-embedded').then((mod) => mod.SeasonalCalendarToolModule), }, + { + path: 'photos', + loadComponent: () => + import('@picsa/shared/features/photo-list/photo-list.component').then((mod) => mod.PhotoListComponent), + }, // NOTE - Home not currently working as standalone component so keeping as module // (possibly needs to import router-outlet or similar for setup) { diff --git a/libs/shared/src/features/photo-list/photo-list.component.html b/libs/shared/src/features/photo-list/photo-list.component.html new file mode 100644 index 00000000..f42b476c --- /dev/null +++ b/libs/shared/src/features/photo-list/photo-list.component.html @@ -0,0 +1 @@ +

photo-list works!

diff --git a/libs/shared/src/features/photo-list/photo-list.component.scss b/libs/shared/src/features/photo-list/photo-list.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/libs/shared/src/features/photo-list/photo-list.component.spec.ts b/libs/shared/src/features/photo-list/photo-list.component.spec.ts new file mode 100644 index 00000000..91d54063 --- /dev/null +++ b/libs/shared/src/features/photo-list/photo-list.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PhotoListComponent } from './photo-list.component'; + +describe('PhotoListComponent', () => { + let component: PhotoListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PhotoListComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PhotoListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/shared/src/features/photo-list/photo-list.component.ts b/libs/shared/src/features/photo-list/photo-list.component.ts new file mode 100644 index 00000000..c2ed3c67 --- /dev/null +++ b/libs/shared/src/features/photo-list/photo-list.component.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'picsa-photo-list', + standalone: true, + imports: [CommonModule], + templateUrl: './photo-list.component.html', + styleUrl: './photo-list.component.scss', +}) +export class PhotoListComponent {} From cadf45691d705a709f0d536e4fb24ed7391ca6a3 Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Fri, 26 Jul 2024 12:32:55 +0300 Subject: [PATCH 06/17] chore: Update PicsaPhotoInput component to include activityId prop --- .../activity-details.component.html | 2 +- .../activity-details.component.ts | 7 +- .../photo-input/photo-input.component.ts | 45 ++++++++- .../photo-input/photo-input.service.ts | 97 ++++++++++++++++--- .../features/photo-input/schema/schema_v0.ts | 20 +++- 5 files changed, 148 insertions(+), 23 deletions(-) diff --git a/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.html b/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.html index 1ac5e3b4..3fc283b8 100644 --- a/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.html +++ b/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.html @@ -35,7 +35,7 @@

{{ videoResource.title | translate }}

{{ 'Activity' | translate }}
- +
diff --git a/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.ts b/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.ts index 8adc2702..d5b62d14 100644 --- a/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.ts +++ b/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.ts @@ -19,6 +19,7 @@ import { ACTIVITY_DATA, IActivityEntry } from '../../data'; }) export class ActivityDetailsComponent implements OnInit, OnDestroy { activity: IActivityEntry; + activityId: string; public videoResource: IResourceFile; public videoUri: string; @@ -40,9 +41,9 @@ export class ActivityDetailsComponent implements OnInit, OnDestroy { async ngOnInit() { // Ensure route config updated before init const { params } = this.route.snapshot; - const activityId = params?.id; - if (activityId) { - const activity = this.getActivityById(activityId); + this.activityId = params?.id; + if (this.activityId) { + const activity = this.getActivityById(this.activityId); if (activity) { this.activity = activity; this.componentsService.setHeader({ title: activity.label }); diff --git a/libs/shared/src/features/photo-input/photo-input.component.ts b/libs/shared/src/features/photo-input/photo-input.component.ts index 91baa193..c6c6da33 100644 --- a/libs/shared/src/features/photo-input/photo-input.component.ts +++ b/libs/shared/src/features/photo-input/photo-input.component.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Component, ElementRef, ViewChild } from '@angular/core'; +import { Component, ElementRef, Input, ViewChild } from '@angular/core'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import { Capacitor } from '@capacitor/core'; @@ -16,12 +16,20 @@ interface Photo { }) export class PicsaPhotoInputComponent { @ViewChild('fileInput') fileInput: ElementRef; + @Input() activityId: string; photos: Photo[] = []; public isWebPlatform = Capacitor.getPlatform() === 'web'; constructor(private photoService: PhotoService) {} + async ngOnInit() { + await this.photoService.init(); + await this.loadPhotos(); + console.info('Photos:', this.photos); + } + + // this method will be called when a user clicks the camera button async takeOrChoosePicture() { const platform = Capacitor.getPlatform(); const source = platform === 'web' ? CameraSource.Photos : CameraSource.Prompt; @@ -36,18 +44,42 @@ export class PicsaPhotoInputComponent { this.photos.push({ webPath: image.webPath, }); + + // Save the photo to the database + const photoEntry = { + id: `${this.activityId}_${generateID()}`, + activity: this.activityId, + photoData: image.webPath || '', + timestamp: Date.now(), + }; + await this.photoService.savePhoto(photoEntry); } + // this method will be called when a user selects a photo from the file input processWebPicture(event: any) { const file = event.target.files[0]; const reader = new FileReader(); - reader.onload = (e) => { + reader.onload = async (e) => { const photo = { webPath: e.target?.result as string }; this.photos.push(photo); + + // Save the photo to the database + const photoEntry = { + id: `${this.activityId}_${generateID()}`, + activity: this.activityId, + photoData: photo.webPath, + timestamp: Date.now(), + }; + await this.photoService.savePhoto(photoEntry); }; reader.readAsDataURL(file); } + // this method will load the photos from the database + async loadPhotos() { + this.photos = await this.photoService.getAllPhotos(this.activityId); + } + removePhoto(index: number) { this.photos.splice(index, 1); } @@ -56,3 +88,12 @@ export class PicsaPhotoInputComponent { this.photos = []; } } + +// this function will generate a random +function generateID(length = 20, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') { + let autoId = ''; + for (let i = 0; i < length; i++) { + autoId += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return autoId; +} diff --git a/libs/shared/src/features/photo-input/photo-input.service.ts b/libs/shared/src/features/photo-input/photo-input.service.ts index 54e4f62e..709d818f 100644 --- a/libs/shared/src/features/photo-input/photo-input.service.ts +++ b/libs/shared/src/features/photo-input/photo-input.service.ts @@ -1,25 +1,98 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Injectable } from '@angular/core'; +import { RxCollection } from 'rxdb'; + +import { PicsaAsyncService } from '../../services/asyncService.service'; +import { PicsaDatabase_V2_Service, PicsaDatabaseAttachmentService } from '../../services/core/db_v2'; +import * as Schema from './schema'; + +interface Photo { + webPath: string; +} @Injectable({ providedIn: 'root', }) +export class PhotoService extends PicsaAsyncService { + private dbService: PicsaDatabase_V2_Service; + private collection: RxCollection; + + constructor(dbService: PicsaDatabase_V2_Service, private attachmentService: PicsaDatabaseAttachmentService) { + super(); + this.dbService = dbService; + } + + override async init() { + try { + await this.dbService.ensureCollections({ [Schema.COLLECTION_NAME]: Schema.COLLECTION }); + this.collection = this.dbService.db.collections[Schema.COLLECTION_NAME] as RxCollection; + } catch (error) { + console.error('Failed to initialize database:', error); + } + } + + // this method will save the photo to the database. + async savePhoto(photo: Schema.IPhotoEntry) { + try { + const photoBlob = await this.getPhotoBlob(photo.photoData); + const doc = await this.collection.insert(photo); + console.info('Document inserted:', doc); + await this.attachmentService.putAttachment(doc, photo.id, photoBlob); + } catch (error) { + console.error('Failed to save photo:', error); + } + } + + // this method will get the photos from the database. + async getAllPhotos(activityId: string): Promise { + if (!this.collection) { + console.error('Photos collection is not initialized.'); + return []; + } + const allPhotos = await this.collection.find().where('activity').eq(activityId).exec(); + console.info('Photos:', allPhotos); + + const photos: Photo[] = []; + for (const photo of allPhotos) { + const uri = await this.attachmentService.getFileAttachmentURI(photo, true); + if (uri) { + photos.push({ webPath: uri }); + } + } + return photos; + } -/** - * This is a temporary service to simulate the photo service that will handle the photo input and output. - */ -export class PhotoService { - constructor() { - null; + // this method will delete a photo from the database. + async deletePhoto(id: string) { + try { + const doc = await this.collection.findOne(id).exec(); + if (doc) { + await this.attachmentService.removeAttachment(doc, id); + await doc.remove(); + console.info('Photo deleted:', id); + } + } catch (error) { + console.error('Failed to delete photo:', error); + } } - // This method will save the photo to the database. - savePhoto() { - return; + // this method will delete all photos from the database. + async deleteAllPhotos() { + try { + const docs = await this.collection.find().exec(); + for (const doc of docs) { + await this.attachmentService.removeAttachment(doc, doc.id); + await doc.remove(); + } + console.info('All photos deleted'); + } catch (error) { + console.error('Failed to delete all photos:', error); + } } - // This method will get the photos from the database. - getPhotos() { - return; + // this method will get the photo as a Blob. + private async getPhotoBlob(photoData: string): Promise { + const response = await fetch(photoData); + return response.blob(); } } diff --git a/libs/shared/src/features/photo-input/schema/schema_v0.ts b/libs/shared/src/features/photo-input/schema/schema_v0.ts index 3d987f08..a5fbd02d 100644 --- a/libs/shared/src/features/photo-input/schema/schema_v0.ts +++ b/libs/shared/src/features/photo-input/schema/schema_v0.ts @@ -7,15 +7,21 @@ const PHOTO_SCHEMA_VERSION = 0; export interface IPhotoEntry_V0 { id: string; - photoData: string; // This could be a URL or base64 string - timestamp: number; // Unix timestamp - custom_meta?: any; // Optional field for any additional data + photoData: string; + timestamp: number; + activity: string; + custom_meta?: any; } -export const PHOTO_ENTRY_TEMPLATE_V0: (id: string, photoData: string) => IPhotoEntry_V0 = (id, photoData) => ({ +export const PHOTO_ENTRY_TEMPLATE_V0: (id: string, photoData: string, activity: string) => IPhotoEntry_V0 = ( + id, + photoData, + activity +) => ({ id, photoData, timestamp: Date.now(), + activity, custom_meta: {}, }); @@ -34,11 +40,15 @@ export const PHOTO_SCHEMA_V0: RxJsonSchema = { timestamp: { type: 'number', }, + activity: { + type: 'string', + }, custom_meta: { type: 'object', }, }, - required: ['id', 'photoData', 'timestamp'], + required: ['id', 'photoData', 'timestamp', 'activity'], + attachments: {}, }; export const PHOTO_COLLECTION_V0: IPicsaCollectionCreator = { From b02980c13497afe7e49e7ec0a24f870e1ca226af Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Fri, 26 Jul 2024 12:35:56 +0300 Subject: [PATCH 07/17] chore: Refactor photo-input component and service ``` --- .../photo-input/photo-input.component.ts | 4 ++-- .../photo-input/photo-input.service.ts | 22 ++----------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/libs/shared/src/features/photo-input/photo-input.component.ts b/libs/shared/src/features/photo-input/photo-input.component.ts index c6c6da33..857c5930 100644 --- a/libs/shared/src/features/photo-input/photo-input.component.ts +++ b/libs/shared/src/features/photo-input/photo-input.component.ts @@ -81,11 +81,11 @@ export class PicsaPhotoInputComponent { } removePhoto(index: number) { - this.photos.splice(index, 1); + return; } removeAllPhotos() { - this.photos = []; + return; } } diff --git a/libs/shared/src/features/photo-input/photo-input.service.ts b/libs/shared/src/features/photo-input/photo-input.service.ts index 709d818f..d6ac1e42 100644 --- a/libs/shared/src/features/photo-input/photo-input.service.ts +++ b/libs/shared/src/features/photo-input/photo-input.service.ts @@ -64,30 +64,12 @@ export class PhotoService extends PicsaAsyncService { // this method will delete a photo from the database. async deletePhoto(id: string) { - try { - const doc = await this.collection.findOne(id).exec(); - if (doc) { - await this.attachmentService.removeAttachment(doc, id); - await doc.remove(); - console.info('Photo deleted:', id); - } - } catch (error) { - console.error('Failed to delete photo:', error); - } + return; } // this method will delete all photos from the database. async deleteAllPhotos() { - try { - const docs = await this.collection.find().exec(); - for (const doc of docs) { - await this.attachmentService.removeAttachment(doc, doc.id); - await doc.remove(); - } - console.info('All photos deleted'); - } catch (error) { - console.error('Failed to delete all photos:', error); - } + return; } // this method will get the photo as a Blob. From b323bcd8d509165584ee2713a6323d782555d457 Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Fri, 26 Jul 2024 16:20:20 +0300 Subject: [PATCH 08/17] refactor: Remove console logs and optimize photo deletion --- .../photo-input/photo-input.component.ts | 10 ++-- .../photo-input/photo-input.service.ts | 47 ++++++++++++++----- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/libs/shared/src/features/photo-input/photo-input.component.ts b/libs/shared/src/features/photo-input/photo-input.component.ts index 857c5930..79bacd17 100644 --- a/libs/shared/src/features/photo-input/photo-input.component.ts +++ b/libs/shared/src/features/photo-input/photo-input.component.ts @@ -26,7 +26,6 @@ export class PicsaPhotoInputComponent { async ngOnInit() { await this.photoService.init(); await this.loadPhotos(); - console.info('Photos:', this.photos); } // this method will be called when a user clicks the camera button @@ -81,15 +80,18 @@ export class PicsaPhotoInputComponent { } removePhoto(index: number) { - return; + const photo = this.photos[index]; + this.photos.splice(index, 1); + if (photo.webPath) this.photoService.deletePhoto(photo.webPath); } removeAllPhotos() { - return; + this.photos = []; + this.photoService.deleteAllPhotos(); } } -// this function will generate a random +// this function will generate a random ID function generateID(length = 20, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') { let autoId = ''; for (let i = 0; i < length; i++) { diff --git a/libs/shared/src/features/photo-input/photo-input.service.ts b/libs/shared/src/features/photo-input/photo-input.service.ts index d6ac1e42..d979b80f 100644 --- a/libs/shared/src/features/photo-input/photo-input.service.ts +++ b/libs/shared/src/features/photo-input/photo-input.service.ts @@ -31,10 +31,23 @@ export class PhotoService extends PicsaAsyncService { } } + async clearPhotosCollection() { + try { + const allDocs = await this.collection.find().exec(); + for (const doc of allDocs) { + await doc.remove(); + } + console.info('All photos deleted from the collection'); + } catch (error) { + console.error('Failed to clear photos collection:', error); + } + } + // this method will save the photo to the database. async savePhoto(photo: Schema.IPhotoEntry) { try { - const photoBlob = await this.getPhotoBlob(photo.photoData); + const response = await fetch(photo.photoData); + const photoBlob = await response.blob(); const doc = await this.collection.insert(photo); console.info('Document inserted:', doc); await this.attachmentService.putAttachment(doc, photo.id, photoBlob); @@ -54,9 +67,15 @@ export class PhotoService extends PicsaAsyncService { const photos: Photo[] = []; for (const photo of allPhotos) { - const uri = await this.attachmentService.getFileAttachmentURI(photo, true); - if (uri) { - photos.push({ webPath: uri }); + const attachment = photo.getAttachment(photo.id); + console.info('Attachment:', attachment); + if (attachment) { + const uri = await this.attachmentService.getFileAttachmentURI(photo, true); + + console.info('Photos to display with image url:', uri); + if (uri) { + photos.push({ webPath: uri }); + } } } return photos; @@ -64,17 +83,21 @@ export class PhotoService extends PicsaAsyncService { // this method will delete a photo from the database. async deletePhoto(id: string) { - return; + const doc = await this.collection.findOne(id).exec(); + if (doc) { + await this.attachmentService.removeAttachment(doc, id); + await doc.remove(); + console.info('Photo deleted:', id); + } } // this method will delete all photos from the database. async deleteAllPhotos() { - return; - } - - // this method will get the photo as a Blob. - private async getPhotoBlob(photoData: string): Promise { - const response = await fetch(photoData); - return response.blob(); + const docs = await this.collection.find().exec(); + for (const doc of docs) { + await this.attachmentService.removeAttachment(doc, doc.id); + await doc.remove(); + } + console.info('All photos deleted'); } } From 334f0b95917775842a2763e5449b39740688e379 Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Fri, 26 Jul 2024 16:42:53 +0300 Subject: [PATCH 09/17] chore: Update attachmentService method in photo-input.service.ts --- libs/shared/src/features/photo-input/photo-input.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/shared/src/features/photo-input/photo-input.service.ts b/libs/shared/src/features/photo-input/photo-input.service.ts index d979b80f..fecf0afd 100644 --- a/libs/shared/src/features/photo-input/photo-input.service.ts +++ b/libs/shared/src/features/photo-input/photo-input.service.ts @@ -70,7 +70,7 @@ export class PhotoService extends PicsaAsyncService { const attachment = photo.getAttachment(photo.id); console.info('Attachment:', attachment); if (attachment) { - const uri = await this.attachmentService.getFileAttachmentURI(photo, true); + const uri = await this.attachmentService.getFileAttachmentURI(attachment.doc, true); console.info('Photos to display with image url:', uri); if (uri) { From faf1617c5d7675ba681d024b90d51d721370c7d8 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 27 Jul 2024 14:56:27 -0700 Subject: [PATCH 10/17] chore: code tidying --- .../extension-app/src/app/app-routing.module.ts | 3 +-- .../src/app/components/components.module.ts | 13 +++---------- libs/shared/src/features/index.ts | 2 +- libs/shared/src/features/photo-input/index.ts | 15 --------------- .../features/photo-list/photo-list.component.html | 1 - .../features/photo-list/photo-list.component.ts | 11 ----------- .../photo-input/photo-input.component.html | 0 .../photo-input/photo-input.component.scss | 0 .../photo-input/photo-input.component.spec.ts | 0 .../photo-input/photo-input.component.ts | 7 ++++++- .../photo-list/photo-list.component.html | 5 +++++ .../photo-list/photo-list.component.scss | 0 .../photo-list/photo-list.component.spec.ts | 0 .../components/photo-list/photo-list.component.ts | 14 ++++++++++++++ libs/shared/src/features/photo/index.ts | 3 +++ .../photo.service.ts} | 0 .../{photo-input => photo}/schema/index.ts | 0 .../{photo-input => photo}/schema/schema_v0.ts | 0 18 files changed, 33 insertions(+), 41 deletions(-) delete mode 100644 libs/shared/src/features/photo-input/index.ts delete mode 100644 libs/shared/src/features/photo-list/photo-list.component.html delete mode 100644 libs/shared/src/features/photo-list/photo-list.component.ts rename libs/shared/src/features/{ => photo/components}/photo-input/photo-input.component.html (100%) rename libs/shared/src/features/{ => photo/components}/photo-input/photo-input.component.scss (100%) rename libs/shared/src/features/{ => photo/components}/photo-input/photo-input.component.spec.ts (100%) rename libs/shared/src/features/{ => photo/components}/photo-input/photo-input.component.ts (90%) create mode 100644 libs/shared/src/features/photo/components/photo-list/photo-list.component.html rename libs/shared/src/features/{ => photo/components}/photo-list/photo-list.component.scss (100%) rename libs/shared/src/features/{ => photo/components}/photo-list/photo-list.component.spec.ts (100%) create mode 100644 libs/shared/src/features/photo/components/photo-list/photo-list.component.ts create mode 100644 libs/shared/src/features/photo/index.ts rename libs/shared/src/features/{photo-input/photo-input.service.ts => photo/photo.service.ts} (100%) rename libs/shared/src/features/{photo-input => photo}/schema/index.ts (100%) rename libs/shared/src/features/{photo-input => photo}/schema/schema_v0.ts (100%) diff --git a/apps/picsa-apps/extension-app/src/app/app-routing.module.ts b/apps/picsa-apps/extension-app/src/app/app-routing.module.ts index 356996cb..8d6f1782 100644 --- a/apps/picsa-apps/extension-app/src/app/app-routing.module.ts +++ b/apps/picsa-apps/extension-app/src/app/app-routing.module.ts @@ -74,8 +74,7 @@ const routes: Routes = [ }, { path: 'photos', - loadComponent: () => - import('@picsa/shared/features/photo-list/photo-list.component').then((mod) => mod.PhotoListComponent), + loadComponent: () => import('@picsa/shared/features/photo').then((mod) => mod.PicsaPhotoListComponent), }, // NOTE - Home not currently working as standalone component so keeping as module // (possibly needs to import router-outlet or similar for setup) diff --git a/apps/picsa-tools/farmer-activity/src/app/components/components.module.ts b/apps/picsa-tools/farmer-activity/src/app/components/components.module.ts index d5b272c5..378eb662 100644 --- a/apps/picsa-tools/farmer-activity/src/app/components/components.module.ts +++ b/apps/picsa-tools/farmer-activity/src/app/components/components.module.ts @@ -4,34 +4,27 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; // Shared modules import { PicsaCommonComponentsModule } from '@picsa/components'; -import { PicsaPhotoInputModule, PicsaVideoPlayerModule } from '@picsa/shared/features'; +import { PicsaPhotoInputComponent, PicsaVideoPlayerModule } from '@picsa/shared/features'; import { PicsaTranslateModule } from '@picsa/shared/modules'; // Local components import { FarmerActivityMaterialModule } from './material.module'; -const Components = []; +const Components = [PicsaPhotoInputComponent]; @NgModule({ imports: [ CommonModule, FormsModule, PicsaCommonComponentsModule, - PicsaPhotoInputModule, PicsaTranslateModule, PicsaVideoPlayerModule, ReactiveFormsModule, RouterModule, FarmerActivityMaterialModule, - ], - exports: [ - FarmerActivityMaterialModule, - PicsaCommonComponentsModule, - PicsaPhotoInputModule, - PicsaVideoPlayerModule, ...Components, ], - declarations: [Components], + exports: [FarmerActivityMaterialModule, PicsaCommonComponentsModule, PicsaVideoPlayerModule, ...Components], providers: [], }) export class FarmerActivityComponentsModule {} diff --git a/libs/shared/src/features/index.ts b/libs/shared/src/features/index.ts index b75676e2..af92875a 100644 --- a/libs/shared/src/features/index.ts +++ b/libs/shared/src/features/index.ts @@ -4,5 +4,5 @@ export * from './data-table'; export * from './dialog'; export * from './drawing'; export * from './pdf-viewer'; -export * from './photo-input'; +export * from './photo'; export * from './video-player'; diff --git a/libs/shared/src/features/photo-input/index.ts b/libs/shared/src/features/photo-input/index.ts deleted file mode 100644 index 2a910808..00000000 --- a/libs/shared/src/features/photo-input/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; - -import { PicsaTranslateModule } from '../../modules'; -import { PicsaPhotoInputComponent } from './photo-input.component'; - -@NgModule({ - imports: [CommonModule, MatButtonModule, MatIconModule, PicsaTranslateModule], - exports: [PicsaPhotoInputComponent], - declarations: [PicsaPhotoInputComponent], - providers: [], -}) -export class PicsaPhotoInputModule {} diff --git a/libs/shared/src/features/photo-list/photo-list.component.html b/libs/shared/src/features/photo-list/photo-list.component.html deleted file mode 100644 index f42b476c..00000000 --- a/libs/shared/src/features/photo-list/photo-list.component.html +++ /dev/null @@ -1 +0,0 @@ -

photo-list works!

diff --git a/libs/shared/src/features/photo-list/photo-list.component.ts b/libs/shared/src/features/photo-list/photo-list.component.ts deleted file mode 100644 index c2ed3c67..00000000 --- a/libs/shared/src/features/photo-list/photo-list.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { Component } from '@angular/core'; - -@Component({ - selector: 'picsa-photo-list', - standalone: true, - imports: [CommonModule], - templateUrl: './photo-list.component.html', - styleUrl: './photo-list.component.scss', -}) -export class PhotoListComponent {} diff --git a/libs/shared/src/features/photo-input/photo-input.component.html b/libs/shared/src/features/photo/components/photo-input/photo-input.component.html similarity index 100% rename from libs/shared/src/features/photo-input/photo-input.component.html rename to libs/shared/src/features/photo/components/photo-input/photo-input.component.html diff --git a/libs/shared/src/features/photo-input/photo-input.component.scss b/libs/shared/src/features/photo/components/photo-input/photo-input.component.scss similarity index 100% rename from libs/shared/src/features/photo-input/photo-input.component.scss rename to libs/shared/src/features/photo/components/photo-input/photo-input.component.scss diff --git a/libs/shared/src/features/photo-input/photo-input.component.spec.ts b/libs/shared/src/features/photo/components/photo-input/photo-input.component.spec.ts similarity index 100% rename from libs/shared/src/features/photo-input/photo-input.component.spec.ts rename to libs/shared/src/features/photo/components/photo-input/photo-input.component.spec.ts diff --git a/libs/shared/src/features/photo-input/photo-input.component.ts b/libs/shared/src/features/photo/components/photo-input/photo-input.component.ts similarity index 90% rename from libs/shared/src/features/photo-input/photo-input.component.ts rename to libs/shared/src/features/photo/components/photo-input/photo-input.component.ts index 79bacd17..836a7c8a 100644 --- a/libs/shared/src/features/photo-input/photo-input.component.ts +++ b/libs/shared/src/features/photo/components/photo-input/photo-input.component.ts @@ -1,9 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Component, ElementRef, Input, ViewChild } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import { Capacitor } from '@capacitor/core'; +import { PicsaTranslateModule } from '@picsa/shared/modules'; -import { PhotoService } from './photo-input.service'; +import { PhotoService } from '../../photo.service'; interface Photo { webPath?: string; @@ -13,6 +16,8 @@ interface Photo { selector: 'picsa-photo-input', templateUrl: './photo-input.component.html', styleUrls: ['./photo-input.component.scss'], + standalone: true, + imports: [PicsaTranslateModule, MatButtonModule, MatIconModule], }) export class PicsaPhotoInputComponent { @ViewChild('fileInput') fileInput: ElementRef; diff --git a/libs/shared/src/features/photo/components/photo-list/photo-list.component.html b/libs/shared/src/features/photo/components/photo-list/photo-list.component.html new file mode 100644 index 00000000..d00925bf --- /dev/null +++ b/libs/shared/src/features/photo/components/photo-list/photo-list.component.html @@ -0,0 +1,5 @@ +
+

Photos

+ +
+ diff --git a/libs/shared/src/features/photo-list/photo-list.component.scss b/libs/shared/src/features/photo/components/photo-list/photo-list.component.scss similarity index 100% rename from libs/shared/src/features/photo-list/photo-list.component.scss rename to libs/shared/src/features/photo/components/photo-list/photo-list.component.scss diff --git a/libs/shared/src/features/photo-list/photo-list.component.spec.ts b/libs/shared/src/features/photo/components/photo-list/photo-list.component.spec.ts similarity index 100% rename from libs/shared/src/features/photo-list/photo-list.component.spec.ts rename to libs/shared/src/features/photo/components/photo-list/photo-list.component.spec.ts diff --git a/libs/shared/src/features/photo/components/photo-list/photo-list.component.ts b/libs/shared/src/features/photo/components/photo-list/photo-list.component.ts new file mode 100644 index 00000000..e3964685 --- /dev/null +++ b/libs/shared/src/features/photo/components/photo-list/photo-list.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; + +import { PicsaPhotoInputComponent } from '../photo-input/photo-input.component'; + +@Component({ + selector: 'picsa-photo-list', + standalone: true, + imports: [CommonModule, MatButtonModule, PicsaPhotoInputComponent], + templateUrl: './photo-list.component.html', + styleUrl: './photo-list.component.scss', +}) +export class PicsaPhotoListComponent {} diff --git a/libs/shared/src/features/photo/index.ts b/libs/shared/src/features/photo/index.ts new file mode 100644 index 00000000..7794f6d3 --- /dev/null +++ b/libs/shared/src/features/photo/index.ts @@ -0,0 +1,3 @@ +export * from './components/photo-input/photo-input.component'; +export * from './components/photo-list/photo-list.component'; +export * from './photo.service'; diff --git a/libs/shared/src/features/photo-input/photo-input.service.ts b/libs/shared/src/features/photo/photo.service.ts similarity index 100% rename from libs/shared/src/features/photo-input/photo-input.service.ts rename to libs/shared/src/features/photo/photo.service.ts diff --git a/libs/shared/src/features/photo-input/schema/index.ts b/libs/shared/src/features/photo/schema/index.ts similarity index 100% rename from libs/shared/src/features/photo-input/schema/index.ts rename to libs/shared/src/features/photo/schema/index.ts diff --git a/libs/shared/src/features/photo-input/schema/schema_v0.ts b/libs/shared/src/features/photo/schema/schema_v0.ts similarity index 100% rename from libs/shared/src/features/photo-input/schema/schema_v0.ts rename to libs/shared/src/features/photo/schema/schema_v0.ts From 1ed9b84ebb0ede3687852e9fce0df5ca5da9e6fc Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 28 Jul 2024 10:38:49 -0700 Subject: [PATCH 11/17] refactor: photo storage schema updates --- .../option-tool/src/app/schemas/schema_v4.ts | 6 +-- .../shared/src/features/photo/schema/index.ts | 2 +- .../src/features/photo/schema/schema_v0.ts | 44 +++++++++++-------- libs/shared/src/services/core/db_v2/index.ts | 5 +++ 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/apps/picsa-tools/option-tool/src/app/schemas/schema_v4.ts b/apps/picsa-tools/option-tool/src/app/schemas/schema_v4.ts index 90a94fc2..cd1643a2 100644 --- a/apps/picsa-tools/option-tool/src/app/schemas/schema_v4.ts +++ b/apps/picsa-tools/option-tool/src/app/schemas/schema_v4.ts @@ -1,15 +1,11 @@ import { generateID } from '@picsa/shared/services/core/db/db.service'; -import type { IPicsaCollectionCreator } from '@picsa/shared/services/core/db_v2'; +import { generateTimestamp, type IPicsaCollectionCreator } from '@picsa/shared/services/core/db_v2'; import { RxJsonSchema } from 'rxdb'; import { IOptionsToolEntry_v3, SCHEMA_V3 } from './schema_v3'; const SCHEMA_VERSION = 4; -const generateTimestamp = (): string => { - return new Date().toISOString(); -}; - /** * ADD 'enterprise' and '_created_at' properties */ diff --git a/libs/shared/src/features/photo/schema/index.ts b/libs/shared/src/features/photo/schema/index.ts index 0a9fbb8b..dfed25f5 100644 --- a/libs/shared/src/features/photo/schema/index.ts +++ b/libs/shared/src/features/photo/schema/index.ts @@ -9,4 +9,4 @@ export const SCHEMA = schema.PHOTO_SCHEMA_V0; // Ensure blank templates always recreated from scratch export const ENTRY_TEMPLATE = schema.PHOTO_ENTRY_TEMPLATE_V0; -export const COLLECTION_NAME = 'photo_storage'; +export const COLLECTION_NAME = 'photos'; diff --git a/libs/shared/src/features/photo/schema/schema_v0.ts b/libs/shared/src/features/photo/schema/schema_v0.ts index a5fbd02d..908204fd 100644 --- a/libs/shared/src/features/photo/schema/schema_v0.ts +++ b/libs/shared/src/features/photo/schema/schema_v0.ts @@ -1,4 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { generateID } from '@picsa/shared/services/core/db/db.service'; +import { generateTimestamp } from '@picsa/shared/services/core/db_v2'; import { RxJsonSchema } from 'rxdb'; import type { IPicsaCollectionCreator } from '../../../services/core/db_v2/models/index'; @@ -6,24 +8,28 @@ import type { IPicsaCollectionCreator } from '../../../services/core/db_v2/model const PHOTO_SCHEMA_VERSION = 0; export interface IPhotoEntry_V0 { + _created_at: string; + /** Generated id consiting of `{album}/{name}` */ id: string; - photoData: string; - timestamp: number; - activity: string; + /** subpath to store photo */ + album: string; + /** name of photo (randomly generated if not specified) */ + name: string; + /** additional metadata to include with photo */ custom_meta?: any; } -export const PHOTO_ENTRY_TEMPLATE_V0: (id: string, photoData: string, activity: string) => IPhotoEntry_V0 = ( - id, - photoData, - activity -) => ({ - id, - photoData, - timestamp: Date.now(), - activity, - custom_meta: {}, -}); +export const PHOTO_ENTRY_TEMPLATE_V0 = (album: string, name = generateID(), custom_meta?: any) => { + const entry: IPhotoEntry_V0 = { + _created_at: generateTimestamp(), + id: `${album}/${name}`, + album, + name, + custom_meta: {}, + }; + if (custom_meta) entry.custom_meta = custom_meta; + return entry; +}; export const PHOTO_SCHEMA_V0: RxJsonSchema = { title: 'photo storage schema', @@ -34,20 +40,20 @@ export const PHOTO_SCHEMA_V0: RxJsonSchema = { id: { type: 'string', }, - photoData: { + album: { type: 'string', }, - timestamp: { - type: 'number', + name: { + type: 'string', }, - activity: { + _created_at: { type: 'string', }, custom_meta: { type: 'object', }, }, - required: ['id', 'photoData', 'timestamp', 'activity'], + required: ['id', '_created_at', 'album'], attachments: {}, }; diff --git a/libs/shared/src/services/core/db_v2/index.ts b/libs/shared/src/services/core/db_v2/index.ts index a8c33e2d..145f7704 100644 --- a/libs/shared/src/services/core/db_v2/index.ts +++ b/libs/shared/src/services/core/db_v2/index.ts @@ -1,3 +1,8 @@ export * from './db.service'; export * from './db-attachment.service'; export * from './models'; + +/** Generate a timestamp as standard ISO string */ +export const generateTimestamp = (): string => { + return new Date().toISOString(); +}; From ec7817e9a1419d717c3372d127658d06d2538456 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 28 Jul 2024 10:51:38 -0700 Subject: [PATCH 12/17] fix: attachment service filename handling --- .../core/db_v2/db-attachment.service.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/libs/shared/src/services/core/db_v2/db-attachment.service.ts b/libs/shared/src/services/core/db_v2/db-attachment.service.ts index 02c5d9af..8fbdd23f 100644 --- a/libs/shared/src/services/core/db_v2/db-attachment.service.ts +++ b/libs/shared/src/services/core/db_v2/db-attachment.service.ts @@ -51,7 +51,12 @@ export class PicsaDatabaseAttachmentService extends PicsaAsyncService { * @param convertNativeSrc - Convert to src usable within web content (e.g as image or pdf src) **/ public async getFileAttachmentURI(doc: RxDocument, convertNativeSrc = false) { - const attachment = await this.getAttachment(doc, doc.filename); + const attachmentName = doc.filename || doc.id; + if (!attachmentName) { + console.error(doc); + throw new Error(`No attachment name provided`); + } + const attachment = await this.getAttachment(doc, attachmentName); if (!attachment) return null; if (attachment) { if (Capacitor.isNativePlatform()) { @@ -63,22 +68,23 @@ export class PicsaDatabaseAttachmentService extends PicsaAsyncService { // On web data stored as base64 string, convert to blob and generate object url if (attachment.data) { const blob = await base64ToBlob(attachment.data, attachment.type); - this.objectURLs[doc.filename] = URL.createObjectURL(blob); - return this.objectURLs[doc.filename]; + this.objectURLs[attachmentName] = URL.createObjectURL(blob); + return this.objectURLs[attachmentName]; } } return null; } /** * Release a file attachment URI when no longer required - * @param filenames specific resource filenames to revoke (default all) + * @param attachmentNames specific resource attachmentNames to revoke + * These will usually be the doc.filename property (if exists) or doc.id * */ - public async revokeFileAttachmentURIs(filenames: string[]) { - for (const filename of filenames) { - const url = this.objectURLs[filename]; + public async revokeFileAttachmentURIs(attachmentNames: string[]) { + for (const attachmentName of attachmentNames) { + const url = this.objectURLs[attachmentName]; if (url) { URL.revokeObjectURL(url); - delete this.objectURLs[filename]; + delete this.objectURLs[attachmentName]; } } } From c61e355229d58d9a162eab80703476121d81ee01 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 28 Jul 2024 11:54:44 -0700 Subject: [PATCH 13/17] refactor: separate photo debug, input, list and view components --- .../src/app/app-routing.module.ts | 2 +- .../src/app/components/components.module.ts | 4 +- .../src/features/photo/components/index.ts | 4 + .../photo-debug/photo-debug.component.html | 5 ++ .../photo-debug/photo-debug.component.scss | 0 .../photo-debug/photo-debug.component.spec.ts | 22 +++++ .../photo-debug/photo-debug.component.ts | 14 ++++ .../photo-input/photo-input.component.html | 40 ++------- .../photo-input/photo-input.component.scss | 42 ---------- .../photo-input/photo-input.component.ts | 84 ++++--------------- .../photo-list/photo-list.component.html | 11 ++- .../photo-list/photo-list.component.scss | 6 ++ .../photo-list/photo-list.component.ts | 21 +++-- .../photo-view/photo-view.component.html | 10 +++ .../photo-view/photo-view.component.scss | 28 +++++++ .../photo-view/photo-view.component.spec.ts | 22 +++++ .../photo-view/photo-view.component.ts | 51 +++++++++++ libs/shared/src/features/photo/index.ts | 4 +- tsconfig.base.json | 2 +- 19 files changed, 212 insertions(+), 160 deletions(-) create mode 100644 libs/shared/src/features/photo/components/index.ts create mode 100644 libs/shared/src/features/photo/components/photo-debug/photo-debug.component.html create mode 100644 libs/shared/src/features/photo/components/photo-debug/photo-debug.component.scss create mode 100644 libs/shared/src/features/photo/components/photo-debug/photo-debug.component.spec.ts create mode 100644 libs/shared/src/features/photo/components/photo-debug/photo-debug.component.ts create mode 100644 libs/shared/src/features/photo/components/photo-view/photo-view.component.html create mode 100644 libs/shared/src/features/photo/components/photo-view/photo-view.component.scss create mode 100644 libs/shared/src/features/photo/components/photo-view/photo-view.component.spec.ts create mode 100644 libs/shared/src/features/photo/components/photo-view/photo-view.component.ts diff --git a/apps/picsa-apps/extension-app/src/app/app-routing.module.ts b/apps/picsa-apps/extension-app/src/app/app-routing.module.ts index 8d6f1782..78ceeb3b 100644 --- a/apps/picsa-apps/extension-app/src/app/app-routing.module.ts +++ b/apps/picsa-apps/extension-app/src/app/app-routing.module.ts @@ -74,7 +74,7 @@ const routes: Routes = [ }, { path: 'photos', - loadComponent: () => import('@picsa/shared/features/photo').then((mod) => mod.PicsaPhotoListComponent), + loadComponent: () => import('@picsa/shared/features/photo').then((mod) => mod.PhotoDebugComponent), }, // NOTE - Home not currently working as standalone component so keeping as module // (possibly needs to import router-outlet or similar for setup) diff --git a/apps/picsa-tools/farmer-activity/src/app/components/components.module.ts b/apps/picsa-tools/farmer-activity/src/app/components/components.module.ts index 378eb662..bc2daf34 100644 --- a/apps/picsa-tools/farmer-activity/src/app/components/components.module.ts +++ b/apps/picsa-tools/farmer-activity/src/app/components/components.module.ts @@ -4,13 +4,13 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; // Shared modules import { PicsaCommonComponentsModule } from '@picsa/components'; -import { PicsaPhotoInputComponent, PicsaVideoPlayerModule } from '@picsa/shared/features'; +import { PhotoInputComponent, PicsaVideoPlayerModule } from '@picsa/shared/features'; import { PicsaTranslateModule } from '@picsa/shared/modules'; // Local components import { FarmerActivityMaterialModule } from './material.module'; -const Components = [PicsaPhotoInputComponent]; +const Components = [PhotoInputComponent]; @NgModule({ imports: [ diff --git a/libs/shared/src/features/photo/components/index.ts b/libs/shared/src/features/photo/components/index.ts new file mode 100644 index 00000000..9007fa09 --- /dev/null +++ b/libs/shared/src/features/photo/components/index.ts @@ -0,0 +1,4 @@ +export * from './photo-debug/photo-debug.component'; +export * from './photo-input/photo-input.component'; +export * from './photo-list/photo-list.component'; +export * from './photo-view/photo-view.component'; diff --git a/libs/shared/src/features/photo/components/photo-debug/photo-debug.component.html b/libs/shared/src/features/photo/components/photo-debug/photo-debug.component.html new file mode 100644 index 00000000..96f58fd4 --- /dev/null +++ b/libs/shared/src/features/photo/components/photo-debug/photo-debug.component.html @@ -0,0 +1,5 @@ +
+

Photos Debug

+ + +
diff --git a/libs/shared/src/features/photo/components/photo-debug/photo-debug.component.scss b/libs/shared/src/features/photo/components/photo-debug/photo-debug.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/libs/shared/src/features/photo/components/photo-debug/photo-debug.component.spec.ts b/libs/shared/src/features/photo/components/photo-debug/photo-debug.component.spec.ts new file mode 100644 index 00000000..f120936b --- /dev/null +++ b/libs/shared/src/features/photo/components/photo-debug/photo-debug.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PhotoDebugComponent } from './photo-debug.component'; + +describe('PhotoDebugComponent', () => { + let component: PhotoDebugComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PhotoDebugComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PhotoDebugComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/shared/src/features/photo/components/photo-debug/photo-debug.component.ts b/libs/shared/src/features/photo/components/photo-debug/photo-debug.component.ts new file mode 100644 index 00000000..27c72e33 --- /dev/null +++ b/libs/shared/src/features/photo/components/photo-debug/photo-debug.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +import { PhotoInputComponent } from '../photo-input/photo-input.component'; +import { PhotoListComponent } from '../photo-list/photo-list.component'; +import { PhotoViewComponent } from '../photo-view/photo-view.component'; + +@Component({ + selector: 'picsa-photo-debug', + standalone: true, + imports: [PhotoInputComponent, PhotoListComponent, PhotoViewComponent], + templateUrl: './photo-debug.component.html', + styleUrl: './photo-debug.component.scss', +}) +export class PhotoDebugComponent {} diff --git a/libs/shared/src/features/photo/components/photo-input/photo-input.component.html b/libs/shared/src/features/photo/components/photo-input/photo-input.component.html index 9a032816..65243d64 100644 --- a/libs/shared/src/features/photo/components/photo-input/photo-input.component.html +++ b/libs/shared/src/features/photo/components/photo-input/photo-input.component.html @@ -1,34 +1,6 @@ -
-
-
-

Uploaded Photos

- -
- -
-
- - close -
-
- - -
No photos uploaded yet
-
-
- -
- - -
-
+ diff --git a/libs/shared/src/features/photo/components/photo-input/photo-input.component.scss b/libs/shared/src/features/photo/components/photo-input/photo-input.component.scss index af40503a..4f638ae1 100644 --- a/libs/shared/src/features/photo/components/photo-input/photo-input.component.scss +++ b/libs/shared/src/features/photo/components/photo-input/photo-input.component.scss @@ -1,45 +1,3 @@ -.header-section { - display: flex; - justify-content: space-between; - align-items: center; -} - -.no-photos-message { - margin: 20px 0; - text-align: center; -} - -.photo-container { - position: relative; - display: inline-block; - - .close-icon { - display: none; - position: absolute; - top: 0; - right: 0; - cursor: pointer; - color: white; - background-color: red; - border-radius: 50%; - padding: 2px; - margin: 5px; - } - - &:hover { - .close-icon { - display: block; - } - } -} - -img { - max-width: 120px; - max-height: 120px; - object-fit: cover; - margin: 10px; -} - .photo-icon { position: relative; top: 2px; diff --git a/libs/shared/src/features/photo/components/photo-input/photo-input.component.ts b/libs/shared/src/features/photo/components/photo-input/photo-input.component.ts index 836a7c8a..29f2bef0 100644 --- a/libs/shared/src/features/photo/components/photo-input/photo-input.component.ts +++ b/libs/shared/src/features/photo/components/photo-input/photo-input.component.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Component, ElementRef, Input, ViewChild } from '@angular/core'; +import { Component, ElementRef, Input, input, ViewChild } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; @@ -7,10 +7,7 @@ import { Capacitor } from '@capacitor/core'; import { PicsaTranslateModule } from '@picsa/shared/modules'; import { PhotoService } from '../../photo.service'; - -interface Photo { - webPath?: string; -} +import { ENTRY_TEMPLATE } from '../../schema'; @Component({ selector: 'picsa-photo-input', @@ -19,18 +16,20 @@ interface Photo { standalone: true, imports: [PicsaTranslateModule, MatButtonModule, MatIconModule], }) -export class PicsaPhotoInputComponent { +export class PhotoInputComponent { @ViewChild('fileInput') fileInput: ElementRef; - @Input() activityId: string; - photos: Photo[] = []; - - public isWebPlatform = Capacitor.getPlatform() === 'web'; + /** Store photo within a specific named album */ + album = input.required(); + /** + * Name to store photo as. If unspecified will be randomly generated + * If duplicate will override + */ + name = input(); constructor(private photoService: PhotoService) {} async ngOnInit() { await this.photoService.init(); - await this.loadPhotos(); } // this method will be called when a user clicks the camera button @@ -45,62 +44,11 @@ export class PicsaPhotoInputComponent { source: source, }); - this.photos.push({ - webPath: image.webPath, - }); - - // Save the photo to the database - const photoEntry = { - id: `${this.activityId}_${generateID()}`, - activity: this.activityId, - photoData: image.webPath || '', - timestamp: Date.now(), - }; - await this.photoService.savePhoto(photoEntry); - } - - // this method will be called when a user selects a photo from the file input - processWebPicture(event: any) { - const file = event.target.files[0]; - const reader = new FileReader(); - reader.onload = async (e) => { - const photo = { webPath: e.target?.result as string }; - this.photos.push(photo); - - // Save the photo to the database - const photoEntry = { - id: `${this.activityId}_${generateID()}`, - activity: this.activityId, - photoData: photo.webPath, - timestamp: Date.now(), - }; - await this.photoService.savePhoto(photoEntry); - }; - reader.readAsDataURL(file); - } - - // this method will load the photos from the database - async loadPhotos() { - this.photos = await this.photoService.getAllPhotos(this.activityId); - } - - removePhoto(index: number) { - const photo = this.photos[index]; - this.photos.splice(index, 1); - if (photo.webPath) this.photoService.deletePhoto(photo.webPath); - } - - removeAllPhotos() { - this.photos = []; - this.photoService.deleteAllPhotos(); - } -} - -// this function will generate a random ID -function generateID(length = 20, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') { - let autoId = ''; - for (let i = 0; i < length; i++) { - autoId += chars.charAt(Math.floor(Math.random() * chars.length)); + if (image.webPath) { + const fetchRes = await fetch(image.webPath); + const blob = await fetchRes.blob(); + const entry = ENTRY_TEMPLATE(this.album(), this.name()); + await this.photoService.savePhoto(entry, blob); + } } - return autoId; } diff --git a/libs/shared/src/features/photo/components/photo-list/photo-list.component.html b/libs/shared/src/features/photo/components/photo-list/photo-list.component.html index d00925bf..b9b1992b 100644 --- a/libs/shared/src/features/photo/components/photo-list/photo-list.component.html +++ b/libs/shared/src/features/photo/components/photo-list/photo-list.component.html @@ -1,5 +1,8 @@ -
-

Photos

- +
+ @for(photo of photos(); track photo.id;){ + + }
- +@if(photos().length ===0){ +
No photos uploaded yet
+} diff --git a/libs/shared/src/features/photo/components/photo-list/photo-list.component.scss b/libs/shared/src/features/photo/components/photo-list/photo-list.component.scss index e69de29b..b0b6e8b8 100644 --- a/libs/shared/src/features/photo/components/photo-list/photo-list.component.scss +++ b/libs/shared/src/features/photo/components/photo-list/photo-list.component.scss @@ -0,0 +1,6 @@ +.photo-grid { + display: grid; + grid-auto-rows: 120px; + grid-template-columns: repeat(auto-fit, 120px); + gap: 8px; +} diff --git a/libs/shared/src/features/photo/components/photo-list/photo-list.component.ts b/libs/shared/src/features/photo/components/photo-list/photo-list.component.ts index e3964685..3da176cf 100644 --- a/libs/shared/src/features/photo/components/photo-list/photo-list.component.ts +++ b/libs/shared/src/features/photo/components/photo-list/photo-list.component.ts @@ -1,14 +1,23 @@ -import { CommonModule } from '@angular/common'; -import { Component } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; +import { Component, computed, input } from '@angular/core'; -import { PicsaPhotoInputComponent } from '../photo-input/photo-input.component'; +import { PhotoService } from '../../photo.service'; +import { PhotoViewComponent } from '../photo-view/photo-view.component'; @Component({ selector: 'picsa-photo-list', standalone: true, - imports: [CommonModule, MatButtonModule, PicsaPhotoInputComponent], + imports: [PhotoViewComponent], templateUrl: './photo-list.component.html', styleUrl: './photo-list.component.scss', }) -export class PicsaPhotoListComponent {} +export class PhotoListComponent { + album = input(); + + photos = computed(() => { + const album = this.album(); + const photoDocs = this.service.photos(); + return album ? photoDocs.filter((d) => d.album === album) : photoDocs; + }); + + constructor(private service: PhotoService) {} +} diff --git a/libs/shared/src/features/photo/components/photo-view/photo-view.component.html b/libs/shared/src/features/photo/components/photo-view/photo-view.component.html new file mode 100644 index 00000000..d08b4d6d --- /dev/null +++ b/libs/shared/src/features/photo/components/photo-view/photo-view.component.html @@ -0,0 +1,10 @@ +@if(uri()){ + + + + +} @if(errorMsg()){ +
{{ errorMsg() }}
+} diff --git a/libs/shared/src/features/photo/components/photo-view/photo-view.component.scss b/libs/shared/src/features/photo/components/photo-view/photo-view.component.scss new file mode 100644 index 00000000..eac3870c --- /dev/null +++ b/libs/shared/src/features/photo/components/photo-view/photo-view.component.scss @@ -0,0 +1,28 @@ +:host { + display: block; + position: relative; +} +img { + height: 100%; + width: 100%; + object-fit: cover; +} +button.delete-button.mat-mdc-icon-button.mat-mdc-button-base { + --mdc-icon-button-state-layer-size: 32px; + padding: 4px; + border-radius: 0; + position: absolute; + top: 0px; + right: 0px; + color: rgb(0 0 0 / 50%); + background: rgb(255 255 255 / 20%); +} +.error-msg { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + height: 100%; + border: 1px solid var(--color-light); +} diff --git a/libs/shared/src/features/photo/components/photo-view/photo-view.component.spec.ts b/libs/shared/src/features/photo/components/photo-view/photo-view.component.spec.ts new file mode 100644 index 00000000..60393009 --- /dev/null +++ b/libs/shared/src/features/photo/components/photo-view/photo-view.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PhotoViewComponent } from './photo-view.component'; + +describe('PhotoViewComponent', () => { + let component: PhotoViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PhotoViewComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PhotoViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/shared/src/features/photo/components/photo-view/photo-view.component.ts b/libs/shared/src/features/photo/components/photo-view/photo-view.component.ts new file mode 100644 index 00000000..90dc0093 --- /dev/null +++ b/libs/shared/src/features/photo/components/photo-view/photo-view.component.ts @@ -0,0 +1,51 @@ +import { Component, effect, input, signal } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; + +import { PicsaDialogService } from '../../../dialog'; +import { PhotoService } from '../../photo.service'; +import { IPhotoEntry } from '../../schema'; + +@Component({ + selector: 'picsa-photo-view', + standalone: true, + imports: [MatButtonModule, MatIconModule], + templateUrl: './photo-view.component.html', + styleUrl: './photo-view.component.scss', +}) +export class PhotoViewComponent { + /** Input photo document ref */ + photo = input.required(); + /** Path to resource for render */ + uri = signal(''); + /** Error message to display */ + errorMsg = signal(''); + + constructor(private service: PhotoService, private dialog: PicsaDialogService) { + effect( + async (onCleanup) => { + const photo = this.photo(); + const uri = await this.service.getPhotoAttachment(photo.id); + if (uri) { + this.uri.set(uri); + } else { + console.error('[Photo] not found', this.photo()); + this.errorMsg.set(`Photo not found`); + } + onCleanup(() => { + this.service.revokePhotoAttachment(photo.id); + }); + }, + { allowSignalWrites: true } + ); + } + + public async promptDelete() { + const dialogRef = await this.dialog.open('delete'); + dialogRef.afterClosed().subscribe(async (shouldDelete) => { + if (shouldDelete) { + await this.service.deletePhoto(this.photo().id); + } + }); + } +} diff --git a/libs/shared/src/features/photo/index.ts b/libs/shared/src/features/photo/index.ts index 7794f6d3..5a96a5ac 100644 --- a/libs/shared/src/features/photo/index.ts +++ b/libs/shared/src/features/photo/index.ts @@ -1,3 +1,3 @@ -export * from './components/photo-input/photo-input.component'; -export * from './components/photo-list/photo-list.component'; +export * from './components'; export * from './photo.service'; +export * from './schema'; diff --git a/tsconfig.base.json b/tsconfig.base.json index 0a35323e..d8603dd3 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -10,7 +10,7 @@ "importHelpers": true, "target": "es2015", "module": "esnext", - "lib": ["es2017", "dom"], + "lib": ["es2017", "dom", "DOM.Iterable"], "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", From 5109867680967e3ec840bffb095984f34287b2cd Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 28 Jul 2024 11:56:24 -0700 Subject: [PATCH 14/17] refactor: photo service signals --- .../activity-details.component.html | 2 +- .../src/features/photo/photo.service.ts | 94 +++++++------------ 2 files changed, 33 insertions(+), 63 deletions(-) diff --git a/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.html b/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.html index 3fc283b8..62398025 100644 --- a/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.html +++ b/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.html @@ -35,7 +35,7 @@

{{ videoResource.title | translate }}

{{ 'Activity' | translate }}
- +
diff --git a/libs/shared/src/features/photo/photo.service.ts b/libs/shared/src/features/photo/photo.service.ts index fecf0afd..84db2b44 100644 --- a/libs/shared/src/features/photo/photo.service.ts +++ b/libs/shared/src/features/photo/photo.service.ts @@ -1,103 +1,73 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Injectable } from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import { RxCollection } from 'rxdb'; import { PicsaAsyncService } from '../../services/asyncService.service'; import { PicsaDatabase_V2_Service, PicsaDatabaseAttachmentService } from '../../services/core/db_v2'; import * as Schema from './schema'; -interface Photo { - webPath: string; -} - @Injectable({ providedIn: 'root', }) export class PhotoService extends PicsaAsyncService { - private dbService: PicsaDatabase_V2_Service; - private collection: RxCollection; + public collection: RxCollection; - constructor(dbService: PicsaDatabase_V2_Service, private attachmentService: PicsaDatabaseAttachmentService) { + /** List of all stored photos, exposed as signal */ + public photos = signal([]); + + constructor(private dbService: PicsaDatabase_V2_Service, private attachmentService: PicsaDatabaseAttachmentService) { super(); - this.dbService = dbService; } override async init() { try { await this.dbService.ensureCollections({ [Schema.COLLECTION_NAME]: Schema.COLLECTION }); this.collection = this.dbService.db.collections[Schema.COLLECTION_NAME] as RxCollection; + this.subscribeToPhotos(); } catch (error) { console.error('Failed to initialize database:', error); } } - async clearPhotosCollection() { - try { - const allDocs = await this.collection.find().exec(); - for (const doc of allDocs) { - await doc.remove(); - } - console.info('All photos deleted from the collection'); - } catch (error) { - console.error('Failed to clear photos collection:', error); + public async getPhotoAttachment(id: string) { + const doc = await this.collection.findOne(id).exec(); + if (doc) { + return this.attachmentService.getFileAttachmentURI(doc, true); } + return undefined; + } + public revokePhotoAttachment(id: string) { + this.attachmentService.revokeFileAttachmentURIs([id]); + } + + /** Subscribe to all photos and store list within angular signal */ + private subscribeToPhotos() { + this.collection.find().$.subscribe((docs) => { + this.photos.set(docs.map((d) => d._data)); + }); } // this method will save the photo to the database. - async savePhoto(photo: Schema.IPhotoEntry) { + async savePhoto(entry: Schema.IPhotoEntry, data: Blob) { try { - const response = await fetch(photo.photoData); - const photoBlob = await response.blob(); - const doc = await this.collection.insert(photo); - console.info('Document inserted:', doc); - await this.attachmentService.putAttachment(doc, photo.id, photoBlob); + const doc = await this.collection.insert(entry); + await this.attachmentService.putAttachment(doc, entry.id, data); } catch (error) { console.error('Failed to save photo:', error); } } - // this method will get the photos from the database. - async getAllPhotos(activityId: string): Promise { - if (!this.collection) { - console.error('Photos collection is not initialized.'); - return []; - } - const allPhotos = await this.collection.find().where('activity').eq(activityId).exec(); - console.info('Photos:', allPhotos); - - const photos: Photo[] = []; - for (const photo of allPhotos) { - const attachment = photo.getAttachment(photo.id); - console.info('Attachment:', attachment); - if (attachment) { - const uri = await this.attachmentService.getFileAttachmentURI(attachment.doc, true); - - console.info('Photos to display with image url:', uri); - if (uri) { - photos.push({ webPath: uri }); - } - } - } - return photos; - } - // this method will delete a photo from the database. async deletePhoto(id: string) { - const doc = await this.collection.findOne(id).exec(); + let doc = await this.collection.findOne(id).exec(); if (doc) { await this.attachmentService.removeAttachment(doc, id); - await doc.remove(); - console.info('Photo deleted:', id); - } - } - - // this method will delete all photos from the database. - async deleteAllPhotos() { - const docs = await this.collection.find().exec(); - for (const doc of docs) { - await this.attachmentService.removeAttachment(doc, doc.id); - await doc.remove(); + // HACK - fetch the doc again as revision will have changed following + // attachment removal + doc = await this.collection.findOne(id).exec(); + if (doc) { + await doc.remove(); + } } - console.info('All photos deleted'); } } From d88d8158d68bfed86eef8f086f0008918adfd936 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 28 Jul 2024 13:42:50 -0700 Subject: [PATCH 15/17] feat: integrate photos into farmer content --- .../src/app/routes/app-routes.ts | 5 ++++ .../module-home/module-home.component.html | 14 +++++++++++ .../module-home/module-home.component.ts | 25 +++++++++++++++++-- .../photo-list/photo-list.component.html | 2 +- .../photo-list/photo-list.component.ts | 3 ++- 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/apps/picsa-apps/extension-app/src/app/routes/app-routes.ts b/apps/picsa-apps/extension-app/src/app/routes/app-routes.ts index a1a75b14..f66074d9 100644 --- a/apps/picsa-apps/extension-app/src/app/routes/app-routes.ts +++ b/apps/picsa-apps/extension-app/src/app/routes/app-routes.ts @@ -19,6 +19,11 @@ export const APP_ROUTES: Routes = [ children: extensionContentRoutes, title: 'PICSA', }, + // Photos debug page + { + path: 'photos', + loadComponent: () => import('@picsa/shared/features/photo').then((mod) => mod.PhotoDebugComponent), + }, // NOTE - Home not currently working as standalone component so keeping as module // (possibly needs to import router-outlet or similar for setup) diff --git a/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.html b/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.html index 7c64154b..26f0e3c7 100644 --- a/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.html +++ b/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.html @@ -55,6 +55,20 @@

{{ content.title | translate }}

+ } + + @if(photoAlbum(); as album){ + + + perm_media + {{ 'Review' | translate }} + +
+ + +
+
+ }