diff --git a/.docker/lt.dockerfile b/.docker/lt.dockerfile
new file mode 100644
index 0000000..44c1b1f
--- /dev/null
+++ b/.docker/lt.dockerfile
@@ -0,0 +1,3 @@
+ARG NODEJS_IMAGE
+FROM ${NODEJS_IMAGE}
+RUN npm i -g localtunnel@2.0.2 && npm cache clean --force
diff --git a/nginx.conf b/.docker/nginx.conf
similarity index 100%
rename from nginx.conf
rename to .docker/nginx.conf
diff --git a/Dockerfile b/.docker/web.dockerfile
similarity index 79%
rename from Dockerfile
rename to .docker/web.dockerfile
index cd4e7c7..6835e84 100644
--- a/Dockerfile
+++ b/.docker/web.dockerfile
@@ -1,4 +1,5 @@
-FROM node:20.11.0-slim as builder
+ARG NODEJS_IMAGE
+FROM ${NODEJS_IMAGE} as builder
ENV NODE_ENV=production
@@ -20,6 +21,4 @@ RUN npm run build:prod
FROM nginx:1.25.4-alpine
COPY --from=builder /app/dist/har-viewer/browser /usr/share/nginx/html
-COPY nginx.conf /etc/nginx/nginx.conf
-
-EXPOSE 80
+COPY .docker/nginx.conf /etc/nginx/nginx.conf
diff --git a/.env b/.env
index 536f20e..d474c3e 100644
--- a/.env
+++ b/.env
@@ -1 +1,2 @@
COMPOSE_PROJECT_NAME=har-viewer
+NODEJS_IMAGE=node:20.11.0-slim
diff --git a/README.md b/README.md
index 20052ab..514b572 100644
--- a/README.md
+++ b/README.md
@@ -3,3 +3,25 @@
> Возможно будет дорабатываться если ну прям надо
## [Открыть](https://sunriselink.github.io/har-viewer)
+
+## Запуск в Docker
+
+Для сборки и запуска проекта выполнить команду:
+
+```shell
+docker compose up -d --build web
+```
+
+Проект будет доступен по адресу `http://localhost:8080`.
+
+Если требуется проверить работу Service Worker'а, то надо подключиться к HTTPS туннелю. Для этого требуется запустить еще один контейнер:
+
+```shell
+docker compose up -d --build lt
+```
+
+Адрес туннеля можно достать из логов командой:
+
+```shell
+docker compose logs lt
+```
diff --git a/docker-compose.yml b/docker-compose.yml
index 732ee0d..8a7ec32 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,18 @@
version: "3"
services:
web:
- build: .
+ build:
+ context: .
+ dockerfile: .docker/web.dockerfile
+ args:
+ - NODEJS_IMAGE=${NODEJS_IMAGE}
ports:
- "8080:80"
+
+ lt:
+ build:
+ context: .docker
+ dockerfile: lt.dockerfile
+ args:
+ - NODEJS_IMAGE=${NODEJS_IMAGE}
+ command: lt --port 80 --local-host web
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 89ee774..b9e68b9 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,18 +1,3 @@
-
- @if (harLog(); as har) {
-
- } @else {
-
- }
-
+
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index 9230ca2..5d4e87f 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -1,25 +1,3 @@
:host {
display: block;
- height: 100%;
-}
-
-.upload {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
-
- &__tip {
- color: rgba(0, 0, 0, 0.5);
- text-align: center;
- margin-top: 16px;
- }
-}
-
-.drop {
- min-height: 100%;
-}
-
-.file-over {
- background-color: rgba(0, 0, 0, 0.35);
}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 09082f1..ad481cb 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,14 +1,6 @@
-import { ChangeDetectionStrategy, Component, inject, Signal, signal } from '@angular/core';
-import { toSignal } from '@angular/core/rxjs-interop';
-import { BehaviorSubject, Observable, of, switchMap } from 'rxjs';
-import { FileUploaderComponent } from './components/file-uploader/file-uploader.component';
-import { HarViewerComponent } from './components/har-viewer/har-viewer.component';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
import { VersionComponent } from './components/version/version.component';
-import { FileDropZoneDirective } from './directives/file-drop-zone.directive';
-import { HarReaderService } from './services/har-reader.service';
-import { IHAR } from './types/har-log';
-import { Unsafe } from './types/unsafe';
-import { catchAndLogError } from './utils/catch-and-log-error';
@Component({
selector: 'app-root',
@@ -16,30 +8,6 @@ import { catchAndLogError } from './utils/catch-and-log-error';
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [FileDropZoneDirective, FileUploaderComponent, VersionComponent, HarViewerComponent],
+ imports: [VersionComponent, RouterOutlet],
})
-export class AppComponent {
- protected readonly fileOver = signal(false);
-
- protected readonly harFile$ = new BehaviorSubject>(null);
- protected readonly harLog = this.createHARSignal();
-
- protected readonly harReader = inject(HarReaderService);
-
- protected loadHAR(file: File): void {
- this.harFile$.next(file);
- }
-
- private createHARSignal(): Signal> {
- const harLog$ = this.harFile$.pipe(switchMap(file => this.readFile(file)));
- return toSignal(harLog$);
- }
-
- private readFile(file: Unsafe): Observable> {
- if (!file) {
- return of(null);
- }
-
- return this.harReader.readHAR(file).pipe(catchAndLogError());
- }
-}
+export class AppComponent {}
diff --git a/src/app/app.config.ts b/src/app/app.config.ts
index 0d586c0..a285371 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -1,10 +1,26 @@
import { ApplicationConfig, isDevMode } from '@angular/core';
+import { provideRouter, Routes } from '@angular/router';
import { provideServiceWorker } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { APP_ENVIRONMENT_TOKEN } from '../environments/environment.interface';
+import { FileHandlerComponent } from './pages/file-handler/file-handler.component';
+import { HarPageComponent } from './pages/har-page/har-page.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ pathMatch: 'full',
+ component: HarPageComponent,
+ },
+ {
+ path: 'file_handler',
+ component: FileHandlerComponent,
+ },
+];
export const appConfig: ApplicationConfig = {
providers: [
+ provideRouter(routes),
{
provide: APP_ENVIRONMENT_TOKEN,
useValue: environment,
diff --git a/src/app/components/file-uploader/file-uploader.component.html b/src/app/components/file-uploader/file-uploader.component.html
index 348bdfb..64916c8 100644
--- a/src/app/components/file-uploader/file-uploader.component.html
+++ b/src/app/components/file-uploader/file-uploader.component.html
@@ -1,4 +1,4 @@
diff --git a/src/app/components/file-uploader/file-uploader.component.ts b/src/app/components/file-uploader/file-uploader.component.ts
index 0087f42..1f8c804 100644
--- a/src/app/components/file-uploader/file-uploader.component.ts
+++ b/src/app/components/file-uploader/file-uploader.component.ts
@@ -11,6 +11,9 @@ export class FileUploaderComponent {
@Input()
public buttonText = 'SELECT FILE';
+ @Input()
+ public accept?: string;
+
@Output()
public fileSelect = new EventEmitter();
diff --git a/src/app/pages/file-handler/file-handler.component.ts b/src/app/pages/file-handler/file-handler.component.ts
new file mode 100644
index 0000000..5c690d5
--- /dev/null
+++ b/src/app/pages/file-handler/file-handler.component.ts
@@ -0,0 +1,19 @@
+import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { LaunchQueueService } from '../../services/launch-queue.service';
+
+@Component({
+ selector: 'app-file-handler',
+ template: '',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class FileHandlerComponent implements OnInit {
+ private readonly launchQueueService = inject(LaunchQueueService);
+ private readonly router = inject(Router);
+
+ public ngOnInit(): void {
+ this.launchQueueService.tryCreateConsumer();
+ this.router.navigateByUrl('/', { replaceUrl: true });
+ }
+}
diff --git a/src/app/pages/har-page/har-page.component.html b/src/app/pages/har-page/har-page.component.html
new file mode 100644
index 0000000..8ae0927
--- /dev/null
+++ b/src/app/pages/har-page/har-page.component.html
@@ -0,0 +1,21 @@
+
+ @if (harLog(); as har) {
+
+ } @else {
+
+
+
+
Or drop file here
+
+ }
+
diff --git a/src/app/pages/har-page/har-page.component.scss b/src/app/pages/har-page/har-page.component.scss
new file mode 100644
index 0000000..9230ca2
--- /dev/null
+++ b/src/app/pages/har-page/har-page.component.scss
@@ -0,0 +1,25 @@
+:host {
+ display: block;
+ height: 100%;
+}
+
+.upload {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+
+ &__tip {
+ color: rgba(0, 0, 0, 0.5);
+ text-align: center;
+ margin-top: 16px;
+ }
+}
+
+.drop {
+ min-height: 100%;
+}
+
+.file-over {
+ background-color: rgba(0, 0, 0, 0.35);
+}
diff --git a/src/app/pages/har-page/har-page.component.ts b/src/app/pages/har-page/har-page.component.ts
new file mode 100644
index 0000000..f6f1a98
--- /dev/null
+++ b/src/app/pages/har-page/har-page.component.ts
@@ -0,0 +1,50 @@
+import { ChangeDetectionStrategy, Component, inject, OnInit, Signal, signal } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { BehaviorSubject, Observable, of, switchMap } from 'rxjs';
+import { FileUploaderComponent } from '../../components/file-uploader/file-uploader.component';
+import { HarViewerComponent } from '../../components/har-viewer/har-viewer.component';
+import { FileDropZoneDirective } from '../../directives/file-drop-zone.directive';
+import { HarReaderService } from '../../services/har-reader.service';
+import { LaunchQueueService } from '../../services/launch-queue.service';
+import { IHAR } from '../../types/har-log';
+import { Unsafe } from '../../types/unsafe';
+import { catchAndLogError } from '../../utils/catch-and-log-error';
+
+@Component({
+ selector: 'app-har-page',
+ standalone: true,
+ templateUrl: './har-page.component.html',
+ styleUrl: './har-page.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [FileDropZoneDirective, FileUploaderComponent, HarViewerComponent],
+})
+export class HarPageComponent implements OnInit {
+ protected readonly fileOver = signal(false);
+
+ protected readonly harFile$ = new BehaviorSubject>(null);
+ protected readonly harLog = this.createHARSignal();
+
+ private readonly harReader = inject(HarReaderService);
+ private readonly launchQueueService = inject(LaunchQueueService);
+
+ public ngOnInit(): void {
+ this.launchQueueService.handleFile().subscribe(file => this.loadHAR(file));
+ }
+
+ protected loadHAR(file: File): void {
+ this.harFile$.next(file);
+ }
+
+ private createHARSignal(): Signal> {
+ const harLog$ = this.harFile$.pipe(switchMap(file => this.readFile(file)));
+ return toSignal(harLog$);
+ }
+
+ private readFile(file: Unsafe): Observable> {
+ if (!file) {
+ return of(null);
+ }
+
+ return this.harReader.readHAR(file).pipe(catchAndLogError());
+ }
+}
diff --git a/src/app/services/launch-queue.service.ts b/src/app/services/launch-queue.service.ts
new file mode 100644
index 0000000..6ac5953
--- /dev/null
+++ b/src/app/services/launch-queue.service.ts
@@ -0,0 +1,50 @@
+import { Injectable } from '@angular/core';
+import { EMPTY, Observable, ReplaySubject } from 'rxjs';
+
+@Injectable({ providedIn: 'root' })
+export class LaunchQueueService {
+ private readonly file$ = new ReplaySubject(1);
+
+ private hasConsumer = false;
+
+ public tryCreateConsumer(): void {
+ if (!this.isSupported()) {
+ console.warn('LaunchQueue is not supported');
+ return;
+ }
+
+ this.createConsumer();
+ }
+
+ public handleFile(): Observable {
+ return this.hasConsumer ? this.file$.asObservable() : EMPTY;
+ }
+
+ private isSupported(): boolean {
+ return !!window.launchQueue;
+ }
+
+ private createConsumer(): void {
+ this.hasConsumer = true;
+
+ window.launchQueue.setConsumer(async params => {
+ if (params.files?.length) {
+ const file = await params.files[0].getFile();
+
+ this.file$.next(file);
+ this.file$.complete();
+ }
+ });
+ }
+}
+
+declare global {
+ interface Window {
+ launchQueue: {
+ setConsumer: (consumer: (params: LaunchParams) => void) => void;
+ };
+ }
+ interface LaunchParams {
+ readonly files?: FileSystemFileHandle[];
+ }
+}
diff --git a/src/manifest.webmanifest b/src/manifest.webmanifest
index 266ecc5..f84cb30 100644
--- a/src/manifest.webmanifest
+++ b/src/manifest.webmanifest
@@ -6,6 +6,14 @@
"display": "standalone",
"scope": "./",
"start_url": "./",
+ "file_handlers": [
+ {
+ "action": "/file_handler",
+ "accept": {
+ "application/json": [".har"]
+ }
+ }
+ ],
"icons": [
{
"src": "assets/icons/icon-72x72.png",