updates
Some checks failed
Node.js CI / build (20.x) (push) Has been cancelled

This commit is contained in:
Flo 2024-08-04 13:18:06 +00:00
parent d3fbda5e74
commit 42312cf409
62 changed files with 1838 additions and 724 deletions

View File

@ -8,11 +8,11 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
#MAC
if [[ "$OSTYPE" == "darwin"* ]]; then
docker-compose -f "${SCRIPT_DIR}/../../docker/docker-compose-mac.yml" $COMMAND
docker compose -f "${SCRIPT_DIR}/../../docker/docker-compose-mac.yml" $COMMAND
#LINUX
elif [[ "$OSTYPE" == "linux-gnu" ]]; then
docker-compose -f "${SCRIPT_DIR}/../../docker/docker-compose.yml" $COMMAND
docker compose -f "${SCRIPT_DIR}/../../docker/docker-compose.yml" $COMMAND
else
echo "Dieses Skript wird auf deinem Gerät nicht unterstützt"

693
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,7 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"tailwindcss": "^3.4.3",
"typescript": "~4.9.4"
}
}

View File

@ -1 +1,2 @@
<app-navigation></app-navigation>
<router-outlet></router-outlet>

View File

@ -6,18 +6,26 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from './shared/shared.module';
import { NavigationComponent } from './component/navigation/navigation.component';
import { HomeComponent } from './component/home/home.component';
const routes: Routes = [
{ path: 'video', loadChildren: () => import('./feature/video/video.module').then( m => m.VideoModule ) },
{ path: 'tag', loadChildren: () => import('./feature/tag/tag.module').then( m => m.TagModule ) },
{ path: '', redirectTo: 'video/list', pathMatch: 'full' }
{ path: 'home', component: HomeComponent },
{
path: 'video',
loadChildren: () =>
import('./feature/video/video.module').then((m) => m.VideoModule),
},
{
path: 'tag',
loadChildren: () =>
import('./feature/tag/tag.module').then((m) => m.TagModule),
},
{ path: '', redirectTo: 'home', pathMatch: 'full' },
];
@NgModule({
declarations: [
AppComponent,
],
declarations: [AppComponent, NavigationComponent, HomeComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
@ -26,6 +34,6 @@ const routes: Routes = [
SharedModule,
],
providers: [],
bootstrap: [AppComponent]
bootstrap: [AppComponent],
})
export class AppModule { }
export class AppModule {}

View File

@ -0,0 +1,5 @@
<shared-video-list
(onReadList)="readList($event)"
[total]="total"
[videos]="videos"
/>

View File

@ -0,0 +1,31 @@
import { Component } from '@angular/core';
import { VideoListEntry } from 'src/app/model/VideoListEntry';
import { RequestService } from 'src/app/request.service';
import { OnReadListModel } from 'src/app/shared/components/video-list/video-list.component';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
})
export class HomeComponent {
total: number = 0;
videos: VideoListEntry[] = [];
constructor(public requestService: RequestService) {}
readList(model: OnReadListModel): void {
this.requestService.post(
'video-list/read-list',
{
query: model.query,
page: model.page,
perPage: model.perPage,
},
(response: any) => {
this.videos = response.items;
this.total = response.total;
}
);
}
}

View File

@ -0,0 +1,15 @@
<a routerLink="/home">
<h1 class="w-full bg-zinc-700 p-5 text-white text-6xl font-bold text-center">
MyTube
</h1>
</a>
<ul
class="flex flex-wrap items-center justify-center text-xl font-bold bg-zinc-700 text-white"
>
<li *ngFor="let link of links" class="p-5 me-4">
<a [routerLink]="[link.routerLink]">
{{ link.caption }}
</a>
</li>
</ul>

View File

@ -0,0 +1,28 @@
import { Component } from '@angular/core';
interface Link {
routerLink: string;
caption: string;
}
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss'],
})
export class NavigationComponent {
links: Link[] = [
{
routerLink: '/video/upload',
caption: 'Upload',
},
{
routerLink: '/video/list',
caption: 'Videos',
},
{
routerLink: '/tag/list',
caption: 'Tags',
},
];
}

View File

@ -1 +1,11 @@
<p>list works!</p>
<div class="p-5">
<div class="mb-5">
<input
type="text"
class="block p-1 w-full text-gray-900 text-black dark:text-white bg-gray-50 dark:bg-zinc-700 rounded-lg bg-gray-100 text-base border border-gray-500 dark:border-transparent"
(input)="onInputChanged($event)"
/>
</div>
<shared-table [items]="tags" [columns]="tagListColumns" />
</div>

View File

@ -1,10 +1,61 @@
import { Component } from '@angular/core';
import { Subject, debounceTime } from 'rxjs';
import { TagListEntry } from 'src/app/model/TagListEntry';
import { RequestService } from 'src/app/request.service';
import { ColumnDefinition } from 'src/app/shared/components/table/table.component';
@Component({
selector: 'app-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.scss']
styleUrls: ['./list.component.scss'],
})
export class ListComponent {
query$ = new Subject<string>();
query: string = '';
tags: TagListEntry[] = [];
tagListColumns: ColumnDefinition[] = [
{
header: 'Tag',
columnFunction: (item: TagListEntry) => {
return item.description;
},
routerLink: (item: TagListEntry) => {
return ['/tag', item.id];
},
},
{
header: 'Anzahl Videos',
columnFunction: (item: TagListEntry) => {
return item.videoCount;
},
columnCss: 'text-center',
},
];
constructor(public requestService: RequestService) {
this.query$.pipe(debounceTime(300)).subscribe((query) => {
this.query = query;
this.readList();
});
}
ngOnInit(): void {
this.readList();
}
onInputChanged(event: Event): void {
this.query$.next((event.target as HTMLTextAreaElement).value);
}
readList(): void {
this.requestService.post(
'tag-list/read-list',
{
query: this.query,
},
(response: any) => {
this.tags = response.items;
}
);
}
}

View File

@ -1 +1,17 @@
<p>tag works!</p>
<div *ngIf="tagDetails !== null">
<h1 class="text-white text-2xl font-bold text-center">
{{ tagDetails.description }}
</h1>
<ul class="text-white">
<li *ngFor="let alias of tagDetails.aliases">
{{ alias.description }}
</li>
</ul>
</div>
<shared-video-list
(onReadList)="readList($event)"
[total]="total"
[videos]="videos"
/>

View File

@ -1,10 +1,63 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { TagDetails } from 'src/app/model/TagDetails';
import { VideoListEntry } from 'src/app/model/VideoListEntry';
import { RequestService } from 'src/app/request.service';
import { OnReadListModel } from 'src/app/shared/components/video-list/video-list.component';
@Component({
selector: 'app-tag',
templateUrl: './tag.component.html',
styleUrls: ['./tag.component.scss']
styleUrls: ['./tag.component.scss'],
})
export class TagComponent {
tagId: string | undefined;
tagDetails: TagDetails | null = null;
total: number = 0;
videos: VideoListEntry[] = [];
constructor(
private route: ActivatedRoute,
public requestService: RequestService
) {
this.route.params.subscribe((params) => this.updateTagId(params['id']));
}
updateTagId(tagId: string) {
this.tagId = tagId;
this.readDetails();
this.readList({ page: 1, query: '', perPage: 36 });
}
readDetails(): void {
if (this.tagId === undefined) return;
this.requestService.post(
'tag/read-details',
{
tagId: this.tagId,
},
(response: any) => {
this.tagDetails = response;
}
);
}
readList(model: OnReadListModel): void {
if (this.tagId === undefined) return;
this.requestService.post(
'tag/read-video-list',
{
tagId: this.tagId,
query: model.query,
page: model.page,
perPage: model.perPage,
},
(response: any) => {
this.videos = response.items;
this.total = response.total;
}
);
}
}

View File

@ -2,27 +2,16 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TagComponent } from './tag.component';
import { ListComponent } from './list/list.component';
import { VideoListComponent } from './video-list/video-list.component';
import { SharedModule } from 'src/app/shared/shared.module';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{ path: ':id/list', component: ListComponent },
{ path: ':id/video-list', component: VideoListComponent },
{ path: 'list', component: ListComponent },
{ path: ':id', component: TagComponent },
{ path: '', redirectTo: 'tag/list', pathMatch: 'full'},
]
];
@NgModule({
declarations: [
TagComponent,
ListComponent,
VideoListComponent
],
imports: [
CommonModule,
SharedModule,
RouterModule.forChild(routes)
]
declarations: [TagComponent, ListComponent],
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
})
export class TagModule { }
export class TagModule {}

View File

@ -1,43 +0,0 @@
<div class="row">
<div class="col-sm-2">
<button [routerLink]="['/video/list']">Videos</button>
</div>
<div class="col-sm-10">
<mat-form-field>
<input matInput type="text" (input)="onInputChanged($event)" />
</mat-form-field>
</div>
</div>
<div class="row" *ngIf="tagDetails !== null">
<h1>{{tagDetails.description}}</h1>
</div>
<div class="row">
<div class="cardContainer">
<mat-card
class="mat-card"
*ngFor="let video of videos"
(click)="selectVideo(video)">
<mat-card-header>
<mat-card-title class="mat-card-title">{{video.title}}</mat-card-title>
<mat-card-subtitle>
<a *ngFor="let tag of video.tags"
[routerLink]="['/tag', tag.id, 'video-list']">
{{tag.description}}
</a>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="mat-card-content">
<img [src]="getThumbnailUrl(video)" alt="Thumbnail">
</mat-card-content>
</mat-card>
</div>
</div>
<mat-paginator [length]="total"
[pageSize]="perPage"
[pageSizeOptions]="[25, 50, 75]"
[hidePageSize]="true"
(page)="onPageEvent($event)">
</mat-paginator>

View File

@ -1,65 +0,0 @@
button {
margin: 1em;
width: 100%;
}
mat-form-field {
margin: 1em;
width: 100%;
}
mat-card {
margin: 1em;
}
mat-card:hover {
background-color: lightgray;
}
.mat-card-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cardContainer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.mat-card {
flex-basis: calc(100% - 20px);
max-width: calc(100% - 20px);
flex: 0 0 calc(100% - 20px);
margin: 10px;
box-sizing: border-box;
}
/* For tablet: */
@media only screen and (min-width: 700px) {
.mat-card {
flex-basis: calc(50% - 20px);
max-width: calc(50% - 20px);
}
}
/* For desktop: */
@media only screen and (min-width: 1200px) {
.mat-card {
max-width: calc(33.33% - 20px);
flex-basis: calc(33.33% - 20px);
}
}
.mat-card .mat-card-content {
height: 100%;
}
.mat-card .mat-card-content img {
width: 100%;
height: 100%;
object-fit: cover;
}

View File

@ -1,94 +0,0 @@
import { Component } from '@angular/core';
import { PageEvent } from '@angular/material/paginator';
import { ActivatedRoute, Router } from '@angular/router';
import { Subject, debounceTime } from 'rxjs';
import { TagDetails } from 'src/app/model/TagDetails';
import { VideoListEntry } from 'src/app/model/VideoListEntry';
import { RequestService } from 'src/app/request.service';
@Component({
selector: 'app-video-list',
templateUrl: './video-list.component.html',
styleUrls: ['./video-list.component.scss']
})
export class VideoListComponent {
tagId: string;
tagDetails: TagDetails|null = null;
query$ = new Subject<string>();
query: string = "";
total: number = 0;
page: number = 1;
perPage: number = 25;
videos: VideoListEntry[] = [];
selectedVideoUrl: string|null = null;
constructor(
private route: ActivatedRoute,
private router: Router,
public requestService : RequestService,
) {
this.tagId = this.route.snapshot.paramMap.get('id') ?? '';
this.query$.pipe(
debounceTime(300)
).subscribe( query => {
this.query = query;
this.page = 1;
this.readList();
} )
}
ngOnInit(): void {
this.readDetails();
this.readList();
}
onPageEvent(event: PageEvent) {
this.page = event.pageIndex + 1;
this.perPage = event.pageSize;
this.readList();
}
onInputChanged(event: Event): void {
this.query$.next((event.target as HTMLTextAreaElement).value);
}
selectVideo(entry: VideoListEntry) {
this.router.navigate(['/video', entry.id]);
}
readDetails(): void {
this.requestService.post(
'tag/read-details',
{
tagId: this.tagId
},
(response:any) => {
this.tagDetails = response;
}
);
}
readList(): void {
this.requestService.post(
'tag/read-video-list',
{
tagId: this.tagId,
query: this.query,
page: this.page,
perPage: this.perPage,
},
(response:any) => {
this.videos = response.items;
this.total = response.total;
}
);
}
getThumbnailUrl(entry: VideoListEntry): string {
return 'http://wsl-flo/api/video/thumbnail/' + entry.id;
}
}

View File

@ -1,38 +1,26 @@
<div class="row">
<div class="col-sm-10">
<mat-form-field>
<input matInput type="text" (input)="onInputChanged($event)" />
</mat-form-field>
<div class="p-5">
<div class="mb-5 flex flex-row">
<input
type="text"
class="basis-full p-1 w-full text-gray-900 text-black dark:text-white bg-gray-50 dark:bg-zinc-700 rounded-lg bg-gray-100 text-base border border-gray-500 dark:border-transparent"
(input)="onInputChanged($event)"
/>
<input
type="checkbox"
class="basis-auto block p-1 w-20 text-gray-900 text-black dark:text-white bg-gray-50 dark:bg-zinc-700 rounded-lg bg-gray-100 text-base border border-gray-500 dark:border-transparent"
(change)="readList()"
[(ngModel)]="onlyTagless"
/>
</div>
<div class="col-sm-2">
<button [routerLink]="['/video/upload']">Upload</button>
<shared-table [items]="videos" [columns]="videoListColumns" />
<div class="mt-5">
<shared-paginator
(pageChange)="onPageChanged($event)"
[page]="page"
[perPage]="perPage"
[total]="total"
/>
</div>
</div>
<div class="row">
<div class="cardContainer">
<mat-card
class="mat-card"
*ngFor="let video of videos">
<mat-card-header>
<mat-card-title class="mat-card-title">{{video.title}}</mat-card-title>
<mat-card-subtitle>
<a *ngFor="let tag of video.tags"
[routerLink]="['/tag', tag.id, 'video-list']">
{{tag.description}}
</a>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="mat-card-content">
<img [src]="getThumbnailUrl(video)" alt="Thumbnail" (click)="selectVideo(video)">
</mat-card-content>
</mat-card>
</div>
</div>
<mat-paginator [length]="total"
[pageSize]="perPage"
[pageSizeOptions]="[25, 50, 75]"
[hidePageSize]="true"
(page)="onPageEvent($event)">
</mat-paginator>

View File

@ -1,65 +0,0 @@
button {
margin: 1em;
width: 100%;
}
mat-form-field {
margin: 1em;
width: 100%;
}
mat-card {
margin: 1em;
}
mat-card:hover {
background-color: lightgray;
}
.mat-card-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cardContainer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.mat-card {
flex-basis: calc(100% - 20px);
max-width: calc(100% - 20px);
flex: 0 0 calc(100% - 20px);
margin: 10px;
box-sizing: border-box;
}
/* For tablet: */
@media only screen and (min-width: 700px) {
.mat-card {
flex-basis: calc(50% - 20px);
max-width: calc(50% - 20px);
}
}
/* For desktop: */
@media only screen and (min-width: 1200px) {
.mat-card {
max-width: calc(33.33% - 20px);
flex-basis: calc(33.33% - 20px);
}
}
.mat-card .mat-card-content {
height: 100%;
}
.mat-card .mat-card-content img {
width: 100%;
height: 100%;
object-fit: cover;
}

View File

@ -1,56 +1,60 @@
import { Component, Input } from '@angular/core';
import { Router } from '@angular/router';
import { Component } from '@angular/core';
import { Subject, debounceTime } from 'rxjs';
import { VideoListEntry } from 'src/app/model/VideoListEntry';
import { RequestService } from 'src/app/request.service';
import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { PageEvent } from '@angular/material/paginator';
import { ColumnDefinition } from 'src/app/shared/components/table/table.component';
@Component({
selector: 'app-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.scss']
styleUrls: ['./list.component.scss'],
})
export class ListComponent {
query$ = new Subject<string>();
query: string = "";
query: string = '';
onlyTagless: boolean = false;
videos: VideoListEntry[] = [];
total: number = 0;
page: number = 1;
perPage: number = 25;
perPage: number = 180;
videoListColumns: ColumnDefinition[] = [
{
header: 'Titel',
columnFunction: (item: VideoListEntry) => {
return item.title;
},
routerLink: (item: VideoListEntry) => {
return ['/video', item.id];
},
},
{
header: 'Tags',
columnFunction: (item: VideoListEntry) => {
return item.tags.map((tag) => tag.description);
},
columnCss: 'text-center',
},
];
videos: VideoListEntry[] = [];
selectedVideoUrl: string|null = null;
constructor(
private router: Router,
public requestService : RequestService,
) {
this.query$.pipe(
debounceTime(300)
).subscribe( query => {
constructor(public requestService: RequestService) {
this.query$.pipe(debounceTime(300)).subscribe((query) => {
this.query = query;
this.page = 1;
this.readList();
})
}
ngOnInit(): void {
this.readList();
}
onPageEvent(event: PageEvent) {
this.page = event.pageIndex + 1;
this.perPage = event.pageSize;
this.readList();
});
}
onInputChanged(event: Event): void {
this.query$.next((event.target as HTMLTextAreaElement).value);
}
selectVideo(entry: VideoListEntry) {
this.router.navigate(['/video', entry.id]);
onPageChanged(newPage: number) {
this.page = newPage;
this.readList();
}
ngOnInit(): void {
this.readList();
}
readList(): void {
@ -60,15 +64,12 @@ export class ListComponent {
query: this.query,
page: this.page,
perPage: this.perPage,
onlyTagless: this.onlyTagless,
},
(response:any) => {
this.videos = response.items;
(response: any) => {
this.total = response.total;
this.videos = response.items;
}
);
}
getThumbnailUrl(entry: VideoListEntry): string {
return 'http://wsl-flo/api/video/thumbnail/' + entry.id;
}
}

View File

@ -1,29 +1,62 @@
<div class="row">
<button [routerLink]="['/video/list']">Liste</button>
<input type="file" multiple (change)="onFileSelected($event)">
<div class="text-white w-full p-5">
<input type="file" multiple (change)="onFileSelected($event)" />
</div>
<div *ngIf="result !== null" class="row">
<h1>{{result.total}} Dateien uploaded</h1>
<h2>{{result.succeeded}} erfolgreich</h2>
<h2>{{result.failed}} fehlgeschlagen</h2>
<h3>Details:</h3>
<table>
<tr>
<th>Datei</th>
<th>Erfolg</th>
<th>Fehler</th>
<div class="p-5">
<table class="text-white w-full">
<tr class="text-left">
<th class="w-4/6">Datei</th>
<th class="w-1/6">Größe</th>
<th class="w-1/6">Status</th>
</tr>
<tr *ngFor="let details of result.details">
<td>{{details.file}}</td>
<td *ngIf="details.success">Erfolg</td>
<td *ngIf="!details.success">Fehlschlag</td>
<td>
<div *ngIf="!details.success">{{details.error}}</div>
<tr *ngFor="let result of uploadResults">
<td class="w-4/6">{{ result.file.name }}</td>
<td class="w-1/6">
<div *ngIf="result.file.size < 1024">
{{ result.file.size | number : "1.0-0" }} Bytes
</div>
<div *ngIf="result.file.size < 1024 * 1024 && result.file.size > 1024">
{{ result.file.size / 1024 | number : "1.0-0" }} KB
</div>
<div
*ngIf="
result.file.size < 1024 * 1024 * 1024 &&
result.file.size > 1024 * 1024
"
>
{{ result.file.size / (1024 * 1024) | number : "1.0-0" }} MB
</div>
<div
*ngIf="
result.file.size < 1024 * 1024 * 1024 * 1024 &&
result.file.size > 1024 * 1024 * 1024
"
>
{{ result.file.size / (1024 * 1024 * 1024) | number : "1.0-0" }} GB
</div>
</td>
<td class="w-1/6">
<div *ngIf="result.status === 'success'" class="font-bold">
<a
*ngIf="result.result !== undefined"
[routerLink]="['/video', result.result.details[0].id]"
>Video</a
>
<div *ngIf="result.result === undefined">success without id</div>
</div>
<div *ngIf="result.status === 'failed'">
<div *ngIf="result.result !== undefined">
{{ result.result.details[0].error }}
</div>
<div *ngIf="result.result === undefined">
{{ result.status }}
</div>
</div>
<div *ngIf="result.status !== 'success' && result.status !== 'failed'">
{{ result.status }}
</div>
</td>
</tr>
</table>
</div>

View File

@ -2,29 +2,51 @@ import { Component } from '@angular/core';
import { UploadResult } from 'src/app/model/UploadResult';
import { RequestService } from 'src/app/request.service';
interface UploadFileModel {
file: File;
result: UploadResult | undefined;
status: string;
}
@Component({
selector: 'app-upload',
templateUrl: './upload.component.html',
styleUrls: ['./upload.component.scss']
styleUrls: ['./upload.component.scss'],
})
export class UploadComponent {
uploadResults: UploadFileModel[] = [];
result: UploadResult|null = null;
constructor(
private requestService: RequestService,
) {
}
constructor(private requestService: RequestService) {}
onFileSelected(event: any) {
var files: File[] = event.target.files;
var files: File[] = Array.from(event.target.files);
files.forEach((file) => {
var existingItem = this.uploadResults.find(
(item) => item.file.name === file.name && item.file.size === file.size
);
if (existingItem === undefined)
this.uploadResults.push({
file,
result: undefined,
status: 'initialized',
});
else if (existingItem.status === 'failed')
existingItem.status = 'initialized';
});
this.uploadResults.forEach(async (item) => {
if (item.status === 'initialized') {
item.status = 'processing';
this.requestService.postFiles(
'video-list/upload',
files,
(response:any) => {
this.result = response;
[item.file],
(response: UploadResult) => {
item.result = response;
item.status = response.succeeded > 0 ? 'success' : 'failed';
}
);
}
});
}
}

View File

@ -1,15 +1,113 @@
<div
*ngIf="videoDetails !== null">
<button [routerLink]="['/video/list']">Videos</button>
<h1>{{videoDetails.title}}</h1>
<video
style="height: 100%; width: 100%;"
controls
autoplay>
<source [src]="videoUrl" type="video/mp4">
</video>
<div *ngIf="videoDetails !== undefined" class="pt-5 px-5">
<div class="text-3xl text-white font-bold">{{ videoDetails.title }}</div>
</div>
<div class="grid grid-cols-1 2xl:grid-cols-4">
<div class="2xl:col-span-3 p-5">
<video
#myVideo
controls
autoplay
(timeupdate)="updateTimestamp()"
class="w-full my-2"
>
<source [src]="host + 'video/stream/' + this.videoId" type="video/mp4" />
</video>
</div>
<div class="w-full 2xl:col-span-1 p-5">
<shared-card *ngIf="videoId !== ''" class="w-full" header="Details">
<div class="grid grid-cols-1 gap-2 mt-2">
<div id="title" class="flex flex-row gap-2">
<div class="basis-auto w-24">Titel:</div>
<div class="basis-full">
<input
*ngIf="videoDetails !== undefined"
type="text"
class="p-1 w-full text-gray-900 text-black dark:text-white bg-gray-50 dark:bg-zinc-700 rounded-lg bg-gray-100 text-base border border-gray-500 dark:border-transparent"
[(ngModel)]="videoDetails.title"
/>
</div>
</div>
<div id="duration" class="flex flex-row gap-2">
<div class="basis-auto w-24">Länge:</div>
<div class="basis-full">
<input
*ngIf="videoDetails !== undefined"
readonly
disabled="true"
type="text"
class="p-1 w-full text-gray-900 text-black dark:text-white bg-gray-50 dark:bg-zinc-700 rounded-lg bg-gray-100 text-base border border-gray-500 dark:border-transparent"
[(ngModel)]="videoDetails.duration"
/>
</div>
</div>
<div id="tags" class="flex flex-row gap-2">
<div class="basis-auto w-24">Tags:</div>
<div class="basis-full" *ngIf="videoDetails !== undefined">
<div class="flex flex-row">
<div class="basis-full">
<a
[routerLink]="['/tag', tag.id]"
*ngFor="let tag of videoDetails.tags"
><span
class="text-sm font-medium me-2 mx-auto px-2.5 py-0.5 rounded bg-zinc-700 hover:bg-zinc-900"
>{{ tag.description }}</span
></a
>
</div>
<div class="basis-auto">
<button (click)="addTag()">
<div
class="block px-3 py-1.5 rounded rounded-lg bg-zinc-700 hover:bg-zinc-900"
>
+
</div>
</button>
</div>
</div>
</div>
</div>
<div id="thumbnails">
<img
[src]="
host +
'video/thumbnail/' +
this.videoId +
'?v=' +
this.thumbnailVersion.toString()
"
alt="Thumbnail"
class="w-full h-full"
/>
</div>
<div id="actions">
<div class="flex flex-col md:flex-row gap-2">
<div class="basis-0 md:basis-full"></div>
<button class="basis-full md:basis-auto" (click)="delete()">
<div
class="min-w-28 px-3 py-1.5 rounded rounded-lg bg-red-900 hover:bg-red-950"
>
Delete
</div>
</button>
<button class="basis-full md:basis-auto" (click)="setThumbnail()">
<div
class="min-w-28 px-3 py-1.5 rounded rounded-lg bg-zinc-700 hover:bg-zinc-900"
>
Thumbnail
</div>
</button>
<button class="basis-full md:basis-auto" (click)="update()">
<div
class="min-w-28 px-3 py-1.5 rounded rounded-lg bg-green-700 hover:bg-green-900"
>
Save
</div>
</button>
</div>
</div>
</div>
</shared-card>
</div>
</div>

View File

@ -1,39 +1,113 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Component, ElementRef, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { TagListEntry } from 'src/app/model/TagListEntry';
import { VideoDetails } from 'src/app/model/VideoDetails';
import { RequestService } from 'src/app/request.service';
import { TagSelectorComponent } from 'src/app/shared/components/tag-selector/tag-selector.component';
@Component({
selector: 'app-video',
templateUrl: './video.component.html',
styleUrls: ['./video.component.scss']
styleUrls: ['./video.component.scss'],
})
export class VideoComponent {
@ViewChild('myVideo') videoElement: ElementRef | undefined;
host: string;
videoId: string;
videoUrl: string|null = null;
videoDetails: VideoDetails|null = null;
thumbnailVersion: number;
videoDetails: VideoDetails | undefined;
currentTimestamp: number | undefined;
constructor(
private route: ActivatedRoute,
private requestService : RequestService,
public router: Router,
public route: ActivatedRoute,
public requestService: RequestService,
public dialog: MatDialog
) {
this.videoId = this.route.snapshot.paramMap.get('id') ?? '';
this.videoUrl = "http://wsl-flo/api/video/stream/" + this.videoId;
let hostString = window.location.host;
let protocol = window.location.protocol;
this.host = protocol + '//' + hostString + '/api/';
this.thumbnailVersion = 0;
}
ngOnInit(): void {
this.readDetails();
}
updateTimestamp(): void {
if (this.videoElement === undefined) {
return;
}
const video: HTMLVideoElement = this.videoElement.nativeElement;
this.currentTimestamp = video.currentTime;
}
readDetails(): void {
this.requestService.post(
'video/read-details',
{
videoId: this.videoId
videoId: this.videoId,
},
(response:any) => {
(response: any) => {
this.videoDetails = response;
}
);
}
setThumbnail(): void {
this.requestService.post(
'video/set-thumbnail',
{
videoId: this.videoId,
timestamp: this.currentTimestamp,
},
(response: any) => {
this.thumbnailVersion++;
}
);
}
addTag(): void {
let dialogRef = this.dialog.open(TagSelectorComponent, {
panelClass: 'bg-zinc-900',
maxHeight: '400px',
width: '600px',
});
dialogRef.afterClosed().subscribe((result) => {
console.log(result);
if (result === undefined) return;
this.videoDetails?.tags.push(result);
});
}
update(): void {
this.requestService.post(
'video/update-details',
{
videoId: this.videoId,
title: this.videoDetails?.title,
tagIds: this.videoDetails?.tags.map((item) => item.id),
},
(response: any) => {
this.readDetails();
}
);
}
delete(): void {
this.requestService.post(
'video/delete',
{
videoId: this.videoId,
},
(response: any) => {
this.router.navigate(['/video/list']);
}
);
}
}

View File

@ -1,28 +1,19 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ListComponent } from './list/list.component';
import { VideoComponent } from './video.component';
import { SharedModule } from 'src/app/shared/shared.module';
import { RouterModule, Routes } from '@angular/router';
import { UploadComponent } from './upload/upload.component';
import { ListComponent } from './list/list.component';
const routes: Routes = [
{ path: 'list', component: ListComponent },
{ path: 'upload', component: UploadComponent },
{ path: 'list', component: ListComponent },
{ path: ':id', component: VideoComponent },
{ path: '', redirectTo: 'video/list', pathMatch: 'full'},
]
];
@NgModule({
declarations: [
ListComponent,
VideoComponent,
UploadComponent
],
imports: [
CommonModule,
SharedModule,
RouterModule.forChild(routes)
]
declarations: [VideoComponent, UploadComponent, ListComponent],
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
})
export class VideoModule { }
export class VideoModule {}

View File

@ -1,5 +1,5 @@
export interface TagListEntry {
id: string;
description: string;
videoCount?: number;
}

View File

@ -1,5 +1,8 @@
import { TagListEntry } from "./TagListEntry";
export interface VideoDetails {
title: string;
id: string;
duration: string;
tags: TagListEntry[];
}

View File

@ -1,7 +1,8 @@
import { TagListEntry } from "./TagListEntry";
import { TagListEntry } from './TagListEntry';
export interface VideoListEntry {
title: string;
id: string;
duration: string;
tags: TagListEntry[];
}

View File

@ -0,0 +1,31 @@
<div
id="card"
class="grow p-3 my-2 rounded-xl shadow-lg border border-gray-100 dark:bg-zinc-800 dark:border-zinc-900"
>
<div id="header">
<div class="flex flex-row">
<div *ngIf="icon !== null">
<img class="h-6 w-6 m-2" [src]="icon" />
</div>
<div
*ngIf="header !== null"
class="my-auto text-xl font-medium text-black dark:text-white truncate"
>
{{ header }}
</div>
</div>
<div
*ngIf="subHeader !== null"
class="text-slate-500 dark:text-slate-400 mx-2"
>
{{ subHeader }}
</div>
</div>
<div id="content" class="mx-5 text-black dark:text-white">
<div *ngIf="content !== null" class="my-2 italic">"{{ content }}"</div>
<div *ngIf="content === null">
<ng-content></ng-content>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'shared-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.scss'],
})
export class CardComponent {
@Input() header: string | null = null;
@Input() icon: string | null = null;
@Input() subHeader: string | null = null;
@Input() content: string | null = null;
}

View File

@ -0,0 +1,21 @@
<div
*ngIf="caption !== null"
class="text-lg font-bold mb-3 text-black dark:text-white"
>
{{ caption }}
</div>
<div class="flex flex-col md:space-x-4">
<div id="form-body" class="flex-1 basis-full">
<ng-content />
</div>
<div class="flex flex-row">
<div class="flex-1"></div>
<button
(click)="submit.emit()"
class="flex-0 p-2 min-w-24 bg-green-700 hover:bg-green-800 dark:bg-green-800 dark:hover:bg-green-900 rounded-lg"
>
<p class="text-lg text-white text-md font-bold">Submit</p>
</button>
</div>
</div>

View File

@ -0,0 +1,12 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'shared-form',
templateUrl: './form.component.html',
styleUrls: ['./form.component.scss']
})
export class FormComponent {
@Output() submit = new EventEmitter<any>();
@Input() caption: string|null = null;
}

View File

@ -0,0 +1,13 @@
<div class="my-5 lg:m-5">
<div
*ngIf="header !== null"
class="text-xl font-bold">
{{header}}
</div>
<img
class="rounded-md w-full"
[src]="source"
[alt]="alt" />
</div>

View File

@ -0,0 +1,12 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'shared-image-presenter',
templateUrl: './image-presenter.component.html',
styleUrls: ['./image-presenter.component.scss']
})
export class ImagePresenterComponent {
@Input() header: string|null = null;
@Input() alt: string|null = null;
@Input() source = '';
}

View File

@ -0,0 +1,69 @@
<nav aria-label="Page navigation example">
<ul class="flex items-center -space-x-px h-8 text-sm">
<li>
<button
(click)="setActivePage(-1)"
class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 dark:text-white bg-white dark:bg-zinc-800 border border-e-0 border-gray-500 dark:border-zinc-800 rounded-s-lg hover:bg-gray-100 hover:text-gray-700"
>
<span class="sr-only">Previous</span>
<svg
class="w-2.5 h-2.5 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 1 1 5l4 4"
/>
</svg>
</button>
</li>
<li *ngFor="let item of items" class="w-full">
<div
class="flex items-center justify-center h-8 leading-tight border border-gray-500 dark:border-zinc-800"
[ngClass]="{
'bg-green-700 dark:bg-green-800 text-white': item.page === page,
'bg-white dark:bg-zinc-700 text-black dark:text-white':
item.page !== page
}"
>
<div *ngIf="item.page === null" class="px-3">...</div>
<button
class="px-3 w-full h-full hover:bg-gray-100 hover:text-gray-700"
*ngIf="item.page !== null"
(click)="setActivePage(item.page)"
>
{{ item.page }}
</button>
</div>
</li>
<li>
<button
(click)="setActivePage(-2)"
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 dark:text-white bg-white dark:bg-zinc-800 border border-gray-500 dark:border-zinc-800 rounded-e-lg hover:bg-gray-100 hover:text-gray-700"
>
<span class="sr-only">Next</span>
<svg
class="w-2.5 h-2.5 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m1 9 4-4-4-4"
/>
</svg>
</button>
</li>
</ul>
</nav>

View File

@ -0,0 +1,80 @@
import {
Component,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
} from '@angular/core';
export interface PaginatorItem {
page: number | null;
}
@Component({
selector: 'shared-paginator',
templateUrl: './paginator.component.html',
styleUrls: ['./paginator.component.scss'],
})
export class PaginatorComponent implements OnChanges {
@Output() pageChange = new EventEmitter<number>();
@Input() page: number = 1;
@Input() perPage: number = 10;
@Input() total: number | null = null;
items: PaginatorItem[] = [];
ngOnChanges(changes: SimpleChanges): void {
this.calculateItems();
}
protected setActivePage(newPage: number | null) {
if (newPage === null) {
return;
}
if (newPage === -1) {
newPage = this.page - 1;
}
if (newPage === -2) {
newPage = this.page + 1;
}
if (newPage <= 0 || newPage > this.getMaxPages()) return;
this.page = newPage;
this.pageChange.emit(newPage);
this.calculateItems();
}
private calculateItems(): void {
this.items = [];
let first = 1;
let last = this.getMaxPages();
let rangeFirst = this.page - 1;
let rangeLast = this.page + 1;
if (this.page === 1) {
rangeFirst = 1;
rangeLast = last < 3 ? last : 3;
}
if (this.page === last) {
rangeFirst = Math.max(1, last - 2);
rangeLast = last;
}
if (rangeFirst > 1) this.items.push({ page: first });
if (rangeFirst > 2) this.items.push({ page: null });
for (let index = rangeFirst; index <= rangeLast; index++) {
this.items.push({ page: index });
}
if (rangeLast < last - 1) this.items.push({ page: null });
if (rangeLast < last) this.items.push({ page: last });
}
private getMaxPages(): number {
return Math.ceil((this.total ?? 0) / this.perPage);
}
}

View File

@ -0,0 +1,20 @@
<div class="text-sm font-medium text-center text-gray-500">
<ul class="flex flex-wrap -mb-px">
<li *ngFor="let tab of tabs; index as currentIndex" class="grow lg:grow-0">
<button
(click)="setActiveTab(tab)"
class="w-full lg:w-auto text-xl font-bold inline-block px-4 pt-4 pb-3 border-b-2 rounded-t-lg hover:text-green-700 hover:border-green-700 hover:no-underline dark:hover:text-green-800 dark:hover:border-green-800"
[ngClass]="{
'text-black border-black dark:text-green-800 dark:border-green-800 hover:text-green-700 dark:hover:border-white dark:hover:text-white':
currentIndex === activeIndex
}"
>
{{ tab.title }}
</button>
</li>
</ul>
</div>
<div class="tab-content">
<ng-content></ng-content>
</div>

View File

@ -0,0 +1,30 @@
import { Component, ContentChildren, EventEmitter, Input, Output, QueryList } from '@angular/core';
import { TabItem } from '../../models/TabItem';
@Component({
selector: 'shared-tab-control',
templateUrl: './tab-control.component.html',
styleUrls: ['./tab-control.component.scss']
})
export class TabControlComponent {
@Input() public tabs: TabItem[] = [];
@Input() public activeTabId: string = '';
@Output() public activeTabIdChange = new EventEmitter<string>();
@ContentChildren('tabContent') tabContents!: QueryList<any>;
public activeIndex = 0;
public ngOnChanges() {
this.setActiveTab(null);
}
public setActiveTab(tab: TabItem|null) {
if (tab !== null) {
this.activeTabId = tab.id;
this.activeTabIdChange.emit(this.activeTabId);
}
this.activeIndex = this.tabs.findIndex((i) => i.id === this.activeTabId);
}
}

View File

@ -0,0 +1,54 @@
<table class="w-full" [ngClass]="[background, foreground, border]">
<thead class="uppercase border-b text-sm" [ngClass]="[border]">
<tr>
<th
*ngFor="let column of columns; index as currentIndex"
[class]="'px-5 py-2 ' + column.headerCss"
[ngClass]="{
backgroundColumn: currentIndex % 2 === 0,
backgroundColumnAlternate: currentIndex % 2 === 1
}"
>
<div [innerHtml]="column.header"></div>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of items" class="border-b" [ngClass]="[border]">
<td
*ngFor="let column of columns; index as currentIndex"
[class]="'px-5 py-2 ' + column.columnCss"
[ngClass]="{
backgroundColumn: currentIndex % 2 === 0,
backgroundColumnAlternate: currentIndex % 2 === 1
}"
>
<div *ngIf="column.routerLink !== undefined">
<a [routerLink]="column.routerLink(item)">
<div *ngIf="column.columnContent !== undefined">
{{ column.columnContent }}
</div>
<div
*ngIf="column.columnFunction !== undefined"
[innerHtml]="column.columnFunction(item)"
></div>
</a>
</div>
<div *ngIf="column.routerLink === undefined">
<div *ngIf="column.columnContent !== undefined">
<div *ngIf="isBoolean(column.columnContent)">
<input type="checkbox" [(ngModel)]="column.columnContent" />
</div>
<div *ngIf="!isBoolean(column.columnContent)">
{{ column.columnContent }}
</div>
</div>
<div
*ngIf="column.columnFunction !== undefined"
[innerHtml]="column.columnFunction(item)"
></div>
</div>
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,31 @@
import { Component, Input } from '@angular/core';
export interface ColumnDefinition {
header: string;
columnContent?: string | undefined;
columnFunction?: Function | undefined;
routerLink?: Function | undefined;
headerCss?: string | undefined;
columnCss?: string | undefined;
}
@Component({
selector: 'shared-table',
templateUrl: './table.component.html',
styleUrls: ['./table.component.scss'],
})
export class TableComponent {
@Input() foreground: string = 'text-white';
@Input() background: string = 'bg-zinc-800';
@Input() backgroundColumn: string = 'bg-zinc-800';
@Input() backgroundColumnAlternate: string = 'bg-zinc-700';
@Input() border: string = 'border-zinc-600';
@Input() items: any[] = [];
@Input() columns: ColumnDefinition[] = [];
isBoolean(obje: any): boolean {
console.log(obje);
return typeof obje === 'boolean';
}
}

View File

@ -0,0 +1,17 @@
<div class="p-5 bg-zinc-700">
<div class="mb-5">
<input
type="text"
class="block p-1 w-full text-black dark:text-white bg-gray-50 dark:bg-zinc-700 rounded-lg bg-gray-100 text-base border border-gray-500 dark:border-transparent"
(input)="onInputChanged($event)"
/>
</div>
<h1
*ngFor="let tag of tags"
(click)="onClickTag(tag)"
class="text-white text-bold"
>
{{ tag.description }}
</h1>
</div>

View File

@ -0,0 +1,55 @@
import { Component, Inject } from '@angular/core';
import { ColumnDefinition } from '../table/table.component';
import { Subject, debounceTime } from 'rxjs';
import { TagListEntry } from 'src/app/model/TagListEntry';
import { RequestService } from 'src/app/request.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'shared-tag-selector',
templateUrl: './tag-selector.component.html',
styleUrls: ['./tag-selector.component.scss'],
})
export class TagSelectorComponent {
query$ = new Subject<string>();
query: string = '';
tags: TagListEntry[] = [];
constructor(
public requestService: RequestService,
public dialogRef: MatDialogRef<TagSelectorComponent>
) {
this.query$.pipe(debounceTime(300)).subscribe((query) => {
this.query = query;
this.readList();
});
}
ngOnInit(): void {
this.readList();
}
onInputChanged(event: Event): void {
this.query$.next((event.target as HTMLTextAreaElement).value);
}
onClickTag(tag: TagListEntry) {
this.dialogRef.close(tag);
}
onClose() {
this.dialogRef.close(undefined);
}
readList(): void {
this.requestService.post(
'tag-list/read-list',
{
query: this.query,
},
(response: any) => {
this.tags = response.items;
}
);
}
}

View File

@ -0,0 +1,42 @@
<div class="p-5 mt-5">
<input
type="text"
class="block p-1 w-full text-gray-900 text-black dark:text-white bg-gray-50 dark:bg-zinc-700 rounded-lg bg-gray-100 text-base border border-gray-500 dark:border-transparent"
(input)="onInputChanged($event)"
/>
</div>
<div
id="card-container"
class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 px-5"
>
<shared-card
*ngFor="let video of videos; index as currentIndex"
class="w-full"
[header]="video.title"
[subHeader]="video.duration"
>
<div class="h-8 my-auto">
<a [routerLink]="['/tag', tag.id]" *ngFor="let tag of video.tags"
><span
class="text-sm font-medium me-2 px-2.5 py-0.5 rounded bg-zinc-700 hover:bg-zinc-900"
>{{ tag.description }}</span
></a
>
</div>
<img
[src]="getThumbnailUrl(video)"
alt="Thumbnail"
(click)="selectVideo(video)"
class="m-auto h-80"
/>
</shared-card>
</div>
<shared-paginator
(pageChange)="onPageChanged($event)"
[page]="page"
[perPage]="perPage"
[total]="total"
>
</shared-paginator>

View File

@ -0,0 +1,67 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Router } from '@angular/router';
import { Subject, debounceTime } from 'rxjs';
import { VideoListEntry } from 'src/app/model/VideoListEntry';
import { RequestService } from 'src/app/request.service';
export interface OnReadListModel {
page: number;
perPage: number;
query: string;
}
@Component({
selector: 'shared-video-list',
templateUrl: './video-list.component.html',
styleUrls: ['./video-list.component.scss'],
})
export class VideoListComponent {
query$ = new Subject<string>();
query: string = '';
@Input() total: number = 0;
@Input() page: number = 1;
@Input() perPage: number = 36;
@Input() videos: VideoListEntry[] = [];
@Output() onReadList: EventEmitter<OnReadListModel> = new EventEmitter();
constructor(private router: Router, public requestService: RequestService) {
this.query$.pipe(debounceTime(300)).subscribe((query) => {
this.query = query;
this.page = 1;
this.readList();
});
}
ngOnInit(): void {
this.readList();
}
onPageChanged(newPage: number) {
this.page = newPage;
this.readList();
}
onInputChanged(event: Event): void {
this.query$.next((event.target as HTMLTextAreaElement).value);
}
selectVideo(entry: VideoListEntry) {
this.router.navigate(['/video', entry.id]);
}
readList(): void {
this.onReadList.emit({
query: this.query,
page: this.page,
perPage: this.perPage,
});
}
getThumbnailUrl(entry: VideoListEntry): string {
let hostString = window.location.host;
let protocol = window.location.protocol;
let host = protocol + '//' + hostString + '/api/';
return host + 'video/thumbnail/' + entry.id;
}
}

View File

@ -0,0 +1,15 @@
<div class="my-5 lg:m-5">
<div
*ngIf="header !== null"
class="text-xl font-bold">
{{header}}
</div>
<video
autoplay
controls
loop
class="rounded-md w-full">
<source [src]="source" type="video/mp4" />
</video>
</div>

View File

@ -0,0 +1,11 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'shared-video-presenter',
templateUrl: './video-presenter.component.html',
styleUrls: ['./video-presenter.component.scss']
})
export class VideoPresenterComponent {
@Input() header: string|null = null;
@Input() source = '';
}

View File

@ -0,0 +1,4 @@
export interface TabItem {
id: string;
title: string;
}

View File

@ -1,6 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { FormComponent } from './components/form/form.component';
import { CardComponent } from './components/card/card.component';
import { PaginatorComponent } from './components/paginator/paginator.component';
import { TabControlComponent } from './components/tab-control/tab-control.component';
import { VideoPresenterComponent } from './components/video-presenter/video-presenter.component';
import { ImagePresenterComponent } from './components/image-presenter/image-presenter.component';
import { TableComponent } from './components/table/table.component';
import { FormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
@ -9,7 +16,7 @@ import { MatToolbarModule } from '@angular/material/toolbar';
import { MatMenuModule } from '@angular/material/menu';
import { MatListModule } from '@angular/material/list';
import { MatIconModule } from '@angular/material/icon';
import { MatGridListModule } from '@angular/material/grid-list'
import { MatGridListModule } from '@angular/material/grid-list';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatExpansionModule } from '@angular/material/expansion';
@ -22,14 +29,58 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTableModule } from '@angular/material/table';
import { MatChipsModule } from '@angular/material/chips';
import { VideoListComponent } from './components/video-list/video-list.component';
import { TagSelectorComponent } from './components/tag-selector/tag-selector.component';
@NgModule({
declarations: [
ImagePresenterComponent,
VideoPresenterComponent,
CardComponent,
TabControlComponent,
PaginatorComponent,
FormComponent,
TableComponent,
VideoListComponent,
TagSelectorComponent,
],
exports: [
ImagePresenterComponent,
VideoPresenterComponent,
CardComponent,
TabControlComponent,
PaginatorComponent,
FormComponent,
TableComponent,
VideoListComponent,
TagSelectorComponent,
MatSlideToggleModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatGridListModule,
MatFormFieldModule,
FormsModule,
MatExpansionModule,
MatMenuModule,
MatListModule,
MatToolbarModule,
MatSidenavModule,
MatInputModule,
MatSelectModule,
MatDividerModule,
MatDialogModule,
MatSnackBarModule,
MatPaginatorModule,
MatCheckboxModule,
MatTabsModule,
MatTableModule,
MatChipsModule,
],
imports: [
RouterModule,
CommonModule,
MatSlideToggleModule,
MatCardModule,
@ -51,32 +102,8 @@ import { MatTableModule } from '@angular/material/table';
MatPaginatorModule,
MatCheckboxModule,
MatTabsModule,
MatTableModule
MatTableModule,
MatChipsModule,
],
exports: [
// to be replaced
MatSlideToggleModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatGridListModule,
MatFormFieldModule,
FormsModule,
MatExpansionModule,
MatMenuModule,
MatListModule,
MatToolbarModule,
MatSidenavModule,
MatInputModule,
MatSelectModule,
MatDividerModule,
MatDialogModule,
MatSnackBarModule,
MatPaginatorModule,
MatCheckboxModule,
MatTabsModule,
MatTableModule
]
})
export class SharedModule { }
export class SharedModule {}

View File

@ -1,17 +1,15 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>mytube</title>
<head>
<meta charset="utf-8" />
<title>MyTube</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
<body class="mat-typography mat-app-background">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="assets/weed.svg" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"></script>
</head>
<body class="bg-zinc-900">
<app-root></app-root>
</body>
</html>

View File

@ -1,65 +1,3 @@
body {
margin: 0;
height: 100vh;
}
app-root {
display:inline-block;
height: 100%;
width: 100%;
}
.flex-container {
display: flex;
}
.flex-1 {
flex: 1;
}
.flex-2 {
flex: 2;
}
.flex-3 {
flex: 3;
}
.flex-4 {
flex: 4;
}
.flex-5 {
flex: 5;
}
.container {
padding: 0;
max-width: unset;
}
.svg {
filter: invert(1);
}
.svg-1 {
filter: invert(1);
height: 1em;
width: 1em;
}
.svg-2 {
filter: invert(1);
height: 2em;
width: 2em;
}
.svg-3 {
filter: invert(1);
height: 3em;
width: 3em;
}
.svg-4 {
filter: invert(1);
height: 4em;
width: 4em;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

9
tailwind.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{html,ts}"],
theme: {
extend: {},
},
plugins: [],
}