1
0

UIItem.js 70 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580
  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 UIWindowPublishWebsite from './UIWindowPublishWebsite.js';
  20. import UIWindowItemProperties from './UIWindowItemProperties.js';
  21. import UIWindowGetCopyLink from './UIWindowGetCopyLink.js';
  22. import UIWindowSaveAccount from './UIWindowSaveAccount.js';
  23. import UIPopover from './UIPopover.js';
  24. import UIWindowEmailConfirmationRequired from './UIWindowEmailConfirmationRequired.js';
  25. import UIContextMenu from './UIContextMenu.js'
  26. import UIAlert from './UIAlert.js'
  27. import path from "../lib/path.js"
  28. function UIItem(options){
  29. const matching_appendto_count = $(options.appendTo).length;
  30. if(matching_appendto_count > 1){
  31. $(options.appendTo).each(function(){
  32. const opts = options;
  33. opts.appendTo = this;
  34. UIItem(opts);
  35. })
  36. return;
  37. }else if(matching_appendto_count === 0){
  38. return;
  39. }
  40. const item_id = global_element_id++;
  41. let last_mousedown_ts = 999999999999999;
  42. let rename_cancelled = false;
  43. // set options defaults
  44. options.disabled = options.disabled ?? false;
  45. options.visible = options.visible ?? 'visible'; // one of 'visible', 'revealed', 'hidden'
  46. options.is_dir = options.is_dir ?? false;
  47. options.is_selected = options.is_selected ?? false;
  48. options.is_shared = options.is_shared ?? false;
  49. options.is_shortcut = options.is_shortcut ?? 0;
  50. options.is_trash = options.is_trash ?? false;
  51. options.metadata = options.metadata ?? '';
  52. options.multiselectable = options.multiselectable ?? true;
  53. options.shortcut_to = options.shortcut_to ?? '';
  54. options.shortcut_to_path = options.shortcut_to_path ?? '';
  55. options.immutable = (options.immutable === false || options.immutable === 0 || options.immutable === undefined ? 0 : 1);
  56. options.sort_container_after_append = (options.sort_container_after_append !== undefined ? options.sort_container_after_append : false);
  57. const is_shared_with_me = (options.path !== '/'+window.user.username && !options.path.startsWith('/'+window.user.username+'/'));
  58. let website_url = determine_website_url(options.path);
  59. // do a quick check to see if the target parent has any file type restrictions
  60. const appendto_allowed_file_types = $(options.appendTo).attr('data-allowed_file_types')
  61. if(!window.check_fsentry_against_allowed_file_types_string({is_dir: options.is_dir, name:options.name, type:options.type}, appendto_allowed_file_types))
  62. options.disabled = true;
  63. // --------------------------------------------------------
  64. // HTML for Item
  65. // --------------------------------------------------------
  66. let h = '';
  67. h += `<div id="item-${item_id}"
  68. class="item${options.is_selected ? ' item-selected':''} ${options.disabled ? 'item-disabled':''} item-${options.visible}"
  69. data-id="${item_id}"
  70. data-name="${html_encode(options.name)}"
  71. data-metadata="${html_encode(options.metadata)}"
  72. data-uid="${options.uid}"
  73. data-is_dir="${options.is_dir ? 1 : 0}"
  74. data-is_trash="${options.is_trash ? 1 : 0}"
  75. data-has_website="${options.has_website ? 1 : 0 }"
  76. data-website_url = "${website_url ? html_encode(website_url) : ''}"
  77. data-immutable="${options.immutable}"
  78. data-is_shortcut = "${options.is_shortcut}"
  79. data-shortcut_to = "${html_encode(options.shortcut_to)}"
  80. data-shortcut_to_path = "${html_encode(options.shortcut_to_path)}"
  81. data-sortable = "${options.sortable ?? 'true'}"
  82. data-sort_by = "${html_encode(options.sort_by) ?? 'name'}"
  83. data-size = "${options.size ?? ''}"
  84. data-type = "${html_encode(options.type) ?? ''}"
  85. data-modified = "${options.modified ?? ''}"
  86. data-associated_app_name = "${html_encode(options.associated_app_name) ?? ''}"
  87. data-path="${html_encode(options.path)}">`;
  88. // spinner
  89. h += `<div class="item-spinner">`;
  90. h += `</div>`;
  91. // modified
  92. h += `<div class="item-attr item-attr--modified">`;
  93. h += `<span>${options.modified === 0 ? '-' : timeago.format(options.modified*1000)}</span>`;
  94. h += `</div>`;
  95. // size
  96. h += `<div class="item-attr item-attr--size">`;
  97. h += `<span>${options.size ? byte_format(options.size) : '-'}</span>`;
  98. h += `</div>`;
  99. // type
  100. h += `<div class="item-attr item-attr--type">`;
  101. if(options.is_dir)
  102. h += `<span>Folder</span>`;
  103. else
  104. h += `<span>${options.type ? html_encode(options.type) : '-'}</span>`;
  105. h += `</div>`;
  106. // icon
  107. h += `<div class="item-icon">`;
  108. h += `<img src="${html_encode(options.icon.image)}" class="item-icon-${options.icon.type}" data-item-id="${item_id}">`;
  109. h += `</div>`;
  110. // badges
  111. h += `<div class="item-badges">`;
  112. // website badge
  113. h += `<img class="item-badge item-has-website-badge long-hover"
  114. style="${options.has_website ? 'display:block;' : ''}"
  115. src="${html_encode(window.icons['world.svg'])}"
  116. data-item-id="${item_id}"
  117. >`;
  118. // link badge
  119. h += `<img class="item-badge item-has-website-url-badge"
  120. style="${website_url ? 'display:block;' : ''}"
  121. src="${html_encode(window.icons['link.svg'])}"
  122. data-item-id="${item_id}"
  123. >`;
  124. // shared badge
  125. h += `<img class="item-badge item-badge-has-permission"
  126. style="display: ${ is_shared_with_me ? 'block' : 'none'};
  127. background-color: #ffffff;
  128. padding: 2px;" src="${html_encode(window.icons['shared.svg'])}"
  129. data-item-id="${item_id}"
  130. title="A user has shared this item with you.">`;
  131. // owner-shared badge
  132. h += `<img class="item-badge item-is-shared"
  133. style="background-color: #ffffff; padding: 2px; ${!is_shared_with_me && options.is_shared ? 'display:block;' : ''}"
  134. src="${html_encode(window.icons['owner-shared.svg'])}"
  135. data-item-id="${item_id}"
  136. data-item-uid="${options.uid}"
  137. data-item-path="${html_encode(options.path)}"
  138. title="You have shared this item with at least one other user."
  139. >`;
  140. // shortcut badge
  141. h += `<img class="item-badge item-shortcut"
  142. style="background-color: #ffffff; padding: 2px; ${options.is_shortcut !== 0 ? 'display:block;' : ''}"
  143. src="${html_encode(window.icons['shortcut.svg'])}"
  144. data-item-id="${item_id}"
  145. title="Shortcut"
  146. >`;
  147. h += `</div>`;
  148. // name
  149. h += `<span class="item-name" data-item-id="${item_id}" title="${html_encode(options.name)}">${options.is_trash ? i18n('trash') : html_encode(truncate_filename(options.name, TRUNCATE_LENGTH)).replaceAll(' ', '&nbsp;')}</span>`
  150. // name editor
  151. h += `<textarea class="item-name-editor hide-scrollbar" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off" data-gramm_editor="false">${html_encode(options.name)}</textarea>`
  152. h += `</div>`;
  153. // append to options.appendTo
  154. $(options.appendTo).append(h);
  155. // updte item_container
  156. const item_container = $(options.appendTo).closest('.item-container');
  157. show_or_hide_empty_folder_message(item_container);
  158. // get all the elements needed
  159. const el_item = document.getElementById(`item-${item_id}`);
  160. const el_item_name = document.querySelector(`#item-${item_id} > .item-name`);
  161. const el_item_icon = document.querySelector(`#item-${item_id} .item-icon`);
  162. const el_item_name_editor = document.querySelector(`#item-${item_id} > .item-name-editor`);
  163. const is_trashed = $(el_item).attr('data-path').startsWith(trash_path + '/');
  164. // update parent window's explorer item count if applicable
  165. if(options.appendTo !== undefined){
  166. let el_window = options.appendTo;
  167. if(!$(el_window).hasClass('.window'))
  168. el_window = $(el_window).closest('.window');
  169. update_explorer_footer_item_count(el_window);
  170. }
  171. // position
  172. if(!is_auto_arrange_enabled && options.position && $(el_item).closest('.item-container').attr('data-path') === window.desktop_path){
  173. el_item.style.position = 'absolute';
  174. el_item.style.left = options.position.left + 'px';
  175. el_item.style.top = options.position.top + 'px';
  176. }
  177. // --------------------------------------------------------
  178. // Dragster
  179. // allow dragging of local files on this window, if it's is_dir
  180. // --------------------------------------------------------
  181. if(options.is_dir){
  182. $(el_item).dragster({
  183. enter: function () {
  184. $(el_item).not('.item-disabled').addClass('item-selected');
  185. },
  186. leave: function () {
  187. $(el_item).removeClass('item-selected');
  188. },
  189. drop: function (dragsterEvent, event) {
  190. const e = event.originalEvent;
  191. $(el_item).removeClass('item-selected');
  192. // if files were dropped...
  193. if(e.dataTransfer?.items?.length > 0){
  194. upload_items( e.dataTransfer.items, $(el_item).attr('data-path'))
  195. }
  196. e.stopPropagation();
  197. e.preventDefault();
  198. return false;
  199. }
  200. });
  201. }
  202. // --------------------------------------------------------
  203. // Draggable
  204. // --------------------------------------------------------
  205. let longer_hover_timeout;
  206. let last_window_dragged_over;
  207. $(el_item).draggable({
  208. appendTo: "body",
  209. helper: "clone",
  210. revert: "invalid",
  211. //containment: "document",
  212. zIndex: 10000,
  213. scroll:false,
  214. distance: 5,
  215. revertDuration: 100,
  216. start: function(event, ui) {
  217. // select this item and its helper
  218. $(el_item).addClass('item-selected');
  219. $('.ui-draggable-dragging').addClass('item-selected');
  220. //clone other selected items
  221. $(el_item)
  222. .siblings('.item-selected')
  223. .clone()
  224. .addClass('item-selected-clone')
  225. .css('position', 'absolute')
  226. .appendTo('body')
  227. .hide();
  228. // Bring item and clones to front
  229. $('.item-selected-clone, .ui-draggable-dragging').css('z-index', 99999);
  230. // count badge
  231. const item_count = $('.item-selected-clone').length;
  232. if(item_count > 0){
  233. $('body').append(`<span class="draggable-count-badge">${item_count + 1}</span>`);
  234. }
  235. // Disable all droppable UIItems that are not a dir/app to avoid accidental cancellation
  236. // on Items that are not droppables. In general if an item is dropped on another, if the
  237. // target is not a dir, the source needs to be dropped on the target's container.
  238. $(`.item[data-is_dir="0"][data-associated_app_name=""]:not(.item-selected)`).droppable('disable');
  239. // Disable pointer events on all app iframes. This is needed because as soon as
  240. // a dragging event enters the iframe the event is delegated to iframe which makes the item
  241. // stuck at the edge of the iframe not allowing us to move items freely across the screen
  242. $('.window-app-iframe').css('pointer-events', 'none')
  243. // reset longer hover timeout and last window dragged over
  244. longer_hover_timeout = null;
  245. last_window_dragged_over = null;
  246. },
  247. drag: function(event, ui) {
  248. // Only show drag helpers if the item has been moved more than 5px
  249. if( Math.abs(ui.originalPosition.top - ui.offset.top) > 5
  250. ||
  251. Math.abs(ui.originalPosition.left - ui.offset.left) > 5 ){
  252. $('.ui-draggable-dragging').show();
  253. $('.item-selected-clone').show();
  254. $('.draggable-count-badge').show();
  255. }
  256. const other_selected_items = $('.item-selected-clone');
  257. const item_count = other_selected_items.length + 1;
  258. // Move count badge with mouse
  259. $('.draggable-count-badge').css({
  260. top: event.pageY,
  261. left: event.pageX + 10,
  262. })
  263. // Move other selected items
  264. for(let i=0; i < item_count - 1; i++){
  265. $(other_selected_items[i]).css({
  266. 'left': ui.position.left + 3 * (i+1),
  267. 'top': ui.position.top + 3 * (i+1),
  268. 'z-index': 999 - (i),
  269. 'opacity': 0.5 - i*0.1,
  270. })
  271. }
  272. // remove all item-container active borders
  273. $('.item-container').removeClass('item-container-active');
  274. // if item has changed container, remove timeout for window focus and reset last target
  275. if(longer_hover_timeout && last_window_dragged_over !== window.mouseover_window){
  276. clearTimeout(longer_hover_timeout);
  277. longer_hover_timeout = null;
  278. last_window_dragged_over = window.mouseover_window;
  279. }
  280. // if item hover for more than 1.2s, focus the window
  281. if(!longer_hover_timeout){
  282. longer_hover_timeout = setTimeout(() => {
  283. $(last_window_dragged_over).focusWindow();
  284. }, 1200);
  285. }
  286. // Highlight item container to help user see more clearly where the item is going to be dropped
  287. if($(window.mouseover_item_container).closest('.window').is(window.mouseover_window) &&
  288. // do not highlight if the target is the same as the item being moved
  289. $(el_item).attr('data-path') !== $(window.mouseover_item_container).attr('data-path') &&
  290. // do not highlight if item is being moved to where it already is
  291. $(el_item).attr('data-path') !== $(window.mouseover_item_container).attr('data-path')){
  292. // highlight item container
  293. $(window.mouseover_item_container).addClass('item-container-active');
  294. }
  295. // send drag event to iframe if mouse is inside iframe
  296. if(mouseover_window){
  297. const $app_iframe = $(mouseover_window).find('.window-app-iframe');
  298. if(!$(mouseover_window).hasClass('window-disabled') && $app_iframe.length > 0){
  299. var rect = $app_iframe.get(0).getBoundingClientRect();
  300. // if mouse is inside iframe, send drag message to iframe
  301. if(mouseX > rect.left && mouseX < rect.right && mouseY > rect.top && mouseY < rect.bottom){
  302. $app_iframe.get(0).contentWindow.postMessage({msg: "drag", x: (mouseX - rect.left), y: (mouseY - rect.top)}, '*');
  303. }
  304. }
  305. }
  306. },
  307. stop: function(event, ui){
  308. // Allow rearranging only if item is on desktop, not trash container, auto arrange is disabled and item is not dropped into another item
  309. if($(el_item).closest('.item-container').attr('data-path') === window.desktop_path &&
  310. !is_auto_arrange_enabled && $(el_item).attr('data-path') !== trash_path && !ui.helper.data('dropped') &&
  311. // Item must be dropped on the Desktop and not on the taskbar
  312. mouseover_window === undefined && ui.position.top <= window.desktop_height - window.taskbar_height - 15){
  313. el_item.style.position = 'absolute';
  314. el_item.style.left = ui.position.left + 'px';
  315. el_item.style.top = ui.position.top + 'px';
  316. $('.ui-draggable-dragging').remove();
  317. desktop_item_positions[$(el_item).attr('data-uid')] = ui.position;
  318. save_desktop_item_positions()
  319. }
  320. $('.item-selected-clone').remove();
  321. $('.draggable-count-badge').remove();
  322. // re-enable all droppable UIItems that are not a dir
  323. $(`.item[data-is_dir='0']:not(.item-selected)`).droppable('enable');
  324. // remove active item-container border highlights
  325. $('.item-container').removeClass('item-container-active');
  326. // reset longer hover timeout and last window dragged over
  327. clearTimeout(longer_hover_timeout);
  328. last_window_dragged_over = null;
  329. }
  330. });
  331. // --------------------------------------------------------
  332. // Droppable
  333. // --------------------------------------------------------
  334. $(el_item).droppable({
  335. accept: '.item',
  336. // 'pointer' is very important because of active window tracking is based on the position of cursor.
  337. tolerance: 'pointer',
  338. drop: async function( event, ui ) {
  339. // Check if hovering over an item that is VISIBILE
  340. if($(event.target).closest('.window').attr('data-id') !== $(mouseover_window).attr('data-id'))
  341. return;
  342. // If ctrl is pressed and source is Trashed, cancel whole operation
  343. if(event.ctrlKey && path.dirname($(ui.draggable).attr('data-path')) === window.trash_path)
  344. return;
  345. // Adding a flag to know whether item is rearraged or dropped
  346. ui.helper.data('dropped', true);
  347. const items_to_move = []
  348. // First item
  349. items_to_move.push(ui.draggable);
  350. // All subsequent items
  351. const cloned_items = document.getElementsByClassName('item-selected-clone');
  352. for(let i =0; i<cloned_items.length; i++){
  353. const source_item = document.getElementById('item-' + $(cloned_items[i]).attr('data-id'));
  354. if(source_item !== null)
  355. items_to_move.push(source_item);
  356. }
  357. // --------------------------------------------------------
  358. // If dropped on an app, open the app with the dropped
  359. // items as argument
  360. //--------------------------------------------------------
  361. if(options.associated_app_name){
  362. // an array that hold the items to sign
  363. const items_to_open = [];
  364. // prepare items to sign
  365. for(let i=0; i < items_to_move.length; i++){
  366. items_to_open.push({
  367. name: $(items_to_move[i]).attr('data-name'),
  368. uid: $(items_to_move[i]).attr('data-uid'),
  369. action: 'write',
  370. path: $(items_to_move[i]).attr('data-path')
  371. });
  372. }
  373. // open each item
  374. for (let i = 0; i < items_to_open.length; i++) {
  375. const item = items_to_open[i];
  376. launch_app({
  377. name: options.associated_app_name,
  378. file_path: item.path,
  379. // app_obj: open_item_meta.suggested_apps[0],
  380. window_title: item.name,
  381. file_uid: item.uid,
  382. file_signature: item,
  383. });
  384. }
  385. // deselect dragged item
  386. for(let i=0; i < items_to_move.length; i++)
  387. $(items_to_move[i]).removeClass('item-selected');
  388. }
  389. //--------------------------------------------------------
  390. // If dropped on a directory, move items to that directory
  391. //--------------------------------------------------------
  392. else{
  393. // If ctrl key is down, copy items. Except if target or source is Trash
  394. if(event.ctrlKey){
  395. if(options.is_dir && $(el_item).attr('data-path') !== window.trash_path )
  396. copy_items(items_to_move, $(el_item).attr('data-path'))
  397. else if(!options.is_dir)
  398. copy_items(items_to_move, path.dirname($(el_item).attr('data-path')));
  399. }
  400. // If alt key is down, create shortcut items
  401. else if(event.altKey && window.feature_flags.create_shortcut){
  402. items_to_move.forEach((item_to_move) => {
  403. create_shortcut(
  404. path.basename($(item_to_move).attr('data-path')),
  405. $(item_to_move).attr('data-is_dir') === '1',
  406. options.is_dir ? $(el_item).attr('data-path') : path.dirname($(el_item).attr('data-path')),
  407. null,
  408. $(item_to_move).attr('data-shortcut_to') === '' ? $(item_to_move).attr('data-uid') : $(item_to_move).attr('data-shortcut_to'),
  409. $(item_to_move).attr('data-shortcut_to_path') === '' ? $(item_to_move).attr('data-path') : $(item_to_move).attr('data-shortcut_to_path'),
  410. );
  411. });
  412. }
  413. // Otherwise, move items
  414. else if(options.is_dir){
  415. if($(el_item).closest('.item-container').attr('data-path') === window.desktop_path){
  416. delete desktop_item_positions[$(el_item).attr('data-uid')];
  417. save_desktop_item_positions()
  418. }
  419. move_items(items_to_move, $(el_item).attr('data-shortcut_to_path') !== '' ? $(el_item).attr('data-shortcut_to_path') : $(el_item).attr('data-path'));
  420. }
  421. }
  422. // Re-enable droppable on all 'item-container's
  423. $('.item-container').droppable('enable')
  424. return false;
  425. },
  426. over: function(event, ui){
  427. // Check hovering over an item that is VISIBILE
  428. const $event_parent_win = $(event.target).closest('.window')
  429. if( $event_parent_win.length > 0 && $event_parent_win.attr('data-id') !== $(mouseover_window).attr('data-id'))
  430. return;
  431. // Don't do anything if the dragged item is NOT a UIItem
  432. if(!$(ui.draggable).hasClass('item'))
  433. return;
  434. // If this is a directory or an app, and an item was dragged over it, highlight it.
  435. if(options.is_dir || options.associated_app_name){
  436. $(el_item).addClass('item-selected');
  437. $('.ui-draggable-dragging .item-name, .item-selected-clone .item-name').css('opacity', 0.1)
  438. // remove all item-container active borders
  439. $('.item-container').addClass('item-container-transparent-border')
  440. }
  441. // Disable all window bodies
  442. $('.item-container').droppable( 'disable' )
  443. },
  444. out: function(event, ui){
  445. // Don't do anything if the dragged item is NOT a UIItem
  446. if(!$(ui.draggable).hasClass('item'))
  447. return;
  448. // Unselect directory/app if item is dragged out
  449. if(options.is_dir || options.associated_app_name){
  450. $(el_item).removeClass('item-selected');
  451. $('.ui-draggable-dragging .item-name, .item-selected-clone .item-name').css('opacity', 'initial')
  452. $('.item-container').removeClass('item-container-transparent-border')
  453. }
  454. $('.item-container').droppable( 'enable' )
  455. }
  456. });
  457. // --------------------------------------------------------
  458. // Double Click/Single Tap on Item
  459. // --------------------------------------------------------
  460. if(isMobile.phone || isMobile.tablet){
  461. $(el_item).on('click', async function (e) {
  462. // if item is disabled, do not allow any action
  463. if($(el_item).hasClass('item-disabled'))
  464. return false;
  465. if($(e.target).hasClass('item-name-editor'))
  466. return false;
  467. open_item({
  468. item: el_item,
  469. maximized: true,
  470. });
  471. });
  472. }else{
  473. $(el_item).on('dblclick', async function (e) {
  474. // if item is disabled, do not allow any action
  475. if($(el_item).hasClass('item-disabled'))
  476. return false;
  477. if($(e.target).hasClass('item-name-editor'))
  478. return false;
  479. open_item({
  480. item: el_item,
  481. new_window: e.metaKey || e.ctrlKey,
  482. });
  483. });
  484. }
  485. // --------------------------------------------------------
  486. // Mousedown
  487. // --------------------------------------------------------
  488. $(el_item).on('mousedown', function (e) {
  489. // if item is disabled, do not allow any action
  490. if($(el_item).hasClass('item-disabled'))
  491. return false;
  492. // if link badge is clicked, don't continue
  493. if($(e.target).hasClass('item-has-website-url-badge'))
  494. return false;
  495. const $el_parent_window = $(el_item).closest('.window');
  496. // first see if this is a ContextMenu call on multiple items
  497. if(e.which === 3 && $(el_item).hasClass('item-selected') && $(el_item).siblings('.item-selected').length > 0){
  498. $(".context-menu").remove();
  499. return false;
  500. }
  501. // unselect other items if neither CTRL nor Command key are held
  502. // or
  503. // if parent is not multiselectable
  504. if((!e.ctrlKey && !e.metaKey && !$(this).hasClass('item-selected')) || ($el_parent_window.length>0 && $el_parent_window.attr('data-multiselectable') !== 'true')){
  505. $(this).closest('.item-container').find('.item-selected').removeClass('item-selected');
  506. }
  507. if((e.ctrlKey || e.metaKey) && $(this).hasClass('item-selected')){
  508. $(this).removeClass('item-selected')
  509. }
  510. else{
  511. $(this).addClass('item-selected')
  512. }
  513. update_explorer_footer_selected_items_count($el_parent_window)
  514. });
  515. // --------------------------------------------------------
  516. // Click
  517. // --------------------------------------------------------
  518. $(el_item).on('click', function (e) {
  519. // if item is disabled, do not allow any action
  520. if($(el_item).hasClass('item-disabled'))
  521. return false;
  522. skip_a_rename_click = false;
  523. const $el_parent_window = $(el_item).closest('.window');
  524. // do not unselect other items if:
  525. // CTRL/Command key is pressed or clicking an item that is already selected
  526. if(!e.ctrlKey && !e.metaKey){
  527. $(this).closest('.item-container').find('.item-selected').not(this).removeClass('item-selected');
  528. update_explorer_footer_selected_items_count($el_parent_window)
  529. }
  530. //----------------------------------------------------------------
  531. // On an OpenFileDialog?
  532. //----------------------------------------------------------------
  533. if($el_parent_window.attr('data-is_openFileDialog') === 'true'){
  534. if(!options.is_dir)
  535. $el_parent_window.find('.openfiledialog-open-btn').removeClass('disabled');
  536. else
  537. $el_parent_window.find('.openfiledialog-open-btn').addClass('disabled');
  538. }
  539. //----------------------------------------------------------------
  540. // On a SaveFileDialog?
  541. //----------------------------------------------------------------
  542. if($el_parent_window.attr('data-is_saveFileDialog') === 'true' && !options.is_dir){
  543. $el_parent_window.find('.savefiledialog-filename').val($(el_item).attr('data-name'));
  544. $el_parent_window.find('.savefiledialog-save-btn').removeClass('disabled');
  545. }
  546. });
  547. $(document).on('click', function(e){
  548. if(!$(e.target).hasClass('item') && !$(e.target).hasClass('item-name') && !$(e.target).hasClass('item-icon')){
  549. skip_a_rename_click = true;
  550. }
  551. if($(e.target).parents('.item').data('id') !== item_id){
  552. skip_a_rename_click = true;
  553. }
  554. })
  555. // --------------------------------------------------------
  556. // Rename
  557. // --------------------------------------------------------
  558. function rename(){
  559. if(rename_cancelled){
  560. rename_cancelled = false;
  561. return;
  562. }
  563. const old_name = $(el_item).attr('data-name');
  564. const old_path = $(el_item).attr('data-path');
  565. const new_name = $(el_item_name_editor).val();
  566. // Don't send a rename request if:
  567. // the new name is the same as the old one,
  568. // or it's empty,
  569. // or editable was not even active at all
  570. if(old_name === new_name || !new_name || new_name === '.' || new_name === '..' || !$(el_item_name_editor).hasClass('item-name-editor-active')){
  571. if(new_name === '.'){
  572. UIAlert(`The name "." is not allowed, because it is a reserved name. Please choose another name.`);
  573. }
  574. else if(new_name === '..'){
  575. UIAlert(`The name ".." is not allowed, because it is a reserved name. Please choose another name.`)
  576. }
  577. $(el_item_name).html(truncate_filename(options.name, TRUNCATE_LENGTH).replaceAll(' ', '&nbsp;'));
  578. $(el_item_name).show();
  579. $(el_item_name_editor).val($(el_item).attr('data-name'));
  580. $(el_item_name_editor).hide();
  581. return;
  582. }
  583. // deactivate item name editable
  584. $(el_item_name_editor).removeClass('item-name-editor-active');
  585. // Perform rename request
  586. rename_file(options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url);
  587. }
  588. // --------------------------------------------------------
  589. // Rename if enter pressed on Item Name Editor
  590. // --------------------------------------------------------
  591. $(el_item_name_editor).on('keypress',function(e) {
  592. // If name editor is not active don't continue
  593. if(!$(el_item_name_editor).is(":visible"))
  594. return;
  595. // Enter key = rename
  596. if(e.which === 13) {
  597. e.stopPropagation();
  598. e.preventDefault();
  599. $(el_item_name_editor).blur();
  600. $(el_item).addClass('item-selected');
  601. last_enter_pressed_to_rename_ts = Date.now();
  602. update_explorer_footer_selected_items_count($(el_item).closest('.item-container'));
  603. return false;
  604. }
  605. })
  606. // --------------------------------------------------------
  607. // Cancel and undo if escape pressed on Item Name Editor
  608. // --------------------------------------------------------
  609. $(el_item_name_editor).on('keyup',function(e) {
  610. if(!$(el_item_name_editor).is(":visible"))
  611. return;
  612. // Escape = undo rename
  613. else if(e.which === 27){
  614. e.stopPropagation();
  615. e.preventDefault();
  616. rename_cancelled = true;
  617. $(el_item_name_editor).hide();
  618. $(el_item_name_editor).val(options.name);
  619. $(el_item_name).show();
  620. }
  621. });
  622. $(el_item_name_editor).on('focusout',function(e) {
  623. e.stopPropagation();
  624. e.preventDefault();
  625. rename();
  626. });
  627. /************************************************
  628. * Takes care of 'click to edit item name'
  629. ************************************************/
  630. let skip_a_rename_click = true;
  631. $(el_item_name).on('click', function(e){
  632. if( !skip_a_rename_click && e.which !== 3 && $(el_item_name).parent('.item-selected').length > 0){
  633. last_mousedown_ts = Date.now();
  634. setTimeout(() => {
  635. if(!skip_a_rename_click && (Date.now() - last_mousedown_ts) > 400){
  636. if (!e.ctrlKey && !e.metaKey)
  637. activate_item_name_editor(el_item)
  638. last_mousedown_ts = 0
  639. }else{
  640. last_mousedown_ts = Date.now() + 500;
  641. skip_a_rename_click= false;
  642. }
  643. }, 500);
  644. }
  645. skip_a_rename_click = false;
  646. })
  647. $(el_item_name).on('dblclick', function(e){
  648. skip_a_rename_click = true;
  649. })
  650. // --------------------------------------------------------
  651. // ContextMenu
  652. // --------------------------------------------------------
  653. $(el_item).bind("contextmenu taphold", async function (event) {
  654. // if item is disabled, do not allow any action
  655. if($(el_item).hasClass('item-disabled'))
  656. return false;
  657. // if on website link badge, don't continue
  658. if($(event.target).hasClass('item-has-website-url-badge'))
  659. return false;
  660. // dimiss taphold on regular devices
  661. if(event.type==='taphold' && !isMobile.phone && !isMobile.tablet)
  662. return;
  663. // if editing item name, preserve native context menu
  664. if(event.target === el_item_name_editor)
  665. return;
  666. // if ctrl is pressed don't open ctxmenu, ctrl is for drag and copy
  667. if(event.ctrlKey)
  668. return false;
  669. event.preventDefault();
  670. let menu_items;
  671. const $selected_items = $(el_item).closest('.item-container').find('.item-selected').not(el_item).addBack();
  672. // -------------------------------------------------------
  673. // Multiple items selected
  674. // -------------------------------------------------------
  675. if($selected_items.length > 1){
  676. const are_trashed = $selected_items.attr('data-path').startsWith(trash_path + '/');
  677. menu_items = []
  678. // -------------------------------------------
  679. // Restore
  680. // -------------------------------------------
  681. if(are_trashed){
  682. menu_items.push({
  683. html: i18n('restore'),
  684. onClick: function(){
  685. $selected_items.each(function() {
  686. const ell = this;
  687. let metadata = $(ell).attr('data-metadata') === '' ? {} : JSON.parse($(ell).attr('data-metadata'))
  688. move_items([ell], path.dirname(metadata.original_path));
  689. })
  690. }
  691. });
  692. // -------------------------------------------
  693. // -
  694. // -------------------------------------------
  695. menu_items.push('-');
  696. }
  697. if(!are_trashed){
  698. // -------------------------------------------
  699. // Donwload
  700. // -------------------------------------------
  701. menu_items.push({
  702. html: i18n('download'),
  703. onClick: async function(){
  704. let items = [];
  705. for (let index = 0; index < $selected_items.length; index++) {
  706. items.push($selected_items[index]);
  707. }
  708. zipItems(items, path.dirname($(el_item).attr('data-path')), true);
  709. }
  710. });
  711. // -------------------------------------------
  712. // Zip
  713. // -------------------------------------------
  714. menu_items.push({
  715. html: i18n('zip'),
  716. onClick: async function(){
  717. let items = [];
  718. for (let index = 0; index < $selected_items.length; index++) {
  719. items.push($selected_items[index]);
  720. }
  721. zipItems(items, path.dirname($(el_item).attr('data-path')), false);
  722. }
  723. });
  724. // -------------------------------------------
  725. // -
  726. // -------------------------------------------
  727. menu_items.push('-');
  728. }
  729. // -------------------------------------------
  730. // Cut
  731. // -------------------------------------------
  732. menu_items.push({
  733. html: i18n('cut'),
  734. onClick: function(){
  735. window.clipboard_op= 'move';
  736. window.clipboard = [];
  737. $selected_items.each(function() {
  738. const ell = this;
  739. window.clipboard.push($(ell).attr('data-path'));
  740. })
  741. }
  742. });
  743. // -------------------------------------------
  744. // Copy
  745. // -------------------------------------------
  746. if(!are_trashed){
  747. menu_items.push({
  748. html: i18n('copy'),
  749. onClick: function(){
  750. window.clipboard_op= 'copy';
  751. window.clipboard = [];
  752. $selected_items.each(function() {
  753. const ell = this;
  754. window.clipboard.push({path: $(ell).attr('data-path')});
  755. })
  756. }
  757. });
  758. }
  759. // -------------------------------------------
  760. // -
  761. // -------------------------------------------
  762. menu_items.push('-');
  763. // -------------------------------------------
  764. // Delete Permanently
  765. // -------------------------------------------
  766. if(are_trashed){
  767. menu_items.push({
  768. html: i18n('delete_permanently'),
  769. onClick: async function(){
  770. const alert_resp = await UIAlert({
  771. message: i18n('confirm_delete_multiple_items'),
  772. buttons:[
  773. {
  774. label: i18n('delete'),
  775. type: 'primary',
  776. },
  777. {
  778. label: i18n('cancel')
  779. },
  780. ]
  781. })
  782. if((alert_resp) === 'Delete'){
  783. for (let index = 0; index < $selected_items.length; index++) {
  784. const element = $selected_items[index];
  785. await delete_item(element);
  786. }
  787. const trash = await puter.fs.stat(trash_path);
  788. // update other clients
  789. if(window.socket){
  790. window.socket.emit('trash.is_empty', {is_empty: trash.is_empty});
  791. }
  792. if(trash.is_empty){
  793. $(`.item[data-path="${html_encode(trash_path)}" i], .item[data-shortcut_to_path="${trash_path}" i]`).find('.item-icon > img').attr('src', window.icons['trash.svg']);
  794. $(`.window[data-path="${html_encode(trash_path)}"]`).find('.window-head-icon').attr('src', window.icons['trash.svg']);
  795. }
  796. }
  797. }
  798. });
  799. }
  800. // -------------------------------------------
  801. // Create Shortcut
  802. // -------------------------------------------
  803. if(!are_trashed && window.feature_flags.create_shortcut){
  804. menu_items.push({
  805. html: i18n('create_shortcut'),
  806. onClick: async function(){
  807. $selected_items.each(function() {
  808. let base_dir = path.dirname($(this).attr('data-path'));
  809. // Trash on Desktop is a special case
  810. if($(this).attr('data-path') && $(this).closest('.item-container').attr('data-path') === window.desktop_path){
  811. base_dir = window.desktop_path;
  812. }
  813. // create shortcut
  814. create_shortcut(
  815. path.basename($(this).attr('data-path')),
  816. $(this).attr('data-is_dir') === '1',
  817. base_dir,
  818. $(this).closest('.item-container'),
  819. $(this).attr('data-shortcut_to') === '' ? $(this).attr('data-uid') : $(this).attr('data-shortcut_to'),
  820. $(this).attr('data-shortcut_to_path') === '' ? $(this).attr('data-path') : $(this).attr('data-shortcut_to_path'),
  821. );
  822. })
  823. }
  824. });
  825. }
  826. // -------------------------------------------
  827. // Delete
  828. // -------------------------------------------
  829. if(!are_trashed){
  830. menu_items.push({
  831. html: i18n('delete'),
  832. onClick: async function(){
  833. move_items($selected_items, trash_path);
  834. }
  835. });
  836. }
  837. }
  838. // -------------------------------------------------------
  839. // One item selected
  840. // -------------------------------------------------------
  841. else{
  842. const is_trash = $(el_item).attr('data-path') === trash_path || $(el_item).attr('data-shortcut_to_path') === trash_path;
  843. menu_items = [];
  844. // -------------------------------------------
  845. // Open
  846. // -------------------------------------------
  847. if(!is_trashed){
  848. menu_items.push({
  849. html: i18n('open'),
  850. onClick: function(){
  851. open_item({item: el_item});
  852. }
  853. });
  854. // -------------------------------------------
  855. // -
  856. // -------------------------------------------
  857. if(options.associated_app_name || is_trash)
  858. menu_items.push('-');
  859. }
  860. // -------------------------------------------
  861. // Open With
  862. // -------------------------------------------
  863. if(!is_trashed && !is_trash && (options.associated_app_name === null || options.associated_app_name === undefined)){
  864. let items = [];
  865. if(!options.suggested_apps || options.suggested_apps.length === 0){
  866. // try to find suitable apps
  867. const suitable_apps = await suggest_apps_for_fsentry({
  868. uid: options.uid,
  869. path: options.path,
  870. });
  871. if(suitable_apps && suitable_apps.length > 0){
  872. options.suggested_apps = suitable_apps;
  873. }
  874. }
  875. if(options.suggested_apps && options.suggested_apps.length > 0){
  876. for (let index = 0; index < options.suggested_apps.length; index++) {
  877. const suggested_app = options.suggested_apps[index];
  878. if ( ! suggested_app ) {
  879. console.warn(`suggested_app is null`, options.suggested_apps, index);
  880. continue;
  881. }
  882. items.push({
  883. html: suggested_app.title,
  884. icon: `<img src="${html_encode(suggested_app.icon ?? window.icons['app.svg'])}" style="width:16px; height: 16px; margin-bottom: -4px;">`,
  885. onClick: async function(){
  886. var extension = path.extname($(el_item).attr('data-path')).toLowerCase();
  887. if(
  888. user_preferences[`default_apps${extension}`] !== suggested_app.name
  889. &&
  890. (
  891. (!user_preferences[`default_apps${extension}`] && index > 0)
  892. ||
  893. (user_preferences[`default_apps${extension}`])
  894. )
  895. ){
  896. const alert_resp = await UIAlert({
  897. message: `${i18n('change_allways_open_with')} ` + html_encode(suggested_app.title) + '?',
  898. body_icon: suggested_app.icon,
  899. buttons:[
  900. {
  901. label: i18n('yes'),
  902. type: 'primary',
  903. value: 'yes'
  904. },
  905. {
  906. label: i18n('no')
  907. },
  908. ]
  909. })
  910. if((alert_resp) === 'yes'){
  911. user_preferences['default_apps' + extension] = suggested_app.name;
  912. window.mutate_user_preferences(user_preferences);
  913. }
  914. }
  915. launch_app({
  916. name: suggested_app.name,
  917. file_path: $(el_item).attr('data-path'),
  918. window_title: $(el_item).attr('data-name'),
  919. file_uid: $(el_item).attr('data-uid'),
  920. });
  921. }
  922. })
  923. }
  924. }else{
  925. items.push({
  926. html: 'No suitable apps found',
  927. disabled: true,
  928. });
  929. }
  930. // add all suitable apps
  931. menu_items.push({
  932. html: i18n('open_with'),
  933. items: items,
  934. });
  935. // -------------------------------------------
  936. // -- separator --
  937. // -------------------------------------------
  938. menu_items.push('-');
  939. }
  940. // -------------------------------------------
  941. // Open in New Window
  942. // (only if the item is on a window)
  943. // -------------------------------------------
  944. if($(el_item).closest('.window-body').length > 0 && options.is_dir){
  945. menu_items.push({
  946. html: i18n('open_in_new_window'),
  947. onClick: function(){
  948. if(options.is_dir){
  949. open_item({item: el_item, new_window: true})
  950. }
  951. }
  952. });
  953. // -------------------------------------------
  954. // -- separator --
  955. // -------------------------------------------
  956. if(!is_trash && !is_trashed && options.is_dir)
  957. menu_items.push('-');
  958. }
  959. // -------------------------------------------
  960. // Publish As Website
  961. // -------------------------------------------
  962. if(!is_trashed && !is_trash && options.is_dir){
  963. menu_items.push({
  964. html: i18n('publish_as_website'),
  965. disabled: !options.is_dir,
  966. onClick: async function () {
  967. if(window.require_email_verification_to_publish_website){
  968. if(window.user.is_temp &&
  969. !await UIWindowSaveAccount({
  970. send_confirmation_code: true,
  971. message: 'Please create an account to proceed.',
  972. window_options: {
  973. backdrop: true,
  974. close_on_backdrop_click: false,
  975. }
  976. }))
  977. return;
  978. else if(!window.user.email_confirmed && !await UIWindowEmailConfirmationRequired())
  979. return;
  980. }
  981. UIWindowPublishWebsite(options.uid, $(el_item).attr('data-name'), $(el_item).attr('data-path'));
  982. }
  983. });
  984. }
  985. // -------------------------------------------
  986. // Deploy As App
  987. // -------------------------------------------
  988. if(!is_trashed && !is_trash && options.is_dir){
  989. menu_items.push({
  990. html: i18n('deploy_as_app'),
  991. disabled: !options.is_dir,
  992. onClick: async function () {
  993. launch_app({
  994. name: 'dev-center',
  995. file_path: $(el_item).attr('data-path'),
  996. file_uid: $(el_item).attr('data-uid'),
  997. params: {
  998. source_path: options.path,
  999. }
  1000. })
  1001. }
  1002. });
  1003. menu_items.push('-');
  1004. }
  1005. // -------------------------------------------
  1006. // Empty Trash
  1007. // -------------------------------------------
  1008. if(is_trash){
  1009. menu_items.push({
  1010. html: i18n('empty_trash'),
  1011. onClick: async function(){
  1012. empty_trash();
  1013. }
  1014. });
  1015. }
  1016. // -------------------------------------------
  1017. // Download
  1018. // -------------------------------------------
  1019. if(!is_trash && !is_trashed && (options.associated_app_name === null || options.associated_app_name === undefined)){
  1020. menu_items.push({
  1021. html: i18n('download'),
  1022. disabled: options.is_dir && !window.feature_flags.download_directory,
  1023. onClick: async function(){
  1024. if(options.is_dir)
  1025. zipItems(el_item, path.dirname($(el_item).attr('data-path')), true);
  1026. else
  1027. trigger_download([options.path]);
  1028. }
  1029. });
  1030. }
  1031. // -------------------------------------------
  1032. // Get Copy Link
  1033. // -------------------------------------------
  1034. if(!is_trashed && !is_trash && (options.associated_app_name === null || options.associated_app_name === undefined)){
  1035. menu_items.push({
  1036. html: i18n('get_copy_link'),
  1037. onClick: async function(){
  1038. if(window.user.is_temp &&
  1039. !await UIWindowSaveAccount({
  1040. message: i18n('save_account_to_get_copy_link'),
  1041. send_confirmation_code: true,
  1042. window_options: {
  1043. backdrop: true,
  1044. close_on_backdrop_click: false,
  1045. }
  1046. }))
  1047. return;
  1048. else if(!window.user.email_confirmed && !await UIWindowEmailConfirmationRequired())
  1049. return;
  1050. UIWindowGetCopyLink({
  1051. name: $(el_item).attr('data-name'),
  1052. uid: $(el_item).attr('data-uid'),
  1053. path: $(el_item).attr('data-path'),
  1054. is_dir: options.is_dir,
  1055. });
  1056. }
  1057. });
  1058. }
  1059. // -------------------------------------------
  1060. // Zip
  1061. // -------------------------------------------
  1062. if(!is_trash && !is_trashed && !$(el_item).attr('data-path').endsWith('.zip')){
  1063. menu_items.push({
  1064. html: i18n('zip'),
  1065. onClick: function(){
  1066. zipItems(el_item, path.dirname($(el_item).attr('data-path')), false);
  1067. }
  1068. })
  1069. }
  1070. // -------------------------------------------
  1071. // Unzip
  1072. // -------------------------------------------
  1073. if(!is_trash && !is_trashed && $(el_item).attr('data-path').endsWith('.zip')){
  1074. menu_items.push({
  1075. html: i18n('unzip'),
  1076. onClick: async function(){
  1077. const zip = new JSZip();
  1078. let filPath = $(el_item).attr('data-path');
  1079. let file = puter.fs.read($(el_item).attr('data-path'));
  1080. zip.loadAsync(file).then(async function (zip) {
  1081. const rootdir = await puter.fs.mkdir(path.dirname(filPath) + '/' + path.basename(filPath, '.zip'), {dedupeName: true});
  1082. Object.keys(zip.files).forEach(async function (filename) {
  1083. if(filename.endsWith('/'))
  1084. await puter.fs.mkdir(rootdir.path +'/' + filename, {createMissingParents: true});
  1085. zip.files[filename].async('blob').then(async function (fileData) {
  1086. await puter.fs.write(rootdir.path +'/' + filename, fileData);
  1087. }).catch(function (e) {
  1088. // UIAlert(e.message);
  1089. })
  1090. })
  1091. }).catch(function (e) {
  1092. // UIAlert(e.message);
  1093. })
  1094. }
  1095. })
  1096. }
  1097. // -------------------------------------------
  1098. // Restore
  1099. // -------------------------------------------
  1100. if(is_trashed){
  1101. menu_items.push({
  1102. html: i18n('restore'),
  1103. onClick: async function(){
  1104. let metadata = $(el_item).attr('data-metadata') === '' ? {} : JSON.parse($(el_item).attr('data-metadata'))
  1105. move_items([el_item], path.dirname(metadata.original_path));
  1106. }
  1107. });
  1108. }
  1109. // -------------------------------------------
  1110. // -
  1111. // -------------------------------------------
  1112. if(!is_trash && (options.associated_app_name === null || options.associated_app_name === undefined))
  1113. menu_items.push('-');
  1114. // -------------------------------------------
  1115. // Cut
  1116. // -------------------------------------------
  1117. if($(el_item).attr('data-immutable') === '0'){
  1118. menu_items.push({
  1119. html: i18n('cut'),
  1120. onClick: function(){
  1121. window.clipboard_op= 'move';
  1122. window.clipboard= [options.path];
  1123. }
  1124. });
  1125. }
  1126. // -------------------------------------------
  1127. // Copy
  1128. // -------------------------------------------
  1129. if(!is_trashed && !is_trash){
  1130. menu_items.push({
  1131. html: i18n('copy'),
  1132. onClick: function(){
  1133. window.clipboard_op= 'copy';
  1134. window.clipboard= [{path: options.path}];
  1135. }
  1136. });
  1137. }
  1138. // -------------------------------------------
  1139. // Paste Into Folder
  1140. // -------------------------------------------
  1141. if($(el_item).attr('data-is_dir') === '1' && !is_trashed && !is_trash){
  1142. menu_items.push({
  1143. html: i18n('paste_into_folder'),
  1144. disabled: clipboard.length > 0 ? false : true,
  1145. onClick: function(){
  1146. if(clipboard_op === 'copy')
  1147. copy_clipboard_items($(el_item).attr('data-path'), null);
  1148. else if(clipboard_op === 'move')
  1149. move_clipboard_items(null, $(el_item).attr('data-path'))
  1150. }
  1151. })
  1152. }
  1153. // -------------------------------------------
  1154. // -
  1155. // -------------------------------------------
  1156. if($(el_item).attr('data-immutable') === '0' && !is_trash){
  1157. menu_items.push('-')
  1158. }
  1159. // -------------------------------------------
  1160. // Create Shortcut
  1161. // -------------------------------------------
  1162. if(!is_trashed && window.feature_flags.create_shortcut){
  1163. menu_items.push({
  1164. html: i18n('create_shortcut'),
  1165. onClick: async function(){
  1166. let base_dir = path.dirname($(el_item).attr('data-path'));
  1167. // Trash on Desktop is a special case
  1168. if($(el_item).attr('data-path') && $(el_item).closest('.item-container').attr('data-path') === window.desktop_path){
  1169. base_dir = window.desktop_path;
  1170. }
  1171. create_shortcut(
  1172. path.basename($(el_item).attr('data-path')),
  1173. options.is_dir,
  1174. base_dir,
  1175. options.appendTo,
  1176. options.shortcut_to === '' ? options.uid : options.shortcut_to,
  1177. options.shortcut_to_path === '' ? options.path : options.shortcut_to_path,
  1178. );
  1179. }
  1180. });
  1181. }
  1182. // -------------------------------------------
  1183. // Delete
  1184. // -------------------------------------------
  1185. if($(el_item).attr('data-immutable') === '0' && !is_trashed){
  1186. menu_items.push({
  1187. html: i18n('delete'),
  1188. onClick: async function(){
  1189. move_items([el_item], trash_path);
  1190. }
  1191. });
  1192. }
  1193. // -------------------------------------------
  1194. // Delete Permanently
  1195. // -------------------------------------------
  1196. if(is_trashed){
  1197. menu_items.push({
  1198. html: i18n('delete_permanently'),
  1199. onClick: async function(){
  1200. const alert_resp = await UIAlert({
  1201. message: i18n('confirm_delete_single_item'),
  1202. buttons:[
  1203. {
  1204. label: i18n('delete'),
  1205. type: 'primary',
  1206. },
  1207. {
  1208. label: i18n('cancel')
  1209. },
  1210. ]
  1211. })
  1212. if((alert_resp) === 'Delete'){
  1213. await delete_item(el_item);
  1214. // check if trash is empty
  1215. const trash = await puter.fs.stat(trash_path);
  1216. // update other clients
  1217. if(window.socket){
  1218. window.socket.emit('trash.is_empty', {is_empty: trash.is_empty});
  1219. }
  1220. // update this client
  1221. if(trash.is_empty){
  1222. $(`.item[data-path="${html_encode(trash_path)}" i], .item[data-shortcut_to_path="${html_encode(trash_path)}" i]`).find('.item-icon > img').attr('src', window.icons['trash.svg']);
  1223. $(`.window[data-path="${trash_path}"]`).find('.window-head-icon').attr('src', window.icons['trash.svg']);
  1224. }
  1225. }
  1226. }
  1227. });
  1228. }
  1229. // -------------------------------------------
  1230. // Rename
  1231. // -------------------------------------------
  1232. if($(el_item).attr('data-immutable') === '0' && !is_trashed && !is_trash){
  1233. menu_items.push({
  1234. html: i18n('rename'),
  1235. onClick: function(){
  1236. activate_item_name_editor(el_item)
  1237. }
  1238. });
  1239. }
  1240. // -------------------------------------------
  1241. // -
  1242. // -------------------------------------------
  1243. menu_items.push('-');
  1244. // -------------------------------------------
  1245. // Properties
  1246. // -------------------------------------------
  1247. menu_items.push({
  1248. html: i18n('properties'),
  1249. onClick: function(){
  1250. let window_height = 500;
  1251. let window_width = 450;
  1252. let left = $(el_item).position().left + $(el_item).width();
  1253. left = left > (window.innerWidth - window_width)? (window.innerWidth - window_width) : left;
  1254. let top = $(el_item).position().top + $(el_item).height();
  1255. top = top > (window.innerHeight - (window_height + window.taskbar_height + window.toolbar_height))? (window.innerHeight - (window_height + window.taskbar_height + window.toolbar_height)) : top;
  1256. UIWindowItemProperties(
  1257. $(el_item).attr('data-name'),
  1258. $(el_item).attr('data-path'),
  1259. $(el_item).attr('data-uid'),
  1260. left,
  1261. top,
  1262. window_width,
  1263. window_height,
  1264. );
  1265. }
  1266. });
  1267. }
  1268. // Create ContextMenu
  1269. UIContextMenu({
  1270. parent_element: ($(options.appendTo).hasClass('desktop') ? undefined : options.appendTo),
  1271. items: menu_items
  1272. });
  1273. return false
  1274. })
  1275. // --------------------------------------------------------
  1276. // Resize Item Name Editor on every keystroke
  1277. // --------------------------------------------------------
  1278. $(el_item_name_editor).on('input keypress focus', function(){
  1279. const val = $(el_item_name_editor).val();
  1280. $('.item-name-shadow').html(html_encode(val).replaceAll(' ', '&nbsp;'));
  1281. if(val !== ''){
  1282. const w = $('.item-name-shadow').width();
  1283. const h = $('.item-name-shadow').height();
  1284. $(el_item_name_editor).width(w + 4)
  1285. $(el_item_name_editor).height(h + 2)
  1286. }
  1287. })
  1288. if(options.sort_container_after_append){
  1289. sort_items(options.appendTo, $(el_item).closest('.item-container').attr('data-sort_by'), $(el_item).closest('.item-container').attr('data-sort_order'));
  1290. }
  1291. if(options.editable){
  1292. activate_item_name_editor(el_item)
  1293. }
  1294. }
  1295. // Create item-name-shadow
  1296. // This element has the exact styling as item name editor and allows us
  1297. // to measure the width and height of the item name editor and automatically
  1298. // resize it to fit the text.
  1299. $('body').append(`<span class="item-name-shadow"></span>`);
  1300. $(document).on('click', '.item-has-website-url-badge', async function(e){
  1301. e.stopPropagation();
  1302. e.preventDefault();
  1303. const website_url = $(this).closest('.item').attr('data-website_url');
  1304. if(website_url){
  1305. window.open(website_url, '_blank');
  1306. }
  1307. return false;
  1308. })
  1309. $(document).on('mousedown', '.item-has-website-url-badge', async function(e){
  1310. console.log('mousedown')
  1311. e.stopPropagation();
  1312. e.preventDefault();
  1313. return false;
  1314. })
  1315. $(document).on('contextmenu', '.item-has-website-url-badge', async function(e){
  1316. e.stopPropagation();
  1317. e.preventDefault();
  1318. // close other context menus
  1319. const $ctxmenus = $(".context-menu");
  1320. $ctxmenus.fadeOut(200, function(){
  1321. $ctxmenus.remove();
  1322. });
  1323. UIContextMenu({
  1324. parent_element: this,
  1325. items: [
  1326. // Open
  1327. {
  1328. html: `${i18n('open_in_new_tab')} <img src="${window.icons['launch.svg']}" style="width:10px; height:10px; margin-left: 5px;">` ,
  1329. html_active: `${i18n('open_in_new_tab')} <img src="${window.icons['launch-white.svg']}" style="width:10px; height:10px; margin-left: 5px;">` ,
  1330. onClick: function(){
  1331. const website_url = $(e.target).closest('.item').attr('data-website_url');
  1332. if(website_url){
  1333. window.open(website_url, '_blank');
  1334. }
  1335. }
  1336. },
  1337. // Copy Link
  1338. {
  1339. html: i18n('copy_link'),
  1340. onClick: async function(){
  1341. const website_url = $(e.target).closest('.item').attr('data-website_url');
  1342. if(website_url){
  1343. await copy_to_clipboard(website_url);
  1344. }
  1345. }
  1346. },
  1347. ]
  1348. });
  1349. return false;
  1350. })
  1351. $(document).on('click', '.item-has-website-badge', async function(e){
  1352. puter.fs.stat({
  1353. uid: $(this).closest('.item').attr('data-uid'),
  1354. returnSubdomains: true,
  1355. returnPermissions: false,
  1356. returnVersions: false,
  1357. success: function (fsentry){
  1358. if(fsentry.subdomains)
  1359. window.open(fsentry.subdomains[0].address, '_blank');
  1360. }
  1361. })
  1362. })
  1363. $(document).on('long-hover', '.item-has-website-badge', function(e){
  1364. puter.fs.stat({
  1365. uid: $(this).closest('.item').attr('data-uid'),
  1366. returnSubdomains: true,
  1367. returnPermissions: false,
  1368. returnVersions: false,
  1369. success: function (fsentry){
  1370. var box = e.target.getBoundingClientRect();
  1371. var body = document.body;
  1372. var docEl = document.documentElement;
  1373. var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
  1374. var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
  1375. var clientTop = docEl.clientTop || body.clientTop || 0;
  1376. var clientLeft = docEl.clientLeft || body.clientLeft || 0;
  1377. var top = box.top + scrollTop - clientTop;
  1378. var left = box.left + scrollLeft - clientLeft;
  1379. if(fsentry.subdomains){
  1380. let h = `<div class="allow-user-select website-badge-popover-content">`;
  1381. h += `<div class="website-badge-popover-title">Associated website${ fsentry.subdomains.length > 1 ? 's':''}</div>`;
  1382. fsentry.subdomains.forEach(subdomain => {
  1383. h += `
  1384. <a class="website-badge-popover-link" href="${subdomain.address}" style="font-size:13px;" target="_blank">${subdomain.address.replace('https://', '')}</a>
  1385. <br>`;
  1386. });
  1387. h += `</div>`;
  1388. // close other website popovers
  1389. $('.website-badge-popover-content').closest('.popover').remove();
  1390. // show a UIPopover with the website
  1391. UIPopover({
  1392. target: e.target,
  1393. content:h,
  1394. snapToElement: e.target,
  1395. parent_element: e.target,
  1396. top: top - 30,
  1397. left: left + 20,
  1398. })
  1399. }
  1400. }
  1401. })
  1402. })
  1403. $(document).on('click', '.website-badge-popover-link', function(e){
  1404. // remove the parent popover
  1405. $(e.target).closest('.popover').remove();
  1406. })
  1407. // removes item(s)
  1408. $.fn.removeItems = async function(options) {
  1409. options = options || {};
  1410. $(this).each(async function() {
  1411. const parent_container = $(this).closest('.item-container');
  1412. $(this).remove();
  1413. show_or_hide_empty_folder_message(parent_container);
  1414. });
  1415. return this;
  1416. }
  1417. window.activate_item_name_editor= function(el_item){
  1418. // files in trash cannot be renamed, the user should be notified with an Alert.
  1419. if($(el_item).attr('data-immutable') !== '0'){
  1420. return;
  1421. }
  1422. // files in trash cannot be renamed, user should be notified with an Alert.
  1423. else if(path.dirname($(el_item).attr('data-path')) === window.trash_path){
  1424. UIAlert(i18n('items_in_trash_cannot_be_renamed'));
  1425. return;
  1426. }
  1427. const el_item_name = $(el_item).find('.item-name');
  1428. const el_item_name_editor = $(el_item).find('.item-name-editor').get(0);
  1429. $(el_item_name).hide();
  1430. $(el_item_name_editor).show();
  1431. $(el_item_name_editor).focus();
  1432. $(el_item_name_editor).addClass('item-name-editor-active');
  1433. // select all text before extension
  1434. const item_name = $(el_item).attr('data-name');
  1435. const is_dir = parseInt($(el_item).attr('data-is_dir'));
  1436. const extname = path.extname('/'+item_name);
  1437. if(extname !== '' && !is_dir)
  1438. el_item_name_editor.setSelectionRange(0, item_name.length - extname.length)
  1439. else
  1440. $(el_item_name_editor).select();
  1441. }
  1442. export default UIItem;