UIDesktop.js 65 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569
  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 path from "../lib/path.js"
  20. import UIWindowClaimReferral from "./UIWindowClaimReferral.js"
  21. import UIContextMenu from './UIContextMenu.js'
  22. import UIItem from './UIItem.js'
  23. import UIAlert from './UIAlert.js'
  24. import UIWindow from './UIWindow.js'
  25. import UIWindowSaveAccount from './UIWindowSaveAccount.js';
  26. import UIWindowDesktopBGSettings from "./UIWindowDesktopBGSettings.js"
  27. import UIWindowMyWebsites from "./UIWindowMyWebsites.js"
  28. import UIWindowFeedback from "./UIWindowFeedback.js"
  29. import UIWindowLogin from "./UIWindowLogin.js"
  30. import UIWindowQR from "./UIWindowQR.js"
  31. import UIWindowRefer from "./UIWindowRefer.js"
  32. import UITaskbar from "./UITaskbar.js"
  33. import new_context_menu_item from "../helpers/new_context_menu_item.js"
  34. import refresh_item_container from "../helpers/refresh_item_container.js"
  35. import changeLanguage from "../i18n/i18nChangeLanguage.js"
  36. import UIWindowSettings from "./Settings/UIWindowSettings.js"
  37. import UIWindowTaskManager from "./UIWindowTaskManager.js"
  38. import truncate_filename from '../helpers/truncate_filename.js';
  39. import UINotification from "./UINotification.js"
  40. async function UIDesktop(options){
  41. let h = '';
  42. // connect socket.
  43. window.socket = io(window.gui_origin + '/', {
  44. auth: {
  45. auth_token: window.auth_token
  46. }
  47. });
  48. window.socket.on('error', (error) => {
  49. console.error('GUI Socket Error:', error);
  50. });
  51. window.socket.on('connect', function(){
  52. // console.log('GUI Socket: Connected', window.socket.id);
  53. });
  54. window.socket.on('reconnect', function(){
  55. console.log('GUI Socket: Reconnected', window.socket.id);
  56. });
  57. window.socket.on('disconnect', () => {
  58. console.log('GUI Socket: Disconnected');
  59. });
  60. window.socket.on('reconnect', (attempt) => {
  61. console.log('GUI Socket: Reconnection', attempt);
  62. });
  63. window.socket.on('reconnect_attempt', (attempt) => {
  64. console.log('GUI Socket: Reconnection Attemps', attempt);
  65. });
  66. window.socket.on('reconnect_error', (error) => {
  67. console.log('GUI Socket: Reconnection Error', error);
  68. });
  69. window.socket.on('reconnect_failed', () => {
  70. console.log('GUI Socket: Reconnection Failed');
  71. });
  72. window.socket.on('error', (error) => {
  73. console.error('GUI Socket Error:', error);
  74. });
  75. window.socket.on('upload.progress', (msg) => {
  76. if(window.progress_tracker[msg.operation_id]){
  77. window.progress_tracker[msg.operation_id].cloud_uploaded += msg.loaded_diff
  78. if(window.progress_tracker[msg.operation_id][msg.item_upload_id]){
  79. window.progress_tracker[msg.operation_id][msg.item_upload_id].cloud_uploaded = msg.loaded;
  80. }
  81. }
  82. });
  83. window.socket.on('download.progress', (msg) => {
  84. if(window.progress_tracker[msg.operation_id]){
  85. if(window.progress_tracker[msg.operation_id][msg.item_upload_id]){
  86. window.progress_tracker[msg.operation_id][msg.item_upload_id].downloaded = msg.loaded;
  87. window.progress_tracker[msg.operation_id][msg.item_upload_id].total = msg.total;
  88. }
  89. }
  90. });
  91. window.socket.on('trash.is_empty', async (msg) => {
  92. $(`.item[data-path="${html_encode(window.trash_path)}" i]`).find('.item-icon > img').attr('src', msg.is_empty ? window.icons['trash.svg'] : window.icons['trash-full.svg']);
  93. $(`.window[data-path="${html_encode(window.trash_path)}" i]`).find('.window-head-icon').attr('src', msg.is_empty ? window.icons['trash.svg'] : window.icons['trash-full.svg']);
  94. // empty trash windows if needed
  95. if(msg.is_empty)
  96. $(`.window[data-path="${html_encode(window.trash_path)}" i]`).find('.item-container').empty();
  97. })
  98. window.socket.on('notif.message', ({ notification }) => {
  99. UINotification({
  100. content: notification.summary
  101. });
  102. });
  103. window.__already_got_unreads = false;
  104. window.socket.on('notif.unreads', ({ unreads }) => {
  105. if ( window.__already_got_unreads ) return;
  106. window.__already_got_unreads = true;
  107. for ( const notif_info of unreads ) {
  108. const notification = notif_info.notification;
  109. const icon = (sys => {
  110. if ( sys === 'share' ) return window.icons['shared.svg'];
  111. if ( sys === 'puter' ) return window.icons['logo-fill.svg'];
  112. return notification.builtin_icon
  113. ? window.icons[notification.builtin_icon]
  114. : notification.icon || window.icons['bell.svg'];
  115. })(notification.subsystem);
  116. UINotification({
  117. // icon: window.icons['shared.svg'],
  118. icon,
  119. title: notification.summary,
  120. text: notification.text ?? notification.summary,
  121. click: async () => {
  122. await fetch(`${window.api_origin}/notif/mark-read`, {
  123. method: 'POST',
  124. headers: {
  125. Authorization: `Bearer ${puter.authToken}`,
  126. 'Content-Type': 'application/json',
  127. },
  128. body: JSON.stringify({
  129. uid: notif_info.uid,
  130. }),
  131. });
  132. },
  133. });
  134. }
  135. });
  136. window.socket.on('app.opened', async (app) => {
  137. // don't update if this is the original client that initiated the action
  138. if(app.original_client_socket_id === window.socket.id)
  139. return;
  140. // add the app to the beginning of the array
  141. window.launch_apps.recent.unshift(app);
  142. // dedupe the array by uuid, uid, and id
  143. window.launch_apps.recent = _.uniqBy(window.launch_apps.recent, 'name');
  144. // limit to 5
  145. window.launch_apps.recent = window.launch_apps.recent.slice(0, window.launch_recent_apps_count);
  146. })
  147. window.socket.on('item.removed', async (item) => {
  148. // don't update if this is the original client that initiated the action
  149. if(item.original_client_socket_id === window.socket.id)
  150. return;
  151. // don't remove items if this was a descendants_only operation
  152. if(item.descendants_only)
  153. return;
  154. // hide all UIItems with matching uids
  155. $(`.item[data-path='${item.path}']`).fadeOut(150, function(){
  156. // close all windows with matching uids
  157. // $('.window-' + item.uid).close();
  158. // close all windows that belong to a descendant of this item
  159. // todo this has to be case-insensitive but the `i` selector doesn't work on ^=
  160. $(`.window[data-path^="${item.path}/"]`).close();
  161. });
  162. })
  163. window.socket.on('item.updated', async (item) => {
  164. // Don't update if this is the original client that initiated the action
  165. if(item.original_client_socket_id === window.socket.id)
  166. return;
  167. // Update matching items
  168. // set new item name
  169. $(`.item[data-uid='${html_encode(item.uid)}'] .item-name`).html(html_encode(truncate_filename(item.name)).replaceAll(' ', '&nbsp;'));
  170. // Set new icon
  171. const new_icon = (item.is_dir ? window.icons['folder.svg'] : (await window.item_icon(item)).image);
  172. $(`.item[data-uid='${item.uid}']`).find('.item-icon-thumb').attr('src', new_icon);
  173. $(`.item[data-uid='${item.uid}']`).find('.item-icon-icon').attr('src', new_icon);
  174. // Set new data-name
  175. $(`.item[data-uid='${item.uid}']`).attr('data-name', html_encode(item.name));
  176. $(`.window-${item.uid}`).attr('data-name', html_encode(item.name));
  177. // Set new title attribute
  178. $(`.item[data-uid='${item.uid}']`).attr('title', html_encode(item.name));
  179. $(`.window-${options.uid}`).attr('title', html_encode(item.name));
  180. // Set new value for item-name-editor
  181. $(`.item[data-uid='${item.uid}'] .item-name-editor`).val(html_encode(item.name));
  182. $(`.item[data-uid='${item.uid}'] .item-name`).attr('title', html_encode(item.name));
  183. // Set new data-path
  184. const new_path = item.path;
  185. $(`.item[data-uid='${item.uid}']`).attr('data-path', new_path);
  186. $(`.window-${item.uid}`).attr('data-path', new_path);
  187. // Update all elements that have matching paths
  188. $(`[data-path="${html_encode(item.old_path)}" i]`).each(function(){
  189. $(this).attr('data-path', new_path)
  190. if($(this).hasClass('window-navbar-path-dirname'))
  191. $(this).text(item.name);
  192. });
  193. // Update all elements whose paths start with old_path
  194. $(`[data-path^="${html_encode(item.old_path) + '/'}"]`).each(function(){
  195. const new_el_path = _.replace($(this).attr('data-path'), item.old_path + '/', new_path+'/');
  196. $(this).attr('data-path', new_el_path);
  197. });
  198. // Update all exact-matching windows
  199. $(`.window-${item.uid}`).each(function(){
  200. window.update_window_path(this, new_path);
  201. })
  202. // Set new name for matching open windows
  203. $(`.window-${item.uid} .window-head-title`).text(item.name);
  204. // Re-sort all matching item containers
  205. $(`.item[data-uid='${item.uid}']`).parent('.item-container').each(function(){
  206. window.sort_items(this, $(this).closest('.item-container').attr('data-sort_by'), $(this).closest('.item-container').attr('data-sort_order'));
  207. })
  208. })
  209. window.socket.on('item.moved', async (resp) => {
  210. let fsentry = resp;
  211. // Notify all apps that are watching this item
  212. window.sendItemChangeEventToWatchingApps(fsentry.uid, {
  213. event: 'moved',
  214. uid: fsentry.uid,
  215. name: fsentry.name,
  216. })
  217. // don't update if this is the original client that initiated the action
  218. if(resp.original_client_socket_id === window.socket.id)
  219. return;
  220. let dest_path = path.dirname(fsentry.path);
  221. let metadata = fsentry.metadata;
  222. // update all shortcut_to_path
  223. $(`.item[data-shortcut_to_path="${html_encode(resp.old_path)}" i]`).attr(`data-shortcut_to_path`, html_encode(fsentry.path));
  224. // remove all items with matching uids
  225. $(`.item[data-uid='${fsentry.uid}']`).fadeOut(150, function(){
  226. // find all parent windows that contain this item
  227. let parent_windows = $(`.item[data-uid='${fsentry.uid}']`).closest('.window');
  228. // remove this item
  229. $(this).removeItems();
  230. // update parent windows' item counts
  231. $(parent_windows).each(function(index){
  232. window.update_explorer_footer_item_count(this);
  233. window.update_explorer_footer_selected_items_count(this)
  234. });
  235. })
  236. // if trashing, close windows of trashed items and its descendants
  237. if(dest_path === window.trash_path){
  238. $(`.window[data-path="${html_encode(resp.old_path)}" i]`).close();
  239. // todo this has to be case-insensitive but the `i` selector doesn't work on ^=
  240. $(`.window[data-path^="${html_encode(resp.old_path)}/"]`).close();
  241. }
  242. // update all paths of its and its descendants' open windows
  243. else{
  244. // todo this has to be case-insensitive but the `i` selector doesn't work on ^=
  245. $(`.window[data-path^="${html_encode(resp.old_path)}/"], .window[data-path="${html_encode(resp.old_path)}" i]`).each(function(){
  246. window.update_window_path(this, $(this).attr('data-path').replace(resp.old_path, fsentry.path));
  247. })
  248. }
  249. if(dest_path === window.trash_path){
  250. $(`.item[data-uid="${fsentry.uid}"]`).find('.item-is-shared').fadeOut(300);
  251. // if trashing dir...
  252. if(fsentry.is_dir){
  253. // remove website badge
  254. $(`.mywebsites-dir-path[data-uuid="${fsentry.uid}"]`).remove();
  255. // remove the website badge from all instances of the dir
  256. $(`.item[data-uid="${fsentry.uid}"]`).find('.item-has-website-badge').fadeOut(300);
  257. // remove File Rrequest Token
  258. // todo, some client-side check to see if this dir has an FR associated with it before sending a whole ajax req
  259. }
  260. }
  261. // if replacing an existing item, remove the old item that was just replaced
  262. if(fsentry.overwritten_uid !== undefined)
  263. $(`.item[data-uid=${fsentry.overwritten_uid}]`).removeItems();
  264. // if this is trash, get original name from item metadata
  265. fsentry.name = (metadata && metadata.original_name) ? metadata.original_name : fsentry.name;
  266. // create new item on matching containers
  267. UIItem({
  268. appendTo: $(`.item-container[data-path='${html_encode(dest_path)}' i]`),
  269. immutable: fsentry.immutable,
  270. uid: fsentry.uid,
  271. path: fsentry.path,
  272. icon: await window.item_icon(fsentry),
  273. name: (dest_path === window.trash_path) ? metadata.original_name : fsentry.name,
  274. is_dir: fsentry.is_dir,
  275. size: fsentry.size,
  276. type: fsentry.type,
  277. modified: fsentry.modified,
  278. is_selected: false,
  279. is_shared: (dest_path === window.trash_path) ? false : fsentry.is_shared,
  280. is_shortcut: fsentry.is_shortcut,
  281. shortcut_to: fsentry.shortcut_to,
  282. shortcut_to_path: fsentry.shortcut_to_path,
  283. // has_website: $(el_item).attr('data-has_website') === '1',
  284. metadata: JSON.stringify(fsentry.metadata) ?? '',
  285. });
  286. if(fsentry.parent_dirs_created && fsentry.parent_dirs_created.length > 0){
  287. // this operation may have created some missing directories,
  288. // see if any of the directories in the path of this file is new AND
  289. // if these new path have any open parents that need to be updated
  290. fsentry.parent_dirs_created.forEach(async dir => {
  291. let item_container = $(`.item-container[data-path='${html_encode(path.dirname(dir.path))}' i]`);
  292. if(item_container.length > 0 && $(`.item[data-path="${html_encode(dir.path)}" i]`).length === 0){
  293. UIItem({
  294. appendTo: item_container,
  295. immutable: false,
  296. uid: dir.uid,
  297. path: dir.path,
  298. icon: await window.item_icon(dir),
  299. name: dir.name,
  300. size: dir.size,
  301. type: dir.type,
  302. modified: dir.modified,
  303. is_dir: true,
  304. is_selected: false,
  305. is_shared: dir.is_shared,
  306. has_website: false,
  307. });
  308. }
  309. window.sort_items(item_container, $(item_container).attr('data-sort_by'), $(item_container).attr('data-sort_order'));
  310. });
  311. }
  312. //sort each container
  313. $(`.item-container[data-path='${html_encode(dest_path)}' i]`).each(function(){
  314. window.sort_items(this, $(this).attr('data-sort_by'), $(this).attr('data-sort_order'))
  315. })
  316. });
  317. window.socket.on('user.email_confirmed', (msg) => {
  318. // don't update if this is the original client that initiated the action
  319. if(msg.original_client_socket_id === window.socket.id)
  320. return;
  321. window.refresh_user_data(window.auth_token);
  322. });
  323. window.socket.on('user.email_changed', (msg) => {
  324. // don't update if this is the original client that initiated the action
  325. if(msg.original_client_socket_id === window.socket.id)
  326. return;
  327. window.refresh_user_data(window.auth_token);
  328. });
  329. window.socket.on('item.renamed', async (item) => {
  330. // Notify all apps that are watching this item
  331. window.sendItemChangeEventToWatchingApps(item.uid, {
  332. event: 'rename',
  333. uid: item.uid,
  334. // path: item.path,
  335. new_name: item.name,
  336. // old_path: item.old_path,
  337. })
  338. // Don't update if this is the original client that initiated the action
  339. if(item.original_client_socket_id === window.socket.id)
  340. return;
  341. // Update matching items
  342. // Set new item name
  343. $(`.item[data-uid='${html_encode(item.uid)}'] .item-name`).html(html_encode(truncate_filename(item.name)).replaceAll(' ', '&nbsp;'));
  344. // Set new icon
  345. const new_icon = (item.is_dir ? window.icons['folder.svg'] : (await window.item_icon(item)).image);
  346. $(`.item[data-uid='${item.uid}']`).find('.item-icon-icon').attr('src', new_icon);
  347. // Set new data-name
  348. $(`.item[data-uid='${item.uid}']`).attr('data-name', html_encode(item.name));
  349. $(`.window-${item.uid}`).attr('data-name', html_encode(item.name));
  350. // Set new title attribute
  351. $(`.item[data-uid='${item.uid}']`).attr('title', html_encode(item.name));
  352. $(`.window-${options.uid}`).attr('title', html_encode(item.name));
  353. // Set new value for item-name-editor
  354. $(`.item[data-uid='${item.uid}'] .item-name-editor`).val(html_encode(item.name));
  355. $(`.item[data-uid='${item.uid}'] .item-name`).attr('title', html_encode(item.name));
  356. // Set new data-path
  357. const new_path = item.path;
  358. $(`.item[data-uid='${item.uid}']`).attr('data-path', new_path);
  359. $(`.window-${item.uid}`).attr('data-path', new_path);
  360. // Update all elements that have matching paths
  361. $(`[data-path="${html_encode(item.old_path)}" i]`).each(function(){
  362. $(this).attr('data-path', new_path)
  363. if($(this).hasClass('window-navbar-path-dirname'))
  364. $(this).text(item.name);
  365. });
  366. // Update all elements whose paths start with old_path
  367. $(`[data-path^="${html_encode(item.old_path) + '/'}"]`).each(function(){
  368. const new_el_path = _.replace($(this).attr('data-path'), item.old_path + '/', new_path+'/');
  369. $(this).attr('data-path', new_el_path);
  370. });
  371. // Update all exact-matching windows
  372. $(`.window-${item.uid}`).each(function(){
  373. window.update_window_path(this, new_path);
  374. })
  375. // Set new name for matching open windows
  376. $(`.window-${item.uid} .window-head-title`).text(item.name);
  377. // Re-sort all matching item containers
  378. $(`.item[data-uid='${item.uid}']`).parent('.item-container').each(function(){
  379. window.sort_items(this, $(this).closest('.item-container').attr('data-sort_by'), $(this).closest('.item-container').attr('data-sort_order'));
  380. })
  381. });
  382. window.socket.on('item.added', async (item) => {
  383. // if item is empty, don't proceed
  384. if(_.isEmpty(item))
  385. return;
  386. // Notify all apps that are watching this item
  387. window.sendItemChangeEventToWatchingApps(item.uid, {
  388. event: 'write',
  389. uid: item.uid,
  390. // path: item.path,
  391. new_size: item.size,
  392. modified: item.modified,
  393. // old_path: item.old_path,
  394. });
  395. // Don't update if this is the original client that initiated the action
  396. if(item.original_client_socket_id === window.socket.id)
  397. return;
  398. // Update replaced items with matching uids
  399. if(item.overwritten_uid){
  400. $(`.item[data-uid='${item.overwritten_uid}']`).attr({
  401. 'data-immutable': item.immutable,
  402. 'data-path': item.path,
  403. 'data-name': item.name,
  404. 'data-size': item.size,
  405. 'data-modified': item.modified,
  406. 'data-is_shared': item.is_shared,
  407. 'data-type': item.type,
  408. })
  409. // set new icon
  410. const new_icon = (item.is_dir ? window.icons['folder.svg'] : (await window.item_icon(item)).image);
  411. $(`.item[data-uid="${item.overwritten_uid}"]`).find('.item-icon > img').attr('src', new_icon);
  412. //sort each window
  413. $(`.item-container[data-path='${html_encode(item.dirpath)}' i]`).each(function(){
  414. window.sort_items(this, $(this).attr('data-sort_by'), $(this).attr('data-sort_order'))
  415. })
  416. }
  417. else{
  418. UIItem({
  419. appendTo: $(`.item-container[data-path='${html_encode(item.dirpath)}' i]`),
  420. uid: item.uid,
  421. immutable: item.immutable,
  422. associated_app_name: item.associated_app?.name,
  423. path: item.path,
  424. icon: await window.item_icon(item),
  425. name: item.name,
  426. size: item.size,
  427. type: item.type,
  428. modified: item.modified,
  429. is_dir: item.is_dir,
  430. is_shared: item.is_shared,
  431. is_shortcut: item.is_shortcut,
  432. shortcut_to: item.shortcut_to,
  433. shortcut_to_path: item.shortcut_to_path,
  434. });
  435. //sort each window
  436. $(`.item-container[data-path='${html_encode(item.dirpath)}' i]`).each(function(){
  437. window.sort_items(this, $(this).attr('data-sort_by'), $(this).attr('data-sort_order'))
  438. })
  439. }
  440. });
  441. // Hidden file dialog
  442. h += `<form name="upload-form" id="upload-form" style="display:hidden;">
  443. <input type="hidden" name="name" id="upload-filename" value="">
  444. <input type="hidden" name="path" id="upload-target-path" value="">
  445. <input type="file" name="file" id="upload-file-dialog" style="display: none;" multiple="multiple">
  446. </form>`;
  447. h += `<div class="window-container"></div>`;
  448. // Desktop
  449. // If desktop is not in fullpage/embedded mode, we hide it until files and directories are loaded and then fade in the UI
  450. // This gives a calm and smooth experience for the user
  451. h += `<div class="desktop item-container disable-user-select"
  452. data-uid="${options.desktop_fsentry.uid}"
  453. data-sort_by="${!options.desktop_fsentry.sort_by ? 'name' : options.desktop_fsentry.sort_by}"
  454. data-sort_order="${!options.desktop_fsentry.sort_order ? 'asc' : options.desktop_fsentry.sort_order}"
  455. data-path="${html_encode(window.desktop_path)}"
  456. >`;
  457. h += `</div>`;
  458. // Get window sidebar width
  459. window.getItem({
  460. key: "window_sidebar_width",
  461. success: async function(res){
  462. let value = parseInt(res.value);
  463. // if value is a valid number
  464. if(!isNaN(value) && value > 0){
  465. window.window_sidebar_width = value;
  466. }
  467. }
  468. })
  469. // Remove `?ref=...` from navbar URL
  470. if(window.url_query_params.has('ref')){
  471. window.history.pushState(null, document.title, '/');
  472. }
  473. // update local user preferences
  474. const user_preferences = {
  475. show_hidden_files: JSON.parse(await puter.kv.get('user_preferences.show_hidden_files')),
  476. language: await puter.kv.get('user_preferences.language'),
  477. clock_visible: await puter.kv.get('user_preferences.clock_visible'),
  478. };
  479. // update default apps
  480. puter.kv.list('user_preferences.default_apps.*').then(async (default_app_keys) => {
  481. for(let key in default_app_keys){
  482. user_preferences[default_app_keys[key].substring(17)] = await puter.kv.get(default_app_keys[key]);
  483. }
  484. window.update_user_preferences(user_preferences);
  485. });
  486. // Append to <body>
  487. $('body').append(h);
  488. // Set desktop height based on taskbar height
  489. $('.desktop').css('height', `calc(100vh - ${window.taskbar_height + window.toolbar_height}px)`)
  490. // ---------------------------------------------------------------
  491. // Taskbar
  492. // ---------------------------------------------------------------
  493. UITaskbar();
  494. const el_desktop = document.querySelector('.desktop');
  495. window.active_element = el_desktop;
  496. window.active_item_container = el_desktop;
  497. // --------------------------------------------------------
  498. // Dragster
  499. // Allow dragging of local files onto desktop.
  500. // --------------------------------------------------------
  501. $(el_desktop).dragster({
  502. enter: function (dragsterEvent, event) {
  503. $('.context-menu').remove();
  504. },
  505. leave: function (dragsterEvent, event) {
  506. },
  507. drop: async function (dragsterEvent, event) {
  508. const e = event.originalEvent;
  509. // no drop on item
  510. if($(event.target).hasClass('item') || $(event.target).parent('.item').length > 0)
  511. return false;
  512. // recursively create directories and upload files
  513. if(e.dataTransfer?.items?.length>0){
  514. window.upload_items(e.dataTransfer.items, window.desktop_path);
  515. }
  516. e.stopPropagation();
  517. e.preventDefault();
  518. return false;
  519. }
  520. });
  521. // --------------------------------------------------------
  522. // Droppable
  523. // --------------------------------------------------------
  524. $(el_desktop).droppable({
  525. accept: '.item',
  526. tolerance: "intersect",
  527. drop: function( event, ui ) {
  528. // Check if item was actually dropped on desktop and not a window
  529. if(window.mouseover_window !== undefined)
  530. return;
  531. // Can't drop anything but UIItems on desktop
  532. if(!$(ui.draggable).hasClass('item'))
  533. return;
  534. // Don't move an item to its current directory
  535. if( path.dirname($(ui.draggable).attr('data-path')) === window.desktop_path && !event.ctrlKey)
  536. return;
  537. // If ctrl is pressed and source is Trashed, cancel whole operation
  538. if(event.ctrlKey && path.dirname($(ui.draggable).attr('data-path')) === window.trash_path)
  539. return;
  540. // Unselect previously selected items
  541. $(el_desktop).children('.item-selected').removeClass('item-selected');
  542. const items_to_move = []
  543. // first item
  544. items_to_move.push(ui.draggable);
  545. // all subsequent items
  546. const cloned_items = document.getElementsByClassName('item-selected-clone');
  547. for(let i =0; i<cloned_items.length; i++){
  548. const source_item = document.getElementById('item-' + $(cloned_items[i]).attr('data-id'));
  549. if(source_item !== null)
  550. items_to_move.push(source_item);
  551. }
  552. // if ctrl key is down, copy items
  553. if(event.ctrlKey){
  554. // unless source is Trash
  555. if(path.dirname($(ui.draggable).attr('data-path')) === window.trash_path)
  556. return;
  557. window.copy_items(items_to_move, window.desktop_path)
  558. }
  559. // otherwise, move items
  560. else{
  561. window.move_items(items_to_move, window.desktop_path);
  562. }
  563. }
  564. });
  565. //--------------------------------------------------
  566. // ContextMenu
  567. //--------------------------------------------------
  568. $(el_desktop).bind("contextmenu taphold", function (event) {
  569. // dismiss taphold on regular devices
  570. if(event.type==='taphold' && !isMobile.phone && !isMobile.tablet)
  571. return;
  572. const $target = $(event.target);
  573. // elements that should retain native ctxmenu
  574. if($target.is('input') || $target.is('textarea'))
  575. return true
  576. // custom ctxmenu for all other elements
  577. if(event.target === el_desktop){
  578. event.preventDefault();
  579. UIContextMenu({
  580. items: [
  581. // -------------------------------------------
  582. // Sort by
  583. // -------------------------------------------
  584. {
  585. html: i18n('sort_by'),
  586. items: [
  587. {
  588. html: i18n('auto_arrange'),
  589. icon: window.is_auto_arrange_enabled ? '✓' : '',
  590. onClick: async function(){
  591. window.is_auto_arrange_enabled = !window.is_auto_arrange_enabled;
  592. window.store_auto_arrange_preference(window.is_auto_arrange_enabled);
  593. if(window.is_auto_arrange_enabled){
  594. window.sort_items(el_desktop, $(el_desktop).attr('data-sort_by'), $(el_desktop).attr('data-sort_order'));
  595. window.set_sort_by(options.desktop_fsentry.uid, $(el_desktop).attr('data-sort_by'), $(el_desktop).attr('data-sort_order'))
  596. window.clear_desktop_item_positions(el_desktop);
  597. }else{
  598. window.set_desktop_item_positions(el_desktop)
  599. }
  600. }
  601. },
  602. // -------------------------------------------
  603. // -
  604. // -------------------------------------------
  605. '-',
  606. {
  607. html: i18n('name'),
  608. disabled: !window.is_auto_arrange_enabled,
  609. icon: $(el_desktop).attr('data-sort_by') === 'name' ? '✓' : '',
  610. onClick: async function(){
  611. window.sort_items(el_desktop, 'name', $(el_desktop).attr('data-sort_order'));
  612. window.set_sort_by(options.desktop_fsentry.uid, 'name', $(el_desktop).attr('data-sort_order'))
  613. }
  614. },
  615. {
  616. html: i18n('date_modified'),
  617. disabled: !window.is_auto_arrange_enabled,
  618. icon: $(el_desktop).attr('data-sort_by') === 'modified' ? '✓' : '',
  619. onClick: async function(){
  620. window.sort_items(el_desktop, 'modified', $(el_desktop).attr('data-sort_order'));
  621. window.set_sort_by(options.desktop_fsentry.uid, 'modified', $(el_desktop).attr('data-sort_order'))
  622. }
  623. },
  624. {
  625. html: i18n('type'),
  626. disabled: !window.is_auto_arrange_enabled,
  627. icon: $(el_desktop).attr('data-sort_by') === 'type' ? '✓' : '',
  628. onClick: async function(){
  629. window.sort_items(el_desktop, 'type', $(el_desktop).attr('data-sort_order'));
  630. window.set_sort_by(options.desktop_fsentry.uid, 'type', $(el_desktop).attr('data-sort_order'))
  631. }
  632. },
  633. {
  634. html: i18n('size'),
  635. disabled: !window.is_auto_arrange_enabled,
  636. icon: $(el_desktop).attr('data-sort_by') === 'size' ? '✓' : '',
  637. onClick: async function(){
  638. window.sort_items(el_desktop, 'size', $(el_desktop).attr('data-sort_order'));
  639. window.set_sort_by(options.desktop_fsentry.uid, 'size', $(el_desktop).attr('data-sort_order'))
  640. }
  641. },
  642. // -------------------------------------------
  643. // -
  644. // -------------------------------------------
  645. '-',
  646. {
  647. html: i18n('ascending'),
  648. disabled: !window.is_auto_arrange_enabled,
  649. icon: $(el_desktop).attr('data-sort_order') === 'asc' ? '✓' : '',
  650. onClick: async function(){
  651. const sort_by = $(el_desktop).attr('data-sort_by')
  652. window.sort_items(el_desktop, sort_by, 'asc');
  653. window.set_sort_by(options.desktop_fsentry.uid, sort_by, 'asc')
  654. }
  655. },
  656. {
  657. html: i18n('descending'),
  658. disabled: !window.is_auto_arrange_enabled,
  659. icon: $(el_desktop).attr('data-sort_order') === 'desc' ? '✓' : '',
  660. onClick: async function(){
  661. const sort_by = $(el_desktop).attr('data-sort_by')
  662. window.sort_items(el_desktop, sort_by, 'desc');
  663. window.set_sort_by(options.desktop_fsentry.uid, sort_by, 'desc')
  664. }
  665. },
  666. ]
  667. },
  668. // -------------------------------------------
  669. // Refresh
  670. // -------------------------------------------
  671. {
  672. html: i18n('refresh'),
  673. onClick: function(){
  674. refresh_item_container(el_desktop);
  675. }
  676. },
  677. // -------------------------------------------
  678. // Show/Hide hidden files
  679. // -------------------------------------------
  680. {
  681. html: i18n('show_hidden'),
  682. icon: window.user_preferences.show_hidden_files ? '✓' : '',
  683. onClick: function(){
  684. window.mutate_user_preferences({
  685. show_hidden_files : !window.user_preferences.show_hidden_files,
  686. });
  687. window.show_or_hide_files(document.querySelectorAll('.item-container'));
  688. }
  689. },
  690. // -------------------------------------------
  691. // -
  692. // -------------------------------------------
  693. '-',
  694. // -------------------------------------------
  695. // New File
  696. // -------------------------------------------
  697. new_context_menu_item(window.desktop_path, el_desktop),
  698. // -------------------------------------------
  699. // -
  700. // -------------------------------------------
  701. '-',
  702. // -------------------------------------------
  703. // Paste
  704. // -------------------------------------------
  705. {
  706. html: i18n('paste'),
  707. disabled: window.clipboard.length > 0 ? false : true,
  708. onClick: function(){
  709. if(window.clipboard_op === 'copy')
  710. window.copy_clipboard_items(window.desktop_path, el_desktop);
  711. else if(window.clipboard_op === 'move')
  712. window.move_clipboard_items(el_desktop)
  713. }
  714. },
  715. // -------------------------------------------
  716. // Undo
  717. // -------------------------------------------
  718. {
  719. html: i18n('undo'),
  720. disabled: window.actions_history.length > 0 ? false : true,
  721. onClick: function(){
  722. window.undo_last_action();
  723. }
  724. },
  725. // -------------------------------------------
  726. // Upload Here
  727. // -------------------------------------------
  728. {
  729. html: i18n('upload_here'),
  730. onClick: function(){
  731. window.init_upload_using_dialog(el_desktop);
  732. }
  733. },
  734. // -------------------------------------------
  735. // -
  736. // -------------------------------------------
  737. '-',
  738. // -------------------------------------------
  739. // Change Desktop Background…
  740. // -------------------------------------------
  741. {
  742. html: i18n('change_desktop_background'),
  743. onClick: function(){
  744. UIWindowDesktopBGSettings();
  745. }
  746. },
  747. ]
  748. });
  749. }
  750. });
  751. //-------------------------------------------
  752. // Desktop Files/Folders
  753. // we don't need to get the desktop items if we're in embedded or fullpage mode
  754. // because the items aren't visible anyway and we don't need to waste bandwidth/server resources
  755. //-------------------------------------------
  756. if(!window.is_embedded && !window.is_fullpage_mode){
  757. refresh_item_container(el_desktop, {fadeInItems: true})
  758. }
  759. // -------------------------------------------
  760. // Selectable
  761. // Only for desktop
  762. // -------------------------------------------
  763. if(!isMobile.phone && !isMobile.tablet){
  764. let selected_ctrl_items = [];
  765. const selection = new SelectionArea({
  766. selectionContainerClass: '.selection-area-container',
  767. container: '.desktop',
  768. selectables: ['.desktop.item-container > .item'],
  769. startareas: ['.desktop'],
  770. boundaries: ['.desktop'],
  771. behaviour: {
  772. overlap: 'drop',
  773. intersect: 'touch',
  774. startThreshold: 10,
  775. scrolling: {
  776. speedDivider: 10,
  777. manualSpeed: 750,
  778. startScrollMargins: {x: 0, y: 0}
  779. }
  780. },
  781. features: {
  782. touch: true,
  783. range: true,
  784. singleTap: {
  785. allow: true,
  786. intersect: 'native'
  787. }
  788. }
  789. });
  790. selection.on('beforestart', ({event}) => {
  791. selected_ctrl_items = [];
  792. // Returning false prevents a selection
  793. return $(event.target).hasClass('item-container');
  794. })
  795. .on('beforedrag', evt => {
  796. })
  797. .on('start', ({store, event}) => {
  798. if (!event.ctrlKey && !event.metaKey) {
  799. for (const el of store.stored) {
  800. el.classList.remove('item-selected');
  801. }
  802. selection.clearSelection();
  803. }
  804. })
  805. .on('move', ({store: {changed: {added, removed}}, event}) => {
  806. for (const el of added) {
  807. // if ctrl or meta key is pressed and the item is already selected, then unselect it
  808. if((event.ctrlKey || event.metaKey) && $(el).hasClass('item-selected')){
  809. el.classList.remove('item-selected');
  810. selected_ctrl_items.push(el);
  811. }
  812. // otherwise select it
  813. else{
  814. el.classList.add('item-selected');
  815. }
  816. }
  817. for (const el of removed) {
  818. el.classList.remove('item-selected');
  819. // in case this item was selected by ctrl+click before, then reselect it again
  820. if(selected_ctrl_items.includes(el))
  821. $(el).not('.item-disabled').addClass('item-selected');
  822. }
  823. })
  824. .on('stop', evt => {
  825. });
  826. }
  827. // ----------------------------------------------------
  828. // User options
  829. // ----------------------------------------------------
  830. let ht = '';
  831. ht += `<div class="toolbar" style="height:${window.toolbar_height}px;">`;
  832. // logo
  833. ht += `<div class="toolbar-btn toolbar-puter-logo" title="Puter" style="margin-left: 10px; margin-right: auto;"><img src="${window.icons['logo-white.svg']}" draggable="false" style="display:block; width:17px; height:17px"></div>`;
  834. // create account button
  835. ht += `<div class="toolbar-btn user-options-create-account-btn ${window.user.is_temp ? '' : 'hidden' }" style="padding:0; opacity:1;" title="Save Account">`;
  836. ht += `<svg style="width: 17px; height: 17px;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48"><g transform="translate(0, 0)"><path d="M45.521,39.04L27.527,5.134c-1.021-1.948-3.427-2.699-5.375-1.679-.717,.376-1.303,.961-1.679,1.679L2.479,39.04c-.676,1.264-.635,2.791,.108,4.017,.716,1.207,2.017,1.946,3.42,1.943H41.993c1.403,.003,2.704-.736,3.42-1.943,.743-1.226,.784-2.753,.108-4.017ZM23.032,15h1.937c.565,0,1.017,.467,1,1.031l-.438,14c-.017,.54-.459,.969-1,.969h-1.062c-.54,0-.983-.429-1-.969l-.438-14c-.018-.564,.435-1.031,1-1.031Zm.968,25c-1.657,0-3-1.343-3-3s1.343-3,3-3,3,1.343,3,3-1.343,3-3,3Z" fill="#ffbb00"></path></g></svg>`;
  837. ht += `</div>`;
  838. // 'show desktop'
  839. if(window.is_fullpage_mode){
  840. ht += `<a href="/" class="show-desktop-btn toolbar-btn antialiased" target="_blank" title="Open Desktop">Open Desktop</a>`;
  841. }
  842. // refer
  843. if(window.user.referral_code){
  844. ht += `<div class="toolbar-btn refer-btn" title="Refer" style="background-image:url(${window.icons['gift.svg']});"></div>`;
  845. }
  846. // do not show the fullscreen button on mobile devices since it's broken
  847. if(!isMobile.phone){
  848. // fullscreen button
  849. ht += `<div class="toolbar-btn fullscreen-btn" title="Enter Full Screen" style="background-image:url(${window.icons['fullscreen.svg']})"></div>`;
  850. }
  851. // qr code button -- only show if not embedded
  852. if(!window.is_embedded)
  853. ht += `<div class="toolbar-btn qr-btn" title="QR code" style="background-image:url(${window.icons['qr.svg']})"></div>`;
  854. // user options menu
  855. ht += `<div class="toolbar-btn user-options-menu-btn" style="background-image:url(${window.icons['profile.svg']})">`;
  856. h += `<span class="user-options-menu-username">${window.user.username}</span>`;
  857. ht += `</div>`;
  858. ht += `</div>`;
  859. // prepend toolbar to desktop
  860. $(ht).insertBefore(el_desktop);
  861. // notification container
  862. $('body').append(`<div class="notification-container"><div class="notifications-close-all">Close all</div></div>`);
  863. // adjust window container to take into account the toolbar height
  864. $('.window-container').css('top', window.toolbar_height);
  865. // ---------------------------------------------
  866. // Run apps from insta-login URL
  867. // ---------------------------------------------
  868. if(window.url_query_params.has('app')){
  869. let url_app_name = window.url_query_params.get('app');
  870. if(url_app_name === 'explorer'){
  871. let predefined_path = window.home_path;
  872. if(window.url_query_params.has('path'))
  873. predefined_path =window.url_query_params.get('path')
  874. // launch explorer
  875. UIWindow({
  876. path: predefined_path,
  877. title: path.basename(predefined_path),
  878. icon: await window.item_icon({is_dir: true, path: predefined_path}),
  879. // todo
  880. // uid: $(el_item).attr('data-uid'),
  881. is_dir: true,
  882. // todo
  883. // sort_by: $(el_item).attr('data-sort_by'),
  884. app: 'explorer',
  885. });
  886. }
  887. }
  888. // ---------------------------------------------
  889. // load from direct app URLs: /app/app-name
  890. // ---------------------------------------------
  891. else if(window.app_launched_from_url){
  892. let qparams = new URLSearchParams(window.location.search);
  893. if(!qparams.has('c')){
  894. window.launch_app({
  895. name: window.app_launched_from_url,
  896. readURL: qparams.get('readURL'),
  897. maximized: qparams.get('maximized'),
  898. params: window.app_query_params ?? [],
  899. is_fullpage: window.is_fullpage_mode,
  900. window_options: {
  901. stay_on_top: false,
  902. }
  903. });
  904. }
  905. }
  906. $(el_desktop).on('mousedown touchstart', { passive: true }, function(e){
  907. // dimiss touchstart on regular devices
  908. if(e.type==='taphold' && !isMobile.phone && !isMobile.tablet)
  909. return;
  910. // disable pointer-events for all app iframes, this is to make sure selectable works
  911. $('.window-app-iframe').css('pointer-events', 'none');
  912. $('.window').find('.item-selected').addClass('item-blurred');
  913. $('.desktop').find('.item-blurred').removeClass('item-blurred');
  914. })
  915. $(el_desktop).on('click', function(e){
  916. // blur all windows
  917. $('.window-active').removeClass('window-active');
  918. })
  919. function display_ct() {
  920. var x = new Date()
  921. var ampm = x.getHours( ) >= 12 ? ' PM' : ' AM';
  922. let hours = x.getHours( ) % 12;
  923. hours = hours ? hours : 12;
  924. hours=hours.toString().length==1? 0+hours.toString() : hours;
  925. var minutes=x.getMinutes().toString()
  926. minutes=minutes.length==1 ? 0+minutes : minutes;
  927. var seconds=x.getSeconds().toString()
  928. seconds=seconds.length==1 ? 0+seconds : seconds;
  929. var month=(x.getMonth() +1).toString();
  930. month=month.length==1 ? 0+month : month;
  931. var dt=x.getDate().toString();
  932. dt=dt.length==1 ? 0+dt : dt;
  933. var x1=month + "/" + dt + "/" + x.getFullYear();
  934. x1 = x1 + " - " + hours + ":" + minutes + ":" + seconds + " " + ampm;
  935. $('#clock').html(x1);
  936. $('#clock').css('line-height', window.taskbar_height + 'px');
  937. }
  938. setInterval(display_ct, 1000);
  939. // show referral notice window
  940. if(window.show_referral_notice && !window.user.email_confirmed){
  941. window.getItem({
  942. key: "shown_referral_notice",
  943. success: async function(res){
  944. if(!res){
  945. setTimeout(() => {
  946. UIWindowClaimReferral();
  947. }, 1000);
  948. window.setItem({
  949. key: "shown_referral_notice",
  950. value: true,
  951. })
  952. }
  953. }
  954. })
  955. }
  956. }
  957. $(document).on('contextmenu taphold', '.taskbar', function(event){
  958. // dismiss taphold on regular devices
  959. if(event.type==='taphold' && !isMobile.phone && !isMobile.tablet)
  960. return;
  961. event.preventDefault();
  962. event.stopPropagation();
  963. UIContextMenu({
  964. parent_element: $('.taskbar'),
  965. items: [
  966. //--------------------------------------------------
  967. // Show open windows
  968. //--------------------------------------------------
  969. {
  970. html: "Show open windows",
  971. onClick: function(){
  972. $(`.window`).showWindow();
  973. }
  974. },
  975. //--------------------------------------------------
  976. // Show the desktop
  977. //--------------------------------------------------
  978. {
  979. html: "Show the desktop",
  980. onClick: function(){
  981. $(`.window`).hideWindow();
  982. }
  983. }
  984. ]
  985. });
  986. return false;
  987. })
  988. $(document).on('click', '.qr-btn', async function (e) {
  989. UIWindowQR({
  990. message_i18n_key: 'scan_qr_c2a',
  991. text: window.gui_origin + '?auth_token=' + window.auth_token,
  992. });
  993. })
  994. $(document).on('click', '.user-options-menu-btn', async function(e){
  995. const pos = this.getBoundingClientRect();
  996. if($('.context-menu[data-id="user-options-menu"]').length > 0)
  997. return;
  998. let items = [];
  999. let parent_element = this;
  1000. //--------------------------------------------------
  1001. // Save Session
  1002. //--------------------------------------------------
  1003. if(window.user.is_temp){
  1004. items.push(
  1005. {
  1006. html: i18n('save_session'),
  1007. icon: `<svg style="margin-bottom: -4px; width: 16px; height: 16px;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48"><g transform="translate(0, 0)"><path d="M45.521,39.04L27.527,5.134c-1.021-1.948-3.427-2.699-5.375-1.679-.717,.376-1.303,.961-1.679,1.679L2.479,39.04c-.676,1.264-.635,2.791,.108,4.017,.716,1.207,2.017,1.946,3.42,1.943H41.993c1.403,.003,2.704-.736,3.42-1.943,.743-1.226,.784-2.753,.108-4.017ZM23.032,15h1.937c.565,0,1.017,.467,1,1.031l-.438,14c-.017,.54-.459,.969-1,.969h-1.062c-.54,0-.983-.429-1-.969l-.438-14c-.018-.564,.435-1.031,1-1.031Zm.968,25c-1.657,0-3-1.343-3-3s1.343-3,3-3,3,1.343,3,3-1.343,3-3,3Z" fill="#ffbb00"></path></g></svg>`,
  1008. icon_active: `<svg style="margin-bottom: -4px; width: 16px; height: 16px;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48"><g transform="translate(0, 0)"><path d="M45.521,39.04L27.527,5.134c-1.021-1.948-3.427-2.699-5.375-1.679-.717,.376-1.303,.961-1.679,1.679L2.479,39.04c-.676,1.264-.635,2.791,.108,4.017,.716,1.207,2.017,1.946,3.42,1.943H41.993c1.403,.003,2.704-.736,3.42-1.943,.743-1.226,.784-2.753,.108-4.017ZM23.032,15h1.937c.565,0,1.017,.467,1,1.031l-.438,14c-.017,.54-.459,.969-1,.969h-1.062c-.54,0-.983-.429-1-.969l-.438-14c-.018-.564,.435-1.031,1-1.031Zm.968,25c-1.657,0-3-1.343-3-3s1.343-3,3-3,3,1.343,3,3-1.343,3-3,3Z" fill="#ffbb00"></path></g></svg>`,
  1009. onClick: async function(){
  1010. UIWindowSaveAccount({
  1011. send_confirmation_code: false,
  1012. default_username: window.user.username
  1013. });
  1014. }
  1015. },
  1016. )
  1017. // -------------------------------------------
  1018. // -
  1019. // -------------------------------------------
  1020. items.push('-')
  1021. }
  1022. // -------------------------------------------
  1023. // Logged in users
  1024. // -------------------------------------------
  1025. if(window.logged_in_users.length > 0){
  1026. let users_arr = window.logged_in_users;
  1027. // bring logged in user's item to top
  1028. users_arr.sort(function(x,y){ return x.uuid === window.user.uuid ? -1 : y.uuid == window.user.uuid ? 1 : 0; });
  1029. // create menu items
  1030. users_arr.forEach(l_user => {
  1031. items.push(
  1032. {
  1033. html: l_user.username,
  1034. icon: l_user.username === window.user.username ? '✓' : '',
  1035. onClick: async function(val){
  1036. // don't reload everything if clicked on already-logged-in user
  1037. if(l_user.username === window.user.username)
  1038. return;
  1039. // update auth data
  1040. window.update_auth_data(l_user.auth_token, l_user);
  1041. // refresh
  1042. location.reload();
  1043. }
  1044. },
  1045. )
  1046. });
  1047. // -------------------------------------------
  1048. // -
  1049. // -------------------------------------------
  1050. items.push('-')
  1051. items.push(
  1052. {
  1053. html: i18n('add_existing_account'),
  1054. // icon: l_user.username === user.username ? '✓' : '',
  1055. onClick: async function(val){
  1056. await UIWindowLogin({
  1057. reload_on_success: true,
  1058. send_confirmation_code: false,
  1059. window_options:{
  1060. has_head: true
  1061. }
  1062. });
  1063. }
  1064. },
  1065. )
  1066. // -------------------------------------------
  1067. // -
  1068. // -------------------------------------------
  1069. items.push('-')
  1070. }
  1071. // -------------------------------------------
  1072. // Load available languages
  1073. // -------------------------------------------
  1074. const supportedLanguagesItems = window.listSupportedLanguages().map(lang => {
  1075. return {
  1076. html: lang.name,
  1077. icon: window.locale === lang.code ? '✓' : '',
  1078. onClick: async function(){
  1079. changeLanguage(lang.code);
  1080. }
  1081. }
  1082. });
  1083. UIContextMenu({
  1084. id: 'user-options-menu',
  1085. parent_element: parent_element,
  1086. position: {top: pos.top + 28, left: pos.left + pos.width - 15},
  1087. items: [
  1088. ...items,
  1089. //--------------------------------------------------
  1090. // My Websites
  1091. //--------------------------------------------------
  1092. {
  1093. html: i18n('my_websites'),
  1094. onClick: async function(){
  1095. UIWindowMyWebsites();
  1096. }
  1097. },
  1098. //--------------------------------------------------
  1099. // Settings
  1100. //--------------------------------------------------
  1101. {
  1102. html: i18n('settings'),
  1103. onClick: async function(){
  1104. UIWindowSettings();
  1105. }
  1106. },
  1107. //--------------------------------------------------
  1108. // Task Manager
  1109. //--------------------------------------------------
  1110. {
  1111. html: i18n('task_manager'),
  1112. onClick: async function(){
  1113. UIWindowTaskManager();
  1114. }
  1115. },
  1116. //--------------------------------------------------
  1117. // Contact Us
  1118. //--------------------------------------------------
  1119. {
  1120. html: i18n('contact_us'),
  1121. onClick: async function(){
  1122. UIWindowFeedback();
  1123. }
  1124. },
  1125. // -------------------------------------------
  1126. // -
  1127. // -------------------------------------------
  1128. '-',
  1129. //--------------------------------------------------
  1130. // Log Out
  1131. //--------------------------------------------------
  1132. {
  1133. html: i18n('log_out'),
  1134. onClick: async function(){
  1135. // see if there are any open windows, if yes notify user
  1136. if($('.window-app').length > 0){
  1137. const alert_resp = await UIAlert({
  1138. message: `<p>${i18n('confirm_open_apps_log_out')}</p>`,
  1139. buttons:[
  1140. {
  1141. label: i18n('close_all_windows_and_log_out'),
  1142. value: 'close_and_log_out',
  1143. type: 'primary',
  1144. },
  1145. {
  1146. label: i18n('cancel')
  1147. },
  1148. ]
  1149. })
  1150. if(alert_resp === 'close_and_log_out')
  1151. window.logout();
  1152. }
  1153. // no open windows
  1154. else
  1155. window.logout();
  1156. }
  1157. },
  1158. ]
  1159. });
  1160. })
  1161. $(document).on('click', '.fullscreen-btn', async function (e) {
  1162. if(!window.is_fullscreen()) {
  1163. var elem = document.documentElement;
  1164. if (elem.requestFullscreen) {
  1165. elem.requestFullscreen();
  1166. } else if (elem.webkitRequestFullscreen) { /* Safari */
  1167. elem.webkitRequestFullscreen();
  1168. } else if (elem.mozRequestFullScreen) { /* moz */
  1169. elem.mozRequestFullScreen();
  1170. } else if (elem.msRequestFullscreen) { /* IE11 */
  1171. elem.msRequestFullscreen();
  1172. }
  1173. }
  1174. else{
  1175. if (document.exitFullscreen) {
  1176. document.exitFullscreen();
  1177. } else if (document.webkitExitFullscreen) {
  1178. document.webkitExitFullscreen();
  1179. } else if (document.mozCancelFullScreen) {
  1180. document.mozCancelFullScreen();
  1181. } else if (document.msExitFullscreen) {
  1182. document.msExitFullscreen();
  1183. }
  1184. }
  1185. })
  1186. $(document).on('click', '.close-launch-popover', function(){
  1187. $(".launch-popover").closest('.popover').fadeOut(200, function(){
  1188. $(".launch-popover").closest('.popover').remove();
  1189. });
  1190. });
  1191. $(document).on('click', '.toolbar-puter-logo', function(){
  1192. UIWindowSettings();
  1193. })
  1194. $(document).on('click', '.user-options-create-account-btn', async function(e){
  1195. UIWindowSaveAccount({
  1196. send_confirmation_code: false,
  1197. default_username: window.user.username,
  1198. });
  1199. })
  1200. $(document).on('click', '.refer-btn', async function(e){
  1201. UIWindowRefer();
  1202. })
  1203. $(document).on('click', '.start-app', async function(e){
  1204. window.launch_app({
  1205. name: $(this).attr('data-app-name')
  1206. })
  1207. // close popovers
  1208. $(".popover").fadeOut(200, function(){
  1209. $(".popover").remove();
  1210. });
  1211. })
  1212. $(document).on('click', '.user-options-login-btn', async function(e){
  1213. const alert_resp = await UIAlert({
  1214. message: `<strong>Save session before exiting!</strong><p>You are in a temporary session and logging into another account will erase all data in your current session.</p>`,
  1215. buttons:[
  1216. {
  1217. label: i18n('save_session'),
  1218. value: 'save-session',
  1219. type: 'primary',
  1220. },
  1221. {
  1222. label: i18n('log_into_another_account_anyway'),
  1223. value: 'login',
  1224. },
  1225. {
  1226. label: i18n('cancel')
  1227. },
  1228. ]
  1229. })
  1230. if(alert_resp === 'save-session'){
  1231. let saved = await UIWindowSaveAccount({
  1232. send_confirmation_code: false,
  1233. });
  1234. if(saved)
  1235. UIWindowLogin({show_signup_button: false, reload_on_success: true});
  1236. }else if (alert_resp === 'login'){
  1237. UIWindowLogin({
  1238. show_signup_button: false,
  1239. reload_on_success: true,
  1240. window_options: {
  1241. backdrop: true,
  1242. close_on_backdrop_click: false,
  1243. }
  1244. });
  1245. }
  1246. })
  1247. $(document).on('click mousedown', '.launch-search, .launch-popover', function(e){
  1248. $(this).focus();
  1249. e.stopPropagation();
  1250. e.preventDefault();
  1251. // don't let click bubble up to window
  1252. e.stopImmediatePropagation();
  1253. })
  1254. $(document).on('focus', '.launch-search', function(e){
  1255. // remove all selected items in start menu
  1256. $('.launch-app-selected').removeClass('launch-app-selected');
  1257. // scroll popover to top
  1258. $('.launch-popover').scrollTop(0);
  1259. })
  1260. $(document).on('change keyup keypress keydown paste', '.launch-search', function(e){
  1261. // search window.launch_apps.recommended for query
  1262. const query = $(this).val().toLowerCase();
  1263. if(query === ''){
  1264. $('.launch-search-clear').hide();
  1265. $(`.start-app-card`).show();
  1266. $('.launch-apps-recent').show();
  1267. $('.start-section-heading').show();
  1268. }else{
  1269. $('.launch-apps-recent').hide();
  1270. $('.start-section-heading').hide();
  1271. $('.launch-search-clear').show();
  1272. window.launch_apps.recommended.forEach((app)=>{
  1273. if(app.title.toLowerCase().includes(query.toLowerCase())){
  1274. $(`.start-app-card[data-name="${app.name}"]`).show();
  1275. }else{
  1276. $(`.start-app-card[data-name="${app.name}"]`).hide();
  1277. }
  1278. })
  1279. }
  1280. })
  1281. $(document).on('click', '.launch-search-clear', function(e){
  1282. $('.launch-search').val('');
  1283. $('.launch-search').trigger('change');
  1284. $('.launch-search').focus();
  1285. })
  1286. document.addEventListener('fullscreenchange', (event) => {
  1287. // document.fullscreenElement will point to the element that
  1288. // is in fullscreen mode if there is one. If there isn't one,
  1289. // the value of the property is null.
  1290. if (document.fullscreenElement) {
  1291. $('.fullscreen-btn').css('background-image', `url(${window.icons['shrink.svg']})`);
  1292. $('.fullscreen-btn').attr('title', 'Exit Full Screen');
  1293. window.user_preferences.clock_visible === 'auto' && $('#clock').show();
  1294. } else {
  1295. $('.fullscreen-btn').css('background-image', `url(${window.icons['fullscreen.svg']})`);
  1296. $('.fullscreen-btn').attr('title', 'Enter Full Screen');
  1297. window.user_preferences.clock_visible === 'auto' && $('#clock').hide();
  1298. }
  1299. })
  1300. window.set_desktop_background = function(options){
  1301. if(options.fit){
  1302. let fit = options.fit;
  1303. if(fit === 'cover' || fit === 'contain'){
  1304. $('body').css('background-size', fit);
  1305. $('body').css('background-repeat', `no-repeat`);
  1306. $('body').css('background-position', `center center`);
  1307. }
  1308. else if(fit === 'center'){
  1309. $('body').css('background-size', 'auto');
  1310. $('body').css('background-repeat', `no-repeat`);
  1311. $('body').css('background-position', `center center`);
  1312. }
  1313. else if( fit === 'repeat'){
  1314. $('body').css('background-size', `auto`);
  1315. $('body').css('background-repeat', `repeat`);
  1316. }
  1317. window.desktop_bg_fit = fit;
  1318. }
  1319. if(options.url){
  1320. $('body').css('background-image', `url(${options.url})`);
  1321. window.desktop_bg_url = options.url;
  1322. window.desktop_bg_color = undefined;
  1323. }
  1324. else if(options.color){
  1325. $('body').css({
  1326. 'background-image': `none`,
  1327. 'background-color': options.color,
  1328. });
  1329. window.desktop_bg_color = options.color;
  1330. window.desktop_bg_url = undefined;
  1331. }
  1332. }
  1333. window.update_taskbar = function(){
  1334. let items = []
  1335. $('.taskbar-item-sortable[data-keep-in-taskbar="true"]').each(function(index){
  1336. items.push({
  1337. name: $( this ).attr('data-app'),
  1338. type: 'app',
  1339. })
  1340. })
  1341. // update taskbar in the server-side
  1342. $.ajax({
  1343. url: window.api_origin + "/update-taskbar-items",
  1344. type: 'POST',
  1345. data: JSON.stringify({
  1346. items: items,
  1347. }),
  1348. async: true,
  1349. contentType: "application/json",
  1350. headers: {
  1351. "Authorization": "Bearer "+window.auth_token
  1352. },
  1353. })
  1354. }
  1355. window.remove_taskbar_item = function(item){
  1356. $(item).find('*').fadeOut(100, function(){});
  1357. $(item).animate({width: 0}, 200, function(){
  1358. $(item).remove();
  1359. })
  1360. }
  1361. window.enter_fullpage_mode = (el_window)=>{
  1362. $('.taskbar').hide();
  1363. $(el_window).find('.window-head').hide();
  1364. $('body').addClass('fullpage-mode');
  1365. $(el_window).css({
  1366. width: '100%',
  1367. height: '100%',
  1368. top: window.toolbar_height + 'px',
  1369. left: 0,
  1370. 'border-radius': 0,
  1371. });
  1372. }
  1373. window.exit_fullpage_mode = (el_window)=>{
  1374. $('body').removeClass('fullpage-mode');
  1375. window.taskbar_height = window.default_taskbar_height;
  1376. $('.taskbar').css('height', window.taskbar_height);
  1377. $('.taskbar').show();
  1378. refresh_item_container($('.desktop.item-container'), {fadeInItems: true});
  1379. $(el_window).removeAttr('data-is_fullpage');
  1380. if(el_window){
  1381. window.reset_window_size_and_position(el_window)
  1382. $(el_window).find('.window-head').show();
  1383. }
  1384. // reset dektop height to take into account the taskbar height
  1385. $('.desktop').css('height', `calc(100vh - ${window.taskbar_height + window.toolbar_height}px)`);
  1386. // hide the 'Show Desktop' button in toolbar
  1387. $('.show-desktop-btn').hide();
  1388. // refresh desktop background
  1389. window.refresh_desktop_background();
  1390. }
  1391. window.reset_window_size_and_position = (el_window)=>{
  1392. $(el_window).css({
  1393. width: 680,
  1394. height: 380,
  1395. 'border-radius': window.window_border_radius,
  1396. top: 'calc(50% - 190px)',
  1397. left: 'calc(50% - 340px)',
  1398. });
  1399. }
  1400. export default UIDesktop;