initgui.js 101 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129
  1. /**
  2. * Copyright (C) 2024 Puter Technologies Inc.
  3. *
  4. * This file is part of Puter.
  5. *
  6. * Puter is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. import UIDesktop from './UI/UIDesktop.js'
  20. import UIWindow from './UI/UIWindow.js'
  21. import UIAlert from './UI/UIAlert.js'
  22. import UIWindowLogin from './UI/UIWindowLogin.js';
  23. import UIWindowSignup from './UI/UIWindowSignup.js';
  24. import path from "./lib/path.js";
  25. import UIWindowSaveAccount from './UI/UIWindowSaveAccount.js';
  26. import UIWindowNewPassword from './UI/UIWindowNewPassword.js';
  27. import UIWindowLoginInProgress from './UI/UIWindowLoginInProgress.js';
  28. import UIWindowEmailConfirmationRequired from './UI/UIWindowEmailConfirmationRequired.js';
  29. import UIWindowSessionList from './UI/UIWindowSessionList.js';
  30. import UIWindowRequestPermission from './UI/UIWindowRequestPermission.js';
  31. import UIWindowChangeUsername from './UI/UIWindowChangeUsername.js';
  32. import update_last_touch_coordinates from './helpers/update_last_touch_coordinates.js';
  33. import update_title_based_on_uploads from './helpers/update_title_based_on_uploads.js';
  34. import PuterDialog from './UI/PuterDialog.js';
  35. import determine_active_container_parent from './helpers/determine_active_container_parent.js';
  36. import { ThemeService } from './services/ThemeService.js';
  37. import { BroadcastService } from './services/BroadcastService.js';
  38. import { ProcessService } from './services/ProcessService.js';
  39. import { PROCESS_RUNNING } from './definitions.js';
  40. import { LocaleService } from './services/LocaleService.js';
  41. import { SettingsService } from './services/SettingsService.js';
  42. import UIComponentWindow from './UI/UIComponentWindow.js';
  43. const launch_services = async function () {
  44. // === Services Data Structures ===
  45. const services_l_ = [];
  46. const services_m_ = {};
  47. globalThis.services = {
  48. get: (name) => services_m_[name],
  49. };
  50. const register = (name, instance) => {
  51. services_l_.push([name, instance]);
  52. services_m_[name] = instance;
  53. }
  54. // === Hooks for Service Scripts from Backend ===
  55. const service_script_deferred = { services: [], on_ready: [] };
  56. const service_script_api = {
  57. register: (...a) => service_script_deferred.services.push(a),
  58. on_ready: fn => service_script_deferred.on_ready.push(fn),
  59. // Some files can't be imported by service scripts,
  60. // so this hack makes that possible.
  61. use: name => ({ UIWindow, UIComponentWindow })[name],
  62. };
  63. globalThis.service_script_api_promise.resolve(service_script_api);
  64. // === Builtin Services ===
  65. register('broadcast', new BroadcastService());
  66. register('theme', new ThemeService());
  67. register('process', new ProcessService());
  68. register('locale', new LocaleService());
  69. register('settings', new SettingsService());
  70. // === Service-Script Services ===
  71. for (const [name, script] of service_script_deferred.services) {
  72. register(name, script);
  73. }
  74. for (const [_, instance] of services_l_) {
  75. await instance.init();
  76. }
  77. // === Service-Script Ready ===
  78. for (const fn of service_script_deferred.on_ready) {
  79. await fn();
  80. }
  81. // Set init process status
  82. {
  83. const svc_process = globalThis.services.get('process');
  84. svc_process.get_init().chstatus(PROCESS_RUNNING);
  85. }
  86. };
  87. // This code snippet addresses the issue flagged by Lighthouse regarding the use of
  88. // passive event listeners to enhance scrolling performance. It provides custom
  89. // implementations for touchstart, touchmove, wheel, and mousewheel events in jQuery.
  90. // By setting the 'passive' option appropriately, it ensures that default browser
  91. // behavior is prevented when necessary, thereby improving page scroll performance.
  92. // More info: https://stackoverflow.com/a/62177358
  93. if(jQuery){
  94. jQuery.event.special.touchstart = {
  95. setup: function( _, ns, handle ) {
  96. this.addEventListener("touchstart", handle, { passive: !ns.includes("noPreventDefault") });
  97. }
  98. };
  99. jQuery.event.special.touchmove = {
  100. setup: function( _, ns, handle ) {
  101. this.addEventListener("touchmove", handle, { passive: !ns.includes("noPreventDefault") });
  102. }
  103. };
  104. jQuery.event.special.wheel = {
  105. setup: function( _, ns, handle ){
  106. this.addEventListener("wheel", handle, { passive: true });
  107. }
  108. };
  109. jQuery.event.special.mousewheel = {
  110. setup: function( _, ns, handle ){
  111. this.addEventListener("mousewheel", handle, { passive: true });
  112. }
  113. };
  114. }
  115. window.initgui = async function(){
  116. let url = new URL(window.location);
  117. url = url.href;
  118. let picked_a_user_for_sdk_login = false;
  119. // update SDK if auth_token is different from the one in the SDK
  120. if(window.auth_token && puter.authToken !== window.auth_token)
  121. puter.setAuthToken(window.auth_token);
  122. // update SDK if api_origin is different from the one in the SDK
  123. if(window.api_origin && puter.APIOrigin !== window.api_origin)
  124. puter.setAPIOrigin(window.api_origin);
  125. // Print the version to the console
  126. puter.os.version()
  127. .then(res => {
  128. const deployed_date = new Date(res.deploy_timestamp);
  129. console.log(`Your Puter information:\n• Version: ${(res.version)}\n• Server: ${(res.location)}\n• Deployed: ${(deployed_date)}`);
  130. })
  131. .catch(error => {
  132. console.error("Failed to fetch server info:", error);
  133. });
  134. // Checks the type of device the user is on (phone, tablet, or desktop).
  135. // Depending on the device type, it sets a class attribute on the body tag
  136. // to style or script the page differently for each device type.
  137. if(isMobile.phone)
  138. $('body').attr('class', 'device-phone');
  139. else if(isMobile.tablet)
  140. $('body').attr('class', 'device-tablet');
  141. else
  142. $('body').attr('class', 'device-desktop');
  143. // Appends a meta tag to the head of the document specifying the character encoding to be UTF-8.
  144. // This ensures that special characters and symbols display correctly across various platforms and browsers.
  145. $('head').append(`<meta charset="utf-8">`);
  146. // Appends a viewport meta tag to the head of the document, ensuring optimal display on mobile devices.
  147. // This tag sets the width of the viewport to the device width, and locks the zoom level to 1 (prevents user scaling).
  148. $('head').append(`<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">`);
  149. // GET query params provided
  150. window.url_query_params = new URLSearchParams(window.location.search);
  151. // will hold the result of the whoami API call
  152. let whoami;
  153. //--------------------------------------------------------------------------------------
  154. // Determine if an app was launched from URL
  155. // i.e. https://puter.com/app/<app_name>
  156. //--------------------------------------------------------------------------------------
  157. const url_paths = window.location.pathname.split('/').filter(element => element);
  158. if(url_paths[0]?.toLocaleLowerCase() === 'app' && url_paths[1]){
  159. window.app_launched_from_url = url_paths[1];
  160. // get query params, any param that doesn't start with 'puter.' will be passed to the app
  161. window.app_query_params = {};
  162. for (let [key, value] of window.url_query_params) {
  163. if(!key.startsWith('puter.'))
  164. window.app_query_params[key] = value;
  165. }
  166. }
  167. //--------------------------------------------------------------------------------------
  168. // Extract 'action' from URL
  169. //--------------------------------------------------------------------------------------
  170. let action;
  171. if(url_paths[0]?.toLocaleLowerCase() === 'action' && url_paths[1]){
  172. action = url_paths[1].toLowerCase();
  173. }
  174. //--------------------------------------------------------------------------------------
  175. // Determine if we are in full-page mode
  176. // i.e. https://puter.com/app/<app_name>/?puter.fullpage=true
  177. //--------------------------------------------------------------------------------------
  178. if(window.url_query_params.has('puter.fullpage') && (window.url_query_params.get('puter.fullpage') === 'false' || window.url_query_params.get('puter.fullpage') === '0')){
  179. window.is_fullpage_mode = false;
  180. }else if(window.url_query_params.has('puter.fullpage') && (window.url_query_params.get('puter.fullpage') === 'true' || window.url_query_params.get('puter.fullpage') === '1')){
  181. // In fullpage mode, we want to hide the taskbar for better UX
  182. window.taskbar_height = 0;
  183. // Puter is in fullpage mode.
  184. window.is_fullpage_mode = true;
  185. }
  186. // Launch services before any UI is rendered
  187. await launch_services();
  188. //--------------------------------------------------------------------------------------
  189. // Is GUI embedded in a popup?
  190. // i.e. https://puter.com/?embedded_in_popup=true
  191. //--------------------------------------------------------------------------------------
  192. if(window.url_query_params.has('embedded_in_popup') && (window.url_query_params.get('embedded_in_popup') === 'true' || window.url_query_params.get('embedded_in_popup') === '1')){
  193. window.embedded_in_popup = true;
  194. $('body').addClass('embedded-in-popup');
  195. // determine the origin of the opener
  196. window.openerOrigin = document.referrer;
  197. // if no referrer, request it from the opener via messaging
  198. if(!document.referrer){
  199. try{
  200. window.openerOrigin = await requestOpenerOrigin();
  201. }catch(e){
  202. throw new Error('No referrer found');
  203. }
  204. }
  205. // this is the referrer in terms of user acquisition
  206. window.referrerStr = window.openerOrigin;
  207. if(action === 'sign-in' && !window.is_auth()){
  208. // show signup window
  209. if(await UIWindowSignup({
  210. reload_on_success: false,
  211. send_confirmation_code: false,
  212. show_close_button: false,
  213. window_options:{
  214. has_head: false,
  215. cover_page: true,
  216. }
  217. }))
  218. await window.getUserAppToken(window.openerOrigin);
  219. }
  220. else if(action === 'sign-in' && window.is_auth()){
  221. picked_a_user_for_sdk_login = await UIWindowSessionList({
  222. reload_on_success: false,
  223. draggable_body: false,
  224. has_head: false,
  225. cover_page: true,
  226. });
  227. if(picked_a_user_for_sdk_login){
  228. await window.getUserAppToken(window.openerOrigin);
  229. }
  230. }
  231. }
  232. //--------------------------------------------------------------------------------------
  233. // Get user referral code from URL query params
  234. // i.e. https://puter.com/?r=123456
  235. //--------------------------------------------------------------------------------------
  236. if(window.url_query_params.has('r')){
  237. window.referral_code = window.url_query_params.get('r');
  238. // remove 'r' from URL
  239. window.history.pushState(null, document.title, '/');
  240. // show referral notice, this will be used later if Desktop is loaded
  241. if(window.first_visit_ever)
  242. window.show_referral_notice = true;
  243. }
  244. //--------------------------------------------------------------------------------------
  245. // Action: Request Permission
  246. //--------------------------------------------------------------------------------------
  247. if(action === 'request-permission'){
  248. let app_uid = window.url_query_params.get('app_uid');
  249. let origin = window.openerOrigin ?? window.url_query_params.get('origin');
  250. let permission = window.url_query_params.get('permission');
  251. let granted = await UIWindowRequestPermission({
  252. app_uid: app_uid,
  253. origin: origin,
  254. permission: permission,
  255. });
  256. let messageTarget = window.embedded_in_popup ? window.opener : window.parent;
  257. messageTarget.postMessage({
  258. msg: "permissionGranted",
  259. granted: granted,
  260. }, origin);
  261. }
  262. //--------------------------------------------------------------------------------------
  263. // Action: Password recovery
  264. //--------------------------------------------------------------------------------------
  265. else if(action === 'set-new-password'){
  266. let user = window.url_query_params.get('user');
  267. let token = window.url_query_params.get('token');
  268. await UIWindowNewPassword({
  269. user: user,
  270. token: token,
  271. });
  272. }
  273. //--------------------------------------------------------------------------------------
  274. // Action: Change Username
  275. //--------------------------------------------------------------------------------------
  276. else if(action === 'change-username'){
  277. await UIWindowChangeUsername();
  278. }
  279. //--------------------------------------------------------------------------------------
  280. // Action: Login
  281. //--------------------------------------------------------------------------------------
  282. else if(action === 'login'){
  283. await UIWindowLogin();
  284. }
  285. //--------------------------------------------------------------------------------------
  286. // Action: Signup
  287. //--------------------------------------------------------------------------------------
  288. else if(action === 'signup'){
  289. await UIWindowSignup();
  290. }
  291. // -------------------------------------------------------------------------------------
  292. // If in embedded in a popup, it is important to check whether the opener app has a relationship with the user
  293. // if yes, we need to get the user app token and send it to the opener
  294. // if not, we need to ask the user for confirmation before proceeding BUT only if the action is a file-picker action
  295. // -------------------------------------------------------------------------------------
  296. if(window.embedded_in_popup && window.openerOrigin){
  297. let response = await window.checkUserSiteRelationship(window.openerOrigin);
  298. window.userAppToken = response.token;
  299. if(!picked_a_user_for_sdk_login && window.logged_in_users.length > 0 && (!window.userAppToken || window.url_query_params.get('request_auth') )){
  300. await UIWindowSessionList({
  301. reload_on_success: false,
  302. draggable_body: false,
  303. has_head: false,
  304. cover_page: true,
  305. });
  306. }
  307. // if not and action is show-open-file-picker, we need confirmation before proceeding
  308. if(action === 'show-open-file-picker' || action === 'show-save-file-picker' || action === 'show-directory-picker'){
  309. if(!window.userAppToken){
  310. let is_confirmed = await PuterDialog();
  311. if(is_confirmed === false){
  312. if(!window.is_auth()){
  313. window.first_visit_ever = false;
  314. localStorage.removeItem("has_visited_before", true);
  315. }
  316. window.close();
  317. window.open('','_self').close();
  318. }
  319. }
  320. }
  321. }
  322. // -------------------------------------------------------------------------------------
  323. // `auth_token` provided in URL, use it to log in
  324. // -------------------------------------------------------------------------------------
  325. else if(window.url_query_params.has('auth_token')){
  326. let query_param_auth_token = window.url_query_params.get('auth_token');
  327. try{
  328. whoami = await puter.os.user();
  329. }catch(e){
  330. if(e.status === 401){
  331. window.logout();
  332. return;
  333. }
  334. }
  335. if(whoami){
  336. if(whoami.requires_email_confirmation){
  337. let is_verified;
  338. do{
  339. is_verified = await UIWindowEmailConfirmationRequired({
  340. stay_on_top: true,
  341. has_head: false
  342. });
  343. }
  344. while(!is_verified)
  345. }
  346. // if user is logging in using an auth token that means it's not their first ever visit to Puter.com
  347. // it might be their first visit to Puter on this specific device but it's not their first time ever visiting Puter.
  348. window.first_visit_ever = false;
  349. // show login progress window
  350. UIWindowLoginInProgress({user_info: whoami});
  351. // update auth data
  352. window.update_auth_data(query_param_auth_token, whoami);
  353. }
  354. // remove auth_token from URL
  355. window.history.pushState(null, document.title, '/');
  356. }
  357. /**
  358. * Logout without showing confirmation or "Save Account" action,
  359. * and without authenticating with the server.
  360. */
  361. const bad_session_logout = async () => {
  362. try {
  363. // TODO: i18n
  364. await UIAlert({
  365. message: 'Your session is invalid. You will be logged out.'
  366. });
  367. // clear local storage
  368. localStorage.clear();
  369. // reload the page
  370. window.location.reload();
  371. }catch(e){
  372. // TODO: i18n
  373. await UIAlert({
  374. message: 'Session is invalid and logout failed; ' +
  375. 'please clear local storage manually.'
  376. });
  377. }
  378. };
  379. // -------------------------------------------------------------------------------------
  380. // Authed
  381. // -------------------------------------------------------------------------------------
  382. if(window.is_auth()){
  383. // try to get user data using /whoami, only if that data is missing
  384. if(!whoami){
  385. try{
  386. whoami = await puter.os.user();
  387. }catch(e){
  388. if(e.status === 401){
  389. bad_session_logout();
  390. return;
  391. }
  392. }
  393. }
  394. // update local user data
  395. if(whoami){
  396. // is email confirmation required?
  397. if(whoami.requires_email_confirmation){
  398. let is_verified;
  399. do{
  400. is_verified = await UIWindowEmailConfirmationRequired({
  401. stay_on_top: true,
  402. has_head: false
  403. });
  404. }
  405. while(!is_verified)
  406. }
  407. window.update_auth_data(whoami.token || window.auth_token, whoami);
  408. // -------------------------------------------------------------------------------------
  409. // Load desktop, only if we're not embedded in a popup
  410. // -------------------------------------------------------------------------------------
  411. if(!window.embedded_in_popup){
  412. await window.get_auto_arrange_data()
  413. puter.fs.stat(window.desktop_path, async function(desktop_fsentry){
  414. UIDesktop({desktop_fsentry: desktop_fsentry});
  415. })
  416. }
  417. // -------------------------------------------------------------------------------------
  418. // If embedded in a popup, send the token to the opener and close the popup
  419. // -------------------------------------------------------------------------------------
  420. else{
  421. let msg_id = window.url_query_params.get('msg_id');
  422. try{
  423. let data = await window.getUserAppToken(new URL(window.openerOrigin).origin);
  424. // This is an implicit app and the app_uid is sent back from the server
  425. // we cache it here so that we can use it later
  426. window.host_app_uid = data.app_uid;
  427. // send token to parent
  428. window.opener.postMessage({
  429. msg: 'puter.token',
  430. success: true,
  431. token: data.token,
  432. app_uid: data.app_uid,
  433. username: window.user.username,
  434. msg_id: msg_id,
  435. }, window.openerOrigin);
  436. // close popup
  437. if(!action || action==='sign-in'){
  438. window.close();
  439. window.open('','_self').close();
  440. }
  441. }catch(err){
  442. // send error to parent
  443. window.opener.postMessage({
  444. msg: 'puter.token',
  445. success: false,
  446. token: null,
  447. msg_id: msg_id,
  448. }, window.openerOrigin);
  449. // close popup
  450. window.close();
  451. window.open('','_self').close();
  452. }
  453. let app_uid;
  454. if(window.openerOrigin){
  455. app_uid = await window.getAppUIDFromOrigin(window.openerOrigin);
  456. window.host_app_uid = app_uid;
  457. }
  458. if(action === 'show-open-file-picker'){
  459. let options = window.url_query_params.get('options');
  460. options = JSON.parse(options ?? '{}');
  461. // Open dialog
  462. UIWindow({
  463. allowed_file_types: options?.accept,
  464. selectable_body: options?.multiple,
  465. path: '/' + window.user.username + '/Desktop',
  466. // this is the uuid of the window to which this dialog will return
  467. return_to_parent_window: true,
  468. show_maximize_button: false,
  469. show_minimize_button: false,
  470. title: 'Open',
  471. is_dir: true,
  472. is_openFileDialog: true,
  473. is_resizable: false,
  474. has_head: false,
  475. cover_page: true,
  476. // selectable_body: is_selectable_body,
  477. iframe_msg_uid: msg_id,
  478. center: true,
  479. initiating_app_uuid: app_uid,
  480. on_close: function(){
  481. window.opener.postMessage({
  482. msg: "fileOpenCanceled",
  483. original_msg_id: msg_id,
  484. }, '*');
  485. }
  486. });
  487. }
  488. //--------------------------------------------------------------------------------------
  489. // Action: Show Directory Picker
  490. //--------------------------------------------------------------------------------------
  491. else if(action === 'show-directory-picker'){
  492. // open directory picker dialog
  493. UIWindow({
  494. path: '/' + window.user.username + '/Desktop',
  495. // this is the uuid of the window to which this dialog will return
  496. // parent_uuid: event.data.appInstanceID,
  497. return_to_parent_window: true,
  498. show_maximize_button: false,
  499. show_minimize_button: false,
  500. title: 'Open',
  501. is_dir: true,
  502. is_directoryPicker: true,
  503. is_resizable: false,
  504. has_head: false,
  505. cover_page: true,
  506. // selectable_body: is_selectable_body,
  507. iframe_msg_uid: msg_id,
  508. center: true,
  509. initiating_app_uuid: app_uid,
  510. on_close: function(){
  511. window.opener.postMessage({
  512. msg: "directoryOpenCanceled",
  513. original_msg_id: msg_id,
  514. }, '*');
  515. }
  516. });
  517. }
  518. //--------------------------------------------------------------------------------------
  519. // Action: Show Save File Dialog
  520. //--------------------------------------------------------------------------------------
  521. else if(action === 'show-save-file-picker'){
  522. let allowed_file_types = window.url_query_params.get('allowed_file_types');
  523. // send 'sendMeFileData' event to parent
  524. window.opener.postMessage({
  525. msg: 'sendMeFileData',
  526. }, '*');
  527. // listen for 'showSaveFilePickerPopup' event from parent
  528. window.addEventListener('message', async (event) => {
  529. if(event.data.msg !== 'showSaveFilePickerPopup')
  530. return;
  531. // Open dialog
  532. UIWindow({
  533. allowed_file_types: allowed_file_types,
  534. path: '/' + window.user.username + '/Desktop',
  535. // this is the uuid of the window to which this dialog will return
  536. return_to_parent_window: true,
  537. show_maximize_button: false,
  538. show_minimize_button: false,
  539. title: 'Save',
  540. is_dir: true,
  541. is_saveFileDialog: true,
  542. is_resizable: false,
  543. has_head: false,
  544. cover_page: true,
  545. // selectable_body: is_selectable_body,
  546. iframe_msg_uid: msg_id,
  547. center: true,
  548. initiating_app_uuid: app_uid,
  549. on_close: function(){
  550. window.opener.postMessage({
  551. msg: "fileSaveCanceled",
  552. original_msg_id: msg_id,
  553. }, '*');
  554. },
  555. onSaveFileDialogSave: async function(target_path, el_filedialog_window){
  556. $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').show();
  557. let busy_init_ts = Date.now();
  558. let overwrite = false;
  559. let file_to_upload = new File([event.data.content], path.basename(target_path));
  560. let item_with_same_name_already_exists = true;
  561. while(item_with_same_name_already_exists){
  562. // overwrite?
  563. if(overwrite)
  564. item_with_same_name_already_exists = false;
  565. // upload
  566. try{
  567. const res = await puter.fs.write(
  568. target_path,
  569. file_to_upload,
  570. {
  571. dedupeName: false,
  572. overwrite: overwrite
  573. }
  574. );
  575. let file_signature = await puter.fs.sign(app_uid, {uid: res.uid, action: 'write'});
  576. file_signature = file_signature.items;
  577. item_with_same_name_already_exists = false;
  578. window.opener.postMessage({
  579. msg: "fileSaved",
  580. original_msg_id: msg_id,
  581. filename: res.name,
  582. saved_file: {
  583. name: file_signature.fsentry_name,
  584. readURL: file_signature.read_url,
  585. writeURL: file_signature.write_url,
  586. metadataURL: file_signature.metadata_url,
  587. type: file_signature.type,
  588. uid: file_signature.uid,
  589. path: `~/` + res.path.split('/').slice(2).join('/'),
  590. },
  591. }, '*');
  592. window.close();
  593. window.open('','_self').close();
  594. }
  595. catch(err){
  596. // item with same name exists
  597. if(err.code === 'item_with_same_name_exists'){
  598. const alert_resp = await UIAlert({
  599. message: `<strong>${html_encode(err.entry_name)}</strong> already exists.`,
  600. buttons:[
  601. {
  602. label: i18n('replace'),
  603. value: 'replace',
  604. type: 'primary',
  605. },
  606. {
  607. label: i18n('cancel'),
  608. value: 'cancel',
  609. },
  610. ],
  611. parent_uuid: $(el_filedialog_window).attr('data-element_uuid'),
  612. })
  613. if(alert_resp === 'replace'){
  614. overwrite = true;
  615. }else if(alert_resp === 'cancel'){
  616. // enable parent window
  617. $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide();
  618. return;
  619. }
  620. }
  621. else{
  622. console.log(err);
  623. // show error
  624. await UIAlert({
  625. message: err.message ?? "Upload failed.",
  626. parent_uuid: $(el_filedialog_window).attr('data-element_uuid'),
  627. });
  628. // enable parent window
  629. $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide();
  630. return;
  631. }
  632. }
  633. }
  634. // done
  635. let busy_duration = (Date.now() - busy_init_ts);
  636. if( busy_duration >= window.busy_indicator_hide_delay){
  637. $(el_filedialog_window).close();
  638. }else{
  639. setTimeout(() => {
  640. // close this dialog
  641. $(el_filedialog_window).close();
  642. }, Math.abs(window.busy_indicator_hide_delay - busy_duration));
  643. }
  644. }
  645. });
  646. });
  647. }
  648. }
  649. // ----------------------------------------------------------
  650. // Get user's sites
  651. // ----------------------------------------------------------
  652. window.update_sites_cache();
  653. }
  654. }
  655. // -------------------------------------------------------------------------------------
  656. // Desktop Background
  657. // If we're in fullpage/emebedded/Auth Popup mode, we don't want to load the custom background
  658. // because it's not visible anyway and it's a waste of bandwidth
  659. // -------------------------------------------------------------------------------------
  660. if(!window.is_fullpage_mode && !window.embedded_in_popup){
  661. window.refresh_desktop_background();
  662. }
  663. // -------------------------------------------------------------------------------------
  664. // Un-authed but not first visit -> try to log in/sign up
  665. // -------------------------------------------------------------------------------------
  666. if(!window.is_auth() && !window.first_visit_ever){
  667. if(window.logged_in_users.length > 0){
  668. UIWindowSessionList();
  669. }
  670. else{
  671. await UIWindowLogin({
  672. reload_on_success: true,
  673. send_confirmation_code: false,
  674. window_options:{
  675. has_head: false
  676. }
  677. });
  678. }
  679. }
  680. // -------------------------------------------------------------------------------------
  681. // Un-authed and first visit ever -> create temp user
  682. // -------------------------------------------------------------------------------------
  683. else if(!window.is_auth() && window.first_visit_ever){
  684. let referrer;
  685. try{
  686. referrer = new URL(window.location.href).pathname;
  687. }catch(e){
  688. console.log(e)
  689. }
  690. referrer = window.openerOrigin ?? referrer;
  691. // a global object that will be used to store the user's referrer
  692. window.referrerStr = referrer;
  693. // in case there is also a referrer query param, add it to the referrer URL
  694. if(window.url_query_params.has('ref')){
  695. if(!referrer)
  696. referrer = '/';
  697. referrer += '?ref=' + html_encode(window.url_query_params.get('ref'));
  698. }
  699. let headers = {};
  700. if(window.custom_headers)
  701. headers = window.custom_headers;
  702. $.ajax({
  703. url: window.gui_origin + "/signup",
  704. type: 'POST',
  705. async: true,
  706. headers: headers,
  707. contentType: "application/json",
  708. data: JSON.stringify({
  709. referrer: referrer,
  710. referral_code: window.referral_code,
  711. is_temp: true,
  712. }),
  713. success: async function (data){
  714. window.update_auth_data(data.token, data.user);
  715. document.dispatchEvent(new Event("login", { bubbles: true}));
  716. },
  717. error: function (err){
  718. $('#signup-error-msg').html(html_encode(err.responseText));
  719. $('#signup-error-msg').fadeIn();
  720. // re-enable 'Create Account' button
  721. $('.signup-btn').prop('disabled', false);
  722. }
  723. });
  724. }
  725. // if there is at least one window open (only non-Explorer windows), ask user for confirmation when navigating away
  726. if(window.feature_flags.prompt_user_when_navigation_away_from_puter){
  727. window.onbeforeunload = function(){
  728. if($(`.window:not(.window[data-app="explorer"])`).length > 0)
  729. return true;
  730. };
  731. }
  732. // -------------------------------------------------------------------------------------
  733. // `login` event handler
  734. // --------------------------------------------------------------------------------------
  735. $(document).on("login", async (e) => {
  736. // close all windows
  737. $('.window').close();
  738. // -------------------------------------------------------------------------------------
  739. // Load desktop, if not embedded in a popup
  740. // -------------------------------------------------------------------------------------
  741. if(!window.embedded_in_popup){
  742. await window.get_auto_arrange_data();
  743. puter.fs.stat(window.desktop_path, function (desktop_fsentry) {
  744. UIDesktop({ desktop_fsentry: desktop_fsentry });
  745. })
  746. }
  747. // -------------------------------------------------------------------------------------
  748. // If embedded in a popup, send the 'ready' event to referrer and close the popup
  749. // -------------------------------------------------------------------------------------
  750. else{
  751. let msg_id = window.url_query_params.get('msg_id');
  752. try{
  753. let data = await window.getUserAppToken(new URL(window.openerOrigin).origin);
  754. // This is an implicit app and the app_uid is sent back from the server
  755. // we cache it here so that we can use it later
  756. window.host_app_uid = data.app_uid;
  757. // send token to parent
  758. window.opener.postMessage({
  759. msg: 'puter.token',
  760. success: true,
  761. msg_id: msg_id,
  762. token: data.token,
  763. username: window.user.username,
  764. app_uid: data.app_uid,
  765. }, window.openerOrigin);
  766. // close popup
  767. if(!action || action==='sign-in'){
  768. window.close();
  769. window.open('','_self').close();
  770. }
  771. }catch(err){
  772. // send error to parent
  773. window.opener.postMessage({
  774. msg: 'puter.token',
  775. msg_id: msg_id,
  776. success: false,
  777. token: null,
  778. }, window.openerOrigin);
  779. // close popup
  780. window.close();
  781. window.open('','_self').close();
  782. }
  783. let app_uid;
  784. if(window.openerOrigin){
  785. app_uid = await window.getAppUIDFromOrigin(window.openerOrigin);
  786. window.host_app_uid = app_uid;
  787. }
  788. //--------------------------------------------------------------------------------------
  789. // Action: Show Open File Picker
  790. //--------------------------------------------------------------------------------------
  791. if(action === 'show-open-file-picker'){
  792. let options = window.url_query_params.get('options');
  793. options = JSON.parse(options ?? '{}');
  794. // Open dialog
  795. UIWindow({
  796. allowed_file_types: options?.accept,
  797. selectable_body: options?.multiple,
  798. path: '/' + window.user.username + '/Desktop',
  799. return_to_parent_window: true,
  800. show_maximize_button: false,
  801. show_minimize_button: false,
  802. title: 'Open',
  803. is_dir: true,
  804. is_openFileDialog: true,
  805. is_resizable: false,
  806. has_head: false,
  807. cover_page: true,
  808. iframe_msg_uid: msg_id,
  809. center: true,
  810. initiating_app_uuid: app_uid,
  811. on_close: function(){
  812. window.opener.postMessage({
  813. msg: "fileOpenCanceled",
  814. original_msg_id: msg_id,
  815. }, '*');
  816. }
  817. });
  818. }
  819. //--------------------------------------------------------------------------------------
  820. // Action: Show Directory Picker
  821. //--------------------------------------------------------------------------------------
  822. else if(action === 'show-directory-picker'){
  823. // open directory picker dialog
  824. UIWindow({
  825. path: '/' + window.user.username + '/Desktop',
  826. // this is the uuid of the window to which this dialog will return
  827. // parent_uuid: event.data.appInstanceID,
  828. return_to_parent_window: true,
  829. show_maximize_button: false,
  830. show_minimize_button: false,
  831. title: 'Open',
  832. is_dir: true,
  833. is_directoryPicker: true,
  834. is_resizable: false,
  835. has_head: false,
  836. cover_page: true,
  837. // selectable_body: is_selectable_body,
  838. iframe_msg_uid: msg_id,
  839. center: true,
  840. initiating_app_uuid: app_uid,
  841. on_close: function(){
  842. window.opener.postMessage({
  843. msg: "directoryOpenCanceled",
  844. original_msg_id: msg_id,
  845. }, '*');
  846. }
  847. });
  848. }
  849. //--------------------------------------------------------------------------------------
  850. // Action: Show Save File Dialog
  851. //--------------------------------------------------------------------------------------
  852. else if(action === 'show-save-file-picker'){
  853. let allowed_file_types = window.url_query_params.get('allowed_file_types');
  854. // send 'sendMeFileData' event to parent
  855. window.opener.postMessage({
  856. msg: 'sendMeFileData',
  857. }, '*');
  858. // listen for 'showSaveFilePickerPopup' event from parent
  859. window.addEventListener('message', async (event) => {
  860. if(event.data.msg !== 'showSaveFilePickerPopup')
  861. return;
  862. // Open dialog
  863. UIWindow({
  864. allowed_file_types: allowed_file_types,
  865. path: '/' + window.user.username + '/Desktop',
  866. // this is the uuid of the window to which this dialog will return
  867. return_to_parent_window: true,
  868. show_maximize_button: false,
  869. show_minimize_button: false,
  870. title: 'Save',
  871. is_dir: true,
  872. is_saveFileDialog: true,
  873. is_resizable: false,
  874. has_head: false,
  875. cover_page: true,
  876. // selectable_body: is_selectable_body,
  877. iframe_msg_uid: msg_id,
  878. center: true,
  879. initiating_app_uuid: app_uid,
  880. on_close: function(){
  881. window.opener.postMessage({
  882. msg: "fileSaveCanceled",
  883. original_msg_id: msg_id,
  884. }, '*');
  885. },
  886. onSaveFileDialogSave: async function(target_path, el_filedialog_window){
  887. $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').show();
  888. let busy_init_ts = Date.now();
  889. let overwrite = false;
  890. let file_to_upload = new File([event.data.content], path.basename(target_path));
  891. let item_with_same_name_already_exists = true;
  892. while(item_with_same_name_already_exists){
  893. // overwrite?
  894. if(overwrite)
  895. item_with_same_name_already_exists = false;
  896. // upload
  897. try{
  898. const res = await puter.fs.write(
  899. target_path,
  900. file_to_upload,
  901. {
  902. dedupeName: false,
  903. overwrite: overwrite
  904. }
  905. );
  906. let file_signature = await puter.fs.sign(app_uid, {uid: res.uid, action: 'write'});
  907. file_signature = file_signature.items;
  908. item_with_same_name_already_exists = false;
  909. window.opener.postMessage({
  910. msg: "fileSaved",
  911. original_msg_id: msg_id,
  912. filename: res.name,
  913. saved_file: {
  914. name: file_signature.fsentry_name,
  915. readURL: file_signature.read_url,
  916. writeURL: file_signature.write_url,
  917. metadataURL: file_signature.metadata_url,
  918. type: file_signature.type,
  919. uid: file_signature.uid,
  920. path: `~/` + res.path.split('/').slice(2).join('/'),
  921. },
  922. }, '*');
  923. window.close();
  924. window.open('','_self').close();
  925. // show_save_account_notice_if_needed();
  926. }
  927. catch(err){
  928. // item with same name exists
  929. if(err.code === 'item_with_same_name_exists'){
  930. const alert_resp = await UIAlert({
  931. message: `<strong>${html_encode(err.entry_name)}</strong> already exists.`,
  932. buttons:[
  933. {
  934. label: i18n('replace'),
  935. value: 'replace',
  936. type: 'primary',
  937. },
  938. {
  939. label: i18n('cancel'),
  940. value: 'cancel',
  941. },
  942. ],
  943. parent_uuid: $(el_filedialog_window).attr('data-element_uuid'),
  944. })
  945. if(alert_resp === 'replace'){
  946. overwrite = true;
  947. }else if(alert_resp === 'cancel'){
  948. // enable parent window
  949. $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide();
  950. return;
  951. }
  952. }
  953. else{
  954. console.log(err);
  955. // show error
  956. await UIAlert({
  957. message: err.message ?? "Upload failed.",
  958. parent_uuid: $(el_filedialog_window).attr('data-element_uuid'),
  959. });
  960. // enable parent window
  961. $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide();
  962. return;
  963. }
  964. }
  965. }
  966. // done
  967. let busy_duration = (Date.now() - busy_init_ts);
  968. if( busy_duration >= window.busy_indicator_hide_delay){
  969. $(el_filedialog_window).close();
  970. }else{
  971. setTimeout(() => {
  972. // close this dialog
  973. $(el_filedialog_window).close();
  974. }, Math.abs(window.busy_indicator_hide_delay - busy_duration));
  975. }
  976. }
  977. });
  978. });
  979. }
  980. }
  981. })
  982. $(".popover, .context-menu").on("remove", function () {
  983. $('.window-active .window-app-iframe').css('pointer-events', 'all');
  984. })
  985. // If the document is clicked/tapped somewhere
  986. $(document).bind("mousedown touchstart", function (e) {
  987. // update last touch coordinates
  988. update_last_touch_coordinates(e);
  989. // dismiss touchstart on regular devices
  990. if(e.type === 'touchstart' && !isMobile.phone && !isMobile.tablet)
  991. return;
  992. // If .item-container clicked, unselect all its item children
  993. if($(e.target).hasClass('item-container') && !e.ctrlKey && !e.metaKey){
  994. $(e.target).children('.item-selected').removeClass('item-selected');
  995. window.update_explorer_footer_selected_items_count(e.target);
  996. }
  997. // If the clicked element is not a context menu, remove all context menus
  998. if ($(e.target).parents(".context-menu").length === 0) {
  999. const $ctxmenus = $(".context-menu");
  1000. $ctxmenus.fadeOut(200, function(){
  1001. $ctxmenus.remove();
  1002. });
  1003. }
  1004. // click on anything will close all popovers, but there are some exceptions
  1005. if(!$(e.target).hasClass('start-app')
  1006. && !$(e.target).hasClass('launch-search')
  1007. && !$(e.target).hasClass('launch-search-clear')
  1008. && $(e.target).closest('.start-app').length === 0
  1009. && !isMobile.phone && !isMobile.tablet
  1010. && !$(e.target).hasClass('popover')
  1011. && $(e.target).parents('.popover').length === 0){
  1012. $(".popover").fadeOut(200, function(){
  1013. $(".popover").remove();
  1014. });
  1015. }
  1016. // Close all tooltips
  1017. $('.ui-tooltip').remove();
  1018. // rename items whose names were being edited
  1019. if(!$(e.target).hasClass('item-name-editor')){
  1020. // blurring an Item Name Editor will automatically trigger renaming the item
  1021. $(".item-name-editor-active").blur();
  1022. }
  1023. // update active_item_container
  1024. if($(e.target).hasClass('item-container')){
  1025. window.active_item_container = e.target;
  1026. }else{
  1027. let ic = $(e.target).closest('.item-container')
  1028. if(ic.length > 0){
  1029. window.active_item_container = ic.get(0);
  1030. }else{
  1031. let pp = $(e.target).find('.item-container')
  1032. if(pp.length > 0){
  1033. window.active_item_container = pp.get(0);
  1034. }
  1035. }
  1036. }
  1037. //active element
  1038. window.active_element = e.target;
  1039. });
  1040. $(document).bind('keydown', async function(e){
  1041. const focused_el = document.activeElement;
  1042. //-----------------------------------------------------------------------
  1043. // ← ↑ → ↓: an arrow key is pressed
  1044. //-----------------------------------------------------------------------
  1045. if((e.which === 37 || e.which === 38 || e.which === 39 || e.which === 40)){
  1046. // ----------------------------------------------
  1047. // Launch menu is open
  1048. // ----------------------------------------------
  1049. if($('.launch-popover').length > 0){
  1050. // If no item is selected and down arrow is pressed, select the first item
  1051. if($('.launch-popover .start-app-card.launch-app-selected').length === 0 && (e.which === 40)){
  1052. $('.launch-popover .start-app-card:visible').first().addClass('launch-app-selected');
  1053. // blur search input
  1054. $('.launch-popover .launch-search').blur();
  1055. return false;
  1056. }
  1057. // if search input is focused and left or right arrow is pressed, return false
  1058. else if($('.launch-popover .launch-search').is(':focus') && (e.which === 37 || e.which === 39)){
  1059. return false;
  1060. }
  1061. else{
  1062. // If an item is already selected, move the selection up, down, left or right
  1063. let selected_item = $('.launch-popover .start-app-card.launch-app-selected').get(0);
  1064. let selected_item_index = $('.launch-popover .start-app-card:visible').index(selected_item);
  1065. let selected_item_row = Math.floor(selected_item_index / 5);
  1066. let selected_item_col = selected_item_index % 5;
  1067. let selected_item_row_count = Math.ceil($('.launch-popover .start-app-card:visible').length / 5);
  1068. let selected_item_col_count = 5;
  1069. let new_selected_item_index = selected_item_index;
  1070. let new_selected_item_row = selected_item_row;
  1071. let new_selected_item_col = selected_item_col;
  1072. let new_selected_item;
  1073. // if up arrow is pressed
  1074. if(e.which === 38){
  1075. // if this item is in the first row, up arrow should bring the focus back to the search input
  1076. if(selected_item_row === 0){
  1077. $('.launch-popover .launch-search').focus();
  1078. // unselect all items
  1079. $('.launch-popover .start-app-card.launch-app-selected').removeClass('launch-app-selected');
  1080. // bring cursor to the end of the search input
  1081. $('.launch-popover .launch-search').val($('.launch-popover .launch-search').val());
  1082. return false;
  1083. }
  1084. // if this item is not in the first row, move the selection up
  1085. else{
  1086. new_selected_item_row = selected_item_row - 1;
  1087. if(new_selected_item_row < 0)
  1088. new_selected_item_row = selected_item_row_count - 1;
  1089. }
  1090. }
  1091. // if down arrow is pressed
  1092. else if(e.which === 40){
  1093. new_selected_item_row = selected_item_row + 1;
  1094. if(new_selected_item_row >= selected_item_row_count)
  1095. new_selected_item_row = 0;
  1096. }
  1097. // if left arrow is pressed
  1098. else if(e.which === 37){
  1099. new_selected_item_col = selected_item_col - 1;
  1100. if(new_selected_item_col < 0)
  1101. new_selected_item_col = selected_item_col_count - 1;
  1102. }
  1103. // if right arrow is pressed
  1104. else if(e.which === 39){
  1105. new_selected_item_col = selected_item_col + 1;
  1106. if(new_selected_item_col >= selected_item_col_count)
  1107. new_selected_item_col = 0;
  1108. }
  1109. new_selected_item_index = (new_selected_item_row * selected_item_col_count) + new_selected_item_col;
  1110. new_selected_item = $('.launch-popover .start-app-card:visible').get(new_selected_item_index);
  1111. $(selected_item).removeClass('launch-app-selected');
  1112. $(new_selected_item).addClass('launch-app-selected');
  1113. // make sure the selected item is visible in the popover by scrolling the popover
  1114. let popover = $('.launch-popover').get(0);
  1115. let popover_height = $('.launch-popover').height();
  1116. let popover_scroll_top = popover.getBoundingClientRect().top;
  1117. let popover_scroll_bottom = popover_scroll_top + popover_height;
  1118. let selected_item_top = new_selected_item.getBoundingClientRect().top;
  1119. let selected_item_bottom = new_selected_item.getBoundingClientRect().bottom;
  1120. let isVisible = (selected_item_top >= popover_scroll_top) && (selected_item_bottom <= popover_scroll_top + popover_height);
  1121. if ( ! isVisible ) {
  1122. const scrollTop = selected_item_top - popover_scroll_top;
  1123. const scrollBot = selected_item_bottom - popover_scroll_bottom;
  1124. if (Math.abs(scrollTop) < Math.abs(scrollBot)) {
  1125. popover.scrollTop += scrollTop;
  1126. } else {
  1127. popover.scrollTop += scrollBot;
  1128. }
  1129. }
  1130. return false;
  1131. }
  1132. }
  1133. // ----------------------------------------------
  1134. // A context menu is open
  1135. // ----------------------------------------------
  1136. else if($('.context-menu').length > 0){
  1137. // if no item is selected and down arrow is pressed, select the first item
  1138. if($('.context-menu-active .context-menu-item-active').length === 0 && (e.which === 40)){
  1139. let selected_item = $('.context-menu-active .context-menu-item').get(0);
  1140. window.select_ctxmenu_item(selected_item);
  1141. return false;
  1142. }
  1143. // if no item is selected and up arrow is pressed, select the last item
  1144. else if($('.context-menu-active .context-menu-item-active').length === 0 && (e.which === 38)){
  1145. let selected_item = $('.context-menu .context-menu-item').get($('.context-menu .context-menu-item').length - 1);
  1146. window.select_ctxmenu_item(selected_item);
  1147. return false;
  1148. }
  1149. // if an item is selected and down arrow is pressed, select the next enabled item
  1150. else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 40)){
  1151. let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
  1152. let selected_item_index = $('.context-menu-active .context-menu-item').index(selected_item);
  1153. let new_selected_item_index = selected_item_index + 1;
  1154. let new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
  1155. while($(new_selected_item).hasClass('context-menu-item-disabled')){
  1156. new_selected_item_index = new_selected_item_index + 1;
  1157. new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
  1158. }
  1159. window.select_ctxmenu_item(new_selected_item);
  1160. return false;
  1161. }
  1162. // if an item is selected and up arrow is pressed, select the previous enabled item
  1163. else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 38)){
  1164. let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
  1165. let selected_item_index = $('.context-menu-active .context-menu-item').index(selected_item);
  1166. let new_selected_item_index = selected_item_index - 1;
  1167. let new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
  1168. while($(new_selected_item).hasClass('context-menu-item-disabled')){
  1169. new_selected_item_index = new_selected_item_index - 1;
  1170. new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
  1171. }
  1172. window.select_ctxmenu_item(new_selected_item);
  1173. return false;
  1174. }
  1175. // if right arrow is pressed, open the submenu by triggering a mouseover event
  1176. else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 39)){
  1177. const selected_item = $('.context-menu-active .context-menu-item-active').get(0);
  1178. $(selected_item).trigger('mouseover');
  1179. // if the submenu is open, select the first item in the submenu
  1180. if($(selected_item).hasClass('context-menu-item-submenu') === true){
  1181. $(selected_item).removeClass('context-menu-item-active');
  1182. $(selected_item).addClass('context-menu-item-active-blurred');
  1183. window.select_ctxmenu_item($('.context-menu[data-is-submenu="true"] .context-menu-item').get(0));
  1184. }
  1185. return false;
  1186. }
  1187. // if left arrow is pressed on a submenu, close the submenu
  1188. else if($('.context-menu-active[data-is-submenu="true"]').length > 0 && (e.which === 37)){
  1189. // get parent menu
  1190. let parent_menu_id = $('.context-menu-active[data-is-submenu="true"]').data('parent-id');
  1191. let parent_menu = $('.context-menu[data-element-id="' + parent_menu_id + '"]');
  1192. // remove the submenu
  1193. $('.context-menu-active[data-is-submenu="true"]').remove();
  1194. // activate the parent menu
  1195. $(parent_menu).addClass('context-menu-active');
  1196. // select the item that opened the submenu
  1197. let selected_item = $('.context-menu-active .context-menu-item-active-blurred').get(0);
  1198. $(selected_item).removeClass('context-menu-item-active-blurred');
  1199. $(selected_item).addClass('context-menu-item-active');
  1200. return false;
  1201. }
  1202. // if enter is pressed, trigger a click event on the selected item
  1203. else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 13)){
  1204. let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
  1205. $(selected_item).trigger('click');
  1206. return false;
  1207. }
  1208. }
  1209. // ----------------------------------------------
  1210. // Navigate items in the active item container
  1211. // ----------------------------------------------
  1212. else if(!$(focused_el).is('input') && !$(focused_el).is('textarea') && (e.which === 37 || e.which === 38 || e.which === 39 || e.which === 40)){
  1213. let item_width = 110, item_height = 110, selected_item;
  1214. // select first item in container if none is selected
  1215. if($(window.active_item_container).find('.item-selected').length === 0){
  1216. selected_item = $(window.active_item_container).find('.item').get(0);
  1217. window.active_element = selected_item;
  1218. $(window.active_item_container).find('.item-selected').removeClass('item-selected');
  1219. $(selected_item).addClass('item-selected');
  1220. return false;
  1221. }
  1222. // if Shift key is pressed and ONE item is already selected, pick that item
  1223. else if($(window.active_item_container).find('.item-selected').length === 1 && e.shiftKey){
  1224. selected_item = $(window.active_item_container).find('.item-selected').get(0);
  1225. }
  1226. // if Shift key is pressed and MORE THAN ONE item is selected, pick the latest active item
  1227. else if($(window.active_item_container).find('.item-selected').length > 1 && e.shiftKey){
  1228. selected_item = $(window.active_element).hasClass('item') ? window.active_element : $(window.active_element).closest('.item').get(0);
  1229. }
  1230. // otherwise if an item is selected, pick that item
  1231. else if($(window.active_item_container).find('.item-selected').length === 1){
  1232. selected_item = $(window.active_item_container).find('.item-selected').get(0);
  1233. }
  1234. else{
  1235. selected_item = $(window.active_element).hasClass('item') ? window.active_element : $(window.active_element).closest('.item').get(0);
  1236. }
  1237. // override the default behavior of ctrl/meta key
  1238. // in some browsers ctrl/meta key + arrow keys will scroll the page or go back/forward in history
  1239. if(e.ctrlKey || e.metaKey){
  1240. e.preventDefault();
  1241. e.stopPropagation();
  1242. }
  1243. // get the position of the selected item
  1244. let active_el_pos = $(selected_item).hasClass('item') ? selected_item.getBoundingClientRect() : $(selected_item).closest('.item').get(0).getBoundingClientRect();
  1245. let xpos = active_el_pos.left + item_width/2;
  1246. let ypos = active_el_pos.top + item_height/2;
  1247. // these hold next item's position on the grid
  1248. let x_nxtpos, y_nxtpos;
  1249. // these hold the amount of pixels to scroll the container
  1250. let x_scroll = 0, y_scroll = 0;
  1251. // determine next item's position on the grid
  1252. // left
  1253. if(e.which === 37){
  1254. x_nxtpos = (xpos - item_width) > 0 ? (xpos - item_width) : 0;
  1255. y_nxtpos = (ypos);
  1256. x_scroll = (item_width / 2);
  1257. }
  1258. // up
  1259. else if(e.which === 38){
  1260. x_nxtpos = (xpos);
  1261. y_nxtpos = (ypos - item_height) > 0 ? (ypos - item_height) : 0;
  1262. y_scroll = -1 * (item_height / 2);
  1263. }
  1264. // right
  1265. else if(e.which === 39){
  1266. x_nxtpos = (xpos + item_width);
  1267. y_nxtpos = (ypos);
  1268. x_scroll = -1 * (item_width / 2);
  1269. }
  1270. // down
  1271. else if(e.which === 40){
  1272. x_nxtpos = (xpos);
  1273. y_nxtpos = (ypos + item_height);
  1274. y_scroll = (item_height / 2);
  1275. }
  1276. let elements_at_next_pos = document.elementsFromPoint(x_nxtpos, y_nxtpos);
  1277. let next_item;
  1278. for (let index = 0; index < elements_at_next_pos.length; index++) {
  1279. const elem_at_next_pos = elements_at_next_pos[index];
  1280. if($(elem_at_next_pos).hasClass('item') && $(elem_at_next_pos).closest('.item-container').is(window.active_item_container)){
  1281. next_item = elem_at_next_pos;
  1282. break;
  1283. }
  1284. }
  1285. if(next_item){
  1286. selected_item = next_item;
  1287. window.active_element = next_item;
  1288. // if ctrl or meta key is not pressed, unselect all items
  1289. if(!e.shiftKey){
  1290. $(window.active_item_container).find('.item').removeClass('item-selected');
  1291. }
  1292. $(next_item).addClass('item-selected');
  1293. window.latest_selected_item = next_item;
  1294. // scroll to the selected item only if this was a down or up move
  1295. if(e.which === 38 || e.which === 40)
  1296. next_item.scrollIntoView(false);
  1297. }
  1298. }
  1299. }
  1300. //-----------------------------------------------------------------------
  1301. // if the Esc key is pressed on a FileDialog/Alert, close that FileDialog/Alert
  1302. //-----------------------------------------------------------------------
  1303. else if(
  1304. // escape key code
  1305. e.which === 27 &&
  1306. // active window must be a FileDialog or Alert
  1307. ($('.window-active').hasClass('window-filedialog') || $('.window-active').hasClass('window-alert')) &&
  1308. // either don't close if an input is focused or if the input is the filename input
  1309. ((!$(focused_el).is('input') && !$(focused_el).is('textarea')) || $(focused_el).hasClass('savefiledialog-filename'))
  1310. ){
  1311. // close the FileDialog
  1312. $('.window-active').close();
  1313. }
  1314. //-----------------------------------------------------------------------
  1315. // if the Esc key is pressed on a Window Navbar Editor, deactivate the editor
  1316. //-----------------------------------------------------------------------
  1317. else if( e.which === 27 && $(focused_el).hasClass('window-navbar-path-input')){
  1318. $(focused_el).blur();
  1319. $(focused_el).val($(focused_el).closest('.window').attr('data-path'));
  1320. $(focused_el).attr('data-path', $(focused_el).closest('.window').attr('data-path'));
  1321. }
  1322. //-----------------------------------------------------------------------
  1323. // Esc key should:
  1324. // - always close open context menus
  1325. // - close the Launch Popover if it's open
  1326. //-----------------------------------------------------------------------
  1327. if( e.which === 27){
  1328. // close open context menus
  1329. $('.context-menu').remove();
  1330. // close the Launch Popover if it's open
  1331. $(".launch-popover").closest('.popover').fadeOut(200, function(){
  1332. $(".launch-popover").closest('.popover').remove();
  1333. });
  1334. }
  1335. })
  1336. $(document).bind('keydown', async function(e){
  1337. const focused_el = document.activeElement;
  1338. //-----------------------------------------------------------------------
  1339. // Shift+Delete (win)/ option+command+delete (Mac) key pressed
  1340. // Permanent delete bypassing trash after alert
  1341. //-----------------------------------------------------------------------
  1342. if((e.keyCode === 46 && e.shiftKey) || (e.altKey && e.metaKey && e.keyCode === 8)) {
  1343. let $selected_items = $(window.active_element).closest(`.item-container`).find(`.item-selected`);
  1344. if($selected_items.length > 0){
  1345. const alert_resp = await UIAlert({
  1346. message: i18n('confirm_delete_multiple_items'),
  1347. buttons:[
  1348. {
  1349. label: i18n('delete'),
  1350. type: 'primary',
  1351. },
  1352. {
  1353. label: i18n('cancel')
  1354. },
  1355. ]
  1356. })
  1357. if((alert_resp) === 'Delete'){
  1358. for (let index = 0; index < $selected_items.length; index++) {
  1359. const element = $selected_items[index];
  1360. await window.delete_item(element);
  1361. }
  1362. }
  1363. }
  1364. return false;
  1365. }
  1366. //-----------------------------------------------------------------------
  1367. // Delete (win)/ ctrl+delete (Mac) / cmd+delete (Mac) key pressed
  1368. // Permanent delete from trash after alert or move to trash
  1369. //-----------------------------------------------------------------------
  1370. if(e.keyCode === 46 || (e.keyCode === 8 && (e.ctrlKey || e.metaKey))) {
  1371. // permanent delete?
  1372. let $selected_items = $(window.active_element).closest(`.item-container`).find(`.item-selected[data-path^="${window.trash_path + '/'}"]`);
  1373. if($selected_items.length > 0){
  1374. const alert_resp = await UIAlert({
  1375. message: i18n('confirm_delete_multiple_items'),
  1376. buttons:[
  1377. {
  1378. label: i18n('delete'),
  1379. type: 'primary',
  1380. },
  1381. {
  1382. label: i18n('cancel')
  1383. },
  1384. ]
  1385. })
  1386. if((alert_resp) === 'Delete'){
  1387. for (let index = 0; index < $selected_items.length; index++) {
  1388. const element = $selected_items[index];
  1389. await window.delete_item(element);
  1390. }
  1391. const trash = await puter.fs.stat(window.trash_path);
  1392. if(window.socket){
  1393. window.socket.emit('trash.is_empty', {is_empty: trash.is_empty});
  1394. }
  1395. if(trash.is_empty){
  1396. $(`[data-app="trash"]`).find('.taskbar-icon > img').attr('src', window.icons['trash.svg']);
  1397. $(`.item[data-path="${html_encode(window.trash_path)}" i]`).find('.item-icon > img').attr('src', window.icons['trash.svg']);
  1398. $(`.window[data-path="${html_encode(window.trash_path)}"]`).find('.window-head-icon').attr('src', window.icons['trash.svg']);
  1399. }
  1400. }
  1401. }
  1402. // regular delete?
  1403. else{
  1404. $selected_items = $(window.active_element).closest('.item-container').find('.item-selected');
  1405. if($selected_items.length > 0){
  1406. // Only delete the items if we're not renaming one.
  1407. if ($selected_items.children('.item-name-editor-active').length === 0) {
  1408. window.move_items($selected_items, window.trash_path);
  1409. }
  1410. }
  1411. }
  1412. return false;
  1413. }
  1414. //-----------------------------------------------------------------------
  1415. // A letter or number is pressed and there is no context menu open: search items by name
  1416. //-----------------------------------------------------------------------
  1417. if(!e.ctrlKey && !e.metaKey && !$(focused_el).is('input') && !$(focused_el).is('textarea') && $('.context-menu').length === 0){
  1418. if(window.keypress_item_seach_term !== '')
  1419. clearTimeout(window.keypress_item_seach_buffer_timeout);
  1420. window.keypress_item_seach_buffer_timeout = setTimeout(()=>{
  1421. window.keypress_item_seach_term = '';
  1422. }, 700);
  1423. window.keypress_item_seach_term += e.key.toLocaleLowerCase();
  1424. let matches= [];
  1425. const selected_items = $(window.active_item_container).find(`.item-selected`).not('.item-disabled').first();
  1426. // if one item is selected and the selected item matches the search term, don't continue search and select this item again
  1427. if(selected_items.length === 1 && $(selected_items).attr('data-name').toLowerCase().startsWith(window.keypress_item_seach_term)){
  1428. return false;
  1429. }
  1430. // search for matches
  1431. let haystack = $(window.active_item_container).find(`.item`).not('.item-disabled');
  1432. for(let j=0; j < haystack.length; j++){
  1433. if($(haystack[j]).attr('data-name').toLowerCase().startsWith(window.keypress_item_seach_term)){
  1434. matches.push(haystack[j])
  1435. }
  1436. }
  1437. if(matches.length > 0){
  1438. // if there are multiple matches and an item is already selected, remove all matches before the selected item
  1439. if(selected_items.length > 0 && matches.length > 1){
  1440. let match_index;
  1441. for(let i=0; i < matches.length - 1; i++){
  1442. if($(matches[i]).is(selected_items)){
  1443. match_index = i;
  1444. break;
  1445. }
  1446. }
  1447. matches.splice(0, match_index+1);
  1448. }
  1449. // deselect all selected sibling items
  1450. $(window.active_item_container).find(`.item-selected`).removeClass('item-selected');
  1451. // select matching item
  1452. $(matches[0]).not('.item-disabled').addClass('item-selected');
  1453. matches[0].scrollIntoView(false);
  1454. window.update_explorer_footer_selected_items_count($(window.active_element).closest('.window'));
  1455. }
  1456. return false;
  1457. }
  1458. //-----------------------------------------------------------------------
  1459. // A letter or number is pressed and there is a context menu open: search items by name
  1460. //-----------------------------------------------------------------------
  1461. else if(!e.ctrlKey && !e.metaKey && !$(focused_el).is('input') && !$(focused_el).is('textarea') && $('.context-menu').length > 0){
  1462. if(window.keypress_item_seach_term !== '')
  1463. clearTimeout(window.keypress_item_seach_buffer_timeout);
  1464. window.keypress_item_seach_buffer_timeout = setTimeout(()=>{
  1465. window.keypress_item_seach_term = '';
  1466. }, 700);
  1467. window.keypress_item_seach_term += e.key.toLocaleLowerCase();
  1468. let matches= [];
  1469. const selected_items = $('.context-menu').find(`.context-menu-item-active`).first();
  1470. // if one item is selected and the selected item matches the search term, don't continue search and select this item again
  1471. if(selected_items.length === 1 && $(selected_items).text().toLowerCase().startsWith(window.keypress_item_seach_term)){
  1472. return false;
  1473. }
  1474. // search for matches
  1475. let haystack = $('.context-menu-active').find(`.context-menu-item`);
  1476. for(let j=0; j < haystack.length; j++){
  1477. if($(haystack[j]).text().toLowerCase().startsWith(window.keypress_item_seach_term)){
  1478. matches.push(haystack[j])
  1479. }
  1480. }
  1481. if(matches.length > 0){
  1482. // if there are multiple matches and an item is already selected, remove all matches before the selected item
  1483. if(selected_items.length > 0 && matches.length > 1){
  1484. let match_index;
  1485. for(let i=0; i < matches.length - 1; i++){
  1486. if($(matches[i]).is(selected_items)){
  1487. match_index = i;
  1488. break;
  1489. }
  1490. }
  1491. matches.splice(0, match_index+1);
  1492. }
  1493. // deselect all selected sibling items
  1494. $('.context-menu').find(`.context-menu-item-active`).removeClass('context-menu-item-active');
  1495. // select matching item
  1496. $(matches[0]).addClass('context-menu-item-active');
  1497. // matches[0].scrollIntoView(false);
  1498. // update_explorer_footer_selected_items_count($(window.active_element).closest('.window'));
  1499. }
  1500. return false;
  1501. }
  1502. })
  1503. $(document).bind("keyup keydown", async function(e){
  1504. const focused_el = document.activeElement;
  1505. //-----------------------------------------------------------------------------
  1506. // Override ctrl/cmd + s/o
  1507. //-----------------------------------------------------------------------------
  1508. if((e.ctrlKey || e.metaKey) && (e.which === 83 || e.which === 79)){
  1509. e.preventDefault()
  1510. return false;
  1511. }
  1512. //-----------------------------------------------------------------------------
  1513. // Select All
  1514. // ctrl/command + a, will select all items on desktop and windows
  1515. //-----------------------------------------------------------------------------
  1516. if((e.ctrlKey || e.metaKey) && e.which === 65 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
  1517. let $parent_container = $(window.active_element).closest('.item-container');
  1518. if($parent_container.length === 0)
  1519. $parent_container = $(window.active_element).find('.item-container');
  1520. if($parent_container.attr('data-multiselectable') === 'false')
  1521. return false;
  1522. if($parent_container){
  1523. $($parent_container).find('.item').not('.item-disabled').addClass('item-selected');
  1524. window.update_explorer_footer_selected_items_count($parent_container.closest('.window'));
  1525. }
  1526. return false;
  1527. }
  1528. //-----------------------------------------------------------------------------
  1529. // Close Window
  1530. // ctrl + w, will close the active window
  1531. //-----------------------------------------------------------------------------
  1532. if(e.ctrlKey && e.which === 87){
  1533. let $parent_window = $(window.active_element).closest('.window');
  1534. if($parent_window.length === 0)
  1535. $parent_window = $(window.active_element).find('.window');
  1536. if($parent_window !== null){
  1537. $($parent_window).close();
  1538. }
  1539. }
  1540. //-----------------------------------------------------------------------------
  1541. // Copy
  1542. // ctrl/command + c, will copy selected items on the active element to the clipboard
  1543. //-----------------------------------------------------------------------------
  1544. if((e.ctrlKey || e.metaKey) && e.which === 67 &&
  1545. $(window.mouseover_window).attr('data-is_dir') !== 'false' &&
  1546. $(window.mouseover_window).attr('data-path') !== window.trash_path &&
  1547. !$(focused_el).is('input') &&
  1548. !$(focused_el).is('textarea')){
  1549. let $selected_items;
  1550. let parent_container = $(window.active_element).closest('.item-container');
  1551. if(parent_container.length === 0)
  1552. parent_container = $(window.active_element).find('.item-container');
  1553. if(parent_container !== null){
  1554. $selected_items = $(parent_container).find('.item-selected');
  1555. if($selected_items.length > 0){
  1556. window.clipboard = [];
  1557. window.clipboard_op = 'copy';
  1558. $selected_items.each(function() {
  1559. // error if trash is being copied
  1560. if($(this).attr('data-path') === window.trash_path){
  1561. return;
  1562. }
  1563. // add to clipboard
  1564. window.clipboard.push({path: $(this).attr('data-path'), uid: $(this).attr('data-uid'), metadata: $(this).attr('data-metadata')});
  1565. })
  1566. }
  1567. }
  1568. return false;
  1569. }
  1570. //-----------------------------------------------------------------------------
  1571. // Cut
  1572. // ctrl/command + x, will copy selected items on the active element to the clipboard
  1573. //-----------------------------------------------------------------------------
  1574. if((e.ctrlKey || e.metaKey) && e.which === 88 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
  1575. let $selected_items;
  1576. let parent_container = $(window.active_element).closest('.item-container');
  1577. if(parent_container.length === 0)
  1578. parent_container = $(window.active_element).find('.item-container');
  1579. if(parent_container !== null){
  1580. $selected_items = $(parent_container).find('.item-selected');
  1581. if($selected_items.length > 0){
  1582. window.clipboard = [];
  1583. window.clipboard_op = 'move';
  1584. $selected_items.each(function() {
  1585. window.clipboard.push($(this).attr('data-path'));
  1586. })
  1587. }
  1588. }
  1589. return false;
  1590. }
  1591. //-----------------------------------------------------------------------
  1592. // Open
  1593. // Enter key on a selected item will open it
  1594. //-----------------------------------------------------------------------
  1595. if(e.which === 13 && !$(focused_el).is('input') && !$(focused_el).is('textarea') && (Date.now() - window.last_enter_pressed_to_rename_ts) >200
  1596. // prevent firing twice, because this will be fired on both keyup and keydown
  1597. && e.type === 'keydown'){
  1598. let $selected_items;
  1599. e.preventDefault();
  1600. e.stopPropagation();
  1601. // ---------------------------------------------
  1602. // if this is a selected Launch menu item, open it
  1603. // ---------------------------------------------
  1604. if($('.launch-app-selected').length > 0){
  1605. // close launch menu
  1606. $(".launch-popover").fadeOut(200, function(){
  1607. window.launch_app({
  1608. name: $('.launch-app-selected').attr('data-name'),
  1609. })
  1610. $(".launch-popover").remove();
  1611. });
  1612. return false;
  1613. }
  1614. // ---------------------------------------------
  1615. // if this is a selected context menu item, open it
  1616. // ---------------------------------------------
  1617. else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 13)){
  1618. // let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
  1619. // $(selected_item).trigger('mouseover');
  1620. // $(selected_item).trigger('click');
  1621. let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
  1622. $(selected_item).removeClass('context-menu-item-active');
  1623. $(selected_item).addClass('context-menu-item-active-blurred');
  1624. $(selected_item).trigger('mouseover');
  1625. $(selected_item).trigger('click');
  1626. if($('.context-menu[data-is-submenu="true"]').length > 0){
  1627. let selected_item = $('.context-menu[data-is-submenu="true"] .context-menu-item').get(0);
  1628. window.select_ctxmenu_item(selected_item);
  1629. }
  1630. return false;
  1631. }
  1632. // ---------------------------------------------
  1633. // if this is a selected item, open it
  1634. // ---------------------------------------------
  1635. else if(window.active_item_container){
  1636. $selected_items = $(window.active_item_container).find('.item-selected');
  1637. if($selected_items.length > 0){
  1638. $selected_items.each(function() {
  1639. window.open_item({
  1640. item: this,
  1641. new_window: e.metaKey || e.ctrlKey,
  1642. });
  1643. })
  1644. }
  1645. return false;
  1646. }
  1647. return false;
  1648. }
  1649. //----------------------------------------------
  1650. // Paste
  1651. // ctrl/command + v, will paste items from the clipboard to the active element
  1652. //----------------------------------------------
  1653. if((e.ctrlKey || e.metaKey) && e.which === 86 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
  1654. let target_path, target_el;
  1655. // continue only if there is something in the clipboard
  1656. if(window.clipboard.length === 0)
  1657. return;
  1658. let parent_container = determine_active_container_parent();
  1659. if(parent_container){
  1660. target_el = parent_container;
  1661. target_path = $(parent_container).attr('data-path');
  1662. // don't allow pasting in Trash
  1663. if((target_path === window.trash_path || target_path.startsWith(window.trash_path + '/')) && window.clipboard_op !== 'move')
  1664. return;
  1665. // execute clipboard operation
  1666. if(window.clipboard_op === 'copy')
  1667. window.copy_clipboard_items(target_path);
  1668. else if(window.clipboard_op === 'move')
  1669. window.move_clipboard_items(target_el, target_path);
  1670. }
  1671. return false;
  1672. }
  1673. //-----------------------------------------------------------------------------
  1674. // Undo
  1675. // ctrl/command + z, will undo last action
  1676. //-----------------------------------------------------------------------------
  1677. if((e.ctrlKey || e.metaKey) && e.which === 90){
  1678. window.undo_last_action();
  1679. return false;
  1680. }
  1681. });
  1682. // update mouse position coordinates
  1683. $(document).mousemove(function(event){
  1684. window.mouseX = event.clientX;
  1685. window.mouseY = event.clientY;
  1686. // mouse in top-left corner of screen
  1687. if((window.mouseX < 150 && window.mouseY < window.toolbar_height + 20) || (window.mouseX < 20 && window.mouseY < 150))
  1688. window.current_active_snap_zone = 'nw';
  1689. // mouse in left edge of screen
  1690. else if(window.mouseX < 20 && window.mouseY >= 150 && window.mouseY < window.desktop_height - 150)
  1691. window.current_active_snap_zone = 'w';
  1692. // mouse in bottom-left corner of screen
  1693. else if(window.mouseX < 20 && window.mouseY > window.desktop_height - 150)
  1694. window.current_active_snap_zone = 'sw';
  1695. // mouse in right edge of screen
  1696. else if(window.mouseX > window.desktop_width - 20 && window.mouseY >= 150 && window.mouseY < window.desktop_height - 150)
  1697. window.current_active_snap_zone = 'e';
  1698. // mouse in top-right corner of screen
  1699. else if((window.mouseX > window.desktop_width - 150 && window.mouseY < window.toolbar_height + 20) || (window.mouseX > window.desktop_width - 20 && window.mouseY < 150))
  1700. window.current_active_snap_zone = 'ne';
  1701. // mouse in bottom-right corner of screen
  1702. else if(window.mouseX > window.desktop_width - 20 && window.mouseY >= window.desktop_height - 150)
  1703. window.current_active_snap_zone = 'se';
  1704. // mouse in top edge of screen
  1705. else if(window.mouseY < window.toolbar_height + 20 && window.mouseX >= 150 && window.mouseX < window.desktop_width - 150)
  1706. window.current_active_snap_zone = 'n';
  1707. // not in any snap zone
  1708. else
  1709. window.current_active_snap_zone = undefined;
  1710. // mouseover_window
  1711. var windows = document.getElementsByClassName("window");
  1712. let active_win;
  1713. if(windows.length > 0){
  1714. let highest_window_zindex = 0;
  1715. for(let i=0; i<windows.length; i++){
  1716. const rect = windows[i].getBoundingClientRect();
  1717. if( window.mouseX > rect.x && window.mouseX < (rect.x + rect.width) && window.mouseY > rect.y && window.mouseY < (rect.y + rect.height)){
  1718. if(parseInt($(windows[i]).css('z-index')) >= highest_window_zindex){
  1719. active_win = windows[i];
  1720. highest_window_zindex = parseInt($(windows[i]).css('z-index'));
  1721. }
  1722. }
  1723. }
  1724. }
  1725. window.mouseover_window = active_win;
  1726. // mouseover_item_container
  1727. var item_containers = document.getElementsByClassName("item-container");
  1728. let active_ic;
  1729. if(item_containers.length > 0){
  1730. let highest_window_zindex = 0;
  1731. for(let i=0; i<item_containers.length; i++){
  1732. const rect = item_containers[i].getBoundingClientRect();
  1733. if( window.mouseX > rect.x && window.mouseX < (rect.x + rect.width) && window.mouseY > rect.y && window.mouseY < (rect.y + rect.height)){
  1734. let active_container_zindex = parseInt($(item_containers[i]).closest('.window').css('z-index'));
  1735. if( !isNaN(active_container_zindex) && active_container_zindex >= highest_window_zindex){
  1736. active_ic = item_containers[i];
  1737. highest_window_zindex = active_container_zindex;
  1738. }
  1739. }
  1740. }
  1741. }
  1742. window.mouseover_item_container = active_ic;
  1743. });
  1744. //--------------------------------------------------------
  1745. // Window Activation
  1746. //--------------------------------------------------------
  1747. $(document).on('mousedown', function(e){
  1748. // if taskbar or any parts of it is clicked, drop the event
  1749. if($(e.target).hasClass('taskbar') || $(e.target).closest('.taskbar').length > 0)
  1750. return;
  1751. // if mouse is clicked on a window, activate it
  1752. if(window.mouseover_window !== undefined){
  1753. $(window.mouseover_window).focusWindow(e);
  1754. }
  1755. })
  1756. // if an element has the .long-hover class, fire a long-hover event after 600ms
  1757. $(document).on('mouseenter', '.long-hover', function(){
  1758. let el = this;
  1759. el.long_hover_timeout = setTimeout(() => {
  1760. $(el).trigger('long-hover');
  1761. }, 600);
  1762. })
  1763. // if an element has the .long-hover class, cancel the long-hover event if the mouse leaves
  1764. $(document).on('mouseleave', '.long-hover', function(){
  1765. clearTimeout(this.long_hover_timeout);
  1766. })
  1767. // if an element has the .long-hover class, cancel the long-hover event if the mouse leaves
  1768. $(document).on('paste', function(event){
  1769. event = event.originalEvent ?? event;
  1770. let clipboardData = event.clipboardData || window.clipboardData;
  1771. let items = clipboardData.items || clipboardData.files;
  1772. // return if paste is on input or textarea
  1773. if($(event.target).is('input') || $(event.target).is('textarea'))
  1774. return;
  1775. if(!(items instanceof DataTransferItemList))
  1776. return;
  1777. // upload files
  1778. if(items?.length>0){
  1779. let parent_container = determine_active_container_parent();
  1780. if(parent_container){
  1781. window.upload_items(items, $(parent_container).attr('data-path'));
  1782. }
  1783. }
  1784. event.stopPropagation();
  1785. event.preventDefault();
  1786. return false;
  1787. })
  1788. document.addEventListener("visibilitychange", (event) => {
  1789. if (document.visibilityState !== "visible") {
  1790. window.doc_title_before_blur = document.title;
  1791. if(!_.isEmpty(window.active_uploads)){
  1792. update_title_based_on_uploads();
  1793. }
  1794. }else if(window.active_uploads){
  1795. document.title = window.doc_title_before_blur ?? 'Puter';
  1796. }
  1797. });
  1798. /**
  1799. * Event handler for a custom 'logout' event attached to the document.
  1800. * This function handles the process of logging out, including user confirmation,
  1801. * communication with the backend, and subsequent UI updates. It takes special
  1802. * precautions if the user is identified as using a temporary account.
  1803. *
  1804. * @listens Document#event:logout
  1805. * @async
  1806. * @param {Event} event - The JQuery event object associated with the logout event.
  1807. * @returns {Promise<void>} - This function does not return anything meaningful, but it performs an asynchronous operation.
  1808. */
  1809. $(document).on("logout", async function(event) {
  1810. // is temp user?
  1811. if(window.user && window.user.is_temp && !window.user.deleted){
  1812. const alert_resp = await UIAlert({
  1813. message: `<strong>Save account before logging out!</strong><p>You are using a temporary account and logging out will erase all your data.</p>`,
  1814. buttons:[
  1815. {
  1816. label: i18n('save_account'),
  1817. value: 'save_account',
  1818. type: 'primary',
  1819. },
  1820. {
  1821. label: i18n('log_out'),
  1822. value: 'log_out',
  1823. type: 'danger',
  1824. },
  1825. {
  1826. label: i18n('cancel'),
  1827. },
  1828. ]
  1829. })
  1830. if(alert_resp === 'save_account'){
  1831. let saved = await UIWindowSaveAccount({
  1832. send_confirmation_code: false,
  1833. default_username: window.user.username
  1834. });
  1835. if(saved)
  1836. window.logout();
  1837. }else if (alert_resp === 'log_out'){
  1838. window.logout();
  1839. }
  1840. else{
  1841. return;
  1842. }
  1843. }
  1844. // logout
  1845. try{
  1846. const resp = await fetch(`${window.gui_origin}/get-anticsrf-token`);
  1847. const { token } = await resp.json();
  1848. await $.ajax({
  1849. url: window.gui_origin + "/logout",
  1850. type: 'POST',
  1851. async: true,
  1852. contentType: "application/json",
  1853. headers: {
  1854. "Authorization": "Bearer " + window.auth_token
  1855. },
  1856. data: JSON.stringify({ anti_csrf: token }),
  1857. statusCode: {
  1858. 401: function () {
  1859. },
  1860. },
  1861. })
  1862. }catch(e){
  1863. // Ignored
  1864. }
  1865. // remove this user from the array of logged_in_users
  1866. for (let i = 0; i < window.logged_in_users.length; i++) {
  1867. if(window.logged_in_users[i].uuid === window.user.uuid){
  1868. window.logged_in_users.splice(i, 1);
  1869. break;
  1870. }
  1871. }
  1872. // update logged_in_users in local storage
  1873. localStorage.setItem('logged_in_users', JSON.stringify(window.logged_in_users));
  1874. // delete this user from local storage
  1875. window.user = null;
  1876. localStorage.removeItem('user');
  1877. window.auth_token = null;
  1878. localStorage.removeItem('auth_token');
  1879. // close all windows
  1880. $('.window').close();
  1881. // close all ctxmenus
  1882. $('.context-menu').remove();
  1883. // remove desktop
  1884. $('.desktop').remove();
  1885. // remove taskbar
  1886. $('.taskbar').remove();
  1887. // disable native browser exit confirmation
  1888. window.onbeforeunload = null;
  1889. // go to home page
  1890. window.location.replace("/");
  1891. });
  1892. }
  1893. function requestOpenerOrigin() {
  1894. return new Promise((resolve, reject) => {
  1895. if (!window.opener) {
  1896. reject(new Error("No window.opener available"));
  1897. return;
  1898. }
  1899. // Function to handle the message event
  1900. const handleMessage = (event) => {
  1901. // Check if the message is the expected response
  1902. if (event.data.msg === 'originResponse') {
  1903. // Clean up by removing the event listener
  1904. window.removeEventListener('message', handleMessage);
  1905. resolve(event.origin);
  1906. }
  1907. };
  1908. // Set up the listener for the response
  1909. window.addEventListener('message', handleMessage, false);
  1910. // Send the request to the opener
  1911. window.opener.postMessage({ msg: 'requestOrigin' }, '*');
  1912. // Optional: Reject the promise if no response is received within a timeout
  1913. setTimeout(() => {
  1914. window.removeEventListener('message', handleMessage);
  1915. reject(new Error("Response timed out"));
  1916. }, 5000); // Timeout after 5 seconds
  1917. });
  1918. }
  1919. $(document).on('click', '.generic-close-window-button', function(e){
  1920. $(this).closest('.window').close();
  1921. });
  1922. // Re-calculate desktop height and width on window resize and re-position the login and signup windows
  1923. $(window).on("resize", function () {
  1924. // If host env is popup, don't continue because the popup window has its own resize requirements.
  1925. if (window.embedded_in_popup)
  1926. return;
  1927. const ratio = window.desktop_width / window.innerWidth;
  1928. window.desktop_height = window.innerHeight - window.toolbar_height - window.taskbar_height;
  1929. window.desktop_width = window.innerWidth;
  1930. // Re-center the login window
  1931. const top = $(".window-login").position()?.top;
  1932. const width = $(".window-login").width();
  1933. $(".window-login").css({
  1934. left: (window.desktop_width - width) / 2,
  1935. top: top / ratio,
  1936. });
  1937. // Re-center the create account window
  1938. const top2 = $(".window-signup").position()?.top;
  1939. const width2 = $(".window-signup").width();
  1940. $(".window-signup").css({
  1941. left: (window.desktop_width - width2) / 2,
  1942. top: top2 / ratio,
  1943. });
  1944. });
  1945. $(document).on('contextmenu', '.disable-context-menu', function(e){
  1946. if($(e.target).hasClass('disable-context-menu') ){
  1947. e.preventDefault();
  1948. return false;
  1949. }
  1950. })