diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2276b2b7..c962e4bc 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -86,7 +86,7 @@ import { } from './wizards'; // Pipes -import { FilesizePipe, SafePipe } from './pipes'; +import { FilesizePipe, ReplayHasValuePipe, SafePipe } from './pipes'; // Dialogs import { @@ -169,6 +169,7 @@ const INTERCEPTORS: Provider[] = [ ResetPasswordDialogComponent, ForgotUsernameDialogComponent, ForgotPasswordDialogComponent, + ReplayHasValuePipe, ], imports: [ CommonModule, diff --git a/src/app/pages/annotate/annotate.component.html b/src/app/pages/annotate/annotate.component.html index 3c8f49b1..22314989 100644 --- a/src/app/pages/annotate/annotate.component.html +++ b/src/app/pages/annotate/annotate.component.html @@ -1,28 +1,60 @@ - - - -
-
- -
-
- - -
-
-
- -
+
+

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..6f4f7ae5 100644 --- a/src/app/pages/annotate/annotate.component.scss +++ b/src/app/pages/annotate/annotate.component.scss @@ -1,50 +1,8 @@ :host { display: flex; + align-items: center; } -#annotation-overview { - background-color: #eee; - padding: 0 2rem; - height: 100%; - width: 100%; - box-sizing: border-box; -} - -#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) { - } +div.container { + max-width: 1024px; } diff --git a/src/app/pages/annotate/annotate.component.ts b/src/app/pages/annotate/annotate.component.ts index 2d51285b..d5be01e3 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 } from 'src/app/services'; +import { map } from 'rxjs/operators'; +import { ReplaySubject } from 'rxjs'; @Component({ selector: 'app-annotate', @@ -12,18 +12,20 @@ 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, + ) {} + + get isAuthenticated$() { + return this.account.isAuthenticated$; + } + + get userAnnotations$() { + return this.account.user$.pipe(map(({ data }) => data.annotation as IAnnotation[])); } ngOnInit() { @@ -33,35 +35,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..10d30c1d 100644 --- a/src/app/pages/collaborate/collaborate.component.html +++ b/src/app/pages/collaborate/collaborate.component.html @@ -1,201 +1,54 @@ - - -
+

Collaborate

-
-

Groups

-
- Groups are a beautiful way to work together with other Kompakkt-users. -
- - - -
- - {{ 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 - - -
- - -
-
+
+

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.

-
-

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 do not have any collections

-
-
-
- - +
+

Your groups

- - - - - - - -
-
- - - -
-

You are not partaking in any collections

-
-
- - -
+ +

You must be logged in to see your groups. +

- -
-
- - My collections - - - Collections I partake in - - -
- - -
-
+ + + + + + + +

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

+ +
+
{{ group.name }}
+
+
+
+ +

You are not a member of any groups yet. +

+ + + +
+
-
+ +
diff --git a/src/app/pages/collaborate/collaborate.component.scss b/src/app/pages/collaborate/collaborate.component.scss index 454e6f68..6f4f7ae5 100644 --- a/src/app/pages/collaborate/collaborate.component.scss +++ b/src/app/pages/collaborate/collaborate.component.scss @@ -1,86 +1,8 @@ -@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 { +:host { 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; - } - } -} - -.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; - } -} - -.content-wrap { +div.container { + max-width: 1024px; } diff --git a/src/app/pages/collaborate/collaborate.component.ts b/src/app/pages/collaborate/collaborate.component.ts index 4aec3dca..6815cf90 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 { AccountService, BackendService } from 'src/app/services'; +import { ReplaySubject } from 'rxjs'; +import { ICompilation, IGroup } from '~common/interfaces'; @Component({ selector: 'app-collaborate', @@ -14,185 +11,18 @@ 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 userInGroups$ = new ReplaySubject(0); + public userInCompilations$ = new ReplaySubject(0); 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; - - // 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() { @@ -201,5 +31,15 @@ export class CollaborateComponent implements OnInit { name: 'description', content: 'Work collaboratively.', }); + + this.userInGroups$.next([]); + this.userInCompilations$.next([]); + + this.account.user$.subscribe(() => { + this.backend.findUserInGroups().then(groups => this.userInGroups$.next(groups)); + this.backend + .findUserInCompilations() + .then(compilations => this.userInCompilations$.next(compilations)); + }); } } 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), + ); + } +}