initgui.js 96 KB

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