From 1ddfe70c68edc75c1826a8feb78d0bcbc499e689 Mon Sep 17 00:00:00 2001 From: Ron Lucke Date: Tue, 25 Jun 2024 14:56:16 +0000 Subject: TIC Avatar Modernisierung Closes #4055 Merge request studip/studip!2877 --- app/controllers/course/avatar.php | 17 + app/controllers/course/basicdata.php | 5 +- app/controllers/course/studygroup.php | 16 +- app/controllers/institute/avatar.php | 44 +++ app/controllers/institute/basicdata.php | 2 - app/controllers/settings/avatar.php | 23 ++ app/views/avatar/update.php | 75 ---- app/views/course/avatar/index.php | 7 + app/views/course/studygroup/avatar.php | 7 + app/views/institute/avatar/index.php | 7 + app/views/institute/basicdata/index.php | 20 - app/views/profile/widget-avatar.php | 10 +- app/views/settings/avatar/index.php | 7 + lib/classes/Avatar.php | 5 + lib/classes/JsonApi/RouteMap.php | 9 + lib/classes/JsonApi/Routes/Avatar/Authority.php | 29 ++ .../JsonApi/Routes/Avatar/AvatarHelpers.php | 40 ++ .../JsonApi/Routes/Avatar/AvatarOfRangeShow.php | 34 ++ lib/classes/JsonApi/Routes/Avatar/AvatarUpload.php | 66 ++++ .../JsonApi/Routes/Avatar/AvatarofRangeDelete.php | 36 ++ lib/classes/JsonApi/SchemaMap.php | 2 + lib/classes/JsonApi/Schemas/Avatar.php | 69 ++++ lib/modules/CoreAdmin.php | 3 +- lib/modules/CoreStudygroupAdmin.php | 2 +- lib/navigation/AdminNavigation.php | 1 + lib/navigation/ProfileNavigation.php | 1 + public/assets/images/icons/blue/flip.svg | 44 +++ resources/assets/javascripts/bootstrap/avatar.js | 61 +-- resources/assets/javascripts/chunk-loader.js | 28 +- resources/assets/javascripts/chunks/avatar.js | 0 resources/vue/avatar-app.js | 76 ++++ resources/vue/components/avatar/AvatarApp.vue | 430 +++++++++++++++++++++ resources/vue/store/avatar.module.js | 102 +++++ 33 files changed, 1100 insertions(+), 178 deletions(-) create mode 100644 app/controllers/course/avatar.php create mode 100644 app/controllers/institute/avatar.php create mode 100644 app/controllers/settings/avatar.php delete mode 100644 app/views/avatar/update.php create mode 100644 app/views/course/avatar/index.php create mode 100644 app/views/course/studygroup/avatar.php create mode 100644 app/views/institute/avatar/index.php create mode 100644 app/views/settings/avatar/index.php create mode 100644 lib/classes/JsonApi/Routes/Avatar/Authority.php create mode 100644 lib/classes/JsonApi/Routes/Avatar/AvatarHelpers.php create mode 100644 lib/classes/JsonApi/Routes/Avatar/AvatarOfRangeShow.php create mode 100644 lib/classes/JsonApi/Routes/Avatar/AvatarUpload.php create mode 100644 lib/classes/JsonApi/Routes/Avatar/AvatarofRangeDelete.php create mode 100644 lib/classes/JsonApi/Schemas/Avatar.php create mode 100644 public/assets/images/icons/blue/flip.svg create mode 100644 resources/assets/javascripts/chunks/avatar.js create mode 100644 resources/vue/avatar-app.js create mode 100644 resources/vue/components/avatar/AvatarApp.vue create mode 100644 resources/vue/store/avatar.module.js diff --git a/app/controllers/course/avatar.php b/app/controllers/course/avatar.php new file mode 100644 index 0000000..b5b34d2 --- /dev/null +++ b/app/controllers/course/avatar.php @@ -0,0 +1,17 @@ +course_id = Context::getId(); + if (!$GLOBALS['perm']->have_studip_perm('tutor', $this->course_id)) { + throw new AccessDeniedException(_("Sie haben keine Berechtigung diese " . + "Veranstaltung zu verändern.")); + } + PageLayout::setTitle(Context::getHeaderLine() . ' - ' . _('Veranstaltungsbild ändern')); + Navigation::activateItem('/course/admin/avatar'); + $avatar = CourseAvatar::getAvatar($this->course_id); + $this->avatar_url = $avatar->getURL(Avatar::NORMAL); + } +} \ No newline at end of file diff --git a/app/controllers/course/basicdata.php b/app/controllers/course/basicdata.php index 97ec053..329554f 100644 --- a/app/controllers/course/basicdata.php +++ b/app/controllers/course/basicdata.php @@ -379,10 +379,7 @@ class Course_BasicdataController extends AuthenticatedController ); } } - $widget->addLink(_('Bild ändern'), - $this->url_for('avatar/update/course', $this->course_id), - Icon::create('edit') - ); + if ($GLOBALS['perm']->have_perm('admin')) { $is_locked = $course->lock_rule; $widget->addLink( diff --git a/app/controllers/course/studygroup.php b/app/controllers/course/studygroup.php index cd08ba3..1f9c4a4 100644 --- a/app/controllers/course/studygroup.php +++ b/app/controllers/course/studygroup.php @@ -383,13 +383,7 @@ class Course_StudygroupController extends AuthenticatedController $this->url_for('course/wizard?studygroup=1'), Icon::create('add') ); - if ($GLOBALS['perm']->have_studip_perm('tutor', $id)) { - $actions->addLink( - _('Bild ändern'), - $this->url_for('avatar/update/course/' . $id), - Icon::create('edit') - ); - } + $actions->addLink( _('Diese Studiengruppe löschen'), $this->deleteURL(), @@ -977,6 +971,12 @@ class Course_StudygroupController extends AuthenticatedController $this->redirect($this->url_for('messages/write', ['course_id' => $id, 'default_subject' => $subject, 'filter' => 'all', 'emailrequest' => 1])); } - + public function avatar_action() + { + Navigation::activateItem('/course/admin/avatar'); + $this->studygroup_id = Context::getId(); + $avatar = StudygroupAvatar::getAvatar($this->studygroup_id); + $this->avatar_url = $avatar->getURL(Avatar::NORMAL); + } } diff --git a/app/controllers/institute/avatar.php b/app/controllers/institute/avatar.php new file mode 100644 index 0000000..24b8400 --- /dev/null +++ b/app/controllers/institute/avatar.php @@ -0,0 +1,44 @@ +have_perm("admin")) { + throw new AccessDeniedException(); + } + } + public function index_action($i_id = false) + { + //get ID from an open Institut + $i_view = $i_id ?: Request::option('i_view', Context::getId()); + + if (!$i_view) { + Navigation::activateItem('/admin/institute/avatar'); + require_once 'lib/admin_search.inc.php'; + + // This search just died a little inside, so it should be safe to + // continue here but we nevertheless return just to be sure + return; + } elseif ($i_view === 'new') { + closeObject(); + Navigation::activateItem('/admin/institute/create'); + } else { + Navigation::activateItem('/admin/institute/avatar'); + } + + // allow only inst-admin and root to view / edit + if ($i_view && !$GLOBALS['perm']->have_studip_perm('admin', $i_view) && $i_view !== 'new') { + throw new AccessDeniedException(); + } + + PageLayout::setTitle(Context::getHeaderLine() . ' - ' . _('Einrichtungsbild ändern')); + $this->institute_id = Context::getId(); + $avatar = InstituteAvatar::getAvatar($this->institute_id); + $this->avatar_url = $avatar->getURL(Avatar::NORMAL); + + } +} \ No newline at end of file diff --git a/app/controllers/institute/basicdata.php b/app/controllers/institute/basicdata.php index 9c800fd..0b7c296 100644 --- a/app/controllers/institute/basicdata.php +++ b/app/controllers/institute/basicdata.php @@ -35,8 +35,6 @@ class Institute_BasicdataController extends AuthenticatedController { PageLayout::setTitle(_('Verwaltung der Grunddaten')); - PageLayout::addSqueezePackage('avatar'); - //get ID from an open Institut $i_view = $i_id ?: Request::option('i_view', Context::getId()); diff --git a/app/controllers/settings/avatar.php b/app/controllers/settings/avatar.php new file mode 100644 index 0000000..31bd705 --- /dev/null +++ b/app/controllers/settings/avatar.php @@ -0,0 +1,23 @@ +login_if($action !== 'logout' && $GLOBALS['auth']->auth['uid'] === 'nobody'); + + if (!$GLOBALS['perm']->have_profile_perm('user', User::findCurrent()->id)) { + throw new AccessDeniedException(_('Sie dürfen dieses Profil nicht bearbeiten')); + } + } + public function index_action() + { + PageLayout::setTitle(_('Profilbild anpassen')); + Navigation::activateItem('/profile/edit/avatar'); + $this->user_id = User::findCurrent()->id; + $avatar = Avatar::getAvatar($this->user_id); + $this->avatar_url = $avatar->getURL(Avatar::NORMAL); + } +} \ No newline at end of file diff --git a/app/views/avatar/update.php b/app/views/avatar/update.php deleted file mode 100644 index f995702..0000000 --- a/app/views/avatar/update.php +++ /dev/null @@ -1,75 +0,0 @@ - -
-
- - - - - -
-
- 'submit-avatar']) ?> - - url_for('avatar/delete', $type, $id) - ) ?> - - -
-
diff --git a/app/views/course/avatar/index.php b/app/views/course/avatar/index.php new file mode 100644 index 0000000..4899aad --- /dev/null +++ b/app/views/course/avatar/index.php @@ -0,0 +1,7 @@ +
+
\ No newline at end of file diff --git a/app/views/course/studygroup/avatar.php b/app/views/course/studygroup/avatar.php new file mode 100644 index 0000000..ed3eeec --- /dev/null +++ b/app/views/course/studygroup/avatar.php @@ -0,0 +1,7 @@ +
+
\ No newline at end of file diff --git a/app/views/institute/avatar/index.php b/app/views/institute/avatar/index.php new file mode 100644 index 0000000..fee3c00 --- /dev/null +++ b/app/views/institute/avatar/index.php @@ -0,0 +1,7 @@ +
+
\ No newline at end of file diff --git a/app/views/institute/basicdata/index.php b/app/views/institute/basicdata/index.php index abb0487..5c36491 100644 --- a/app/views/institute/basicdata/index.php +++ b/app/views/institute/basicdata/index.php @@ -142,23 +142,3 @@ - -isNew()) { - $widget = new ActionsWidget(); - $widget->addLink( - _('Infobild ändern'), - URLHelper::getURL('dispatch.php/avatar/update/institute/' . $institute->id), - Icon::create('edit') - )->asDialog(); - if (InstituteAvatar::getAvatar($institute->id)->is_customized()) { - $widget->addLink( - _('Infobild löschen'), - URLHelper::getURL('dispatch.php/avatar/delete/institute/' . $institute->id), - Icon::create('trash') - ); - } - $sidebar->addWidget($widget); -} diff --git a/app/views/profile/widget-avatar.php b/app/views/profile/widget-avatar.php index 157b74a..74b142c 100644 --- a/app/views/profile/widget-avatar.php +++ b/app/views/profile/widget-avatar.php @@ -1,17 +1,11 @@
have_profile_perm('user', $current_user)) : ?> + href=""> getImageTag(Avatar::NORMAL) ?>
- -
- +
diff --git a/app/views/settings/avatar/index.php b/app/views/settings/avatar/index.php new file mode 100644 index 0000000..cee9ba4 --- /dev/null +++ b/app/views/settings/avatar/index.php @@ -0,0 +1,7 @@ +
+
\ No newline at end of file diff --git a/lib/classes/Avatar.php b/lib/classes/Avatar.php index 959523f..d2dcd9d 100644 --- a/lib/classes/Avatar.php +++ b/lib/classes/Avatar.php @@ -632,4 +632,9 @@ class Avatar imagedestroy($img); } } + + public function getId() + { + return $this->user_id; + } } diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index 63c69e6..e9a0a01 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -125,6 +125,7 @@ class RouteMap $this->addAuthenticatedCoursewareRoutes($group); } + $this->addAuthenticatedAvatarRoutes($group); $this->addAuthenticatedEventsRoutes($group); $this->addAuthenticatedFeedbackRoutes($group); $this->addAuthenticatedFilesRoutes($group); @@ -650,6 +651,14 @@ class RouteMap $group->post('/stock-images/{id}/blob', Routes\StockImages\StockImagesUpload::class); } + private function addAuthenticatedAvatarRoutes(RouteCollectorProxy $group): void + { + $group->get('/{type:courses|institutes|users}/{id}/avatar', Routes\Avatar\AvatarOfRangeShow::class); + $group->delete('/{type:courses|institutes|users}/{id}/avatar', Routes\Avatar\AvatarofRangeDelete::class); + + $group->post('/{type:courses|institutes|users}/{id}/avatar', Routes\Avatar\AvatarUpload::class); + } + private function addRelationship(RouteCollectorProxy $group, string $url, string $handler): void { $group->map(['GET', 'PATCH', 'POST', 'DELETE'], $url, $handler); diff --git a/lib/classes/JsonApi/Routes/Avatar/Authority.php b/lib/classes/JsonApi/Routes/Avatar/Authority.php new file mode 100644 index 0000000..20166e7 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Avatar/Authority.php @@ -0,0 +1,29 @@ +hasPermissionLevel('user', $user); + } + public static function canUpdateAvatarOfInstitute(User $user, Institute $institute): bool + { + return $user->hasPermissionLevel('admin', $institute); + } + public static function canUpdateAvatarOfSeminar(User $user, Course $course): bool + { + return $user->hasPermissionLevel('tutor', $course); + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Avatar/AvatarHelpers.php b/lib/classes/JsonApi/Routes/Avatar/AvatarHelpers.php new file mode 100644 index 0000000..b288d08 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Avatar/AvatarHelpers.php @@ -0,0 +1,40 @@ +isStudygroup()) { + $class = \StudygroupAvatar::class; + } else { + $class = \CourseAvatar::class; + } + } + } else { + throw new RecordNotFoundException(); + } + + return ['class' => $class, 'has_perm' => $has_perm]; + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Avatar/AvatarOfRangeShow.php b/lib/classes/JsonApi/Routes/Avatar/AvatarOfRangeShow.php new file mode 100644 index 0000000..2857295 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Avatar/AvatarOfRangeShow.php @@ -0,0 +1,34 @@ +getUser($request); + + ['class' => $class] = self::getAvatarClass($range_id, $range_type, $user); + + $resource = $class::getAvatar($range_id); + + if (!$resource) { + throw new RecordNotFoundException(); + } + + if (!Authority::canShowAvatarOfRange($this->getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($resource); + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Avatar/AvatarUpload.php b/lib/classes/JsonApi/Routes/Avatar/AvatarUpload.php new file mode 100644 index 0000000..777d65c --- /dev/null +++ b/lib/classes/JsonApi/Routes/Avatar/AvatarUpload.php @@ -0,0 +1,66 @@ +getUser($request); + $json = $this->validate($request); + $range_id = self::arrayGet($json, 'data.range-id'); + $range_type = self::arrayGet($json, 'data.range-type'); + + ['class' => $class, 'has_perm' => $has_perm] = self::getAvatarClass($range_id, $range_type, $user); + + if (!$has_perm) { + throw new AuthorizationFailedException(); + } + + $avatar = $class::getAvatar($range_id); + $imgdata_string = self::arrayGet($json, 'data.image'); + [$type, $imgdata_part] = explode(';', $imgdata_string); + [$base, $imgdata_base64] = explode(',', $imgdata_part); + $imgdata = base64_decode($imgdata_base64); + // Write data to file. + $filename = $GLOBALS['TMP_PATH'] . '/avatar-' . $range_id . '.webp'; + file_put_contents($filename, $imgdata); + + // Use new image file for avatar creation. + $avatar->createFrom($filename); + + + return $response->withStatus(201); + } + + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (!self::arrayHas($json, 'data.range-id')) { + return 'New avatar must have an `range-id`.'; + } + if (!self::arrayHas($json, 'data.range-type')) { + return 'New avatar must have a `range-type`.'; + } + if (!self::arrayHas($json, 'data.image')) { + return 'New avatar must have a `image`.'; + } + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Avatar/AvatarofRangeDelete.php b/lib/classes/JsonApi/Routes/Avatar/AvatarofRangeDelete.php new file mode 100644 index 0000000..05353cb --- /dev/null +++ b/lib/classes/JsonApi/Routes/Avatar/AvatarofRangeDelete.php @@ -0,0 +1,36 @@ +getUser($request); + + ['class' => $class, 'has_perm' => $has_perm] = self::getAvatarClass($range_id, $range_type, $user); + + if (!$has_perm) { + throw new AuthorizationFailedException(); + } + + $class::getAvatar($range_id)->reset(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index 1498daf..ff5040d 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -16,6 +16,8 @@ class SchemaMap \JsonApi\Models\ScheduleEntry::class => Schemas\ScheduleEntry::class, + \Avatar::class => Schemas\Avatar::class, + \BlubberComment::class => Schemas\BlubberComment::class, \BlubberStatusgruppeThread::class => Schemas\BlubberStatusgruppeThread::class, \BlubberThread::class => Schemas\BlubberThread::class, diff --git a/lib/classes/JsonApi/Schemas/Avatar.php b/lib/classes/JsonApi/Schemas/Avatar.php new file mode 100644 index 0000000..5926f59 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Avatar.php @@ -0,0 +1,69 @@ +getId(); + } + + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'type' => $resource::AVATAR_TYPE, + 'customized' => $resource->is_customized(), + 'is-nobody' => $resource->isNobody(), + ]; + } + public function hasResourceMeta($resource): bool + { + return true; + } + + public function getResourceMeta($resource) + { + return [ + 'url' => [ + 'normal' => $resource->getURL(\Avatar::NORMAL), + 'medium' => $resource->getURL(\Avatar::MEDIUM), + 'small' => $resource->getURL(\Avatar::SMALL), + ] + ]; + } + + public function getRelationships($resource, ContextInterface $context): iterable + { + $relationships = []; + $range = self::getRange($resource->getId(), $resource::AVATAR_TYPE); + $relationships[self::REL_RANGE] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($range), + ], + self::RELATIONSHIP_DATA => $range, + ]; + return $relationships; + } + + private function getRange(String $range_id, String $range_type) + { + switch ($range_type) { + case 'course': + case 'studygroup': + return \Course::build(['id' => $range_id], false); + case 'user': + return \User::build(['id' => $range_id], false); + case 'institute': + return \Institute::build(['id' => $range_id], false); + } + return null; + } +} \ No newline at end of file diff --git a/lib/modules/CoreAdmin.php b/lib/modules/CoreAdmin.php index 4bf1880..8184976 100644 --- a/lib/modules/CoreAdmin.php +++ b/lib/modules/CoreAdmin.php @@ -38,8 +38,7 @@ class CoreAdmin extends CorePlugin implements StudipModule $item->setDescription(_('Bearbeiten der Grundeinstellungen dieser Veranstaltung.')); $navigation->addSubNavigation('details', $item); - $item = new Navigation(_('Infobild'), 'dispatch.php/avatar/update/course/' . $course_id); - $item->setImage(Icon::create('file-pic')); + $item = new Navigation(_('Veranstaltungsbild'), 'dispatch.php/course/avatar'); $item->setDescription(_('Infobild dieser Veranstaltung bearbeiten oder löschen.')); $navigation->addSubNavigation('avatar', $item); diff --git a/lib/modules/CoreStudygroupAdmin.php b/lib/modules/CoreStudygroupAdmin.php index d299adf..60819c9 100644 --- a/lib/modules/CoreStudygroupAdmin.php +++ b/lib/modules/CoreStudygroupAdmin.php @@ -37,7 +37,7 @@ class CoreStudygroupAdmin extends CorePlugin implements StudipModule $navigation->addSubNavigation('contentmodules', new Navigation(_('Werkzeuge'), "dispatch.php/course/contentmodules?cid={$course_id}")); $navigation->addSubNavigation('main', new Navigation(_('Verwaltung'), "dispatch.php/course/studygroup/edit/?cid={$course_id}")); - $navigation->addSubNavigation('avatar', new Navigation(_('Infobild'), "dispatch.php/avatar/update/course/{$course_id}?cid={$course_id}")); + $navigation->addSubNavigation('avatar', new Navigation(_(' Studiengruppenbild'), "dispatch.php/course/studygroup/avatar?cid={$course_id}")); if (!$GLOBALS['perm']->have_perm('admin') && Config::get()->VOTE_ENABLE) { $item = new Navigation(_('Fragebögen'), 'dispatch.php/questionnaire/courseoverview'); diff --git a/lib/navigation/AdminNavigation.php b/lib/navigation/AdminNavigation.php index 3e63876..1dccf0e 100644 --- a/lib/navigation/AdminNavigation.php +++ b/lib/navigation/AdminNavigation.php @@ -72,6 +72,7 @@ class AdminNavigation extends Navigation $navigation->setURL('dispatch.php/institute/basicdata/index?cid='); $navigation->addSubNavigation('details', new Navigation(_('Grunddaten'), 'dispatch.php/institute/basicdata/index')); + $navigation->addSubNavigation('avatar', new Navigation(_('Einrichtungsbild'), 'dispatch.php/institute/avatar/index')); $navigation->addSubNavigation('faculty', new Navigation(_('Mitarbeiter'), 'dispatch.php/institute/members?admin_view=1')); $navigation->addSubNavigation('groups', new Navigation(_('Funktionen / Gruppen'), 'dispatch.php/admin/statusgroups?type=inst')); diff --git a/lib/navigation/ProfileNavigation.php b/lib/navigation/ProfileNavigation.php index 307cd98..2cf4574 100644 --- a/lib/navigation/ProfileNavigation.php +++ b/lib/navigation/ProfileNavigation.php @@ -62,6 +62,7 @@ class ProfileNavigation extends Navigation // profile data $navigation = new Navigation(_('Persönliche Angaben')); $navigation->addSubNavigation('profile', new Navigation(_('Grunddaten'), 'dispatch.php/settings/account')); + $navigation->addSubNavigation('avatar', new Navigation(_('Profilbild'), 'dispatch.php/settings/avatar')); if ( !StudipAuthAbstract::CheckField('auth_user_md5.password', $current_user->auth_plugin) && ( diff --git a/public/assets/images/icons/blue/flip.svg b/public/assets/images/icons/blue/flip.svg new file mode 100644 index 0000000..3e3bd45 --- /dev/null +++ b/public/assets/images/icons/blue/flip.svg @@ -0,0 +1,44 @@ + + + + + + + diff --git a/resources/assets/javascripts/bootstrap/avatar.js b/resources/assets/javascripts/bootstrap/avatar.js index cbda588..31d724d 100644 --- a/resources/assets/javascripts/bootstrap/avatar.js +++ b/resources/assets/javascripts/bootstrap/avatar.js @@ -1,54 +1,17 @@ STUDIP.domReady(() => { - STUDIP.Avatar.init('#avatar-upload'); - - // Get file data on drop - var dropZone = document.getElementById('avatar-overlay'); - - if (dropZone) { - dropZone.addEventListener('dragover', function(e) { - e.stopPropagation(); - e.preventDefault(); - e.target.parentNode.classList.add("dragging"); - }); - - dropZone.addEventListener('dragleave', function(e) { - e.stopPropagation(); - e.preventDefault(); - e.target.parentNode.classList.remove("dragging"); - }); - - dropZone.addEventListener('drop', function(e) { - e.stopPropagation(); - e.preventDefault(); - e.target.parentNode.classList.remove("dragging"); - var files = e.dataTransfer.files; - var div = e.target.parentNode; - var avatar_dialog = div.getElementsByTagName('a')[0]; - - if (!div.getAttribute('accept') || !div.getAttribute('accept').includes(files[0].type)) { - alert(div.getAttribute('data-message-unaccepted')); - return false; - } - - if (!div.getAttribute('data-max-size') || files[0].size > div.getAttribute('data-max-size')) { - alert(div.getAttribute('data-message-too-large')); - return false; - } - - avatar_dialog.click(); - div.files = files; - STUDIP.dialogReady(() => { - STUDIP.Avatar.readFile(div); + const avatarTypes = ['courses', 'institutes', 'studygroups', 'users']; + + avatarTypes.forEach((type) => { + if (document.getElementById(`avatar-${type}-app`)) { + Promise.all([ + STUDIP.loadChunk('avatar'), + import( + /* webpackChunkName: "avatar-app" */ + '@/vue/avatar-app.js' + ), + ]).then(([{ createApp }, { default: mountApp }]) => { + return mountApp(STUDIP, createApp, `#avatar-${type}-app`); }); - }); - } - - //"Redirecting" the event is necessary so that the avatar image upload - //is accessible by pressing the enter key when its focused. - jQuery(document).on('keydown', 'form.settings-avatar label.file-upload a.button', function(event) { - if (event.code == "Enter") { - //The enter key has been pressed. - jQuery(this).parent('.file-upload').trigger('click'); } }); }); diff --git a/resources/assets/javascripts/chunk-loader.js b/resources/assets/javascripts/chunk-loader.js index f372286..995b35f 100644 --- a/resources/assets/javascripts/chunk-loader.js +++ b/resources/assets/javascripts/chunk-loader.js @@ -21,15 +21,6 @@ let mathjax_promise = null; export const loadChunk = function (chunk, { silent = false } = {}) { let promise = null; switch (chunk) { - case 'code-highlight': - promise = import( - /* webpackChunkName: "code-highlight" */ - './chunks/code-highlight' - ).then(({ default: hljs }) => { - return hljs; - }); - break; - case 'courseware': promise = Promise.all([ STUDIP.loadChunk('vue'), @@ -40,6 +31,25 @@ export const loadChunk = function (chunk, { silent = false } = {}) { ]).then(([Vue]) => Vue); break; + case 'avatar': + promise = Promise.all([ + STUDIP.loadChunk('vue'), + import( + /* webpackChunkName: "avatar" */ + './chunks/avatar' + ), + ]).then(([Vue]) => Vue); + break; + + case 'code-highlight': + promise = import( + /* webpackChunkName: "code-highlight" */ + './chunks/code-highlight' + ).then(({default: hljs}) => { + return hljs; + }); + break; + case 'chartist': promise = import( /* webpackChunkName: "chartist" */ diff --git a/resources/assets/javascripts/chunks/avatar.js b/resources/assets/javascripts/chunks/avatar.js new file mode 100644 index 0000000..e69de29 diff --git a/resources/vue/avatar-app.js b/resources/vue/avatar-app.js new file mode 100644 index 0000000..0092ebe --- /dev/null +++ b/resources/vue/avatar-app.js @@ -0,0 +1,76 @@ +import AvatarApp from './components/avatar/AvatarApp.vue'; +import AvatarModule from './store/avatar.module'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import { mapResourceModules } from '@elan-ev/reststate-vuex'; +import axios from 'axios'; + +const mountApp = async (STUDIP, createApp, element) => { + + let entry_id = null; + let entry_type = null; + let avatar_url = null; + let elem; + + if ((elem = document.getElementById(element.substring(1))) !== undefined) { + if (elem.attributes !== undefined) { + if (elem.attributes['entry-type'] !== undefined) { + entry_type = elem.attributes['entry-type'].value; + } + + if (elem.attributes['entry-id'] !== undefined) { + entry_id = elem.attributes['entry-id'].value; + } + + if (elem.attributes['avatar-url'] !== undefined) { + avatar_url = elem.attributes['avatar-url'].value; + } + } + } + + const getHttpClient = () => + axios.create({ + baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true), + headers: { + 'Content-Type': 'application/vnd.api+json', + }, + }); + const httpClient = getHttpClient(); + + const store = new Vuex.Store({ + modules: { + 'avatar-module': AvatarModule, + ...mapResourceModules({ + names: [ + 'avatar', + 'courses', + 'institutes', + 'stock-images', + 'studygroups', + 'users', + ], + httpClient, + }), + } + }); + + const context = { + type: entry_type, + id: entry_id + } + store.dispatch('setUserId', STUDIP.USER_ID); + await store.dispatch('users/loadById', { id: STUDIP.USER_ID }); + store.dispatch('setHttpClient', httpClient); + store.dispatch('setContext', context); + const avatar = await store.dispatch('loadAvatar'); + + const app = createApp({ + render: (h) => h(AvatarApp), + store, + }); + app.$mount(element); + + return app; +} + +export default mountApp; \ No newline at end of file diff --git a/resources/vue/components/avatar/AvatarApp.vue b/resources/vue/components/avatar/AvatarApp.vue new file mode 100644 index 0000000..0e89b5e --- /dev/null +++ b/resources/vue/components/avatar/AvatarApp.vue @@ -0,0 +1,430 @@ + + + + diff --git a/resources/vue/store/avatar.module.js b/resources/vue/store/avatar.module.js new file mode 100644 index 0000000..7a45576 --- /dev/null +++ b/resources/vue/store/avatar.module.js @@ -0,0 +1,102 @@ +const getDefaultState = () => { + return { + context: null, + httpClient: null, + userId: null, + }; +}; + +const initialState = getDefaultState(); + +const getters = { + context(state) { + return state.context; + }, + httpClient(state) { + return state.httpClient; + }, + userId(state) { + return state.userId; + }, + + currentAvatar(state, getters, rootState, rootGetters) { + if (getters.context === null) { + return null; + } + const parent = { + type: getters.context.type, + id: getters.context.id, + }; + + const relationship = 'avatar'; + + return rootGetters['avatar/related']({ parent, relationship }); + }, + currentUser(state, getters, rootState, rootGetters) { + const id = getters.userId; + return rootGetters['users/byId']({ id }); + }, + isCourseAvatar(state, getters) { + return getters.context?.type === 'courses'; + }, + isInstituteAvatar(state, getters) { + return getters.context?.type === 'institutes'; + }, + isStudygroupAvatar(state, getters) { + return getters.context?.type === 'studygroups'; + }, + isUserAvatar(state, getters) { + return getters.context?.type === 'users'; + }, + isCustomized(state, getters) { + return getters.currentAvatar.attributes.customized; + } +}; + +export const state = { ...initialState }; + +export const actions = { + // setters + setContext({ commit }, context) { + commit('setContext', context); + }, + setHttpClient({ commit }, httpClient) { + commit('setHttpClient', httpClient); + }, + setUserId({ commit }, userId) { + commit('setUserId', userId); + }, + + // other actions + loadAvatar({ dispatch, getters, rootGetters }) { + const parent = { + type: getters.context.type, + id: getters.context.id, + }; + + const relationship = 'avatar'; + + return dispatch('avatar/loadRelated', { parent, relationship }, { root: true }).then(() => { + rootGetters['avatar/related']({ parent, relationship }); + }); + }, +}; + +export const mutations = { + setContext(state, context) { + state.context = context; + }, + setHttpClient(state, httpClient) { + state.httpClient = httpClient; + }, + setUserId(state, data) { + state.userId = data; + }, +}; + +export default { + state, + actions, + mutations, + getters, +}; -- cgit v1.0