UIDesktop.js 62 KB

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