Skip to content

Commit 43f3f7e

Browse files
Merge staging into master
2 parents 4913bae + 0a74842 commit 43f3f7e

File tree

32 files changed

+372
-14
lines changed

32 files changed

+372
-14
lines changed

app/abilities/photo-tag.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Ability } from 'ember-can';
2+
import { isNone } from '@ember/utils';
3+
4+
export default class PhotoTag extends Ability {
5+
get canShow() {
6+
return this.session.hasPermission('photo-tag.read');
7+
}
8+
9+
get canCreate() {
10+
return this.session.hasPermission('photo-tag.create');
11+
}
12+
13+
get canDestroy() {
14+
return (
15+
this.session.hasPermission('photo-tag.destroy') ||
16+
this.isTagOwner(this.model) ||
17+
this.isTagged(this.model)
18+
);
19+
}
20+
21+
isTagOwner(photoTag) {
22+
const { currentUser } = this.session;
23+
return (
24+
!isNone(currentUser) &&
25+
photoTag.get('author.id') === currentUser.get('id')
26+
);
27+
}
28+
29+
isTagged(photoTag) {
30+
const { currentUser } = this.session;
31+
return (
32+
!isNone(currentUser) &&
33+
photoTag.get('taggedUser.id') === currentUser.get('id')
34+
);
35+
}
36+
}

app/abilities/photo.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ export default class Photo extends Ability {
1111
this.model.photoAlbum.get('publiclyVisible')
1212
);
1313
}
14+
15+
get canShowPhotoTags() {
16+
return this.session.hasPermission('photo-tag.read');
17+
}
1418
}

app/components/photo-albums/photo.hbs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
<div class='d-flex justify-content-center align-items-center'>
2-
<img src={{@model.imageUrl}} class='photo-large' />
2+
{{#if this.showTags}}
3+
<PhotoTags::PhotoTags @model={{@model}}>
4+
<img src={{@model.imageUrl}} class='photo-large' />
5+
</PhotoTags::PhotoTags>
6+
{{else}}
7+
<img src={{@model.imageUrl}} class='photo-large' />
8+
{{/if}}
39
</div>
410

511
{{#if (can 'show individual users')}}

app/components/photo-albums/photo.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import { action } from '@ember/object';
22
import Component from '@glimmer/component';
33
import { tracked } from '@glimmer/tracking';
4+
import { inject as service } from '@ember/service';
45

56
export default class Photo extends Component {
7+
@service abilities;
8+
69
@tracked
710
showExif = false;
811

912
get showInfo() {
1013
return this.args.showInfo ?? true;
1114
}
1215

16+
get showTags() {
17+
return this.showInfo && this.abilities.can('show photo-tags');
18+
}
19+
1320
@action
1421
toggleShowExif() {
1522
this.showExif = !this.showExif;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<div class="position-relative photo-tags
2+
{{if showTags 'photo-tags--show'}}"
3+
{{on 'click' this.addTag}}
4+
{{did-insert this.addCloseAddTagListener}}
5+
{{will-destroy this.removeCloseAddTagListener}}
6+
>
7+
{{yield}}
8+
9+
{{#if (gt @model.amountOfTags 0)}}
10+
<button class="btn btn-info photo-tags-button" type="button" {{on 'click' this.toggleShowTags}}>
11+
<FaIcon @icon='tag' />
12+
{{ @model.amountOfTags }}
13+
</button>
14+
{{/if}}
15+
16+
{{#each @model.tags as |tag|}}
17+
<div class="photo-tag" style={{ tag.tagStyle }}>
18+
<LinkTo @route='users.user.photos' @model={{tag.taggedUser.id}}>
19+
{{ tag.taggedUser.fullName }}
20+
</LinkTo>
21+
{{#if (can 'destroy photo-tag' tag)}}
22+
<FaIcon @icon='xmark' {{ on 'click' (fn this.deleteTag tag) }} />
23+
{{/if}}
24+
</div>
25+
{{/each}}
26+
27+
{{#if this.newTagStyle }}
28+
<div class="photo-tag photo-tag--new" style={{ this.newTagStyle }}>
29+
<PowerSelect
30+
@options={{this.users}}
31+
@onChange={{this.storeTag}}
32+
@searchEnabled={{true}}
33+
@searchField='fullName'
34+
@registerAPI={{this.openUserSelect}}
35+
as |user|
36+
>
37+
{{user.fullName}}
38+
</PowerSelect>
39+
</div>
40+
{{/if}}
41+
</div>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import Component from '@glimmer/component';
2+
import { inject as service } from '@ember/service';
3+
import { tracked } from '@glimmer/tracking';
4+
import { action } from '@ember/object';
5+
import { next } from '@ember/runloop';
6+
import { htmlSafe } from '@ember/template';
7+
8+
export default class PhotoTags extends Component {
9+
@service store;
10+
@service flashNotice;
11+
@tracked newTagX;
12+
@tracked newTagY;
13+
@tracked selectApi;
14+
@tracked showTags = false;
15+
16+
get users() {
17+
return this.store.findAll('user');
18+
}
19+
20+
@action
21+
toggleShowTags() {
22+
this.showTags = !this.showTags;
23+
}
24+
25+
@action
26+
addTag(e) {
27+
if (e.target.tagName.toLowerCase() != 'img' || this.newTagX || this.newTagY)
28+
return;
29+
e.stopPropagation();
30+
let x = (e.layerX / e.target.width) * 100;
31+
let y = (e.layerY / e.target.height) * 100;
32+
this.newTagX = x;
33+
this.newTagY = y;
34+
next(this, () => {
35+
this.selectApi.actions.open();
36+
});
37+
}
38+
39+
@action
40+
addCloseAddTagListener() {
41+
this.closeAddTagListener = (e) => {
42+
let element = e.target;
43+
if (
44+
element.closest('.ember-power-select-dropdown') !== null ||
45+
element.closest('.photo-tag--new') !== null
46+
)
47+
return;
48+
e.stopPropagation();
49+
this.newTagX = null;
50+
this.newTagY = null;
51+
console.log('Closed tag', element);
52+
};
53+
54+
document.addEventListener('click', this.closeAddTagListener);
55+
}
56+
57+
@action
58+
removeCloseAddTagListener() {
59+
document.removeEventListener('click', this.closeAddTagListener);
60+
}
61+
62+
@action
63+
async storeTag(taggedUser) {
64+
let photo = this.args.model;
65+
let photoTag = this.store.createRecord('photo-tag', {
66+
photo,
67+
taggedUser,
68+
x: this.newTagX,
69+
y: this.newTagY,
70+
});
71+
this.newTagX = null;
72+
this.newTagY = null;
73+
74+
try {
75+
await photoTag.save();
76+
this.flashNotice.sendSuccess('Tag opgeslagen!');
77+
photo.reload();
78+
this.showTags = true;
79+
} catch (e) {
80+
this.flashNotice.sendError(
81+
'Tag opslaan mislukt. Is deze gebruiker al getagged?'
82+
);
83+
photoTag.deleteRecord();
84+
}
85+
}
86+
87+
@action
88+
async deleteTag(tag) {
89+
try {
90+
tag.deleteRecord();
91+
await tag.save();
92+
this.flashNotice.sendSuccess('Tag verwijderd!');
93+
this.args.model.reload();
94+
} catch (e) {
95+
this.flashNotice.sendError('Tag verwijderen mislukt.');
96+
tag.rollbackAttributes();
97+
}
98+
}
99+
100+
@action
101+
openUserSelect(userSelect) {
102+
if (this.selectApi == null) {
103+
this.selectApi = userSelect;
104+
}
105+
}
106+
107+
get newTagStyle() {
108+
if (!this.newTagX || !this.newTagY) return null;
109+
return htmlSafe(
110+
`left: ${parseFloat(this.newTagX)}%; top: ${parseFloat(this.newTagY)}%;`
111+
);
112+
}
113+
}

app/models/photo-tag.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Model, { belongsTo, attr } from '@ember-data/model';
2+
3+
export default class PhotoTag extends Model {
4+
// Properties
5+
@attr x;
6+
@attr y;
7+
@attr('date') updatedAt;
8+
@attr('date') createdAt;
9+
10+
// Relations
11+
@belongsTo('user') author;
12+
@belongsTo('user') taggedUser;
13+
@belongsTo photo;
14+
15+
get tagStyle() {
16+
return `left: ${parseFloat(this.x)}%; top: ${parseFloat(this.y)}%;`;
17+
}
18+
}

app/models/photo.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export default class Photo extends Model {
1818
@attr imageThumbUrl;
1919
@attr imageMediumUrl;
2020
@attr amountOfComments;
21+
@attr amountOfTags;
2122
@attr('date') updatedAt;
2223
@attr('date') createdAt;
2324

@@ -35,6 +36,7 @@ export default class Photo extends Model {
3536
@belongsTo photoAlbum;
3637
@belongsTo('user') uploader;
3738
@hasMany('photoComment') comments;
39+
@hasMany('photoTag') tags;
3840

3941
// Getters
4042
get hasExif() {

app/models/user.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ export default class User extends Model {
6464
@hasMany('debit/mandate') mandates;
6565
@hasMany mailAliases;
6666
@hasMany('mail-alias') groupMailAliases;
67+
@hasMany('photo-tags', { inverse: 'author' }) createdPhotoTags;
68+
@hasMany('photo-tags', { inverse: 'taggedUser' }) photoTags;
69+
@hasMany('photos') photos;
6770

6871
// Computed properties
6972
get fullName() {
@@ -151,6 +154,10 @@ export default class User extends Model {
151154
return this.avatarThumbUrl || AvatarThumbFallback;
152155
}
153156

157+
get sortedPhotos() {
158+
return this.photos?.sortBy('exifDateTimeOriginal', 'createdAt');
159+
}
160+
154161
// Methods
155162
setNullIfEmptyString(property) {
156163
const value = this.get(property);

app/router.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ Router.map(function () {
240240
this.route('mail');
241241
this.route('mandates');
242242
this.route('permissions');
243+
this.route('photos');
243244
this.route('settings');
244245

245246
this.route('resend-activation-code');

0 commit comments

Comments
 (0)