Преглед изворни кода

feat: Implement profile pictures

jelveh пре 6 месеци
родитељ
комит
0885937f03

+ 2 - 1
src/gui/puter-gui.json

@@ -14,7 +14,8 @@
             "/lib/timeago.min.js",
             "/lib/iro.min.js",
             "/lib/isMobile.min.js",
-            "/lib/fflate-0.8.2.min.js"
+            "/lib/fflate-0.8.2.min.js",
+            "/lib/croppie.min.js"
         ],
         "css_paths": [
             "/css/normalize.css",

+ 53 - 1
src/gui/src/UI/Settings/UITabAccount.js

@@ -22,6 +22,7 @@ import UIWindowChangeEmail from './UIWindowChangeEmail.js';
 import UIWindowChangeUsername from '../UIWindowChangeUsername.js';
 import UIWindowConfirmUserDeletion from './UIWindowConfirmUserDeletion.js';
 import UIWindowManageSessions from '../UIWindowManageSessions.js';
+import UIWindow from '../UIWindow.js';
 
 // About
 export default {
@@ -29,7 +30,14 @@ export default {
     title_i18n_key: 'account',
     icon: 'user.svg',
     html: () => {
-        let h = `<h1>${i18n('account')}</h1>`;
+        let h = '';
+        // h += `<h1>${i18n('account')}</h1>`;
+
+        // profile picture
+        h += `<div style="overflow: hidden; display: flex; margin-bottom: 20px; flex-direction: column; align-items: center;">`;
+            h += `<div class="profile-picture change-profile-picture" style="background-image: url('${html_encode(window.user?.profile?.picture ?? window.icons['profile.svg'])}');">`;
+            h += `</div>`;
+        h += `</div>`;
 
         // change password button
         if(!window.user.is_temp){
@@ -125,5 +133,49 @@ export default {
                 }
             });
         });
+
+        $el_window.find('.change-profile-picture').on('click', async function (e) {
+            // open dialog
+            UIWindow({
+                path: '/' + window.user.username + '/Desktop',
+                // this is the uuid of the window to which this dialog will return
+                parent_uuid: $el_window.attr('data-element_uuid'),
+                allowed_file_types: ['.png', '.jpg', '.jpeg'],
+                show_maximize_button: false,
+                show_minimize_button: false,
+                title: 'Open',
+                is_dir: true,
+                is_openFileDialog: true,
+                selectable_body: false,
+            });    
+        })
+
+        $el_window.on('file_opened', async function(e){
+            let selected_file = Array.isArray(e.detail) ? e.detail[0] : e.detail;
+            // set profile picture
+            const profile_pic = await puter.fs.read(selected_file.path)
+            // blob to base64
+            const reader = new FileReader();
+            reader.readAsDataURL(profile_pic);
+            reader.onloadend = function() {
+                // resizes the image to 150x150
+                const img = new Image();
+                img.src = reader.result;
+                img.onload = function() {
+                    const canvas = document.createElement('canvas');
+                    const ctx = canvas.getContext('2d');
+                    canvas.width = 150;
+                    canvas.height = 150;
+                    ctx.drawImage(img, 0, 0, 150, 150);
+                    const base64data = canvas.toDataURL('image/png');
+                    // update profile picture
+                    $el_window.find('.profile-picture').css('background-image', 'url(' + html_encode(base64data) + ')');
+                    $('.profile-image').css('background-image', 'url(' + html_encode(base64data) + ')');
+                    $('.profile-image').addClass('profile-image-has-picture');
+                    // update profile picture
+                    update_profile(window.user.username, {picture: base64data})
+                }
+            }
+        })
     },
 };

+ 2 - 2
src/gui/src/UI/UIDesktop.js

@@ -1052,8 +1052,8 @@ async function UIDesktop(options){
         ht += `<div class="toolbar-btn search-btn" title="Search" style="background-image:url('${window.icons['search.svg']}')"></div>`;
 
         // user options menu
-        ht += `<div class="toolbar-btn user-options-menu-btn" style="background-image:url(${window.icons['profile.svg']})">`;
-            h += `<span class="user-options-menu-username">${window.user.username}</span>`;
+        ht += `<div class="toolbar-btn user-options-menu-btn profile-pic" style="display:block;">`;
+            ht += `<div class="profile-image ${window.user?.profile?.picture && 'profile-image-has-picture'}" style="border-radius: 50%; background-image:url(${window.user?.profile?.picture || window.icons['profile.svg']}); box-sizing: border-box; width: 17px !important; height: 17px !important; background-size: contain; background-repeat: no-repeat; background-position: center; background-position: center; background-size: cover;"></div>`;
         ht += `</div>`;
     ht += `</div>`;
 

+ 6 - 2
src/gui/src/UI/UIWindowSessionList.js

@@ -32,10 +32,14 @@ async function UIWindowSessionList(options){
             h += `<div class="loading">${i18n('signing_in')}</div>`;
             // session list
             h += `<div class="hide-scrollbar" style="overflow-y: scroll; max-width: 400px; margin: 0 auto;">`;
-                h += `<h1 style="text-align: center; font-size: 18px; font-weight: normal; color: #757575;"><img src="${window.icons['logo-white.svg']}" style="padding: 4px; background-color: blue; border-radius: 5px; width: 25px; box-sizing: border-box; margin-bottom: -6px; margin-right: 6px;">${i18n('sign_in_with_puter')}</h1>`
+                h += `<h1 style="text-align: center; font-size: 18px; font-weight: normal; color: #757575; margin-bottom: 30px;"><img src="${window.icons['logo-white.svg']}" style="padding: 4px; background-color: blue; border-radius: 5px; width: 25px; box-sizing: border-box; margin-bottom: -6px; margin-right: 6px;">${i18n('sign_in_with_puter')}</h1>`
                 for (let index = 0; index < window.logged_in_users.length; index++) {
                     const l_user = window.logged_in_users[index];
-                    h += `<div data-uuid="${l_user.uuid}" class="session-entry">${l_user.username}</div>`;
+                    h += `<div data-uuid="${l_user.uuid}" class="session-entry" style="display: flex; padding: 15px 10px;">`;
+                        // profile picture
+                        h += `<div class="profile-picture" style="background-color: #cbced1; width: 30px; height: 30px; margin:0; margin-right: 10px; background-image: url('${l_user.profile.picture ?? window.icons['profile.svg']}');"></div>`;
+                        h += `<div style="display: flex; align-items: center;">${l_user.username}</div>`;
+                    h += `</div>`;
                 }
             h += `</div>`;
             // c2a

+ 94 - 44
src/gui/src/css/style.css

@@ -79,7 +79,7 @@
     font-variation-settings: "slnt"0;
 }
 
-pre{
+pre {
     font-family: "Inter", "Helvetica Neue", HelveticaNeue, Helvetica, Arial, sans-serif;
 }
 
@@ -611,7 +611,7 @@ span.header-sort-icon img {
 }
 
 .item-name, .item-name-editor, .item-name-shadow {
-    font-size: 13px;
+    font-size: 12px;
     color: white;
     text-shadow: 0px 0px 3px #00000082, 0px 0px 3px #00000082, 0px 0px 3px #00000082;
     -webkit-font-smoothing: antialiased;
@@ -1263,27 +1263,32 @@ span.header-sort-icon img {
     background-color: #fefeff;
 }
 
-.window-sidebar-item-placeholder{
+.window-sidebar-item-placeholder {
     height: 27px !important;
 }
+
 .window-sidebar-item {
     cursor: pointer !important;
     user-select: none;
 }
+
 .window-sidebar-item:not(.window-sidebar-title):hover {
     cursor: grab;
 }
+
 .window-sidebar-item.grabbing {
     cursor: grabbing !important;
 }
+
 .window-sidebar-item-dragging {
     background-color: #f5f5f5 !important;
     opacity: 0.8;
     cursor: grabbing;
 }
+
 .ui-sortable-helper {
     background: white !important;
-    box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
 }
 
 .window-sidebar-item-icon {
@@ -1362,16 +1367,17 @@ span.header-sort-icon img {
     flex-grow: 1;
 }
 
-.window-filedialog-upload-here{
+.window-filedialog-upload-here {
     -webkit-font-smoothing: antialiased;
     opacity: 0.7;
     font-size: 14px;
 }
 
-.window-filedialog-upload-here:hover{
+.window-filedialog-upload-here:hover {
     cursor: pointer;
     opacity: 1;
 }
+
 .savefiledialog-save-btn, .openfiledialog-open-btn {
     margin-left: 10px;
 }
@@ -1590,7 +1596,8 @@ span.header-sort-icon img {
     border-bottom: none;
     border-top: 1px solid #00000033;
 }
-.context-menu .context-menu-divider{
+
+.context-menu .context-menu-divider {
     padding-top: 5px;
     padding-bottom: 5px;
 }
@@ -2115,11 +2122,11 @@ label {
     background-color: #9dacbd;
 }
 
-.permission-editor-badge{
+.permission-editor-badge {
     background-color: #007cff;
 }
 
-.permission-viewer-badge{
+.permission-viewer-badge {
     background-color: #41c95d;
 }
 
@@ -2168,20 +2175,24 @@ label {
     max-height: 200px;
     overflow: hidden;
 }
-.ui-menu{
+
+.ui-menu {
     margin-top: 5px;
     border-radius: 5px;
 }
-.ui-menu .ui-menu-item{
+
+.ui-menu .ui-menu-item {
     padding: 5px 10px;
     border-radius: 5px;
 }
+
 .ui-menu .ui-menu-item .ui-menu-item-wrapper {
     background: none;
     border: none;
     padding: 5px 10px;
     font-size: 14px;
 }
+
 .ui-menu .ui-menu-item:hover .ui-menu-item-wrapper,
 .ui-menu .ui-menu-item:focus .ui-menu-item-wrapper,
 .ui-menu .ui-menu-item:active .ui-menu-item-wrapper,
@@ -2856,13 +2867,14 @@ fieldset[name=number-code] {
     opacity: 1;
 }
 
-.welcome-window-close-button{
+.welcome-window-close-button {
     opacity: 0.7;
     font-weight: 300;
     top: 5px;
     right: 5px;
 }
-.welcome-window-close-button:hover{
+
+.welcome-window-close-button:hover {
     opacity: 1;
 }
 
@@ -3690,12 +3702,12 @@ fieldset[name=number-code] {
     margin-top: 1px;
 }
 
-.settings-sidebar-title{
-    margin-bottom: 20px; 
-    font-weight: bold; 
-    -webkit-font-smoothing: antialiased; 
-    margin-top: 15px; 
-    color: #8c8c8c; 
+.settings-sidebar-title {
+    margin-bottom: 20px;
+    font-weight: bold;
+    -webkit-font-smoothing: antialiased;
+    margin-top: 15px;
+    color: #8c8c8c;
     font-size: 19px;
 }
 
@@ -3835,6 +3847,30 @@ fieldset[name=number-code] {
     opacity: 1;
 }
 
+.profile-picture {
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+    background-position: center;
+    background-size: cover;
+    background-repeat: no-repeat;
+    border: 1px solid #EEE;
+    width: 120px;
+    height: 120px;
+    border-radius: 50%;
+    margin-right: 0;
+    margin-top: 20px;
+    margin-bottom: 20px;
+    background-color: #c5cdd4;
+}
+
+.profile-picture:hover {
+    background-color: #a6afb7;
+}
+
+.profile-image-has-picture{
+    border: 1px solid white;
+}
 .driver-usage {
     background-color: white;
     bottom: 0;
@@ -3982,7 +4018,7 @@ fieldset[name=number-code] {
     background-color: #f6f6f6;
 }
 
-.language-item .checkmark{
+.language-item .checkmark {
     width: 15px;
     height: 15px;
     border-radius: 50%;
@@ -3991,9 +4027,11 @@ fieldset[name=number-code] {
     position: absolute;
     right: 10px;
 }
+
 .language-item.active {
     background-color: #e0e0e0;
 }
+
 .language-item.active .checkmark {
     display: inline-block;
 }
@@ -4011,9 +4049,11 @@ fieldset[name=number-code] {
     align-items: center;
     height: 45px;
 }
-.settings-card .button{
+
+.settings-card .button {
     box-shadow: none;
 }
+
 .thin-card {
     padding: 0 15px;
 }
@@ -4193,6 +4233,7 @@ fieldset[name=number-code] {
     .visible-xs {
         display: block !important;
     }
+
     .settings-sidebar {
         display: none;
         position: fixed;
@@ -4205,6 +4246,7 @@ fieldset[name=number-code] {
     .visible-sm {
         display: block !important;
     }
+
     .settings-sidebar {
         display: none;
         position: fixed;
@@ -4231,46 +4273,49 @@ fieldset[name=number-code] {
     }
 }
 
-.sidebar-toggle{
-	position: absolute;
+.sidebar-toggle {
+    position: absolute;
     z-index: 9999999999;
     left: 2px;
-	border: 0;
-	padding-top: 5px;
-	padding-bottom: 5px;
+    border: 0;
+    padding-top: 5px;
+    padding-bottom: 5px;
     top: 3px;
 }
+
 .sidebar-toggle .sidebar-toggle-button {
-	height: 20px;
+    height: 20px;
     width: 20px;
 }
 
 .sidebar-toggle span:nth-child(1) {
-	margin-top: 5px;
+    margin-top: 5px;
 }
 
 .sidebar-toggle span {
-	border-bottom: 2px solid #858585;
-	display: block;
-	margin-bottom: 5px;
-	width: 100%;
+    border-bottom: 2px solid #858585;
+    display: block;
+    margin-bottom: 5px;
+    width: 100%;
 }
+
 .settings-sidebar.active {
     display: block;
 }
 
-.welcome-window-footer{
-    position: absolute; bottom: 20px;
+.welcome-window-footer {
+    position: absolute;
+    bottom: 20px;
 }
 
-.welcome-window-footer a{
+.welcome-window-footer a {
     color: #727c8d;
     text-decoration: none;
     font-size: 12px;
     -webkit-font-smoothing: antialiased;
 }
 
-.welcome-window-footer a:hover{
+.welcome-window-footer a:hover {
     color: #1d1e23;
 }
 
@@ -4279,7 +4324,7 @@ fieldset[name=number-code] {
 * Search
 * ------------------------------------
 */
-.search-input-wrapper{
+.search-input-wrapper {
     width: 100%;
     border-radius: 5px;
     padding-bottom: 10px;
@@ -4290,13 +4335,15 @@ fieldset[name=number-code] {
     box-sizing: border-box;
     background: #f1f6fc;
 }
-.search-input{
+
+.search-input {
     padding-left: 33px !important;
     background-repeat: no-repeat;
     background-position: 5px center;
     background-size: 20px;
 }
-.search-results{
+
+.search-results {
     padding-right: 15px;
     margin-top: 70px;
     padding-left: 15px;
@@ -4304,18 +4351,21 @@ fieldset[name=number-code] {
     padding-bottom: 5px;
     display: none;
 }
-.search-result{
-    padding: 10px; cursor: pointer;
+
+.search-result {
+    padding: 10px;
+    cursor: pointer;
     font-size: 13px;
     display: flex;
     align-items: center;
 }
-.search-result-active{
+
+.search-result-active {
     background-color: #4092da;
     color: #fff;
     border-radius: 5px;
 }
+
 .search-results .search-result:last-child {
     margin-bottom: 0;
-}
-  
+}

+ 55 - 0
src/gui/src/helpers.js

@@ -445,6 +445,35 @@ window.update_auth_data = async (auth_token, user)=>{
         $('.user-email').html(html_encode(user.email));
     }
 
+    // ----------------------------------------------------
+    // get .profile file and update user profile
+    // ----------------------------------------------------
+    user.profile = {};
+    puter.fs.read('/'+user.username+'/Public/.profile').then((blob)=>{
+        blob.text()
+        .then(text => {
+            const profile = JSON.parse(text);
+            if(profile.picture){
+                window.user.profile.picture = html_encode(profile.picture);
+            }
+
+            // update profile picture in GUI
+            if(window.user.profile.picture){
+                $('.profile-pic').css('background-image', 'url('+window.user.profile.picture+')');
+            }
+        })
+        .catch(error => {
+            console.error('Error converting Blob to JSON:', error);
+        });
+    }).catch((e)=>{
+        if(e?.code === "subject_does_not_exist"){
+            // create .profile file
+            puter.fs.write('/'+user.username+'/Public/.profile', JSON.stringify({}));
+        }
+    });
+
+    // ----------------------------------------------------
+
     const to_storable_user = user => {
         const storable_user = {...user};
         delete storable_user.taskbar_items;
@@ -2597,3 +2626,29 @@ window.detectHostOS = function(){
     }
 }
 
+window.update_profile = function(username, key_vals){
+    puter.fs.read('/'+username+'/Public/.profile').then((blob)=>{
+        blob.text()
+        .then(text => {
+            const profile = JSON.parse(text);
+
+            for (const key in key_vals) {
+                profile[key] = key_vals[key];
+                // update window.user.profile
+                window.user.profile[key] = key_vals[key];
+            }
+
+            puter.fs.write('/'+username+'/Public/.profile', JSON.stringify(profile));
+        })
+        .catch(error => {
+            console.error('Error converting Blob to JSON:', error);
+        });
+    }).catch((e)=>{
+        if(e?.code === "subject_does_not_exist"){
+            // create .profile file
+            puter.fs.write('/'+username+'/Public/.profile', JSON.stringify({}));
+        }
+        // Ignored
+        console.log(e);
+    });
+}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
src/gui/src/lib/croppie.min.js


+ 1 - 0
src/gui/src/static-assets.js

@@ -32,6 +32,7 @@ const lib_paths =[
     `/lib/iro.min.js`,
     `/lib/isMobile.min.js`,
     `/lib/fflate-0.8.2.min.js`,
+    `/lib/croppie.min.js`
 ]
 
 // Ordered list of CSS stylesheets

+ 2 - 1
src/gui/webpack/libPaths.cjs

@@ -11,5 +11,6 @@ module.exports = [
     "timeago.min.js",
     "iro.min.js",
     "isMobile.min.js",
-    "fflate-0.8.2.min.js"
+    "fflate-0.8.2.min.js",
+    "croppie.min.js"
 ];

Неке датотеке нису приказане због велике количине промена