This commit is contained in:
parent
d3fbda5e74
commit
42312cf409
@ -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
693
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
<app-navigation></app-navigation>
|
||||
<router-outlet></router-outlet>
|
||||
@ -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 {}
|
||||
|
||||
5
src/app/component/home/home.component.html
Normal file
5
src/app/component/home/home.component.html
Normal file
@ -0,0 +1,5 @@
|
||||
<shared-video-list
|
||||
(onReadList)="readList($event)"
|
||||
[total]="total"
|
||||
[videos]="videos"
|
||||
/>
|
||||
0
src/app/component/home/home.component.scss
Normal file
0
src/app/component/home/home.component.scss
Normal file
31
src/app/component/home/home.component.ts
Normal file
31
src/app/component/home/home.component.ts
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
15
src/app/component/navigation/navigation.component.html
Normal file
15
src/app/component/navigation/navigation.component.html
Normal 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>
|
||||
28
src/app/component/navigation/navigation.component.ts
Normal file
28
src/app/component/navigation/navigation.component.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,38 +1,26 @@
|
||||
<div class="row">
|
||||
<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-10">
|
||||
<mat-form-field>
|
||||
<input matInput type="text" (input)="onInputChanged($event)" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<button [routerLink]="['/video/upload']">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
<shared-table [items]="videos" [columns]="videoListColumns" />
|
||||
|
||||
<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 class="mt-5">
|
||||
<shared-paginator
|
||||
(pageChange)="onPageChanged($event)"
|
||||
[page]="page"
|
||||
[perPage]="perPage"
|
||||
[total]="total"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<mat-paginator [length]="total"
|
||||
[pageSize]="perPage"
|
||||
[pageSizeOptions]="[25, 50, 75]"
|
||||
[hidePageSize]="true"
|
||||
(page)="onPageEvent($event)">
|
||||
</mat-paginator>
|
||||
@ -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;
|
||||
}
|
||||
@ -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 => {
|
||||
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();
|
||||
constructor(public requestService: RequestService) {
|
||||
this.query$.pipe(debounceTime(300)).subscribe((query) => {
|
||||
this.query = query;
|
||||
this.page = 1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</tr>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<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>
|
||||
@ -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);
|
||||
|
||||
this.requestService.postFiles(
|
||||
'video-list/upload',
|
||||
files,
|
||||
(response:any) => {
|
||||
this.result = response;
|
||||
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',
|
||||
[item.file],
|
||||
(response: UploadResult) => {
|
||||
item.result = response;
|
||||
item.status = response.succeeded > 0 ? 'success' : 'failed';
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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']);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
|
||||
export interface TagListEntry {
|
||||
id: string;
|
||||
description: string;
|
||||
id: string;
|
||||
description: string;
|
||||
videoCount?: number;
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
import { TagListEntry } from "./TagListEntry";
|
||||
|
||||
export interface VideoDetails {
|
||||
title: string;
|
||||
id: string;
|
||||
duration: string;
|
||||
tags: TagListEntry[];
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
import { TagListEntry } from "./TagListEntry";
|
||||
import { TagListEntry } from './TagListEntry';
|
||||
|
||||
export interface VideoListEntry {
|
||||
title: string;
|
||||
id: string;
|
||||
tags: TagListEntry[];
|
||||
title: string;
|
||||
id: string;
|
||||
duration: string;
|
||||
tags: TagListEntry[];
|
||||
}
|
||||
31
src/app/shared/components/card/card.component.html
Normal file
31
src/app/shared/components/card/card.component.html
Normal 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>
|
||||
0
src/app/shared/components/card/card.component.scss
Normal file
0
src/app/shared/components/card/card.component.scss
Normal file
13
src/app/shared/components/card/card.component.ts
Normal file
13
src/app/shared/components/card/card.component.ts
Normal 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;
|
||||
}
|
||||
21
src/app/shared/components/form/form.component.html
Normal file
21
src/app/shared/components/form/form.component.html
Normal 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>
|
||||
0
src/app/shared/components/form/form.component.scss
Normal file
0
src/app/shared/components/form/form.component.scss
Normal file
12
src/app/shared/components/form/form.component.ts
Normal file
12
src/app/shared/components/form/form.component.ts
Normal 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;
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -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 = '';
|
||||
}
|
||||
69
src/app/shared/components/paginator/paginator.component.html
Normal file
69
src/app/shared/components/paginator/paginator.component.html
Normal 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>
|
||||
80
src/app/shared/components/paginator/paginator.component.ts
Normal file
80
src/app/shared/components/paginator/paginator.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
54
src/app/shared/components/table/table.component.html
Normal file
54
src/app/shared/components/table/table.component.html
Normal 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>
|
||||
31
src/app/shared/components/table/table.component.ts
Normal file
31
src/app/shared/components/table/table.component.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
67
src/app/shared/components/video-list/video-list.component.ts
Normal file
67
src/app/shared/components/video-list/video-list.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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 = '';
|
||||
}
|
||||
4
src/app/shared/models/TabItem.ts
Normal file
4
src/app/shared/models/TabItem.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface TabItem {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
@ -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 {}
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<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">
|
||||
<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="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>
|
||||
|
||||
@ -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
9
tailwind.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{html,ts}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user