diff --git a/angular.json b/angular.json index a38c206..0365a7b 100644 --- a/angular.json +++ b/angular.json @@ -47,8 +47,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumWarning": "1mb", + "maximumError": "3mb" }, { "type": "anyComponentStyle", diff --git a/src/_shared/mobians-image.interface.ts b/src/_shared/mobians-image.interface.ts index 577963d..673139b 100644 --- a/src/_shared/mobians-image.interface.ts +++ b/src/_shared/mobians-image.interface.ts @@ -6,5 +6,8 @@ export interface MobiansImage { base64: string; UUID: string; rating?: boolean; + timestamp?: Date; + promptSummary?: string; + thumbnailUrl?: string; // Add this line } \ No newline at end of file diff --git a/src/app/home/image-grid/image-grid.component.html b/src/app/home/image-grid/image-grid.component.html index e5de2c1..0178a23 100644 --- a/src/app/home/image-grid/image-grid.component.html +++ b/src/app/home/image-grid/image-grid.component.html @@ -32,7 +32,7 @@ Image4 goes here
- Reference Image goes here + Reference Image goes here
diff --git a/src/app/home/image-grid/image-grid.component.ts b/src/app/home/image-grid/image-grid.component.ts index e589a2c..dfa9382 100644 --- a/src/app/home/image-grid/image-grid.component.ts +++ b/src/app/home/image-grid/image-grid.component.ts @@ -94,6 +94,7 @@ export class ImageGridComponent { if (image) { console.log('Reference Image changed:', image); this.showReferenceImage = true; + this.showInstructions = false; } else{ console.log('Reference Image removed'); diff --git a/src/app/home/options/options.component.css b/src/app/home/options/options.component.css index bbf461d..81006c4 100644 --- a/src/app/home/options/options.component.css +++ b/src/app/home/options/options.component.css @@ -31,6 +31,72 @@ margin-left: 10px; } +.history-panel { + margin-top: 20px; + margin-left: 10px; +} + +.history-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + grid-gap: 10px; + margin-top: 10px; +} + +.history-item { + cursor: pointer; +} + +.history-item img { + width: 100%; + height: auto; + border-radius: 4px; +} + +.history-details { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 5px; +} + +.sorting-options { + display: flex; + flex-wrap: nowrap; + align-items: center; + gap: 10px; + padding-right: 10px; +} + +.sort-dropdown { + display: flex; + align-items: center; + gap: 5px; + flex-shrink: 0; +} + +.search-bar { + flex: 1; + min-width: 0; + max-width: 200px; +} + +.summary-text { + max-height: 40px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + text-align: center; +} + +.no-images-message { + text-align: center; + padding: 20px; + color: #888; +} + @media (max-width: 480px) { .input-button-container { display: flex; @@ -69,4 +135,8 @@ #advanced-options { width: 97vw; } + + .p-inputtext{ + padding-right: 0px; + } } \ No newline at end of file diff --git a/src/app/home/options/options.component.html b/src/app/home/options/options.component.html index 316c792..ccada39 100644 --- a/src/app/home/options/options.component.html +++ b/src/app/home/options/options.component.html @@ -62,26 +62,67 @@ [(ngModel)]="generationRequest.strength" /> + + + + +
- - - +
+
+
+
+ + +
+ +
+ +
+
+ Generated Image +
+ {{ image.timestamp | date:'short' }} + {{ image.promptSummary }} +
+
+
+ +
+ No generated images available. +
+ +
+
- - -
+ [(ngModel)]="generationRequest.fast_pass_code" (input)="onFastPassCodeChange($event)" />
@@ -199,10 +240,5 @@
- - + \ No newline at end of file diff --git a/src/app/home/options/options.component.ts b/src/app/home/options/options.component.ts index 908948f..d952110 100644 --- a/src/app/home/options/options.component.ts +++ b/src/app/home/options/options.component.ts @@ -22,6 +22,8 @@ import { SwPush } from '@angular/service-worker'; export class OptionsComponent { private subscription!: Subscription; private referenceImageSubscription!: Subscription; + private dbName = 'ImageDatabase'; + private storeName = 'ImageStore'; models = ["sonicDiffusionV4", "fluffySonic"] enableGenerationButton: boolean = true; @@ -58,6 +60,23 @@ export class OptionsComponent { currentSeed?: number; supporter: boolean = false; serverMember: boolean = false; + userGeneratedImages: MobiansImage[] = []; + + // Pagination + paginatedImages: MobiansImage[] = []; + currentPage = 1; + imagesPerPage = 6; // Display 8 images per page (2 rows of 4 images each) + totalPages = 1; + + // Sorting + selectedSortOption: string = 'timestamp'; + sortOrder: 'asc' | 'desc' = 'desc'; + searchQuery: string = ''; + filteredImages: MobiansImage[] = []; + dropdownOptions: { label: string, value: string }[] = [ + { label: 'Date', value: 'timestamp' }, + { label: 'Alphabetical', value: 'promptSummary' } + ]; @Input() inpaintMask?: string; @@ -78,12 +97,52 @@ export class OptionsComponent { , private swPush: SwPush ) { } + private openDatabase(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, 5); + + request.onerror = (event) => { + reject(new Error('Failed to open database')); + }; + + request.onsuccess = (event) => { + const db = request.result; + resolve(db); + }; + + request.onupgradeneeded = (event) => { + const db = request.result; + // Check if the object store exists before trying to delete it + if (db.objectStoreNames.contains('yourObjectStoreName')) { + db.deleteObjectStore('yourObjectStoreName'); + console.log('Object store deleted') + console.log(this.storeName) + } + + // Now, create the object store as needed + db.createObjectStore(this.storeName, { keyPath: 'UUID' }); // Recreate the object store with the correct key path + console.log('Object store created') + console.log(this.storeName) + }; + }); + } + ngOnInit() { this.subscription = this.sharedService.getPrompt().subscribe(value => { this.generationRequest.prompt = value; }); + + this.openDatabase().then(() => { + // Database and object store created successfully + // Perform any necessary operations + console.log('Database and object store created successfully') + this.loadSettings(); + }).catch((error) => { + console.error('Failed to open database:', error); + // Handle the error appropriately + }); + this.sharedService.setGenerationRequest(this.generationRequest); - this.loadSettings(); this.updateSharedPrompt(); this.referenceImageSubscription = this.sharedService.getReferenceImage().subscribe(image => { @@ -219,11 +278,15 @@ export class OptionsComponent { // Save aspect ratio localStorage.setItem("aspect-ratio", this.aspectRatio.aspectRatio); - if (this.generationRequest.fast_pass_code == undefined || this.generationRequest.fast_pass_code == "") { + + // Save fast pass code + if (this.generationRequest.fast_pass_code == undefined || this.generationRequest.fast_pass_code.trim() == "") { this.generationRequest.fast_pass_code = undefined; localStorage.removeItem("fast-pass-code"); } - else if (this.generationRequest.fast_pass_code != undefined && this.generationRequest.fast_pass_code != "") { + else if (this.generationRequest.fast_pass_code != undefined && this.generationRequest.fast_pass_code.trim() != "") { + // Convert fast_pass_code to lowercase and remove whitespace + this.generationRequest.fast_pass_code = this.generationRequest.fast_pass_code.toLowerCase().replace(/\s/g, ''); localStorage.setItem("fast-pass-code", this.generationRequest.fast_pass_code); } @@ -232,7 +295,7 @@ export class OptionsComponent { } // Load session storage info of changed settings - loadSettings() { + async loadSettings() { if (localStorage.getItem("prompt-input") != null) { this.generationRequest.prompt = localStorage.getItem("prompt-input")!; } @@ -257,6 +320,45 @@ export class OptionsComponent { if (localStorage.getItem("model") != null) { this.generationRequest.model = localStorage.getItem("model")!; } + + try { + const db = await this.openDatabase(); + const transaction = db.transaction(this.storeName, 'readonly'); + const store = transaction.objectStore(this.storeName); + + const request = store.getAll(); + + request.onsuccess = (event) => { + this.userGeneratedImages = request.result; + + // Sort the userGeneratedImages array based on the timestamp in descending order + this.userGeneratedImages.sort((a, b) => { + const timestampA = a.timestamp ? a.timestamp.getTime() : 0; + const timestampB = b.timestamp ? b.timestamp.getTime() : 0; + return timestampB - timestampA; + }); + + // Calculate the total number of pages + this.totalPages = Math.ceil(this.userGeneratedImages.length / this.imagesPerPage); + + // Display the first page of images + this.searchImages(); + this.paginateImages(); + }; + + request.onerror = (event) => { + console.error('Failed to retrieve images from IndexedDB', event); + }; + + // Wait for the transaction to complete before proceeding + await new Promise((resolve) => { + transaction.oncomplete = () => { + resolve(undefined); + }; + }); + } catch (error) { + console.error('Failed to open database', error); + } } // Reset session storage info of changed settings and reset view @@ -345,9 +447,9 @@ export class OptionsComponent { "API_IP": API_URL } - // Create an interval which fires every 3 seconds + // Create an interval which fires every 1 second let subscription: Subscription; // Declare a variable to hold the subscription - subscription = interval(3000) + subscription = interval(1000) .pipe( // For each tick of the interval, call the service concatMap(() => this.stableDiffusionService.getJob(getJobInfo).pipe( @@ -372,30 +474,73 @@ export class OptionsComponent { // Only continue the stream while the job is incomplete takeWhile(response => !(jobComplete = (response.status === 'completed')), true), // Once the stream completes, do any cleanup if necessary - finalize(() => { + finalize(async () => { if (this.enableNotifications) { this.notificationService.playDing(); this.notificationService.sendPushNotification(); } if (jobComplete && lastResponse) { console.log(lastResponse); - this.images = lastResponse.result.map((base64String: string) => { + const generatedImages = lastResponse.result.map((base64String: string) => { return { base64: base64String, - // Fill in other properties as needed - width: this.generationRequest.width, // Example value, update as needed - height: this.generationRequest.height, // Example value, update as needed - aspectRatio: this.aspectRatio.aspectRatio, // Example value, update as needed - UUID: uuidv4(), // Example value, update as needed - rated: false, // Example value, update as needed + width: this.generationRequest.width, + height: this.generationRequest.height, + aspectRatio: this.aspectRatio.aspectRatio, + UUID: uuidv4(), + rated: false, + timestamp: new Date(), + promptSummary: this.generationRequest.prompt.slice(0, 50) + '...', // Truncate prompt summary + url: 'data:image/png;base64,' + base64String // Generate URL }; }); + this.images = generatedImages; this.sharedService.setImages(this.images); + + // Add the generated images to userGeneratedImages array + this.userGeneratedImages.push(...generatedImages); + + // Sort the userGeneratedImages array based on the timestamp in descending order + this.userGeneratedImages.sort((a, b) => { + const timestampA = a.timestamp ? a.timestamp.getTime() : 0; + const timestampB = b.timestamp ? b.timestamp.getTime() : 0; + return timestampB - timestampA; + }); + + // Calculate the total number of pages + this.totalPages = Math.ceil(this.userGeneratedImages.length / this.imagesPerPage); + + // Display the current page of images + this.paginateImages(); + + // update the view + this.sortImages(); + + try { + const db = await this.openDatabase(); + const transaction = db.transaction(this.storeName, 'readwrite'); + const store = transaction.objectStore(this.storeName); + + for (const image of generatedImages) { + console.log(image); + store.put(image); + } + + transaction.oncomplete = () => { + console.log('Images stored in IndexedDB'); + }; + + transaction.onerror = (event) => { + console.error('Failed to store images in IndexedDB', event); + }; + } catch (error) { + console.error('Failed to open database', error); + } + this.loadingChange.emit(false); this.enableGenerationButton = true; } }) - ) .subscribe( response => { @@ -412,8 +557,8 @@ export class OptionsComponent { } else { // This will be called every 3 seconds, so we do nothing here - console.log("queue position: " + response.queue_position); - this.queuePositionChange.emit(response.queue_position); + console.log("queue position: " + response.queue_position ?? 0); + this.queuePositionChange.emit(response.queue_position ?? 0); } }, error => { @@ -468,8 +613,95 @@ export class OptionsComponent { this.imageModalOpen.emit(true); } + openImageDetails(image: MobiansImage) { + // Implement the logic to open the image details modal or view + console.log('Opening image details for:', image); + + // Set the reference image to the selected image + this.referenceImage = image; + this.generationRequest.image = image.base64; + this.sharedService.setReferenceImage(image); + } + + toggleOptions() { + const historyCollapse = document.getElementById('historyCollapse'); + if (historyCollapse) { + historyCollapse.classList.remove('show'); + } + } + + toggleHistory() { + const optionsCollapse = document.getElementById('collapseExample'); + if (optionsCollapse) { + optionsCollapse.classList.remove('show'); + } + } + + searchImages() { + if (this.searchQuery.trim() === '') { + this.filteredImages = [...this.userGeneratedImages]; + } else { + const lowercaseQuery = this.searchQuery.toLowerCase(); + this.filteredImages = this.userGeneratedImages.filter(image => + image.promptSummary && image.promptSummary.toLowerCase().includes(lowercaseQuery) + ); + } + this.currentPage = 1; + this.totalPages = Math.ceil(this.filteredImages.length / this.imagesPerPage); + this.paginateImages(); + } + + paginateImages() { + const startIndex = (this.currentPage - 1) * this.imagesPerPage; + const endIndex = startIndex + this.imagesPerPage; + this.paginatedImages = this.filteredImages.slice(startIndex, endIndex); + } + + previousPage() { + if (this.currentPage > 1) { + this.currentPage--; + this.paginateImages(); + } + } + + nextPage() { + if (this.currentPage < this.totalPages) { + this.currentPage++; + this.paginateImages(); + } + } + + sortImages() { + if (this.selectedSortOption === 'timestamp') { + this.userGeneratedImages.sort((a, b) => { + const timestampA = a.timestamp ? a.timestamp.getTime() : 0; + const timestampB = b.timestamp ? b.timestamp.getTime() : 0; + return this.sortOrder === 'asc' ? timestampA - timestampB : timestampB - timestampA; + }); + } else if (this.selectedSortOption === 'promptSummary') { + this.userGeneratedImages.sort((a, b) => { + const summaryA = a.promptSummary ? a.promptSummary.toLowerCase() : ''; + const summaryB = b.promptSummary ? b.promptSummary.toLowerCase() : ''; + return this.sortOrder === 'asc' ? summaryA.localeCompare(summaryB) : summaryB.localeCompare(summaryA); + }); + } + // Add more sorting options as needed + + this.paginateImages(); + this.searchImages(); + } + + reverseSortOrder() { + this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc'; + this.sortImages(); + } + // Example function called after successful Discord login onDiscordLoginSuccess(userData: any) { localStorage.setItem('discordUserData', JSON.stringify(userData)); } + + onFastPassCodeChange(event: any) { + this.generationRequest.fast_pass_code = event.target.value.toLowerCase().replace(/\s/g, ''); + } } diff --git a/src/modules/home.module.ts b/src/modules/home.module.ts index 78ab849..2c998a9 100644 --- a/src/modules/home.module.ts +++ b/src/modules/home.module.ts @@ -19,6 +19,8 @@ import { ButtonModule } from 'primeng/button'; import { ColorPickerModule } from 'primeng/colorpicker'; import { ToggleButtonModule } from 'primeng/togglebutton'; import { OverlayPanelModule } from 'primeng/overlaypanel'; +import { InputTextModule } from 'primeng/inputtext'; +import { DropdownModule } from 'primeng/dropdown'; @NgModule({ declarations: [ @@ -41,6 +43,8 @@ import { OverlayPanelModule } from 'primeng/overlaypanel'; ColorPickerModule, ToggleButtonModule, OverlayPanelModule, + InputTextModule, + DropdownModule, ], exports: [ ImageGridComponent, @@ -50,6 +54,8 @@ import { OverlayPanelModule } from 'primeng/overlaypanel'; GligenDisplayComponent, InpaintingDisplayComponent, OverlayPanelModule, + InputTextModule, + DropdownModule, ], providers: [MessageService] })