diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2276b2b7..372de081 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -53,7 +53,9 @@ import { EntityComponent, EntityDetailComponent, FooterComponent, + GridComponent, GridElementComponent, + GroupElementComponent, InstitutionComponent, NavbarComponent, PersonComponent, @@ -86,7 +88,8 @@ import { } from './wizards'; // Pipes -import { FilesizePipe, SafePipe } from './pipes'; +import { FilesizePipe, ReplayHasValuePipe, SafePipe } from './pipes'; +import { CountUniqueGroupMembersPipe } from './components/elements/group-element/count-unique-group-members.pipe'; // Dialogs import { @@ -94,16 +97,15 @@ import { EditEntityDialogComponent, EntityRightsDialogComponent, EntitySettingsDialogComponent, - ExploreCompilationDialogComponent, - ExploreEntityDialogComponent, + ForgotPasswordDialogComponent, + ForgotUsernameDialogComponent, GroupMemberDialogComponent, PasswordProtectedDialogComponent, RegisterDialogComponent, + ResetPasswordDialogComponent, UploadApplicationDialogComponent, + ViewerDialogComponent, } from './dialogs'; -import { ResetPasswordDialogComponent } from './dialogs/reset-password-dialog/reset-password-dialog.component'; -import { ForgotUsernameDialogComponent } from './dialogs/forgot-username-dialog/forgot-username-dialog.component'; -import { ForgotPasswordDialogComponent } from './dialogs/forgot-password-dialog/forgot-password-dialog.component'; // Interceptors import { HttpOptionsInterceptor } from './services/interceptors/http-options-interceptor'; @@ -148,12 +150,10 @@ const INTERCEPTORS: Provider[] = [ AnnotateComponent, CollaborateComponent, AboutComponent, - ExploreEntityDialogComponent, UploadApplicationDialogComponent, ProfilePageHelpComponent, ActionbarComponent, AnimatedImageComponent, - ExploreCompilationDialogComponent, EditEntityDialogComponent, AdminPageComponent, CompilationDetailComponent, @@ -169,6 +169,11 @@ const INTERCEPTORS: Provider[] = [ ResetPasswordDialogComponent, ForgotUsernameDialogComponent, ForgotPasswordDialogComponent, + ReplayHasValuePipe, + GroupElementComponent, + CountUniqueGroupMembersPipe, + GridComponent, + ViewerDialogComponent, ], imports: [ CommonModule, diff --git a/src/app/components/elements/grid/grid.component.html b/src/app/components/elements/grid/grid.component.html new file mode 100644 index 00000000..6dbc7430 --- /dev/null +++ b/src/app/components/elements/grid/grid.component.html @@ -0,0 +1 @@ + diff --git a/src/app/components/elements/grid/grid.component.scss b/src/app/components/elements/grid/grid.component.scss new file mode 100644 index 00000000..8cf06f5a --- /dev/null +++ b/src/app/components/elements/grid/grid.component.scss @@ -0,0 +1,5 @@ +:host { + display: grid; + grid-template-columns: repeat(var(--items-per-row, 4), 1fr); + gap: var(--gap-size, 8px); +} diff --git a/src/app/components/elements/grid/grid.component.ts b/src/app/components/elements/grid/grid.component.ts new file mode 100644 index 00000000..df882db7 --- /dev/null +++ b/src/app/components/elements/grid/grid.component.ts @@ -0,0 +1,20 @@ +import { Component, HostBinding, Input } from '@angular/core'; + +@Component({ + selector: 'app-grid', + templateUrl: './grid.component.html', + styleUrls: ['./grid.component.scss'], +}) +export class GridComponent { + @Input('itemsPerRow') itemsPerRow = 4; + + @Input('gapSizePx') gapSizePx = 8; + + @HostBinding('style.--items-per-row') get itemsPerRowStyle() { + return this.itemsPerRow; + } + + @HostBinding('style.--gap-size') get gapSizeStyle() { + return this.gapSizePx + 'px'; + } +} diff --git a/src/app/components/elements/group-element/count-unique-group-members.pipe.ts b/src/app/components/elements/group-element/count-unique-group-members.pipe.ts new file mode 100644 index 00000000..d936b382 --- /dev/null +++ b/src/app/components/elements/group-element/count-unique-group-members.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { IGroup } from '~common/interfaces'; + +@Pipe({ + name: 'countUniqueGroupMembers', +}) +export class CountUniqueGroupMembersPipe implements PipeTransform { + transform({ members, owners }: IGroup): unknown { + const uniqueIds = new Set([...members, ...owners].map(({ _id }) => _id)); + return Array.from(uniqueIds).length; + } +} diff --git a/src/app/components/elements/group-element/group-element.component.html b/src/app/components/elements/group-element/group-element.component.html new file mode 100644 index 00000000..6c7d0ef7 --- /dev/null +++ b/src/app/components/elements/group-element/group-element.component.html @@ -0,0 +1,11 @@ + +

+ {{group.name}}
+ by {{group.creator.fullname}} +

+ + +
diff --git a/src/app/components/elements/group-element/group-element.component.scss b/src/app/components/elements/group-element/group-element.component.scss new file mode 100644 index 00000000..f469fc03 --- /dev/null +++ b/src/app/components/elements/group-element/group-element.component.scss @@ -0,0 +1,17 @@ +:host { + display: inline-flex; + flex-direction: column; + padding: 24px; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.2); + border-radius: 4px; + gap: 16px; + min-width: 240px; + + * { + margin: 0; + } + + button { + align-self: flex-start; + } +} diff --git a/src/app/components/elements/group-element/group-element.component.ts b/src/app/components/elements/group-element/group-element.component.ts new file mode 100644 index 00000000..676ef9fc --- /dev/null +++ b/src/app/components/elements/group-element/group-element.component.ts @@ -0,0 +1,28 @@ +import { Component, Input } from '@angular/core'; +import { IGroup } from '~common/interfaces'; +import { combineLatest, map, ReplaySubject } from 'rxjs'; +import { AccountService } from '~services'; +import { GroupMemberDialogComponent } from '~dialogs'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'app-group-element', + templateUrl: './group-element.component.html', + styleUrls: ['./group-element.component.scss'], +}) +export class GroupElementComponent { + @Input('group') set group(group: IGroup) { + this.group$.next(group); + } + + public group$ = new ReplaySubject(1); + public isUserOwner$ = combineLatest([this.group$, this.account.user$]).pipe( + map(([group, user]) => group?.creator._id === user._id), + ); + + public openMemberList(group: IGroup) { + this.dialog.open(GroupMemberDialogComponent, { data: group }); + } + + constructor(private account: AccountService, private dialog: MatDialog) {} +} diff --git a/src/app/components/grid-element/grid-element.component.ts b/src/app/components/grid-element/grid-element.component.ts index 44523756..2d2ae04d 100644 --- a/src/app/components/grid-element/grid-element.component.ts +++ b/src/app/components/grid-element/grid-element.component.ts @@ -1,18 +1,17 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { MatMenu } from '@angular/material/menu'; -import { MatDialog } from '@angular/material/dialog'; import { + ICompilation, + IEntity, isAnnotation, isCompilation, isEntity, isResolvedEntity, - ICompilation, - IEntity, ObjectId, } from 'src/common'; import { environment } from 'src/environments/environment'; -import { ExploreEntityDialogComponent, ExploreCompilationDialogComponent } from 'src/app/dialogs'; +import { DialogHelperService } from '~services'; @Component({ selector: 'app-grid-element', @@ -52,7 +51,7 @@ export class GridElementComponent { @Output() public updateSelectedObject = new EventEmitter(); - constructor(private dialog: MatDialog) {} + constructor(private dialogHelper: DialogHelperService) {} get tooltipContent() { let description = ''; @@ -125,23 +124,17 @@ export class GridElementComponent { public openExploreDialog(element: IEntity | ICompilation) { if (!element) return; - if (isCompilation(element)) { - // tslint:disable-next-line:no-non-null-assertion - const eId = (Object.values(element.entities)[0] as IEntity)._id; - - this.dialog.open(ExploreCompilationDialogComponent, { - data: { - collectionId: element._id, - entityId: eId, - }, - id: 'explore-compilation-dialog', - }); - } else { - this.dialog.open(ExploreEntityDialogComponent, { - data: element._id, - id: 'explore-entity-dialog', - }); - } + console.log(element); + const compilation = isCompilation(element) ? element._id.toString() : undefined; + const entity = compilation + ? Object.keys((element as ICompilation).entities)[0].toString() + : element._id.toString(); + + this.dialogHelper.openViewerDialog({ + compilation, + entity, + mode: 'explore', + }); } public selectObject(id: string | ObjectId) { diff --git a/src/app/components/index.ts b/src/app/components/index.ts index 1c70c8fb..fba34057 100644 --- a/src/app/components/index.ts +++ b/src/app/components/index.ts @@ -15,3 +15,5 @@ export { SidenavListComponent } from './navigation/sidenav-list/sidenav-list.com export { NavbarComponent } from './navigation/navbar/navbar.component'; export { FooterComponent } from './navigation/footer/footer.component'; export { UploadComponent } from './upload/upload.component'; +export { GroupElementComponent } from './elements/group-element/group-element.component'; +export { GridComponent } from './elements/grid/grid.component'; diff --git a/src/app/dialogs/explore-compilation-dialog/explore-compilation-dialog.component.html b/src/app/dialogs/explore-compilation-dialog/explore-compilation-dialog.component.html deleted file mode 100644 index 704cecd2..00000000 --- a/src/app/dialogs/explore-compilation-dialog/explore-compilation-dialog.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/app/dialogs/explore-compilation-dialog/explore-compilation-dialog.component.scss b/src/app/dialogs/explore-compilation-dialog/explore-compilation-dialog.component.scss deleted file mode 100644 index 75a80552..00000000 --- a/src/app/dialogs/explore-compilation-dialog/explore-compilation-dialog.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -#close-explore-compilation-button { - margin-top: -15px; - margin-right: -10px; - margin-bottom: 10px; - background-color: #ddd; -} diff --git a/src/app/dialogs/explore-compilation-dialog/explore-compilation-dialog.component.ts b/src/app/dialogs/explore-compilation-dialog/explore-compilation-dialog.component.ts deleted file mode 100644 index 18d95e78..00000000 --- a/src/app/dialogs/explore-compilation-dialog/explore-compilation-dialog.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; - -import { environment } from 'src/environments/environment'; - -@Component({ - selector: 'app-explore-compilation-dialog', - templateUrl: './explore-compilation-dialog.component.html', - styleUrls: ['./explore-compilation-dialog.component.scss'], -}) -export class ExploreCompilationDialogComponent { - public viewerUrl: string; - - constructor( - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) - public data: { entityId: string; collectionId: string }, - ) { - // tslint:disable-next-line:max-line-length - this.viewerUrl = `${environment.viewer_url}?compilation=${data.collectionId}&entity=${data.entityId}&mode=explore`; - } -} diff --git a/src/app/dialogs/explore-entity/explore-entity-dialog.component.html b/src/app/dialogs/explore-entity/explore-entity-dialog.component.html deleted file mode 100644 index 704cecd2..00000000 --- a/src/app/dialogs/explore-entity/explore-entity-dialog.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/app/dialogs/explore-entity/explore-entity-dialog.component.scss b/src/app/dialogs/explore-entity/explore-entity-dialog.component.scss deleted file mode 100644 index 531f2ac9..00000000 --- a/src/app/dialogs/explore-entity/explore-entity-dialog.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -#close-explore-entity-button { - margin-top: -15px; - margin-right: -10px; - margin-bottom: 10px; - background-color: #ddd; -} diff --git a/src/app/dialogs/explore-entity/explore-entity-dialog.component.ts b/src/app/dialogs/explore-entity/explore-entity-dialog.component.ts deleted file mode 100644 index a272750e..00000000 --- a/src/app/dialogs/explore-entity/explore-entity-dialog.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; - -import { environment } from 'src/environments/environment'; - -@Component({ - selector: 'app-explore-entity-dialog', - templateUrl: './explore-entity-dialog.component.html', - styleUrls: ['./explore-entity-dialog.component.scss'], -}) -export class ExploreEntityDialogComponent { - public viewerUrl: string; - - constructor( - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public id: string, - ) { - this.viewerUrl = `${environment.viewer_url}?entity=${this.id}&mode=explore`; - } -} diff --git a/src/app/dialogs/index.ts b/src/app/dialogs/index.ts index 64c0d738..d9a1349b 100644 --- a/src/app/dialogs/index.ts +++ b/src/app/dialogs/index.ts @@ -2,8 +2,6 @@ export { ConfirmationDialogComponent } from './confirmation-dialog/confirmation- export { EditEntityDialogComponent } from './edit-entity-dialog/edit-entity-dialog.component'; export { EntityRightsDialogComponent } from './entity-rights-dialog/entity-rights-dialog.component'; export { EntitySettingsDialogComponent } from './entity-settings-dialog/entity-settings-dialog.component'; -export { ExploreCompilationDialogComponent } from './explore-compilation-dialog/explore-compilation-dialog.component'; -export { ExploreEntityDialogComponent } from './explore-entity/explore-entity-dialog.component'; export { GroupMemberDialogComponent } from './group-member-dialog/group-member-dialog.component'; export { PasswordProtectedDialogComponent } from './password-protected-dialog/password-protected-dialog.component'; export { RegisterDialogComponent } from './register-dialog/register-dialog.component'; @@ -11,3 +9,4 @@ export { UploadApplicationDialogComponent } from './upload-application-dialog/up export { ResetPasswordDialogComponent } from './reset-password-dialog/reset-password-dialog.component'; export { ForgotUsernameDialogComponent } from './forgot-username-dialog/forgot-username-dialog.component'; export { ForgotPasswordDialogComponent } from './forgot-password-dialog/forgot-password-dialog.component'; +export { ViewerDialogComponent, ViewerDialogData } from './viewer-dialog/viewer-dialog.component' diff --git a/src/app/dialogs/viewer-dialog/viewer-dialog.component.html b/src/app/dialogs/viewer-dialog/viewer-dialog.component.html new file mode 100644 index 00000000..f1fe4db8 --- /dev/null +++ b/src/app/dialogs/viewer-dialog/viewer-dialog.component.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/app/dialogs/viewer-dialog/viewer-dialog.component.scss b/src/app/dialogs/viewer-dialog/viewer-dialog.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/dialogs/viewer-dialog/viewer-dialog.component.ts b/src/app/dialogs/viewer-dialog/viewer-dialog.component.ts new file mode 100644 index 00000000..115c8b94 --- /dev/null +++ b/src/app/dialogs/viewer-dialog/viewer-dialog.component.ts @@ -0,0 +1,31 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { environment } from '~environments'; +import { ReplaySubject } from 'rxjs'; + +export type ViewerDialogData = { + entity?: string; + compilation?: string; + mode?: 'upload' | 'explore' | 'edit' | 'annotation' | 'open'; +}; + +@Component({ + selector: 'app-viewer-dialog', + templateUrl: './viewer-dialog.component.html', + styleUrls: ['./viewer-dialog.component.scss'], +}) +export class ViewerDialogComponent { + public viewerUrl$ = new ReplaySubject(0); + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) private data: ViewerDialogData, + ) { + const url = new URL(environment.viewer_url); + for (const [key, value] of Object.entries(data)) { + if (!value) continue; + url.searchParams.set(key, value); + } + this.viewerUrl$.next(url.toString()); + } +} diff --git a/src/app/pages/admin-page/admin-page.component.html b/src/app/pages/admin-page/admin-page.component.html index a67522bf..4314c4ff 100644 --- a/src/app/pages/admin-page/admin-page.component.html +++ b/src/app/pages/admin-page/admin-page.component.html @@ -1,4 +1,4 @@ -
+

User Administration

Waiting for admin data

diff --git a/src/app/pages/annotate/annotate.component.html b/src/app/pages/annotate/annotate.component.html index 3c8f49b1..30075e5b 100644 --- a/src/app/pages/annotate/annotate.component.html +++ b/src/app/pages/annotate/annotate.component.html @@ -1,28 +1,71 @@ - - - -
-
- -
-
- - -
-
-
- -
-
+
+

Annotate

+ +

What are annotations?

+ +

Annotations are multimedia containers that allow you to attach descriptions or references to points in space from + a + chosen camera perspective on 3D objects in Kompakkt. Through the selection of different perspectives on + consecutive + annotations you can generate a walk-through. Annotations can contain text, images, AV media, URLs and more. You + can + annotate your own objects with default annotations visible to everyone else. You can also create your own private + annotations or annotate other people’s objects by creating a collection and then annotating the objects you curate + into this collection.

+ +

How to annotate objects in Kompakkt?

+ +

You can start annotating objects when you switch to Annotate mode via the Edit menu within your own objects or + when + you access Annotate mode from the Collection’s page Edit menu.

+ + -
-
-

-

+

Your annotations

+ + +

You must be logged in to see your annotations.

+ +
+ +
-
+ + + + + +

You have created annotations within the following objects and/or collections:

+ + + + + + + + +
+
+ +

You don’t have any annotations yet. Start creating annotations by accessing your existing objects and + collections, or creating new ones:

+ +
+ + + +
+
+
diff --git a/src/app/pages/annotate/annotate.component.scss b/src/app/pages/annotate/annotate.component.scss index 9769a378..4f2b0fcd 100644 --- a/src/app/pages/annotate/annotate.component.scss +++ b/src/app/pages/annotate/annotate.component.scss @@ -1,50 +1,11 @@ -:host { - display: flex; +p { + margin: 12px 0; } -#annotation-overview { - background-color: #eee; - padding: 0 2rem; - height: 100%; - width: 100%; - box-sizing: border-box; +p:has(b) { + margin-top: 24px; } -#wrap-annotate { - display: grid; - grid-template-columns: 3.5fr 1.5fr; - grid-template-rows: auto; - - background-color: #eee; - height: calc(100vh - 169px); - max-height: calc(100vh - 169px); - - .iframe-container { - height: calc(100vh - 169px); - min-height: 40vh; - max-height: calc(100vh - 169px); - } - - @media (max-width: 1024px) { - display: flex; - flex-flow: column; - - height: calc(100vh - 105px); - max-height: calc(100vh - 105px); - } -} - -#annotation-viewer { - margin-left: 36px; - - @media (max-width: 1024px) { - margin-right: 35px; - } -} - -#annotation-data { - margin: 0 35px; - - @media (max-width: 1024px) { - } +button { + margin-top: 12px; } diff --git a/src/app/pages/annotate/annotate.component.ts b/src/app/pages/annotate/annotate.component.ts index 2d51285b..8101e33e 100644 --- a/src/app/pages/annotate/annotate.component.ts +++ b/src/app/pages/annotate/annotate.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { Meta, Title } from '@angular/platform-browser'; -import { ActivatedRoute } from '@angular/router'; -import { IEntity, IDigitalEntity } from 'src/common'; -import { environment } from 'src/environments/environment'; -import { BackendService } from 'src/app/services'; +import { IAnnotation, ICompilation, IEntity } from 'src/common'; +import { AccountService, BackendService, DialogHelperService } from 'src/app/services'; +import { map } from 'rxjs/operators'; +import { ReplaySubject } from 'rxjs'; @Component({ selector: 'app-annotate', @@ -12,18 +12,22 @@ import { BackendService } from 'src/app/services'; styleUrls: ['./annotate.component.scss'], }) export class AnnotateComponent implements OnInit { - public entity: IEntity | undefined; - public object: IDigitalEntity | undefined; - public objectID: string | undefined; - public viewerUrl: string; + public entitiesAndCompilations$ = new ReplaySubject>(0); constructor( - private route: ActivatedRoute, - private backend: BackendService, private titleService: Title, private metaService: Meta, - ) { - this.viewerUrl = ``; + private account: AccountService, + private backend: BackendService, + public dialogHelper: DialogHelperService, + ) {} + + get isAuthenticated$() { + return this.account.isAuthenticated$; + } + + get userAnnotations$() { + return this.account.user$.pipe(map(({ data }) => data.annotation as IAnnotation[])); } ngOnInit() { @@ -33,35 +37,34 @@ export class AnnotateComponent implements OnInit { content: 'Annotate object.', }); - this.objectID = this.route.snapshot.paramMap.get('id') || undefined; - const isCompilation = this.route.snapshot.paramMap.get('type') === 'compilation'; + this.userAnnotations$.subscribe(annotations => { + // Multiple annotations can and will be on the same objects or collections + // Using reverse mapping, we can narrow down the targets + const items = Object.fromEntries( + annotations + .map(annotation => annotation.target.source) + .map(({ relatedCompilation, relatedEntity }) => { + { + const isCompilation = typeof relatedCompilation === 'string' && !!relatedCompilation; + const target = isCompilation ? relatedCompilation : relatedEntity; + return [target, isCompilation]; + } + }), + ); - const params: string[] = ['mode=annotation']; - if (this.objectID) { - params.push(isCompilation ? `compilation=${this.objectID}` : `entity=${this.objectID}`); - } - - this.viewerUrl = `${environment.viewer_url}?${params.join('&')}`; - - if (this.objectID && !isCompilation) { - this.backend - .getEntity(this.objectID) - .then(resultEntity => { - this.entity = resultEntity; - if (!resultEntity.relatedDigitalEntity) { - throw new Error('Invalid object metadata.'); - } - return this.backend.getEntityMetadata(resultEntity.relatedDigitalEntity._id); - }) - .then(result => { - this.object = result; + Promise.all( + Object.entries(items).map(async ([target, isCompilation]) => { + return isCompilation + ? await this.backend.getCompilation(target) + : await this.backend.getEntity(target); + }), + ) + .then(items => { + return items.filter(item => !!item).map(item => item as IEntity | ICompilation); }) - .catch(e => { - this.object = undefined; - console.error(e); + .then(items => { + this.entitiesAndCompilations$.next(Array.from(new Set(items))); }); - } - - console.log(this.viewerUrl); + }); } } diff --git a/src/app/pages/collaborate/collaborate.component.html b/src/app/pages/collaborate/collaborate.component.html index 66199a2f..7b0b8155 100644 --- a/src/app/pages/collaborate/collaborate.component.html +++ b/src/app/pages/collaborate/collaborate.component.html @@ -1,201 +1,60 @@ - - -
+

Collaborate

-
-

Groups

-
- Groups are a beautiful way to work together with other Kompakkt-users. +

What are groups?

+

You can easily collaborate with your team or your students when managing annotations and collections of objects if + you create a dedicated group. Groups allow you to manage access rights across multiple registered users. Regular + group members receive annotation access for objects/collections associated with the group. Group owners receive + annotation access and can add/remove other group members. Groups can also be helpful when managing viewing access to + private objects, which remain unpublished to other Kompakkt users.

+ +

How do groups work with collections?

+

When you create a collection of Kompakkt objects you can annotate them regardless of their owners. This can be an + effective way to collaborate as you can invite either other individual Kompakkt users, or non-registred users (via + password-protection) to gain access rights to specific collections. If you need to continuously give access and/or + annotation rights to a number of individuals to different objects / collections, creating a group will just make + this process more efficient and will enable you to manage access rights centrally by managing the group membership. + Note that groups do require all their members to be registered users.

+ +

Your groups

+ + +

You must be logged in to see your groups.

+ +
+ +
+
- - -
- - {{ group.name }} - Members: {{ group.members.length }} | Owners: - {{ group.owners.length }} - - - - - - -
-
- - - -
-

You are not partaking in any group

-
-
- - {{ group.name }} - Members: {{ group.members.length }} | Owners: - {{ group.owners.length }} - - - - - -
+ + + - -
-
- - My groups - - - Groups I partake in - - -
- - -
-
-
-
-

Collections

-
- You are giving a course and would like to annotate objects together with - your students? You work together with colleagues on a specific topic? Then - create a collection or invite other members to your collection! -
+ + +

You are either the owner or a member of one of the following groups:

- -
-

You do not have any collections

-
-
-
- - - - - - - - - - -
-
+ + + +
- - -
-

You are not partaking in any collections

-
-
- - -
-
- - -
-
- - My collections - - - Collections I partake in - - -
- - +
-
-
-
+ + +
diff --git a/src/app/pages/collaborate/collaborate.component.scss b/src/app/pages/collaborate/collaborate.component.scss index 454e6f68..4f2b0fcd 100644 --- a/src/app/pages/collaborate/collaborate.component.scss +++ b/src/app/pages/collaborate/collaborate.component.scss @@ -1,86 +1,11 @@ -@use '@angular/material' as mat; - -.content { - display: flex; - flex-direction: column; -} - -div.tab-help { - display: flex; -} - -.mat-divider { - width: 0.1rem; - margin: 0 1rem; -} - -.mat-card-actions { - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; -} - -.collaborate-grid, -.entity-grid { - margin: 25px 15px; -} - -.collaborate-grid { - display: grid; - grid-gap: 1rem; - grid-template-columns: repeat(2, 1fr); - @media (min-width: 1024px) { - grid-template-columns: repeat(3, 1fr); - } - @media (min-width: 1280px) { - grid-template-columns: repeat(4, 1fr); - } - @media (min-width: 1600px) { - grid-template-columns: repeat(6, 1fr); - } - - .mat-card { - @include mat.elevation-transition; - @include mat.elevation(2); - - border-radius: 0; - - &:hover { - @include mat.elevation(8); - } - - .mat-card-title { - font-weight: lighter; - font-size: medium; - } - } +p { + margin: 12px 0; } -.grid-item { - position: relative; - cursor: context-menu; - - .actionbutton { - @include mat.elevation-transition; - @include mat.elevation(2); - } - - &:hover { - .actionbutton { - @include mat.elevation(8); - } - } -} - -.group-grid-item { - @include mat.elevation(2); - - h1, - p { - text-align: center; - } +p:has(b) { + margin-top: 24px; } -.content-wrap { +button { + margin-top: 12px; } diff --git a/src/app/pages/collaborate/collaborate.component.ts b/src/app/pages/collaborate/collaborate.component.ts index 4aec3dca..dd917e53 100644 --- a/src/app/pages/collaborate/collaborate.component.ts +++ b/src/app/pages/collaborate/collaborate.component.ts @@ -1,12 +1,9 @@ import { Component, OnInit } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { PageEvent } from '@angular/material/paginator'; import { Meta, Title } from '@angular/platform-browser'; import { AccountService, BackendService, DialogHelperService } from 'src/app/services'; -import { ConfirmationDialogComponent, GroupMemberDialogComponent } from 'src/app/dialogs'; -import { ICompilation, IEntity, IGroup, IUserData } from 'src/common'; -import { AddCompilationWizardComponent, AddGroupWizardComponent } from 'src/app/wizards'; +import { map, Observable, of, switchMap } from 'rxjs'; +import { IGroup } from '~common/interfaces'; @Component({ selector: 'app-collaborate', @@ -14,185 +11,31 @@ import { AddCompilationWizardComponent, AddGroupWizardComponent } from 'src/app/ styleUrls: ['./collaborate.component.scss'], }) export class CollaborateComponent implements OnInit { - public userData: IUserData | undefined; - - public filter = { - public: true, - private: false, - restricted: false, - unfinished: false, - }; - public filteredEntities: IEntity[] = []; - public filteredCompilations: ICompilation[] = []; - public filteredGroups: IGroup[] = []; - - public showPartakingGroups = false; - public showPartakingCompilations = false; - - private __partakingGroups: IGroup[] = []; - private __partakingCompilations: ICompilation[] = []; - - public icons = { - audio: 'audiotrack', - video: 'movie', - image: 'image', - model: 'language', - collection: 'apps', - }; - - public pageEvent: PageEvent = { - previousPageIndex: 0, - pageIndex: 0, - pageSize: 20, - length: Number.POSITIVE_INFINITY, - }; - - public entitySearchInput = ''; + public groups$: Observable> = this.account.userData$.pipe( + switchMap(user => { + if (!user) return of({ user, groups: [] }); + return this.backend.findUserInGroups().then(groups => ({ user, groups })); + }), + map(({ user, groups }) => { + if (!user) return []; + return groups.map(group => ({ + ...group, + userOwned: + group.creator._id === user._id || group.owners.some(owner => owner._id === user._id), + })); + }), + ); constructor( private account: AccountService, - private dialog: MatDialog, private backend: BackendService, private titleService: Title, private metaService: Meta, - private helper: DialogHelperService, - ) { - this.account.userData$.subscribe(newData => { - this.userData = newData; - if (!this.userData) return; - - this.backend - .findUserInGroups() - .then(groups => (this.__partakingGroups = groups)) - .catch(e => console.error(e)); - - this.backend - .findUserInCompilations() - .then(compilations => (this.__partakingCompilations = compilations)) - .catch(e => console.error(e)); - }); - } - - // Groups - get userGroups(): IGroup[] { - return this.userData?.data?.group ?? []; - } - - get partakingGroups(): IGroup[] { - return this.__partakingGroups; - } - - public openGroupCreation(group?: IGroup) { - const dialogRef = this.dialog.open(AddGroupWizardComponent, { - data: group ? group : undefined, - disableClose: true, - }); - dialogRef - .afterClosed() - .toPromise() - .then((result: undefined | IGroup) => { - if (!result) return; - if (!this.userData) return; - // Add new group to list - this.userData.data.group = this.userData.data.group - ? [...this.userData.data.group, result] - : [result]; - }); - } - - public openMemberList(group: IGroup) { - this.dialog.open(GroupMemberDialogComponent, { - data: group, - }); - } - - public async removeGroupDialog(group: IGroup) { - const loginData = await this.helper.confirmWithAuth( - `Do you really want to delete ${group.name}?`, - `Validate login before deleting ${group.name}`, - ); - if (!loginData) return; - const { username, password } = loginData; - - // Delete - this.backend - .deleteRequest(group._id, 'group', username, password) - .then(result => { - if (this.userData?.data?.group) { - this.userData.data.group = (this.userData.data.group as IGroup[]).filter( - _g => _g._id !== group._id, - ); - } - }) - .catch(e => console.error(e)); - } - - public leaveGroupDialog(group: IGroup) { - const dialogRef = this.dialog.open(ConfirmationDialogComponent, { - data: `Do you really want to leave ${group.name}?`, - }); - dialogRef - .afterClosed() - .toPromise() - .then(result => { - if (result) { - // TODO: leave - console.log('Leave', group); - } - }); - } - - // Compilations - get userCompilations(): ICompilation[] { - return this.userData?.data?.compilation ?? []; - } - - get partakingCompilations(): ICompilation[] { - return this.__partakingCompilations; - } - - public openCompilationCreation(compilation?: ICompilation) { - const dialogRef = this.dialog.open(AddCompilationWizardComponent, { - data: compilation ? compilation : undefined, - disableClose: true, - }); - dialogRef - .afterClosed() - .toPromise() - .then((result: undefined | ICompilation) => { - if (result && this.userData && this.userData.data.compilation) { - if (compilation) { - const index = (this.userData.data.compilation as ICompilation[]).findIndex( - comp => comp._id === result._id, - ); - if (index === -1) return; - this.userData.data.compilation.splice(index, 1, result); - } else { - (this.userData.data.compilation as ICompilation[]).push(result); - } - } - }); - } - - public async removeCompilationDialog(compilation: ICompilation) { - const loginData = await this.helper.confirmWithAuth( - `Do you really want to delete ${compilation.name}?`, - `Validate login before deleting ${compilation.name}`, - ); - if (!loginData) return; - const { username, password } = loginData; + public dialogHelper: DialogHelperService, + ) {} - // Delete - this.backend - .deleteRequest(compilation._id, 'compilation', username, password) - .then(result => { - if (this.userData?.data?.compilation) { - this.userData.data.compilation = ( - this.userData.data.compilation as ICompilation[] - ).filter(comp => comp._id !== compilation._id); - } - }) - .catch(e => console.error(e)); + get isAuthenticated$() { + return this.account.isAuthenticated$; } ngOnInit() { diff --git a/src/app/pages/profile-page/profile-page.component.html b/src/app/pages/profile-page/profile-page.component.html index 80772b90..ba2a9e15 100644 --- a/src/app/pages/profile-page/profile-page.component.html +++ b/src/app/pages/profile-page/profile-page.component.html @@ -1,6 +1,6 @@ -
+

User Profile

@@ -14,7 +14,7 @@

No data available for the current user.

-
+

User Profile

@@ -193,81 +193,17 @@

No matches

- -
-

You have not created any groups

-
- -
-
-

{{ group.name }}

-

- Members: {{ group.members.length }} | Owners: - {{ group.owners.length }} -

- - - - - - - - - -
+ +
+

You are not associated with any groups

- - -
-

You are not partaking in any group

-
-
- - {{ group.name }} - Members: {{ group.members.length }} | Owners: - {{ group.owners.length }} - - - - - -
-
+ + + + +
@@ -278,26 +214,6 @@

{{ group.name }}

> Create a new group - -
- - My groups - - Groups I partake in - -
diff --git a/src/app/pages/static-pages/consortium/consortium.component.html b/src/app/pages/static-pages/consortium/consortium.component.html index 5124252d..7347686e 100644 --- a/src/app/pages/static-pages/consortium/consortium.component.html +++ b/src/app/pages/static-pages/consortium/consortium.component.html @@ -1,4 +1,4 @@ -
+

Kompakkt Developer Consortium

diff --git a/src/app/pages/static-pages/contact/contact.component.html b/src/app/pages/static-pages/contact/contact.component.html index 3b133ccb..c3c4861a 100644 --- a/src/app/pages/static-pages/contact/contact.component.html +++ b/src/app/pages/static-pages/contact/contact.component.html @@ -1,4 +1,4 @@ -

+

Contact

Kompakkt is being developed at:

diff --git a/src/app/pages/static-pages/privacy/privacy.component.html b/src/app/pages/static-pages/privacy/privacy.component.html index b605f617..eb235ca8 100644 --- a/src/app/pages/static-pages/privacy/privacy.component.html +++ b/src/app/pages/static-pages/privacy/privacy.component.html @@ -1,4 +1,4 @@ -
+

Privacy Policy

diff --git a/src/app/pipes/index.ts b/src/app/pipes/index.ts index ea748e80..eba6e849 100644 --- a/src/app/pipes/index.ts +++ b/src/app/pipes/index.ts @@ -1,2 +1,3 @@ export { FilesizePipe } from './filesize.pipe'; export { SafePipe } from './safe.pipe'; +export { ReplayHasValuePipe } from './replay-has-value.pipe'; diff --git a/src/app/pipes/replay-has-value.pipe.ts b/src/app/pipes/replay-has-value.pipe.ts new file mode 100644 index 00000000..2467aa38 --- /dev/null +++ b/src/app/pipes/replay-has-value.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { Observable, ReplaySubject } from 'rxjs'; +import { map, startWith } from 'rxjs/operators'; + +@Pipe({ + name: 'replayHasValue', +}) +export class ReplayHasValuePipe implements PipeTransform { + transform(value: ReplaySubject): Observable { + return value.pipe( + startWith(false), + map(value => !!value), + ); + } +} diff --git a/src/app/services/dialog-helper.service.ts b/src/app/services/dialog-helper.service.ts index e58768dc..64b792bf 100644 --- a/src/app/services/dialog-helper.service.ts +++ b/src/app/services/dialog-helper.service.ts @@ -4,13 +4,19 @@ import { MatDialog } from '@angular/material/dialog'; import { EventsService } from './'; import { ICompilation, IEntity } from 'src/common'; import { AuthDialogComponent } from 'src/app/components'; -import { AddCompilationWizardComponent, AddEntityWizardComponent } from 'src/app/wizards'; +import { + AddCompilationWizardComponent, + AddEntityWizardComponent, + AddGroupWizardComponent, +} from 'src/app/wizards'; import { ConfirmationDialogComponent, - RegisterDialogComponent, EditEntityDialogComponent, EntitySettingsDialogComponent, PasswordProtectedDialogComponent, + RegisterDialogComponent, + ViewerDialogComponent, + ViewerDialogData, } from 'src/app/dialogs'; @Injectable({ @@ -47,6 +53,22 @@ export class DialogHelperService { }); } + public openEntityWizard() { + return this.dialog.open(AddEntityWizardComponent, { + disableClose: true, + }); + } + + public openGroupWizard() { + return this.dialog.open(AddGroupWizardComponent, { + disableClose: true, + }); + } + + public openViewerDialog(data: ViewerDialogData) { + return this.dialog.open(ViewerDialogComponent, { data, id: 'viewer-dialog' }); + } + public editCompilation(element: ICompilation | undefined) { if (!element) return; const dialogRef = this.dialog.open(AddCompilationWizardComponent, { diff --git a/src/styles.scss b/src/styles.scss index 6987fb9a..c4685375 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -8,10 +8,12 @@ @import url('./assets/fonts/OpenSans.css'); @import url('./assets/fonts/MaterialIcons.css'); +@import 'utility'; + :root { --font-stack: 'Open Sans', Frutiger, 'Frutiger Linotype', Univers, Calibri, 'Gill Sans', - 'Gill Sans MT', 'Myriad Pro', Myriad, 'DejaVu Sans Condensed', 'Liberation Sans', - 'Nimbus Sans L', Tahoma, Geneva, 'Helvetica Neue', Helvetica, Arial, sans-serif; + 'Gill Sans MT', 'Myriad Pro', Myriad, 'DejaVu Sans Condensed', 'Liberation Sans', + 'Nimbus Sans L', Tahoma, Geneva, 'Helvetica Neue', Helvetica, Arial, sans-serif; --drop-shadow: drop-shadow(0 0.1rem 0.1rem rgba(0, 0, 0, 0.25)); --brand-color: #00afe7; } @@ -241,9 +243,10 @@ iframe { /* !important to overwrite default mat-card rule */ transition: border 280ms cubic-bezier(0.4, 0, 0.2, 1), - box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1) !important; --color: var(--brand-color); + &.error { --color: red; } @@ -412,6 +415,7 @@ app-actionbar { height: 100%; > * { + display: flex; flex-direction: column !important; } } @@ -422,7 +426,8 @@ app-annotate { #edit-entity-dialog, #explore-entity-dialog, -#explore-compilation-dialog { +#explore-compilation-dialog, +#viewer-dialog { height: 100vh; width: 100vw; @@ -433,7 +438,8 @@ app-annotate { } #explore-compilation-dialog, -#explore-entity-dialog { +#explore-entity-dialog, +#viewer-dialog{ padding: 0 !important; border-radius: 1rem !important; background: #111; @@ -447,7 +453,7 @@ app-annotate { grid-gap: 1em; padding-bottom: 10px; - grid-template-columns: repeat(5, 1fr); + grid-template-columns: repeat(4, 1fr); @include queries.small { grid-template-columns: repeat(1, 1fr); @@ -458,11 +464,11 @@ app-annotate { } @include queries.large { - grid-template-columns: repeat(5, 1fr); + grid-template-columns: repeat(4, 1fr); } @include queries.huge { - grid-template-columns: repeat(6, 1fr); + grid-template-columns: repeat(4, 1fr); } } @@ -502,6 +508,24 @@ app-annotate { } } +button { + &.rounded { + border-radius: 2em; + } + .mat-button-wrapper { + display: flex; + justify-content: center; + align-items: center; + gap: 4px; + } +} + +.button-row { + display: flex; + align-items: center; + gap: 8px; +} + #actionbar .mat-slide-toggle.mat-checked:not(.mat-disabled) { .mat-slide-toggle-bar { background-color: #aaa; @@ -546,6 +570,7 @@ app-edit-entity-dialog { .can-be-disabled { transition: filter 500ms; + &.disabled { filter: opacity(0.5); pointer-events: none; @@ -558,6 +583,7 @@ app-edit-entity-dialog { div.block { margin-bottom: 0 !important; + & + div.block { margin-top: 1rem !important; } diff --git a/src/utility.scss b/src/utility.scss new file mode 100644 index 00000000..d2102074 --- /dev/null +++ b/src/utility.scss @@ -0,0 +1,36 @@ +.centered-content { + box-sizing: border-box; + width: min(1440px, 100%); + max-width: 1440px; + align-self: center; + padding: 16px 32px; +} + +.flex { + display: flex; + align-items: flex-start; +} + +.flex-column { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.gap-12 { + gap: 12px; +} + +.gap-24 { + gap: 24px; +} + +.flex-text-margin { + h1, h2, h3, h4, h5, h6, p { + margin: 0; + } + + h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p, p + p { + margin-top: 1em; + } +}