diff --git a/src/gui/puter-gui.json b/src/gui/puter-gui.json index 3f40f762..9ad5a666 100644 --- a/src/gui/puter-gui.json +++ b/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", diff --git a/src/gui/src/UI/Settings/UITabAccount.js b/src/gui/src/UI/Settings/UITabAccount.js index 47a027d7..95de65cf 100644 --- a/src/gui/src/UI/Settings/UITabAccount.js +++ b/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 = `

${i18n('account')}

`; + let h = ''; + // h += `

${i18n('account')}

`; + + // profile picture + h += `
`; + h += `
`; + h += `
`; + h += `
`; // 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}) + } + } + }) }, }; diff --git a/src/gui/src/UI/UIDesktop.js b/src/gui/src/UI/UIDesktop.js index 9c733886..21375340 100644 --- a/src/gui/src/UI/UIDesktop.js +++ b/src/gui/src/UI/UIDesktop.js @@ -1052,8 +1052,8 @@ async function UIDesktop(options){ ht += `
`; // user options menu - ht += `
`; - h += `${window.user.username}`; + ht += `
`; + ht += `
`; ht += `
`; ht += `
`; diff --git a/src/gui/src/UI/UIWindowSessionList.js b/src/gui/src/UI/UIWindowSessionList.js index 023b5546..c1bfdd65 100644 --- a/src/gui/src/UI/UIWindowSessionList.js +++ b/src/gui/src/UI/UIWindowSessionList.js @@ -32,10 +32,14 @@ async function UIWindowSessionList(options){ h += `
${i18n('signing_in')}
`; // session list h += `
`; - h += `

${i18n('sign_in_with_puter')}

` + h += `

${i18n('sign_in_with_puter')}

` for (let index = 0; index < window.logged_in_users.length; index++) { const l_user = window.logged_in_users[index]; - h += `
${l_user.username}
`; + h += `
`; + // profile picture + h += `
`; + h += `
${l_user.username}
`; + h += `
`; } h += `
`; // c2a diff --git a/src/gui/src/css/style.css b/src/gui/src/css/style.css index 0d595792..43fca3c8 100644 --- a/src/gui/src/css/style.css +++ b/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; -} - \ No newline at end of file +} \ No newline at end of file diff --git a/src/gui/src/helpers.js b/src/gui/src/helpers.js index 2657bcef..a968f59f 100644 --- a/src/gui/src/helpers.js +++ b/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); + }); +} \ No newline at end of file diff --git a/src/gui/src/lib/croppie.min.js b/src/gui/src/lib/croppie.min.js new file mode 100644 index 00000000..1223fd23 --- /dev/null +++ b/src/gui/src/lib/croppie.min.js @@ -0,0 +1 @@ +!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports&&"string"!=typeof exports.nodeName?module.exports=t():e.Croppie=t()}("undefined"!=typeof self?self:this,function(){"function"!=typeof Promise&&function(e){function n(e,t){return function(){e.apply(t,arguments)}}function r(e){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],u(e,n(i,this),n(o,this))}function a(n){var i=this;return null===this._state?void this._deferreds.push(n):void c(function(){var e=i._state?n.onFulfilled:n.onRejected;if(null!==e){var t;try{t=e(i._value)}catch(e){return void n.reject(e)}n.resolve(t)}else(i._state?n.resolve:n.reject)(i._value)})}function i(e){try{if(e===this)throw new TypeError("A promise cannot be resolved with itself.");if(e&&("object"==typeof e||"function"==typeof e)){var t=e.then;if("function"==typeof t)return void u(n(t,e),n(i,this),n(o,this))}this._state=!0,this._value=e,s.call(this)}catch(e){o.call(this,e)}}function o(e){this._state=!1,this._value=e,s.call(this)}function s(){for(var e=0,t=this._deferreds.length;en.top+t&&l.bottomn.left+e&&l.right=s.maxX&&(o.x=l.minX,n.x=s.maxX),n.x<=s.minX&&(o.x=l.maxX,n.x=s.minX),n.y>=s.maxY&&(o.y=l.minY,n.y=s.maxY),n.y<=s.minY&&(o.y=l.maxY,n.y=s.minY)}r(),I.call(t),F.call(t)}).call(i,{value:parseFloat(t.value),origin:new L(i.elements.preview),viewportRect:i.elements.viewport.getBoundingClientRect(),transform:E.parse(i.elements.preview)})}function n(e){var t,n;if("ctrl"===i.options.mouseWheelZoom&&!0!==e.ctrlKey)return 0;t=e.wheelDelta?e.wheelDelta/1200:e.deltaY?e.deltaY/1060:e.detail?e.detail/-60:0,n=i._currentZoom+t*i._currentZoom,e.preventDefault(),B.call(i,n),o.call(i)}x(e,"cr-slider-wrap"),x(t,"cr-slider"),t.type="range",t.step="0.0001",t.value="1",t.style.display=i.options.showZoomer?"":"none",t.setAttribute("aria-label","zoom"),i.element.appendChild(e),e.appendChild(t),i._currentZoom=1,i.elements.zoomer.addEventListener("input",o),i.elements.zoomer.addEventListener("change",o),i.options.mouseWheelZoom&&(i.elements.boundary.addEventListener("mousewheel",n),i.elements.boundary.addEventListener("DOMMouseScroll",n))}.call(a),a.options.enableResize&&function(){var l,u,c,h,p,e,t,d=this,m=document.createElement("div"),i=!1,f=50;x(m,"cr-resizer"),b(m,{width:this.options.viewport.width+"px",height:this.options.viewport.height+"px"}),this.options.resizeControls.height&&(x(e=document.createElement("div"),"cr-resizer-vertical"),m.appendChild(e));this.options.resizeControls.width&&(x(t=document.createElement("div"),"cr-resizer-horisontal"),m.appendChild(t));function n(e){if((void 0===e.button||0===e.button)&&(e.preventDefault(),!i)){var t=d.elements.overlay.getBoundingClientRect();if(i=!0,u=e.pageX,c=e.pageY,l=-1!==e.currentTarget.className.indexOf("vertical")?"v":"h",h=t.width,p=t.height,e.touches){var n=e.touches[0];u=n.pageX,c=n.pageY}window.addEventListener("mousemove",o),window.addEventListener("touchmove",o),window.addEventListener("mouseup",r),window.addEventListener("touchend",r),document.body.style[w]="none"}}function o(e){var t=e.pageX,n=e.pageY;if(e.preventDefault(),e.touches){var i=e.touches[0];t=i.pageX,n=i.pageY}var o=t-u,r=n-c,a=d.options.viewport.height+r,s=d.options.viewport.width+o;"v"===l&&f<=a&&a<=p?(b(m,{height:a+"px"}),d.options.boundary.height+=r,b(d.elements.boundary,{height:d.options.boundary.height+"px"}),d.options.viewport.height+=r,b(d.elements.viewport,{height:d.options.viewport.height+"px"})):"h"===l&&f<=s&&s<=h&&(b(m,{width:s+"px"}),d.options.boundary.width+=o,b(d.elements.boundary,{width:d.options.boundary.width+"px"}),d.options.viewport.width+=o,b(d.elements.viewport,{width:d.options.viewport.width+"px"})),z.call(d),W.call(d),Z.call(d),F.call(d),c=n,u=t}function r(){i=!1,window.removeEventListener("mousemove",o),window.removeEventListener("touchmove",o),window.removeEventListener("mouseup",r),window.removeEventListener("touchend",r),document.body.style[w]=""}e&&(e.addEventListener("mousedown",n),e.addEventListener("touchstart",n));t&&(t.addEventListener("mousedown",n),t.addEventListener("touchstart",n));this.elements.boundary.appendChild(m)}.call(a)}function R(){return this.options.enableExif&&window.EXIF}function B(e){if(this.options.enableZoom){var t=this.elements.zoomer,n=A(e,4);t.value=Math.max(parseFloat(t.min),Math.min(parseFloat(t.max),n)).toString()}}function Z(e){var t=this,n=t._currentZoom,i=t.elements.preview.getBoundingClientRect(),o=t.elements.viewport.getBoundingClientRect(),r=E.parse(t.elements.preview.style[g]),a=new L(t.elements.preview),s=o.top-i.top+o.height/2,l=o.left-i.left+o.width/2,u={},c={};if(e){var h=a.x,p=a.y,d=r.x,m=r.y;u.y=h,u.x=p,r.y=d,r.x=m}else u.y=s/n,u.x=l/n,c.y=(u.y-a.y)*(1-n),c.x=(u.x-a.x)*(1-n),r.x-=c.x,r.y-=c.y;var f={};f[v]=u.x+"px "+u.y+"px",f[g]=r.toString(),b(t.elements.preview,f)}function z(){if(this.elements){var e=this.elements.boundary.getBoundingClientRect(),t=this.elements.preview.getBoundingClientRect();b(this.elements.overlay,{width:t.width+"px",height:t.height+"px",top:t.top-e.top+"px",left:t.left-e.left+"px"})}}L.prototype.toString=function(){return this.x+"px "+this.y+"px"};var a,s,h,M,I=(a=z,s=500,function(){var e=this,t=arguments,n=h&&!M;clearTimeout(M),M=setTimeout(function(){M=null,h||a.apply(e,t)},s),n&&a.apply(e,t)});function F(){var e,t=this,n=t.get();X.call(t)&&(t.options.update.call(t,n),t.$&&"undefined"==typeof Prototype?t.$(t.element).trigger("update.croppie",n):(window.CustomEvent?e=new CustomEvent("update",{detail:n}):(e=document.createEvent("CustomEvent")).initCustomEvent("update",!0,!0,n),t.element.dispatchEvent(e)))}function X(){return 0l.max)?B.call(r,uthis._originalImageWidth&&(g=(d=this._originalImageWidth-h)/o*u),i<0&&(p=0,v=Math.abs(i)/r*c),m+p>this._originalImageHeight&&(w=(m=this._originalImageHeight-p)/r*c),l.drawImage(this.elements.preview,h,p,d,m,f,v,g,w),a&&(l.fillStyle="#fff",l.globalCompositeOperation="destination-in",l.beginPath(),l.arc(s.width/2,s.height/2,s.width/2,0,2*Math.PI,!0),l.closePath(),l.fill()),s}function k(c,h){var e,i,o,r,p=this,d=[],t=null,n=R.call(p);if("string"==typeof c)e=c,c={};else if(Array.isArray(c))d=c.slice();else{if(void 0===c&&p.data.url)return Y.call(p),F.call(p),null;e=c.url,d=c.points||[],t=void 0===c.zoom?null:c.zoom}return p.data.bound=!1,p.data.url=e||p.data.url,p.data.boundZoom=t,(i=e,o=n,r=new Image,r.style.opacity="0",new Promise(function(e,t){function n(){r.style.opacity="1",setTimeout(function(){e(r)},1)}r.removeAttribute("crossOrigin"),i.match(/^https?:\/\/|^\/\//)&&r.setAttribute("crossOrigin","anonymous"),r.onload=function(){o?EXIF.getData(r,function(){n()}):n()},r.onerror=function(e){r.style.opacity=1,setTimeout(function(){t(e)},1)},r.src=i})).then(function(e){if(function(t){this.elements.img.parentNode&&(Array.prototype.forEach.call(this.elements.img.classList,function(e){t.classList.add(e)}),this.elements.img.parentNode.replaceChild(t,this.elements.img),this.elements.preview=t),this.elements.img=t}.call(p,e),d.length)p.options.relative&&(d=[d[0]*e.naturalWidth/100,d[1]*e.naturalHeight/100,d[2]*e.naturalWidth/100,d[3]*e.naturalHeight/100]);else{var t,n,i=m(e),o=p.elements.viewport.getBoundingClientRect(),r=o.width/o.height;r