initgui.js 95 KB

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