initgui.js 65 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500
  1. /**
  2. * Copyright (C) 2024 Puter Technologies Inc.
  3. *
  4. * This file is part of Puter.
  5. *
  6. * Puter is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. import UIDesktop from './UI/UIDesktop.js'
  20. import UIWindow from './UI/UIWindow.js'
  21. import UIAlert from './UI/UIAlert.js'
  22. import UIWindowLogin from './UI/UIWindowLogin.js';
  23. import UIWindowSignup from './UI/UIWindowSignup.js';
  24. import path from "./lib/path.js";
  25. import UIWindowSaveAccount from './UI/UIWindowSaveAccount.js';
  26. import UIWindowNewPassword from './UI/UIWindowNewPassword.js';
  27. import UIWindowLoginInProgress from './UI/UIWindowLoginInProgress.js';
  28. import UIWindowEmailConfirmationRequired from './UI/UIWindowEmailConfirmationRequired.js';
  29. import UIWindowSessionList from './UI/UIWindowSessionList.js';
  30. import UIWindowRequestPermission from './UI/UIWindowRequestPermission.js';
  31. import UIWindowChangeUsername from './UI/UIWindowChangeUsername.js';
  32. import update_last_touch_coordinates from './helpers/update_last_touch_coordinates.js';
  33. import update_title_based_on_uploads from './helpers/update_title_based_on_uploads.js';
  34. import PuterDialog from './UI/PuterDialog.js';
  35. import determine_active_container_parent from './helpers/determine_active_container_parent.js';
  36. import { ThemeService } from './services/ThemeService.js';
  37. import { BroadcastService } from './services/BroadcastService.js';
  38. import { ProcessService } from './services/ProcessService.js';
  39. import { PROCESS_RUNNING } from './definitions.js';
  40. import { LocaleService } from './services/LocaleService.js';
  41. import { SettingsService } from './services/SettingsService.js';
  42. import UIComponentWindow from './UI/UIComponentWindow.js';
  43. import update_mouse_position from './helpers/update_mouse_position.js';
  44. import { LaunchOnInitService } from './services/LaunchOnInitService.js';
  45. import item_icon from './helpers/item_icon.js';
  46. const launch_services = async function (options) {
  47. // === Services Data Structures ===
  48. const services_l_ = [];
  49. const services_m_ = {};
  50. globalThis.services = {
  51. get: (name) => services_m_[name],
  52. };
  53. const register = (name, instance) => {
  54. services_l_.push([name, instance]);
  55. services_m_[name] = instance;
  56. }
  57. globalThis.def(UIComponentWindow, 'ui.UIComponentWindow');
  58. // === Hooks for Service Scripts from Backend ===
  59. const service_script_deferred = { services: [], on_ready: [] };
  60. const service_script_api = {
  61. register: (...a) => service_script_deferred.services.push(a),
  62. on_ready: fn => service_script_deferred.on_ready.push(fn),
  63. // Some files can't be imported by service scripts,
  64. // so this hack makes that possible.
  65. def: globalThis.def,
  66. use: globalThis.use,
  67. // use: name => ({ UIWindow, UIComponentWindow })[name],
  68. };
  69. globalThis.service_script_api_promise.resolve(service_script_api);
  70. // === Builtin Services ===
  71. register('broadcast', new BroadcastService());
  72. register('theme', new ThemeService());
  73. register('process', new ProcessService());
  74. register('locale', new LocaleService());
  75. register('settings', new SettingsService());
  76. register('__launch-on-init', new LaunchOnInitService());
  77. // === Service-Script Services ===
  78. for (const [name, script] of service_script_deferred.services) {
  79. register(name, script);
  80. }
  81. for (const [_, instance] of services_l_) {
  82. await instance.construct({
  83. gui_params: options,
  84. });
  85. }
  86. for (const [_, instance] of services_l_) {
  87. await instance.init({
  88. services: globalThis.services,
  89. });
  90. }
  91. // === Service-Script Ready ===
  92. for (const fn of service_script_deferred.on_ready) {
  93. await fn();
  94. }
  95. // Set init process status
  96. {
  97. const svc_process = globalThis.services.get('process');
  98. svc_process.get_init().chstatus(PROCESS_RUNNING);
  99. }
  100. };
  101. // This code snippet addresses the issue flagged by Lighthouse regarding the use of
  102. // passive event listeners to enhance scrolling performance. It provides custom
  103. // implementations for touchstart, touchmove, wheel, and mousewheel events in jQuery.
  104. // By setting the 'passive' option appropriately, it ensures that default browser
  105. // behavior is prevented when necessary, thereby improving page scroll performance.
  106. // More info: https://stackoverflow.com/a/62177358
  107. if(jQuery){
  108. jQuery.event.special.touchstart = {
  109. setup: function( _, ns, handle ) {
  110. this.addEventListener("touchstart", handle, { passive: !ns.includes("noPreventDefault") });
  111. }
  112. };
  113. jQuery.event.special.touchmove = {
  114. setup: function( _, ns, handle ) {
  115. this.addEventListener("touchmove", handle, { passive: !ns.includes("noPreventDefault") });
  116. }
  117. };
  118. jQuery.event.special.wheel = {
  119. setup: function( _, ns, handle ){
  120. this.addEventListener("wheel", handle, { passive: true });
  121. }
  122. };
  123. jQuery.event.special.mousewheel = {
  124. setup: function( _, ns, handle ){
  125. this.addEventListener("mousewheel", handle, { passive: true });
  126. }
  127. };
  128. }
  129. window.initgui = async function(options){
  130. let url = new URL(window.location);
  131. url = url.href;
  132. let picked_a_user_for_sdk_login = false;
  133. // update SDK if auth_token is different from the one in the SDK
  134. if(window.auth_token && puter.authToken !== window.auth_token)
  135. puter.setAuthToken(window.auth_token);
  136. // update SDK if api_origin is different from the one in the SDK
  137. if(window.api_origin && puter.APIOrigin !== window.api_origin)
  138. puter.setAPIOrigin(window.api_origin);
  139. // Print the version to the console
  140. puter.os.version()
  141. .then(res => {
  142. const deployed_date = new Date(res.deploy_timestamp);
  143. console.log(`Your Puter information:\n• Version: ${(res.version)}\n• Server: ${(res.location)}\n• Deployed: ${(deployed_date)}`);
  144. })
  145. .catch(error => {
  146. console.error("Failed to fetch server info:", error);
  147. });
  148. // Checks the type of device the user is on (phone, tablet, or desktop).
  149. // Depending on the device type, it sets a class attribute on the body tag
  150. // to style or script the page differently for each device type.
  151. if(isMobile.phone)
  152. $('body').attr('class', 'device-phone');
  153. else if(isMobile.tablet)
  154. $('body').attr('class', 'device-tablet');
  155. else
  156. $('body').attr('class', 'device-desktop');
  157. // Appends a meta tag to the head of the document specifying the character encoding to be UTF-8.
  158. // This ensures that special characters and symbols display correctly across various platforms and browsers.
  159. $('head').append(`<meta charset="utf-8">`);
  160. // Appends a viewport meta tag to the head of the document, ensuring optimal display on mobile devices.
  161. // This tag sets the width of the viewport to the device width, and locks the zoom level to 1 (prevents user scaling).
  162. $('head').append(`<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">`);
  163. // GET query params provided
  164. window.url_query_params = new URLSearchParams(window.location.search);
  165. // will hold the result of the whoami API call
  166. let whoami;
  167. const url_paths = window.location.pathname.split('/').filter(element => element);
  168. //--------------------------------------------------------------------------------------
  169. // Trying to view a user's public folder?
  170. // i.e. https://puter.com/@<username>
  171. //--------------------------------------------------------------------------------------
  172. if(url_paths[0]?.startsWith('@')){
  173. let username = url_paths[0].substring(1);
  174. let item_path = '/' + username + '/Public';
  175. // check if username has valid characters
  176. if(!username.match(/^[a-z0-9_]+$/i)){
  177. UIAlert({
  178. message: 'Invalid username.'
  179. });
  180. }else{
  181. UIWindow({
  182. path: item_path,
  183. title: path.basename(item_path),
  184. icon: await item_icon({is_dir: true, path: item_path}),
  185. is_dir: true,
  186. app: 'explorer',
  187. });
  188. }
  189. }
  190. //--------------------------------------------------------------------------------------
  191. // `share_token` provided
  192. // i.e. https://puter.com/?share_token=<share_token>
  193. //--------------------------------------------------------------------------------------
  194. if(window.url_query_params.has('share_token')){
  195. let share_token = window.url_query_params.get('share_token');
  196. fetch(`${puter.APIOrigin}/sharelink/check`, {
  197. "headers": {
  198. "Content-Type": "application/json",
  199. "Authorization": `Bearer ${puter.authToken}`,
  200. },
  201. "body": JSON.stringify({
  202. token: share_token,
  203. }),
  204. "method": "POST",
  205. }).then(response => response.json())
  206. .then(async data => {
  207. if(data.email && data.email !== window.user.email){
  208. await UIWindowLogin({
  209. reload_on_success: true,
  210. send_confirmation_code: false,
  211. window_options:{
  212. has_head: false
  213. }
  214. });
  215. }else{
  216. UIAlert({
  217. type: 'success',
  218. message: 'You are authorized to view this link.'
  219. });
  220. }
  221. }).catch(error => {
  222. console.error('Error:', error);
  223. })
  224. }
  225. //--------------------------------------------------------------------------------------
  226. // Determine if an app was launched from URL
  227. // i.e. https://puter.com/app/<app_name>
  228. //--------------------------------------------------------------------------------------
  229. if(url_paths[0]?.toLocaleLowerCase() === 'app' && url_paths[1]){
  230. window.app_launched_from_url = url_paths[1];
  231. // get app metadata
  232. try{
  233. window.app_launched_from_url = await puter.apps.get(window.app_launched_from_url)
  234. window.is_fullpage_mode = window.app_launched_from_url.metadata?.fullpage_on_landing ?? false;
  235. }catch(e){
  236. console.error(e);
  237. }
  238. // get query params, any param that doesn't start with 'puter.' will be passed to the app
  239. window.app_query_params = {};
  240. for (let [key, value] of window.url_query_params) {
  241. if(!key.startsWith('puter.'))
  242. window.app_query_params[key] = value;
  243. }
  244. }
  245. //--------------------------------------------------------------------------------------
  246. // Extract 'action' from URL
  247. //--------------------------------------------------------------------------------------
  248. let action;
  249. if(url_paths[0]?.toLocaleLowerCase() === 'action' && url_paths[1]){
  250. action = url_paths[1].toLowerCase();
  251. }
  252. //--------------------------------------------------------------------------------------
  253. // Determine if we are in full-page mode
  254. // i.e. https://puter.com/app/<app_name>/?puter.fullpage=true
  255. //--------------------------------------------------------------------------------------
  256. if(window.url_query_params.has('puter.fullpage') && (window.url_query_params.get('puter.fullpage') === 'false' || window.url_query_params.get('puter.fullpage') === '0')){
  257. window.is_fullpage_mode = false;
  258. }else if(window.url_query_params.has('puter.fullpage') && (window.url_query_params.get('puter.fullpage') === 'true' || window.url_query_params.get('puter.fullpage') === '1')){
  259. // In fullpage mode, we want to hide the taskbar for better UX
  260. window.taskbar_height = 0;
  261. // Puter is in fullpage mode.
  262. window.is_fullpage_mode = true;
  263. }
  264. // Launch services before any UI is rendered
  265. await launch_services(options);
  266. //--------------------------------------------------------------------------------------
  267. // Is GUI embedded in a popup?
  268. // i.e. https://puter.com/?embedded_in_popup=true
  269. //--------------------------------------------------------------------------------------
  270. if(window.url_query_params.has('embedded_in_popup') && (window.url_query_params.get('embedded_in_popup') === 'true' || window.url_query_params.get('embedded_in_popup') === '1')){
  271. window.embedded_in_popup = true;
  272. $('body').addClass('embedded-in-popup');
  273. // determine the origin of the opener
  274. window.openerOrigin = document.referrer;
  275. // if no referrer, request it from the opener via messaging
  276. if(!document.referrer){
  277. try{
  278. window.openerOrigin = await requestOpenerOrigin();
  279. }catch(e){
  280. throw new Error('No referrer found');
  281. }
  282. }
  283. // this is the referrer in terms of user acquisition
  284. window.referrerStr = window.openerOrigin;
  285. if(action === 'sign-in' && !window.is_auth()){
  286. // show signup window
  287. if(await UIWindowSignup({
  288. reload_on_success: false,
  289. send_confirmation_code: false,
  290. show_close_button: false,
  291. window_options:{
  292. has_head: false,
  293. cover_page: true,
  294. }
  295. }))
  296. await window.getUserAppToken(window.openerOrigin);
  297. }
  298. else if(action === 'sign-in' && window.is_auth()){
  299. picked_a_user_for_sdk_login = await UIWindowSessionList({
  300. reload_on_success: false,
  301. draggable_body: false,
  302. has_head: false,
  303. cover_page: true,
  304. });
  305. if(picked_a_user_for_sdk_login){
  306. await window.getUserAppToken(window.openerOrigin);
  307. }
  308. }
  309. }
  310. //--------------------------------------------------------------------------------------
  311. // Display an error if the query parameters have an error
  312. //--------------------------------------------------------------------------------------
  313. if ( window.url_query_params.has('error') ) {
  314. // TODO: i18n
  315. await UIAlert({
  316. message: window.url_query_params.get('message')
  317. });
  318. }
  319. //--------------------------------------------------------------------------------------
  320. // Get user referral code from URL query params
  321. // i.e. https://puter.com/?r=123456
  322. //--------------------------------------------------------------------------------------
  323. if(window.url_query_params.has('r')){
  324. window.referral_code = window.url_query_params.get('r');
  325. // remove 'r' from URL
  326. window.history.pushState(null, document.title, '/');
  327. // show referral notice, this will be used later if Desktop is loaded
  328. if(window.first_visit_ever)
  329. window.show_referral_notice = true;
  330. }
  331. //--------------------------------------------------------------------------------------
  332. // Action: Request Permission
  333. //--------------------------------------------------------------------------------------
  334. if(action === 'request-permission'){
  335. let app_uid = window.url_query_params.get('app_uid');
  336. let origin = window.openerOrigin ?? window.url_query_params.get('origin');
  337. let permission = window.url_query_params.get('permission');
  338. let granted = await UIWindowRequestPermission({
  339. app_uid: app_uid,
  340. origin: origin,
  341. permission: permission,
  342. });
  343. let messageTarget = window.embedded_in_popup ? window.opener : window.parent;
  344. messageTarget.postMessage({
  345. msg: "permissionGranted",
  346. granted: granted,
  347. }, origin);
  348. }
  349. //--------------------------------------------------------------------------------------
  350. // Action: Password recovery
  351. //--------------------------------------------------------------------------------------
  352. else if(action === 'set-new-password'){
  353. let user = window.url_query_params.get('user');
  354. let token = window.url_query_params.get('token');
  355. await UIWindowNewPassword({
  356. user: user,
  357. token: token,
  358. });
  359. }
  360. //--------------------------------------------------------------------------------------
  361. // Action: Change Username
  362. //--------------------------------------------------------------------------------------
  363. else if(action === 'change-username'){
  364. await UIWindowChangeUsername();
  365. }
  366. //--------------------------------------------------------------------------------------
  367. // Action: Login
  368. //--------------------------------------------------------------------------------------
  369. else if(action === 'login'){
  370. await UIWindowLogin();
  371. }
  372. //--------------------------------------------------------------------------------------
  373. // Action: Signup
  374. //--------------------------------------------------------------------------------------
  375. else if(action === 'signup'){
  376. await UIWindowSignup();
  377. }
  378. // -------------------------------------------------------------------------------------
  379. // If in embedded in a popup, it is important to check whether the opener app has a relationship with the user
  380. // if yes, we need to get the user app token and send it to the opener
  381. // if not, we need to ask the user for confirmation before proceeding BUT only if the action is a file-picker action
  382. // -------------------------------------------------------------------------------------
  383. if(window.embedded_in_popup && window.openerOrigin){
  384. let response = await window.checkUserSiteRelationship(window.openerOrigin);
  385. window.userAppToken = response.token;
  386. if(!picked_a_user_for_sdk_login && window.logged_in_users.length > 0 && (!window.userAppToken || window.url_query_params.get('request_auth') )){
  387. await UIWindowSessionList({
  388. reload_on_success: false,
  389. draggable_body: false,
  390. has_head: false,
  391. cover_page: true,
  392. });
  393. }
  394. // if not and action is show-open-file-picker, we need confirmation before proceeding
  395. if(action === 'show-open-file-picker' || action === 'show-save-file-picker' || action === 'show-directory-picker'){
  396. if(!window.userAppToken){
  397. let is_confirmed = await PuterDialog();
  398. if(is_confirmed === false){
  399. if(!window.is_auth()){
  400. window.first_visit_ever = false;
  401. localStorage.removeItem("has_visited_before", true);
  402. }
  403. window.close();
  404. window.open('','_self').close();
  405. }
  406. }
  407. }
  408. }
  409. // -------------------------------------------------------------------------------------
  410. // `auth_token` provided in URL, use it to log in
  411. // -------------------------------------------------------------------------------------
  412. else if(window.url_query_params.has('auth_token')){
  413. let query_param_auth_token = window.url_query_params.get('auth_token');
  414. try{
  415. whoami = await puter.os.user();
  416. }catch(e){
  417. if(e.status === 401){
  418. window.logout();
  419. return;
  420. }
  421. }
  422. if(whoami){
  423. if(whoami.requires_email_confirmation){
  424. let is_verified;
  425. do{
  426. is_verified = await UIWindowEmailConfirmationRequired({
  427. stay_on_top: true,
  428. has_head: false
  429. });
  430. }
  431. while(!is_verified)
  432. }
  433. // if user is logging in using an auth token that means it's not their first ever visit to Puter.com
  434. // it might be their first visit to Puter on this specific device but it's not their first time ever visiting Puter.
  435. window.first_visit_ever = false;
  436. // show login progress window
  437. UIWindowLoginInProgress({user_info: whoami});
  438. // update auth data
  439. window.update_auth_data(query_param_auth_token, whoami);
  440. }
  441. // remove auth_token from URL
  442. window.history.pushState(null, document.title, '/');
  443. }
  444. /**
  445. * Logout without showing confirmation or "Save Account" action,
  446. * and without authenticating with the server.
  447. */
  448. const bad_session_logout = async () => {
  449. try {
  450. // TODO: i18n
  451. await UIAlert({
  452. message: 'Your session is invalid. You will be logged out.'
  453. });
  454. // clear local storage
  455. localStorage.clear();
  456. // reload the page
  457. window.location.reload();
  458. }catch(e){
  459. // TODO: i18n
  460. await UIAlert({
  461. message: 'Session is invalid and logout failed; ' +
  462. 'please clear local storage manually.'
  463. });
  464. }
  465. };
  466. // -------------------------------------------------------------------------------------
  467. // Authed
  468. // -------------------------------------------------------------------------------------
  469. if(window.is_auth()){
  470. // try to get user data using /whoami, only if that data is missing
  471. if(!whoami){
  472. try{
  473. whoami = await puter.os.user();
  474. }catch(e){
  475. if(e.status === 401){
  476. bad_session_logout();
  477. return;
  478. }
  479. }
  480. }
  481. // update local user data
  482. if(whoami){
  483. // is email confirmation required?
  484. if(whoami.requires_email_confirmation){
  485. let is_verified;
  486. do{
  487. is_verified = await UIWindowEmailConfirmationRequired({
  488. stay_on_top: true,
  489. has_head: false
  490. });
  491. }
  492. while(!is_verified)
  493. }
  494. window.update_auth_data(whoami.token || window.auth_token, whoami);
  495. // -------------------------------------------------------------------------------------
  496. // Load desktop, only if we're not embedded in a popup
  497. // -------------------------------------------------------------------------------------
  498. if(!window.embedded_in_popup){
  499. await window.get_auto_arrange_data()
  500. puter.fs.stat(window.desktop_path, async function(desktop_fsentry){
  501. UIDesktop({desktop_fsentry: desktop_fsentry});
  502. })
  503. }
  504. // -------------------------------------------------------------------------------------
  505. // If embedded in a popup, send the token to the opener and close the popup
  506. // -------------------------------------------------------------------------------------
  507. else{
  508. let msg_id = window.url_query_params.get('msg_id');
  509. try{
  510. let data = await window.getUserAppToken(new URL(window.openerOrigin).origin);
  511. // This is an implicit app and the app_uid is sent back from the server
  512. // we cache it here so that we can use it later
  513. window.host_app_uid = data.app_uid;
  514. // send token to parent
  515. window.opener.postMessage({
  516. msg: 'puter.token',
  517. success: true,
  518. token: data.token,
  519. app_uid: data.app_uid,
  520. username: window.user.username,
  521. msg_id: msg_id,
  522. }, window.openerOrigin);
  523. // close popup
  524. if(!action || action==='sign-in'){
  525. window.close();
  526. window.open('','_self').close();
  527. }
  528. }catch(err){
  529. // send error to parent
  530. window.opener.postMessage({
  531. msg: 'puter.token',
  532. success: false,
  533. token: null,
  534. msg_id: msg_id,
  535. }, window.openerOrigin);
  536. // close popup
  537. window.close();
  538. window.open('','_self').close();
  539. }
  540. let app_uid;
  541. if(window.openerOrigin){
  542. app_uid = await window.getAppUIDFromOrigin(window.openerOrigin);
  543. window.host_app_uid = app_uid;
  544. }
  545. if(action === 'show-open-file-picker'){
  546. let options = window.url_query_params.get('options');
  547. options = JSON.parse(options ?? '{}');
  548. // Open dialog
  549. UIWindow({
  550. allowed_file_types: options?.accept,
  551. selectable_body: options?.multiple,
  552. path: '/' + window.user.username + '/Desktop',
  553. // this is the uuid of the window to which this dialog will return
  554. return_to_parent_window: true,
  555. show_maximize_button: false,
  556. show_minimize_button: false,
  557. title: 'Open',
  558. is_dir: true,
  559. is_openFileDialog: true,
  560. is_resizable: false,
  561. has_head: false,
  562. cover_page: true,
  563. // selectable_body: is_selectable_body,
  564. iframe_msg_uid: msg_id,
  565. center: true,
  566. initiating_app_uuid: app_uid,
  567. on_close: function(){
  568. window.opener.postMessage({
  569. msg: "fileOpenCanceled",
  570. original_msg_id: msg_id,
  571. }, '*');
  572. }
  573. });
  574. }
  575. //--------------------------------------------------------------------------------------
  576. // Action: Show Directory Picker
  577. //--------------------------------------------------------------------------------------
  578. else if(action === 'show-directory-picker'){
  579. // open directory picker dialog
  580. UIWindow({
  581. path: '/' + window.user.username + '/Desktop',
  582. // this is the uuid of the window to which this dialog will return
  583. // parent_uuid: event.data.appInstanceID,
  584. return_to_parent_window: true,
  585. show_maximize_button: false,
  586. show_minimize_button: false,
  587. title: 'Open',
  588. is_dir: true,
  589. is_directoryPicker: true,
  590. is_resizable: false,
  591. has_head: false,
  592. cover_page: true,
  593. // selectable_body: is_selectable_body,
  594. iframe_msg_uid: msg_id,
  595. center: true,
  596. initiating_app_uuid: app_uid,
  597. on_close: function(){
  598. window.opener.postMessage({
  599. msg: "directoryOpenCanceled",
  600. original_msg_id: msg_id,
  601. }, '*');
  602. }
  603. });
  604. }
  605. //--------------------------------------------------------------------------------------
  606. // Action: Show Save File Dialog
  607. //--------------------------------------------------------------------------------------
  608. else if(action === 'show-save-file-picker'){
  609. let allowed_file_types = window.url_query_params.get('allowed_file_types');
  610. // send 'sendMeFileData' event to parent
  611. window.opener.postMessage({
  612. msg: 'sendMeFileData',
  613. }, '*');
  614. // listen for 'showSaveFilePickerPopup' event from parent
  615. window.addEventListener('message', async (event) => {
  616. if(event.data.msg !== 'showSaveFilePickerPopup')
  617. return;
  618. // Open dialog
  619. UIWindow({
  620. allowed_file_types: allowed_file_types,
  621. path: '/' + window.user.username + '/Desktop',
  622. // this is the uuid of the window to which this dialog will return
  623. return_to_parent_window: true,
  624. show_maximize_button: false,
  625. show_minimize_button: false,
  626. title: 'Save',
  627. is_dir: true,
  628. is_saveFileDialog: true,
  629. is_resizable: false,
  630. has_head: false,
  631. cover_page: true,
  632. // selectable_body: is_selectable_body,
  633. iframe_msg_uid: msg_id,
  634. center: true,
  635. initiating_app_uuid: app_uid,
  636. on_close: function(){
  637. window.opener.postMessage({
  638. msg: "fileSaveCanceled",
  639. original_msg_id: msg_id,
  640. }, '*');
  641. },
  642. onSaveFileDialogSave: async function(target_path, el_filedialog_window){
  643. $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').show();
  644. let busy_init_ts = Date.now();
  645. let overwrite = false;
  646. let file_to_upload = new File([event.data.content], path.basename(target_path));
  647. let item_with_same_name_already_exists = true;
  648. while(item_with_same_name_already_exists){
  649. // overwrite?
  650. if(overwrite)
  651. item_with_same_name_already_exists = false;
  652. // upload
  653. try{
  654. const res = await puter.fs.write(
  655. target_path,
  656. file_to_upload,
  657. {
  658. dedupeName: false,
  659. overwrite: overwrite
  660. }
  661. );
  662. let file_signature = await puter.fs.sign(app_uid, {uid: res.uid, action: 'write'});
  663. file_signature = file_signature.items;
  664. item_with_same_name_already_exists = false;
  665. window.opener.postMessage({
  666. msg: "fileSaved",
  667. original_msg_id: msg_id,
  668. filename: res.name,
  669. saved_file: {
  670. name: file_signature.fsentry_name,
  671. readURL: file_signature.read_url,
  672. writeURL: file_signature.write_url,
  673. metadataURL: file_signature.metadata_url,
  674. type: file_signature.type,
  675. uid: file_signature.uid,
  676. path: privacy_aware_path(res.path),
  677. },
  678. }, '*');
  679. window.close();
  680. window.open('','_self').close();
  681. }
  682. catch(err){
  683. // item with same name exists
  684. if(err.code === 'item_with_same_name_exists'){
  685. const alert_resp = await UIAlert({
  686. message: `<strong>${html_encode(err.entry_name)}</strong> already exists.`,
  687. buttons:[
  688. {
  689. label: i18n('replace'),
  690. value: 'replace',
  691. type: 'primary',
  692. },
  693. {
  694. label: i18n('cancel'),
  695. value: 'cancel',
  696. },
  697. ],
  698. parent_uuid: $(el_filedialog_window).attr('data-element_uuid'),
  699. })
  700. if(alert_resp === 'replace'){
  701. overwrite = true;
  702. }else if(alert_resp === 'cancel'){
  703. // enable parent window
  704. $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide();
  705. return;
  706. }
  707. }
  708. else{
  709. console.log(err);
  710. // show error
  711. await UIAlert({
  712. message: err.message ?? "Upload failed.",
  713. parent_uuid: $(el_filedialog_window).attr('data-element_uuid'),
  714. });
  715. // enable parent window
  716. $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide();
  717. return;
  718. }
  719. }
  720. }
  721. // done
  722. let busy_duration = (Date.now() - busy_init_ts);
  723. if( busy_duration >= window.busy_indicator_hide_delay){
  724. $(el_filedialog_window).close();
  725. }else{
  726. setTimeout(() => {
  727. // close this dialog
  728. $(el_filedialog_window).close();
  729. }, Math.abs(window.busy_indicator_hide_delay - busy_duration));
  730. }
  731. }
  732. });
  733. });
  734. }
  735. }
  736. // ----------------------------------------------------------
  737. // Get user's sites
  738. // ----------------------------------------------------------
  739. window.update_sites_cache();
  740. }
  741. }
  742. // -------------------------------------------------------------------------------------
  743. // Desktop Background
  744. // If we're in fullpage/emebedded/Auth Popup mode, we don't want to load the custom background
  745. // because it's not visible anyway and it's a waste of bandwidth
  746. // -------------------------------------------------------------------------------------
  747. if(!window.is_fullpage_mode && !window.embedded_in_popup){
  748. window.refresh_desktop_background();
  749. }
  750. // -------------------------------------------------------------------------------------
  751. // Un-authed but not first visit -> try to log in/sign up
  752. // -------------------------------------------------------------------------------------
  753. if(!window.is_auth() && !window.first_visit_ever){
  754. if(window.logged_in_users.length > 0){
  755. UIWindowSessionList();
  756. }
  757. else{
  758. await UIWindowLogin({
  759. reload_on_success: true,
  760. send_confirmation_code: false,
  761. window_options:{
  762. has_head: false
  763. }
  764. });
  765. }
  766. }
  767. // -------------------------------------------------------------------------------------
  768. // Un-authed and first visit ever -> create temp user
  769. // -------------------------------------------------------------------------------------
  770. else if(!window.is_auth() && window.first_visit_ever){
  771. let referrer;
  772. try{
  773. referrer = new URL(window.location.href).pathname;
  774. }catch(e){
  775. console.log(e)
  776. }
  777. referrer = window.openerOrigin ?? referrer;
  778. // a global object that will be used to store the user's referrer
  779. window.referrerStr = referrer;
  780. // in case there is also a referrer query param, add it to the referrer URL
  781. if(window.url_query_params.has('ref')){
  782. if(!referrer)
  783. referrer = '/';
  784. referrer += '?ref=' + html_encode(window.url_query_params.get('ref'));
  785. }
  786. let headers = {};
  787. if(window.custom_headers)
  788. headers = window.custom_headers;
  789. $.ajax({
  790. url: window.gui_origin + "/signup",
  791. type: 'POST',
  792. async: true,
  793. headers: headers,
  794. contentType: "application/json",
  795. data: JSON.stringify({
  796. referrer: referrer,
  797. referral_code: window.referral_code,
  798. is_temp: true,
  799. }),
  800. success: async function (data){
  801. window.update_auth_data(data.token, data.user);
  802. document.dispatchEvent(new Event("login", { bubbles: true}));
  803. },
  804. error: function (err){
  805. $('#signup-error-msg').html(html_encode(err.responseText));
  806. $('#signup-error-msg').fadeIn();
  807. // re-enable 'Create Account' button
  808. $('.signup-btn').prop('disabled', false);
  809. }
  810. });
  811. }
  812. // if there is at least one window open (only non-Explorer windows), ask user for confirmation when navigating away
  813. if(window.feature_flags.prompt_user_when_navigation_away_from_puter){
  814. window.onbeforeunload = function(){
  815. if($(`.window:not(.window[data-app="explorer"])`).length > 0)
  816. return true;
  817. };
  818. }
  819. // -------------------------------------------------------------------------------------
  820. // `login` event handler
  821. // --------------------------------------------------------------------------------------
  822. $(document).on("login", async (e) => {
  823. // close all windows
  824. $('.window').close();
  825. // -------------------------------------------------------------------------------------
  826. // Load desktop, if not embedded in a popup
  827. // -------------------------------------------------------------------------------------
  828. if(!window.embedded_in_popup){
  829. await window.get_auto_arrange_data();
  830. puter.fs.stat(window.desktop_path, function (desktop_fsentry) {
  831. UIDesktop({ desktop_fsentry: desktop_fsentry });
  832. })
  833. }
  834. // -------------------------------------------------------------------------------------
  835. // If embedded in a popup, send the 'ready' event to referrer and close the popup
  836. // -------------------------------------------------------------------------------------
  837. else{
  838. let msg_id = window.url_query_params.get('msg_id');
  839. try{
  840. let data = await window.getUserAppToken(new URL(window.openerOrigin).origin);
  841. // This is an implicit app and the app_uid is sent back from the server
  842. // we cache it here so that we can use it later
  843. window.host_app_uid = data.app_uid;
  844. // send token to parent
  845. window.opener.postMessage({
  846. msg: 'puter.token',
  847. success: true,
  848. msg_id: msg_id,
  849. token: data.token,
  850. username: window.user.username,
  851. app_uid: data.app_uid,
  852. }, window.openerOrigin);
  853. // close popup
  854. if(!action || action==='sign-in'){
  855. window.close();
  856. window.open('','_self').close();
  857. }
  858. }catch(err){
  859. // send error to parent
  860. window.opener.postMessage({
  861. msg: 'puter.token',
  862. msg_id: msg_id,
  863. success: false,
  864. token: null,
  865. }, window.openerOrigin);
  866. // close popup
  867. window.close();
  868. window.open('','_self').close();
  869. }
  870. let app_uid;
  871. if(window.openerOrigin){
  872. app_uid = await window.getAppUIDFromOrigin(window.openerOrigin);
  873. window.host_app_uid = app_uid;
  874. }
  875. //--------------------------------------------------------------------------------------
  876. // Action: Show Open File Picker
  877. //--------------------------------------------------------------------------------------
  878. if(action === 'show-open-file-picker'){
  879. let options = window.url_query_params.get('options');
  880. options = JSON.parse(options ?? '{}');
  881. // Open dialog
  882. UIWindow({
  883. allowed_file_types: options?.accept,
  884. selectable_body: options?.multiple,
  885. path: '/' + window.user.username + '/Desktop',
  886. return_to_parent_window: true,
  887. show_maximize_button: false,
  888. show_minimize_button: false,
  889. title: 'Open',
  890. is_dir: true,
  891. is_openFileDialog: true,
  892. is_resizable: false,
  893. has_head: false,
  894. cover_page: true,
  895. iframe_msg_uid: msg_id,
  896. center: true,
  897. initiating_app_uuid: app_uid,
  898. on_close: function(){
  899. window.opener.postMessage({
  900. msg: "fileOpenCanceled",
  901. original_msg_id: msg_id,
  902. }, '*');
  903. }
  904. });
  905. }
  906. //--------------------------------------------------------------------------------------
  907. // Action: Show Directory Picker
  908. //--------------------------------------------------------------------------------------
  909. else if(action === 'show-directory-picker'){
  910. // open directory picker dialog
  911. UIWindow({
  912. path: '/' + window.user.username + '/Desktop',
  913. // this is the uuid of the window to which this dialog will return
  914. // parent_uuid: event.data.appInstanceID,
  915. return_to_parent_window: true,
  916. show_maximize_button: false,
  917. show_minimize_button: false,
  918. title: 'Open',
  919. is_dir: true,
  920. is_directoryPicker: true,
  921. is_resizable: false,
  922. has_head: false,
  923. cover_page: true,
  924. // selectable_body: is_selectable_body,
  925. iframe_msg_uid: msg_id,
  926. center: true,
  927. initiating_app_uuid: app_uid,
  928. on_close: function(){
  929. window.opener.postMessage({
  930. msg: "directoryOpenCanceled",
  931. original_msg_id: msg_id,
  932. }, '*');
  933. }
  934. });
  935. }
  936. //--------------------------------------------------------------------------------------
  937. // Action: Show Save File Dialog
  938. //--------------------------------------------------------------------------------------
  939. else if(action === 'show-save-file-picker'){
  940. let allowed_file_types = window.url_query_params.get('allowed_file_types');
  941. // send 'sendMeFileData' event to parent
  942. window.opener.postMessage({
  943. msg: 'sendMeFileData',
  944. }, '*');
  945. // listen for 'showSaveFilePickerPopup' event from parent
  946. window.addEventListener('message', async (event) => {
  947. if(event.data.msg !== 'showSaveFilePickerPopup')
  948. return;
  949. // Open dialog
  950. UIWindow({
  951. allowed_file_types: allowed_file_types,
  952. path: '/' + window.user.username + '/Desktop',
  953. // this is the uuid of the window to which this dialog will return
  954. return_to_parent_window: true,
  955. show_maximize_button: false,
  956. show_minimize_button: false,
  957. title: 'Save',
  958. is_dir: true,
  959. is_saveFileDialog: true,
  960. is_resizable: false,
  961. has_head: false,
  962. cover_page: true,
  963. // selectable_body: is_selectable_body,
  964. iframe_msg_uid: msg_id,
  965. center: true,
  966. initiating_app_uuid: app_uid,
  967. on_close: function(){
  968. window.opener.postMessage({
  969. msg: "fileSaveCanceled",
  970. original_msg_id: msg_id,
  971. }, '*');
  972. },
  973. onSaveFileDialogSave: async function(target_path, el_filedialog_window){
  974. $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').show();
  975. let busy_init_ts = Date.now();
  976. let overwrite = false;
  977. let file_to_upload = new File([event.data.content], path.basename(target_path));
  978. let item_with_same_name_already_exists = true;
  979. while(item_with_same_name_already_exists){
  980. // overwrite?
  981. if(overwrite)
  982. item_with_same_name_already_exists = false;
  983. // upload
  984. try{
  985. const res = await puter.fs.write(
  986. target_path,
  987. file_to_upload,
  988. {
  989. dedupeName: false,
  990. overwrite: overwrite
  991. }
  992. );
  993. let file_signature = await puter.fs.sign(app_uid, {uid: res.uid, action: 'write'});
  994. file_signature = file_signature.items;
  995. item_with_same_name_already_exists = false;
  996. window.opener.postMessage({
  997. msg: "fileSaved",
  998. original_msg_id: msg_id,
  999. filename: res.name,
  1000. saved_file: {
  1001. name: file_signature.fsentry_name,
  1002. readURL: file_signature.read_url,
  1003. writeURL: file_signature.write_url,
  1004. metadataURL: file_signature.metadata_url,
  1005. type: file_signature.type,
  1006. uid: file_signature.uid,
  1007. path: privacy_aware_path(res.path),
  1008. },
  1009. }, '*');
  1010. window.close();
  1011. window.open('','_self').close();
  1012. // show_save_account_notice_if_needed();
  1013. }
  1014. catch(err){
  1015. // item with same name exists
  1016. if(err.code === 'item_with_same_name_exists'){
  1017. const alert_resp = await UIAlert({
  1018. message: `<strong>${html_encode(err.entry_name)}</strong> already exists.`,
  1019. buttons:[
  1020. {
  1021. label: i18n('replace'),
  1022. value: 'replace',
  1023. type: 'primary',
  1024. },
  1025. {
  1026. label: i18n('cancel'),
  1027. value: 'cancel',
  1028. },
  1029. ],
  1030. parent_uuid: $(el_filedialog_window).attr('data-element_uuid'),
  1031. })
  1032. if(alert_resp === 'replace'){
  1033. overwrite = true;
  1034. }else if(alert_resp === 'cancel'){
  1035. // enable parent window
  1036. $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide();
  1037. return;
  1038. }
  1039. }
  1040. else{
  1041. console.log(err);
  1042. // show error
  1043. await UIAlert({
  1044. message: err.message ?? "Upload failed.",
  1045. parent_uuid: $(el_filedialog_window).attr('data-element_uuid'),
  1046. });
  1047. // enable parent window
  1048. $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide();
  1049. return;
  1050. }
  1051. }
  1052. }
  1053. // done
  1054. let busy_duration = (Date.now() - busy_init_ts);
  1055. if( busy_duration >= window.busy_indicator_hide_delay){
  1056. $(el_filedialog_window).close();
  1057. }else{
  1058. setTimeout(() => {
  1059. // close this dialog
  1060. $(el_filedialog_window).close();
  1061. }, Math.abs(window.busy_indicator_hide_delay - busy_duration));
  1062. }
  1063. }
  1064. });
  1065. });
  1066. }
  1067. }
  1068. })
  1069. $(".popover, .context-menu").on("remove", function () {
  1070. $('.window-active .window-app-iframe').css('pointer-events', 'all');
  1071. })
  1072. // If the document is clicked/tapped somewhere
  1073. $(document).bind("mousedown touchstart", function (e) {
  1074. // update last touch coordinates
  1075. update_last_touch_coordinates(e);
  1076. // dismiss touchstart on regular devices
  1077. if(e.type === 'touchstart' && !isMobile.phone && !isMobile.tablet)
  1078. return;
  1079. // If .item-container clicked, unselect all its item children
  1080. if($(e.target).hasClass('item-container') && !e.ctrlKey && !e.metaKey){
  1081. $(e.target).children('.item-selected').removeClass('item-selected');
  1082. window.update_explorer_footer_selected_items_count(e.target);
  1083. }
  1084. // If the clicked element is not a context menu, remove all context menus
  1085. if ($(e.target).parents(".context-menu").length === 0) {
  1086. const $ctxmenus = $(".context-menu");
  1087. $ctxmenus.fadeOut(200, function(){
  1088. $ctxmenus.remove();
  1089. });
  1090. }
  1091. // click on anything will close all popovers, but there are some exceptions
  1092. if(!$(e.target).hasClass('start-app')
  1093. && !$(e.target).hasClass('launch-search')
  1094. && !$(e.target).hasClass('launch-search-clear')
  1095. && $(e.target).closest('.start-app').length === 0
  1096. && !isMobile.phone && !isMobile.tablet
  1097. && !$(e.target).hasClass('popover')
  1098. && $(e.target).parents('.popover').length === 0){
  1099. $(".popover").fadeOut(200, function(){
  1100. $(".popover").remove();
  1101. });
  1102. }
  1103. // Close all tooltips
  1104. $('.ui-tooltip').remove();
  1105. // rename items whose names were being edited
  1106. if(!$(e.target).hasClass('item-name-editor')){
  1107. // blurring an Item Name Editor will automatically trigger renaming the item
  1108. $(".item-name-editor-active").blur();
  1109. }
  1110. // update active_item_container
  1111. if($(e.target).hasClass('item-container')){
  1112. window.active_item_container = e.target;
  1113. }else{
  1114. let ic = $(e.target).closest('.item-container')
  1115. if(ic.length > 0){
  1116. window.active_item_container = ic.get(0);
  1117. }else{
  1118. let pp = $(e.target).find('.item-container')
  1119. if(pp.length > 0){
  1120. window.active_item_container = pp.get(0);
  1121. }
  1122. }
  1123. }
  1124. //active element
  1125. window.active_element = e.target;
  1126. });
  1127. // update mouse position coordinates
  1128. $(document).mousemove(function(event){
  1129. update_mouse_position(event.clientX, event.clientY);
  1130. });
  1131. //--------------------------------------------------------
  1132. // Window Activation
  1133. //--------------------------------------------------------
  1134. $(document).on('mousedown', function(e){
  1135. // if taskbar or any parts of it is clicked, drop the event
  1136. if($(e.target).hasClass('taskbar') || $(e.target).closest('.taskbar').length > 0)
  1137. return;
  1138. // if mouse is clicked on a window, activate it
  1139. if(window.mouseover_window !== undefined){
  1140. $(window.mouseover_window).focusWindow(e);
  1141. }
  1142. })
  1143. // if an element has the .long-hover class, fire a long-hover event after 600ms
  1144. $(document).on('mouseenter', '.long-hover', function(){
  1145. let el = this;
  1146. el.long_hover_timeout = setTimeout(() => {
  1147. $(el).trigger('long-hover');
  1148. }, 600);
  1149. })
  1150. // if an element has the .long-hover class, cancel the long-hover event if the mouse leaves
  1151. $(document).on('mouseleave', '.long-hover', function(){
  1152. clearTimeout(this.long_hover_timeout);
  1153. })
  1154. // if an element has the .long-hover class, cancel the long-hover event if the mouse leaves
  1155. $(document).on('paste', function(event){
  1156. event = event.originalEvent ?? event;
  1157. let clipboardData = event.clipboardData || window.clipboardData;
  1158. let items = clipboardData.items || clipboardData.files;
  1159. // return if paste is on input or textarea
  1160. if($(event.target).is('input') || $(event.target).is('textarea'))
  1161. return;
  1162. if(!(items instanceof DataTransferItemList))
  1163. return;
  1164. // upload files
  1165. if(items?.length>0){
  1166. let parent_container = determine_active_container_parent();
  1167. if(parent_container){
  1168. window.upload_items(items, $(parent_container).attr('data-path'));
  1169. }
  1170. }
  1171. event.stopPropagation();
  1172. event.preventDefault();
  1173. return false;
  1174. })
  1175. document.addEventListener("visibilitychange", (event) => {
  1176. if (document.visibilityState !== "visible") {
  1177. window.doc_title_before_blur = document.title;
  1178. if(!_.isEmpty(window.active_uploads)){
  1179. update_title_based_on_uploads();
  1180. }
  1181. }else if(window.active_uploads){
  1182. document.title = window.doc_title_before_blur ?? 'Puter';
  1183. }
  1184. });
  1185. /**
  1186. * Event handler for a custom 'logout' event attached to the document.
  1187. * This function handles the process of logging out, including user confirmation,
  1188. * communication with the backend, and subsequent UI updates. It takes special
  1189. * precautions if the user is identified as using a temporary account.
  1190. *
  1191. * @listens Document#event:logout
  1192. * @async
  1193. * @param {Event} event - The JQuery event object associated with the logout event.
  1194. * @returns {Promise<void>} - This function does not return anything meaningful, but it performs an asynchronous operation.
  1195. */
  1196. $(document).on("logout", async function(event) {
  1197. // is temp user?
  1198. if(window.user && window.user.is_temp && !window.user.deleted){
  1199. const alert_resp = await UIAlert({
  1200. message: `<strong>Save account before logging out!</strong><p>You are using a temporary account and logging out will erase all your data.</p>`,
  1201. buttons:[
  1202. {
  1203. label: i18n('save_account'),
  1204. value: 'save_account',
  1205. type: 'primary',
  1206. },
  1207. {
  1208. label: i18n('log_out'),
  1209. value: 'log_out',
  1210. type: 'danger',
  1211. },
  1212. {
  1213. label: i18n('cancel'),
  1214. },
  1215. ]
  1216. })
  1217. if(alert_resp === 'save_account'){
  1218. let saved = await UIWindowSaveAccount({
  1219. send_confirmation_code: false,
  1220. default_username: window.user.username
  1221. });
  1222. if(saved)
  1223. window.logout();
  1224. }else if (alert_resp === 'log_out'){
  1225. window.logout();
  1226. }
  1227. else{
  1228. return;
  1229. }
  1230. }
  1231. // logout
  1232. try{
  1233. const resp = await fetch(`${window.gui_origin}/get-anticsrf-token`);
  1234. const { token } = await resp.json();
  1235. await $.ajax({
  1236. url: window.gui_origin + "/logout",
  1237. type: 'POST',
  1238. async: true,
  1239. contentType: "application/json",
  1240. headers: {
  1241. "Authorization": "Bearer " + window.auth_token
  1242. },
  1243. data: JSON.stringify({ anti_csrf: token }),
  1244. statusCode: {
  1245. 401: function () {
  1246. },
  1247. },
  1248. })
  1249. }catch(e){
  1250. // Ignored
  1251. }
  1252. // remove this user from the array of logged_in_users
  1253. for (let i = 0; i < window.logged_in_users.length; i++) {
  1254. if(window.logged_in_users[i].uuid === window.user.uuid){
  1255. window.logged_in_users.splice(i, 1);
  1256. break;
  1257. }
  1258. }
  1259. // update logged_in_users in local storage
  1260. localStorage.setItem('logged_in_users', JSON.stringify(window.logged_in_users));
  1261. // delete this user from local storage
  1262. window.user = null;
  1263. localStorage.removeItem('user');
  1264. window.auth_token = null;
  1265. localStorage.removeItem('auth_token');
  1266. // close all windows
  1267. $('.window').close();
  1268. // close all ctxmenus
  1269. $('.context-menu').remove();
  1270. // remove desktop
  1271. $('.desktop').remove();
  1272. // remove taskbar
  1273. $('.taskbar').remove();
  1274. // disable native browser exit confirmation
  1275. window.onbeforeunload = null;
  1276. // go to home page
  1277. window.location.replace("/");
  1278. });
  1279. }
  1280. function requestOpenerOrigin() {
  1281. return new Promise((resolve, reject) => {
  1282. if (!window.opener) {
  1283. reject(new Error("No window.opener available"));
  1284. return;
  1285. }
  1286. // Function to handle the message event
  1287. const handleMessage = (event) => {
  1288. // Check if the message is the expected response
  1289. if (event.data.msg === 'originResponse') {
  1290. // Clean up by removing the event listener
  1291. window.removeEventListener('message', handleMessage);
  1292. resolve(event.origin);
  1293. }
  1294. };
  1295. // Set up the listener for the response
  1296. window.addEventListener('message', handleMessage, false);
  1297. // Send the request to the opener
  1298. window.opener.postMessage({ msg: 'requestOrigin' }, '*');
  1299. // Optional: Reject the promise if no response is received within a timeout
  1300. setTimeout(() => {
  1301. window.removeEventListener('message', handleMessage);
  1302. reject(new Error("Response timed out"));
  1303. }, 5000); // Timeout after 5 seconds
  1304. });
  1305. }
  1306. $(document).on('click', '.generic-close-window-button', function(e){
  1307. $(this).closest('.window').close();
  1308. });
  1309. // Re-calculate desktop height and width on window resize and re-position the login and signup windows
  1310. $(window).on("resize", function () {
  1311. // If host env is popup, don't continue because the popup window has its own resize requirements.
  1312. if (window.embedded_in_popup)
  1313. return;
  1314. const ratio = window.desktop_width / window.innerWidth;
  1315. window.desktop_height = window.innerHeight - window.toolbar_height - window.taskbar_height;
  1316. window.desktop_width = window.innerWidth;
  1317. // Re-center the login window
  1318. const top = $(".window-login").position()?.top;
  1319. const width = $(".window-login").width();
  1320. $(".window-login").css({
  1321. left: (window.desktop_width - width) / 2,
  1322. top: top / ratio,
  1323. });
  1324. // Re-center the create account window
  1325. const top2 = $(".window-signup").position()?.top;
  1326. const width2 = $(".window-signup").width();
  1327. $(".window-signup").css({
  1328. left: (window.desktop_width - width2) / 2,
  1329. top: top2 / ratio,
  1330. });
  1331. });
  1332. $(document).on('contextmenu', '.disable-context-menu', function(e){
  1333. if($(e.target).hasClass('disable-context-menu') ){
  1334. e.preventDefault();
  1335. return false;
  1336. }
  1337. })
  1338. /**
  1339. * Converts a file system path to a privacy-aware path.
  1340. * - Paths starting with `~/` are returned unchanged.
  1341. * - Paths starting with the user's home path are replaced with `~`.
  1342. * - Absolute paths not starting with the user's home path are returned unchanged.
  1343. * - Relative paths are prefixed with `~/`.
  1344. * - Other paths are returned unchanged.
  1345. *
  1346. * @param {string} fspath - The file system path to be converted.
  1347. * @returns {string} The privacy-aware path.
  1348. */
  1349. window.privacy_aware_path = function(fspath){
  1350. // e.g. /my_username/test.txt -> ~/test.txt
  1351. if(fspath.startsWith('~/'))
  1352. return fspath;
  1353. // e.g. /my_username/test.txt -> ~/test.txt
  1354. else if(fspath.startsWith(window.home_path))
  1355. return fspath.replace(window.home_path, '~');
  1356. // e.g. /other_username/test.txt -> /other_username/test.txt
  1357. else if(fspath.startsWith('/') && !fspath.startsWith(window.home_path))
  1358. return fspath;
  1359. // e.g. test.txt -> ~/test.txt
  1360. else if(!fspath.startsWith('/'))
  1361. return '~/' + fspath;
  1362. // e.g. /username/path/to/item -> /username/path/to/item
  1363. else
  1364. return fspath;
  1365. }