diff --git a/build/Build.cs b/build/Build.cs index da20db2..587309e 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -94,16 +94,20 @@ class Build : NukeBuild .Executes(() => { WriteFileVersionProvider(); - - DotNetBuild(s => s - .SetProjectFile(Solution) - .SetConfiguration(Configuration) - .SetAssemblyVersion(GitVersion.AssemblySemVer) - .SetFileVersion(GitVersion.AssemblySemVer) - .SetInformationalVersion(GitVersion.InformationalVersion) - .EnableNoRestore()); + CompileBackend(); }); + private void CompileBackend() + { + DotNetBuild(s => s + .SetProjectFile(Solution) + .SetConfiguration(Configuration) + .SetAssemblyVersion(GitVersion.AssemblySemVer) + .SetFileVersion(GitVersion.AssemblySemVer) + .SetInformationalVersion(GitVersion.InformationalVersion) + .EnableNoRestore()); + } + private void WriteFileVersionProvider() { var fileVersionPath = RootDirectory / "src" / "IPA.BCFier" / "FileVersionProvider.cs"; @@ -505,6 +509,8 @@ await PublishRelease(x => x .DependsOn(Restore) .Executes(() => { + CompileBackend(); + var nSwagConfigPath = SourceDirectory / "ipa-bcfier-ui" / "src" / "nswag.json"; var nSwagToolPath = NuGetToolPathResolver.GetPackageExecutable("NSwag.MSBuild", "tools/Net80/dotnet-nswag.dll"); DotNetRun(x => x diff --git a/src/IPA.Bcfier.App/Controllers/SettingsController.cs b/src/IPA.Bcfier.App/Controllers/SettingsController.cs index 58068d3..fd1bf6d 100644 --- a/src/IPA.Bcfier.App/Controllers/SettingsController.cs +++ b/src/IPA.Bcfier.App/Controllers/SettingsController.cs @@ -84,5 +84,33 @@ public async Task SaveSettingsAsync([FromBody] Settings settings) await _settingsService.SaveSettingsAsync(settings); return NoContent(); } + + [HttpGet("always-on-top")] + [ProducesResponseType(typeof(bool), (int)HttpStatusCode.OK)] + public async Task GetIsAlwaysOnTopAsync() + { + var electronWindow = _electronWindowProvider.BrowserWindow; + if (electronWindow == null) + { + return BadRequest(); + } + + var isAlwaysOnTop = await electronWindow.IsAlwaysOnTopAsync(); + return Ok(isAlwaysOnTop); + } + + [HttpPut("always-on-top")] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public async Task SetIsAlwaysOnTopAsync(bool isAlwaysOnTop) + { + var electronWindow = _electronWindowProvider.BrowserWindow; + if (electronWindow == null) + { + return BadRequest(); + } + + electronWindow.SetAlwaysOnTop(isAlwaysOnTop); + return NoContent(); + } } } diff --git a/src/ipa-bcfier-ui/src/app/components/top-menu/top-menu.component.html b/src/ipa-bcfier-ui/src/app/components/top-menu/top-menu.component.html index f19c163..f1bf3e0 100644 --- a/src/ipa-bcfier-ui/src/app/components/top-menu/top-menu.component.html +++ b/src/ipa-bcfier-ui/src/app/components/top-menu/top-menu.component.html @@ -31,6 +31,12 @@ support Help + + Always On Top +
{{ (selectedProject$ | async)?.name | uppercase }} diff --git a/src/ipa-bcfier-ui/src/app/components/top-menu/top-menu.component.ts b/src/ipa-bcfier-ui/src/app/components/top-menu/top-menu.component.ts index 49634b3..f39fb3c 100644 --- a/src/ipa-bcfier-ui/src/app/components/top-menu/top-menu.component.ts +++ b/src/ipa-bcfier-ui/src/app/components/top-menu/top-menu.component.ts @@ -3,8 +3,9 @@ import { BcfFileWrapper, LastOpenedFileGet, LastOpenedFilesClient, + SettingsClient, } from '../../generated-client/generated-client'; -import { Component, OnDestroy, inject } from '@angular/core'; +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { Subject, @@ -19,8 +20,10 @@ import { import { BackendService } from '../../services/BackendService'; import { BcfFilesMessengerService } from '../../services/bcf-files-messenger.service'; +import { FormsModule } from '@angular/forms'; import { LastOpenedFilesComponent } from '../last-opened-files/last-opened-files.component'; import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; import { NotificationsService } from '../../services/notifications.service'; @@ -39,26 +42,77 @@ import { version } from '../../version'; AsyncPipe, MatMenuModule, LastOpenedFilesComponent, + MatCheckboxModule, + FormsModule, ], templateUrl: './top-menu.component.html', styleUrl: './top-menu.component.scss', }) -export class TopMenuComponent implements OnDestroy { +export class TopMenuComponent implements OnDestroy, OnInit { private destroyed$ = new Subject(); version = version.version; selectedProject$ = inject(SelectedProjectMessengerService).selectedProject; lastFileMenuOpened = false; lastOpenedFiles: LastOpenedFileGet[] = []; + alwaysOnTopRequestRunning = false; + private _alwaysOnTop = true; + set alwaysOnTop(value: boolean) { + this._alwaysOnTop = value; + this.changeAlwaysOnTopInBackend(); + } + get alwaysOnTop(): boolean { + return this._alwaysOnTop; + } + constructor( private backendService: BackendService, private notificationsService: NotificationsService, private bcfFilesMessengerService: BcfFilesMessengerService, private matDialog: MatDialog, - private lastOpenedFilesClient: LastOpenedFilesClient + private lastOpenedFilesClient: LastOpenedFilesClient, + private settingsClient: SettingsClient ) { this.checkOpenedFileAndSendInfo(); } + private changeAlwaysOnTopInBackend(): void { + const isAlwaysOnTop = this._alwaysOnTop; + this.alwaysOnTopRequestRunning = true; + + this.settingsClient.getIsAlwaysOnTop().subscribe({ + next: (serverIsAlwaysOnTop) => { + if (serverIsAlwaysOnTop === isAlwaysOnTop) { + this.alwaysOnTopRequestRunning = false; + return; + } + + this.settingsClient.setIsAlwaysOnTop(isAlwaysOnTop).subscribe({ + next: () => { + this.alwaysOnTopRequestRunning = false; + }, + error: () => { + this.notificationsService.error( + 'Could not change always on top setting.' + ); + this.alwaysOnTopRequestRunning = false; + }, + }); + }, + error: () => { + this.notificationsService.error( + 'Could not change always on top setting.' + ); + this.alwaysOnTopRequestRunning = false; + }, + }); + } + + ngOnInit(): void { + this.settingsClient.getIsAlwaysOnTop().subscribe((isAlwaysOnTop) => { + this._alwaysOnTop = isAlwaysOnTop; + }); + } + ngOnDestroy(): void { this.destroyed$.next(); this.destroyed$.complete(); diff --git a/src/ipa-bcfier-ui/src/app/generated-client/generated-client.ts b/src/ipa-bcfier-ui/src/app/generated-client/generated-client.ts index fbef5ff..e25b9ae 100644 --- a/src/ipa-bcfier-ui/src/app/generated-client/generated-client.ts +++ b/src/ipa-bcfier-ui/src/app/generated-client/generated-client.ts @@ -1012,6 +1012,8 @@ export interface ISettingsClient { getSettings(): Observable; saveSettings(settings: Settings): Observable; choseMainDatabaseLocation(): Observable; + getIsAlwaysOnTop(): Observable; + setIsAlwaysOnTop(isAlwaysOnTop: boolean | undefined): Observable; } @Injectable({ @@ -1165,6 +1167,101 @@ export class SettingsClient implements ISettingsClient { } return _observableOf(null as any); } + + getIsAlwaysOnTop(): Observable { + let url_ = this.baseUrl + "/api/settings/always-on-top"; + url_ = url_.replace(/[?&]$/, ""); + + let options_ : any = { + observe: "response", + responseType: "blob", + headers: new HttpHeaders({ + "Accept": "application/json" + }) + }; + + return this.http.request("get", url_, options_).pipe(_observableMergeMap((response_ : any) => { + return this.processGetIsAlwaysOnTop(response_); + })).pipe(_observableCatch((response_: any) => { + if (response_ instanceof HttpResponseBase) { + try { + return this.processGetIsAlwaysOnTop(response_ as any); + } catch (e) { + return _observableThrow(e) as any as Observable; + } + } else + return _observableThrow(response_) as any as Observable; + })); + } + + protected processGetIsAlwaysOnTop(response: HttpResponseBase): Observable { + const status = response.status; + const responseBlob = + response instanceof HttpResponse ? response.body : + (response as any).error instanceof Blob ? (response as any).error : undefined; + + let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }} + if (status === 200) { + return blobToText(responseBlob).pipe(_observableMergeMap((_responseText: string) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as boolean; + return _observableOf(result200); + })); + } else if (status !== 200 && status !== 204) { + return blobToText(responseBlob).pipe(_observableMergeMap((_responseText: string) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + })); + } + return _observableOf(null as any); + } + + setIsAlwaysOnTop(isAlwaysOnTop: boolean | undefined): Observable { + let url_ = this.baseUrl + "/api/settings/always-on-top?"; + if (isAlwaysOnTop === null) + throw new Error("The parameter 'isAlwaysOnTop' cannot be null."); + else if (isAlwaysOnTop !== undefined) + url_ += "isAlwaysOnTop=" + encodeURIComponent("" + isAlwaysOnTop) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_ : any = { + observe: "response", + responseType: "blob", + headers: new HttpHeaders({ + }) + }; + + return this.http.request("put", url_, options_).pipe(_observableMergeMap((response_ : any) => { + return this.processSetIsAlwaysOnTop(response_); + })).pipe(_observableCatch((response_: any) => { + if (response_ instanceof HttpResponseBase) { + try { + return this.processSetIsAlwaysOnTop(response_ as any); + } catch (e) { + return _observableThrow(e) as any as Observable; + } + } else + return _observableThrow(response_) as any as Observable; + })); + } + + protected processSetIsAlwaysOnTop(response: HttpResponseBase): Observable { + const status = response.status; + const responseBlob = + response instanceof HttpResponse ? response.body : + (response as any).error instanceof Blob ? (response as any).error : undefined; + + let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }} + if (status === 204) { + return blobToText(responseBlob).pipe(_observableMergeMap((_responseText: string) => { + return _observableOf(null as any); + })); + } else if (status !== 200 && status !== 204) { + return blobToText(responseBlob).pipe(_observableMergeMap((_responseText: string) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + })); + } + return _observableOf(null as any); + } } export interface ITeamsMessagesClient {